@conduction/docusaurus-preset 3.33.0 → 3.34.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 (25) hide show
  1. package/MISSING_COMPONENTS.md +5 -4
  2. package/package.json +1 -1
  3. package/src/components/AgentTrace/AgentTrace.jsx +26 -5
  4. package/src/components/AgentTrace/AgentTrace.module.css +41 -2
  5. package/src/components/AgentTrace/__tests__/AgentTrace.render.test.js +108 -0
  6. package/src/components/AppMock/AppMock.jsx +14 -5
  7. package/src/components/AppMock/AppMock.module.css +439 -0
  8. package/src/components/AppMock/__tests__/AppMock.render.test.js +181 -0
  9. package/src/components/AppMock/variants/LarpingAppMock.jsx +23 -4
  10. package/src/components/AppMock/variants/LaunchPadBiMock.jsx +12 -5
  11. package/src/components/AppMock/variants/LaunchPadTilesMock.jsx +6 -1
  12. package/src/components/AppMock/variants/LaunchPadWidgetsMock.jsx +17 -8
  13. package/src/components/AppMock/variants/NLDesignMock.jsx +15 -4
  14. package/src/components/AppMock/variants/PipelinQMock.jsx +12 -2
  15. package/src/components/AppMock/variants/PortaliqMock.jsx +21 -9
  16. package/src/components/AppMock/variants/ScholiqMock.jsx +12 -6
  17. package/src/components/FlowMock/FlowMock.jsx +5 -3
  18. package/src/components/FlowMock/FlowMock.module.css +36 -9
  19. package/src/components/FlowMock/__tests__/FlowMock.render.test.js +83 -0
  20. package/src/components/LeafMock/LeafMock.jsx +23 -8
  21. package/src/components/LeafMock/LeafMock.module.css +123 -0
  22. package/src/components/LeafMock/__tests__/LeafMock.render.test.js +105 -0
  23. package/src/components/MockScene/MockScene.jsx +16 -2
  24. package/src/components/MockScene/MockScene.module.css +22 -2
  25. package/src/components/MockScene/__tests__/MockScene.render.test.js +101 -0
@@ -5,6 +5,13 @@
5
5
  * on a register without ETL. The mock shows four graph cards in a
6
6
  * 2×2 grid (bar chart, line chart, donut KPI, second bar with a
7
7
  * different accent), plus a ranked-list table widget on the right.
8
+ *
9
+ * Animated (9s loop, --am-dur): the charts draw themselves — bars
10
+ * grow from the axis in a stagger, the line draws along its path
11
+ * (pathLength="1" dash technique) and its end dot pops, the donut
12
+ * sweeps in, and the KPI delta bars fade up (inline delays).
13
+ * `running={false}` / prefers-reduced-motion show the drawn charts.
14
+ * Keyframes live in AppMock.module.css (`.lpBi` section).
8
15
  */
9
16
 
10
17
  import React from 'react';
@@ -20,7 +27,7 @@ export default function LaunchPadBiMock() {
20
27
  <div className={styles.bell}></div>
21
28
  <div className={styles.avatar}></div>
22
29
  </div>
23
- <div className={[styles.body, styles.launchpad].filter(Boolean).join(' ')}>
30
+ <div className={[styles.body, styles.launchpad, styles.lpBi].filter(Boolean).join(' ')}>
24
31
  <div className={styles.grid}>
25
32
  {/* Column 1: Bar chart on top, line chart below */}
26
33
  <div className={styles.col}>
@@ -45,7 +52,7 @@ export default function LaunchPadBiMock() {
45
52
  <div className={styles.chart}>
46
53
  <svg viewBox="0 0 100 40" preserveAspectRatio="none">
47
54
  <path className={styles.fill} d="M0,32 L14,24 L28,28 L42,18 L56,22 L70,12 L84,16 L100,8 L100,40 L0,40 Z"/>
48
- <path className={styles.stroke} d="M0,32 L14,24 L28,28 L42,18 L56,22 L70,12 L84,16 L100,8"/>
55
+ <path className={styles.stroke} pathLength="1" d="M0,32 L14,24 L28,28 L42,18 L56,22 L70,12 L84,16 L100,8"/>
49
56
  <circle className={styles.dot} cx="100" cy="8" r="2.5"/>
50
57
  </svg>
51
58
  </div>
@@ -89,7 +96,7 @@ export default function LaunchPadBiMock() {
89
96
  </div>
90
97
  <div style={{display: 'flex', alignItems: 'baseline', gap: 4}}>
91
98
  <div style={{height: 12, width: 28, background: 'var(--c-cobalt-900)', borderRadius: 1}}></div>
92
- <div style={{height: 4, width: 16, background: 'var(--c-mint-500)', borderRadius: 1}}></div>
99
+ <div className={styles.biDelta} style={{height: 4, width: 16, background: 'var(--c-mint-500)', borderRadius: 1}}></div>
93
100
  </div>
94
101
  <div style={{height: 3, width: '70%', background: 'var(--c-cobalt-200)', borderRadius: 1}}></div>
95
102
  </div>
@@ -99,7 +106,7 @@ export default function LaunchPadBiMock() {
99
106
  </div>
100
107
  <div style={{display: 'flex', alignItems: 'baseline', gap: 4}}>
101
108
  <div style={{height: 12, width: 22, background: 'var(--c-cobalt-900)', borderRadius: 1}}></div>
102
- <div style={{height: 4, width: 14, background: 'var(--c-orange-knvb)', borderRadius: 1}}></div>
109
+ <div className={styles.biDelta} style={{height: 4, width: 14, background: 'var(--c-orange-knvb)', borderRadius: 1, animationDelay: '0.2s'}}></div>
103
110
  </div>
104
111
  <div style={{height: 3, width: '60%', background: 'var(--c-cobalt-200)', borderRadius: 1}}></div>
105
112
  </div>
@@ -109,7 +116,7 @@ export default function LaunchPadBiMock() {
109
116
  </div>
110
117
  <div style={{display: 'flex', alignItems: 'baseline', gap: 4}}>
111
118
  <div style={{height: 12, width: 30, background: 'var(--c-cobalt-900)', borderRadius: 1}}></div>
112
- <div style={{height: 4, width: 12, background: 'var(--c-mint-500)', borderRadius: 1}}></div>
119
+ <div className={styles.biDelta} style={{height: 4, width: 12, background: 'var(--c-mint-500)', borderRadius: 1, animationDelay: '0.4s'}}></div>
113
120
  </div>
114
121
  <div style={{height: 3, width: '80%', background: 'var(--c-cobalt-200)', borderRadius: 1}}></div>
115
122
  </div>
@@ -7,6 +7,11 @@
7
7
  * a sub-grid of further tiles. The mock pictures exactly that mix:
8
8
  * four launcher tiles top-left, a 2×2 sub-grid bottom-left ("custom
9
9
  * tile group"), and two info widgets right.
10
+ *
11
+ * Animated (7s loop, --am-dur): the custom tile group is pressed
12
+ * (a short scale dip) and unfolds its four sub-tiles in a stagger.
13
+ * `running={false}` / prefers-reduced-motion show the unfolded
14
+ * group. Keyframes live in AppMock.module.css (`.lpTiles` section).
10
15
  */
11
16
 
12
17
  import React from 'react';
@@ -22,7 +27,7 @@ export default function LaunchPadTilesMock() {
22
27
  <div className={styles.bell}></div>
23
28
  <div className={styles.avatar}></div>
24
29
  </div>
25
- <div className={[styles.body, styles.launchpad].filter(Boolean).join(' ')}>
30
+ <div className={[styles.body, styles.launchpad, styles.lpTiles].filter(Boolean).join(' ')}>
26
31
  <div className={styles.grid}>
27
32
  {/* Column 1: Launcher tiles in cobalt + KNVB */}
28
33
  <div className={styles.col}>
@@ -11,11 +11,20 @@
11
11
  * - Jira → external-app status board
12
12
  * - RSS → headlines feed
13
13
  * - Video → video-call shortcut
14
+ *
15
+ * Animated (10s loop, --am-dur): the dashboard assembles — the seven
16
+ * widgets arrive one by one on a 900ms stagger (inline
17
+ * animation-delay, reading order), hold, then fade for the reset.
18
+ * `running={false}` / prefers-reduced-motion show the assembled
19
+ * board. Keyframes live in AppMock.module.css (`.lpWidgets` section).
14
20
  */
15
21
 
16
22
  import React from 'react';
17
23
  import styles from '../AppMock.module.css';
18
24
 
25
+ /* Per-widget arrival delay on the shared stagger. */
26
+ const ARRIVE = (i) => ({animationDelay: `${i * 900}ms`});
27
+
19
28
  export default function LaunchPadWidgetsMock() {
20
29
  return (
21
30
  <>
@@ -26,11 +35,11 @@ export default function LaunchPadWidgetsMock() {
26
35
  <div className={styles.bell}></div>
27
36
  <div className={styles.avatar}></div>
28
37
  </div>
29
- <div className={[styles.body, styles.launchpad].filter(Boolean).join(' ')}>
38
+ <div className={[styles.body, styles.launchpad, styles.lpWidgets].filter(Boolean).join(' ')}>
30
39
  <div className={styles.grid}>
31
40
  {/* Column 1: Procest werkvoorraad + DocuDesk upload */}
32
41
  <div className={styles.col}>
33
- <div className={[styles.w, styles['w-werkvoorraad']].join(' ')}>
42
+ <div className={[styles.w, styles['w-werkvoorraad']].join(' ')} style={ARRIVE(0)}>
34
43
  <div className={styles.wHead}>
35
44
  <div className={styles.h}></div><div className={styles.t}></div>
36
45
  </div>
@@ -42,7 +51,7 @@ export default function LaunchPadWidgetsMock() {
42
51
  <div className={styles.item}><div className={styles.stage}></div><div className={styles.l1}></div><div className={[styles.av, styles.b].join(' ')}></div></div>
43
52
  </div>
44
53
  </div>
45
- <div className={[styles.w, styles['w-upload']].join(' ')}>
54
+ <div className={[styles.w, styles['w-upload']].join(' ')} style={ARRIVE(1)}>
46
55
  <div className={styles.wHead}>
47
56
  <div className={styles.h}></div><div className={styles.t}></div>
48
57
  </div>
@@ -53,7 +62,7 @@ export default function LaunchPadWidgetsMock() {
53
62
  </div>
54
63
  </div>
55
64
  {/* Column 2: Mail (Important mail) */}
56
- <div className={[styles.w, styles['w-mail']].join(' ')}>
65
+ <div className={[styles.w, styles['w-mail']].join(' ')} style={ARRIVE(2)}>
57
66
  <div className={styles.wHead}>
58
67
  <div className={styles.h}></div><div className={styles.t}></div>
59
68
  </div>
@@ -71,7 +80,7 @@ export default function LaunchPadWidgetsMock() {
71
80
  </div>
72
81
  {/* Column 3: Jira status board + RSS feed */}
73
82
  <div className={styles.col}>
74
- <div className={[styles.w, styles['w-jira']].join(' ')}>
83
+ <div className={[styles.w, styles['w-jira']].join(' ')} style={ARRIVE(3)}>
75
84
  <div className={styles.wHead}>
76
85
  <div className={styles.h}></div><div className={styles.t}></div>
77
86
  </div>
@@ -83,7 +92,7 @@ export default function LaunchPadWidgetsMock() {
83
92
  <div className={styles.item}><div className={styles.id}></div><div className={styles.l}></div><div className={styles.pip}></div></div>
84
93
  </div>
85
94
  </div>
86
- <div className={[styles.w, styles['w-rss']].join(' ')}>
95
+ <div className={[styles.w, styles['w-rss']].join(' ')} style={ARRIVE(4)}>
87
96
  <div className={styles.wHead}>
88
97
  <div className={styles.h}></div><div className={styles.t}></div>
89
98
  </div>
@@ -96,7 +105,7 @@ export default function LaunchPadWidgetsMock() {
96
105
  </div>
97
106
  {/* Column 4: Calendar + Video call shortcut */}
98
107
  <div className={styles.col}>
99
- <div className={[styles.w, styles['w-calendar']].join(' ')}>
108
+ <div className={[styles.w, styles['w-calendar']].join(' ')} style={ARRIVE(5)}>
100
109
  <div className={styles.wHead}>
101
110
  <div className={styles.h}></div><div className={styles.t}></div>
102
111
  </div>
@@ -106,7 +115,7 @@ export default function LaunchPadWidgetsMock() {
106
115
  ))}
107
116
  </div>
108
117
  </div>
109
- <div className={[styles.w, styles['w-video']].join(' ')}>
118
+ <div className={[styles.w, styles['w-video']].join(' ')} style={ARRIVE(6)}>
110
119
  <div className={styles.wHead}>
111
120
  <div className={styles.h}></div><div className={styles.t}></div>
112
121
  </div>
@@ -5,6 +5,14 @@
5
5
  * centre shows a settings layout with a colour-swatch row, a type
6
6
  * specimen, and a component preview block. Left nav for theme /
7
7
  * colours / typography / spacing / components / overrides.
8
+ *
9
+ * Animated (6s loop, --am-dur): the lavender swatch is selected (a
10
+ * cobalt focus ring pops on it), the component preview rethemes in a
11
+ * cascade (button, then status pill crossfade to lavender), and the
12
+ * type-scale bars restretch as if the theme changed the scale. The
13
+ * loop reverts by crossfading back. `running={false}` /
14
+ * prefers-reduced-motion show the resting cobalt/mint theme with no
15
+ * ring. Keyframes live in AppMock.module.css (`.nldesign` section).
8
16
  */
9
17
 
10
18
  import React from 'react';
@@ -20,7 +28,7 @@ export default function NLDesignMock() {
20
28
  <div className={styles.bell}></div>
21
29
  <div className={styles.avatar}></div>
22
30
  </div>
23
- <div className={[styles.body, styles.openregister].filter(Boolean).join(' ')}>
31
+ <div className={[styles.body, styles.openregister, styles.nldesign].filter(Boolean).join(' ')}>
24
32
  <div className={styles.nav}>
25
33
  <div className={styles.navHead}><div className={styles.h}></div><div className={styles.l}></div></div>
26
34
  {[true, false, false, false, false, false].map((active, i) => (
@@ -35,15 +43,16 @@ export default function NLDesignMock() {
35
43
  <div className={styles.panel}>
36
44
  <div className={styles.head}><div className={styles.title}></div></div>
37
45
  <div style={{display: 'flex', gap: 6, flexWrap: 'wrap'}}>
46
+ {/* Index 3 (lavender) is the swatch the animation selects. */}
38
47
  {['var(--c-blue-cobalt)','var(--c-orange-knvb)','var(--c-mint-500)','var(--c-lavender-500)','var(--c-forest-500)','var(--c-terracotta-500)','var(--c-cobalt-300)','var(--c-cobalt-100)'].map((bg, i) => (
39
- <span key={i} style={{width: 22, height: 22, borderRadius: 4, background: bg}}></span>
48
+ <span key={i} className={[styles.swatch, i === 3 && styles.swatchSel].filter(Boolean).join(' ')} style={{background: bg}}></span>
40
49
  ))}
41
50
  </div>
42
51
  </div>
43
52
  {/* Type specimen */}
44
53
  <div className={styles.panel}>
45
54
  <div className={styles.head}><div className={styles.title}></div></div>
46
- <div style={{display: 'flex', alignItems: 'baseline', gap: 8}}>
55
+ <div className={styles.typeRow} style={{display: 'flex', alignItems: 'baseline', gap: 8}}>
47
56
  <div style={{height: 14, width: 18, background: 'var(--c-cobalt-900)', borderRadius: 1}}></div>
48
57
  <div style={{height: 11, width: 14, background: 'var(--c-cobalt-700)', borderRadius: 1}}></div>
49
58
  <div style={{height: 8, width: 12, background: 'var(--c-cobalt-400)', borderRadius: 1}}></div>
@@ -56,7 +65,9 @@ export default function NLDesignMock() {
56
65
  {/* Component preview */}
57
66
  <div className={styles.panel}>
58
67
  <div className={styles.head}><div className={styles.title}></div></div>
59
- <div style={{display: 'flex', gap: 6, flexWrap: 'wrap'}}>
68
+ {/* The .actions wrapper is required for .btn to pick up its
69
+ styles (the buttons rendered invisible without it). */}
70
+ <div className={styles.actions} style={{alignItems: 'center', flexWrap: 'wrap'}}>
60
71
  <div className={styles.btn}></div>
61
72
  <div className={styles.btn + ' ' + styles.ghost}></div>
62
73
  <div className={styles.statusPill}><div className={styles.h}></div><div className={styles.t}></div></div>
@@ -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>
@@ -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>
@@ -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
+ });
@@ -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
- * - files: folder/file rows with a link-to-record chip (forest)
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
- <path 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" />
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 && (