@conduction/docusaurus-preset 3.31.0 → 3.33.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.
@@ -1,7 +1,23 @@
1
1
  /**
2
- * <WidgetShelf /> styles. Header + responsive grid of widget-preview
3
- * cards. Each card has a tinted panel containing the widget mock,
4
- * with title + description below.
2
+ * <WidgetShelf /> styles. Header + widget-preview cards. Each card
3
+ * has a tinted panel containing the widget mock, with title +
4
+ * description below.
5
+ *
6
+ * Two layouts: the default single-row carousel (slow marquee, track
7
+ * duplicated for a seamless loop, paused on hover/focus) and the
8
+ * static responsive grid (carousel={false}, and what
9
+ * prefers-reduced-motion collapses the carousel to).
10
+ *
11
+ * The CSS keyframe marquee is the SSR/no-JS fallback; once the
12
+ * component mounts, the JSX adds .jsDriven to the track and a rAF
13
+ * loop drives the same transform so the prev/next/pause controls can
14
+ * steer it. Controls are cobalt ghost buttons (never orange fill)
15
+ * and are hidden under reduced motion together with the marquee.
16
+ *
17
+ * Auto panels (.auto*): token-built mini mocks for widgets without an
18
+ * explicit panel. Four archetypes (KPI tile, bar chart, list rows,
19
+ * donut gauge); the accent comes from --wsa / --wsa-soft, set inline
20
+ * by the JSX from a hash of the widget title.
5
21
  */
6
22
 
7
23
  .shelf {
@@ -11,7 +27,11 @@
11
27
  font-family: var(--conduction-typography-font-family-body);
12
28
  }
13
29
 
14
- .head { max-width: 70ch; margin: 0 0 var(--space-8); }
30
+ /* The head runs full width so the title can fill the line; only the
31
+ lede (body text) keeps the 70ch reading measure. Capping the whole
32
+ head at 70ch forced titles into an early wrap, which the base
33
+ h1-h6 `text-wrap: balance` then split into two even lines. */
34
+ .head { margin: 0 0 var(--space-8); }
15
35
  .eyebrow {
16
36
  display: inline-flex; align-items: center; gap: var(--space-2);
17
37
  font-family: var(--conduction-typography-font-family-code);
@@ -23,8 +43,9 @@
23
43
  .title {
24
44
  font-size: 36px; font-weight: 700; letter-spacing: -0.02em;
25
45
  line-height: 1.1; color: var(--c-cobalt-900); margin: 0 0 var(--space-3);
46
+ text-wrap: pretty; /* run full width; no artificial two-line balance */
26
47
  }
27
- .lede { font-size: 17px; line-height: 1.5; color: var(--c-cobalt-700); margin: 0; }
48
+ .lede { font-size: 17px; line-height: 1.5; color: var(--c-cobalt-700); margin: 0; max-width: 70ch; }
28
49
 
29
50
  .grid {
30
51
  display: grid;
@@ -34,6 +55,126 @@
34
55
  .cols-3 { grid-template-columns: repeat(3, 1fr); }
35
56
  @media (max-width: 900px) { .grid { grid-template-columns: 1fr; } }
36
57
 
58
+ /* ---- Carousel (default) ----
59
+ *
60
+ * Marquee mechanics: the track holds two identical groups of cards
61
+ * and translates from 0 to -50% on a slow linear loop; because each
62
+ * group carries a trailing padding equal to the card gap, -50% of
63
+ * the track lands exactly one group later and the loop is seamless.
64
+ * The duplicate group is aria-hidden in the JSX so screen readers
65
+ * hear each widget once. Hover or keyboard focus anywhere in the
66
+ * viewport pauses the animation. */
67
+
68
+ .viewport {
69
+ overflow: hidden;
70
+ /* Soft fade at both edges so cards enter/exit gently. */
71
+ -webkit-mask-image: linear-gradient(to right, transparent, black 48px, black calc(100% - 48px), transparent);
72
+ mask-image: linear-gradient(to right, transparent, black 48px, black calc(100% - 48px), transparent);
73
+ }
74
+
75
+ .track {
76
+ display: flex;
77
+ width: max-content;
78
+ animation: wsMarquee var(--ws-speed, 45s) linear infinite;
79
+ }
80
+
81
+ .viewport:hover .track,
82
+ .viewport:focus-within .track {
83
+ animation-play-state: paused;
84
+ }
85
+
86
+ .group {
87
+ display: flex;
88
+ gap: var(--space-5);
89
+ padding-right: var(--space-5);
90
+ }
91
+
92
+ .carousel .card {
93
+ width: 340px;
94
+ flex: 0 0 auto;
95
+ }
96
+ @media (max-width: 700px) { .carousel .card { width: 280px; } }
97
+
98
+ @keyframes wsMarquee {
99
+ from { transform: translateX(0); }
100
+ to { transform: translateX(-50%); }
101
+ }
102
+
103
+ /* Once the rAF loop owns the transform, the keyframe animation must
104
+ let go — otherwise it would overwrite the inline transform. */
105
+ .track.jsDriven { animation: none; }
106
+
107
+ /* ---- Controls (pause/play + prev/next) ----
108
+ *
109
+ * Cobalt ghost buttons per the kit: transparent ground, cobalt ink,
110
+ * cobalt-200 hairline; hover tints, focus ring is a visible cobalt
111
+ * outline. Never orange. Hidden under reduced motion because the
112
+ * static grid has nothing to steer. */
113
+
114
+ .controls {
115
+ display: flex;
116
+ justify-content: flex-end;
117
+ gap: var(--space-2);
118
+ margin-top: var(--space-4);
119
+ }
120
+
121
+ .ctrl {
122
+ appearance: none;
123
+ display: inline-flex;
124
+ align-items: center;
125
+ justify-content: center;
126
+ width: 38px;
127
+ height: 38px;
128
+ padding: 0;
129
+ border: 1px solid var(--c-cobalt-200);
130
+ border-radius: var(--radius-pill);
131
+ background: transparent;
132
+ color: var(--c-blue-cobalt);
133
+ cursor: pointer;
134
+ }
135
+ .ctrl:hover {
136
+ background: var(--c-cobalt-50);
137
+ border-color: var(--c-blue-cobalt);
138
+ }
139
+ .ctrl:focus-visible {
140
+ outline: 2px solid var(--c-blue-cobalt);
141
+ outline-offset: 2px;
142
+ }
143
+ .ctrl svg { width: 16px; height: 16px; display: block; }
144
+ .chev {
145
+ fill: none;
146
+ stroke: currentColor;
147
+ stroke-width: 2.4;
148
+ stroke-linecap: round;
149
+ stroke-linejoin: round;
150
+ }
151
+ .glyph { fill: currentColor; }
152
+
153
+ /* Reduced motion: drop the duplicate copy and render the first group
154
+ as the static wrapping grid — identical to carousel={false}. The
155
+ .cols-2 / .cols-3 class on the group supplies the column template. */
156
+ @media (prefers-reduced-motion: reduce) {
157
+ .viewport {
158
+ overflow: visible;
159
+ -webkit-mask-image: none;
160
+ mask-image: none;
161
+ }
162
+ .controls { display: none; }
163
+ .track { animation: none; display: block; width: 100%; transform: none !important; }
164
+ .group {
165
+ display: grid;
166
+ gap: var(--space-5);
167
+ padding-right: 0;
168
+ width: 100%;
169
+ }
170
+ /* After .group so its display wins the equal-specificity contest. */
171
+ .group.dup { display: none; }
172
+ .carousel .card { width: auto; }
173
+ }
174
+ @media (prefers-reduced-motion: reduce) and (max-width: 900px) {
175
+ .group { grid-template-columns: 1fr; }
176
+ }
177
+
37
178
  .card {
38
179
  background: white;
39
180
  border: 1px solid var(--c-cobalt-100);
@@ -71,3 +212,133 @@
71
212
  margin: 0;
72
213
  padding: var(--space-2) var(--space-5) var(--space-5);
73
214
  }
215
+
216
+ /* ---- Auto-generated mini panels ----
217
+ *
218
+ * White widget tile on the tinted panel ground, same chrome family as
219
+ * the hand-built .w mocks. All greeked (bars, dots, one number); the
220
+ * accent tokens --wsa / --wsa-soft arrive inline from the JSX. */
221
+
222
+ .auto {
223
+ box-sizing: border-box;
224
+ width: 100%;
225
+ max-width: 280px;
226
+ min-height: 124px;
227
+ background: white;
228
+ border: 1px solid var(--c-cobalt-100);
229
+ border-radius: var(--radius-md);
230
+ box-shadow: var(--shadow-1);
231
+ padding: 16px 18px;
232
+ }
233
+
234
+ /* KPI number tile */
235
+ .autoKpi {
236
+ display: flex;
237
+ flex-direction: column;
238
+ justify-content: center;
239
+ gap: 10px;
240
+ }
241
+ .autoHex {
242
+ display: block;
243
+ width: 13px;
244
+ height: 15px;
245
+ clip-path: var(--hex-pointy-top);
246
+ background: var(--wsa);
247
+ }
248
+ .autoKpiValue {
249
+ font-family: var(--conduction-typography-font-family-code);
250
+ font-size: 34px;
251
+ font-weight: 700;
252
+ line-height: 1;
253
+ letter-spacing: -0.01em;
254
+ color: var(--c-cobalt-900);
255
+ }
256
+ .autoKpiBar {
257
+ display: block;
258
+ height: 5px;
259
+ border-radius: 1.5px;
260
+ background: var(--wsa-soft);
261
+ }
262
+
263
+ /* Mini bar chart */
264
+ .autoBars {
265
+ display: flex;
266
+ align-items: flex-end;
267
+ gap: 8px;
268
+ height: 124px;
269
+ }
270
+ .autoBar {
271
+ flex: 1;
272
+ border-radius: 2px 2px 0 0;
273
+ background: var(--c-cobalt-200);
274
+ }
275
+ .autoBarHi { background: var(--wsa); }
276
+
277
+ /* List rows */
278
+ .autoList {
279
+ display: flex;
280
+ flex-direction: column;
281
+ justify-content: center;
282
+ gap: 12px;
283
+ }
284
+ .autoRow {
285
+ display: flex;
286
+ align-items: center;
287
+ gap: 10px;
288
+ }
289
+ .autoDot {
290
+ flex-shrink: 0;
291
+ width: 12px;
292
+ height: 14px;
293
+ clip-path: var(--hex-pointy-top);
294
+ background: var(--c-cobalt-200);
295
+ }
296
+ .autoDotHi { background: var(--wsa); }
297
+ .autoRowBars {
298
+ flex: 1;
299
+ display: flex;
300
+ flex-direction: column;
301
+ gap: 5px;
302
+ }
303
+ .autoRowBar {
304
+ display: block;
305
+ height: 5px;
306
+ border-radius: 1.5px;
307
+ background: var(--c-cobalt-700);
308
+ }
309
+ .autoRowLine {
310
+ display: block;
311
+ width: 86%;
312
+ height: 4px;
313
+ border-radius: 1px;
314
+ background: var(--c-cobalt-100);
315
+ }
316
+
317
+ /* Donut gauge */
318
+ .autoDonut {
319
+ display: flex;
320
+ align-items: center;
321
+ gap: 18px;
322
+ }
323
+ .autoRing {
324
+ flex-shrink: 0;
325
+ width: 84px;
326
+ height: 84px;
327
+ }
328
+ .autoRingTrack {
329
+ fill: none;
330
+ stroke: var(--c-cobalt-100);
331
+ stroke-width: 6;
332
+ }
333
+ .autoRingFill {
334
+ fill: none;
335
+ stroke: var(--wsa);
336
+ stroke-width: 6;
337
+ stroke-linecap: round;
338
+ }
339
+ .autoDonutBars {
340
+ flex: 1;
341
+ display: flex;
342
+ flex-direction: column;
343
+ gap: 6px;
344
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * WidgetShelf.render.test.js — renders the real <WidgetShelf> JSX to
3
+ * static markup (the SSR path Docusaurus takes at build time) and
4
+ * asserts on the output: derived carousel speed, auto-generated
5
+ * panels for widgets without an explicit `panel`, and the carousel
6
+ * controls with their accessible names.
7
+ *
8
+ * Uses the same esbuild-bundle-then-renderToStaticMarkup technique as
9
+ * AiDisclosure.render.test.js / scripts/build-kit.mjs. CSS modules
10
+ * are stubbed with an identity proxy (styles.foo → 'foo'), so class
11
+ * assertions read as the source class names.
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const test = require('node:test');
17
+ const {before, after} = test;
18
+ const assert = require('node:assert/strict');
19
+ const path = require('node:path');
20
+ const fs = require('node:fs/promises');
21
+ const {build} = require('esbuild');
22
+ const React = require('react');
23
+ const {renderToStaticMarkup} = require('react-dom/server');
24
+
25
+ const COMPONENT = path.resolve(__dirname, '..', 'WidgetShelf.jsx');
26
+ const PRESET_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
27
+ const SCRATCH = path.join(PRESET_ROOT, '.tmp-widget-shelf-test');
28
+
29
+ const cssModuleStub = {
30
+ name: 'css-module-stub',
31
+ setup(b) {
32
+ b.onResolve({filter: /\.module\.css$/}, (args) => ({path: args.path, namespace: 'css-stub'}));
33
+ b.onLoad({filter: /.*/, namespace: 'css-stub'}, () => ({
34
+ contents: 'export default new Proxy({}, {get: (_, p) => p});',
35
+ loader: 'js',
36
+ }));
37
+ },
38
+ };
39
+
40
+ let WidgetShelf;
41
+
42
+ before(async () => {
43
+ await fs.mkdir(SCRATCH, {recursive: true});
44
+ const tmpDir = await fs.mkdtemp(path.join(SCRATCH, 'run-'));
45
+ const outFile = path.join(tmpDir, 'bundle.cjs');
46
+ await build({
47
+ entryPoints: [COMPONENT],
48
+ outfile: outFile,
49
+ bundle: true,
50
+ format: 'cjs',
51
+ jsx: 'automatic',
52
+ jsxImportSource: 'react',
53
+ tsconfigRaw: {compilerOptions: {jsx: 'react-jsx', jsxImportSource: 'react'}},
54
+ platform: 'node',
55
+ external: ['react'],
56
+ plugins: [cssModuleStub],
57
+ logLevel: 'warning',
58
+ });
59
+ delete require.cache[require.resolve(outFile)];
60
+ WidgetShelf = require(outFile).default;
61
+ });
62
+
63
+ after(async () => {
64
+ await fs.rm(SCRATCH, {recursive: true, force: true});
65
+ });
66
+
67
+ function render(props) {
68
+ return renderToStaticMarkup(React.createElement(WidgetShelf, props));
69
+ }
70
+
71
+ function makeWidgets(n) {
72
+ return Array.from({length: n}, (_, i) => ({
73
+ title: `Widget number ${i}`,
74
+ desc: `Description ${i}`,
75
+ }));
76
+ }
77
+
78
+ test('SSR smoke: carousel renders every widget twice (live group + aria-hidden dup)', () => {
79
+ const html = render({title: 'Shelf', widgets: makeWidgets(5)});
80
+ assert.equal((html.match(/Widget number 3/g) || []).length, 2);
81
+ assert.match(html, /aria-hidden="true"/);
82
+ });
83
+
84
+ test('default loop duration derives from widget count at 6s per card', () => {
85
+ const html = render({widgets: makeWidgets(15)});
86
+ assert.match(html, /--ws-speed:90s/);
87
+ });
88
+
89
+ test('default loop duration never drops below the 30s floor', () => {
90
+ const html = render({widgets: makeWidgets(3)});
91
+ assert.match(html, /--ws-speed:30s/);
92
+ });
93
+
94
+ test('an explicit speed prop overrides the derived duration', () => {
95
+ const html = render({widgets: makeWidgets(15), speed: 45});
96
+ assert.match(html, /--ws-speed:45s/);
97
+ });
98
+
99
+ test('a widget without a panel gets an auto-generated mini panel', () => {
100
+ const html = render({widgets: [{title: 'Werkvoorraad', desc: 'x'}]});
101
+ assert.match(html, /class="auto auto(Kpi|Bars|List|Donut)"/);
102
+ });
103
+
104
+ test('an explicit panel always wins over the auto panel', () => {
105
+ const html = render({
106
+ widgets: [{title: 'Custom', desc: 'x', panel: React.createElement('div', {className: 'w w-custom'})}],
107
+ });
108
+ assert.match(html, /w-custom/);
109
+ assert.doesNotMatch(html, /class="auto /);
110
+ });
111
+
112
+ test('auto panels are deterministic: same input, same markup', () => {
113
+ const props = {widgets: makeWidgets(8)};
114
+ assert.equal(render(props), render(props));
115
+ });
116
+
117
+ test('auto panels vary: 8 distinct titles hit at least two archetypes and two accent families', () => {
118
+ const html = render({widgets: makeWidgets(8)});
119
+ const kinds = new Set(
120
+ (html.match(/class="auto auto(Kpi|Bars|List|Donut)"/g) || []).map((m) => m),
121
+ );
122
+ assert.ok(kinds.size >= 2, `expected >=2 archetypes, saw ${[...kinds].join(', ')}`);
123
+ const families = new Set(
124
+ (html.match(/--wsa:var\(--c-([a-z]+)-500\)/g) || []).map((m) => m),
125
+ );
126
+ assert.ok(families.size >= 2, `expected >=2 accent families, saw ${[...families].join(', ')}`);
127
+ });
128
+
129
+ test('auto panels never use reserved accent families (coral/orange, gold)', () => {
130
+ // 64 distinct titles sweep the family picker; the hex-family policy
131
+ // reserves coral (KNVB orange) and gold, so neither may ever appear.
132
+ const html = render({widgets: makeWidgets(64)});
133
+ assert.doesNotMatch(html, /--c-coral|--c-gold|--c-orange/);
134
+ });
135
+
136
+ test('carousel renders pause/play and prev/next controls with accessible names', () => {
137
+ const html = render({widgets: makeWidgets(4)});
138
+ assert.match(html, /aria-label="Previous widgets"/);
139
+ assert.match(html, /aria-label="Next widgets"/);
140
+ assert.match(html, /aria-label="Pause"/);
141
+ assert.match(html, /<button type="button"/);
142
+ });
143
+
144
+ test('the static grid (carousel={false}) has no controls and no duplicate group', () => {
145
+ const html = render({widgets: makeWidgets(4), carousel: false});
146
+ assert.doesNotMatch(html, /aria-label="Previous widgets"/);
147
+ assert.doesNotMatch(html, /aria-label="Pause"/);
148
+ assert.equal((html.match(/Widget number 3/g) || []).length, 1);
149
+ });
@@ -77,6 +77,10 @@ export {default as ComposeBlock} from './ComposeBlock/ComposeBlock.jsx';
77
77
  export {default as AppsGrid} from './AppsGrid/AppsGrid.jsx';
78
78
  export {default as AppMock} from './AppMock/AppMock.jsx';
79
79
  export {default as WidgetMock} from './WidgetMock/WidgetMock.jsx';
80
+ export {default as FlowMock} from './FlowMock/FlowMock.jsx';
81
+ export {default as LeafMock} from './LeafMock/LeafMock.jsx';
82
+ export {default as BuildMock} from './BuildMock/BuildMock.jsx';
83
+ export {default as KanbanMock} from './KanbanMock/KanbanMock.jsx';
80
84
  export {default as SidebarMock} from './SidebarMock/SidebarMock.jsx';
81
85
  export {default as MockScene} from './MockScene/MockScene.jsx';
82
86
  export {default as IntegrationIcon} from './IntegrationIcon/IntegrationIcon.jsx';
@@ -27,6 +27,12 @@
27
27
  letter-spacing: -0.02em;
28
28
  color: var(--c-cobalt-900);
29
29
  margin: 0;
30
+ /* Section titles run the full line before wrapping. The kit's base
31
+ h1-h6 rule sets `text-wrap: balance`, which splits a title into
32
+ two near-equal lines even when it fits comfortably on one; the
33
+ PO decision is that section titles fill the available width.
34
+ `pretty` keeps orphan control without the artificial balance. */
35
+ text-wrap: pretty;
30
36
  }
31
37
  @media (max-width: 700px) { .title { font-size: 32px; } }
32
38