@conduction/docusaurus-preset 3.33.0 → 3.35.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/ANIMATION_ROADMAP.md +129 -0
- package/MISSING_COMPONENTS.md +15 -5
- package/package.json +3 -2
- package/src/components/AgentTrace/AgentTrace.jsx +26 -5
- package/src/components/AgentTrace/AgentTrace.module.css +41 -2
- package/src/components/AgentTrace/__tests__/AgentTrace.render.test.js +108 -0
- package/src/components/AppMock/AppMock.jsx +14 -5
- package/src/components/AppMock/AppMock.module.css +1702 -0
- package/src/components/AppMock/__tests__/AppMock.render.test.js +329 -0
- package/src/components/AppMock/variants/DeciDeskMock.jsx +46 -5
- package/src/components/AppMock/variants/DocuDeskMock.jsx +39 -9
- package/src/components/AppMock/variants/DoriathMock.jsx +28 -13
- package/src/components/AppMock/variants/HermiqMock.jsx +35 -15
- package/src/components/AppMock/variants/HrmqMock.jsx +46 -28
- package/src/components/AppMock/variants/LarpingAppMock.jsx +36 -7
- package/src/components/AppMock/variants/LaunchPadBiMock.jsx +12 -5
- package/src/components/AppMock/variants/LaunchPadTilesMock.jsx +6 -1
- package/src/components/AppMock/variants/LaunchPadWidgetsMock.jsx +17 -8
- package/src/components/AppMock/variants/NLDesignMock.jsx +15 -4
- package/src/components/AppMock/variants/OpenBuildMock.jsx +20 -5
- package/src/components/AppMock/variants/OpenCatalogiMock.jsx +23 -4
- package/src/components/AppMock/variants/OpenConnectorMock.jsx +25 -3
- package/src/components/AppMock/variants/OpenRegisterMock.jsx +13 -2
- package/src/components/AppMock/variants/PipelinQMock.jsx +12 -2
- package/src/components/AppMock/variants/PortaliqMock.jsx +21 -9
- package/src/components/AppMock/variants/ProcestMock.jsx +34 -7
- package/src/components/AppMock/variants/ScholiqMock.jsx +12 -6
- package/src/components/AppMock/variants/ShillinqMock.jsx +33 -17
- package/src/components/AppMock/variants/SoftwareCatalogMock.jsx +15 -5
- package/src/components/AppMock/variants/ZaakAfhandelAppMock.jsx +19 -4
- package/src/components/FlowMock/FlowMock.jsx +5 -3
- package/src/components/FlowMock/FlowMock.module.css +36 -9
- package/src/components/FlowMock/__tests__/FlowMock.render.test.js +83 -0
- package/src/components/LeafMock/LeafMock.jsx +23 -8
- package/src/components/LeafMock/LeafMock.module.css +123 -0
- package/src/components/LeafMock/__tests__/LeafMock.render.test.js +105 -0
- package/src/components/MockScene/MockScene.jsx +16 -2
- package/src/components/MockScene/MockScene.module.css +22 -2
- package/src/components/MockScene/__tests__/MockScene.render.test.js +101 -0
- package/src/components/SidebarMock/SidebarMock.jsx +29 -2
- package/src/components/SidebarMock/__tests__/SidebarMock.render.test.js +115 -0
- package/src/components/WidgetMock/WidgetMock.jsx +35 -14
- package/src/components/WidgetMock/__tests__/WidgetMock.render.test.js +125 -0
- package/src/components/WidgetMock/variants/DocuDeskAnonymise.jsx +6 -1
|
@@ -8,26 +8,39 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Same abstraction level as <AppMock> (honeycomb.io/technologies/*):
|
|
10
10
|
* greeked content, tokens only, no real text beyond short labels.
|
|
11
|
-
* Four variants
|
|
11
|
+
* Four variants, each with a small looping animation on the shared
|
|
12
|
+
* 6s timeline (--lm-dur):
|
|
12
13
|
*
|
|
13
14
|
* - contacts: address-book rows with pointy-top hex avatars and a
|
|
14
|
-
* mint sync arrow to a phone silhouette
|
|
15
|
+
* mint sync arrow to a phone silhouette. The arrow pulses and the
|
|
16
|
+
* phone rows arrive in a stagger (the sync lands on the phone)
|
|
15
17
|
* - calendar: mini month grid with one highlighted day and a
|
|
16
|
-
* linked-event chip (lavender bar)
|
|
18
|
+
* linked-event chip (lavender bar). The day pops highlighted,
|
|
19
|
+
* then the event chip slides in beneath
|
|
17
20
|
* - mail: message list with one highlighted row carrying a
|
|
18
|
-
* mint "create from email" action chip
|
|
19
|
-
*
|
|
21
|
+
* mint "create from email" action chip. The row highlights, then
|
|
22
|
+
* the chip pops with an overshoot
|
|
23
|
+
* - files: folder/file rows with a link-to-record chip (forest).
|
|
24
|
+
* The linked row slides in, the chip pops, and its link icon
|
|
25
|
+
* draws itself (pathLength dash technique)
|
|
26
|
+
*
|
|
27
|
+
* `running={false}` (the `.static` frame class) and the user's
|
|
28
|
+
* prefers-reduced-motion setting freeze each leaf to its meaningful
|
|
29
|
+
* end state: synced rows, highlighted day + chip, highlighted mail
|
|
30
|
+
* row + chip, linked file row.
|
|
20
31
|
*
|
|
21
32
|
* Usage:
|
|
22
33
|
*
|
|
23
34
|
* <LeafMock leaf="contacts" />
|
|
24
35
|
* <LeafMock leaf="calendar" caption size="sm" />
|
|
36
|
+
* <LeafMock leaf="mail" running={false} />
|
|
25
37
|
*
|
|
26
38
|
* Props:
|
|
27
39
|
* - leaf: 'contacts' | 'calendar' | 'mail' | 'files' (required)
|
|
28
40
|
* - size: 'sm' | 'md' (default) — frame width
|
|
29
41
|
* - caption: string | true — small label below the frame;
|
|
30
42
|
* `true` uses the variant's default label
|
|
43
|
+
* - running: boolean (default true) — false freezes the leaf
|
|
31
44
|
* - className: string
|
|
32
45
|
*/
|
|
33
46
|
|
|
@@ -126,7 +139,9 @@ function FilesLeaf() {
|
|
|
126
139
|
{i === 1 && (
|
|
127
140
|
<span className={[styles.actionChip, styles.recordChip].join(' ')}>
|
|
128
141
|
<svg className={styles.linkIcon} viewBox="0 0 24 24" aria-hidden="true">
|
|
129
|
-
|
|
142
|
+
{/* pathLength="1" so the draw animation's dash
|
|
143
|
+
arithmetic is geometry-independent. */}
|
|
144
|
+
<path pathLength="1" d="M10 14a4 4 0 0 0 6 0l3-3a4 4 0 0 0-6-6l-1.5 1.5M14 10a4 4 0 0 0-6 0l-3 3a4 4 0 0 0 6 6L12.5 17.5" />
|
|
130
145
|
</svg>
|
|
131
146
|
<Greek w="26px" tone="chip" />
|
|
132
147
|
</span>
|
|
@@ -144,7 +159,7 @@ const VARIANTS = {
|
|
|
144
159
|
files: {Component: FilesLeaf, label: 'Files leaf'},
|
|
145
160
|
};
|
|
146
161
|
|
|
147
|
-
export default function LeafMock({leaf, size = 'md', caption, className}) {
|
|
162
|
+
export default function LeafMock({leaf, size = 'md', caption, running = true, className}) {
|
|
148
163
|
const variant = VARIANTS[leaf];
|
|
149
164
|
if (!variant) {
|
|
150
165
|
return (
|
|
@@ -159,7 +174,7 @@ export default function LeafMock({leaf, size = 'md', caption, className}) {
|
|
|
159
174
|
return (
|
|
160
175
|
<div className={styles.lm}>
|
|
161
176
|
<figure className={[styles.figure, className].filter(Boolean).join(' ')}>
|
|
162
|
-
<div className={[styles.frame, styles[`size-${size}`]].filter(Boolean).join(' ')}>
|
|
177
|
+
<div className={[styles.frame, styles[`size-${size}`], !running && styles.static].filter(Boolean).join(' ')}>
|
|
163
178
|
<Component />
|
|
164
179
|
</div>
|
|
165
180
|
{caption && (
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* Tokens only. Accent discipline per leaf: contacts sync = mint,
|
|
7
7
|
* calendar event bar = lavender, mail action chip = mint, files
|
|
8
8
|
* record chip = forest. No orange anywhere in this component.
|
|
9
|
+
*
|
|
10
|
+
* All four leaves animate on the shared --lm-dur timeline (6s),
|
|
11
|
+
* KanbanMock conventions: base styles are the meaningful end state,
|
|
12
|
+
* loops reset by opacity swap, `.static` (running={false}) and
|
|
13
|
+
* prefers-reduced-motion freeze to the base styles.
|
|
9
14
|
*/
|
|
10
15
|
|
|
11
16
|
.lm { display: contents; }
|
|
@@ -20,6 +25,7 @@
|
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
.lm .frame {
|
|
28
|
+
--lm-dur: 6s;
|
|
23
29
|
width: 100%;
|
|
24
30
|
max-width: 360px;
|
|
25
31
|
aspect-ratio: 4 / 3;
|
|
@@ -235,3 +241,120 @@
|
|
|
235
241
|
}
|
|
236
242
|
.lm .fileLines { flex: 1; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
|
237
243
|
.lm .recordChip { background: var(--c-forest-500); }
|
|
244
|
+
|
|
245
|
+
/* ======================================================================
|
|
246
|
+
Animations — one shared 6s timeline (--lm-dur) per leaf
|
|
247
|
+
====================================================================== */
|
|
248
|
+
|
|
249
|
+
/* ---- Contacts: the sync arrow double-pulses, then the contact data
|
|
250
|
+
lands on the phone as three staggered rows. ---- */
|
|
251
|
+
|
|
252
|
+
.lm .syncArrow { animation: lmSyncPulse var(--lm-dur) ease-in-out infinite; }
|
|
253
|
+
@keyframes lmSyncPulse {
|
|
254
|
+
0%, 6% { transform: scale(1); }
|
|
255
|
+
12% { transform: scale(1.18); }
|
|
256
|
+
18% { transform: scale(1); }
|
|
257
|
+
26% { transform: scale(1.18); }
|
|
258
|
+
32%, 100% { transform: scale(1); }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.lm .phoneRow { animation: lmPhoneRow var(--lm-dur) ease-out infinite both; }
|
|
262
|
+
.lm .phoneRow:nth-child(2) { animation-delay: 0.35s; }
|
|
263
|
+
.lm .phoneRow:nth-child(3) { animation-delay: 0.7s; }
|
|
264
|
+
@keyframes lmPhoneRow {
|
|
265
|
+
0%, 18% { opacity: 0; transform: translateX(-4px); }
|
|
266
|
+
26%, 92% { opacity: 1; transform: none; }
|
|
267
|
+
97%, 100% { opacity: 0; transform: none; }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* ---- Calendar: the linked day pops highlighted, then the event chip
|
|
271
|
+
slides in beneath the month grid. ---- */
|
|
272
|
+
|
|
273
|
+
.lm .dayActive { animation: lmDayPop var(--lm-dur) ease-in-out infinite; }
|
|
274
|
+
@keyframes lmDayPop {
|
|
275
|
+
0%, 8% { background-color: var(--c-cobalt-50); border-color: var(--c-cobalt-100); transform: scale(1); }
|
|
276
|
+
16% { background-color: var(--c-lavender-300); border-color: var(--c-lavender-500); transform: scale(1.35); }
|
|
277
|
+
22%, 90% { background-color: var(--c-lavender-300); border-color: var(--c-lavender-500); transform: scale(1); }
|
|
278
|
+
98%, 100% { background-color: var(--c-cobalt-50); border-color: var(--c-cobalt-100); transform: scale(1); }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.lm .eventChip { animation: lmChipSlide var(--lm-dur) ease-out infinite both; }
|
|
282
|
+
@keyframes lmChipSlide {
|
|
283
|
+
0%, 22% { opacity: 0; transform: translateY(8px); }
|
|
284
|
+
32%, 92% { opacity: 1; transform: none; }
|
|
285
|
+
98%, 100% { opacity: 0; transform: none; }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/* ---- Mail: the middle row highlights, then the create-from-email
|
|
289
|
+
chip pops with an overshoot. ---- */
|
|
290
|
+
|
|
291
|
+
.lm .mailRowActive { animation: lmRowHl var(--lm-dur) ease-in-out infinite; }
|
|
292
|
+
@keyframes lmRowHl {
|
|
293
|
+
0%, 10% { background-color: transparent; border-color: transparent; }
|
|
294
|
+
20%, 90% { background-color: var(--c-cobalt-50); border-color: var(--c-cobalt-100); }
|
|
295
|
+
98%, 100% { background-color: transparent; border-color: transparent; }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.lm .mail .actionChip { transform-origin: center; animation: lmChipPop var(--lm-dur) ease-in-out infinite both; }
|
|
299
|
+
@keyframes lmChipPop {
|
|
300
|
+
0%, 26% { opacity: 0; transform: scale(0); }
|
|
301
|
+
34% { opacity: 1; transform: scale(1.18); }
|
|
302
|
+
39%, 90% { opacity: 1; transform: scale(1); }
|
|
303
|
+
97%, 100% { opacity: 0; transform: scale(1); }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/* ---- Files: the linked row slides in, the record chip pops, and its
|
|
307
|
+
link icon draws itself (the path carries pathLength="1"). ---- */
|
|
308
|
+
|
|
309
|
+
.lm .files .fileRow:nth-child(2) { animation: lmFileRow var(--lm-dur) ease-out infinite both; }
|
|
310
|
+
@keyframes lmFileRow {
|
|
311
|
+
0%, 8% { opacity: 0; transform: translateY(-4px); }
|
|
312
|
+
16%, 92% { opacity: 1; transform: none; }
|
|
313
|
+
98%, 100% { opacity: 0; transform: none; }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.lm .files .recordChip { transform-origin: center; animation: lmChipPop var(--lm-dur) ease-in-out infinite both; }
|
|
317
|
+
.lm .files .recordChip .linkIcon path {
|
|
318
|
+
stroke-dasharray: 1;
|
|
319
|
+
stroke-dashoffset: 1;
|
|
320
|
+
animation: lmLinkDraw var(--lm-dur) ease-in-out infinite both;
|
|
321
|
+
}
|
|
322
|
+
@keyframes lmLinkDraw {
|
|
323
|
+
0%, 34% { stroke-dashoffset: 1; }
|
|
324
|
+
56%, 100% { stroke-dashoffset: 0; }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* ======================================================================
|
|
328
|
+
Static frames — running={false} adds `.static` to `.lm .frame`; the
|
|
329
|
+
media query mirrors it for prefers-reduced-motion. Base styles are
|
|
330
|
+
the end state, so freezing is animation: none plus the icon-draw
|
|
331
|
+
dash reset.
|
|
332
|
+
====================================================================== */
|
|
333
|
+
|
|
334
|
+
.lm .static .syncArrow,
|
|
335
|
+
.lm .static .phoneRow,
|
|
336
|
+
.lm .static .dayActive,
|
|
337
|
+
.lm .static .eventChip,
|
|
338
|
+
.lm .static .mailRowActive,
|
|
339
|
+
.lm .static .mail .actionChip,
|
|
340
|
+
.lm .static .files .fileRow:nth-child(2),
|
|
341
|
+
.lm .static .files .recordChip,
|
|
342
|
+
.lm .static .files .recordChip .linkIcon path {
|
|
343
|
+
animation: none;
|
|
344
|
+
}
|
|
345
|
+
.lm .static .files .recordChip .linkIcon path { stroke-dasharray: none; stroke-dashoffset: 0; }
|
|
346
|
+
|
|
347
|
+
@media (prefers-reduced-motion: reduce) {
|
|
348
|
+
.lm .syncArrow,
|
|
349
|
+
.lm .phoneRow,
|
|
350
|
+
.lm .dayActive,
|
|
351
|
+
.lm .eventChip,
|
|
352
|
+
.lm .mailRowActive,
|
|
353
|
+
.lm .mail .actionChip,
|
|
354
|
+
.lm .files .fileRow:nth-child(2),
|
|
355
|
+
.lm .files .recordChip,
|
|
356
|
+
.lm .files .recordChip .linkIcon path {
|
|
357
|
+
animation: none;
|
|
358
|
+
}
|
|
359
|
+
.lm .files .recordChip .linkIcon path { stroke-dasharray: none; stroke-dashoffset: 0; }
|
|
360
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LeafMock.render.test.js — SSR-renders the real <LeafMock> for each
|
|
3
|
+
* of its four leaves and asserts the animation hooks and the P0
|
|
4
|
+
* `running` prop plumbing (static class on the frame). Same
|
|
5
|
+
* esbuild-bundle-then-renderToStaticMarkup harness as
|
|
6
|
+
* KanbanMock.render.test.js; CSS modules stubbed to identity.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const test = require('node:test');
|
|
12
|
+
const {before, after} = test;
|
|
13
|
+
const assert = require('node:assert/strict');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
const fs = require('node:fs/promises');
|
|
16
|
+
const {build} = require('esbuild');
|
|
17
|
+
const React = require('react');
|
|
18
|
+
const {renderToStaticMarkup} = require('react-dom/server');
|
|
19
|
+
|
|
20
|
+
const COMPONENT = path.resolve(__dirname, '..', 'LeafMock.jsx');
|
|
21
|
+
const PRESET_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
|
22
|
+
const SCRATCH = path.join(PRESET_ROOT, '.tmp-leaf-mock-test');
|
|
23
|
+
|
|
24
|
+
const LEAVES = ['contacts', 'calendar', 'mail', 'files'];
|
|
25
|
+
|
|
26
|
+
const cssModuleStub = {
|
|
27
|
+
name: 'css-module-stub',
|
|
28
|
+
setup(b) {
|
|
29
|
+
b.onResolve({filter: /\.module\.css$/}, (args) => ({path: args.path, namespace: 'css-stub'}));
|
|
30
|
+
b.onLoad({filter: /.*/, namespace: 'css-stub'}, () => ({
|
|
31
|
+
contents: 'export default new Proxy({}, {get: (_, p) => p});',
|
|
32
|
+
loader: 'js',
|
|
33
|
+
}));
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let LeafMock;
|
|
38
|
+
|
|
39
|
+
before(async () => {
|
|
40
|
+
await fs.mkdir(SCRATCH, {recursive: true});
|
|
41
|
+
const tmpDir = await fs.mkdtemp(path.join(SCRATCH, 'run-'));
|
|
42
|
+
const outFile = path.join(tmpDir, 'bundle.cjs');
|
|
43
|
+
await build({
|
|
44
|
+
entryPoints: [COMPONENT],
|
|
45
|
+
outfile: outFile,
|
|
46
|
+
bundle: true,
|
|
47
|
+
format: 'cjs',
|
|
48
|
+
jsx: 'automatic',
|
|
49
|
+
jsxImportSource: 'react',
|
|
50
|
+
tsconfigRaw: {compilerOptions: {jsx: 'react-jsx', jsxImportSource: 'react'}},
|
|
51
|
+
platform: 'node',
|
|
52
|
+
external: ['react'],
|
|
53
|
+
plugins: [cssModuleStub],
|
|
54
|
+
logLevel: 'warning',
|
|
55
|
+
});
|
|
56
|
+
delete require.cache[require.resolve(outFile)];
|
|
57
|
+
LeafMock = require(outFile).default;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
after(async () => {
|
|
61
|
+
await fs.rm(SCRATCH, {recursive: true, force: true});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
function render(props) {
|
|
65
|
+
return renderToStaticMarkup(React.createElement(LeafMock, props));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
test('SSR smoke: every leaf renders a frame, none falls through to Unknown leaf', () => {
|
|
69
|
+
for (const leaf of LEAVES) {
|
|
70
|
+
const html = render({leaf});
|
|
71
|
+
assert.match(html, /class="frame size-md"/, `${leaf}: frame missing`);
|
|
72
|
+
assert.doesNotMatch(html, /Unknown leaf/, `${leaf}: fell through`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('P0 plumbing: running={false} adds the static class on every leaf; default omits it', () => {
|
|
77
|
+
for (const leaf of LEAVES) {
|
|
78
|
+
assert.match(render({leaf, running: false}), /class="frame size-md static"/, `${leaf}: no static frame`);
|
|
79
|
+
assert.doesNotMatch(render({leaf}), /\bstatic\b/, `${leaf}: static leaked into the running frame`);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('contacts: sync arrow and three phone rows (the staggered landing targets)', () => {
|
|
84
|
+
const html = render({leaf: 'contacts'});
|
|
85
|
+
assert.match(html, /class="syncArrow"/);
|
|
86
|
+
assert.equal((html.match(/class="phoneRow"/g) || []).length, 3);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('calendar: active day and the sliding event chip', () => {
|
|
90
|
+
const html = render({leaf: 'calendar'});
|
|
91
|
+
assert.match(html, /day dayActive/);
|
|
92
|
+
assert.match(html, /class="eventChip"/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('mail: highlighted row carries the popping action chip', () => {
|
|
96
|
+
const html = render({leaf: 'mail'});
|
|
97
|
+
assert.match(html, /mailRow mailRowActive/);
|
|
98
|
+
assert.match(html, /class="actionChip"/);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('files: the record chip link icon carries pathLength="1" for the draw animation', () => {
|
|
102
|
+
const html = render({leaf: 'files'});
|
|
103
|
+
assert.match(html, /actionChip recordChip/);
|
|
104
|
+
assert.match(html, /pathLength="1"/);
|
|
105
|
+
});
|
|
@@ -44,6 +44,12 @@
|
|
|
44
44
|
* - items: Array of item descriptors (see above)
|
|
45
45
|
* - width: scene width in px (default 960)
|
|
46
46
|
* - height: scene height in px (default 420)
|
|
47
|
+
* - running: boolean (default true) — the scene assembles: items
|
|
48
|
+
* land one by one on a 220ms stagger (opacity + translateY only,
|
|
49
|
+
* so the inline left/top positioning is never fought), hold, and
|
|
50
|
+
* fade for the loop reset. The loop period derives from the item
|
|
51
|
+
* count (--ms-dur, set inline). `running={false}` and
|
|
52
|
+
* prefers-reduced-motion show the assembled scene.
|
|
47
53
|
* - className: string
|
|
48
54
|
*/
|
|
49
55
|
|
|
@@ -64,16 +70,24 @@ function renderItem(item) {
|
|
|
64
70
|
return item.node || null;
|
|
65
71
|
}
|
|
66
72
|
|
|
67
|
-
export default function MockScene({ items = [], width = 960, height = 420, className }) {
|
|
73
|
+
export default function MockScene({ items = [], width = 960, height = 420, running = true, className }) {
|
|
74
|
+
/* Landing timing: item i arrives at i × 220ms; ~5s hold after the
|
|
75
|
+
last landing, then a fade window before the loop restarts. */
|
|
76
|
+
const STEP = 220;
|
|
77
|
+
const duration = items.length * STEP + 5600;
|
|
68
78
|
return (
|
|
69
79
|
<div className={[amStyles.am, styles.scene, className].filter(Boolean).join(' ')}>
|
|
70
|
-
<div
|
|
80
|
+
<div
|
|
81
|
+
className={[styles.sceneFrame, !running && styles.static].filter(Boolean).join(' ')}
|
|
82
|
+
style={{ width, height, '--ms-dur': `${duration}ms` }}
|
|
83
|
+
>
|
|
71
84
|
{items.map((item, i) => {
|
|
72
85
|
const wrapperStyle = {
|
|
73
86
|
...(item.style || {}),
|
|
74
87
|
left: item.x || 0,
|
|
75
88
|
top: item.y || 0,
|
|
76
89
|
zIndex: item.z != null ? item.z : i,
|
|
90
|
+
animationDelay: `${i * STEP}ms`,
|
|
77
91
|
};
|
|
78
92
|
return (
|
|
79
93
|
<div key={i} className={styles.sceneItem} style={wrapperStyle}>
|
|
@@ -21,8 +21,28 @@
|
|
|
21
21
|
|
|
22
22
|
.am .sceneItem {
|
|
23
23
|
position: absolute;
|
|
24
|
-
/* `left`, `top`,
|
|
25
|
-
|
|
24
|
+
/* `left`, `top`, optionally `z-index`, and the landing
|
|
25
|
+
animation-delay (i × 220ms) are set inline by the component from
|
|
26
|
+
the item's x/y/z values and index. The landing is transform +
|
|
27
|
+
opacity only — the inline left/top positioning is never fought.
|
|
28
|
+
0% and 100% are both hidden, so the staggered delays stay
|
|
29
|
+
seamless in steady state; `both` keeps a delayed item on its
|
|
30
|
+
hidden first frame before the initial landing. */
|
|
31
|
+
animation: msLand var(--ms-dur, 8s) cubic-bezier(0.2, 0.7, 0.3, 1) infinite both;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@keyframes msLand {
|
|
35
|
+
0% { opacity: 0; transform: translateY(16px); }
|
|
36
|
+
4% { opacity: 1; transform: none; }
|
|
37
|
+
94% { opacity: 1; transform: none; }
|
|
38
|
+
99%, 100% { opacity: 0; transform: none; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Static scene (running={false} and reduced motion): assembled. */
|
|
42
|
+
.am .static .sceneItem { animation: none; }
|
|
43
|
+
|
|
44
|
+
@media (prefers-reduced-motion: reduce) {
|
|
45
|
+
.am .sceneItem { animation: none; }
|
|
26
46
|
}
|
|
27
47
|
|
|
28
48
|
/* Widgets inside a scene drop their LaunchPad-style cobalt-900 dashboard
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MockScene.render.test.js — SSR-renders the real <MockScene> and
|
|
3
|
+
* asserts the landing-stagger wiring: inline left/top positioning is
|
|
4
|
+
* preserved untouched (the animation is transform-only), each item
|
|
5
|
+
* carries its 220ms-stagger animation-delay, the loop period --ms-dur
|
|
6
|
+
* derives from the item count, and running={false} puts the static
|
|
7
|
+
* class on the scene frame. Same esbuild harness as
|
|
8
|
+
* KanbanMock.render.test.js; CSS modules stubbed to identity.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const test = require('node:test');
|
|
14
|
+
const {before, after} = test;
|
|
15
|
+
const assert = require('node:assert/strict');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
const fs = require('node:fs/promises');
|
|
18
|
+
const {build} = require('esbuild');
|
|
19
|
+
const React = require('react');
|
|
20
|
+
const {renderToStaticMarkup} = require('react-dom/server');
|
|
21
|
+
|
|
22
|
+
const COMPONENT = path.resolve(__dirname, '..', 'MockScene.jsx');
|
|
23
|
+
const PRESET_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
|
24
|
+
const SCRATCH = path.join(PRESET_ROOT, '.tmp-mock-scene-test');
|
|
25
|
+
|
|
26
|
+
const ITEMS = [
|
|
27
|
+
{type: 'widget', kind: 'nextcloud-mail', x: 0, y: 0, size: 'sm'},
|
|
28
|
+
{type: 'sidebar', kind: 'openregister-metadata', x: 220, y: 30, size: 'md'},
|
|
29
|
+
{type: 'widget', kind: 'openregister-activity', x: 540, y: 80, size: 'md'},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const cssModuleStub = {
|
|
33
|
+
name: 'css-module-stub',
|
|
34
|
+
setup(b) {
|
|
35
|
+
b.onResolve({filter: /\.module\.css$/}, (args) => ({path: args.path, namespace: 'css-stub'}));
|
|
36
|
+
b.onLoad({filter: /.*/, namespace: 'css-stub'}, () => ({
|
|
37
|
+
contents: 'export default new Proxy({}, {get: (_, p) => p});',
|
|
38
|
+
loader: 'js',
|
|
39
|
+
}));
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
let MockScene;
|
|
44
|
+
|
|
45
|
+
before(async () => {
|
|
46
|
+
await fs.mkdir(SCRATCH, {recursive: true});
|
|
47
|
+
const tmpDir = await fs.mkdtemp(path.join(SCRATCH, 'run-'));
|
|
48
|
+
const outFile = path.join(tmpDir, 'bundle.cjs');
|
|
49
|
+
await build({
|
|
50
|
+
entryPoints: [COMPONENT],
|
|
51
|
+
outfile: outFile,
|
|
52
|
+
bundle: true,
|
|
53
|
+
format: 'cjs',
|
|
54
|
+
jsx: 'automatic',
|
|
55
|
+
jsxImportSource: 'react',
|
|
56
|
+
tsconfigRaw: {compilerOptions: {jsx: 'react-jsx', jsxImportSource: 'react'}},
|
|
57
|
+
platform: 'node',
|
|
58
|
+
external: ['react'],
|
|
59
|
+
plugins: [cssModuleStub],
|
|
60
|
+
logLevel: 'warning',
|
|
61
|
+
});
|
|
62
|
+
delete require.cache[require.resolve(outFile)];
|
|
63
|
+
MockScene = require(outFile).default;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
after(async () => {
|
|
67
|
+
await fs.rm(SCRATCH, {recursive: true, force: true});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
function render(props) {
|
|
71
|
+
return renderToStaticMarkup(React.createElement(MockScene, props));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
test('SSR smoke: renders the scene frame with one wrapper per item', () => {
|
|
75
|
+
const html = render({items: ITEMS});
|
|
76
|
+
assert.match(html, /class="sceneFrame"/);
|
|
77
|
+
assert.equal((html.match(/class="sceneItem"/g) || []).length, ITEMS.length);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('positioning: inline left/top from item x/y are preserved (animation is transform-only)', () => {
|
|
81
|
+
const html = render({items: ITEMS});
|
|
82
|
+
assert.match(html, /left:220px;top:30px/);
|
|
83
|
+
assert.match(html, /left:540px;top:80px/);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('landing stagger: each item carries its 220ms-step animation-delay', () => {
|
|
87
|
+
const html = render({items: ITEMS});
|
|
88
|
+
for (let i = 0; i < ITEMS.length; i++) {
|
|
89
|
+
assert.match(html, new RegExp(`animation-delay:${i * 220}ms`), `item ${i}: missing delay`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('loop period: --ms-dur on the frame derives from the item count (+ hold)', () => {
|
|
94
|
+
const html = render({items: ITEMS});
|
|
95
|
+
assert.match(html, new RegExp(`--ms-dur:${ITEMS.length * 220 + 5600}ms`));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('P0 plumbing: running={false} adds the static class to the scene frame; default omits it', () => {
|
|
99
|
+
assert.match(render({items: ITEMS, running: false}), /class="sceneFrame static"/);
|
|
100
|
+
assert.doesNotMatch(render({items: ITEMS}), /sceneFrame static/);
|
|
101
|
+
});
|
|
@@ -53,7 +53,20 @@
|
|
|
53
53
|
* .smFrame chrome so the
|
|
54
54
|
* panel slots into
|
|
55
55
|
* AppMock's .body layout
|
|
56
|
+
* - running: boolean (default true) — false freezes the panel
|
|
57
|
+
* animation on its settled
|
|
58
|
+
* end state (same rendering
|
|
59
|
+
* prefers-reduced-motion
|
|
60
|
+
* gives)
|
|
56
61
|
* - className: string
|
|
62
|
+
*
|
|
63
|
+
* Animation (wave 5, `.sbLive` in AppMock.module.css): the panel
|
|
64
|
+
* slides in from the right, the head title/description wipe in, the
|
|
65
|
+
* active-tab underline travels from the neighbouring tab to the
|
|
66
|
+
* active one — positioned by the `--sb-ix` / `--sb-n` custom
|
|
67
|
+
* properties the component sets from the variant's active index —
|
|
68
|
+
* and the body rows land in a stagger. Base styles are the opened,
|
|
69
|
+
* settled panel.
|
|
57
70
|
*/
|
|
58
71
|
|
|
59
72
|
import React from 'react';
|
|
@@ -175,7 +188,7 @@ const VARIANTS = {
|
|
|
175
188
|
},
|
|
176
189
|
};
|
|
177
190
|
|
|
178
|
-
export default function SidebarMock({ kind, size = 'md', embedded = false, className }) {
|
|
191
|
+
export default function SidebarMock({ kind, size = 'md', embedded = false, running = true, className }) {
|
|
179
192
|
const variant = VARIANTS[kind];
|
|
180
193
|
if (!variant) {
|
|
181
194
|
return (
|
|
@@ -187,8 +200,19 @@ export default function SidebarMock({ kind, size = 'md', embedded = false, class
|
|
|
187
200
|
);
|
|
188
201
|
}
|
|
189
202
|
const { Component, tabs } = variant;
|
|
203
|
+
/* The travelling underline needs the active tab's index and the tab
|
|
204
|
+
count; both ride custom properties so the CSS can compute the
|
|
205
|
+
resting offset. It slides in from the neighbouring tab — from the
|
|
206
|
+
left normally, from the right when the first tab is the active
|
|
207
|
+
one (there is no left neighbour to come from). */
|
|
208
|
+
const activeIx = Math.max(0, tabs.findIndex((t) => t.active));
|
|
209
|
+
const underlineVars = {
|
|
210
|
+
'--sb-ix': activeIx,
|
|
211
|
+
'--sb-n': tabs.length,
|
|
212
|
+
'--sb-from': activeIx > 0 ? '-100%' : '100%',
|
|
213
|
+
};
|
|
190
214
|
const panel = (
|
|
191
|
-
<div className={[amStyles.detail, amStyles.rich].join(' ')}>
|
|
215
|
+
<div className={[amStyles.detail, amStyles.rich, amStyles.sbLive, !running && amStyles.static].filter(Boolean).join(' ')}>
|
|
192
216
|
<div className={amStyles['sb-head']}>
|
|
193
217
|
<div className={amStyles.ico}></div>
|
|
194
218
|
<div className={amStyles.meta}>
|
|
@@ -209,6 +233,9 @@ export default function SidebarMock({ kind, size = 'md', embedded = false, class
|
|
|
209
233
|
<div className={amStyles.l}></div>
|
|
210
234
|
</div>
|
|
211
235
|
))}
|
|
236
|
+
{/* Travelling active-tab underline — rests under the active
|
|
237
|
+
tab; the loop replays the travel from its neighbour. */}
|
|
238
|
+
<div className={amStyles['sb-underline']} style={underlineVars}></div>
|
|
212
239
|
</div>
|
|
213
240
|
)}
|
|
214
241
|
<div className={amStyles['sb-body']}>
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SidebarMock.render.test.js — renders the real <SidebarMock> JSX to
|
|
3
|
+
* static markup and asserts on the wave-5 animation plumbing: the
|
|
4
|
+
* panel carries the `.sbLive` scope, `running={false}` puts `.static`
|
|
5
|
+
* on the panel itself (both standalone and embedded), and the
|
|
6
|
+
* travelling active-tab underline is present with the `--sb-ix` /
|
|
7
|
+
* `--sb-n` / `--sb-from` custom properties derived from the
|
|
8
|
+
* variant's active tab index.
|
|
9
|
+
*
|
|
10
|
+
* Same esbuild-bundle-then-renderToStaticMarkup technique as
|
|
11
|
+
* AppMock.render.test.js; CSS modules stubbed with an identity proxy.
|
|
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, '..', 'SidebarMock.jsx');
|
|
26
|
+
const PRESET_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
|
27
|
+
const SCRATCH = path.join(PRESET_ROOT, '.tmp-sidebar-mock-test');
|
|
28
|
+
|
|
29
|
+
/* Must stay in sync with the VARIANTS map in SidebarMock.jsx.
|
|
30
|
+
activeIx mirrors each variant's active-tab index. */
|
|
31
|
+
const KINDS = {
|
|
32
|
+
'procest-xwiki': 1,
|
|
33
|
+
'procest-timeline': 2,
|
|
34
|
+
'docudesk-signatures': 1,
|
|
35
|
+
'docudesk-pii-map': 2,
|
|
36
|
+
'openregister-metadata': 1,
|
|
37
|
+
'opencatalogi-publication-history': 1,
|
|
38
|
+
'openconnector-run-detail': 1,
|
|
39
|
+
'decidesk-decision': 1,
|
|
40
|
+
'nextcloud-activity': 0,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const cssModuleStub = {
|
|
44
|
+
name: 'css-module-stub',
|
|
45
|
+
setup(b) {
|
|
46
|
+
b.onResolve({filter: /\.module\.css$/}, (args) => ({path: args.path, namespace: 'css-stub'}));
|
|
47
|
+
b.onLoad({filter: /.*/, namespace: 'css-stub'}, () => ({
|
|
48
|
+
contents: 'export default new Proxy({}, {get: (_, p) => p});',
|
|
49
|
+
loader: 'js',
|
|
50
|
+
}));
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let SidebarMock;
|
|
55
|
+
|
|
56
|
+
before(async () => {
|
|
57
|
+
await fs.mkdir(SCRATCH, {recursive: true});
|
|
58
|
+
const tmpDir = await fs.mkdtemp(path.join(SCRATCH, 'run-'));
|
|
59
|
+
const outFile = path.join(tmpDir, 'bundle.cjs');
|
|
60
|
+
await build({
|
|
61
|
+
entryPoints: [COMPONENT],
|
|
62
|
+
outfile: outFile,
|
|
63
|
+
bundle: true,
|
|
64
|
+
format: 'cjs',
|
|
65
|
+
jsx: 'automatic',
|
|
66
|
+
jsxImportSource: 'react',
|
|
67
|
+
tsconfigRaw: {compilerOptions: {jsx: 'react-jsx', jsxImportSource: 'react'}},
|
|
68
|
+
platform: 'node',
|
|
69
|
+
external: ['react'],
|
|
70
|
+
plugins: [cssModuleStub],
|
|
71
|
+
logLevel: 'warning',
|
|
72
|
+
});
|
|
73
|
+
delete require.cache[require.resolve(outFile)];
|
|
74
|
+
SidebarMock = require(outFile).default;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
after(async () => {
|
|
78
|
+
await fs.rm(SCRATCH, {recursive: true, force: true});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
function render(props) {
|
|
82
|
+
return renderToStaticMarkup(React.createElement(SidebarMock, props));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
test('SSR smoke: every kind renders the live panel, none falls through to Unknown sidebar', () => {
|
|
86
|
+
for (const kind of Object.keys(KINDS)) {
|
|
87
|
+
const html = render({kind});
|
|
88
|
+
assert.match(html, /detail rich sbLive/, `${kind}: missing sbLive panel scope`);
|
|
89
|
+
assert.doesNotMatch(html, /Unknown sidebar/, `${kind}: fell through to the unknown branch`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('running={false} puts static on the panel — standalone and embedded', () => {
|
|
94
|
+
assert.match(render({kind: 'procest-xwiki', running: false}), /detail rich sbLive static/);
|
|
95
|
+
assert.match(render({kind: 'procest-xwiki', running: false, embedded: true}), /detail rich sbLive static/);
|
|
96
|
+
assert.doesNotMatch(render({kind: 'procest-xwiki'}), /\bstatic\b/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('the travelling underline carries --sb-ix / --sb-n / --sb-from per variant', () => {
|
|
100
|
+
for (const [kind, ix] of Object.entries(KINDS)) {
|
|
101
|
+
const html = render({kind});
|
|
102
|
+
assert.match(html, /class="sb-underline"/, `${kind}: underline missing`);
|
|
103
|
+
assert.match(html, new RegExp(`--sb-ix:${ix}[;"]`), `${kind}: wrong active index`);
|
|
104
|
+
assert.match(html, /--sb-n:[0-9]/, `${kind}: tab count missing`);
|
|
105
|
+
const from = ix > 0 ? '-100%' : '100%';
|
|
106
|
+
assert.match(html, new RegExp(`--sb-from:${from}`), `${kind}: wrong travel origin`);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('embedded mode drops the standalone frame but keeps the live panel', () => {
|
|
111
|
+
const html = render({kind: 'openregister-metadata', embedded: true});
|
|
112
|
+
assert.doesNotMatch(html, /smFrame/);
|
|
113
|
+
assert.match(html, /detail rich sbLive/);
|
|
114
|
+
assert.match(html, /class="sb-underline"/);
|
|
115
|
+
});
|