@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.
Files changed (44) hide show
  1. package/ANIMATION_ROADMAP.md +129 -0
  2. package/MISSING_COMPONENTS.md +15 -5
  3. package/package.json +3 -2
  4. package/src/components/AgentTrace/AgentTrace.jsx +26 -5
  5. package/src/components/AgentTrace/AgentTrace.module.css +41 -2
  6. package/src/components/AgentTrace/__tests__/AgentTrace.render.test.js +108 -0
  7. package/src/components/AppMock/AppMock.jsx +14 -5
  8. package/src/components/AppMock/AppMock.module.css +1702 -0
  9. package/src/components/AppMock/__tests__/AppMock.render.test.js +329 -0
  10. package/src/components/AppMock/variants/DeciDeskMock.jsx +46 -5
  11. package/src/components/AppMock/variants/DocuDeskMock.jsx +39 -9
  12. package/src/components/AppMock/variants/DoriathMock.jsx +28 -13
  13. package/src/components/AppMock/variants/HermiqMock.jsx +35 -15
  14. package/src/components/AppMock/variants/HrmqMock.jsx +46 -28
  15. package/src/components/AppMock/variants/LarpingAppMock.jsx +36 -7
  16. package/src/components/AppMock/variants/LaunchPadBiMock.jsx +12 -5
  17. package/src/components/AppMock/variants/LaunchPadTilesMock.jsx +6 -1
  18. package/src/components/AppMock/variants/LaunchPadWidgetsMock.jsx +17 -8
  19. package/src/components/AppMock/variants/NLDesignMock.jsx +15 -4
  20. package/src/components/AppMock/variants/OpenBuildMock.jsx +20 -5
  21. package/src/components/AppMock/variants/OpenCatalogiMock.jsx +23 -4
  22. package/src/components/AppMock/variants/OpenConnectorMock.jsx +25 -3
  23. package/src/components/AppMock/variants/OpenRegisterMock.jsx +13 -2
  24. package/src/components/AppMock/variants/PipelinQMock.jsx +12 -2
  25. package/src/components/AppMock/variants/PortaliqMock.jsx +21 -9
  26. package/src/components/AppMock/variants/ProcestMock.jsx +34 -7
  27. package/src/components/AppMock/variants/ScholiqMock.jsx +12 -6
  28. package/src/components/AppMock/variants/ShillinqMock.jsx +33 -17
  29. package/src/components/AppMock/variants/SoftwareCatalogMock.jsx +15 -5
  30. package/src/components/AppMock/variants/ZaakAfhandelAppMock.jsx +19 -4
  31. package/src/components/FlowMock/FlowMock.jsx +5 -3
  32. package/src/components/FlowMock/FlowMock.module.css +36 -9
  33. package/src/components/FlowMock/__tests__/FlowMock.render.test.js +83 -0
  34. package/src/components/LeafMock/LeafMock.jsx +23 -8
  35. package/src/components/LeafMock/LeafMock.module.css +123 -0
  36. package/src/components/LeafMock/__tests__/LeafMock.render.test.js +105 -0
  37. package/src/components/MockScene/MockScene.jsx +16 -2
  38. package/src/components/MockScene/MockScene.module.css +22 -2
  39. package/src/components/MockScene/__tests__/MockScene.render.test.js +101 -0
  40. package/src/components/SidebarMock/SidebarMock.jsx +29 -2
  41. package/src/components/SidebarMock/__tests__/SidebarMock.render.test.js +115 -0
  42. package/src/components/WidgetMock/WidgetMock.jsx +35 -14
  43. package/src/components/WidgetMock/__tests__/WidgetMock.render.test.js +125 -0
  44. package/src/components/WidgetMock/variants/DocuDeskAnonymise.jsx +6 -1
@@ -0,0 +1,329 @@
1
+ /**
2
+ * AppMock.render.test.js — renders the real <AppMock> JSX to static
3
+ * markup (the SSR path Docusaurus takes at build time) and asserts on
4
+ * the output: every variant slug renders, the P0 `running` prop
5
+ * plumbing puts `.static` on the frame, the animated variants expose
6
+ * their animation hooks, the `openwoo` slug aliases to the
7
+ * OpenCatalogi mock, and no rendered inline style reaches for the
8
+ * reserved coral/gold families.
9
+ *
10
+ * Uses the same esbuild-bundle-then-renderToStaticMarkup technique as
11
+ * KanbanMock.render.test.js. CSS modules are stubbed with an identity
12
+ * proxy (styles.foo → 'foo'), so class assertions read as the source
13
+ * class names.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const test = require('node:test');
19
+ const {before, after} = test;
20
+ const assert = require('node:assert/strict');
21
+ const path = require('node:path');
22
+ const fs = require('node:fs/promises');
23
+ const {build} = require('esbuild');
24
+ const React = require('react');
25
+ const {renderToStaticMarkup} = require('react-dom/server');
26
+
27
+ const COMPONENT = path.resolve(__dirname, '..', 'AppMock.jsx');
28
+ const PRESET_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
29
+ const SCRATCH = path.join(PRESET_ROOT, '.tmp-app-mock-test');
30
+
31
+ /* Must stay in sync with the VARIANTS map in AppMock.jsx. */
32
+ const SLUGS = [
33
+ 'launchpad', 'launchpad-tiles', 'launchpad-bi', 'launchpad-widgets',
34
+ 'openbuild', 'opencatalogi', 'openconnector', 'openregister',
35
+ 'procest', 'decidesk', 'docudesk', 'larpingapp', 'nldesign',
36
+ 'openwoo', 'pipelinq', 'softwarecatalog', 'zaakafhandelapp',
37
+ 'hermiq', 'portaliq', 'scholiq', 'shillinq', 'doriath', 'planix',
38
+ 'hrmq', 'app-versions',
39
+ ];
40
+
41
+ const cssModuleStub = {
42
+ name: 'css-module-stub',
43
+ setup(b) {
44
+ b.onResolve({filter: /\.module\.css$/}, (args) => ({path: args.path, namespace: 'css-stub'}));
45
+ b.onLoad({filter: /.*/, namespace: 'css-stub'}, () => ({
46
+ contents: 'export default new Proxy({}, {get: (_, p) => p});',
47
+ loader: 'js',
48
+ }));
49
+ },
50
+ };
51
+
52
+ let AppMock;
53
+
54
+ before(async () => {
55
+ await fs.mkdir(SCRATCH, {recursive: true});
56
+ const tmpDir = await fs.mkdtemp(path.join(SCRATCH, 'run-'));
57
+ const outFile = path.join(tmpDir, 'bundle.cjs');
58
+ await build({
59
+ entryPoints: [COMPONENT],
60
+ outfile: outFile,
61
+ bundle: true,
62
+ format: 'cjs',
63
+ jsx: 'automatic',
64
+ jsxImportSource: 'react',
65
+ tsconfigRaw: {compilerOptions: {jsx: 'react-jsx', jsxImportSource: 'react'}},
66
+ platform: 'node',
67
+ external: ['react'],
68
+ plugins: [cssModuleStub],
69
+ logLevel: 'warning',
70
+ });
71
+ delete require.cache[require.resolve(outFile)];
72
+ AppMock = require(outFile).default;
73
+ });
74
+
75
+ after(async () => {
76
+ await fs.rm(SCRATCH, {recursive: true, force: true});
77
+ });
78
+
79
+ function render(props) {
80
+ return renderToStaticMarkup(React.createElement(AppMock, props));
81
+ }
82
+
83
+ test('SSR smoke: every variant slug renders a frame, none falls through to Unknown app', () => {
84
+ for (const app of SLUGS) {
85
+ const html = render({app});
86
+ assert.match(html, /class="frame size-md"/, `${app}: frame missing`);
87
+ assert.doesNotMatch(html, /Unknown app/, `${app}: fell through to the unknown-variant branch`);
88
+ }
89
+ });
90
+
91
+ test('P0 plumbing: running={false} adds the static class to the frame; default omits it', () => {
92
+ assert.match(render({app: 'portaliq', running: false}), /class="frame size-md static"/);
93
+ assert.doesNotMatch(render({app: 'portaliq'}), /\bstatic\b/);
94
+ });
95
+
96
+ test('P0 plumbing: static frame works on every variant slug', () => {
97
+ for (const app of SLUGS) {
98
+ assert.match(render({app, running: false}), /class="frame size-md static"/, `${app}: no static frame`);
99
+ }
100
+ });
101
+
102
+ test('openwoo slug is an alias for the OpenCatalogi mock', () => {
103
+ const openwoo = render({app: 'openwoo'});
104
+ const opencatalogi = render({app: 'opencatalogi'});
105
+ assert.equal(openwoo, opencatalogi);
106
+ assert.equal(
107
+ render({app: 'openwoo', caption: true}).match(/<figcaption[^>]*>([^<]*)</)[1],
108
+ 'OpenCatalogi'
109
+ );
110
+ });
111
+
112
+ test('portaliq: form fields, fills, submit, confirm sweep, and the arriving inbox row are all present', () => {
113
+ const html = render({app: 'portaliq'});
114
+ assert.match(html, /class="body decidesk portaliq"/);
115
+ assert.match(html, /field field-1/);
116
+ assert.match(html, /field field-2/);
117
+ assert.match(html, /field field-3 tall/);
118
+ assert.equal((html.match(/class="fieldFill"/g) || []).length, 3);
119
+ assert.match(html, /btn submit/);
120
+ assert.match(html, /class="confirmSweep"/);
121
+ assert.match(html, /item newItem/);
122
+ });
123
+
124
+ test('scholiq: staggered progress fills (inline delays) and the credential chip', () => {
125
+ const html = render({app: 'scholiq'});
126
+ assert.equal((html.match(/class="progressFill"/g) || []).length, 6);
127
+ for (const d of [0, 250, 500, 750, 1000, 1250]) {
128
+ assert.match(html, new RegExp(`animation-delay:${d}ms`), `missing ${d}ms bar delay`);
129
+ }
130
+ assert.match(html, /class="credChip"/);
131
+ });
132
+
133
+ test('launchpad-widgets: seven widgets arrive on a 900ms stagger', () => {
134
+ const html = render({app: 'launchpad-widgets'});
135
+ assert.match(html, /class="body launchpad lpWidgets"/);
136
+ for (const d of [0, 900, 1800, 2700, 3600, 4500, 5400]) {
137
+ assert.match(html, new RegExp(`animation-delay:${d}ms`), `missing ${d}ms widget delay`);
138
+ }
139
+ });
140
+
141
+ test('launchpad-bi: the line chart stroke carries pathLength="1" for the draw animation', () => {
142
+ const html = render({app: 'launchpad-bi'});
143
+ assert.match(html, /class="body launchpad lpBi"/);
144
+ assert.match(html, /class="stroke" pathLength="1"/);
145
+ assert.equal((html.match(/class="biDelta"/g) || []).length, 3);
146
+ });
147
+
148
+ test('pipelinq: the closing deal (dealGone) and the won card (dealWon) are both in the board', () => {
149
+ const html = render({app: 'pipelinq'});
150
+ assert.match(html, /class="body decidesk plq"/);
151
+ assert.match(html, /card dealGone/);
152
+ assert.match(html, /card dealWon/);
153
+ });
154
+
155
+ test('nldesign: selected swatch, type row, and the actions wrapper around the preview buttons', () => {
156
+ const html = render({app: 'nldesign'});
157
+ assert.match(html, /class="body openregister nldesign"/);
158
+ assert.equal((html.match(/class="swatch(?: |")/g) || []).length, 8);
159
+ assert.match(html, /swatch swatchSel/);
160
+ assert.match(html, /class="typeRow"/);
161
+ // The preview buttons need the .actions parent to be styled at all.
162
+ assert.match(html, /class="actions"[^>]*><div class="btn"><\/div><div class="btn ghost">/);
163
+ });
164
+
165
+ test('larpingapp: the scene timeline renders five steps (was an empty shell)', () => {
166
+ const html = render({app: 'larpingapp'});
167
+ assert.match(html, /class="procest"/);
168
+ assert.match(html, /class="timeline"/);
169
+ assert.equal((html.match(/class="step[ "]/g) || []).length, 5);
170
+ assert.equal((html.match(/step now/g) || []).length, 1, 'exactly one active (orange) step');
171
+ });
172
+
173
+ /* ---- Wave 2 — storytelling animation hooks ---- */
174
+
175
+ test('docudesk: word-level rows with five ordered PII marks, and the live drop-zone', () => {
176
+ const html = render({app: 'docudesk'});
177
+ assert.match(html, /class="body opencatalogi docudesk"/);
178
+ assert.equal((html.match(/class="words"/g) || []).length, 5, 'five word-bar rows');
179
+ for (const n of [1, 2, 3, 4, 5]) {
180
+ assert.match(html, new RegExp(`word mark mark-${n}`), `mark-${n} missing`);
181
+ }
182
+ assert.match(html, /w w-upload zoneLive/);
183
+ assert.match(html, /class="zoneFile"/);
184
+ });
185
+
186
+ test('shillinq: bank line, reconciling row 3, ledger totals, and the KPI grow hook', () => {
187
+ const html = render({app: 'shillinq'});
188
+ assert.match(html, /class="body decidesk shillinq"/);
189
+ assert.match(html, /class="bankLine"/);
190
+ assert.equal((html.match(/item inv/g) || []).length, 5, 'five invoice rows');
191
+ assert.equal((html.match(/item inv reconciling/g) || []).length, 1, 'exactly one reconciling row');
192
+ // The reconciling row is row 3: two plain rows precede it.
193
+ assert.match(html, /item inv"[\s\S]*?item inv"[\s\S]*?item inv reconciling/);
194
+ assert.equal((html.match(/tick open/g) || []).length, 1, 'one still-open tick (row 5)');
195
+ assert.match(html, /class="totNet"/);
196
+ assert.match(html, /kpi kpiGrow/);
197
+ });
198
+
199
+ test('doriath: key hex, both mask layers, value, and the countdown hairline', () => {
200
+ const html = render({app: 'doriath'});
201
+ assert.match(html, /class="body decidesk doriath"/);
202
+ assert.match(html, /class="keyHex"/);
203
+ assert.match(html, /class="vMask"/);
204
+ assert.match(html, /class="vValue"/);
205
+ assert.match(html, /class="vCountdown"/);
206
+ assert.equal((html.match(/class="maskDots"/g) || []).length, 5, 'five locked list entries keep their dot masks');
207
+ });
208
+
209
+ test('opencatalogi: search bar with query fill; three matches and three dimmed cards', () => {
210
+ const html = render({app: 'opencatalogi'});
211
+ assert.match(html, /class="search"/);
212
+ assert.match(html, /class="q"/);
213
+ assert.equal((html.match(/class="card[^"]*match"/g) || []).length, 3, 'three matching cards');
214
+ assert.equal((html.match(/class="card[^"]*dim"/g) || []).length, 3, 'three dimmed cards');
215
+ });
216
+
217
+ test('openconnector: two travel dots on the arrows and the inserted run row with mint tick', () => {
218
+ const html = render({app: 'openconnector'});
219
+ assert.match(html, /class="travelDotA"/);
220
+ assert.match(html, /class="travelDotB"/);
221
+ assert.match(html, /item newRun/);
222
+ assert.match(html, /class="runTick"/);
223
+ });
224
+
225
+ test('procest: advanced base state (2 done, 1 now), two ghost overlays, and the re-sort pair', () => {
226
+ const html = render({app: 'procest'});
227
+ assert.match(html, /class="step"/, 'first step done');
228
+ assert.match(html, /step advanceFrom/, 'second step advanced out of active');
229
+ assert.match(html, /step now advanceTo/, 'third step is the newly active one');
230
+ assert.equal((html.match(/step now/g) || []).length, 1, 'one active step');
231
+ assert.equal((html.match(/step todo/g) || []).length, 2, 'two to-do steps');
232
+ assert.match(html, /ghost ghostNow/);
233
+ assert.match(html, /ghost ghostTodo/);
234
+ assert.match(html, /item sortUp/);
235
+ assert.match(html, /item sortDown/);
236
+ });
237
+
238
+ test('openregister: the landing object row and the KPI grow hook', () => {
239
+ const html = render({app: 'openregister'});
240
+ assert.match(html, /item newObj/);
241
+ assert.match(html, /kpi kpiGrow/);
242
+ });
243
+
244
+ /* ---- Wave 4 — content reworks ---- */
245
+
246
+ test('decidesk: vote tally (for/against/abstain + quorum) and the adopt-flip decision row', () => {
247
+ const html = render({app: 'decidesk'});
248
+ assert.equal((html.match(/class="tallyRow /g) || []).length, 3, 'three tally rows');
249
+ assert.match(html, /tallyRow for/);
250
+ assert.match(html, /tallyRow against/);
251
+ assert.match(html, /tallyRow abstain/);
252
+ assert.equal((html.match(/class="quorum"/g) || []).length, 1, 'quorum line on the for-track only');
253
+ assert.match(html, /item decisionRow/);
254
+ assert.match(html, /statusPill adoptFlip/);
255
+ });
256
+
257
+ test('hrmq: reworked to a timesheet + approval surface (no people-list panels left)', () => {
258
+ const html = render({app: 'hrmq'});
259
+ assert.match(html, /class="body decidesk hrmq"/);
260
+ assert.equal((html.match(/class="dayCol"/g) || []).length, 5, 'five day columns');
261
+ assert.equal((html.match(/class="dayBar"/g) || []).length, 5, 'five hour bars');
262
+ assert.match(html, /btn submitBtn/);
263
+ assert.match(html, /item approveRow/);
264
+ assert.match(html, /class="approvePip"/);
265
+ assert.match(html, /class="payroll"/);
266
+ assert.match(html, /class="pNum"/);
267
+ });
268
+
269
+ test('larpingapp: XP ripple — lifted source card, travelling token, timeline intact', () => {
270
+ const html = render({app: 'larpingapp'});
271
+ assert.match(html, /class="body opencatalogi larping"/);
272
+ assert.match(html, /card b xpSource/);
273
+ assert.match(html, /class="xpToken"/);
274
+ assert.equal((html.match(/step now/g) || []).length, 1);
275
+ });
276
+
277
+ test('softwarecatalog: scan band, CVE-hit row 4, and the vermillion CVE bar (orange accent bar intact)', () => {
278
+ const html = render({app: 'softwarecatalog'});
279
+ assert.match(html, /class="body openregister swc"/);
280
+ assert.match(html, /class="scanBand"/);
281
+ assert.match(html, /item blocked cveHit/);
282
+ assert.match(html, /bar cve/);
283
+ assert.equal((html.match(/bar accent/g) || []).length, 1, 'exactly one orange accent bar');
284
+ });
285
+
286
+ test('zaakafhandelapp: travelling submission ghost and the dressed new case row', () => {
287
+ const html = render({app: 'zaakafhandelapp'});
288
+ assert.match(html, /class="body procest zaak"/);
289
+ assert.match(html, /class="zkGhost"/);
290
+ assert.match(html, /item zkNew/);
291
+ assert.match(html, /item zkSent/);
292
+ assert.equal((html.match(/item late/g) || []).length, 1, 'the SLA-late row survives the rework');
293
+ });
294
+
295
+ test('hermiq: inline styles moved to the module — bubbles, typing indicator, chips, approval', () => {
296
+ const html = render({app: 'hermiq'});
297
+ assert.match(html, /class="body decidesk hermiq"/);
298
+ assert.equal((html.match(/bubble user/g) || []).length, 2, 'two user bubbles');
299
+ assert.equal((html.match(/bubble agent/g) || []).length, 2, 'two agent bubbles');
300
+ assert.match(html, /class="typing"/);
301
+ assert.equal((html.match(/class="tDot"/g) || []).length, 3, 'three typing dots');
302
+ assert.match(html, /statusPill chip1/);
303
+ assert.match(html, /chip chip2/);
304
+ assert.match(html, /chip chip3/);
305
+ assert.match(html, /statusPill approveChip/);
306
+ assert.match(html, /class="promptField"/);
307
+ assert.match(html, /btn sendBtn/);
308
+ // The wave-4 requirement: presentation no longer rides inline styles.
309
+ assert.doesNotMatch(html, /style="[^"]*(background|border-radius|align-self)/, 'presentation styles must live in the module');
310
+ });
311
+
312
+ test('openbuild: template highlight, travelling ghost chip, landing app row, publish pulse', () => {
313
+ const html = render({app: 'openbuild'});
314
+ assert.match(html, /item tplSource/);
315
+ assert.match(html, /class="buildGhost"/);
316
+ assert.match(html, /item newApp/);
317
+ assert.match(html, /kpi kpiGrow/);
318
+ assert.match(html, /kpi forest publishedKpi/);
319
+ });
320
+
321
+ test('reserved accent families: no variant inline-styles coral or gold', () => {
322
+ // SKILL.md reserves the coral and gold families; the one-orange rule
323
+ // is enforced per component by review, but coral/gold must never
324
+ // appear at all. Class-based colours live in the stylesheet; this
325
+ // guards the inline styles the variants set directly.
326
+ for (const app of SLUGS) {
327
+ assert.doesNotMatch(render({app}), /--c-coral|--c-gold/, `${app}: reserved family in inline style`);
328
+ }
329
+ });
@@ -1,14 +1,32 @@
1
1
  /**
2
2
  * DeciDesk abstract — left nav + centre with action row, KPI strip,
3
- * and two tables. Reference: localhost:8080/apps/decidesk.
3
+ * vote tally, and decision row. Reference: localhost:8080/apps/decidesk.
4
4
  *
5
5
  * Hero of the centre is the trio of primary buttons (New Decision /
6
- * Action Item / Minutes); we render them as accent-pill rows top right.
6
+ * Action Item / Minutes) rendered as accent-pill rows top right.
7
+ *
8
+ * Content rework (wave 4): the two former placeholder panels (a bare
9
+ * accent row / a bare short row) are now the app's actual story — a
10
+ * vote tally (for / against / abstain bars with a quorum line) and a
11
+ * decision row whose status flips pending → adopted. The tally fills,
12
+ * the "for" bar crosses the quorum line (which ticks mint), and the
13
+ * decision pill flips to adopted. Base styles are the adopted end
14
+ * state. The KPI strip keeps its single amber cell as the variant's
15
+ * one orange; the pending pill replays via the shared beta-pill
16
+ * coral/orange styling only transiently.
7
17
  */
8
18
 
9
19
  import React from 'react';
10
20
  import styles from '../AppMock.module.css';
11
21
 
22
+ /* for / against / abstain — fill widths of the tally tracks. The
23
+ "for" bar carries the quorum line and crosses it as it fills. */
24
+ const TALLY = [
25
+ {cls: 'for', fill: '72%'},
26
+ {cls: 'against', fill: '30%'},
27
+ {cls: 'abstain', fill: '16%'},
28
+ ];
29
+
12
30
  export default function DeciDeskMock() {
13
31
  return (
14
32
  <>
@@ -55,12 +73,35 @@ export default function DeciDeskMock() {
55
73
  </div>
56
74
  </div>
57
75
  <div className={styles.panelRow}>
58
- <div className={styles.panel}>
76
+ {/* Vote tally — for / against / abstain with quorum line */}
77
+ <div className={[styles.panel, styles.tally].join(' ')}>
59
78
  <div className={styles.head}><div className={styles.title}></div></div>
60
- <div className={styles.row + ' ' + styles.accent}></div>
79
+ {TALLY.map(({cls, fill}, i) => (
80
+ <div key={i} className={[styles.tallyRow, styles[cls]].join(' ')}>
81
+ <div className={styles.tLabel}></div>
82
+ <div className={styles.track}>
83
+ <div className={styles.fill} style={{width: fill}}></div>
84
+ {cls === 'for' && <div className={styles.quorum}></div>}
85
+ </div>
86
+ </div>
87
+ ))}
61
88
  </div>
62
- <div className={styles.panel}>
89
+ {/* Decision row — pending → adopted flip */}
90
+ <div className={[styles.panel, styles.decision].join(' ')}>
63
91
  <div className={styles.head}><div className={styles.title}></div></div>
92
+ <div className={[styles.item, styles.decisionRow].join(' ')}>
93
+ <div className={styles.dIco}></div>
94
+ <div className={styles.lines}>
95
+ <div className={styles.l1}></div>
96
+ <div className={styles.l2}></div>
97
+ </div>
98
+ {/* Base pill = adopted (mint). The keyframes replay the
99
+ pending (beta-coloured) phase before the flip. */}
100
+ <div className={[styles.statusPill, styles.adoptFlip].join(' ')}>
101
+ <div className={styles.h}></div><div className={styles.t}></div>
102
+ </div>
103
+ </div>
104
+ <div className={styles.row}></div>
64
105
  <div className={styles.row + ' ' + styles.short}></div>
65
106
  </div>
66
107
  </div>
@@ -6,11 +6,30 @@
6
6
  * recent documents with status pips (green = signed, amber = awaiting
7
7
  * sign, terra = anonymised, red = blocked) plus an anonymise drop-zone
8
8
  * widget. Left nav for templates / drafts / signed / archive.
9
+ *
10
+ * Animation (wave 2): the document rows are word-level bars; the
11
+ * marked (PII) words black out one by one — the anonymiser working
12
+ * through the open document set. Base styles are the redacted end
13
+ * state; the keyframes replay the before-state and the black-out.
14
+ * The drop-zone shares the `.zoneLive` marching-ants + file-drop
15
+ * treatment with the docudesk-anonymise dashboard widget.
9
16
  */
10
17
 
11
18
  import React from 'react';
12
19
  import styles from '../AppMock.module.css';
13
20
 
21
+ /* Word-bar layout per document row: widths in px, `mark` = a PII word
22
+ the anonymiser blacks out. Five rows, five marked words total, so
23
+ the black-out cascade reads one word at a time across the list.
24
+ markIndex orders the cascade (drives the amDdMark<N> keyframes). */
25
+ const DOC_ROWS = [
26
+ {pip: '', words: [{w: 16}, {w: 10, mark: 1}, {w: 20}, {w: 12}]},
27
+ {pip: 'review', words: [{w: 12}, {w: 18}, {w: 9, mark: 2}, {w: 14}]},
28
+ {pip: '', words: [{w: 20}, {w: 12}, {w: 16}, {w: 8, mark: 3}]},
29
+ {pip: 'todo', words: [{w: 10, mark: 4}, {w: 16}, {w: 12}, {w: 18}]},
30
+ {pip: '', words: [{w: 14}, {w: 12, mark: 5}, {w: 18}, {w: 10}]},
31
+ ];
32
+
14
33
  export default function DocuDeskMock() {
15
34
  return (
16
35
  <>
@@ -21,7 +40,7 @@ export default function DocuDeskMock() {
21
40
  <div className={styles.bell}></div>
22
41
  <div className={styles.avatar}></div>
23
42
  </div>
24
- <div className={[styles.body, styles.opencatalogi].filter(Boolean).join(' ')}>
43
+ <div className={[styles.body, styles.opencatalogi, styles.docudesk].filter(Boolean).join(' ')}>
25
44
  <div className={styles.nav}>
26
45
  <div className={styles.navHead}><div className={styles.h}></div><div className={styles.l}></div></div>
27
46
  {[true, false, false, false, false].map((active, i) => (
@@ -39,25 +58,36 @@ export default function DocuDeskMock() {
39
58
  <div className={styles.btn}></div>
40
59
  </div>
41
60
  </div>
42
- {/* Document list with anonymisation/sign status pips */}
61
+ {/* Document list — word bars with PII marks blacking out */}
43
62
  <div className={[styles.w, styles['w-jira']].join(' ')}>
44
63
  <div className={styles.wHead}>
45
64
  <div className={styles.h}></div><div className={styles.t}></div>
46
65
  </div>
47
66
  <div className={styles.list}>
48
- <div className={styles.item}><div className={styles.id}></div><div className={styles.l}></div><div className={styles.pip}></div></div>
49
- <div className={[styles.item, styles.review].join(' ')}><div className={styles.id}></div><div className={styles.l}></div><div className={styles.pip}></div></div>
50
- <div className={styles.item}><div className={styles.id}></div><div className={styles.l}></div><div className={styles.pip}></div></div>
51
- <div className={[styles.item, styles.todo].join(' ')}><div className={styles.id}></div><div className={styles.l}></div><div className={styles.pip}></div></div>
52
- <div className={styles.item}><div className={styles.id}></div><div className={styles.l}></div><div className={styles.pip}></div></div>
67
+ {DOC_ROWS.map(({pip, words}, i) => (
68
+ <div key={i} className={[styles.item, pip && styles[pip]].filter(Boolean).join(' ')}>
69
+ <div className={styles.id}></div>
70
+ <div className={styles.words}>
71
+ {words.map(({w, mark}, j) => (
72
+ <div
73
+ key={j}
74
+ className={[styles.word, mark && styles.mark, mark && styles[`mark-${mark}`]].filter(Boolean).join(' ')}
75
+ style={{width: w}}
76
+ ></div>
77
+ ))}
78
+ </div>
79
+ <div className={styles.pip}></div>
80
+ </div>
81
+ ))}
53
82
  </div>
54
83
  </div>
55
- {/* Anonymise drop-zone */}
56
- <div className={[styles.w, styles['w-upload']].join(' ')}>
84
+ {/* Anonymise drop-zone — live marching ants + file drop */}
85
+ <div className={[styles.w, styles['w-upload'], styles.zoneLive].join(' ')}>
57
86
  <div className={styles.wHead}>
58
87
  <div className={styles.h}></div><div className={styles.t}></div>
59
88
  </div>
60
89
  <div className={styles.zone}>
90
+ <div className={styles.zoneFile}></div>
61
91
  <div className={styles.ico}></div>
62
92
  <div className={styles.label}></div>
63
93
  </div>
@@ -6,6 +6,12 @@
6
6
  * pattern — next to one unlocked detail card on a cobalt-900 surface
7
7
  * with the revealed value in light. Tones: cobalt-900 for the vault
8
8
  * interior, lavender for the keys.
9
+ *
10
+ * Animation (wave 2): the key hex turns, the mask dots unmask, the
11
+ * value wipes in, a mint countdown hairline runs out, and the entry
12
+ * auto-relocks. Base styles are the revealed state (what static
13
+ * frames show); the keyframes replay lock → unlock → countdown →
14
+ * relock as one seamless loop.
9
15
  */
10
16
 
11
17
  import React from 'react';
@@ -13,9 +19,9 @@ import styles from '../AppMock.module.css';
13
19
 
14
20
  export default function DoriathMock() {
15
21
  const mask = (n) => (
16
- <div style={{display: 'flex', gap: 2, flexShrink: 0}}>
22
+ <div className={styles.maskDots}>
17
23
  {Array.from({length: n}).map((_, i) => (
18
- <div key={i} style={{width: 3, height: 3, borderRadius: '50%', background: 'var(--c-cobalt-400)'}}></div>
24
+ <div key={i} className={styles.dot}></div>
19
25
  ))}
20
26
  </div>
21
27
  );
@@ -28,7 +34,7 @@ export default function DoriathMock() {
28
34
  <div className={styles.bell}></div>
29
35
  <div className={styles.avatar}></div>
30
36
  </div>
31
- <div className={[styles.body, styles.decidesk].filter(Boolean).join(' ')}>
37
+ <div className={[styles.body, styles.decidesk, styles.doriath].filter(Boolean).join(' ')}>
32
38
  <div className={styles.nav}>
33
39
  <div className={styles.navHead}><div className={styles.h}></div><div className={styles.l}></div></div>
34
40
  {[true, false, false, false].map((active, i) => (
@@ -53,23 +59,32 @@ export default function DoriathMock() {
53
59
  <div className={styles.stack}>
54
60
  {[6, 5, 7, 5, 6].map((dots, i) => (
55
61
  <div key={i} className={styles.item}>
56
- <div style={{width: 10, height: 11, clipPath: 'var(--hex-pointy-top)', background: 'var(--c-lavender-500)', flexShrink: 0}}></div>
62
+ <div className={styles.keyChip}></div>
57
63
  <div className={styles.lines}><div className={styles.l1}></div></div>
58
64
  {mask(dots)}
59
65
  </div>
60
66
  ))}
61
67
  </div>
62
68
  </div>
63
- {/* Unlocked entry — vault-dark card, value revealed */}
64
- <div className={styles.panel} style={{background: 'var(--c-cobalt-900)', borderColor: 'var(--c-cobalt-900)'}}>
65
- <div style={{display: 'flex', alignItems: 'center', gap: 5}}>
66
- <div style={{width: 12, height: 14, clipPath: 'var(--hex-pointy-top)', background: 'var(--c-lavender-300)', flexShrink: 0}}></div>
67
- <div style={{height: 6, width: '45%', background: 'var(--c-lavender-300)', borderRadius: 1}}></div>
69
+ {/* Unlocked entry — vault-dark card. The key turns, the
70
+ masked dots unmask, the value wipes in, the countdown
71
+ hairline runs out, and the card relocks. */}
72
+ <div className={[styles.panel, styles.vaultCard].join(' ')}>
73
+ <div className={styles.vHead}>
74
+ <div className={styles.keyHex}></div>
75
+ <div className={styles.vTitle}></div>
76
+ </div>
77
+ <div className={styles.vMeta}></div>
78
+ <div className={styles.vSecret}>
79
+ <div className={styles.vMask}>
80
+ {Array.from({length: 7}).map((_, i) => (
81
+ <div key={i} className={styles.dot}></div>
82
+ ))}
83
+ </div>
84
+ <div className={styles.vValue}></div>
68
85
  </div>
69
- <div style={{height: 3, width: '60%', background: 'rgba(255,255,255,0.25)', borderRadius: 1}}></div>
70
- <div style={{height: 10, width: '85%', background: 'rgba(255,255,255,0.9)', borderRadius: 2}}></div>
71
- <div style={{height: 3, width: '50%', background: 'rgba(255,255,255,0.25)', borderRadius: 1}}></div>
72
- <div style={{height: 3, width: '70%', background: 'rgba(255,255,255,0.25)', borderRadius: 1}}></div>
86
+ <div className={styles.vCountdown}></div>
87
+ <div className={styles.vMetaShort}></div>
73
88
  </div>
74
89
  </div>
75
90
  </div>
@@ -6,6 +6,14 @@
6
6
  * user bubbles right, agent bubbles left — with a tool-call chip row
7
7
  * between agent turns and a prompt input at the bottom. Tones:
8
8
  * workspace-blue for the human side, mint for confirmed tool runs.
9
+ *
10
+ * Wave 4: the inline styles moved into AppMock.module.css (.hermiq
11
+ * scope) so the surface can animate. The loop: the user's bubble
12
+ * pops in, a typing indicator blinks, the agent's answer grows, the
13
+ * tool chips light left-to-right, the approval chip — the variant's
14
+ * single orange — ticks mint once approved, the tool-result bubble
15
+ * grows, and the user follows up. Base styles are the approved end
16
+ * state (all chips lit, approval mint).
9
17
  */
10
18
 
11
19
  import React from 'react';
@@ -21,13 +29,13 @@ export default function HermiqMock() {
21
29
  <div className={styles.bell}></div>
22
30
  <div className={styles.avatar}></div>
23
31
  </div>
24
- <div className={[styles.body, styles.decidesk].filter(Boolean).join(' ')}>
32
+ <div className={[styles.body, styles.decidesk, styles.hermiq].filter(Boolean).join(' ')}>
25
33
  {/* Agent list rail */}
26
34
  <div className={styles.nav}>
27
35
  <div className={styles.navHead}><div className={styles.h}></div><div className={styles.l}></div></div>
28
36
  {[true, false, false, false].map((active, i) => (
29
37
  <div key={i} className={[styles.item, active && styles.active].filter(Boolean).join(' ')}>
30
- <div className={styles.ico} style={{borderRadius: '50%'}}></div>
38
+ <div className={[styles.ico, styles.round].join(' ')}></div>
31
39
  <div className={styles.l}></div>
32
40
  </div>
33
41
  ))}
@@ -40,29 +48,41 @@ export default function HermiqMock() {
40
48
  <div className={styles.btn + ' ' + styles.ghost}></div>
41
49
  </div>
42
50
  </div>
43
- <div className={styles.panel} style={{flex: 1, gap: 8, justifyContent: 'flex-end'}}>
51
+ <div className={[styles.panel, styles.convo].join(' ')}>
44
52
  {/* User asks */}
45
- <div style={{alignSelf: 'flex-end', width: '42%', height: 12, borderRadius: '8px 2px 8px 8px', background: 'var(--c-workspaceblue-300)'}}></div>
53
+ <div className={[styles.bubble, styles.user, styles.turn1].join(' ')}></div>
54
+ {/* Typing indicator — blinks while the agent thinks.
55
+ Hidden at rest and in static frames. */}
56
+ <div className={styles.typing}>
57
+ <div className={styles.tDot}></div>
58
+ <div className={styles.tDot}></div>
59
+ <div className={styles.tDot}></div>
60
+ </div>
46
61
  {/* Agent answers */}
47
- <div style={{alignSelf: 'flex-start', width: '58%', height: 18, borderRadius: '2px 8px 8px 8px', background: 'var(--c-cobalt-50)', border: '1px solid var(--c-cobalt-100)'}}></div>
48
- {/* Tool-call chip row — mint = tool run confirmed */}
49
- <div style={{display: 'flex', alignItems: 'center', gap: 4}}>
50
- <div className={styles.statusPill}>
62
+ <div className={[styles.bubble, styles.agent, styles.turn2].join(' ')}></div>
63
+ {/* Tool-call chip row — chips light L→R; the approval chip
64
+ (the variant's one orange while pending) ticks mint. */}
65
+ <div className={styles.chips}>
66
+ <div className={[styles.statusPill, styles.chip1].join(' ')}>
67
+ <div className={styles.h}></div>
68
+ <div className={styles.t}></div>
69
+ </div>
70
+ <div className={[styles.chip, styles.chip2].join(' ')}></div>
71
+ <div className={[styles.chip, styles.chip3].join(' ')}></div>
72
+ <div className={[styles.statusPill, styles.approveChip].join(' ')}>
51
73
  <div className={styles.h}></div>
52
74
  <div className={styles.t}></div>
53
75
  </div>
54
- <div style={{width: 34, height: 9, borderRadius: 999, background: 'var(--c-cobalt-100)'}}></div>
55
- <div style={{width: 26, height: 9, borderRadius: 999, background: 'var(--c-cobalt-100)'}}></div>
56
76
  </div>
57
77
  {/* Agent continues with the tool result */}
58
- <div style={{alignSelf: 'flex-start', width: '64%', height: 24, borderRadius: '2px 8px 8px 8px', background: 'var(--c-cobalt-50)', border: '1px solid var(--c-cobalt-100)'}}></div>
78
+ <div className={[styles.bubble, styles.agent, styles.tall, styles.turn3].join(' ')}></div>
59
79
  {/* User follows up */}
60
- <div style={{alignSelf: 'flex-end', width: '34%', height: 12, borderRadius: '8px 2px 8px 8px', background: 'var(--c-workspaceblue-300)'}}></div>
80
+ <div className={[styles.bubble, styles.user, styles.narrow, styles.turn4].join(' ')}></div>
61
81
  </div>
62
82
  {/* Prompt input */}
63
- <div className={styles.actions} style={{alignItems: 'center'}}>
64
- <div style={{flex: 1, height: 16, background: 'white', border: '1px solid var(--c-cobalt-200)', borderRadius: 8}}></div>
65
- <div className={styles.btn} style={{width: 28}}></div>
83
+ <div className={[styles.actions, styles.promptRow].join(' ')}>
84
+ <div className={styles.promptField}></div>
85
+ <div className={[styles.btn, styles.sendBtn].join(' ')}></div>
66
86
  </div>
67
87
  </div>
68
88
  </div>