@conduction/docusaurus-preset 3.15.3 → 3.16.1

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.
@@ -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: 96px 64px;
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 {
@@ -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: 24px;
17
- height: 24px;
18
- border-radius: 50%;
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; }
@@ -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
+ };