Skip to content

Commit 437098f

Browse files
author
Dobromir Hristov
authored
Add new Card component (#418)
closes rdar://97714707
1 parent b6da958 commit 437098f

File tree

10 files changed

+732
-3
lines changed

10 files changed

+732
-3
lines changed

src/components/Card.vue

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
<!--
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2022 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
-->
10+
11+
<template>
12+
<Reference
13+
class="card"
14+
:url="url"
15+
:class="classes"
16+
v-bind="linkAriaTags"
17+
>
18+
<CardCover
19+
:variants="imageVariants"
20+
:rounded="floatingStyle"
21+
aria-hidden="true"
22+
#default="coverProps"
23+
>
24+
<slot name="cover" v-bind="coverProps" />
25+
</CardCover>
26+
<div class="details" aria-hidden="true">
27+
<div
28+
v-if="eyebrow"
29+
:id="eyebrowId"
30+
class="eyebrow"
31+
:aria-label="formatAriaLabel(`- ${eyebrow}`)"
32+
>
33+
{{ eyebrow }}
34+
</div>
35+
<div
36+
:id="titleId"
37+
class="title"
38+
>
39+
{{ title }}
40+
</div>
41+
<div v-if="$slots.default" class="card-content" :id="contentId">
42+
<slot />
43+
</div>
44+
<component
45+
v-if="linkText"
46+
:is="hasButton ? 'ButtonLink': 'div'"
47+
class="link"
48+
>
49+
{{ linkText }}
50+
<DiagonalArrowIcon class="icon-inline link-icon" v-if="showExternalLinks" />
51+
<InlineChevronRightIcon class="icon-inline link-icon" v-else-if="!hasButton" />
52+
</component>
53+
</div>
54+
</Reference>
55+
</template>
56+
57+
<script>
58+
import ButtonLink from 'docc-render/components/ButtonLink.vue';
59+
import InlineChevronRightIcon from 'theme/components/Icons/InlineChevronRightIcon.vue';
60+
import DiagonalArrowIcon from 'theme/components/Icons/DiagonalArrowIcon.vue';
61+
import Reference from 'docc-render/components/ContentNode/Reference.vue';
62+
import CardSize from 'docc-render/constants/CardSize';
63+
import CardCover from './CardCover.vue';
64+
65+
export default {
66+
name: 'Card',
67+
components: {
68+
Reference,
69+
DiagonalArrowIcon,
70+
InlineChevronRightIcon,
71+
CardCover,
72+
ButtonLink,
73+
},
74+
constants: {
75+
CardSize,
76+
},
77+
inject: {
78+
references: { default: () => ({}) },
79+
},
80+
computed: {
81+
titleId: ({ _uid }) => `card_title_${_uid}`,
82+
contentId: ({ _uid }) => `card_content_${_uid}`,
83+
eyebrowId: ({ _uid }) => `card_eyebrow_${_uid}`,
84+
linkAriaTags: ({
85+
titleId, eyebrowId, contentId, eyebrow, $slots,
86+
}) => ({
87+
'aria-labelledby': titleId.concat(eyebrow ? ` ${eyebrowId}` : ''),
88+
'aria-describedby': $slots.default ? `${contentId}` : null,
89+
}),
90+
classes: ({
91+
size,
92+
floatingStyle,
93+
}) => ([
94+
size,
95+
{
96+
'floating-style': floatingStyle,
97+
},
98+
]),
99+
imageReference: ({
100+
image,
101+
references,
102+
}) => (references[image] || {}),
103+
imageVariants: ({ imageReference }) => imageReference.variants || [],
104+
},
105+
props: {
106+
linkText: {
107+
type: String,
108+
required: false,
109+
},
110+
url: {
111+
type: String,
112+
required: false,
113+
default: '',
114+
},
115+
eyebrow: {
116+
type: String,
117+
required: false,
118+
},
119+
image: {
120+
type: String,
121+
required: false,
122+
},
123+
size: {
124+
type: String,
125+
validator: s => Object.prototype.hasOwnProperty.call(CardSize, s),
126+
},
127+
title: {
128+
type: String,
129+
required: true,
130+
},
131+
hasButton: {
132+
type: Boolean,
133+
default: () => false,
134+
},
135+
floatingStyle: {
136+
type: Boolean,
137+
default: false,
138+
},
139+
showExternalLinks: {
140+
type: Boolean,
141+
default: false,
142+
},
143+
formatAriaLabel: {
144+
type: Function,
145+
default: v => v,
146+
},
147+
},
148+
};
149+
</script>
150+
151+
<style scoped lang="scss">
152+
@import 'docc-render/styles/_core.scss';
153+
154+
$details-padding: 17px;
155+
$content-margin: 4px;
156+
157+
@mixin static-card-size($card-height, $img-height) {
158+
@include inTargetWeb {
159+
--card-height: #{$card-height};
160+
--card-details-height: #{$card-height - $img-height - ($details-padding * 2)};
161+
}
162+
--card-cover-height: #{$img-height};
163+
}
164+
165+
.card {
166+
overflow: hidden;
167+
display: block;
168+
transition: box-shadow, transform 160ms ease-out;
169+
will-change: box-shadow, transform;
170+
backface-visibility: hidden;
171+
height: var(--card-height);
172+
173+
&:hover {
174+
text-decoration: none;
175+
176+
.link {
177+
text-decoration: underline;
178+
}
179+
}
180+
181+
@include inTargetWeb {
182+
border-radius: $big-border-radius;
183+
184+
&:hover {
185+
box-shadow: 0 5px 10px var(--color-card-shadow);
186+
transform: scale(1.007);
187+
188+
@media (prefers-reduced-motion: reduce) {
189+
box-shadow: none;
190+
transform: none;
191+
}
192+
}
193+
}
194+
195+
&.small {
196+
@include static-card-size(408px, 235px);
197+
@include breakpoint(medium) {
198+
@include static-card-size(341px, 163px);
199+
}
200+
}
201+
202+
&.large {
203+
@include static-card-size(556px, 359px);
204+
@include breakpoint(medium) {
205+
@include static-card-size(420px, 249px);
206+
}
207+
}
208+
209+
&.floating-style {
210+
--color-card-shadow: transparent;
211+
--card-height: auto;
212+
--card-details-height: auto;
213+
}
214+
}
215+
216+
.details {
217+
background-color: var(--color-card-background);
218+
padding: $details-padding;
219+
position: relative;
220+
height: var(--card-details-height);
221+
@include font-styles(card-content-small);
222+
223+
.large & {
224+
@include font-styles(card-content-large);
225+
}
226+
227+
.floating-style & {
228+
--color-card-background: transparent;
229+
padding: $details-padding 0;
230+
}
231+
}
232+
233+
.eyebrow {
234+
color: var(--color-card-eyebrow);
235+
display: block;
236+
margin-bottom: $content-margin;
237+
@include font-styles(card-eyebrow-small);
238+
239+
.large & {
240+
@include font-styles(card-eyebrow-large);
241+
}
242+
}
243+
244+
.title {
245+
font-weight: $font-weight-semibold;
246+
color: var(--color-card-content-text);
247+
@include font-styles(card-title-small);
248+
249+
.large & {
250+
@include font-styles(card-title-large);
251+
}
252+
}
253+
254+
.card-content {
255+
color: var(--color-card-content-text);
256+
margin-top: $content-margin;
257+
}
258+
259+
.link {
260+
bottom: 17px;
261+
display: flex;
262+
align-items: center;
263+
position: absolute;
264+
265+
.link-icon {
266+
height: 0.6em;
267+
width: 0.6em;
268+
// move the icon closer
269+
margin-left: .3em;
270+
}
271+
272+
.floating-style & {
273+
bottom: unset;
274+
margin-top: $stacked-margin-large;
275+
position: relative;
276+
}
277+
}
278+
279+
@include breakpoint(small) {
280+
.card {
281+
margin-left: auto;
282+
margin-right: auto;
283+
284+
& + & {
285+
margin-bottom: 20px;
286+
margin-top: 20px;
287+
}
288+
289+
&.small, &.large {
290+
--card-height: auto;
291+
--card-details-height: auto;
292+
293+
@include inTargetWeb {
294+
min-width: 280px;
295+
max-width: 300px;
296+
--card-cover-height: 227px;
297+
}
298+
@include inTargetIde {
299+
--card-cover-height: 325px;
300+
}
301+
302+
.link {
303+
bottom: unset;
304+
margin-top: 7px;
305+
position: relative;
306+
}
307+
}
308+
}
309+
}
310+
</style>

src/components/CardCover.vue

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<!--
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2022 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
-->
10+
11+
<template>
12+
<div class="card-cover-wrap" :class="{ rounded }">
13+
<slot classes="card-cover">
14+
<ImageAsset :variants="variants" class="card-cover" />
15+
</slot>
16+
</div>
17+
</template>
18+
19+
<script>
20+
import ImageAsset from 'docc-render/components/ImageAsset.vue';
21+
22+
export default {
23+
name: 'CardCover',
24+
components: { ImageAsset },
25+
props: {
26+
variants: {
27+
type: Array,
28+
required: true,
29+
},
30+
rounded: {
31+
type: Boolean,
32+
default: false,
33+
},
34+
},
35+
};
36+
</script>
37+
<style lang="scss" scoped>
38+
@import 'docc-render/styles/_core.scss';
39+
40+
.card-cover-wrap {
41+
&.rounded {
42+
border-radius: $big-border-radius;
43+
overflow: hidden;
44+
}
45+
}
46+
47+
.card-cover {
48+
background-color: var(--color-card-background);
49+
display: block;
50+
// default height for a card, if no size is specified
51+
height: var(--card-cover-height, 180px);
52+
53+
/deep/ img {
54+
width: 100%;
55+
height: 100%;
56+
object-fit: cover;
57+
object-position: center;
58+
display: block;
59+
}
60+
}
61+
</style>

src/components/Tutorial/Hero.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ import LinkableElement from 'docc-render/components/LinkableElement.vue';
7373
7474
import GenericModal from 'docc-render/components/GenericModal.vue';
7575
import PlayIcon from 'theme/components/Icons/PlayIcon.vue';
76-
import { normalizeAssetUrl } from 'docc-render/utils/assets';
76+
import { normalizeAssetUrl, toCSSUrl } from 'docc-render/utils/assets';
7777
import HeroMetadata from './HeroMetadata.vue';
7878
7979
export default {
@@ -140,14 +140,14 @@ export default {
140140
variant.traits.includes('light')
141141
));
142142
143-
return lightVariant ? normalizeAssetUrl(lightVariant.url) : '';
143+
return (lightVariant || {}).url;
144144
},
145145
projectFilesUrl() {
146146
return this.projectFiles ? normalizeAssetUrl(this.references[this.projectFiles].url) : null;
147147
},
148148
bgStyle() {
149149
return {
150-
backgroundImage: `url('${this.backgroundImageUrl}')`,
150+
backgroundImage: toCSSUrl(this.backgroundImageUrl),
151151
};
152152
},
153153
xcodeRequirementData() {

0 commit comments

Comments
 (0)