@conduction/docusaurus-preset 3.15.2 → 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/css/brand.css +9 -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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <ModuleCard />
|
|
3
|
+
*
|
|
4
|
+
* Composite card for an academy module (an ordered group of academy
|
|
5
|
+
* entries that share a `module:` slug). Mirrors <ContentCard/> so the
|
|
6
|
+
* two sit in the same academy grid without visually diverging. Left
|
|
7
|
+
* column carries thumbnail + four-line meta stack (curator, latest
|
|
8
|
+
* date · total min, MODULE · N PARTS, composite type summary). Right
|
|
9
|
+
* column is title + lede + audience line.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
*
|
|
13
|
+
* <ModuleCard
|
|
14
|
+
* href="/academy/modules/deskdesk-tutorial"
|
|
15
|
+
* title="Build a Nextcloud app on the Conduction stack"
|
|
16
|
+
* lede="Four parts from blank Nextcloud to published app."
|
|
17
|
+
* parts={4}
|
|
18
|
+
* totalMinutes={95}
|
|
19
|
+
* latestDate="2026-05-17"
|
|
20
|
+
* curator={{ name: 'Ruben van der Linde', imageURL: '...' }}
|
|
21
|
+
* audience={['developer']}
|
|
22
|
+
* contentTypes={['tutorial']}
|
|
23
|
+
* />
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import React from 'react';
|
|
27
|
+
import HexThumbnail from '../primitives/HexThumbnail';
|
|
28
|
+
import {AUDIENCE_SHORT_LABELS} from '../../data/audience';
|
|
29
|
+
import {CONTENT_TYPE_PLURAL_LABELS} from '../ContentTypeFilter/contentTypes';
|
|
30
|
+
import styles from './ModuleCard.module.css';
|
|
31
|
+
|
|
32
|
+
function StackedHexIcon() {
|
|
33
|
+
/* Three pointy-top hexes layered to read as a stack/series. Uses
|
|
34
|
+
currentColor so the surrounding tone variant drives the stroke
|
|
35
|
+
colour (white on cobalt-deep, cobalt-700 on cobalt-50, etc.). */
|
|
36
|
+
const stroke = {fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, strokeLinejoin: 'round'};
|
|
37
|
+
return (
|
|
38
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" {...stroke}>
|
|
39
|
+
<path d="M6 4l5 2.5v5L6 14l-5-2.5v-5L6 4z" opacity="0.5" />
|
|
40
|
+
<path d="M12 7l5 2.5v5L12 17l-5-2.5v-5L12 7z" opacity="0.75" />
|
|
41
|
+
<path d="M18 10l5 2.5v5L18 20l-5-2.5v-5L18 10z" />
|
|
42
|
+
</svg>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatDate(date, locale = 'nl') {
|
|
47
|
+
if (!date) return null;
|
|
48
|
+
try {
|
|
49
|
+
const d = typeof date === 'string' ? new Date(date) : date;
|
|
50
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
51
|
+
return d.toLocaleDateString(locale, {day: 'numeric', month: 'short', year: 'numeric'});
|
|
52
|
+
} catch (_) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function initialsFrom(name) {
|
|
58
|
+
if (!name) return '';
|
|
59
|
+
return name
|
|
60
|
+
.split(/\s+/)
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.slice(0, 2)
|
|
63
|
+
.map((part) => part.charAt(0))
|
|
64
|
+
.join('')
|
|
65
|
+
.toUpperCase();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default function ModuleCard({
|
|
69
|
+
href,
|
|
70
|
+
title,
|
|
71
|
+
lede,
|
|
72
|
+
parts,
|
|
73
|
+
totalMinutes,
|
|
74
|
+
latestDate,
|
|
75
|
+
curator,
|
|
76
|
+
locale,
|
|
77
|
+
audience = [],
|
|
78
|
+
contentTypes = [],
|
|
79
|
+
className,
|
|
80
|
+
...rest
|
|
81
|
+
}) {
|
|
82
|
+
const Tag = href ? 'a' : 'div';
|
|
83
|
+
const composed = [styles.card, className].filter(Boolean).join(' ');
|
|
84
|
+
|
|
85
|
+
/* No verb after the duration — the type line (MODULE · N PARTS) and
|
|
86
|
+
the composite type summary below already signal whether this is
|
|
87
|
+
something the visitor reads or watches. Keeps the date line on a
|
|
88
|
+
single row in narrow columns. */
|
|
89
|
+
const totalLabel = typeof totalMinutes === 'number' ? `${totalMinutes} min` : null;
|
|
90
|
+
const dateLabel = formatDate(latestDate, locale);
|
|
91
|
+
const dateReadLine = [dateLabel, totalLabel].filter(Boolean).join(' · ');
|
|
92
|
+
|
|
93
|
+
const partsLabel = typeof parts === 'number'
|
|
94
|
+
? (parts === 1 ? '1 PART' : `${parts} PARTS`)
|
|
95
|
+
: null;
|
|
96
|
+
const typeLine = ['MODULE', partsLabel].filter(Boolean).join(' · ');
|
|
97
|
+
|
|
98
|
+
/* Composite content-type summary on line 4. For a single-type
|
|
99
|
+
module ("Tutorials") this is one word; mixed modules read
|
|
100
|
+
"Tutorials + Guides". Drops entirely when the module has no
|
|
101
|
+
content types resolved yet. */
|
|
102
|
+
const typeSummary = contentTypes.length > 0
|
|
103
|
+
? contentTypes.map((ct) => CONTENT_TYPE_PLURAL_LABELS[ct] || ct).join(' + ')
|
|
104
|
+
: null;
|
|
105
|
+
|
|
106
|
+
const curatorName = curator && curator.name;
|
|
107
|
+
const curatorAvatar = curator && curator.imageURL;
|
|
108
|
+
const curatorInitials = (curator && initialsFrom(curator.name)) || '';
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<Tag href={href} className={composed} {...rest}>
|
|
112
|
+
<div className={styles.leftCol}>
|
|
113
|
+
<div className={styles.thumbPanel}>
|
|
114
|
+
<HexThumbnail size="md" tone="cobalt">
|
|
115
|
+
<StackedHexIcon />
|
|
116
|
+
</HexThumbnail>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{(curatorName || dateReadLine || typeLine || typeSummary) && (
|
|
120
|
+
<div className={styles.metaStack}>
|
|
121
|
+
{(curatorAvatar || curatorInitials) && (
|
|
122
|
+
<span className={styles.avatar} aria-hidden="true">
|
|
123
|
+
{curatorAvatar
|
|
124
|
+
? <img src={curatorAvatar} alt="" />
|
|
125
|
+
: <span className={styles.avatarInitials}>{curatorInitials}</span>}
|
|
126
|
+
</span>
|
|
127
|
+
)}
|
|
128
|
+
<div className={styles.metaLines}>
|
|
129
|
+
{curatorName && <div className={styles.metaAuthor}>{curatorName}</div>}
|
|
130
|
+
{dateReadLine && <div className={styles.metaSub}>{dateReadLine}</div>}
|
|
131
|
+
{typeLine && <div className={styles.metaType}>{typeLine}</div>}
|
|
132
|
+
{typeSummary && <div className={styles.metaModule}>{typeSummary}</div>}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div className={styles.body}>
|
|
139
|
+
{title && <h3 className={styles.title}>{title}</h3>}
|
|
140
|
+
{lede && <p className={styles.lede}>{lede}</p>}
|
|
141
|
+
|
|
142
|
+
{audience.length > 0 && (
|
|
143
|
+
<div className={styles.audienceLine}>
|
|
144
|
+
For: {audience.map((a) => AUDIENCE_SHORT_LABELS[a] || a).join(', ')}
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
</Tag>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/* <ModuleCard /> styles. Mirror <ContentCard/> so a module composite
|
|
2
|
+
sits in the same academy grid without visually diverging. Same
|
|
3
|
+
column ratio, same hover, same bottom-pinned meta stack. The only
|
|
4
|
+
visual cue that distinguishes a module card is the stacked-hex
|
|
5
|
+
glyph in the thumbnail panel. */
|
|
6
|
+
|
|
7
|
+
.card {
|
|
8
|
+
display: grid;
|
|
9
|
+
grid-template-columns: 200px 1fr;
|
|
10
|
+
gap: var(--space-6);
|
|
11
|
+
padding: var(--space-5);
|
|
12
|
+
background: var(--c-cobalt-50);
|
|
13
|
+
border-radius: var(--radius-lg);
|
|
14
|
+
text-decoration: none;
|
|
15
|
+
color: inherit;
|
|
16
|
+
transition: transform 160ms ease, box-shadow 160ms ease, background 160ms ease;
|
|
17
|
+
align-items: stretch;
|
|
18
|
+
font-family: var(--conduction-typography-font-family-body);
|
|
19
|
+
}
|
|
20
|
+
.card:hover {
|
|
21
|
+
background: white;
|
|
22
|
+
box-shadow: var(--shadow-2);
|
|
23
|
+
transform: translateY(-2px);
|
|
24
|
+
text-decoration: none;
|
|
25
|
+
color: inherit;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@media (max-width: 700px) {
|
|
29
|
+
.card { grid-template-columns: 1fr; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.leftCol {
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
min-height: 100%;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.thumbPanel {
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
justify-content: center;
|
|
42
|
+
width: 100%;
|
|
43
|
+
height: 184px;
|
|
44
|
+
border-radius: var(--radius-md);
|
|
45
|
+
background: var(--c-cobalt-900);
|
|
46
|
+
color: white;
|
|
47
|
+
position: relative;
|
|
48
|
+
overflow: hidden;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.metaStack {
|
|
52
|
+
margin-top: auto;
|
|
53
|
+
padding-top: var(--space-4);
|
|
54
|
+
display: grid;
|
|
55
|
+
grid-template-columns: 32px 1fr;
|
|
56
|
+
gap: 10px;
|
|
57
|
+
align-items: start;
|
|
58
|
+
}
|
|
59
|
+
.avatar {
|
|
60
|
+
width: 32px;
|
|
61
|
+
height: 32px;
|
|
62
|
+
background: var(--c-cobalt-100);
|
|
63
|
+
color: var(--c-cobalt-700);
|
|
64
|
+
display: inline-flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
justify-content: center;
|
|
67
|
+
font-family: var(--conduction-typography-font-family-code);
|
|
68
|
+
font-size: 11px;
|
|
69
|
+
font-weight: 600;
|
|
70
|
+
text-transform: uppercase;
|
|
71
|
+
flex-shrink: 0;
|
|
72
|
+
overflow: hidden;
|
|
73
|
+
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
|
|
74
|
+
}
|
|
75
|
+
.avatar img { width: 100%; height: 100%; object-fit: cover; }
|
|
76
|
+
.avatarInitials { line-height: 1; }
|
|
77
|
+
|
|
78
|
+
.metaLines { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
79
|
+
.metaAuthor {
|
|
80
|
+
font-size: 13px;
|
|
81
|
+
font-weight: 600;
|
|
82
|
+
color: var(--c-cobalt-900);
|
|
83
|
+
line-height: 1.3;
|
|
84
|
+
overflow-wrap: anywhere;
|
|
85
|
+
}
|
|
86
|
+
.metaSub {
|
|
87
|
+
font-family: var(--conduction-typography-font-family-code);
|
|
88
|
+
font-size: 11px;
|
|
89
|
+
letter-spacing: 0.04em;
|
|
90
|
+
color: var(--c-cobalt-700);
|
|
91
|
+
line-height: 1.3;
|
|
92
|
+
white-space: nowrap;
|
|
93
|
+
}
|
|
94
|
+
.metaType {
|
|
95
|
+
font-family: var(--conduction-typography-font-family-code);
|
|
96
|
+
font-size: 11px;
|
|
97
|
+
letter-spacing: 0.08em;
|
|
98
|
+
text-transform: uppercase;
|
|
99
|
+
color: var(--c-blue-cobalt);
|
|
100
|
+
line-height: 1.3;
|
|
101
|
+
margin-top: 2px;
|
|
102
|
+
font-weight: 600;
|
|
103
|
+
}
|
|
104
|
+
.metaModule {
|
|
105
|
+
font-family: var(--conduction-typography-font-family-code);
|
|
106
|
+
font-size: 11px;
|
|
107
|
+
letter-spacing: 0.04em;
|
|
108
|
+
color: var(--c-cobalt-400);
|
|
109
|
+
line-height: 1.3;
|
|
110
|
+
overflow-wrap: anywhere;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@media (max-width: 700px) {
|
|
114
|
+
.metaStack { margin-top: 0; padding-top: var(--space-3); }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.body {
|
|
118
|
+
display: flex;
|
|
119
|
+
flex-direction: column;
|
|
120
|
+
gap: var(--space-3);
|
|
121
|
+
min-width: 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.title {
|
|
125
|
+
font-size: 18px;
|
|
126
|
+
font-weight: 700;
|
|
127
|
+
letter-spacing: -0.01em;
|
|
128
|
+
color: var(--c-cobalt-900);
|
|
129
|
+
margin: 0;
|
|
130
|
+
line-height: 1.3;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.lede {
|
|
134
|
+
font-size: 14px;
|
|
135
|
+
color: var(--c-cobalt-700);
|
|
136
|
+
margin: 0;
|
|
137
|
+
line-height: 1.55;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.audienceLine {
|
|
141
|
+
font-family: var(--conduction-typography-font-family-code);
|
|
142
|
+
font-size: 11px;
|
|
143
|
+
letter-spacing: 0.04em;
|
|
144
|
+
color: var(--c-cobalt-700);
|
|
145
|
+
line-height: 1.4;
|
|
146
|
+
margin-top: 4px;
|
|
147
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <ModulePage />
|
|
3
|
+
*
|
|
4
|
+
* Wrapper for per-module MDX index pages at /academy/modules/{slug}.
|
|
5
|
+
* Each module gets its own MDX file at
|
|
6
|
+
* `src/pages/academy/modules/{slug}.mdx` that imports this component,
|
|
7
|
+
* passes the module slug, and provides the hero + body copy as
|
|
8
|
+
* children. ModulePage handles the rest:
|
|
9
|
+
*
|
|
10
|
+
* - Reads the module's parts from the `academy-modules` plugin's
|
|
11
|
+
* globalData (slug, title, lede, ordered parts, audience, apps,
|
|
12
|
+
* totals).
|
|
13
|
+
* - Renders the hero (title + lede + meta line: parts count, total
|
|
14
|
+
* duration, audience tier).
|
|
15
|
+
* - Renders the editor-supplied MDX children below the hero (room
|
|
16
|
+
* for "What you build", "Who this is for", "Prerequisites", etc.).
|
|
17
|
+
* - Renders the ordered parts list at the bottom with one
|
|
18
|
+
* <ContentCard/> per part.
|
|
19
|
+
*
|
|
20
|
+
* The site must register the `academy-modules` plugin in
|
|
21
|
+
* docusaurus.config.js for usePluginData('academy-modules') to
|
|
22
|
+
* return data. When the plugin is missing, ModulePage falls back to
|
|
23
|
+
* a "module not found" message rather than crashing.
|
|
24
|
+
*
|
|
25
|
+
* Usage (MDX):
|
|
26
|
+
*
|
|
27
|
+
* import {ModulePage} from '@conduction/docusaurus-preset/components';
|
|
28
|
+
*
|
|
29
|
+
* <ModulePage module="deskdesk-tutorial">
|
|
30
|
+
* Hier hoort de marketing-lede van de module, korte zinnen,
|
|
31
|
+
* niet langer dan een alinea of vier.
|
|
32
|
+
* </ModulePage>
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import React from 'react';
|
|
36
|
+
import {usePluginData} from '@docusaurus/useGlobalData';
|
|
37
|
+
import ContentCard, {ContentCardGrid} from '../ContentCard/ContentCard.jsx';
|
|
38
|
+
import {AUDIENCE_LABELS} from '../../data/audience';
|
|
39
|
+
import {CONTENT_TYPE_LABELS} from '../ContentTypeFilter/contentTypes';
|
|
40
|
+
import styles from './ModulePage.module.css';
|
|
41
|
+
|
|
42
|
+
function defaultIconFor(contentType) {
|
|
43
|
+
const stroke = {strokeWidth: 1.6, fill: 'none', stroke: 'currentColor'};
|
|
44
|
+
switch (contentType) {
|
|
45
|
+
case 'tutorial':
|
|
46
|
+
return (
|
|
47
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" {...stroke}>
|
|
48
|
+
<path d="M4 7h16" /><path d="M4 12h16" /><path d="M4 17h10" />
|
|
49
|
+
<path d="M19 14v6" /><path d="M16 17l3 3 3-3" />
|
|
50
|
+
</svg>
|
|
51
|
+
);
|
|
52
|
+
case 'guide':
|
|
53
|
+
return (
|
|
54
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" {...stroke}>
|
|
55
|
+
<path d="M4 4h12a4 4 0 0 1 4 4v12H8a4 4 0 0 1-4-4V4z" />
|
|
56
|
+
<path d="M4 16a4 4 0 0 1 4-4h12" />
|
|
57
|
+
</svg>
|
|
58
|
+
);
|
|
59
|
+
case 'webinar':
|
|
60
|
+
return (
|
|
61
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" {...stroke}>
|
|
62
|
+
<circle cx="12" cy="12" r="9" />
|
|
63
|
+
<path d="M10 9l5 3-5 3z" fill="currentColor" stroke="none" />
|
|
64
|
+
</svg>
|
|
65
|
+
);
|
|
66
|
+
case 'case-study':
|
|
67
|
+
return (
|
|
68
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" {...stroke}>
|
|
69
|
+
<rect x="3" y="7" width="18" height="13" rx="1" />
|
|
70
|
+
<path d="M9 7V5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2" />
|
|
71
|
+
<path d="M3 13h18" />
|
|
72
|
+
</svg>
|
|
73
|
+
);
|
|
74
|
+
case 'blog':
|
|
75
|
+
default:
|
|
76
|
+
return (
|
|
77
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" {...stroke}>
|
|
78
|
+
<path d="M3 11l9-8 9 8" />
|
|
79
|
+
<path d="M5 10v10h14V10" />
|
|
80
|
+
<path d="M9 20v-6h6v6" />
|
|
81
|
+
</svg>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function partToCardProps(part) {
|
|
87
|
+
return {
|
|
88
|
+
href: part.permalink,
|
|
89
|
+
contentType: part.contentType,
|
|
90
|
+
title: part.title,
|
|
91
|
+
summary: part.summary,
|
|
92
|
+
date: part.date,
|
|
93
|
+
tags: [`Part ${part.modulePosition}`],
|
|
94
|
+
thumbnail: {icon: defaultIconFor(part.contentType), panelTone: 'cobalt-dark'},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default function ModulePage({module: moduleSlug, children}) {
|
|
99
|
+
const data = usePluginData('academy-modules') || {};
|
|
100
|
+
const mod = data.modules && data.modules[moduleSlug];
|
|
101
|
+
|
|
102
|
+
if (!mod) {
|
|
103
|
+
return (
|
|
104
|
+
<main className="marketing-page">
|
|
105
|
+
<div className={styles.notFound}>
|
|
106
|
+
<h1>Module not found</h1>
|
|
107
|
+
<p>No academy module with slug <code>{moduleSlug}</code> is registered. Check the module slug in this page's frontmatter.</p>
|
|
108
|
+
</div>
|
|
109
|
+
</main>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const metaBits = [];
|
|
114
|
+
metaBits.push(mod.parts.length === 1 ? '1 part' : `${mod.parts.length} parts`);
|
|
115
|
+
if (mod.totalMinutes) metaBits.push(`${mod.totalMinutes} min total`);
|
|
116
|
+
if (mod.contentTypes && mod.contentTypes.length === 1) {
|
|
117
|
+
metaBits.push(CONTENT_TYPE_LABELS[mod.contentTypes[0]] || mod.contentTypes[0]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<main className="marketing-page">
|
|
122
|
+
<header className={styles.hero}>
|
|
123
|
+
<div className={styles.eyebrow}>Module · academy</div>
|
|
124
|
+
<h1 className={styles.title}>{mod.title}</h1>
|
|
125
|
+
{mod.lede && <p className={styles.lede}>{mod.lede}</p>}
|
|
126
|
+
|
|
127
|
+
<div className={styles.metaRow}>
|
|
128
|
+
<span className={styles.metaBit}>{metaBits.join(' · ')}</span>
|
|
129
|
+
{mod.audience && mod.audience.length > 0 && (
|
|
130
|
+
<span className={styles.metaBit}>
|
|
131
|
+
{mod.audience.map((a) => AUDIENCE_LABELS[a] || a).join(' · ')}
|
|
132
|
+
</span>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
</header>
|
|
136
|
+
|
|
137
|
+
{children && <section className={styles.intro}>{children}</section>}
|
|
138
|
+
|
|
139
|
+
<section className={styles.parts}>
|
|
140
|
+
<h2 className={styles.partsTitle}>In this module</h2>
|
|
141
|
+
<ContentCardGrid columns={1}>
|
|
142
|
+
{mod.parts.map((part) => (
|
|
143
|
+
<ContentCard key={part.slug} {...partToCardProps(part)} />
|
|
144
|
+
))}
|
|
145
|
+
</ContentCardGrid>
|
|
146
|
+
</section>
|
|
147
|
+
</main>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <ModulePage /> styles. Light surface, generous spacing — the page
|
|
3
|
+
* is a small landing, not an article. Echoes the ContentDetailHero
|
|
4
|
+
* structure without the hex visual (the parts-grid below carries the
|
|
5
|
+
* visual weight).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
.hero {
|
|
9
|
+
padding: var(--space-7) 0 var(--space-5);
|
|
10
|
+
max-width: 720px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.eyebrow {
|
|
14
|
+
font-family: var(--conduction-typography-font-family-code);
|
|
15
|
+
font-size: 12px;
|
|
16
|
+
letter-spacing: 0.08em;
|
|
17
|
+
text-transform: uppercase;
|
|
18
|
+
color: var(--c-cobalt-400);
|
|
19
|
+
margin-bottom: var(--space-3);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.title {
|
|
23
|
+
font-size: 36px;
|
|
24
|
+
font-weight: 700;
|
|
25
|
+
letter-spacing: -0.015em;
|
|
26
|
+
color: var(--c-cobalt-900);
|
|
27
|
+
line-height: 1.15;
|
|
28
|
+
margin: 0 0 var(--space-3);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.lede {
|
|
32
|
+
font-size: 18px;
|
|
33
|
+
color: var(--c-cobalt-700);
|
|
34
|
+
line-height: 1.55;
|
|
35
|
+
margin: 0 0 var(--space-4);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.metaRow {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-wrap: wrap;
|
|
41
|
+
gap: var(--space-4);
|
|
42
|
+
font-family: var(--conduction-typography-font-family-code);
|
|
43
|
+
font-size: 12px;
|
|
44
|
+
letter-spacing: 0.04em;
|
|
45
|
+
color: var(--c-cobalt-400);
|
|
46
|
+
text-transform: uppercase;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.metaBit { white-space: nowrap; }
|
|
50
|
+
|
|
51
|
+
.intro {
|
|
52
|
+
padding: var(--space-4) 0 var(--space-5);
|
|
53
|
+
max-width: 720px;
|
|
54
|
+
font-size: 16px;
|
|
55
|
+
color: var(--c-cobalt-700);
|
|
56
|
+
line-height: 1.7;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.parts {
|
|
60
|
+
padding: var(--space-5) 0 var(--space-7);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.partsTitle {
|
|
64
|
+
font-size: 22px;
|
|
65
|
+
font-weight: 700;
|
|
66
|
+
letter-spacing: -0.01em;
|
|
67
|
+
color: var(--c-cobalt-900);
|
|
68
|
+
margin: 0 0 var(--space-4);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.notFound {
|
|
72
|
+
padding: var(--space-7) 0;
|
|
73
|
+
max-width: 600px;
|
|
74
|
+
}
|
|
75
|
+
.notFound h1 {
|
|
76
|
+
font-size: 28px;
|
|
77
|
+
color: var(--c-cobalt-900);
|
|
78
|
+
margin-bottom: var(--space-3);
|
|
79
|
+
}
|
|
80
|
+
.notFound p {
|
|
81
|
+
color: var(--c-cobalt-700);
|
|
82
|
+
line-height: 1.6;
|
|
83
|
+
}
|
|
84
|
+
.notFound code {
|
|
85
|
+
background: var(--c-cobalt-50);
|
|
86
|
+
padding: 2px 6px;
|
|
87
|
+
border-radius: 4px;
|
|
88
|
+
font-family: var(--conduction-typography-font-family-code);
|
|
89
|
+
}
|
|
@@ -5,11 +5,17 @@
|
|
|
5
5
|
|
|
6
6
|
.section {
|
|
7
7
|
background: white;
|
|
8
|
-
padding:
|
|
8
|
+
/* Bottom padding deliberately lighter than top: the diagram's
|
|
9
|
+
orange-dashed flow lines around OpenBuilt extend visually below
|
|
10
|
+
the last text label, so the section already has ~80px of optical
|
|
11
|
+
trailing space from the diagram itself before any padding kicks
|
|
12
|
+
in. Top stays at 96px to keep the rhythm coming in from the
|
|
13
|
+
Hero / StatsStrip above. */
|
|
14
|
+
padding: 96px 64px 32px;
|
|
9
15
|
font-family: var(--conduction-typography-font-family-body);
|
|
10
16
|
}
|
|
11
17
|
@media (max-width: 900px) {
|
|
12
|
-
.section { padding: 64px 24px; }
|
|
18
|
+
.section { padding: 64px 24px 24px; }
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
.inner {
|
package/src/components/index.js
CHANGED
|
@@ -30,6 +30,7 @@ export * from './primitives';
|
|
|
30
30
|
export {default as Hero} from './Hero/Hero.jsx';
|
|
31
31
|
export {default as StatsStrip} from './StatsStrip/StatsStrip.jsx';
|
|
32
32
|
export {default as CtaBanner} from './CtaBanner/CtaBanner.jsx';
|
|
33
|
+
export {default as DownloadPanel} from './DownloadPanel/DownloadPanel.jsx';
|
|
33
34
|
export {default as PlatformOverview} from './PlatformOverview/PlatformOverview.jsx';
|
|
34
35
|
export {default as AppsPreview, AppCard} from './AppsPreview/AppsPreview.jsx';
|
|
35
36
|
|
|
@@ -109,6 +110,23 @@ export {default as ContentDetailHero} from './ContentDetailHero/ContentDetailHer
|
|
|
109
110
|
export {default as AppCrossLinks} from './AppCrossLinks/AppCrossLinks.jsx';
|
|
110
111
|
export {APPS_REGISTRY, APP_SLUGS, APP_LABELS, getApp, getApps} from '../data/apps-registry';
|
|
111
112
|
|
|
113
|
+
/* Academy modules — composite-card and per-module index page that
|
|
114
|
+
group a multi-part academy series (e.g. deskdesk-tutorial,
|
|
115
|
+
openwoo-getting-started) into one entity. ModuleCard renders on
|
|
116
|
+
the academy landing in place of the individual member cards;
|
|
117
|
+
ModulePage wraps /academy/modules/{slug}.mdx index pages. Both
|
|
118
|
+
read frontmatter from the consuming site's `academy-modules`
|
|
119
|
+
plugin via usePluginData('academy-modules'). */
|
|
120
|
+
export {default as ModuleCard} from './ModuleCard/ModuleCard.jsx';
|
|
121
|
+
export {default as ModulePage} from './ModulePage/ModulePage.jsx';
|
|
122
|
+
export {
|
|
123
|
+
AUDIENCES,
|
|
124
|
+
AUDIENCE_LABELS,
|
|
125
|
+
AUDIENCE_PLURAL_LABELS,
|
|
126
|
+
AUDIENCE_BULLET_COLOR,
|
|
127
|
+
AUDIENCE_SHORT_LABELS,
|
|
128
|
+
} from '../data/audience';
|
|
129
|
+
|
|
112
130
|
/* Tutorial-body components. Drop-in replacements for the ad-hoc
|
|
113
131
|
"What you need", "Troubleshooting", and "Next steps" h2 + bullet
|
|
114
132
|
patterns that academy tutorials kept duplicating. Designed for use
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
.avatar {
|
|
16
|
-
width:
|
|
17
|
-
height:
|
|
18
|
-
|
|
16
|
+
width: 22px;
|
|
17
|
+
height: 25px;
|
|
18
|
+
clip-path: var(--hex-pointy-top);
|
|
19
19
|
background: var(--c-cobalt-100);
|
|
20
20
|
color: var(--c-cobalt-700);
|
|
21
21
|
display: inline-flex;
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
width: 100%;
|
|
32
32
|
height: 100%;
|
|
33
33
|
object-fit: cover;
|
|
34
|
+
display: block;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
.name { font-weight: 500; }
|
package/src/css/brand.css
CHANGED
|
@@ -324,15 +324,21 @@ a:not(.navbar__link):not(.footer__link-item):not(.button):hover {
|
|
|
324
324
|
the same orange (no second hover colour) so the visual stays
|
|
325
325
|
stable once the hash is on-screen.
|
|
326
326
|
============================================================ */
|
|
327
|
+
/* `!important` survives the cssnano `mergeRules` pass at build time —
|
|
328
|
+
without it the bare `.hash-link` selector gets dropped from the
|
|
329
|
+
comma list when cssnano merges nearby `.hash-link {opacity:0; ...}`
|
|
330
|
+
rules from Docusaurus, leaving only the pseudo-class variants
|
|
331
|
+
orange and the default-state hash inheriting cobalt from the
|
|
332
|
+
surrounding heading. */
|
|
327
333
|
.hash-link,
|
|
328
334
|
.hash-link:hover,
|
|
329
335
|
.hash-link:focus,
|
|
330
336
|
.hash-link:focus-visible,
|
|
331
337
|
.hash-link:active,
|
|
332
338
|
.hash-link:visited {
|
|
333
|
-
color: var(--c-orange-knvb);
|
|
334
|
-
text-decoration: none;
|
|
335
|
-
border-bottom: 0;
|
|
339
|
+
color: var(--c-orange-knvb) !important;
|
|
340
|
+
text-decoration: none !important;
|
|
341
|
+
border-bottom: 0 !important;
|
|
336
342
|
}
|
|
337
343
|
|
|
338
344
|
/* ============================================================
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @conduction/docusaurus-preset/data/audience
|
|
3
|
+
*
|
|
4
|
+
* Audience taxonomy for academy content. Mirrors the three tiers
|
|
5
|
+
* defined in design-system/CLAUDE.md and preview/identity/audience.html:
|
|
6
|
+
*
|
|
7
|
+
* 1. MKB-beslisser / IT-lead (primary)
|
|
8
|
+
* 2. Overheid-beslisser (secondary)
|
|
9
|
+
* 3. Developer / integrator (tertiary)
|
|
10
|
+
*
|
|
11
|
+
* Used by:
|
|
12
|
+
* - <ContentCard/> renders the audience pill row
|
|
13
|
+
* - <ModuleCard/> same
|
|
14
|
+
* - <FeaturedCard/> same
|
|
15
|
+
* - <ContentTypeFilter/> when reused as the audience filter row
|
|
16
|
+
* - schemas/academy/content.schema.json (mirrored as the audience enum)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const AUDIENCES = ['mkb', 'government', 'developer'];
|
|
20
|
+
|
|
21
|
+
export const AUDIENCE_LABELS = {
|
|
22
|
+
mkb: 'Voor MKB',
|
|
23
|
+
government: 'Voor overheid',
|
|
24
|
+
developer: 'Voor developers',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Plural, sentence-case labels for the chip row. Identical to the
|
|
29
|
+
* singular labels because each one already reads as a collection target
|
|
30
|
+
* ("Voor MKB", "Voor developers"), unlike content types ("Blog" vs
|
|
31
|
+
* "Blogs"). Kept as a separate export for symmetry with
|
|
32
|
+
* CONTENT_TYPE_PLURAL_LABELS so call sites that switch between the two
|
|
33
|
+
* filter rows don't need a branch.
|
|
34
|
+
*/
|
|
35
|
+
export const AUDIENCE_PLURAL_LABELS = AUDIENCE_LABELS;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Bullet (HexBullet) colour for the audience chip. We do NOT brand the
|
|
39
|
+
* audience pills with the orange accent — the "one orange per
|
|
40
|
+
* component" rule reserves orange for the page's single highlighted
|
|
41
|
+
* action. Audience uses three cobalt shades to give each tier a quiet
|
|
42
|
+
* differentiator without competing with the type chip.
|
|
43
|
+
*/
|
|
44
|
+
export const AUDIENCE_BULLET_COLOR = {
|
|
45
|
+
mkb: 'var(--c-blue-cobalt)',
|
|
46
|
+
government: 'var(--c-cobalt-700)',
|
|
47
|
+
developer: 'var(--c-cobalt-400)',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Short labels used when the "For: A, B, C" line composes the
|
|
52
|
+
* audience set into a single sentence (the layout that replaced the
|
|
53
|
+
* audience pill row on ContentCard / ModuleCard / FeaturedCard).
|
|
54
|
+
* Dropping the "Voor" prefix avoids duplicating the leading "For:".
|
|
55
|
+
*/
|
|
56
|
+
export const AUDIENCE_SHORT_LABELS = {
|
|
57
|
+
mkb: 'MKB',
|
|
58
|
+
government: 'Overheid',
|
|
59
|
+
developer: 'Developers',
|
|
60
|
+
};
|