srcdev-nuxt-components 9.6.19 → 9.6.20

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 (135) hide show
  1. package/.claude/rules/consumer/ids-from-useid.md +3 -2
  2. package/.claude/skills/component-compliance-checklist.md +5 -0
  3. package/.claude/skills/components/auto-grid.md +3 -1
  4. package/.claude/skills/components/capture-qr-code.md +9 -5
  5. package/.claude/skills/components/carousel-flip.md +41 -30
  6. package/.claude/skills/components/column-flow-grid.md +2 -0
  7. package/.claude/skills/components/dashboard-quad-grid.md +7 -2
  8. package/.claude/skills/components/dashboard-stats-grid.md +4 -0
  9. package/.claude/skills/components/decode-qr-code.md +7 -2
  10. package/.claude/skills/components/deep-expanding-menu-classic.md +31 -30
  11. package/.claude/skills/components/deep-expanding-menu.md +22 -21
  12. package/.claude/skills/components/grid-stack.md +2 -0
  13. package/.claude/skills/components/layout-grid-by-cols.md +2 -0
  14. package/.claude/skills/components/masonry-grid.md +9 -1
  15. package/.claude/skills/components/page-row.md +3 -0
  16. package/.claude/skills/components/samaritan-prompt.md +24 -8
  17. package/.claude/skills/components/site-header.md +16 -2
  18. package/.claude/skills/index.md +12 -12
  19. package/.claude/skills/storybook-add-story.md +25 -0
  20. package/.claude/skills/testing-add-unit-test.md +26 -0
  21. package/.vscode/srcdev-component-capture-decode-qr-code.code-snippets +11 -2
  22. package/.vscode/srcdev-component-carousel-flip.code-snippets +22 -4
  23. package/.vscode/srcdev-component-deep-expanding-menu-classic.code-snippets +32 -2
  24. package/.vscode/srcdev-component-deep-expanding-menu.code-snippets +31 -1
  25. package/.vscode/srcdev-component-samaritan-prompt.code-snippets +5 -4
  26. package/.vscode/srcdev-component-site-header.code-snippets +13 -1
  27. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +12 -2
  28. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue +19 -17
  29. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts +42 -0
  30. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/tests/DashboardQuadGrid.spec.ts +31 -44
  31. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +13 -1
  32. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +40 -31
  33. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/stories/DashboardStatsGrid.stories.ts +47 -0
  34. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/tests/DashboardStatsGrid.spec.ts +51 -53
  35. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/CONSUMER-STYLING.md +3 -0
  36. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/LayoutGridByCols.vue +4 -0
  37. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/stories/LayoutGridByCols.stories.ts +47 -2
  38. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/LayoutGridByCols.spec.ts +36 -23
  39. package/app/components/01.atoms/grids/auto-grid/AutoGrid.vue +7 -3
  40. package/app/components/01.atoms/grids/auto-grid/CONSUMER-STYLING.md +1 -1
  41. package/app/components/01.atoms/grids/auto-grid/stories/AutoGrid.stories.ts +46 -1
  42. package/app/components/01.atoms/grids/auto-grid/tests/AutoGrid.spec.ts +40 -27
  43. package/app/components/01.atoms/grids/auto-grid/tests/__snapshots__/AutoGrid.spec.ts.snap +1 -1
  44. package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +9 -0
  45. package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +3 -2
  46. package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +46 -0
  47. package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +24 -11
  48. package/app/components/01.atoms/grids/grid-stack/CONSUMER-STYLING.md +3 -0
  49. package/app/components/01.atoms/grids/grid-stack/GridStack.vue +2 -0
  50. package/app/components/01.atoms/grids/grid-stack/stories/GridStack.stories.ts +41 -0
  51. package/app/components/01.atoms/grids/grid-stack/tests/GridStack.spec.ts +26 -13
  52. package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +15 -0
  53. package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +51 -31
  54. package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +48 -0
  55. package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +84 -29
  56. package/app/components/01.atoms/page-row/CONSUMER-STYLING.md +6 -1
  57. package/app/components/01.atoms/page-row/PageRow.vue +16 -15
  58. package/app/components/01.atoms/page-row/stories/PageRow.stories.ts +23 -0
  59. package/app/components/01.atoms/page-row/tests/PageRow.spec.ts +6 -5
  60. package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +63 -44
  61. package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +74 -37
  62. package/app/components/02.molecules/navigation/deep-expanding-menu/stories/DeepExpandingMenu.stories.ts +139 -19
  63. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +90 -27
  64. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +8 -8
  65. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +58 -26
  66. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +113 -57
  67. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/stories/DeepExpandingMenuClassic.stories.ts +139 -19
  68. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/DeepExpandingMenuClassic.spec.ts +140 -22
  69. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/__snapshots__/DeepExpandingMenuClassic.spec.ts.snap +9 -9
  70. package/app/components/02.molecules/qr-code/CONSUMER-STYLING.md +22 -7
  71. package/app/components/02.molecules/qr-code/CaptureQrCode.vue +63 -71
  72. package/app/components/02.molecules/qr-code/DecodeQrCode.vue +47 -10
  73. package/app/components/02.molecules/qr-code/stories/CaptureQrCode.stories.ts +114 -26
  74. package/app/components/02.molecules/qr-code/stories/DecodeQrCode.stories.ts +115 -24
  75. package/app/components/02.molecules/qr-code/tests/CaptureQrCode.spec.ts +124 -51
  76. package/app/components/02.molecules/qr-code/tests/DecodeQrCode.spec.ts +74 -25
  77. package/app/components/02.molecules/samaritan-prompt/CONSUMER-STYLING.md +22 -10
  78. package/app/components/02.molecules/samaritan-prompt/SamaritanPrompt.vue +30 -12
  79. package/app/components/02.molecules/samaritan-prompt/SamaritanPromptMixed.vue +34 -10
  80. package/app/components/02.molecules/samaritan-prompt/stories/SamaritanPrompt.stories.ts +55 -4
  81. package/app/components/02.molecules/samaritan-prompt/stories/SamaritanPromptMixed.stories.ts +67 -7
  82. package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPrompt.spec.ts +113 -29
  83. package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPromptMixed.spec.ts +92 -45
  84. package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +82 -29
  85. package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +176 -113
  86. package/app/components/03.organisms/image-galleries/carousel-flip/stories/CarouselFlip.stories.ts +153 -23
  87. package/app/components/03.organisms/image-galleries/carousel-flip/tests/CarouselFlip.spec.ts +119 -63
  88. package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +8 -8
  89. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +4 -4
  90. package/app/components/03.organisms/site-header/CONSUMER-STYLING.md +41 -23
  91. package/app/components/03.organisms/site-header/SiteHeader.vue +16 -21
  92. package/app/components/03.organisms/site-header/stories/SiteHeader.stories.ts +138 -0
  93. package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +53 -26
  94. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +2 -2
  95. package/app/types/components/qr-code.d.ts +12 -0
  96. package/app/utils/graphemes.ts +8 -0
  97. package/package.json +1 -1
  98. package/app/components/01.atoms/animations/entry/.nuxt/app.config.mjs +0 -21
  99. package/app/components/01.atoms/animations/entry/.nuxt/components.d.ts +0 -66
  100. package/app/components/01.atoms/animations/entry/.nuxt/imports.d.ts +0 -30
  101. package/app/components/01.atoms/animations/entry/.nuxt/manifest/meta/test.json +0 -1
  102. package/app/components/01.atoms/animations/entry/.nuxt/types/app.config.d.ts +0 -35
  103. package/app/components/01.atoms/animations/entry/.nuxt/types/build.d.ts +0 -23
  104. package/app/components/01.atoms/animations/entry/.nuxt/types/builder-env.d.ts +0 -1
  105. package/app/components/01.atoms/animations/entry/.nuxt/types/components.d.ts +0 -71
  106. package/app/components/01.atoms/animations/entry/.nuxt/types/imports.d.ts +0 -386
  107. package/app/components/01.atoms/animations/entry/.nuxt/types/middleware.d.ts +0 -11
  108. package/app/components/01.atoms/animations/entry/.nuxt/types/modules.d.ts +0 -79
  109. package/app/components/01.atoms/animations/entry/.nuxt/types/nitro-layouts.d.ts +0 -17
  110. package/app/components/01.atoms/animations/entry/.nuxt/types/nitro-nuxt.d.ts +0 -64
  111. package/app/components/01.atoms/animations/entry/.nuxt/types/plugins.d.ts +0 -29
  112. package/app/components/01.atoms/animations/entry/.nuxt/types/runtime-config.d.ts +0 -32
  113. package/app/components/01.atoms/animations/entry/.nuxt/types/shared-imports.d.ts +0 -10
  114. package/app/components/01.atoms/animations/entry/.nuxt/types/vue-shim.d.ts +0 -0
  115. package/app/components/01.atoms/animations/entry/.nuxtrc +0 -1
  116. package/app/components/01.atoms/animations/entry/node_modules/.cache/nuxt/chrome-workspace.json +0 -1
  117. package/app/components/01.atoms/animations/entry/node_modules/.cache/vite/client/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  118. package/app/components/03.organisms/responsive-header/tests/.nuxt/app.config.mjs +0 -21
  119. package/app/components/03.organisms/responsive-header/tests/.nuxt/components.d.ts +0 -66
  120. package/app/components/03.organisms/responsive-header/tests/.nuxt/imports.d.ts +0 -30
  121. package/app/components/03.organisms/responsive-header/tests/.nuxt/manifest/meta/test.json +0 -1
  122. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/app.config.d.ts +0 -35
  123. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/build.d.ts +0 -23
  124. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/builder-env.d.ts +0 -1
  125. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/components.d.ts +0 -71
  126. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/imports.d.ts +0 -386
  127. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/middleware.d.ts +0 -11
  128. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/modules.d.ts +0 -79
  129. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-layouts.d.ts +0 -17
  130. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-nuxt.d.ts +0 -64
  131. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/plugins.d.ts +0 -29
  132. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/runtime-config.d.ts +0 -32
  133. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/shared-imports.d.ts +0 -10
  134. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/vue-shim.d.ts +0 -0
  135. package/app/components/03.organisms/responsive-header/tests/node_modules/.cache/nuxt/chrome-workspace.json +0 -1
@@ -7,11 +7,12 @@ paths:
7
7
 
8
8
  Build `id`/`for` pairs from `useId()` (plus a loop index for repeated items), never from props or
9
9
  data values like `` `${name}-${item.value}` ``. A radio group's default `name` comes from
10
- `useId()` too, never a fixed string. Call `useId()` once at setup and store the result; never call
10
+ `useId()` too, never a fixed string, and so does a `<details name>` exclusive group. Call `useId()` once at setup and store the result; never call
11
11
  it inside a `computed`, which can re-run it.
12
12
 
13
13
  Why: two instances sharing a `name` and values produce duplicate ids, and a value with spaces makes
14
14
  an invalid one, so labels toggle the wrong control (`MultipleCheckboxes`, 2026-10-16). A fixed default radio `name` makes every instance on a page
15
- one group, so picking in one clears the others (`SegmentedControl`, 2026-10-17). Test
15
+ one group, so picking in one clears the others (`SegmentedControl`, 2026-10-17); a fixed `<details>`
16
+ `name` did the same across every `DeepExpandingMenuClassic` on a page (fixed 2026-10-21). Test
16
17
  uniqueness with both instances inside one host component: separate `mountSuspended` calls each
17
18
  restart the `useId()` counter. Detail: skill `testing-add-unit-test.md` (srcdev-nuxt-components).
@@ -140,6 +140,11 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
140
140
  `computed()`: Vue renders nothing for a fractional or negative range and only warns in dev
141
141
  (`StepperList`, 2026-10-15)
142
142
  - broken image or link URLs
143
+ - an empty list where script loops over the data: a `while` loop that walks or cycles the
144
+ items must still terminate, and an async loop must await something on every pass, or the
145
+ tab freezes. `SamaritanPromptMixed` (empty `messageConfigs` with `introDelay: 0`) and
146
+ `CarouselFlip` (empty `carouselDataIds`) both did, fixed 2026-10-21. Grep the script for
147
+ `while (` and check each one with zero and one item
143
148
  - data a third-party child can't handle: a library that throws while rendering (an encoder, a
144
149
  parser) breaks the whole page render. Catch it with `onErrorCaptured` and render a fallback
145
150
  (`DisplayQrCode` threw "Data too long" on an over-capacity value until 2026-10-19). Check story
@@ -37,11 +37,13 @@ When filling from a data array, use a dynamic slot name in a `v-for`:
37
37
  | Prop (template form) | Type | Default | Notes |
38
38
  | -------------------------- | ------------------------------------------- | ------- | --------------------------------------------- |
39
39
  | `tag` | `"div" \| "section" \| "article" \| "main"` | `"div"` | Use a semantic tag for page landmark regions. |
40
- | `is-responsive` | `boolean` | `false` | Adds `.is-responsive`: column minimum steps small → default → large via `@container` queries at 768px / 1024px. Needs a `container-type: inline-size` ancestor. |
40
+ | `is-responsive` | `boolean` | `false` | Adds `data-responsive` (was the class `.is-responsive` until 2026-10-21): column minimum steps small → default → large via `@container` queries at 768px / 1024px. Needs a `container-type: inline-size` ancestor. |
41
41
  | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes on the root element. |
42
42
 
43
43
  ---
44
44
 
45
+ Each slotted item gets `min-inline-size: 0`, so an unbroken string can't widen its column. The `StressTest` story feeds every cell oversized content (an unbroken string and URL, long German, a 1600px image, emoji, right-to-left, an empty cell).
46
+
45
47
  ## CSS custom properties
46
48
 
47
49
  Set these on the grid itself (`style`, passthrough class) or on any ancestor you own; they inherit
@@ -25,13 +25,14 @@ Camera access requires a secure context (HTTPS or localhost) and the user granti
25
25
  |------|------|---------|-------------|
26
26
  | `camera-stopped-label` | `string` | `"Camera stopped"` | Message while the camera is stopped (tab hidden, route leave) |
27
27
  | `reset-camera-label` | `string` | `"Reset camera"` | Reset button text in the error state |
28
+ | `error-messages` | `CaptureQrCodeErrorMessages` | `{}` | Error copy keyed by error name (`NotAllowedError`, `NotFoundError`, `NotSupportedError`, `NotReadableError`, `OverconstrainedError`, `StreamApiNotSupportedError`, `InsecureContextError`, `StreamLoadTimeoutError`) plus `default`. Each key falls back to a built-in plain-English message; any other error name uses `default`. Type from `~/types/components`. |
28
29
  | `style-class-passthrough` | `string \| string[]` | `[]` | Extra classes on the root element |
29
30
 
30
31
  ## Slots
31
32
 
32
33
  | Slot | Scope | Description |
33
34
  |------|-------|-------------|
34
- | `#error` | `{ error, message, reset }` | Replaces the default error content (message + reset button). `error` is the `Error.name` (e.g. `NotAllowedError`), so you can show your own translated copy per case; call `reset()` to retry. |
35
+ | `#error` | `{ error, message, reset }` | Replaces the default error content (message + reset button). `message` is the resolved `errorMessages` text. `error` is the `Error.name` (e.g. `NotAllowedError`), so you can show your own translated copy per case; call `reset()` to retry. |
35
36
 
36
37
  ## CSS classes
37
38
 
@@ -40,15 +41,17 @@ Camera access requires a secure context (HTTPS or localhost) and the user granti
40
41
  | `.capture-qr-code` | root | always present |
41
42
  | `.capture-qr-code-stopped` | inner | shown when camera is off but no error |
42
43
  | `.capture-qr-code-error` | inner | shown when an error has occurred |
43
- | `.capture-qr-code-results` | inner | polite live region, always rendered; its `<ul>` appears once a code is decoded |
44
+ | `.capture-qr-code-results` | inner | polite live region, always rendered; its `.capture-qr-code-results-list` `<ul>` appears once a code is decoded |
45
+ | `.capture-qr-code-result` | `<li>` | one decoded value; wraps anywhere |
46
+ | `.capture-qr-code-error-message` | `<p>` | default error text |
44
47
 
45
48
  ## Behaviour
46
49
 
47
50
  - Camera starts automatically on mount (`state.cameraOn = true`)
48
- - When the page becomes hidden (tab switch, minimize), the camera stops and media streams are released
51
+ - When the page becomes hidden (tab switch, minimize), the camera stops and media streams are released; when it becomes visible again the camera restarts, unless an error is showing (until 2026-10-21 it stayed on "Camera stopped" with no way back)
49
52
  - On route leave (`onBeforeRouteLeave`), the camera stops and streams are released
50
53
  - On `<KeepAlive>` deactivate/activate, camera is stopped/restarted accordingly
51
- - If the user denies camera permission or another error occurs, the `.capture-qr-code-error` state is shown with the error message and a **Reset camera** button
54
+ - If the user denies camera permission or another error occurs, the `.capture-qr-code-error` state is shown with a plain-language message (from `errorMessages`, not the library's raw `[Name]: message` text, which it showed until 2026-10-21) and a **Reset camera** button
52
55
  - Detected QR codes replace the previous results — each scan is a fresh result set
53
56
  - Results are displayed as a `<ul>` inside `.capture-qr-code-results`
54
57
 
@@ -91,4 +94,5 @@ The root element is a square (`aspect-ratio: 1 / 1`) by default. Override in a c
91
94
  ## Notes
92
95
 
93
96
  - The component does not emit the detected values — results are rendered inline. To act on scan results in a parent, extend the component or use `nuxt-qrcode`'s `QrcodeStream` directly with `@detect`.
94
- - Media streams are stopped via two strategies: via the `QrcodeStream` component ref and via a document-level `querySelectorAll("video")` sweep — the sweep acts as a safety net if the ref is unavailable.
97
+ - Media streams are stopped by sweeping `<video>` elements inside the component's own root. Until 2026-10-21 it also swept every `<video>` in the document, which stopped any other camera or call stream on the page.
98
+ - 2026-10-21 migration: `errorMessages` prop, visible-again restart, own-root stream cleanup, wrapping results, `StressTest` story (simulation buttons drive the internal handlers, since Storybook can't fake a camera).
@@ -14,8 +14,7 @@ dynamic slots keyed by `carouselDataIds`. It supports swipe, arrow-key, marker-c
14
14
  prev/next-button navigation, and four `buttonLayout` variants for positioning the prev/next
15
15
  buttons relative to the marker controls.
16
16
 
17
- Has live production usage in a consumer app — check before making a breaking change to its
18
- default rendered output.
17
+ No consumer app uses it as of 2026-10-21 (an earlier note said one did; a scan found none).
19
18
 
20
19
  ## Props
21
20
 
@@ -27,12 +26,12 @@ default rendered output.
27
26
  | `allowCarouselOverflow` | `boolean` | `false` | Allows items to overflow the item track horizontally instead of clipping. |
28
27
  | `useFlipAnimation` | `boolean` | `false` | Changes the initial item offset used in the FLIP reorder calculation. |
29
28
  | `useSpringEffect` | `boolean` | `false` | Uses `var(--spring-easing)` as the transition timing function instead of `ease`. |
30
- | `buttonLayout` | `"sides" \| "controls-flanking" \| "controls-grouped-right" \| "overlay"` | `"sides"` | Where the prev/next buttons sit relative to the carousel/controls. |
29
+ | `buttonLayout` | `"sides" \| "controls-flanking" \| "controls-grouped-right" \| "overlay" \| "track-edges"` | `"sides"` | Where the prev/next buttons sit relative to the carousel/controls. |
31
30
  | `showControls` | `boolean` | `true` | Shows/hides the marker (dot) controls bar. Prev/next buttons remain regardless. |
32
31
  | `ariaLabel` | `string` | `"Image carousel"` | `aria-label` on the root region — override for localisation. |
33
32
  | `itemsAriaLabel` | `string` | `"Carousel items"` | `aria-label` on the item group — override for localisation. |
34
- | `previousLabel` | `string` | `"Go to previous item"` | Prev button `aria-label` — override for localisation. |
35
- | `nextLabel` | `string` | `"Go to next item"` | Next button `aria-label` — override for localisation. |
33
+ | `previousLabel` | `string` | `"Go to previous item"` | Prev button accessible name (its visually hidden text) — override for localisation. |
34
+ | `nextLabel` | `string` | `"Go to next item"` | Next button accessible name (its visually hidden text) — override for localisation. |
36
35
  | `jumpToItemLabel` | `string` | `"Jump to item"` | Marker button `aria-label` prefix — the 1-based index is appended (`"Jump to item 3"`). |
37
36
  | `itemPositionAnnouncement` | `(current: number, total: number) => string` | `` (current, total) => `Item ${current} of ${total}` `` | Builds the screen-reader live-region text on navigation — override for localisation or a different phrasing. |
38
37
  | `prevIcon` | `string` | `"ic:outline-keyboard-arrow-left"` | Iconify name for the previous button icon. Ignored if the `prev-icon` slot is used. |
@@ -64,10 +63,12 @@ One dynamically-named slot per `carouselDataIds` entry, named after that id:
64
63
  ## Navigation
65
64
 
66
65
  - **Swipe** (touch): left swipe → next, right swipe → previous.
67
- - **Keyboard**: `ArrowLeft`/`ArrowRight` on either the item track or the controls bar (both are
68
- `tabindex="0"`).
66
+ - **Keyboard**: `ArrowLeft`/`ArrowRight` while the item track (`tabindex="0"`) or a marker has
67
+ focus. The controls bar itself is no longer a tab stop (it was `tabindex="0"` with no name until
68
+ 2026-10-21).
69
69
  - **Marker click**: jumps directly to that item.
70
- - **Prev/next buttons**: always rendered, regardless of `showControls`.
70
+ - **Prev/next buttons**: `InputButton variant="inline"` with an `iconOnly` slot; rendered regardless
71
+ of `showControls`, but only when there are at least two items (the markers too).
71
72
  - All navigation wraps around at the ends.
72
73
 
73
74
  ## `buttonLayout` variants
@@ -77,31 +78,35 @@ One dynamically-named slot per `carouselDataIds` entry, named after that id:
77
78
  - `"controls-grouped-right"`: markers centred, prev/next buttons grouped to the right of them.
78
79
  - `"overlay"`: markers float over the bottom edge of the item track instead of occupying their
79
80
  own grid row.
81
+ - `"track-edges"` (added 2026-10-21): prev/next overlay the ends of the visible track, inset by
82
+ `--carousel-flip-button-edge-inset` (default: the edge-preview width plus `0.8rem`), so they sit
83
+ on the active frame. Use it with `allowCarouselOverflow`, where `"sides"` leaves the buttons at
84
+ the component box's edges, over the neighbouring items that peek out past it.
85
+
86
+ Placement tokens (added 2026-10-21): `--carousel-flip-button-align` (`start`/`center`/`end`,
87
+ vertical) and `--carousel-flip-button-inline-offset` (`sides` and `controls-flanking`: pushes each
88
+ button outward, negative pulls it inward; mirrors in RTL via `:dir(rtl)`). For the viewport edges
89
+ of a centred carousel of known width: `calc((100vw - <that width>) / 2 - 1.6rem)`. A percentage
90
+ won't work, since `translate` percentages resolve against the button.
80
91
 
81
92
  ## CSS token API
82
93
 
83
- See [CONSUMER-STYLING.md](../../app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md).
84
-
85
- | Token | Default | Controls |
86
- |---|---|---|
87
- | `--carousel-flip-gap` | `1rem` | Gap between items inside the item track |
88
- | `--carousel-flip-layout-gap` | `1rem` | Grid gap between the prev/carousel/next/controls areas |
89
- | `--carousel-flip-display-max-width` | `80rem` | Max width of the item track and the controls bar |
90
- | `--carousel-flip-item-max-width` | `80rem` | Max width of a single item, before subtracting the edge-preview width |
91
- | `--carousel-flip-edge-preview-width` | `4rem` | How much of the neighbouring items peek in at each edge |
92
- | `--carousel-flip-marker-gap` | `1rem` | Gap between marker (dot) buttons |
93
- | `--carousel-flip-marker-active-colour` | `var(--slate-10)` | Active marker background colour |
94
- | `--carousel-flip-button-background-colour` | `white` | Prev/next button background colour |
95
- | `--carousel-flip-button-border-width` | `0.1rem` | Prev/next button border width |
96
- | `--carousel-flip-button-border-colour` | `hsl(0, 29%, 3%)` | Prev/next button border colour |
97
- | `--carousel-flip-button-border-radius` | `100vw` | Prev/next button corner radius |
98
- | `--carousel-flip-button-padding` | `0.8rem` | Prev/next button padding |
99
- | `--carousel-flip-button-icon-size` | `2.4rem` | Prev/next button icon glyph size |
100
- | `--carousel-flip-buttons-gap` | `1rem` | Gap between prev/next buttons in the `controls-grouped-right` layout |
101
- | `--carousel-flip-overlay-controls-offset` | `1rem` | Distance of the controls bar from the bottom edge in the `overlay` layout |
102
- | `--carousel-flip-focus-outline-width` | `0.2rem` | Marker button focus-visible outline width |
103
- | `--carousel-flip-focus-outline-colour` | `var(--theme-ring)` | Marker button focus-visible outline colour |
104
- | `--carousel-flip-focus-outline-offset` | `0.2rem` | Marker button focus-visible outline offset |
94
+ Full table, state hooks and class names: [CONSUMER-STYLING.md](../../app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md).
95
+ Tokens are `--carousel-flip-*`: gaps and max widths, edge preview, marker gap and active colour,
96
+ prev/next button colours, border, radius, padding and icon size/colour, and the focus outline.
97
+ Colours default from `--theme-surface`, `--theme-surface-subtle`, `--theme-border`,
98
+ `--theme-text` and `--theme-ring`.
99
+
100
+ ## Data handling
101
+
102
+ - `itemCount` follows `carouselDataIds` after mount (it was frozen at the mount-time length until
103
+ 2026-10-21, so adding items left the markers and announcement stale). When the list changes, the
104
+ layout is rebuilt and the active item resets to the first if it was removed.
105
+ - An empty `carouselDataIds` renders an empty track. Until 2026-10-21 it sent `reorderItems` into
106
+ an infinite loop on mount and froze the tab.
107
+ - Items are keyed on index + id, so a repeated id still renders (both items show that slot).
108
+ - Item content wraps (`min-inline-size: 0`, `overflow-wrap: anywhere`), the centre grid column is
109
+ `minmax(0, 1fr)`, and the markers wrap onto more rows. The `StressTest` story covers all of these.
105
110
 
106
111
  ## Accessibility
107
112
 
@@ -114,6 +119,12 @@ See [CONSUMER-STYLING.md](../../app/components/03.organisms/image-galleries/caro
114
119
 
115
120
  ## Notes
116
121
 
122
+ - 2026-10-21 migration: prefixed every structural class and moved state classes to `data-*`
123
+ attributes (see CONSUMER-STYLING.md), prev/next became `InputButton`, theme-token colour defaults,
124
+ `transitionSpeed` and the class passthrough became reactive, `aria-current` is omitted (not
125
+ `"false"`) on inactive items, and the spec lost a dead `#imports` mock of
126
+ `useStyleClassPassthrough` that never applied.
127
+
117
128
  - Migrated from `app/components/carousels/CarouselFlip.vue` (unplaced, legacy location) to
118
129
  `03.organisms/image-galleries/carousel-flip/` on 2026-09-21, alongside `SliderGallery`.
119
130
  - Sibling components `CarouselBasic` and `CarouselInfinite` were deleted in the same migration —
@@ -82,6 +82,8 @@ properties, since they need JS-computed values rather than being purely CSS-driv
82
82
 
83
83
  ## Notes
84
84
 
85
+ - The `StressTest` story feeds every cell oversized content (an unbroken string and URL, long German, a 1600px image, emoji, right-to-left, an empty cell).
86
+
85
87
  - **Renamed from `MasonryGrid`** (2026-09-13) — see **Which one do I want?** above for why. If you
86
88
  have consumer code importing this as `MasonryGrid`, it needs updating to `ColumnFlowGrid`; the
87
89
  props and behaviour are otherwise unchanged. CSS classes/tokens were renamed to match
@@ -50,6 +50,11 @@ column count instead, use `LayoutGridByCols` (fixed count) or `AutoGrid` (auto-f
50
50
 
51
51
  ---
52
52
 
53
+ ## Cell classes
54
+
55
+ Each slot renders in a `.dashboard-quad-grid-cell` with `.dashboard-quad-grid-slot-1` to `-slot-4`
56
+ (`.slot1` to `.slot4` until 2026-10-21). The `StressTest` story feeds every cell oversized content (an unbroken string and URL, long German, a 1600px image, emoji, right-to-left, an empty cell).
57
+
53
58
  ## CSS token API
54
59
 
55
60
  See [CONSUMER-STYLING.md](../../app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md).
@@ -59,10 +64,10 @@ See [CONSUMER-STYLING.md](../../app/components/01.atoms/content-wrappers/layout-
59
64
  | `--dashboard-quad-grid-gap` | `2rem` | Gap between the four slot panels |
60
65
  | `--dashboard-quad-grid-padding` | `2rem` | Padding inside each slot panel |
61
66
  | `--dashboard-quad-grid-outline-width` | `0.1rem` | Slot panel outline width |
62
- | `--dashboard-quad-grid-outline-colour` | `black` | Slot panel outline colour |
67
+ | `--dashboard-quad-grid-outline-colour` | `var(--theme-border)` | Slot panel outline colour |
63
68
  | `--dashboard-quad-grid-outline-offset` | `0` | Slot panel outline offset |
64
69
  | `--dashboard-quad-grid-border-width` | `0` | Slot panel border width (off by default — the panel edge is drawn by the outline) |
65
- | `--dashboard-quad-grid-border-colour` | `black` | Slot panel border colour, only visible once `--dashboard-quad-grid-border-width` is set |
70
+ | `--dashboard-quad-grid-border-colour` | `var(--theme-border)` | Slot panel border colour, only visible once `--dashboard-quad-grid-border-width` is set |
66
71
  | `--dashboard-quad-grid-border-radius` | `0.5rem` | Slot panel corner radius |
67
72
  | `--dashboard-quad-grid-rule-width` | `0.1rem` | Width of the grid gap rule line (CSS Gap Decorations, limited browser support) |
68
73
  | `--dashboard-quad-grid-rule-colour` | `transparent` | Colour of the grid gap rule line, invisible by default |
@@ -32,6 +32,10 @@ top-row slots and overall shape are fixed.
32
32
 
33
33
  ---
34
34
 
35
+ The two counts are floored, and a negative or non-finite count renders no panels (until 2026-10-21 a
36
+ fractional count rendered none at all, since Vue skips a fractional `v-for` range). Element classes
37
+ are prefixed `.dashboard-stats-grid-*` (see CONSUMER-STYLING.md). The `StressTest` story feeds every cell oversized content (an unbroken string and URL, long German, a 1600px image, emoji, right-to-left, an empty cell).
38
+
35
39
  ## Props reference
36
40
 
37
41
  | Prop (template form) | Type | Default | Notes |
@@ -25,6 +25,8 @@ No camera permission is required — this component works entirely with uploaded
25
25
  |------|------|---------|-------------|
26
26
  | `upload-label` | `string` | `"Upload a QR code image"` | Visible label wrapping the file input (its accessible name) |
27
27
  | `drop-label` | `string` | `"Or drop a QR code image here"` | Instruction text inside the drop zone |
28
+ | `no-result-label` | `string` | `"No QR code was found in that image."` | Shown when an image decodes but contains no QR code. Empty string shows nothing |
29
+ | `read-error-label` | `string` | `"That file couldn't be read as an image."` | Shown when the drop zone reports an error (a dropped file or URL that isn't a readable image). Empty string shows nothing |
28
30
  | `style-class-passthrough` | `string \| string[]` | `[]` | Extra classes on the root element |
29
31
 
30
32
  ## CSS classes
@@ -35,7 +37,8 @@ No camera permission is required — this component works entirely with uploaded
35
37
  | `.decode-qr-code-capture` | file input | the `QrcodeCapture` file-picker element |
36
38
  | `.decode-qr-code-dropzone` | drop zone | the `QrcodeDropZone` drag-and-drop area |
37
39
  | `.decode-qr-code-upload` | label | wraps `.decode-qr-code-upload-label` and the file input |
38
- | `.decode-qr-code-dropzone.is-dropping` | drop zone | an image is being dragged over it |
40
+ | `.decode-qr-code-dropzone[data-dropping]` | drop zone | an image is being dragged over it (was `.is-dropping` until 2026-10-21) |
41
+ | `.decode-qr-code-status` | `<p>` | the no-result or (with `[data-error]`) read-error message |
39
42
  | `.decode-qr-code-results` | inner | polite live region, always rendered; its `<ul>` appears once a code is decoded |
40
43
 
41
44
  ## Behaviour
@@ -43,7 +46,8 @@ No camera permission is required — this component works entirely with uploaded
43
46
  - Two input methods are rendered side by side: a file picker (`.decode-qr-code-capture`) and a drag-and-drop zone (`.decode-qr-code-dropzone`)
44
47
  - Both share the same `onDetect` handler — results are displayed in the same `.decode-qr-code-results` list regardless of input method
45
48
  - Detected QR codes replace the previous results — each decode is a fresh result set
46
- - If the decoded array is empty, the results `<ul>` isn't rendered (the live-region wrapper stays)
49
+ - If the decoded array is empty, the results `<ul>` isn't rendered and `noResultLabel` shows instead; a drop-zone error clears the results and shows `readErrorLabel`. Both go through the same polite live region. Before 2026-10-21 neither case gave any feedback.
50
+ - Results and labels wrap (`overflow-wrap: anywhere`), so a long decoded URL stays inside the component
47
51
  - The drop zone has a dashed border and minimum height by default — style with `.decode-qr-code-dropzone` to customise
48
52
 
49
53
  ## Usage
@@ -87,4 +91,5 @@ With styling:
87
91
  ## Notes
88
92
 
89
93
  - Like `CaptureQrCode`, results are rendered inline and not emitted. To act on results in a parent, extend the component or use `nuxt-qrcode`'s `QrcodeCapture` / `QrcodeDropZone` primitives directly.
94
+ - 2026-10-21 migration: `data-dropping` state hook, theme-token drop zone colours, status messages, wrapping, `StressTest` story (simulation buttons drive the internal handlers).
90
95
  - The component renders both input methods unconditionally. If only one is needed, use the underlying `QrcodeCapture` or `QrcodeDropZone` primitives directly.
@@ -11,8 +11,10 @@ type: reference
11
11
  `<details>`/`<summary>`-based fallback for [`DeepExpandingMenu`](deep-expanding-menu.md) — same
12
12
  props, same `ResponsiveHeaderNavItem` data shape, same visual result, but built without CSS
13
13
  anchor-positioning or the Popover API. Each nav item with `childLinks` renders as a native
14
- `<details>` element; opening one and clicking outside it closes it again via `@vueuse/core`'s
15
- `onClickOutside`.
14
+ `<details>` element. Groups in one menu share a per-instance `name` (from `useId()`), so opening
15
+ one closes the others. A click outside the menu (`@vueuse/core` `onClickOutside`), a click
16
+ elsewhere inside it, Escape (focus returns to the group's `<summary>`) or following a child link
17
+ closes the open group.
16
18
 
17
19
  Prefer `DeepExpandingMenu` unless the modern implementation's browser support isn't acceptable for
18
20
  your target audience — see its own docs for the caveat.
@@ -21,7 +23,8 @@ your target audience — see its own docs for the caveat.
21
23
 
22
24
  | Prop | Type | Default | Description |
23
25
  |------|------|---------|-------------|
24
- | `tag` | `"div" \| "section" \| "nav" \| "ul" \| "ol"` | `"nav"` | Root element tag. |
26
+ | `tag` | `"div" \| "section" \| "nav"` | `"nav"` | Root element tag. `ul`/`ol` were dropped 2026-10-21: the root wraps a `<div>`, so they rendered invalid HTML. |
27
+ | `ariaLabel` | `string` | `undefined` | Accessible name on the root. Set it when the page has more than one `<nav>`. |
25
28
  | `navLinks` | `ResponsiveHeaderNavItem[]` | `[]` | Top-level nav items. An item with `path` renders as a direct link; an item with `childLinks` renders as a `<details>` group. |
26
29
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
27
30
 
@@ -53,37 +56,35 @@ const navLinks: ResponsiveHeaderNavItem[] = [
53
56
 
54
57
  ## CSS custom properties
55
58
 
56
- | Property | Default | Controls |
57
- |----------|---------|----------|
58
- | `--deep-expanding-menu-classic-gap` | `1.2rem` | Gap on the root grid |
59
- | `--deep-expanding-menu-classic-item-gap` | `2.4rem` | Gap between top-level items |
60
- | `--deep-expanding-menu-classic-link-border-width` | `0.2rem` | Bottom border width on links/toggles |
61
- | `--deep-expanding-menu-classic-link-padding-block` | `0.8rem` | Vertical padding on links/toggles |
62
- | `--deep-expanding-menu-classic-link-border-colour-hover` | `var(--blue-10)` | Bottom border colour on hover/focus |
63
- | `--deep-expanding-menu-classic-icon-size` | `1.2rem` | Caret icon size |
64
- | `--deep-expanding-menu-classic-panel-offset-top` | `4rem` | Panel offset from the toggle |
65
- | `--deep-expanding-menu-classic-panel-width` | `20rem` | Panel width below `768px` |
66
- | `--deep-expanding-menu-classic-panel-width-tablet` | `40rem` | Panel width from `768px` |
67
- | `--deep-expanding-menu-classic-panel-width-desktop` | `60rem` | Panel width from `1024px` |
68
- | `--deep-expanding-menu-classic-panel-background-colour` | `white` | Panel background |
69
- | `--deep-expanding-menu-classic-panel-border-width` | `0.1rem` | Panel border width |
70
- | `--deep-expanding-menu-classic-panel-border-colour` | `black` | Panel border colour |
71
- | `--deep-expanding-menu-classic-panel-border-radius` | `1.2rem` | Panel corner radius |
72
- | `--deep-expanding-menu-classic-panel-shadow` | `0 0 1rem rgba(0, 0, 0, 0.1)` | Panel box-shadow |
73
- | `--deep-expanding-menu-classic-panel-padding` | `1.2rem` | Panel padding |
74
- | `--deep-expanding-menu-classic-panel-heading-colour` | `var(--slate-10)` | `childLinksTitle` heading colour |
75
- | `--deep-expanding-menu-classic-panel-list-gap` | `1.2rem` | Gap between child link grid items |
76
- | `--deep-expanding-menu-classic-group-link-colour` | `var(--slate-10)` | Child link text colour |
77
- | `--deep-expanding-menu-classic-group-link-border-colour-hover` | `var(--slate-10)` | Child link bottom border colour on hover/focus |
78
-
79
- See `CONSUMER-STYLING.md` for the full token API.
59
+ Public tokens are all `--deep-expanding-menu-classic-*`: item gap, link border/padding/hover
60
+ colour, focus ring, icon size, and the panel's offset, three container-width tiers, colours,
61
+ border, radius, shadow, padding, heading line clamp, list gap and child link colours. Colours
62
+ default from `--theme-text`, `--theme-border`, `--theme-surface-subtle` and `--theme-ring`. The
63
+ full table with defaults, the prefixed class names and the sizing model are in this component's
64
+ `CONSUMER-STYLING.md`.
65
+
66
+ ## Data handling
67
+
68
+ - An item with `path` is a link; anything else is a `<details>` group. An empty `path` is a group.
69
+ - A group's heading renders only when `childLinksTitle` is non-empty, and its list only when
70
+ `childLinks` has items.
71
+ - Child links are keyed on index + name, so duplicate names are safe.
72
+ - Long or unbroken labels wrap, the top-level row wraps onto more lines, and each panel is capped
73
+ at the menu's width. A panel opens from its toggle's left edge and doesn't flip, so near the
74
+ right edge it can overrun the menu. The `StressTest` story covers all of these.
80
75
 
81
76
  ## Notes
82
77
 
83
78
  - Auto-imported in Nuxt — no manual import needed.
84
- - Multiple `<details>` groups sharing `name="navigation-group"` means opening one does **not**
85
- auto-close siblings (unlike a native accordion) — each group closes independently via
86
- click-outside.
79
+ - Until 2026-10-21 every instance used the fixed `name="navigation-group"`, so all menus on a page
80
+ formed one exclusive group (opening a panel in one menu closed another menu's), and the doc
81
+ wrongly said siblings didn't auto-close. Click-outside listeners were also bound per group in
82
+ `onMounted`, so groups added later never closed.
83
+ - 2026-10-21 migration: prefixed every structural class, moved colours to `--theme-*`, panel
84
+ offset to `calc(100% + 1rem)`, panel widths from viewport `@media` to `@container`, removed
85
+ the no-op `--deep-expanding-menu-classic-gap` and a `display: flex !important`, added
86
+ `ariaLabel`, Escape and link-follow closing, a `:focus-visible` ring, `role="list"`, an
87
+ `aria-hidden` caret, the heading line-clamp token and the `StressTest` story.
87
88
  - 2026-09-07 migration: moved from an unplaced top-level folder (was `DeepExpandingMenuOld.vue`)
88
89
  into `02.molecules/navigation/deep-expanding-menu-classic/`; converted options-style
89
90
  `defineProps` to `interface Props` + `withDefaults`; switched its locally-declared
@@ -24,7 +24,8 @@ built on `<details>`/`<summary>`: same API and visual result, with no popover la
24
24
 
25
25
  | Prop | Type | Default | Description |
26
26
  |------|------|---------|-------------|
27
- | `tag` | `"div" \| "section" \| "nav" \| "ul" \| "ol"` | `"nav"` | Root element tag. |
27
+ | `tag` | `"div" \| "section" \| "nav"` | `"nav"` | Root element tag. `ul`/`ol` were dropped 2026-10-21: the root wraps a `<div>`, so they rendered invalid HTML. |
28
+ | `ariaLabel` | `string` | `undefined` | Accessible name on the root. Set it when the page has more than one `<nav>`. |
28
29
  | `navLinks` | `ResponsiveHeaderNavItem[]` | `[]` | Top-level nav items. An item with `path` renders as a direct link; an item with `childLinks` renders as a toggle + popover panel. |
29
30
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
30
31
 
@@ -56,26 +57,22 @@ const navLinks: ResponsiveHeaderNavItem[] = [
56
57
 
57
58
  ## CSS custom properties
58
59
 
59
- | Property | Default | Controls |
60
- |----------|---------|----------|
61
- | `--deep-expanding-menu-gap` | `2.4rem` | Gap between top-level items |
62
- | `--deep-expanding-menu-link-border-width` | `0.2rem` | Bottom border width on links/toggles |
63
- | `--deep-expanding-menu-link-padding-block` | `0.8rem` | Vertical padding on links/toggles |
64
- | `--deep-expanding-menu-link-border-colour-hover` | `var(--blue-10)` | Bottom border colour on hover/focus |
65
- | `--deep-expanding-menu-icon-size` | `1.2rem` | Caret icon size |
66
- | `--deep-expanding-menu-panel-width` | `min(100%, 50vw)` | Popover panel width |
67
- | `--deep-expanding-menu-panel-background-colour` | `white` | Popover panel background |
68
- | `--deep-expanding-menu-panel-border-width` | `0.1rem` | Popover panel border width |
69
- | `--deep-expanding-menu-panel-border-colour` | `black` | Popover panel border colour |
70
- | `--deep-expanding-menu-panel-border-radius` | `1.2rem` | Popover panel corner radius |
71
- | `--deep-expanding-menu-panel-shadow` | `0 0 1rem rgba(0, 0, 0, 0.1)` | Popover panel box-shadow |
72
- | `--deep-expanding-menu-panel-padding` | `1.2rem` | Popover panel padding |
73
- | `--deep-expanding-menu-panel-heading-colour` | `var(--slate-10)` | `childLinksTitle` heading colour |
74
- | `--deep-expanding-menu-panel-list-gap` | `1.2rem` | Gap between child link grid items |
75
- | `--deep-expanding-menu-group-link-colour` | `var(--slate-10)` | Child link text colour |
76
- | `--deep-expanding-menu-group-link-border-colour-hover` | `var(--slate-10)` | Child link bottom border colour on hover/focus |
77
-
78
- See `CONSUMER-STYLING.md` for the full token API and what's not tokenised.
60
+ Public tokens are all `--deep-expanding-menu-*`: row gap, link border/padding/hover colour, focus
61
+ ring, icon size, and the panel's width, max height, colours, border, radius, shadow, padding,
62
+ heading line clamp, list gap and child link colours. Colours default from `--theme-text`,
63
+ `--theme-border`, `--theme-surface-subtle` and `--theme-ring`. The full table with defaults, the
64
+ prefixed class names and the state hooks are in this component's `CONSUMER-STYLING.md`.
65
+
66
+ ## Data handling
67
+
68
+ - An item with `path` is a link; anything else is a group (toggle + panel). An empty `path` is a group.
69
+ - A group's heading renders only when `childLinksTitle` is non-empty, and its list only when
70
+ `childLinks` has items, so an empty group opens a panel with just its heading.
71
+ - Child links are keyed on index + name, so duplicate names are safe.
72
+ - Long or unbroken labels wrap (`overflow-wrap: anywhere`), the top-level row wraps onto more
73
+ lines, panel lists longer than `--deep-expanding-menu-panel-max-height` scroll, and a panel near
74
+ the right edge flips to align with its toggle's right edge where anchor positioning is supported.
75
+ The `StressTest` story covers all of these.
79
76
 
80
77
  ## Notes
81
78
 
@@ -92,6 +89,10 @@ See `CONSUMER-STYLING.md` for the full token API and what's not tokenised.
92
89
  `aria-expanded` revealing ordinary links), not an ARIA menu. `role="menu"` was removed 2026-10-05
93
90
  because it requires `menuitem` children and arrow-key handling the panels don't have, so screen
94
91
  readers announced a menu that didn't behave like one. Don't add it back.
92
+ - 2026-10-21 migration: prefixed every structural class (`.inner`, `.navigation-*`, `.icon` became
93
+ `.deep-expanding-menu-*`), moved colour defaults to `--theme-*` tokens, added `ariaLabel`, a
94
+ `:focus-visible` ring, `role="list"` on the child list, an `aria-hidden` caret, the heading
95
+ line-clamp and max-height tokens, `position-try-fallbacks: flip-inline`, and the `StressTest` story.
95
96
  - 2026-10-05: removed two never-referenced `@position-try` blocks (`--anchor-left`, and an invalid
96
97
  `@position-try-fallbacks --anchor-right` at-rule).
97
98
  - 2026-09-07 migration: moved from an unplaced top-level folder into
@@ -134,6 +134,8 @@ consumer's `<style scoped>` file, `.grid-stack__layer` rules need `:deep()`. Ful
134
134
 
135
135
  ## Notes
136
136
 
137
+ - The `StressTest` story stacks an overlay of unbroken, long and right-to-left text over an image layer.
138
+
137
139
  - `pointer-events: none` on overlay layers (and `pointer-events: auto` on interactive children within them) is the standard pattern for overlays that shouldn't block interaction with layers behind them.
138
140
  - There is no built-in `z-index` — stacking is handled purely by DOM order. If a consumer applies `z-index` on a layer for other reasons, be aware it creates a new stacking context.
139
141
  - The slot name is used as the Vue `:key` on the layer wrapper, so slot names must be unique.
@@ -130,6 +130,8 @@ Tokens: `--layout-grid-by-cols-column-count`, `-single-col-below`, `-gap`, `-row
130
130
 
131
131
  ## Notes
132
132
 
133
+ - The `StressTest` story feeds every cell oversized content (an unbroken string and URL, long German, a 1600px image, emoji, right-to-left, an empty cell).
134
+
133
135
  - Auto-imported in Nuxt — no manual import needed.
134
136
  - Slot names can be anything — semantic or indexed. Document order determines render order.
135
137
  - 2026-09-27 migration: `singleColBelow` never worked before (the breakpoint was a hardcoded `768px` container query); it does now. `gap`/`columnCount` moved from `v-bind()` to public tokens, and the layout props lost their defaults (defaults now live in CSS). A `section` without a `label` used to read the placeholder "If tag='section' then a label is required" to screen readers; it now renders nothing and relies on the composable's dev warning. `gap` must be a single length (compound values were claimed before but break the column calculation). Inner class `.layout-grid-inner` → `.layout-grid-by-cols-inner`; root no longer a `layoutGrid` size container. No consumer app used the component at the time.
@@ -98,9 +98,17 @@ See `CONSUMER-STYLING.md` in the component folder.
98
98
 
99
99
  ## Notes
100
100
 
101
+ - 2026-10-21: tiles re-pack when any tile changes height (a `ResizeObserver` per tile, throttled to
102
+ one `requestAnimationFrame`), not only when the container width changes, so a late-loading image
103
+ no longer overlaps the tile below. Slot names are read per render (they were a `computed` over
104
+ `useSlots()`, which missed slots added after mount), and tiles are read in DOM order from the
105
+ wrapper instead of a `v-for` ref array, which Vue doesn't keep in DOM order. The multi-column state
106
+ is `data-multiple-cols` and the private tokens are prefixed `--_masonry-grid-*`.
107
+ - The `StressTest` story mixes unbroken, long, image, emoji, right-to-left, empty and one-character tiles.
108
+
101
109
  - **How positioning works**: on mount and on every measured resize, each item is read (`item.
102
110
  offsetHeight`) in DOM order and placed at the top of whichever column currently has the least
103
- accumulated height, via inline `--_position-top`/`--_position-left`/`--_element-width` custom
111
+ accumulated height, via inline `--_masonry-grid-item-top`/`--_masonry-grid-item-left`/`--_masonry-grid-item-width` custom
104
112
  properties the component sets directly (not part of the public token API — these are internal
105
113
  positioning state, not a styling surface). Column steps are computed in real pixels
106
114
  (`itemWidth + gap`) rather than equal percentage slots of the container — a percentage slot only
@@ -24,6 +24,9 @@ no extra wrapper divs needed.
24
24
 
25
25
  ---
26
26
 
27
+ The `variant` prop renders as `data-variant` on the root (a bare class like `.content` until
28
+ 2026-10-21). The `StressTest` story nests every variant with oversized content.
29
+
27
30
  ## Track widths
28
31
 
29
32
  | Variant | Max width | Typical use |
@@ -17,7 +17,9 @@ effects, styled as a monospace line over an underline with a pulsing cursor glyp
17
17
 
18
18
  Effects:
19
19
 
20
- - `typewriter` — characters typed one by one, held, deleted one by one.
20
+ - `typewriter` — characters typed one by one, held, deleted one by one. "Characters" are graphemes
21
+ (`splitGraphemes()` in `app/utils/graphemes.ts`, `Intl.Segmenter`), so an emoji with a skin tone
22
+ or a family emoji never shows half a glyph.
21
23
  - `word-pulse` — the whole message fades in, holds, fades out.
22
24
 
23
25
  Both use `useCancellableTimer` for all timing, so the loop exits cleanly on unmount.
@@ -31,8 +33,9 @@ Both use `useCancellableTimer` for all timing, so the loop exits cleanly on unmo
31
33
  deletes or fades out, so a repeated message announces again.
32
34
  - **Pause on hover** (WCAG 2.2.2): pointer over the root pauses the sequence where it is and the
33
35
  cursor pulse; leaving resumes. The root gets `data-paused` while paused.
34
- - `prefers-reduced-motion: reduce` removes the cursor pulse and the word-pulse fade transition. The
35
- text sequence keeps running (it's the content).
36
+ - `prefers-reduced-motion: reduce` removes the cursor pulse and the word-pulse fade transition, and
37
+ the typewriter shows each message whole instead of typing and deleting it (since 2026-10-21). The
38
+ message sequence keeps running (it's the content).
36
39
 
37
40
  > **Changed 2026-09-27**: `SamaritanPrompt` previously had no live region (the visible text was read
38
41
  > character by character, if at all) and no reduced-motion handling; both now match
@@ -123,9 +126,13 @@ props on the component tag are hyphenated.
123
126
 
124
127
  Full token list and class hooks: `app/components/02.molecules/samaritan-prompt/CONSUMER-STYLING.md`.
125
128
  Tokens are `--samaritan-prompt-*` (text/underline/cursor colours, font, sizes, gaps, pulse
126
- duration). Defaults are white text on the assumption of a dark page; for a light or theme-switching
127
- page set `--samaritan-prompt-text-colour` and `--samaritan-prompt-underline-colour` (e.g. to
128
- `var(--theme-text)`).
129
+ duration, white-space, line clamp). Colours default to `--theme-text` (text, underline) and
130
+ `--theme-accent` (cursor); set the three colour tokens to `#fff`, `#fff`, `#cc0000` for the classic
131
+ white-on-black look. Text wraps by default (`--samaritan-prompt-text-white-space: nowrap` restores
132
+ one line).
133
+
134
+ > **Changed 2026-10-21**: colour defaults were `#ffffff` text/underline and a `#cc0000` cursor on the
135
+ > assumption of a dark page, and text was always `nowrap`, so a long message overflowed.
129
136
 
130
137
  The component brings no page layout; centre it from its parent.
131
138
 
@@ -134,7 +141,16 @@ The component brings no page layout; centre it from its parent.
134
141
 
135
142
  ## Notes
136
143
 
144
+ - Both stories files have a `StressTest` story (long German, unbroken URL, empty message, emoji,
145
+ HTML-like text, right-to-left, single character) with the `CanvasSwitcher` decorator.
146
+
137
147
  - `SamaritanPromptMixed` reads `messageConfigs` at the start of each loop, so prop changes land on
138
- the next loop. `SamaritanPrompt` restarts only when `effect` changes.
148
+ the next loop. With an empty list it polls every 500 ms (or `introDelay`, if longer) and starts
149
+ once messages arrive; until 2026-10-21 an empty list with `introDelay: 0` spun forever and froze
150
+ the tab.
151
+ - `SamaritanPrompt` restarts when `effect` or the `messages` values change (compared by value, so
152
+ an inline array literal re-created on every parent render doesn't restart it). Until 2026-10-21 it
153
+ only restarted on `effect`, and an empty string in `messages` (or a list that shrank mid-cycle)
154
+ stopped the loop for good.
139
155
  - The `Mono MMM 5` font is declared with `@font-face` in each component's unscoped style from
140
- `/fonts/monoMMM_5.ttf` (`font-display: swap` in `SamaritanPrompt`, `optional` in `SamaritanPromptMixed`).
156
+ `/fonts/monoMMM_5.ttf` (`font-display: swap` in both since 2026-10-21; `SamaritanPromptMixed` used `optional`).
@@ -23,7 +23,7 @@ Branding (logo/wordmark) is entirely consumer-authored via the `#branding` slot
23
23
  | `pageRowVariant` | `"full" \| "popout" \| "content" \| "inset-content"` | `"content"` | Forwarded to the root `PageRow`'s `variant` prop. |
24
24
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes on the root `PageRow` — targets `--site-header-*` tokens (see `CONSUMER-STYLING.md`). |
25
25
  | `navStyleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes on the nested `ResponsiveHeader` — targets its own `--responsive-header-*`/`--overflow-nav-*` tokens. |
26
- | `homeNavAriaLabel` | `string` | `"Home Navigation"` | aria-label on the home/branding nav landmark — override for localisation. |
26
+ | `homeNavAriaLabel` | `string` | `"Home navigation"` | aria-label on the home/branding nav landmark — override for localisation. |
27
27
  | `mainNavAriaLabel` | `string` | `"Main navigation"` | Forwarded to `ResponsiveHeader`'s `mainNavAriaLabel` — override for localisation. |
28
28
  | `secondaryNavAriaLabel` | `string` | `"Secondary navigation"` | Forwarded to `ResponsiveHeader`'s `secondaryNavAriaLabel` — override for localisation. |
29
29
  | `overflowMenuAriaLabel` | `string` | `"Overflow navigation menu"` | Forwarded to `ResponsiveHeader`'s `overflowMenuAriaLabel` — override for localisation. |
@@ -97,8 +97,22 @@ const responsiveNavLinks = {
97
97
 
98
98
  ---
99
99
 
100
+ ## Styling and layout
101
+
102
+ Tokens, class names and the sticky recipe are in this component's `CONSUMER-STYLING.md`. Since
103
+ 2026-10-21:
104
+
105
+ - `--site-header-position: sticky` (with `--site-header-sticky-offset`, `--site-header-z-index`)
106
+ applies to the outer `.site-header-row` (the `PageRow`). Before that it sat on the inner
107
+ `<header>`, whose parent is exactly its height, so the header never actually stuck.
108
+ - The branding column is `fit-content(var(--site-header-branding-max-width, 50%))` and the nav
109
+ column `minmax(0, 1fr)`, so a long brand wraps instead of squeezing the nav, and the nav can
110
+ shrink and collapse instead of overflowing. The `StressTest` story covers it.
111
+ - `.home-navigation` became `.site-header-home-nav`, and `homeNavAriaLabel` defaults to
112
+ `"Home navigation"` (was `"Home Navigation"`).
113
+
100
114
  ## Migration notes
101
115
 
102
- - Consuming layouts previously assembling `PageRow` + `SkipLinks` + `ResponsiveHeader` (or the removed `TabNavigation`) by hand can switch to `SiteHeader` directly; the two `styleClassPassthrough` props map 1:1 onto the two independent passthrough hooks that pattern used.
116
+ - Consuming layouts previously assembling `PageRow` + `SkipLinks` + `ResponsiveHeader` (or the removed `TabNavigation`) by hand can switch to `SiteHeader` directly; the two `styleClassPassthrough` props map 1:1 onto the two independent passthrough hooks that pattern used. Their hand-built markup often uses a `.home-navigation` class of its own; SiteHeader's nav is `.site-header-home-nav`, so move any styles across.
103
117
  - `TabNavigation` (removed 2026-10-07) took `NavItemData` (`{ main: [{ text, href, iconName, cssName }] }`, also removed), which is **not** the same shape as `ResponsiveHeaderProp` (`{ groupKey: [{ name, path, childLinksTitle, childLinks }] }`): migrating means reshaping the nav data, not just swapping the component. See "Migrating from TabNavigation" in `responsive-header.md`. Its `navAlign` prop has no `SiteHeader` equivalent since `ResponsiveHeader`'s layout is fixed (branding column + nav column).
104
118
  - Requires the consuming layout's main/footer regions to carry `id="main-content"` / `id="footer-content"` — same requirement as using `SkipLinks` directly.