@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/docusaurus-preset",
3
- "version": "3.15.3",
3
+ "version": "3.16.1",
4
4
  "scripts": {
5
5
  "prepack": "node scripts/prepack-bundle-css.js"
6
6
  },
@@ -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
- CONTENT_TYPE_LABELS,
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
- const typeBullet = contentType
85
- ? CONTENT_TYPE_BULLET_COLOR[contentType]
86
- : undefined;
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={[styles.thumbPanel, panelToneClass].join(' ')}>
91
- {thumbProps.src
92
- ? <HexThumbnail size={thumbProps.size || 'md'} tone={thumbProps.tone || 'cobalt'} src={thumbProps.src} alt={thumbProps.alt} />
93
- : <HexThumbnail size={thumbProps.size || 'md'} tone={thumbProps.tone || 'cobalt'}>{thumbProps.icon}</HexThumbnail>}
94
- </div>
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
- <div className={styles.body}>
97
- {(author || date) && (
98
- <AuthorByline
99
- name={author && author.name}
100
- avatarSrc={author && author.avatarSrc}
101
- initials={author && author.initials}
102
- date={date}
103
- dateLabel={dateLabel}
104
- locale={locale}
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
- {(typeLabel || tags.length > 0) && (
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: center;
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,6 +149,33 @@
76
149
  line-height: 1.55;
77
150
  }
78
151
 
152
+ .meta {
153
+ display: flex;
154
+ flex-wrap: wrap;
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;
177
+ }
178
+
79
179
  .tags {
80
180
  display: flex;
81
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 {