@conduction/docusaurus-preset 3.15.3 → 3.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/ContentCard/ContentCard.jsx +88 -29
- package/src/components/ContentCard/ContentCard.module.css +101 -4
- package/src/components/DownloadPanel/DownloadPanel.jsx +69 -0
- package/src/components/DownloadPanel/DownloadPanel.module.css +130 -0
- package/src/components/FeaturedCard/FeaturedCard.jsx +36 -0
- package/src/components/FeaturedCard/FeaturedCard.module.css +15 -0
- package/src/components/ModuleCard/ModuleCard.jsx +150 -0
- package/src/components/ModuleCard/ModuleCard.module.css +147 -0
- package/src/components/ModulePage/ModulePage.jsx +149 -0
- package/src/components/ModulePage/ModulePage.module.css +89 -0
- package/src/components/PlatformOverview/PlatformOverview.module.css +8 -2
- package/src/components/index.js +18 -0
- package/src/components/primitives/AuthorByline.module.css +4 -3
- package/src/data/audience.js +60 -0
- package/src/theme/ErrorPageContent/index.jsx +218 -0
- package/src/theme/ErrorPageContent/styles.module.css +203 -0
package/package.json
CHANGED
|
@@ -38,14 +38,41 @@
|
|
|
38
38
|
|
|
39
39
|
import React from 'react';
|
|
40
40
|
import HexThumbnail from '../primitives/HexThumbnail';
|
|
41
|
-
import AuthorByline from '../primitives/AuthorByline';
|
|
42
41
|
import Pill from '../primitives/Pill';
|
|
43
|
-
import {
|
|
44
|
-
|
|
45
|
-
CONTENT_TYPE_BULLET_COLOR,
|
|
46
|
-
} from '../ContentTypeFilter/contentTypes';
|
|
42
|
+
import {CONTENT_TYPE_LABELS} from '../ContentTypeFilter/contentTypes';
|
|
43
|
+
import {AUDIENCE_SHORT_LABELS} from '../../data/audience';
|
|
47
44
|
import styles from './ContentCard.module.css';
|
|
48
45
|
|
|
46
|
+
function readOrWatch(_contentType, minutes) {
|
|
47
|
+
if (!minutes) return null;
|
|
48
|
+
/* No verb appended — the type line below the date (BLOG / TUTORIAL /
|
|
49
|
+
WEBINAR) already signals read vs. watch, and the extra word kept
|
|
50
|
+
wrapping to a second line on narrow card columns. */
|
|
51
|
+
return `${minutes} min`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function formatDate(date, locale = 'nl') {
|
|
55
|
+
if (!date) return null;
|
|
56
|
+
try {
|
|
57
|
+
const d = typeof date === 'string' ? new Date(date) : date;
|
|
58
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
59
|
+
return d.toLocaleDateString(locale, {day: 'numeric', month: 'short', year: 'numeric'});
|
|
60
|
+
} catch (_) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function authorInitials(name) {
|
|
66
|
+
if (!name) return '';
|
|
67
|
+
return name
|
|
68
|
+
.split(/\s+/)
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.slice(0, 2)
|
|
71
|
+
.map((part) => part.charAt(0))
|
|
72
|
+
.join('')
|
|
73
|
+
.toUpperCase();
|
|
74
|
+
}
|
|
75
|
+
|
|
49
76
|
export function ContentCardGrid({columns = 2, children, className}) {
|
|
50
77
|
const composed = [
|
|
51
78
|
styles.grid,
|
|
@@ -67,6 +94,12 @@ export default function ContentCard({
|
|
|
67
94
|
locale,
|
|
68
95
|
tags = [],
|
|
69
96
|
thumbnail,
|
|
97
|
+
durationMinutes,
|
|
98
|
+
audience = [],
|
|
99
|
+
module: moduleSlug,
|
|
100
|
+
modulePosition,
|
|
101
|
+
moduleTotalParts,
|
|
102
|
+
moduleTitle,
|
|
70
103
|
className,
|
|
71
104
|
...rest
|
|
72
105
|
}) {
|
|
@@ -81,40 +114,66 @@ export default function ContentCard({
|
|
|
81
114
|
const typeLabel = contentTypeLabel
|
|
82
115
|
|| (contentType && CONTENT_TYPE_LABELS[contentType])
|
|
83
116
|
|| null;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
117
|
+
|
|
118
|
+
/* Four-line meta block under the thumbnail:
|
|
119
|
+
1. Author name
|
|
120
|
+
2. Date · Read time
|
|
121
|
+
3. Type (TUTORIAL / BLOG / GUIDE)
|
|
122
|
+
4. Module title N/M (only when part of a module)
|
|
123
|
+
Each line is independently optional. */
|
|
124
|
+
const readWatch = readOrWatch(contentType, durationMinutes);
|
|
125
|
+
const formattedDate = formatDate(date, locale);
|
|
126
|
+
const dateReadLine = [formattedDate, readWatch].filter(Boolean).join(' · ');
|
|
127
|
+
const moduleLine = moduleSlug
|
|
128
|
+
? (modulePosition && moduleTotalParts
|
|
129
|
+
? `${moduleTitle || moduleSlug} ${modulePosition}/${moduleTotalParts}`
|
|
130
|
+
: (moduleTitle || moduleSlug))
|
|
131
|
+
: null;
|
|
132
|
+
|
|
133
|
+
const authorName = author && author.name;
|
|
134
|
+
const authorAvatar = author && author.avatarSrc;
|
|
135
|
+
const initials = (author && author.initials) || authorInitials(authorName);
|
|
87
136
|
|
|
88
137
|
return (
|
|
89
138
|
<Tag href={href} className={composed} {...rest}>
|
|
90
|
-
<div className={
|
|
91
|
-
{
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
139
|
+
<div className={styles.leftCol}>
|
|
140
|
+
<div className={[styles.thumbPanel, panelToneClass].join(' ')}>
|
|
141
|
+
{thumbProps.src
|
|
142
|
+
? <HexThumbnail size={thumbProps.size || 'md'} tone={thumbProps.tone || 'cobalt'} src={thumbProps.src} alt={thumbProps.alt} />
|
|
143
|
+
: <HexThumbnail size={thumbProps.size || 'md'} tone={thumbProps.tone || 'cobalt'}>{thumbProps.icon}</HexThumbnail>}
|
|
144
|
+
</div>
|
|
95
145
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
146
|
+
{(authorName || dateReadLine || typeLabel || moduleLine) && (
|
|
147
|
+
<div className={styles.metaStack}>
|
|
148
|
+
{(authorAvatar || initials) && (
|
|
149
|
+
<span className={styles.avatar} aria-hidden="true">
|
|
150
|
+
{authorAvatar
|
|
151
|
+
? <img src={authorAvatar} alt="" />
|
|
152
|
+
: <span className={styles.avatarInitials}>{initials}</span>}
|
|
153
|
+
</span>
|
|
154
|
+
)}
|
|
155
|
+
<div className={styles.metaLines}>
|
|
156
|
+
{authorName && <div className={styles.metaAuthor}>{authorName}</div>}
|
|
157
|
+
{dateReadLine && <div className={styles.metaSub}>{dateReadLine}</div>}
|
|
158
|
+
{typeLabel && <div className={styles.metaType}>{typeLabel}</div>}
|
|
159
|
+
{moduleLine && <div className={styles.metaModule}>{moduleLine}</div>}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
106
162
|
)}
|
|
163
|
+
</div>
|
|
107
164
|
|
|
165
|
+
<div className={styles.body}>
|
|
108
166
|
{title && <h3 className={styles.title}>{title}</h3>}
|
|
109
167
|
{summary && <p className={styles.summary}>{summary}</p>}
|
|
110
168
|
|
|
111
|
-
{
|
|
169
|
+
{audience.length > 0 && (
|
|
170
|
+
<div className={styles.audienceLine}>
|
|
171
|
+
For: {audience.map((a) => AUDIENCE_SHORT_LABELS[a] || a).join(', ')}
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
{tags.length > 0 && (
|
|
112
176
|
<div className={styles.tags}>
|
|
113
|
-
{typeLabel && (
|
|
114
|
-
<Pill bullet bulletColor={typeBullet}>
|
|
115
|
-
{typeLabel}
|
|
116
|
-
</Pill>
|
|
117
|
-
)}
|
|
118
177
|
{tags.map((tag, i) => (
|
|
119
178
|
<Pill key={i} bullet bulletColor="var(--c-cobalt-300)">{tag}</Pill>
|
|
120
179
|
))}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
text-decoration: none;
|
|
22
22
|
color: inherit;
|
|
23
23
|
transition: transform 160ms ease, box-shadow 160ms ease, background 160ms ease;
|
|
24
|
-
align-items:
|
|
24
|
+
align-items: stretch;
|
|
25
25
|
font-family: var(--conduction-typography-font-family-body);
|
|
26
26
|
}
|
|
27
27
|
.card:hover {
|
|
@@ -32,8 +32,81 @@
|
|
|
32
32
|
color: inherit;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
.leftCol {
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
min-height: 100%;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Four-line meta block under the thumbnail.
|
|
42
|
+
Layout: 32 px hex avatar on the left, four stacked lines on the
|
|
43
|
+
right. margin-top:auto pins the block to the bottom of the left
|
|
44
|
+
column so a tall right column doesn't leave it floating. */
|
|
45
|
+
.metaStack {
|
|
46
|
+
margin-top: auto;
|
|
47
|
+
padding-top: var(--space-4);
|
|
48
|
+
display: grid;
|
|
49
|
+
grid-template-columns: 32px 1fr;
|
|
50
|
+
gap: 10px;
|
|
51
|
+
align-items: start;
|
|
52
|
+
}
|
|
53
|
+
.avatar {
|
|
54
|
+
width: 32px;
|
|
55
|
+
height: 32px;
|
|
56
|
+
background: var(--c-cobalt-100);
|
|
57
|
+
color: var(--c-cobalt-700);
|
|
58
|
+
display: inline-flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
justify-content: center;
|
|
61
|
+
font-family: var(--conduction-typography-font-family-code);
|
|
62
|
+
font-size: 11px;
|
|
63
|
+
font-weight: 600;
|
|
64
|
+
text-transform: uppercase;
|
|
65
|
+
flex-shrink: 0;
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
|
|
68
|
+
}
|
|
69
|
+
.avatar img { width: 100%; height: 100%; object-fit: cover; }
|
|
70
|
+
.avatarInitials { line-height: 1; }
|
|
71
|
+
|
|
72
|
+
.metaLines { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
73
|
+
.metaAuthor {
|
|
74
|
+
font-size: 13px;
|
|
75
|
+
font-weight: 600;
|
|
76
|
+
color: var(--c-cobalt-900);
|
|
77
|
+
line-height: 1.3;
|
|
78
|
+
overflow-wrap: anywhere;
|
|
79
|
+
}
|
|
80
|
+
.metaSub {
|
|
81
|
+
font-family: var(--conduction-typography-font-family-code);
|
|
82
|
+
font-size: 11px;
|
|
83
|
+
letter-spacing: 0.04em;
|
|
84
|
+
color: var(--c-cobalt-700);
|
|
85
|
+
line-height: 1.3;
|
|
86
|
+
white-space: nowrap;
|
|
87
|
+
}
|
|
88
|
+
.metaType {
|
|
89
|
+
font-family: var(--conduction-typography-font-family-code);
|
|
90
|
+
font-size: 11px;
|
|
91
|
+
letter-spacing: 0.08em;
|
|
92
|
+
text-transform: uppercase;
|
|
93
|
+
color: var(--c-blue-cobalt);
|
|
94
|
+
line-height: 1.3;
|
|
95
|
+
margin-top: 2px;
|
|
96
|
+
font-weight: 600;
|
|
97
|
+
}
|
|
98
|
+
.metaModule {
|
|
99
|
+
font-family: var(--conduction-typography-font-family-code);
|
|
100
|
+
font-size: 11px;
|
|
101
|
+
letter-spacing: 0.04em;
|
|
102
|
+
color: var(--c-cobalt-400);
|
|
103
|
+
line-height: 1.3;
|
|
104
|
+
overflow-wrap: anywhere;
|
|
105
|
+
}
|
|
106
|
+
|
|
35
107
|
@media (max-width: 700px) {
|
|
36
108
|
.card { grid-template-columns: 1fr; }
|
|
109
|
+
.metaStack { margin-top: 0; padding-top: var(--space-3); }
|
|
37
110
|
}
|
|
38
111
|
|
|
39
112
|
.thumbPanel {
|
|
@@ -76,9 +149,33 @@
|
|
|
76
149
|
line-height: 1.55;
|
|
77
150
|
}
|
|
78
151
|
|
|
79
|
-
.
|
|
152
|
+
.meta {
|
|
80
153
|
display: flex;
|
|
81
|
-
gap: 6px;
|
|
82
154
|
flex-wrap: wrap;
|
|
83
|
-
|
|
155
|
+
align-items: center;
|
|
156
|
+
gap: 6px;
|
|
157
|
+
font-family: var(--conduction-typography-font-family-code);
|
|
158
|
+
font-size: 12px;
|
|
159
|
+
letter-spacing: 0.04em;
|
|
160
|
+
color: var(--c-cobalt-400);
|
|
161
|
+
text-transform: uppercase;
|
|
162
|
+
margin-top: 2px;
|
|
163
|
+
}
|
|
164
|
+
.metaBit { white-space: nowrap; }
|
|
165
|
+
.metaSep { opacity: 0.6; }
|
|
166
|
+
|
|
167
|
+
/* "For: MKB, Overheid, Developers" — replaces the audience pill row
|
|
168
|
+
so a card with multiple audiences doesn't fill the body with chips.
|
|
169
|
+
Matches the .metaSub typography so it reads as another meta line
|
|
170
|
+
rather than a heading. */
|
|
171
|
+
.audienceLine {
|
|
172
|
+
font-family: var(--conduction-typography-font-family-code);
|
|
173
|
+
font-size: 11px;
|
|
174
|
+
letter-spacing: 0.04em;
|
|
175
|
+
color: var(--c-cobalt-700);
|
|
176
|
+
line-height: 1.4;
|
|
84
177
|
}
|
|
178
|
+
|
|
179
|
+
.tags {
|
|
180
|
+
display: flex;
|
|
181
|
+
gap: 6px;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <DownloadPanel />
|
|
3
|
+
*
|
|
4
|
+
* Split cobalt-800 panel with a pitch on the left and a cut-corner
|
|
5
|
+
* white card on the right. Modelled on the lead-capture specimen in
|
|
6
|
+
* preview/components.html (.download-panel) and re-homed in
|
|
7
|
+
* preview/components/download-panel.html.
|
|
8
|
+
*
|
|
9
|
+
* The card is form-less by default and renders a single primary
|
|
10
|
+
* CTA, KNVB-orange via Button tone="orange". The form variant from
|
|
11
|
+
* the kit can be reintroduced later by passing `card.children` and
|
|
12
|
+
* dropping the cta.
|
|
13
|
+
*
|
|
14
|
+
* Usage in MDX:
|
|
15
|
+
*
|
|
16
|
+
* <DownloadPanel
|
|
17
|
+
* title={<>This is not an app shelf.<br/>It is <span className="next-blue">Nextcloud</span> as a platform.</>}
|
|
18
|
+
* lede="The sovereign-workplace bundles are the start, not the finish."
|
|
19
|
+
* meta="Thinkpiece · 7 min · By Ruben van der Linde"
|
|
20
|
+
* card={{
|
|
21
|
+
* title: 'Want the full essay?',
|
|
22
|
+
* cta: {label: 'Read the thinkpiece', href: '/academy/the-platform-moment'},
|
|
23
|
+
* }}
|
|
24
|
+
* />
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import React from 'react';
|
|
28
|
+
import ConductionBg from '../ConductionBg/ConductionBg';
|
|
29
|
+
import Button from '../primitives/Button';
|
|
30
|
+
import styles from './DownloadPanel.module.css';
|
|
31
|
+
|
|
32
|
+
export default function DownloadPanel({
|
|
33
|
+
title,
|
|
34
|
+
lede,
|
|
35
|
+
meta,
|
|
36
|
+
card,
|
|
37
|
+
}) {
|
|
38
|
+
return (
|
|
39
|
+
<section className={styles.section}>
|
|
40
|
+
<div className={styles.inner}>
|
|
41
|
+
<div className={styles.panel}>
|
|
42
|
+
<ConductionBg />
|
|
43
|
+
<div className={styles.pitch}>
|
|
44
|
+
{title && <h3 className={styles.title}>{title}</h3>}
|
|
45
|
+
{lede && <p className={styles.lede}>{lede}</p>}
|
|
46
|
+
{meta && <p className={styles.meta}>{meta}</p>}
|
|
47
|
+
</div>
|
|
48
|
+
{card && (
|
|
49
|
+
<div className={styles.card}>
|
|
50
|
+
{card.title && <h4 className={styles.cardTitle}>{card.title}</h4>}
|
|
51
|
+
{card.body && <p className={styles.cardBody}>{card.body}</p>}
|
|
52
|
+
{card.children}
|
|
53
|
+
{card.cta && (
|
|
54
|
+
<Button
|
|
55
|
+
variant="primary"
|
|
56
|
+
tone="orange"
|
|
57
|
+
href={card.cta.href}
|
|
58
|
+
className={styles.cardCta}
|
|
59
|
+
>
|
|
60
|
+
{card.cta.label}
|
|
61
|
+
</Button>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</section>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <DownloadPanel /> styles. Mirrors the .download-panel specimen
|
|
3
|
+
* in preview/components.html: cobalt-800 panel, 2-column grid with
|
|
4
|
+
* the pitch on the left and a white cut-corner card on the right.
|
|
5
|
+
*
|
|
6
|
+
* Button + ConductionBg carry their own styling; this module only
|
|
7
|
+
* owns the panel chrome (frame, pitch column, card chrome).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/* Tinted band so the cobalt-800 panel reads as a deliberate break from
|
|
11
|
+
the surrounding white sections. Mirrors the Section primitive's
|
|
12
|
+
bg-tinted + sp-tight tokens exactly (cobalt-50 fill, hairline
|
|
13
|
+
borders top + bottom, 48px block padding) so the rhythm matches the
|
|
14
|
+
other tinted bands on /connext without inheriting Section's full
|
|
15
|
+
96px gutter, which left too much air around the panel itself. The
|
|
16
|
+
panel's own 56px internal padding already provides the breathing
|
|
17
|
+
room. */
|
|
18
|
+
/* margin-top eats into the previous section's bottom padding so the
|
|
19
|
+
tinted band sits close to the previous section's last visible
|
|
20
|
+
content. PlatformOverview ships ~240px of structural-empty space
|
|
21
|
+
at the bottom of its diagram-grid (reserved for absolutely-
|
|
22
|
+
positioned `.desc` hover tooltips that anchor above their app
|
|
23
|
+
cards — they pop up, not down, so they don't intrude into this
|
|
24
|
+
zone). Pulling up by ~200px lands the band just below the c-bracket
|
|
25
|
+
flow line without clipping any visible content. Outer chrome only;
|
|
26
|
+
the panel's own 56px internal padding and grid layout are
|
|
27
|
+
untouched. */
|
|
28
|
+
.section {
|
|
29
|
+
background: var(--c-cobalt-50);
|
|
30
|
+
border-top: 1px solid var(--c-cobalt-100);
|
|
31
|
+
border-bottom: 1px solid var(--c-cobalt-100);
|
|
32
|
+
margin-top: -200px;
|
|
33
|
+
font-family: var(--conduction-typography-font-family-body);
|
|
34
|
+
}
|
|
35
|
+
.inner {
|
|
36
|
+
max-width: 1280px;
|
|
37
|
+
margin: 0 auto;
|
|
38
|
+
padding: 24px 64px;
|
|
39
|
+
}
|
|
40
|
+
@media (max-width: 700px) {
|
|
41
|
+
.inner { padding: 24px 16px; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.panel {
|
|
45
|
+
background: var(--c-cobalt-800);
|
|
46
|
+
color: white;
|
|
47
|
+
border-radius: var(--radius-xl);
|
|
48
|
+
padding: 56px;
|
|
49
|
+
display: grid;
|
|
50
|
+
grid-template-columns: 1fr 440px;
|
|
51
|
+
gap: 56px;
|
|
52
|
+
align-items: center;
|
|
53
|
+
position: relative;
|
|
54
|
+
overflow: hidden;
|
|
55
|
+
isolation: isolate;
|
|
56
|
+
}
|
|
57
|
+
@media (max-width: 900px) {
|
|
58
|
+
.panel {
|
|
59
|
+
grid-template-columns: 1fr;
|
|
60
|
+
gap: 40px;
|
|
61
|
+
padding: 40px;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.pitch,
|
|
66
|
+
.card {
|
|
67
|
+
position: relative;
|
|
68
|
+
z-index: 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.title {
|
|
72
|
+
color: white;
|
|
73
|
+
font-size: 32px;
|
|
74
|
+
font-weight: 700;
|
|
75
|
+
letter-spacing: -0.02em;
|
|
76
|
+
line-height: 1.2;
|
|
77
|
+
margin: 0 0 var(--space-5);
|
|
78
|
+
}
|
|
79
|
+
@media (max-width: 700px) {
|
|
80
|
+
.title { font-size: 24px; }
|
|
81
|
+
}
|
|
82
|
+
.title :global(.next-blue) { color: var(--c-nextcloud-cyan); }
|
|
83
|
+
.title :global(.cg-yellow) { color: var(--c-commonground-yellow); }
|
|
84
|
+
|
|
85
|
+
.lede {
|
|
86
|
+
color: rgba(255, 255, 255, 0.78);
|
|
87
|
+
font-size: 15px;
|
|
88
|
+
line-height: 1.6;
|
|
89
|
+
margin: 0 0 var(--space-4);
|
|
90
|
+
max-width: 520px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.meta {
|
|
94
|
+
color: rgba(255, 255, 255, 0.55);
|
|
95
|
+
font-size: 13px;
|
|
96
|
+
line-height: 1.5;
|
|
97
|
+
margin: 0;
|
|
98
|
+
max-width: 520px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.card {
|
|
102
|
+
background: white;
|
|
103
|
+
color: var(--conduction-color-text-default);
|
|
104
|
+
border-radius: var(--radius-lg);
|
|
105
|
+
padding: 36px;
|
|
106
|
+
clip-path: var(--clip-cut-tlbr-lg);
|
|
107
|
+
display: flex;
|
|
108
|
+
flex-direction: column;
|
|
109
|
+
gap: var(--space-4);
|
|
110
|
+
align-items: flex-start;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.cardTitle {
|
|
114
|
+
font-size: 20px;
|
|
115
|
+
font-weight: 600;
|
|
116
|
+
margin: 0;
|
|
117
|
+
color: var(--c-blue-cobalt);
|
|
118
|
+
letter-spacing: -0.005em;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.cardBody {
|
|
122
|
+
font-size: 14px;
|
|
123
|
+
line-height: 1.55;
|
|
124
|
+
color: var(--c-cobalt-700);
|
|
125
|
+
margin: 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.cardCta {
|
|
129
|
+
margin-top: var(--space-2);
|
|
130
|
+
}
|
|
@@ -30,8 +30,15 @@ import React from 'react';
|
|
|
30
30
|
import HexThumbnail from '../primitives/HexThumbnail';
|
|
31
31
|
import HexBullet from '../primitives/HexBullet';
|
|
32
32
|
import AuthorByline from '../primitives/AuthorByline';
|
|
33
|
+
import {AUDIENCE_LABELS} from '../../data/audience';
|
|
33
34
|
import styles from './FeaturedCard.module.css';
|
|
34
35
|
|
|
36
|
+
function readOrWatch(contentType, minutes) {
|
|
37
|
+
if (!minutes) return null;
|
|
38
|
+
const verb = contentType === 'webinar' ? 'watch' : 'read';
|
|
39
|
+
return `${minutes} min ${verb}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
35
42
|
export default function FeaturedCard({
|
|
36
43
|
href,
|
|
37
44
|
eyebrow,
|
|
@@ -44,8 +51,26 @@ export default function FeaturedCard({
|
|
|
44
51
|
locale,
|
|
45
52
|
thumbnail,
|
|
46
53
|
accent = 'orange',
|
|
54
|
+
contentType,
|
|
55
|
+
durationMinutes,
|
|
56
|
+
audience = [],
|
|
57
|
+
module: moduleSlug,
|
|
58
|
+
modulePosition,
|
|
59
|
+
moduleTotalParts,
|
|
60
|
+
moduleTitle,
|
|
47
61
|
className,
|
|
48
62
|
}) {
|
|
63
|
+
const readWatch = readOrWatch(contentType, durationMinutes);
|
|
64
|
+
const moduleLabel = moduleSlug
|
|
65
|
+
? (modulePosition && moduleTotalParts
|
|
66
|
+
? `Part ${modulePosition} of ${moduleTotalParts}`
|
|
67
|
+
: (modulePosition ? `Part ${modulePosition}` : null))
|
|
68
|
+
: null;
|
|
69
|
+
const audienceLabel = audience.length > 0
|
|
70
|
+
? audience.map((a) => AUDIENCE_LABELS[a] || a).join(' · ')
|
|
71
|
+
: null;
|
|
72
|
+
const metaBits = [readWatch, moduleLabel, moduleSlug ? (moduleTitle || moduleSlug) : null, audienceLabel]
|
|
73
|
+
.filter(Boolean);
|
|
49
74
|
const Tag = href ? 'a' : 'div';
|
|
50
75
|
const composed = [styles.card, className].filter(Boolean).join(' ');
|
|
51
76
|
const thumbProps = thumbnail || {};
|
|
@@ -62,6 +87,17 @@ export default function FeaturedCard({
|
|
|
62
87
|
{title && <h2 className={styles.title}>{title}</h2>}
|
|
63
88
|
{lede && <p className={styles.lede}>{lede}</p>}
|
|
64
89
|
|
|
90
|
+
{metaBits.length > 0 && (
|
|
91
|
+
<div className={styles.metaBits}>
|
|
92
|
+
{metaBits.map((bit, i) => (
|
|
93
|
+
<React.Fragment key={i}>
|
|
94
|
+
{i > 0 && <span className={styles.metaSep} aria-hidden="true">·</span>}
|
|
95
|
+
<span className={styles.metaBit}>{bit}</span>
|
|
96
|
+
</React.Fragment>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
|
|
65
101
|
{(author || date) && (
|
|
66
102
|
<div className={styles.meta}>
|
|
67
103
|
<AuthorByline
|
|
@@ -55,6 +55,21 @@
|
|
|
55
55
|
max-width: 50ch;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
.metaBits {
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-wrap: wrap;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: 6px;
|
|
63
|
+
font-family: var(--conduction-typography-font-family-code);
|
|
64
|
+
font-size: 12px;
|
|
65
|
+
letter-spacing: 0.04em;
|
|
66
|
+
color: var(--c-cobalt-200);
|
|
67
|
+
text-transform: uppercase;
|
|
68
|
+
margin-bottom: var(--space-4);
|
|
69
|
+
}
|
|
70
|
+
.metaBits .metaBit { white-space: nowrap; }
|
|
71
|
+
.metaBits .metaSep { opacity: 0.6; }
|
|
72
|
+
|
|
58
73
|
.meta { margin-bottom: var(--space-5); }
|
|
59
74
|
|
|
60
75
|
.cta {
|