@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
@@ -6,6 +6,13 @@
6
6
  * (lead, qualified, proposal, won, lost) plus a KPI strip on top
7
7
  * (pipeline value, win rate, deals this week). Left nav for kanban /
8
8
  * customers / contacts / deals / quotes.
9
+ *
10
+ * Animated (7s loop, --am-dur): a deal closes — the proposal-column
11
+ * card collapses, the won card scales in with its mint bar, and the
12
+ * pipeline-value KPI widens. Resets by opacity swap. `running={false}`
13
+ * / prefers-reduced-motion show the closed state (won card in place,
14
+ * proposal card gone). Keyframes live in AppMock.module.css (`.plq`
15
+ * section).
9
16
  */
10
17
 
11
18
  import React from 'react';
@@ -21,7 +28,7 @@ export default function PipelinQMock() {
21
28
  <div className={styles.bell}></div>
22
29
  <div className={styles.avatar}></div>
23
30
  </div>
24
- <div className={[styles.body, styles.decidesk].filter(Boolean).join(' ')}>
31
+ <div className={[styles.body, styles.decidesk, styles.plq].filter(Boolean).join(' ')}>
25
32
  <div className={styles.nav}>
26
33
  <div className={styles.navHead}><div className={styles.h}></div><div className={styles.l}></div></div>
27
34
  {[true, false, false, false, false].map((active, i) => (
@@ -71,9 +78,12 @@ export default function PipelinQMock() {
71
78
  <div className={styles.col}>
72
79
  <div className={[styles.card, styles.c].join(' ')}></div>
73
80
  <div className={styles.card}></div>
74
- <div className={styles.card}></div>
81
+ {/* The deal being closed — collapses out of proposal. */}
82
+ <div className={[styles.card, styles.dealGone].join(' ')}></div>
75
83
  </div>
76
84
  <div className={styles.col}>
85
+ {/* …and lands here as the won card (mint bar). */}
86
+ <div className={[styles.card, styles.dealWon].join(' ')}></div>
77
87
  <div className={[styles.card, styles.c].join(' ')}></div>
78
88
  </div>
79
89
  <div className={styles.col}>
@@ -6,6 +6,14 @@
6
6
  * row, then a submission form card next to a message inbox list. No
7
7
  * left nav — the visitor gets one page, not an admin surface. Tones:
8
8
  * cobalt for the frame, mint for the delivered/confirmed states.
9
+ *
10
+ * Animated (8s loop, --am-dur): a visitor works the form — focus ring
11
+ * and greeked value fill each field in turn, the submit button
12
+ * presses, a mint confirmation band sweeps the form card, and the
13
+ * message lands as a new row (mint avatar) at the top of the inbox.
14
+ * `running={false}` / prefers-reduced-motion show the static end
15
+ * state: filled form, delivered row. Keyframes live in
16
+ * AppMock.module.css (`.portaliq` section). No orange.
9
17
  */
10
18
 
11
19
  import React from 'react';
@@ -21,7 +29,7 @@ export default function PortaliqMock() {
21
29
  {Array.from({length: 3}).map((_, i) => <div key={i} className={styles.icon}></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.portaliq].filter(Boolean).join(' ')}>
25
33
  <div className={styles.col}>
26
34
  {/* Portal masthead */}
27
35
  <div className={styles.head}>
@@ -32,24 +40,28 @@ export default function PortaliqMock() {
32
40
  </div>
33
41
  </div>
34
42
  <div className={styles.panelRow} style={{gridTemplateColumns: '1.2fr 1fr'}}>
35
- {/* Submission form card */}
36
- <div className={styles.panel}>
43
+ {/* Submission form card. Each .field carries a .fieldFill
44
+ bar that greeks the typed value; the sweep is the mint
45
+ confirmation band after submit. */}
46
+ <div className={[styles.panel, styles.formPanel].join(' ')}>
37
47
  <div className={styles.head}><div className={styles.title}></div></div>
38
48
  <div className={styles.row + ' ' + styles.head} style={{width: '25%', height: 4}}></div>
39
- <div style={{height: 14, background: 'white', border: '1px solid var(--c-cobalt-200)', borderRadius: 3}}></div>
49
+ <div className={[styles.field, styles['field-1']].join(' ')}><span className={styles.fieldFill}></span></div>
40
50
  <div className={styles.row + ' ' + styles.head} style={{width: '30%', height: 4}}></div>
41
- <div style={{height: 14, background: 'white', border: '1px solid var(--c-cobalt-200)', borderRadius: 3}}></div>
51
+ <div className={[styles.field, styles['field-2']].join(' ')}><span className={styles.fieldFill}></span></div>
42
52
  <div className={styles.row + ' ' + styles.head} style={{width: '20%', height: 4}}></div>
43
- <div style={{height: 26, background: 'white', border: '1px solid var(--c-cobalt-200)', borderRadius: 3}}></div>
53
+ <div className={[styles.field, styles['field-3'], styles.tall].join(' ')}><span className={styles.fieldFill}></span></div>
44
54
  <div className={styles.actions} style={{justifyContent: 'flex-end', marginTop: 2}}>
45
- <div className={styles.btn}></div>
55
+ <div className={[styles.btn, styles.submit].join(' ')}></div>
46
56
  </div>
57
+ <span className={styles.confirmSweep} aria-hidden="true"></span>
47
58
  </div>
48
- {/* Message inbox list */}
59
+ {/* Message inbox list — the .newItem row is the submitted
60
+ message arriving (mint avatar = delivered). */}
49
61
  <div className={[styles.panel, styles['w-mail']].join(' ')}>
50
62
  <div className={styles.head}><div className={styles.title}></div></div>
51
63
  <div className={styles.list}>
52
- <div className={styles.item}><div className={styles.av}></div><div className={styles.lines}><div className={styles.l1}></div><div className={styles.l2}></div></div></div>
64
+ <div className={[styles.item, styles.newItem].join(' ')}><div className={styles.av}></div><div className={styles.lines}><div className={styles.l1}></div><div className={styles.l2}></div></div></div>
53
65
  <div className={styles.item}><div className={styles.av}></div><div className={styles.lines}><div className={styles.l1}></div><div className={styles.l2}></div></div></div>
54
66
  <div className={styles.item}><div className={styles.av}></div><div className={styles.lines}><div className={styles.l1}></div><div className={styles.l2}></div></div></div>
55
67
  <div className={styles.item}><div className={styles.av}></div><div className={styles.lines}><div className={styles.l1}></div><div className={styles.l2}></div></div></div>
@@ -3,9 +3,15 @@
3
3
  *
4
4
  * Inferred from app role (ZGW case-management for VTH and citizen
5
5
  * processes): centre shows a canonical case detail with the timeline
6
- * of stages running across the top (one done, one active, three to-do)
6
+ * of stages running across the top (two done, one active, two to-do)
7
7
  * plus a list of recent cases below. Left nav for cases, types,
8
8
  * roles, decisions, archive.
9
+ *
10
+ * Animation (wave 2): the case advances one stage — ghost hexes
11
+ * overlaid on steps 2 and 3 replay the before-state (step 2 still
12
+ * orange-active, step 3 still to-do) and fade away to reveal the
13
+ * advanced base state; the advanced case's list row re-sorts to the
14
+ * top. Base styles are the advanced end state throughout.
9
15
  */
10
16
 
11
17
  import React from 'react';
@@ -41,17 +47,21 @@ export default function ProcestMock({ sidebar = null }) {
41
47
  <div className={styles.btn}></div>
42
48
  </div>
43
49
  </div>
50
+ {/* Base = advanced state: done, done, now, todo, todo.
51
+ Ghost hexes on steps 2+3 replay the before-state. */}
44
52
  <div className={styles.timeline}>
45
53
  <div className={styles.step}>
46
54
  <div className={styles.h}></div>
47
55
  <div className={styles.label}></div>
48
56
  </div>
49
- <div className={[styles.step, styles.now].join(' ')}>
57
+ <div className={[styles.step, styles.advanceFrom].join(' ')}>
50
58
  <div className={styles.h}></div>
59
+ <div className={[styles.ghost, styles.ghostNow].join(' ')}></div>
51
60
  <div className={styles.label}></div>
52
61
  </div>
53
- <div className={[styles.step, styles.todo].join(' ')}>
62
+ <div className={[styles.step, styles.now, styles.advanceTo].join(' ')}>
54
63
  <div className={styles.h}></div>
64
+ <div className={[styles.ghost, styles.ghostTodo].join(' ')}></div>
55
65
  <div className={styles.label}></div>
56
66
  </div>
57
67
  <div className={[styles.step, styles.todo].join(' ')}>
@@ -66,16 +76,33 @@ export default function ProcestMock({ sidebar = null }) {
66
76
  <div className={styles.panel}>
67
77
  <div className={styles.head}><div className={styles.title}></div></div>
68
78
  <div className={styles.stack}>
69
- {['a','b','d','c','a'].map((cls, i) => (
79
+ {/* DOM order = end state: the advanced case (status pill,
80
+ .sortUp) sits on top; the row it overtook (.sortDown)
81
+ is second. The keyframes replay the pre-sort order. */}
82
+ <div className={[styles.item, styles.sortUp].join(' ')}>
83
+ <div className={[styles.av, styles.b].join(' ')}></div>
84
+ <div className={styles.lines}>
85
+ <div className={styles.l1}></div>
86
+ <div className={styles.l2}></div>
87
+ </div>
88
+ <div className={styles.statusPill}>
89
+ <div className={styles.h}></div><div className={styles.t}></div>
90
+ </div>
91
+ </div>
92
+ <div className={[styles.item, styles.sortDown].join(' ')}>
93
+ <div className={[styles.av, styles.a].filter(Boolean).join(' ')}></div>
94
+ <div className={styles.lines}>
95
+ <div className={styles.l1}></div>
96
+ <div className={styles.l2}></div>
97
+ </div>
98
+ </div>
99
+ {['d','c','a'].map((cls, i) => (
70
100
  <div key={i} className={styles.item}>
71
101
  <div className={[styles.av, styles[cls]].join(' ')}></div>
72
102
  <div className={styles.lines}>
73
103
  <div className={styles.l1}></div>
74
104
  <div className={styles.l2}></div>
75
105
  </div>
76
- <div className={[styles.statusPill, i === 1 && styles.beta].filter(Boolean).join(' ')}>
77
- <div className={styles.h}></div><div className={styles.t}></div>
78
- </div>
79
106
  </div>
80
107
  ))}
81
108
  </div>
@@ -6,6 +6,12 @@
6
6
  * credential chip on the completed course. Left nav for courses /
7
7
  * lessons / learners / certificates. Tone: forest — data you can
8
8
  * trust a diploma to.
9
+ *
10
+ * Animated (6s loop, --am-dur): the six progress bars fill on a
11
+ * 250ms stagger (inline animation-delay), then the completed course's
12
+ * credential chip pops in with an overshoot. `running={false}` /
13
+ * prefers-reduced-motion show the filled bars and the chip. Keyframes
14
+ * live in AppMock.module.css (`.scholiq` section). No orange.
9
15
  */
10
16
 
11
17
  import React from 'react';
@@ -30,7 +36,7 @@ export default function ScholiqMock() {
30
36
  <div className={styles.bell}></div>
31
37
  <div className={styles.avatar}></div>
32
38
  </div>
33
- <div className={[styles.body, styles.opencatalogi].filter(Boolean).join(' ')}>
39
+ <div className={[styles.body, styles.opencatalogi, styles.scholiq].filter(Boolean).join(' ')}>
34
40
  <div className={styles.nav}>
35
41
  <div className={styles.navHead}><div className={styles.h}></div><div className={styles.l}></div></div>
36
42
  {[true, false, false, false].map((active, i) => (
@@ -54,14 +60,14 @@ export default function ScholiqMock() {
54
60
  <div className={styles.ico}></div>
55
61
  <div className={styles.row}></div>
56
62
  <div className={styles.row + ' ' + styles.short}></div>
57
- <div style={{height: 4, background: 'var(--c-cobalt-100)', borderRadius: 2}}>
58
- <div style={{width: fill, height: 4, background: 'var(--c-forest-500)', borderRadius: 2}}></div>
63
+ <div className={styles.progress}>
64
+ <div className={styles.progressFill} style={{width: fill, animationDelay: `${i * 250}ms`}}></div>
59
65
  </div>
60
66
  {done && (
61
67
  /* Credential chip — course completed, certificate issued */
62
- <div style={{display: 'inline-flex', alignItems: 'center', gap: 3, padding: '2px 6px', borderRadius: 999, background: 'var(--c-forest-300)', alignSelf: 'flex-start'}}>
63
- <div style={{width: 6, height: 7, clipPath: 'var(--hex-pointy-top)', background: 'var(--c-forest-500)'}}></div>
64
- <div style={{height: 3, width: 22, background: 'var(--c-forest-500)', borderRadius: 1}}></div>
68
+ <div className={styles.credChip}>
69
+ <div className={styles.credHex}></div>
70
+ <div className={styles.credBar}></div>
65
71
  </div>
66
72
  )}
67
73
  </div>
@@ -6,15 +6,23 @@
6
6
  * description, two amount columns (net / VAT), and a reconciliation
7
7
  * tick column where a mint hex = matched against the bank and a
8
8
  * muted pip = still open. Tone: terracotta — documents, human work.
9
+ *
10
+ * Animation (wave 2): a bank statement line slides down onto invoice
11
+ * row 3, both tick mint (reconciled), the totals line and the first
12
+ * KPI grow to absorb the match. Base styles are the reconciled end
13
+ * state; keyframes replay the unmatched before-state and the slide.
9
14
  */
10
15
 
11
16
  import React from 'react';
12
17
  import styles from '../AppMock.module.css';
13
18
 
19
+ /* Row 3 (index 2) is the row the bank line reconciles: its tick is
20
+ mint in the base (end) state and the amShqTick keyframe replays
21
+ the open cobalt-200 before-state. Row 5 stays open. */
14
22
  const INVOICES = [
15
23
  {amount: 26, vat: 16, matched: true},
16
24
  {amount: 20, vat: 12, matched: true},
17
- {amount: 30, vat: 18, matched: false},
25
+ {amount: 30, vat: 18, matched: true, reconciling: true},
18
26
  {amount: 22, vat: 14, matched: true},
19
27
  {amount: 18, vat: 10, matched: false},
20
28
  ];
@@ -29,7 +37,7 @@ export default function ShillinqMock() {
29
37
  <div className={styles.bell}></div>
30
38
  <div className={styles.avatar}></div>
31
39
  </div>
32
- <div className={[styles.body, styles.decidesk].filter(Boolean).join(' ')}>
40
+ <div className={[styles.body, styles.decidesk, styles.shillinq].filter(Boolean).join(' ')}>
33
41
  <div className={styles.nav}>
34
42
  <div className={styles.navHead}><div className={styles.h}></div><div className={styles.l}></div></div>
35
43
  {[true, false, false, false, false].map((active, i) => (
@@ -47,14 +55,15 @@ export default function ShillinqMock() {
47
55
  <div className={styles.btn}></div>
48
56
  </div>
49
57
  </div>
50
- {/* VAT / summary strip */}
58
+ {/* VAT / summary strip. The first KPI's number widens when
59
+ the reconciliation lands. */}
51
60
  <div className={styles.kpiRow}>
52
- <div className={styles.kpi}>
53
- <div className={styles.ico} style={{background: 'var(--c-terracotta-500)'}}></div>
61
+ <div className={[styles.kpi, styles.kpiGrow].join(' ')}>
62
+ <div className={[styles.ico, styles.terra].join(' ')}></div>
54
63
  <div className={styles.meta}><div className={styles.num}></div><div className={styles.label}></div></div>
55
64
  </div>
56
65
  <div className={styles.kpi}>
57
- <div className={styles.ico} style={{background: 'var(--c-terracotta-300)'}}></div>
66
+ <div className={[styles.ico, styles.terraSoft].join(' ')}></div>
58
67
  <div className={styles.meta}><div className={styles.num}></div><div className={styles.label}></div></div>
59
68
  </div>
60
69
  <div className={styles.kpi}>
@@ -63,24 +72,31 @@ export default function ShillinqMock() {
63
72
  </div>
64
73
  </div>
65
74
  {/* Invoice table: doc · description · net · VAT · reconciled */}
66
- <div className={styles.panel} style={{flex: 1}}>
75
+ <div className={[styles.panel, styles.ledger].join(' ')}>
67
76
  <div className={styles.head}><div className={styles.title}></div></div>
77
+ {/* Bank statement line — slides onto row 3 and dissolves
78
+ into the mint tick. Hidden at rest and in static frames. */}
79
+ <div className={styles.bankLine}>
80
+ <div className={styles.bIco}></div>
81
+ <div className={styles.bBar}></div>
82
+ <div className={styles.bAmt}></div>
83
+ </div>
68
84
  <div className={styles.stack}>
69
- {INVOICES.map(({amount, vat, matched}, i) => (
70
- <div key={i} className={styles.item}>
71
- <div style={{width: 11, height: 13, clipPath: 'var(--hex-pointy-top)', background: 'var(--c-terracotta-300)', flexShrink: 0}}></div>
85
+ {INVOICES.map(({amount, vat, matched, reconciling}, i) => (
86
+ <div key={i} className={[styles.item, styles.inv, reconciling && styles.reconciling].filter(Boolean).join(' ')}>
87
+ <div className={styles.doc}></div>
72
88
  <div className={styles.lines}><div className={styles.l1}></div></div>
73
- <div style={{width: amount, height: 4, background: 'var(--c-cobalt-700)', borderRadius: 1, flexShrink: 0}}></div>
74
- <div style={{width: vat, height: 4, background: 'var(--c-cobalt-300)', borderRadius: 1, flexShrink: 0}}></div>
89
+ <div className={styles.net} style={{width: amount}}></div>
90
+ <div className={styles.vat} style={{width: vat}}></div>
75
91
  {/* Bank-reconciliation tick: mint hex = matched */}
76
- <div style={{width: 9, height: 10, clipPath: 'var(--hex-pointy-top)', background: matched ? 'var(--c-mint-500)' : 'var(--c-cobalt-200)', flexShrink: 0}}></div>
92
+ <div className={[styles.tick, !matched && styles.open].filter(Boolean).join(' ')}></div>
77
93
  </div>
78
94
  ))}
79
95
  </div>
80
- {/* Totals line */}
81
- <div style={{display: 'flex', justifyContent: 'flex-end', gap: 6, paddingTop: 4, borderTop: '1px solid var(--c-cobalt-100)'}}>
82
- <div style={{width: 32, height: 6, background: 'var(--c-cobalt-900)', borderRadius: 1}}></div>
83
- <div style={{width: 20, height: 6, background: 'var(--c-cobalt-400)', borderRadius: 1}}></div>
96
+ {/* Totals line — grows when the match lands */}
97
+ <div className={styles.totals}>
98
+ <div className={styles.totNet}></div>
99
+ <div className={styles.totVat}></div>
84
100
  </div>
85
101
  </div>
86
102
  </div>
@@ -6,6 +6,13 @@
6
6
  * (mint = stable, orange = update available, red = end-of-life) plus
7
7
  * a graph card on top showing licence-renewal timeline. Left nav for
8
8
  * apps / licences / contracts / dependencies / discovery / archive.
9
+ *
10
+ * Animation (wave 4, the CVE scan): a scan band sweeps down the app
11
+ * list, row 4's pip flips to red as the sweep crosses it (a CVE
12
+ * found), and the matching chart bar surges in vermillion. The chart
13
+ * keeps its single KNVB-orange accent bar; the CVE reading is
14
+ * red-family throughout so the component stays at one orange.
15
+ * Base styles are the post-scan end state (red pip, tall CVE bar).
9
16
  */
10
17
 
11
18
  import React from 'react';
@@ -21,7 +28,7 @@ export default function SoftwareCatalogMock() {
21
28
  <div className={styles.bell}></div>
22
29
  <div className={styles.avatar}></div>
23
30
  </div>
24
- <div className={[styles.body, styles.openregister].filter(Boolean).join(' ')}>
31
+ <div className={[styles.body, styles.openregister, styles.swc].filter(Boolean).join(' ')}>
25
32
  <div className={styles.nav}>
26
33
  <div className={styles.navHead}><div className={styles.h}></div><div className={styles.l}></div></div>
27
34
  {[true, false, false, false, false, false].map((active, i) => (
@@ -40,7 +47,8 @@ export default function SoftwareCatalogMock() {
40
47
  <div className={styles.bars}>
41
48
  <div className={styles.bar} style={{height: '30%'}}></div>
42
49
  <div className={styles.bar} style={{height: '50%'}}></div>
43
- <div className={styles.bar} style={{height: '40%'}}></div>
50
+ {/* CVE bar surges (vermillion, red family) post-scan */}
51
+ <div className={[styles.bar, styles.cve].join(' ')} style={{height: '64%'}}></div>
44
52
  <div className={styles.bar} style={{height: '70%'}}></div>
45
53
  <div className={[styles.bar, styles.accent].join(' ')} style={{height: '88%'}}></div>
46
54
  <div className={styles.bar} style={{height: '60%'}}></div>
@@ -49,16 +57,18 @@ export default function SoftwareCatalogMock() {
49
57
  </div>
50
58
  <div className={styles.axis}></div>
51
59
  </div>
52
- {/* App list with status pips */}
53
- <div className={[styles.w, styles['w-jira']].join(' ')}>
60
+ {/* App list with status pips. The scan band sweeps this
61
+ panel; row 4 (.blocked, .cveHit) flips red as it passes. */}
62
+ <div className={[styles.w, styles['w-jira'], styles.scanPanel].join(' ')}>
54
63
  <div className={styles.wHead}>
55
64
  <div className={styles.h}></div><div className={styles.t}></div>
56
65
  </div>
66
+ <div className={styles.scanBand}></div>
57
67
  <div className={styles.list}>
58
68
  <div className={styles.item}><div className={styles.id}></div><div className={styles.l}></div><div className={styles.pip}></div></div>
59
69
  <div className={[styles.item, styles.review].join(' ')}><div className={styles.id}></div><div className={styles.l}></div><div className={styles.pip}></div></div>
60
70
  <div className={styles.item}><div className={styles.id}></div><div className={styles.l}></div><div className={styles.pip}></div></div>
61
- <div className={[styles.item, styles.blocked].join(' ')}><div className={styles.id}></div><div className={styles.l}></div><div className={styles.pip}></div></div>
71
+ <div className={[styles.item, styles.blocked, styles.cveHit].join(' ')}><div className={styles.id}></div><div className={styles.l}></div><div className={styles.pip}></div></div>
62
72
  <div className={styles.item}><div className={styles.id}></div><div className={styles.l}></div><div className={styles.pip}></div></div>
63
73
  <div className={styles.item}><div className={styles.id}></div><div className={styles.l}></div><div className={styles.pip}></div></div>
64
74
  <div className={[styles.item, styles.todo].join(' ')}><div className={styles.id}></div><div className={styles.l}></div><div className={styles.pip}></div></div>
@@ -6,6 +6,13 @@
6
6
  * (werkvoorraad widget) with stage pips, plus a citizen-side timeline
7
7
  * of recent submissions. Left nav for queue / archive / public portal /
8
8
  * audit / settings.
9
+ *
10
+ * Animation (wave 4): a citizen submission travels up from the
11
+ * submissions list into the werkvoorraad — the ghost row rises, and
12
+ * the new case row at the top of the queue gains its stage hex and
13
+ * assignee avatar as it lands. The late row's red stage pip pulses
14
+ * once per loop as the SLA reminder. Base styles are the assigned
15
+ * end state (new case present, fully dressed).
9
16
  */
10
17
 
11
18
  import React from 'react';
@@ -21,7 +28,7 @@ export default function ZaakAfhandelAppMock() {
21
28
  <div className={styles.bell}></div>
22
29
  <div className={styles.avatar}></div>
23
30
  </div>
24
- <div className={[styles.body, styles.procest].filter(Boolean).join(' ')}>
31
+ <div className={[styles.body, styles.procest, styles.zaak].filter(Boolean).join(' ')}>
25
32
  <div className={styles.nav}>
26
33
  <div className={styles.navHead}><div className={styles.h}></div><div className={styles.l}></div></div>
27
34
  {[true, false, false, false, false].map((active, i) => (
@@ -39,26 +46,34 @@ export default function ZaakAfhandelAppMock() {
39
46
  <div className={styles.btn}></div>
40
47
  </div>
41
48
  </div>
42
- {/* Case-worker werkvoorraad */}
49
+ {/* Case-worker werkvoorraad. The first row (.zkNew) is the
50
+ citizen submission after landing: its stage hex and
51
+ assignee avatar dress it as it arrives. */}
43
52
  <div className={[styles.w, styles['w-werkvoorraad']].join(' ')}>
44
53
  <div className={styles.wHead}>
45
54
  <div className={styles.h}></div><div className={styles.t}></div>
46
55
  </div>
47
56
  <div className={styles.list}>
57
+ <div className={[styles.item, styles.zkNew].join(' ')}><div className={styles.stage}></div><div className={styles.l1}></div><div className={[styles.av, styles.b].join(' ')}></div></div>
48
58
  <div className={[styles.item, styles.now].join(' ')}><div className={styles.stage}></div><div className={styles.l1}></div><div className={styles.av}></div></div>
49
59
  <div className={styles.item}><div className={styles.stage}></div><div className={styles.l1}></div><div className={[styles.av, styles.b].join(' ')}></div></div>
50
60
  <div className={[styles.item, styles.late].join(' ')}><div className={styles.stage}></div><div className={styles.l1}></div><div className={[styles.av, styles.c].join(' ')}></div></div>
51
61
  <div className={styles.item}><div className={styles.stage}></div><div className={styles.l1}></div><div className={styles.av}></div></div>
52
- <div className={styles.item}><div className={styles.stage}></div><div className={styles.l1}></div><div className={[styles.av, styles.b].join(' ')}></div></div>
53
62
  </div>
54
63
  </div>
64
+ {/* Travelling submission ghost — rises from the submissions
65
+ list into the werkvoorraad. Hidden at rest + static. */}
66
+ <div className={styles.zkGhost}>
67
+ <div className={styles.src}></div>
68
+ <div className={styles.gBar}></div>
69
+ </div>
55
70
  {/* Citizen-side recent submissions list */}
56
71
  <div className={[styles.w, styles['w-rss']].join(' ')}>
57
72
  <div className={styles.wHead}>
58
73
  <div className={styles.h}></div><div className={styles.t}></div>
59
74
  </div>
60
75
  <div className={styles.list}>
61
- <div className={styles.item}><div className={styles.src}></div><div className={styles.title}></div><div className={styles.when}></div></div>
76
+ <div className={[styles.item, styles.zkSent].join(' ')}><div className={styles.src}></div><div className={styles.title}></div><div className={styles.when}></div></div>
62
77
  <div className={styles.item}><div className={styles.src}></div><div className={styles.title}></div><div className={styles.when}></div></div>
63
78
  <div className={styles.item}><div className={styles.src}></div><div className={styles.title}></div><div className={styles.when}></div></div>
64
79
  <div className={styles.item}><div className={styles.src}></div><div className={styles.title}></div><div className={styles.when}></div></div>
@@ -12,11 +12,13 @@
12
12
  * Five nodes: trigger → two parallel steps → approval (join) → end.
13
13
  * A mint run-line travels the trigger → step → approval → end route
14
14
  * on a slow loop, with a subtle halo pulse on each node as the line
15
- * passes. `prefers-reduced-motion: reduce` (and `running={false}`)
16
- * renders the full route as a static mint path with no animation.
15
+ * passes. At the approval node the run HOLDS for ~600ms while an
16
+ * orange halo flashes a human decides here. `prefers-reduced-motion:
17
+ * reduce` (and `running={false}`) renders the full route as a static
18
+ * mint path with no animation and no flash.
17
19
  *
18
20
  * Tokens only; no images; rectangles (the nodes *do* work, so no
19
- * hexes); no orange accent used.
21
+ * hexes); the approval hold flash is the single orange accent.
20
22
  *
21
23
  * Usage:
22
24
  *
@@ -9,7 +9,10 @@
9
9
  * transparent to layout.
10
10
  *
11
11
  * Tokens only. Node bars: mint = trigger, lavender = steps/approval,
12
- * vermillion = end. The run-line is mint (--c-mint-500); no orange.
12
+ * vermillion = end. The run-line is mint (--c-mint-500). The run
13
+ * holds ~600ms at the approval node while an orange halo flashes
14
+ * there — "a human decides here" — and that flash is this
15
+ * component's single orange accent.
13
16
  */
14
17
 
15
18
  .fm { display: contents; }
@@ -102,7 +105,13 @@
102
105
  * travels the full route once per cycle. Animating the offset from
103
106
  * 1.28 to 0 shifts the pattern forward exactly one period, so the
104
107
  * loop is seamless (the segment exits the end node as its successor
105
- * enters the trigger). */
108
+ * enters the trigger).
109
+ *
110
+ * The offset keyframes are not linear end-to-end: the run reaches the
111
+ * approval node at 58% (offset 1.28 × (1 − 0.62) ≈ 0.487, the node
112
+ * sitting ~62% along the route) and HOLDS there for ~600ms (8.6% of
113
+ * the 7s cycle) — approval is where a human decides — before
114
+ * continuing to the end node. */
106
115
 
107
116
  .fm .runline {
108
117
  fill: none;
@@ -115,13 +124,17 @@
115
124
  }
116
125
 
117
126
  @keyframes fmRun {
118
- from { stroke-dashoffset: 1.28; }
119
- to { stroke-dashoffset: 0; }
127
+ 0% { stroke-dashoffset: 1.28; }
128
+ 58% { stroke-dashoffset: 0.487; } /* segment reaches the approval node */
129
+ 66.6% { stroke-dashoffset: 0.487; } /* ~600ms hold: a human decides */
130
+ 100% { stroke-dashoffset: 0; }
120
131
  }
121
132
 
122
133
  /* Halo pulse on the nodes the run-line passes. Delays approximate
123
- where along the route each node sits (trigger 0%, step ~30%,
124
- approval ~62%, end ~85% of the cycle). */
134
+ where along the (held) cycle each node sits: trigger 0%, step ~28%,
135
+ approval 58% (the hold), end ~87%. The approval halo is the
136
+ exception: it flashes KNVB orange for the length of the hold — the
137
+ component's one orange accent. */
125
138
  .fm .halo {
126
139
  fill: none;
127
140
  stroke: var(--c-mint-500);
@@ -130,9 +143,13 @@
130
143
  animation: fmPulse var(--fm-dur, 7s) linear infinite;
131
144
  }
132
145
  .fm .halo-0 { animation-delay: 0s; }
133
- .fm .halo-1 { animation-delay: calc(var(--fm-dur, 7s) * 0.3); }
134
- .fm .halo-2 { animation-delay: calc(var(--fm-dur, 7s) * 0.62); }
135
- .fm .halo-3 { animation-delay: calc(var(--fm-dur, 7s) * 0.85); }
146
+ .fm .halo-1 { animation-delay: calc(var(--fm-dur, 7s) * 0.28); }
147
+ .fm .halo-2 {
148
+ stroke: var(--c-orange-knvb);
149
+ animation-name: fmPulseHold;
150
+ animation-delay: calc(var(--fm-dur, 7s) * 0.58);
151
+ }
152
+ .fm .halo-3 { animation-delay: calc(var(--fm-dur, 7s) * 0.87); }
136
153
 
137
154
  @keyframes fmPulse {
138
155
  0% { opacity: 0; }
@@ -141,6 +158,16 @@
141
158
  100% { opacity: 0; }
142
159
  }
143
160
 
161
+ /* The approval flash spans the whole ~600ms hold (8.6% of the cycle)
162
+ instead of the quick pass-through pulse. */
163
+ @keyframes fmPulseHold {
164
+ 0% { opacity: 0; }
165
+ 1.5% { opacity: 0.8; }
166
+ 8% { opacity: 0.8; }
167
+ 11% { opacity: 0; }
168
+ 100% { opacity: 0; }
169
+ }
170
+
144
171
  /* ---- Static renderings ----
145
172
  *
146
173
  * Both `running={false}` (the .static class) and the user's
@@ -0,0 +1,83 @@
1
+ /**
2
+ * FlowMock.render.test.js — SSR-renders the real <FlowMock> and
3
+ * asserts the canvas structure the approval-hold animation relies on:
4
+ * the run-line with pathLength="1", the four halo rects (halo-2 being
5
+ * the approval flash), and the `running` prop's static class. Same
6
+ * esbuild harness as KanbanMock.render.test.js; CSS modules stubbed
7
+ * to identity.
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const test = require('node:test');
13
+ const {before, after} = test;
14
+ const assert = require('node:assert/strict');
15
+ const path = require('node:path');
16
+ const fs = require('node:fs/promises');
17
+ const {build} = require('esbuild');
18
+ const React = require('react');
19
+ const {renderToStaticMarkup} = require('react-dom/server');
20
+
21
+ const COMPONENT = path.resolve(__dirname, '..', 'FlowMock.jsx');
22
+ const PRESET_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
23
+ const SCRATCH = path.join(PRESET_ROOT, '.tmp-flow-mock-test');
24
+
25
+ const cssModuleStub = {
26
+ name: 'css-module-stub',
27
+ setup(b) {
28
+ b.onResolve({filter: /\.module\.css$/}, (args) => ({path: args.path, namespace: 'css-stub'}));
29
+ b.onLoad({filter: /.*/, namespace: 'css-stub'}, () => ({
30
+ contents: 'export default new Proxy({}, {get: (_, p) => p});',
31
+ loader: 'js',
32
+ }));
33
+ },
34
+ };
35
+
36
+ let FlowMock;
37
+
38
+ before(async () => {
39
+ await fs.mkdir(SCRATCH, {recursive: true});
40
+ const tmpDir = await fs.mkdtemp(path.join(SCRATCH, 'run-'));
41
+ const outFile = path.join(tmpDir, 'bundle.cjs');
42
+ await build({
43
+ entryPoints: [COMPONENT],
44
+ outfile: outFile,
45
+ bundle: true,
46
+ format: 'cjs',
47
+ jsx: 'automatic',
48
+ jsxImportSource: 'react',
49
+ tsconfigRaw: {compilerOptions: {jsx: 'react-jsx', jsxImportSource: 'react'}},
50
+ platform: 'node',
51
+ external: ['react'],
52
+ plugins: [cssModuleStub],
53
+ logLevel: 'warning',
54
+ });
55
+ delete require.cache[require.resolve(outFile)];
56
+ FlowMock = require(outFile).default;
57
+ });
58
+
59
+ after(async () => {
60
+ await fs.rm(SCRATCH, {recursive: true, force: true});
61
+ });
62
+
63
+ function render(props) {
64
+ return renderToStaticMarkup(React.createElement(FlowMock, props));
65
+ }
66
+
67
+ test('SSR smoke: renders the canvas with the run-line on pathLength="1"', () => {
68
+ const html = render({});
69
+ assert.match(html, /class="canvas"/);
70
+ assert.match(html, /class="runline"[^>]*pathLength="1"/);
71
+ });
72
+
73
+ test('halos: four pulse rects, halo-2 (the approval hold flash) among them', () => {
74
+ const html = render({});
75
+ for (const i of [0, 1, 2, 3]) {
76
+ assert.match(html, new RegExp(`halo halo-${i}`), `missing halo-${i}`);
77
+ }
78
+ });
79
+
80
+ test('running={false} renders the static frame class', () => {
81
+ assert.match(render({running: false}), /class="frame size-md static"/);
82
+ assert.doesNotMatch(render({}), /\bstatic\b/);
83
+ });