@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,129 @@
1
+ # Animation roadmap — wave 6 (planned, not yet implemented)
2
+
3
+ Waves 1–5 of the animated-atomics programme are shipped (see
4
+ `MISSING_COMPONENTS.md` for the per-component coverage). This file
5
+ holds the designs for the two remaining L-size items from the
6
+ animation audit, plus the small registry debts the audit surfaced.
7
+ Nothing in this file is implemented; it is the working spec for the
8
+ next animation PR.
9
+
10
+ Rules carried over from the shipped waves, binding for wave 6 too:
11
+
12
+ - Tokens only; no images, no hardcoded colours.
13
+ - One KNVB-orange accent per component, maximum.
14
+ - Every loop is seamless (opacity-swap resets, no visible rewind).
15
+ - `running={false}` and `prefers-reduced-motion` render the
16
+ meaningful end state, not a blank.
17
+ - Timelines ride the `--am-dur`-style custom-property convention so
18
+ a consumer can retune duration without touching keyframes.
19
+
20
+ ## 1 · Pipeline — the document travels the flow (L)
21
+
22
+ **Component:** `src/components/Pipeline/Pipeline.jsx` +
23
+ `Pipeline.module.css` (React, in the preset).
24
+
25
+ **Audit finding on the claimed dotted-line animation — VERIFIED, it
26
+ exists.** `Pipeline.module.css` draws the full-width dotted flow line
27
+ as `.steps::before` (a `repeating-linear-gradient` dash pattern) and
28
+ animates it with `flow-shift` (0.9s linear infinite,
29
+ `background-position 0 → 10px`, lines 39–67). The stacked mobile
30
+ layout (`max-width: 1100px`) switches to a vertical line with its own
31
+ `flow-shift-vert` twin, and `prefers-reduced-motion: reduce` disables
32
+ both. So the base "the line flows" motion is real and shipping; what
33
+ wave 6 adds is the *payload* making the journey, not the line itself.
34
+
35
+ **Design — a document hex rides the line:**
36
+
37
+ - A small terracotta document hex (pointy-top, ~10×11, the same
38
+ vocabulary as the AppMock `.doc` atoms) spawns at the left end-box
39
+ (the `IconList` "Sources" column), travels the dotted line
40
+ left-to-right, and pauses ~400ms at each step hex's equator.
41
+ - At each pause the step hex answers: a 1.06 scale pulse plus a brief
42
+ ring in the step's own family colour (mint for the OpenConnector
43
+ step, workspace-blue for the platform hex, terracotta for
44
+ OpenCatalogi — never a new orange). The pill inside the hex, when
45
+ present, bumps with it.
46
+ - Past the last step the document fades into the right end-box
47
+ ("Consumers"), whose list items tick top-to-bottom with a 120ms
48
+ stagger (each item's icon tile flashes its border cobalt→mint→rest)
49
+ — the "everyone downstream can see it now" beat.
50
+ - Loop length: `--pl-dur`, default 9s. The traveller is hidden at
51
+ 0%/100% (opacity swap), so the loop is seamless; the dotted line's
52
+ independent 0.9s `flow-shift` keeps running underneath and needs no
53
+ synchronisation.
54
+ - The vertical (stacked) layout re-routes the traveller top-to-bottom
55
+ along the vertical line; same beats.
56
+ - Static / reduced-motion: the traveller is hidden, every step hex at
57
+ rest, the consumer list fully ticked — the delivered end state.
58
+ (`flow-shift` is already disabled by the existing media query.)
59
+ - Implementation note: the traveller must be a sibling of `.steps`
60
+ positioned against the same `top: 85px` midline calc the line uses;
61
+ the hexes' `z-index: 1` will occlude it while it passes "through"
62
+ a hex, which is exactly the right reading.
63
+
64
+ ## 2 · PlatformDiagram — the platform assembles (L, cross-repo)
65
+
66
+ **Component:** the preset's `PlatformDiagram/PlatformDiagram.jsx` is
67
+ a typed wrapper around the bespoke `<platform-diagram>` web component
68
+ that lives **in the design-system repo, not the preset**:
69
+ `preview/components/_lib/platform-diagram.js` +
70
+ `platform-diagram.css`. The animation therefore lands cross-repo: the
71
+ keyframes and progress logic go into `_lib/platform-diagram.{js,css}`
72
+ (runtime CSS is injected by the web component), while the preset
73
+ wrapper only grows a `running` prop it forwards as an attribute.
74
+
75
+ **Today:** the component already reveals on scroll via the
76
+ `--pd-list-progress` / `--pd-hex-progress` custom properties — a
77
+ one-shot entrance, not a loop.
78
+
79
+ **Design — assembly loop:**
80
+
81
+ - Beat 1, the workspace: the centre workspace slab draws in (scaleY
82
+ from the baseline) and its topbar dots pop left-to-right.
83
+ - Beat 2, the corner hexes: the four app hexes fly in from their
84
+ compass corners (translate + fade, 90ms stagger), each with a
85
+ 1.05 settle-bounce as it docks.
86
+ - Beat 3, the flows: the connecting flow lines draw (stroke-dash
87
+ reveal) from workspace to each hex in the same order the hexes
88
+ landed.
89
+ - Beat 4, the lists: each side list's rows land with the shipped
90
+ `.sbLive`-style stagger; the workspace pill flashes mint once when
91
+ the last row is in — "the platform is complete".
92
+ - Hold ~2.5s assembled, then a soft opacity-cascade reset (the shipped
93
+ waves' convention; no rewind).
94
+ - Loop length: `--pd-dur`, default 12s. When a `running="false"`
95
+ attribute (or `prefers-reduced-motion`) is set, the diagram renders
96
+ fully assembled — identical to today's post-scroll-reveal state, so
97
+ the existing scroll-progress path can stay as the reduced-motion
98
+ fallback.
99
+ - Cross-repo sequencing: land the web-component change in
100
+ `preview/components/_lib/` first (the kit page
101
+ `preview/components/platform-overview.html` is the specimen), then
102
+ bump the copy the preset consumes, then add the wrapper prop +
103
+ render test in the preset. Two PRs, one per repo.
104
+
105
+ ## 3 · Pointer — flow-canvas run animation (spec lives elsewhere)
106
+
107
+ The FlowMock atomic (shipped, wave 3) is the marketing abstraction of
108
+ the flow editor. The *product* counterpart — the run-line animation on
109
+ the real flow canvas — is specified in the **nextcloud-vue** repo
110
+ (spec merged; `CnFlowCanvas` run-animation spec). Do not re-specify it
111
+ here: when that lands, FlowMock should stay visually consistent with
112
+ it (mint run-line, ~600ms approval hold, single orange halo at the
113
+ approval node), but the preset carries no implementation work for it.
114
+
115
+ ## 4 · SidebarMock icon-registry stand-ins (S, debt)
116
+
117
+ The audit flagged two tab icons that ship as stand-ins because the
118
+ `IntegrationIcon` registry has no dedicated glyph:
119
+
120
+ - `docudesk-signatures` — the Signatures tab renders the **mail**
121
+ icon. Needs a signature glyph (pen-stroke over a baseline, in the
122
+ registry's stroke style).
123
+ - `docudesk-pii-map` — the PII-map tab renders the **keycloak** icon.
124
+ Needs a PII/redaction glyph (masked-text bar or eye-off).
125
+
126
+ Add both to `IntegrationIcon/registry.js`, switch the two tab
127
+ definitions in `SidebarMock.jsx`, and extend the SidebarMock render
128
+ test to pin the new names. Until then the stand-ins are acceptable at
129
+ mock abstraction level but must not leak into product UI.
@@ -56,12 +56,15 @@ Most consume one or more primitives.
56
56
  - **GameModal** — `GameModal/GameModal.jsx`. End-of-game dialog with cross-game progress tracking. Subscribes to `connext:gameend` events.
57
57
  - **CookieCli** — `CookieCli/CookieCli.jsx`. Terminal-styled cookie consent banner with localStorage persistence.
58
58
  - **ComposeBlock** — `ComposeBlock/ComposeBlock.jsx`. Branded code block (cobalt-900 + Plex Mono + filename pill + copy button). For docker-compose, bash recipes, and any verbatim copy-paste content. Used on `/demo`.
59
- - **AgentTrace** — `AgentTrace/AgentTrace.jsx`. Terminal-styled agent execution trace with cursor + mode chrome. Specimen: `preview/components/agent-trace.html`.
60
- - **AppMock** — `AppMock/AppMock.jsx`. Selector that renders one of 16 app-specific UI mockups (DeciDesk, DocuDesk, LarpingApp, LaunchPad + variants, NLDesign, OpenCatalogi, OpenConnector, OpenRegister, OpenWoo, PipelinQ, Procest, SoftwareCatalog, ZaakAfhandelApp). Each variant lives under `AppMock/variants/`. Specimen: `preview/components/app-mock.html`.
61
- - **FlowMock** — `FlowMock/FlowMock.jsx`. Token-painted abstract of the flow editor: node cards on a dotted canvas with coloured left bars (mint trigger, lavender steps, vermillion end), curved grey edges with pill labels, and a mint run-line looping trigger step approval end. Freezes to the full static route under `prefers-reduced-motion` or `running={false}`. Specimen: `preview/components/flow-mock.html`.
62
- - **LeafMock** — `LeafMock/LeafMock.jsx`. Small integration-leaf panels for Showcase items: `leaf="contacts"` (hex-avatar rows + mint sync arrow to a phone), `"calendar"` (mini month grid + linked-event chip), `"mail"` (message list + mint create-from-email chip), `"files"` (folder/file rows + forest link-to-record chip).
59
+ - **AgentTrace** — `AgentTrace/AgentTrace.jsx`. Terminal-styled agent execution trace with cursor + mode chrome. Types itself out on a loop: lines reveal left-to-right on a 550ms stagger, status dots start after their line, ~2s hold, soft cascade restart; `running={false}` / `prefers-reduced-motion` show the full trace at rest. Specimen: `preview/components/agent-trace.html`.
60
+ - **AppMock** — `AppMock/AppMock.jsx`. Selector that renders one of 24 app-specific UI mockups (DeciDesk, DocuDesk, LarpingApp, LaunchPad + variants, NLDesign, PipelinQ, Portaliq, Procest, Scholiq, SoftwareCatalog, ZaakAfhandelApp, …; `openwoo` aliases to the OpenCatalogi mock). Each variant lives under `AppMock/variants/`. As of the wave-2/4 animation pass every product variant loops a one-scene story on the shared `--am-dur` timeline: portaliq form-submit, nldesign retheme, scholiq progress+credential, launchpad widgets/tiles/BI, pipelinq deal-close (wave 1+3); docudesk PII words blacking out + live dropzone, shillinq bank-line reconciliation, doriath unlock → countdown → relock, opencatalogi federated-search filter, openconnector record-travel + run insert, procest stage advance + row re-sort, openregister object landing + rail repaint (wave 2); decidesk vote tally + pending→adopted flip, hrmq timesheet submit→approve→payroll (content reworked from the earlier people-list, which misrepresented the app), larpingapp XP ripple, softwarecatalog CVE scan sweep, zaakafhandelapp submission travel, hermiq conversation + tool chips + approval tick (inline styles moved to the module), openbuild template→app ghost travel (wave 4). `running={false}` / `prefers-reduced-motion` freeze every loop to its meaningful end state. Specimen: `preview/components/app-mock.html`.
61
+ - **WidgetMock** — `WidgetMock/WidgetMock.jsx`. Standalone abstract of one Nextcloud dashboard widget (14 kinds: nine Conduction + five stock-Nextcloud framing widgets). The ten list-shaped kinds share a single wave-5 refresh primitive (`.wmLive` in AppMock.module.css): a fresh row slides in at the top, the bottom row clips out, and the frame's unread badge bumps transform/opacity only, so tile height never jumps. `docudesk-anonymise` instead runs the `.zoneLive` dropzone loop (marching-ants dashes, a file hex drops, mint confirm flash). `running={false}` / `prefers-reduced-motion` freeze to the resting tile. Specimen: `preview/components/widget-mock.html`.
62
+ - **SidebarMock** — `SidebarMock/SidebarMock.jsx`. Token-painted Nextcloud right-side detail panel, one registered tab active (9 kinds across Procest, DocuDesk, OpenRegister, OpenCatalogi, OpenConnector, DeciDesk, stock activity). Standalone card or `embedded` into an AppMock's `.detail` slot. Wave-5 loop (`.sbLive`): the panel slides in from the right, the head wipes, the active-tab underline travels in from the neighbouring tab (positioned by `--sb-ix`/`--sb-n` custom properties set from the variant's active index), and the body rows land on a stagger. `running={false}` / `prefers-reduced-motion` freeze to the opened panel. Known stand-ins in the tab icon registry: Signatures tabs borrow the `mail` icon, PII-map tabs borrow `keycloak` (see `ANIMATION_ROADMAP.md`). Specimen: `preview/components/sidebar-mock.html`.
63
+ - **FlowMock** — `FlowMock/FlowMock.jsx`. Token-painted abstract of the flow editor: node cards on a dotted canvas with coloured left bars (mint trigger, lavender steps, vermillion end), curved grey edges with pill labels, and a mint run-line looping trigger → step → approval → end. The run holds ~600ms at the approval node while an orange halo flashes there (the component's one orange). Freezes to the full static route under `prefers-reduced-motion` or `running={false}`. Specimen: `preview/components/flow-mock.html`.
64
+ - **LeafMock** — `LeafMock/LeafMock.jsx`. Small integration-leaf panels for Showcase items: `leaf="contacts"` (hex-avatar rows + mint sync arrow to a phone), `"calendar"` (mini month grid + linked-event chip), `"mail"` (message list + mint create-from-email chip), `"files"` (folder/file rows + forest link-to-record chip). Each leaf animates on the shared 6s `--lm-dur` loop (sync pulse + phone rows; day pop + chip slide; row highlight + chip pop; row + link-icon draw); `running={false}` / `prefers-reduced-motion` freeze to the end state.
63
65
  - **BuildMock** — `BuildMock/BuildMock.jsx`. Looping "make it yours" animation: the orange OpenBuild edit FAB is pressed, a component palette opens, and a widget is dragged into the page layout where it snaps in (~8s loop). `prefers-reduced-motion` / `running={false}` renders the static end state (component placed).
64
66
  - **KanbanMock** — `KanbanMock/KanbanMock.jsx`. Looping kanban drag for the pipelinq hero: a cursor lifts the top lead card, drags it to the next column, drops it flush into the column's card slot, and at the drop the column counts nudge (3→2, 2→3) and the per-lane € KPI tiles above the board shift the moved deal's value (~7s loop, clean reset). `prefers-reduced-motion` / `running={false}` renders the static board with the initial counts and totals.
67
+ - **MockScene** — `MockScene/MockScene.jsx`. Free-form positioning stage for overlap shots of WidgetMocks + SidebarMocks (caller supplies canvas size and per-item x/y/z). The scene assembles on a loop: items land on a 220ms stagger, transform-only so the inline left/top positioning is never fought; `running={false}` / `prefers-reduced-motion` show the assembled scene.
65
68
  - **ExternalAppShelf** — `ExternalAppShelf/ExternalAppShelf.jsx`. Third-party-app shelf (Outlook-style, Mattermost, Keycloak, etc.) used on `/connext`. Specimen: `preview/components/external-app-shelf.html`.
66
69
  - **WidgetShelf** — `WidgetShelf/WidgetShelf.jsx`. Widget showcase with eyebrow + title + lede. Default is a slow single-row carousel (seamless duplicated track, pause on hover/focus, static wrapping grid under `prefers-reduced-motion`); loop duration derives from the widget count (6s per card, min 30s; `speed` overrides), with pause/play + prev/next controls (cobalt ghost, hidden under reduced motion). Widgets without an explicit `panel` get a deterministic auto-generated mini panel (KPI tile / bar chart / list rows / donut, hashed from the title). `carousel={false}` renders the N-column grid. Specimen: `preview/components/widget-shelf.html`.
67
70
  - **Showcase** — `Showcase/Showcase.jsx`. Multi-item collapsible showcase with screenshot + body. Used on `/commonground` for the "5-lagen model". Specimen: `preview/components/showcase.html`.
@@ -158,4 +161,11 @@ plus the new `IntegrationsBlock`); the locale-switcher integration on
158
161
  the `PartnerDirectory solutions` field (consumed in the offering facet);
159
162
  the `PartnerCard logoAlt` prop (accepted, with `name + ' logo'` fallback).
160
163
 
161
- No open gaps as of 2026-05-19.
164
+ No open component gaps as of 2026-05-19.
165
+
166
+ Animation coverage (2026-08-19): waves 1–5 of the animated-atomics
167
+ programme shipped — every AppMock product variant, the WidgetMock
168
+ list-refresh + dropzone primitives, and the SidebarMock open loop.
169
+ The two remaining L-size items (Pipeline document-travels-the-flow,
170
+ PlatformDiagram assembly) and the SidebarMock icon-registry stand-ins
171
+ are specified in [`ANIMATION_ROADMAP.md`](ANIMATION_ROADMAP.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/docusaurus-preset",
3
- "version": "3.33.0",
3
+ "version": "3.35.0",
4
4
  "scripts": {
5
5
  "prepack": "node scripts/prepack-bundle-css.js",
6
6
  "test": "node --test"
@@ -34,7 +34,8 @@
34
34
  "static/",
35
35
  "bin/",
36
36
  "README.md",
37
- "MISSING_COMPONENTS.md"
37
+ "MISSING_COMPONENTS.md",
38
+ "ANIMATION_ROADMAP.md"
38
39
  ],
39
40
  "keywords": [
40
41
  "conduction",
@@ -47,6 +47,14 @@
47
47
  * - kind 'status' → KNVB-orange "ing…" status line
48
48
  * - cursor: boolean — render a blinking cursor at the bottom
49
49
  * - mode: string — bottom strip caption ("accept edits on…")
50
+ * - running: boolean (default true) — the trace types itself out:
51
+ * each line reveals left-to-right on a 550ms stagger (a per-line
52
+ * --at-d delay; the loop period --at-total derives from the line
53
+ * count plus a ~2s hold), status-line dots start ticking only
54
+ * after their line has appeared, and the loop restarts softly by
55
+ * fading lines out in the same order they arrived. `running={false}`
56
+ * and prefers-reduced-motion show the full trace at rest (every
57
+ * line visible — the meaningful end state).
50
58
  */
51
59
 
52
60
  import React from 'react';
@@ -62,12 +70,25 @@ function bulletClass(b) {
62
70
  }
63
71
  }
64
72
 
65
- export default function AgentTrace({lines = [], cursor = false, mode, className}) {
73
+ export default function AgentTrace({lines = [], cursor = false, mode, running = true, className}) {
74
+ /* Typed-reveal timing: line i appears at i × 550ms; after the last
75
+ line a ~2s hold, then a 600ms fade window before the loop. The
76
+ CSS animation reads both custom properties. */
77
+ const STEP = 550;
78
+ const total = lines.length * STEP + 2600;
79
+ const delayOf = (i) => ({'--at-d': `${i * STEP}ms`});
66
80
  return (
67
- <div className={[styles.trace, className].filter(Boolean).join(' ')} role="img" aria-label="Agent trace">
68
- <div className={styles.body}>
81
+ <div
82
+ className={[styles.trace, !running && styles.static, className].filter(Boolean).join(' ')}
83
+ role="img"
84
+ aria-label="Agent trace"
85
+ >
86
+ <div className={styles.body} style={{'--at-total': `${total}ms`}}>
69
87
  {lines.map((ln, i) => {
70
- const indent = ln.indent ? {paddingLeft: `${ln.indent * 18}px`} : undefined;
88
+ const indent = {
89
+ ...(ln.indent ? {paddingLeft: `${ln.indent * 18}px`} : null),
90
+ ...delayOf(i),
91
+ };
71
92
  if (ln.kind === 'expand') {
72
93
  return (
73
94
  <div key={i} className={[styles.line, styles.expand].join(' ')} style={indent}>
@@ -111,7 +132,7 @@ export default function AgentTrace({lines = [], cursor = false, mode, className}
111
132
  );
112
133
  })}
113
134
  {cursor && (
114
- <div className={[styles.line, styles.cursorLine].join(' ')}>
135
+ <div className={[styles.line, styles.cursorLine].join(' ')} style={delayOf(lines.length)}>
115
136
  <span className={styles.prompt}>&gt;</span>
116
137
  <span className={styles.cursor} aria-hidden="true"></span>
117
138
  </div>
@@ -1,6 +1,17 @@
1
1
  /**
2
2
  * <AgentTrace /> styles. Cobalt-900 terminal panel with monospace
3
3
  * lines and Claude-CLI-style coloured bullets.
4
+ *
5
+ * Typed reveal: every .line runs one shared looping animation whose
6
+ * period is --at-total (set inline by the component from the line
7
+ * count) and whose start is staggered by the per-line --at-d delay
8
+ * (i × 550ms, also inline). A line clips open left-to-right over the
9
+ * first ~4% of the period, holds, and fades near the end — with the
10
+ * stagger, lines type in and later fade out in arrival order, so the
11
+ * restart is a soft cascade rather than a hard cut. Status dots and
12
+ * the prompt cursor inherit the same delay so they only start after
13
+ * their line exists. `.static` (running={false}) and
14
+ * prefers-reduced-motion show every line at rest.
4
15
  */
5
16
 
6
17
  .trace {
@@ -29,6 +40,14 @@
29
40
  align-items: flex-start;
30
41
  gap: var(--space-2);
31
42
  font-feature-settings: "calt" 0;
43
+ animation: atType var(--at-total, 6s) linear var(--at-d, 0ms) infinite both;
44
+ }
45
+
46
+ @keyframes atType {
47
+ 0% { opacity: 0; clip-path: inset(0 100% 0 0); }
48
+ 4% { opacity: 1; clip-path: inset(0 0 0 0); }
49
+ 90% { opacity: 1; clip-path: inset(0 0 0 0); }
50
+ 96%, 100% { opacity: 0; clip-path: inset(0 0 0 0); }
32
51
  }
33
52
 
34
53
  /* Bullets — set inline-block so the gap works the same across kinds. */
@@ -56,7 +75,9 @@
56
75
  .dots {
57
76
  display: inline-block;
58
77
  margin-left: 2px;
59
- animation: dots 1400ms steps(4, end) infinite;
78
+ /* Inherits the line's --at-d so the dots only start ticking after
79
+ their line has typed in; backwards-fill hides them until then. */
80
+ animation: dots 1400ms steps(4, end) var(--at-d, 0ms) infinite backwards;
60
81
  white-space: pre;
61
82
  overflow: hidden;
62
83
  vertical-align: bottom;
@@ -94,7 +115,8 @@
94
115
  height: 14px;
95
116
  background: white;
96
117
  vertical-align: -2px;
97
- animation: blink 1100ms steps(2, end) infinite;
118
+ /* Same delay-inheritance as .dots: blinking starts with its line. */
119
+ animation: blink 1100ms steps(2, end) var(--at-d, 0ms) infinite backwards;
98
120
  }
99
121
  @keyframes blink {
100
122
  0%, 50% { opacity: 1; }
@@ -113,3 +135,20 @@
113
135
  }
114
136
  .modeArrow { color: var(--c-cobalt-300); }
115
137
  .modeText { color: var(--c-cobalt-200); }
138
+
139
+ /* ---- Static trace (running={false} and reduced motion) ----
140
+ Every line visible at rest, dots at full width, cursor solid. */
141
+
142
+ .static .line,
143
+ .static .dots,
144
+ .static .cursor {
145
+ animation: none;
146
+ }
147
+
148
+ @media (prefers-reduced-motion: reduce) {
149
+ .line,
150
+ .dots,
151
+ .cursor {
152
+ animation: none;
153
+ }
154
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * AgentTrace.render.test.js — SSR-renders the real <AgentTrace> and
3
+ * asserts the typed-reveal wiring: per-line --at-d delays on the
4
+ * 550ms stagger, the loop period --at-total derived from the line
5
+ * count, the cursor delayed until after the last line, and the
6
+ * `running` prop's static class. Same esbuild harness as
7
+ * KanbanMock.render.test.js; CSS modules stubbed 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, '..', 'AgentTrace.jsx');
22
+ const PRESET_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
23
+ const SCRATCH = path.join(PRESET_ROOT, '.tmp-agent-trace-test');
24
+
25
+ const LINES = [
26
+ {kind: 'note', bullet: 'done', text: 'Found the root cause.'},
27
+ {kind: 'tool', text: 'openconnector - run_pipeline (MCP)'},
28
+ {kind: 'expand', text: '+38 lines (ctrl+o to expand)'},
29
+ {kind: 'continuation', text: 'register_slug: "zaak", limit: 50'},
30
+ {kind: 'status', text: 'Synthesising…'},
31
+ ];
32
+
33
+ const cssModuleStub = {
34
+ name: 'css-module-stub',
35
+ setup(b) {
36
+ b.onResolve({filter: /\.module\.css$/}, (args) => ({path: args.path, namespace: 'css-stub'}));
37
+ b.onLoad({filter: /.*/, namespace: 'css-stub'}, () => ({
38
+ contents: 'export default new Proxy({}, {get: (_, p) => p});',
39
+ loader: 'js',
40
+ }));
41
+ },
42
+ };
43
+
44
+ let AgentTrace;
45
+
46
+ before(async () => {
47
+ await fs.mkdir(SCRATCH, {recursive: true});
48
+ const tmpDir = await fs.mkdtemp(path.join(SCRATCH, 'run-'));
49
+ const outFile = path.join(tmpDir, 'bundle.cjs');
50
+ await build({
51
+ entryPoints: [COMPONENT],
52
+ outfile: outFile,
53
+ bundle: true,
54
+ format: 'cjs',
55
+ jsx: 'automatic',
56
+ jsxImportSource: 'react',
57
+ tsconfigRaw: {compilerOptions: {jsx: 'react-jsx', jsxImportSource: 'react'}},
58
+ platform: 'node',
59
+ external: ['react'],
60
+ plugins: [cssModuleStub],
61
+ logLevel: 'warning',
62
+ });
63
+ delete require.cache[require.resolve(outFile)];
64
+ AgentTrace = require(outFile).default;
65
+ });
66
+
67
+ after(async () => {
68
+ await fs.rm(SCRATCH, {recursive: true, force: true});
69
+ });
70
+
71
+ function render(props) {
72
+ return renderToStaticMarkup(React.createElement(AgentTrace, props));
73
+ }
74
+
75
+ test('SSR smoke: renders every line kind', () => {
76
+ const html = render({lines: LINES, cursor: true, mode: 'accept edits on'});
77
+ for (const cls of ['note', 'tool', 'expand', 'continuation', 'status', 'cursorLine', 'modeBar']) {
78
+ assert.match(html, new RegExp(`class="[^"]*\\b${cls}\\b`), `missing ${cls}`);
79
+ }
80
+ });
81
+
82
+ test('typed reveal: each line carries its --at-d delay on the 550ms stagger', () => {
83
+ const html = render({lines: LINES});
84
+ for (let i = 0; i < LINES.length; i++) {
85
+ assert.match(html, new RegExp(`--at-d:${i * 550}ms`), `line ${i}: missing delay`);
86
+ }
87
+ });
88
+
89
+ test('typed reveal: the loop period --at-total derives from the line count (+ hold)', () => {
90
+ const html = render({lines: LINES});
91
+ assert.match(html, new RegExp(`--at-total:${LINES.length * 550 + 2600}ms`));
92
+ });
93
+
94
+ test('cursor: delayed until after the last line', () => {
95
+ const html = render({lines: LINES, cursor: true});
96
+ assert.match(html, new RegExp(`--at-d:${LINES.length * 550}ms`));
97
+ });
98
+
99
+ test('indented lines keep their padding alongside the delay', () => {
100
+ const html = render({lines: [{kind: 'note', text: 'x', indent: 2}]});
101
+ assert.match(html, /padding-left:36px/);
102
+ assert.match(html, /--at-d:0ms/);
103
+ });
104
+
105
+ test('P0 plumbing: running={false} adds the static class to the trace root; default omits it', () => {
106
+ assert.match(render({lines: LINES, running: false}), /class="trace static"/);
107
+ assert.doesNotMatch(render({lines: LINES}), /\bstatic\b/);
108
+ });
@@ -22,6 +22,12 @@
22
22
  * Props:
23
23
  * - app: keyof VARIANTS (required)
24
24
  * - size: 'sm' | 'md' (default) — frame width
25
+ * - running: boolean (default true) — false freezes every variant
26
+ * animation to its meaningful static end state (the same
27
+ * rendering `prefers-reduced-motion: reduce` gives).
28
+ * Forwarded to the variant component as well, for variants
29
+ * that need JS-side awareness; the CSS `.static` class on
30
+ * the frame is what actually stops the keyframes.
25
31
  * - sidebar: ReactNode — renders inside
26
32
  * the right-edge overlay of the frame. Pass a SidebarMock
27
33
  * JSX to model a Nextcloud sidebar opened over this app
@@ -47,7 +53,6 @@ import DeciDeskMock from './variants/DeciDeskMock.jsx';
47
53
  import DocuDeskMock from './variants/DocuDeskMock.jsx';
48
54
  import LarpingAppMock from './variants/LarpingAppMock.jsx';
49
55
  import NLDesignMock from './variants/NLDesignMock.jsx';
50
- import OpenWooMock from './variants/OpenWooMock.jsx';
51
56
  import PipelinQMock from './variants/PipelinQMock.jsx';
52
57
  import SoftwareCatalogMock from './variants/SoftwareCatalogMock.jsx';
53
58
  import ZaakAfhandelAppMock from './variants/ZaakAfhandelAppMock.jsx';
@@ -74,7 +79,11 @@ const VARIANTS = {
74
79
  docudesk: {Component: DocuDeskMock, label: 'DocuDesk'},
75
80
  larpingapp: {Component: LarpingAppMock, label: 'LarpingApp'},
76
81
  nldesign: {Component: NLDesignMock, label: 'NLDesign'},
77
- openwoo: {Component: OpenWooMock, label: 'OpenWoo'},
82
+ /* `openwoo` is an alias: the OpenWoo app is retired in favour of
83
+ OpenCatalogi, so the slug renders the OpenCatalogi mock. The
84
+ OpenWooMock.jsx variant file is kept for now (no importer left);
85
+ delete it in a later cleanup wave. */
86
+ openwoo: {Component: OpenCatalogiMock, label: 'OpenCatalogi'},
78
87
  pipelinq: {Component: PipelinQMock, label: 'PipelinQ'},
79
88
  softwarecatalog: {Component: SoftwareCatalogMock, label: 'SoftwareCatalog'},
80
89
  zaakafhandelapp: {Component: ZaakAfhandelAppMock, label: 'ZaakAfhandelApp'},
@@ -88,7 +97,7 @@ const VARIANTS = {
88
97
  'app-versions': {Component: AppVersionsMock, label: 'App Versions'},
89
98
  };
90
99
 
91
- export default function AppMock({app, size = 'md', sidebar = null, caption = false, className}) {
100
+ export default function AppMock({app, size = 'md', sidebar = null, caption = false, running = true, className}) {
92
101
  const variant = VARIANTS[app];
93
102
  if (!variant) {
94
103
  return (
@@ -113,8 +122,8 @@ export default function AppMock({app, size = 'md', sidebar = null, caption = fal
113
122
  return (
114
123
  <div className={styles.am}>
115
124
  <figure className={[styles.figure, className].filter(Boolean).join(' ')}>
116
- <div className={[styles.frame, styles[`size-${size}`]].filter(Boolean).join(' ')}>
117
- <Component sidebar={renderedSidebar} />
125
+ <div className={[styles.frame, styles[`size-${size}`], !running && styles.static].filter(Boolean).join(' ')}>
126
+ <Component sidebar={renderedSidebar} running={running} />
118
127
  </div>
119
128
  {caption && <figcaption className={styles.caption}>{label}</figcaption>}
120
129
  </figure>