Skip to content

Commit 9cbb666

Browse files
author
Dobromir Hristov
committed
feat: introduce Card component
1 parent bd518f5 commit 9cbb666

File tree

8 files changed

+738
-0
lines changed

8 files changed

+738
-0
lines changed

src/components/Card.vue

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

0 commit comments

Comments
 (0)