srcdev-nuxt-components 9.6.18 → 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 (187) hide show
  1. package/.claude/rules/consumer/css-relative-colour-calc.md +15 -0
  2. package/.claude/rules/consumer/ids-from-useid.md +3 -2
  3. package/.claude/rules/consumer/vue-dynamic-arg-no-spaces.md +16 -0
  4. package/.claude/skills/component-compliance-checklist.md +11 -1
  5. package/.claude/skills/component-dynamic-slots.md +2 -2
  6. package/.claude/skills/component-naming.md +1 -1
  7. package/.claude/skills/components/accordion-chat.md +99 -0
  8. package/.claude/skills/components/accordion-group.md +98 -0
  9. package/.claude/skills/components/action-menu.md +11 -5
  10. package/.claude/skills/components/auto-grid.md +3 -1
  11. package/.claude/skills/components/capture-qr-code.md +9 -5
  12. package/.claude/skills/components/carousel-flip.md +41 -30
  13. package/.claude/skills/components/column-flow-grid.md +2 -0
  14. package/.claude/skills/components/dashboard-quad-grid.md +7 -2
  15. package/.claude/skills/components/dashboard-stats-grid.md +4 -0
  16. package/.claude/skills/components/decode-qr-code.md +7 -2
  17. package/.claude/skills/components/deep-expanding-menu-classic.md +31 -30
  18. package/.claude/skills/components/deep-expanding-menu.md +22 -21
  19. package/.claude/skills/components/expanding-panel-classic.md +9 -7
  20. package/.claude/skills/components/expanding-panel.md +9 -8
  21. package/.claude/skills/components/grid-stack.md +2 -0
  22. package/.claude/skills/components/layout-grid-by-cols.md +2 -0
  23. package/.claude/skills/components/masonry-grid.md +9 -1
  24. package/.claude/skills/components/page-row.md +3 -0
  25. package/.claude/skills/components/samaritan-prompt.md +24 -8
  26. package/.claude/skills/components/site-header.md +16 -2
  27. package/.claude/skills/components/sparkle-effect.md +79 -0
  28. package/.claude/skills/index.md +16 -14
  29. package/.claude/skills/storybook-add-story.md +33 -0
  30. package/.claude/skills/testing-add-unit-test.md +26 -0
  31. package/.claude/skills/using-component-skills.md +1 -1
  32. package/.vscode/srcdev-component-accordion-chat.code-snippets +42 -0
  33. package/.vscode/srcdev-component-accordion-group.code-snippets +38 -0
  34. package/.vscode/srcdev-component-action-menu.code-snippets +3 -3
  35. package/.vscode/srcdev-component-capture-decode-qr-code.code-snippets +11 -2
  36. package/.vscode/srcdev-component-carousel-flip.code-snippets +22 -4
  37. package/.vscode/srcdev-component-deep-expanding-menu-classic.code-snippets +32 -2
  38. package/.vscode/srcdev-component-deep-expanding-menu.code-snippets +31 -1
  39. package/.vscode/srcdev-component-samaritan-prompt.code-snippets +5 -4
  40. package/.vscode/srcdev-component-site-header.code-snippets +13 -1
  41. package/.vscode/srcdev-component-sparkle-effect.code-snippets +33 -0
  42. package/app/components/01.atoms/animations/sparkle-effect/CONSUMER-STYLING.md +75 -0
  43. package/app/components/01.atoms/animations/sparkle-effect/SparkleEffect.vue +213 -0
  44. package/app/components/01.atoms/animations/sparkle-effect/stories/SparkleEffect.stories.ts +303 -0
  45. package/app/components/01.atoms/animations/sparkle-effect/tests/SparkleEffect.spec.ts +150 -0
  46. package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +1 -1
  47. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +12 -2
  48. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue +19 -17
  49. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts +42 -0
  50. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/tests/DashboardQuadGrid.spec.ts +31 -44
  51. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +13 -1
  52. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +40 -31
  53. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/stories/DashboardStatsGrid.stories.ts +47 -0
  54. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/tests/DashboardStatsGrid.spec.ts +51 -53
  55. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/CONSUMER-STYLING.md +3 -0
  56. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/LayoutGridByCols.vue +4 -0
  57. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/stories/LayoutGridByCols.stories.ts +47 -2
  58. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/LayoutGridByCols.spec.ts +36 -23
  59. package/app/components/01.atoms/grids/auto-grid/AutoGrid.vue +7 -3
  60. package/app/components/01.atoms/grids/auto-grid/CONSUMER-STYLING.md +1 -1
  61. package/app/components/01.atoms/grids/auto-grid/stories/AutoGrid.stories.ts +46 -1
  62. package/app/components/01.atoms/grids/auto-grid/tests/AutoGrid.spec.ts +40 -27
  63. package/app/components/01.atoms/grids/auto-grid/tests/__snapshots__/AutoGrid.spec.ts.snap +1 -1
  64. package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +9 -0
  65. package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +3 -2
  66. package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +46 -0
  67. package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +24 -11
  68. package/app/components/01.atoms/grids/grid-stack/CONSUMER-STYLING.md +3 -0
  69. package/app/components/01.atoms/grids/grid-stack/GridStack.vue +2 -0
  70. package/app/components/01.atoms/grids/grid-stack/stories/GridStack.stories.ts +41 -0
  71. package/app/components/01.atoms/grids/grid-stack/tests/GridStack.spec.ts +26 -13
  72. package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +15 -0
  73. package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +51 -31
  74. package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +48 -0
  75. package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +84 -29
  76. package/app/components/01.atoms/page-row/CONSUMER-STYLING.md +6 -1
  77. package/app/components/01.atoms/page-row/PageRow.vue +16 -15
  78. package/app/components/01.atoms/page-row/stories/PageRow.stories.ts +23 -0
  79. package/app/components/01.atoms/page-row/tests/PageRow.spec.ts +6 -5
  80. package/app/components/02.molecules/action-menu/ActionMenu.vue +6 -2
  81. package/app/components/02.molecules/action-menu/ActionMenuItem.vue +6 -3
  82. package/app/components/02.molecules/action-menu/CONSUMER-STYLING.md +11 -4
  83. package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +130 -18
  84. package/app/components/02.molecules/expandable/accordion-chat/AccordionChat.vue +227 -0
  85. package/app/components/02.molecules/expandable/accordion-chat/CONSUMER-STYLING.md +150 -0
  86. package/app/components/02.molecules/expandable/accordion-chat/stories/AccordionChat.stories.ts +347 -0
  87. package/app/components/02.molecules/expandable/accordion-chat/tests/AccordionChat.spec.ts +205 -0
  88. package/app/components/02.molecules/expandable/accordion-chat/tests/__snapshots__/AccordionChat.spec.ts.snap +40 -0
  89. package/app/components/02.molecules/expandable/{accordian/AccordianCore.vue → accordion-group/AccordionGroup.vue} +32 -20
  90. package/app/components/02.molecules/expandable/accordion-group/CONSUMER-STYLING.md +66 -0
  91. package/app/components/02.molecules/expandable/accordion-group/stories/AccordionGroup.stories.ts +238 -0
  92. package/app/components/02.molecules/expandable/accordion-group/tests/AccordionGroup.spec.ts +172 -0
  93. package/app/components/02.molecules/expandable/accordion-group/tests/__snapshots__/AccordionGroup.spec.ts.snap +45 -0
  94. package/app/components/02.molecules/expandable/expanding-panel/CONSUMER-STYLING.md +1 -0
  95. package/app/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue +11 -6
  96. package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +82 -1
  97. package/app/components/02.molecules/expandable/expanding-panel/tests/ExpandingPanel.spec.ts +24 -30
  98. package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +8 -8
  99. package/app/components/02.molecules/expandable/expanding-panel-classic/CONSUMER-STYLING.md +1 -0
  100. package/app/components/02.molecules/expandable/expanding-panel-classic/ExpandingPanelClassic.vue +10 -5
  101. package/app/components/02.molecules/expandable/expanding-panel-classic/stories/ExpandingPanelClassic.stories.ts +83 -1
  102. package/app/components/02.molecules/expandable/expanding-panel-classic/tests/ExpandingPanelClassic.spec.ts +24 -30
  103. package/app/components/02.molecules/expandable/expanding-panel-classic/tests/__snapshots__/ExpandingPanelClassic.spec.ts.snap +8 -8
  104. package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +63 -44
  105. package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +74 -37
  106. package/app/components/02.molecules/navigation/deep-expanding-menu/stories/DeepExpandingMenu.stories.ts +139 -19
  107. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +90 -27
  108. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +8 -8
  109. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +58 -26
  110. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +113 -57
  111. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/stories/DeepExpandingMenuClassic.stories.ts +139 -19
  112. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/DeepExpandingMenuClassic.spec.ts +140 -22
  113. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/__snapshots__/DeepExpandingMenuClassic.spec.ts.snap +9 -9
  114. package/app/components/02.molecules/qr-code/CONSUMER-STYLING.md +22 -7
  115. package/app/components/02.molecules/qr-code/CaptureQrCode.vue +63 -71
  116. package/app/components/02.molecules/qr-code/DecodeQrCode.vue +47 -10
  117. package/app/components/02.molecules/qr-code/stories/CaptureQrCode.stories.ts +114 -26
  118. package/app/components/02.molecules/qr-code/stories/DecodeQrCode.stories.ts +115 -24
  119. package/app/components/02.molecules/qr-code/tests/CaptureQrCode.spec.ts +124 -51
  120. package/app/components/02.molecules/qr-code/tests/DecodeQrCode.spec.ts +74 -25
  121. package/app/components/02.molecules/samaritan-prompt/CONSUMER-STYLING.md +22 -10
  122. package/app/components/02.molecules/samaritan-prompt/SamaritanPrompt.vue +30 -12
  123. package/app/components/02.molecules/samaritan-prompt/SamaritanPromptMixed.vue +34 -10
  124. package/app/components/02.molecules/samaritan-prompt/stories/SamaritanPrompt.stories.ts +55 -4
  125. package/app/components/02.molecules/samaritan-prompt/stories/SamaritanPromptMixed.stories.ts +67 -7
  126. package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPrompt.spec.ts +113 -29
  127. package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPromptMixed.spec.ts +92 -45
  128. package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +82 -29
  129. package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +176 -113
  130. package/app/components/03.organisms/image-galleries/carousel-flip/stories/CarouselFlip.stories.ts +153 -23
  131. package/app/components/03.organisms/image-galleries/carousel-flip/tests/CarouselFlip.spec.ts +119 -63
  132. package/app/components/03.organisms/responsive-header/tests/__snapshots__/NavigationItems.spec.ts.snap +2 -2
  133. package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +2 -2
  134. package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +8 -8
  135. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +4 -4
  136. package/app/components/03.organisms/site-header/CONSUMER-STYLING.md +41 -23
  137. package/app/components/03.organisms/site-header/SiteHeader.vue +16 -21
  138. package/app/components/03.organisms/site-header/stories/SiteHeader.stories.ts +138 -0
  139. package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +53 -26
  140. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +2 -2
  141. package/app/types/components/qr-code.d.ts +12 -0
  142. package/app/utils/graphemes.ts +8 -0
  143. package/package.json +1 -1
  144. package/.claude/skills/components/accordian-core.md +0 -180
  145. package/.vscode/srcdev-component-accordian-core.code-snippets +0 -74
  146. package/app/components/01.atoms/animations/entry/.nuxt/app.config.mjs +0 -21
  147. package/app/components/01.atoms/animations/entry/.nuxt/components.d.ts +0 -66
  148. package/app/components/01.atoms/animations/entry/.nuxt/imports.d.ts +0 -30
  149. package/app/components/01.atoms/animations/entry/.nuxt/manifest/meta/test.json +0 -1
  150. package/app/components/01.atoms/animations/entry/.nuxt/types/app.config.d.ts +0 -35
  151. package/app/components/01.atoms/animations/entry/.nuxt/types/build.d.ts +0 -23
  152. package/app/components/01.atoms/animations/entry/.nuxt/types/builder-env.d.ts +0 -1
  153. package/app/components/01.atoms/animations/entry/.nuxt/types/components.d.ts +0 -71
  154. package/app/components/01.atoms/animations/entry/.nuxt/types/imports.d.ts +0 -386
  155. package/app/components/01.atoms/animations/entry/.nuxt/types/middleware.d.ts +0 -11
  156. package/app/components/01.atoms/animations/entry/.nuxt/types/modules.d.ts +0 -79
  157. package/app/components/01.atoms/animations/entry/.nuxt/types/nitro-layouts.d.ts +0 -17
  158. package/app/components/01.atoms/animations/entry/.nuxt/types/nitro-nuxt.d.ts +0 -64
  159. package/app/components/01.atoms/animations/entry/.nuxt/types/plugins.d.ts +0 -29
  160. package/app/components/01.atoms/animations/entry/.nuxt/types/runtime-config.d.ts +0 -32
  161. package/app/components/01.atoms/animations/entry/.nuxt/types/shared-imports.d.ts +0 -10
  162. package/app/components/01.atoms/animations/entry/.nuxt/types/vue-shim.d.ts +0 -0
  163. package/app/components/01.atoms/animations/entry/.nuxtrc +0 -1
  164. package/app/components/01.atoms/animations/entry/node_modules/.cache/nuxt/chrome-workspace.json +0 -1
  165. package/app/components/01.atoms/animations/entry/node_modules/.cache/vite/client/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  166. package/app/components/02.molecules/expandable/accordian/CONSUMER-STYLING.md +0 -100
  167. package/app/components/02.molecules/expandable/accordian/stories/AccordianCore.stories.ts +0 -272
  168. package/app/components/02.molecules/expandable/accordian/tests/AccordianCore.spec.ts +0 -257
  169. package/app/components/02.molecules/expandable/accordian/tests/__snapshots__/AccordianCore.spec.ts.snap +0 -95
  170. package/app/components/03.organisms/responsive-header/tests/.nuxt/app.config.mjs +0 -21
  171. package/app/components/03.organisms/responsive-header/tests/.nuxt/components.d.ts +0 -66
  172. package/app/components/03.organisms/responsive-header/tests/.nuxt/imports.d.ts +0 -30
  173. package/app/components/03.organisms/responsive-header/tests/.nuxt/manifest/meta/test.json +0 -1
  174. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/app.config.d.ts +0 -35
  175. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/build.d.ts +0 -23
  176. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/builder-env.d.ts +0 -1
  177. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/components.d.ts +0 -71
  178. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/imports.d.ts +0 -386
  179. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/middleware.d.ts +0 -11
  180. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/modules.d.ts +0 -79
  181. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-layouts.d.ts +0 -17
  182. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-nuxt.d.ts +0 -64
  183. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/plugins.d.ts +0 -29
  184. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/runtime-config.d.ts +0 -32
  185. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/shared-imports.d.ts +0 -10
  186. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/vue-shim.d.ts +0 -0
  187. package/app/components/03.organisms/responsive-header/tests/node_modules/.cache/nuxt/chrome-workspace.json +0 -1
@@ -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
@@ -4,7 +4,7 @@
4
4
 
5
5
  **Prefer [`ExpandingPanel`](expanding-panel.md) unless you have a concrete reason not to.** `ExpandingPanelClassic` is the pre-`::details-content` implementation, kept only as a fallback for consumers who need the open/close animation to work identically in every browser today (its `grid-template-rows: 0fr → 1fr` sibling trick has no Baseline-2025 feature dependency). `ExpandingPanel` looks and behaves identically everywhere too — the only difference is that its animation degrades to an instant (unanimated) open/close in browsers without `interpolate-size` support (Chromium only, as of 2026), where this component still animates.
6
6
 
7
- `ExpandingPanelClassic` is a single expand/collapse panel built on the native `<details>`/`<summary>` element. It animates open/close via a CSS grid-template-rows trick, supports `v-model` for controlled state, and can be locked open with `forceOpened`. Multiple panels can be grouped into a native accordion by sharing the same `name` prop (see `AccordianCore`).
7
+ `ExpandingPanelClassic` is a single expand/collapse panel built on the native `<details>`/`<summary>` element. It animates open/close via a CSS grid-template-rows trick, supports `v-model` for controlled state, and can be locked open with `forceOpened`. Multiple panels can be grouped into a native accordion by sharing the same `name` prop (see `AccordionGroup`).
8
8
 
9
9
  ---
10
10
 
@@ -12,7 +12,7 @@
12
12
 
13
13
  | Prop | Type | Default | Notes |
14
14
  |------|------|---------|-------|
15
- | `name` | `string` | `useId()` | Identifies the panel. Used in ARIA attributes (`id-{name}-trigger`, `id-{name}-content`). If omitted, a unique id is generated automatically. |
15
+ | `name` | `string` | `useId()` | The `<details>` `name`; panels sharing it form an exclusive-open group. Not used for element ids (each panel gets its own `useId()`). If omitted, a unique name is generated automatically. |
16
16
  | `animationDuration` | `number` | `400` | Expand/collapse transition duration in milliseconds. Pass `0` to disable animation. |
17
17
  | `forceOpened` | `boolean` | `false` | When `true`, the panel is always open. The toggle icon is hidden and clicks do not close the panel. |
18
18
  | `contentIsOnTop` | `boolean` | `false` | When `true`, the content region is taken out of flow and absolutely positioned directly below the summary, raised above surrounding page content via `z-index` — instead of pushing layout down when it opens. Applies a `content-is-on-top` class to the root `.expanding-panel-classic` element (not the content div). Also enables click-outside-to-close (see below) — not applied when `forceOpened` is `true`. |
@@ -132,11 +132,11 @@ The component wires ARIA automatically from the `name` prop:
132
132
 
133
133
  | Element | Attribute | Value |
134
134
  |---------|-----------|-------|
135
- | `<summary>` | `id` | `id-{name}-trigger` |
136
- | `<summary>` | `aria-controls` | `id-{name}-content` |
135
+ | `<summary>` | `id` | `{useId()}-trigger` |
136
+ | `<summary>` | `aria-controls` | `{useId()}-content` |
137
137
  | `<summary>` | `aria-expanded` | `true` / `false` |
138
- | content div | `id` | `id-{name}-content` |
139
- | content div | `aria-labelledby` | `id-{name}-trigger` |
138
+ | content div | `id` | `{useId()}-content` |
139
+ | content div | `aria-labelledby` | `{useId()}-trigger` |
140
140
  | content div | `role` | `region` |
141
141
 
142
142
  Always supply a meaningful `name` prop when using multiple panels on the same page to avoid duplicate IDs.
@@ -213,9 +213,11 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
213
213
 
214
214
  ## Notes
215
215
 
216
+ - 2026-10-20: element ids come from a per-panel `useId()` instead of `name`. Panels sharing a `name` (an exclusive group, ResponsiveHeader's overflow groups) all rendered the same `id-{name}-trigger`/`-content`, so every `aria-controls` pointed at the first panel. The same pass made the summary track `minmax(0, 1fr) auto` and added `overflow-wrap: anywhere` on the root, so a long unbroken summary wraps instead of pushing the caret out of the clipped summary. The `StressTest` story covers long, RTL, emoji and empty-slot cases.
216
217
  - The open/close animation uses `grid-template-rows: 0fr → 1fr` — no JS height measurement needed.
217
218
  - `content-is-on-top` is applied to the root `.expanding-panel-classic` element, not `.expanding-panel-classic-content` — style overrides must scope through it, e.g. `.expanding-panel-classic.my-panel .expanding-panel-classic-content .inner { ... }`.
218
219
  - When `forceOpened` is `true`, `open` stays `true` regardless of `v-model`, but `v-model` still updates internally on clicks (useful if you later set `forceOpened` back to `false`).
219
- - Group panels into a native accordion (only one open at a time) by passing the same `name` to multiple panels or use `AccordianCore` which handles this automatically.
220
+ - Group panels into a native accordion (only one open at a time) by passing the same `name` to multiple panels or use `AccordionGroup` which handles this automatically.
221
+ - **Used by:** `AccordionGroup` (`variant="classic"`, the default), `AccordionChat` (restyles the summary as a question bubble and wraps the content in an answer bubble; never touches `.inner`), `ContentDocs`, `ResponsiveHeader` and `NavigationItems` (`panelVariant="classic"`).
220
222
  - Auto-imported in Nuxt — no manual import needed.
221
223
  - File: `app/components/02.molecules/expandable/expanding-panel-classic/ExpandingPanelClassic.vue`
@@ -2,11 +2,11 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- `ExpandingPanel` is a single expand/collapse panel built on the native `<details>`/`<summary>` element. It animates open/close by styling the `::details-content` pseudo-element (the browser's own anonymous box wrapping everything after `<summary>`) where supported, supports `v-model` for controlled state, and can be locked open with `forceOpened`. Multiple panels can be grouped into a native accordion by sharing the same `name` prop (see `AccordianCore`).
5
+ `ExpandingPanel` is a single expand/collapse panel built on the native `<details>`/`<summary>` element. It animates open/close by styling the `::details-content` pseudo-element (the browser's own anonymous box wrapping everything after `<summary>`) where supported, supports `v-model` for controlled state, and can be locked open with `forceOpened`. Multiple panels can be grouped into a native accordion by sharing the same `name` prop (see `AccordionGroup`).
6
6
 
7
7
  **WebKit (Safari/iOS) does not support `::details-content`** as of 2026-08, confirmed via real-device testing — Chrome's device-emulation mode cannot catch this, since it's still the Blink engine underneath regardless of which browser UI it's simulating. WebKit silently drops the whole rule block: no animation and (more seriously) `content-is-on-top`'s `position: absolute` never applying, so an overlay panel renders in normal document flow and can break the surrounding layout. An `@supports not selector(::details-content)` CSS-only fallback was tried (2026-08-22) but didn't reliably kick in on real devices either and was reverted the same day — **do not reintroduce that approach**.
8
8
 
9
- **The actual fix is component-level, not CSS-level**: every consumer that gave `ExpandingPanel` a choice of implementation now defaults to `ExpandingPanelClassic` instead — see `AccordianCore`'s `variant` prop (default `"classic"`), `ContentDocs`'s `panelVariant` prop (default `"classic"`), and `ResponsiveHeader`/`NavigationItems`'s `panelVariant` prop (default `"classic"`). `ExpandingPanel` itself is unchanged (still `::details-content`-only, no fallback) and logs a `console.warn` in dev mode on every mount warning about this. Reach for `ExpandingPanel` directly, or pass `variant`/`panelVariant: "modern"` to one of the above, only once you've confirmed it's acceptable for your actual audience — it is **not** the safe default anywhere in this library anymore. See pitfall #19 in the repo's `CLAUDE.md` for the full history.
9
+ **The actual fix is component-level, not CSS-level**: every consumer that gave `ExpandingPanel` a choice of implementation now defaults to `ExpandingPanelClassic` instead — see `AccordionGroup`'s `variant` prop (default `"classic"`), `ContentDocs`'s `panelVariant` prop (default `"classic"`), and `ResponsiveHeader`/`NavigationItems`'s `panelVariant` prop (default `"classic"`). `ExpandingPanel` itself is unchanged (still `::details-content`-only, no fallback) and logs a `console.warn` in dev mode on every mount warning about this. Reach for `ExpandingPanel` directly, or pass `variant`/`panelVariant: "modern"` to one of the above, only once you've confirmed it's acceptable for your actual audience — it is **not** the safe default anywhere in this library anymore. See pitfall #19 in the repo's `CLAUDE.md` for the full history.
10
10
 
11
11
  ---
12
12
 
@@ -14,7 +14,7 @@
14
14
 
15
15
  | Prop | Type | Default | Notes |
16
16
  |------|------|---------|-------|
17
- | `name` | `string` | `useId()` | Identifies the panel. Used in ARIA attributes (`id-{name}-trigger`, `id-{name}-content`). If omitted, a unique id is generated automatically. |
17
+ | `name` | `string` | `useId()` | The `<details>` `name`; panels sharing it form an exclusive-open group. Not used for element ids (each panel gets its own `useId()`). If omitted, a unique name is generated automatically. |
18
18
  | `animationDuration` | `number` | `400` | Expand/collapse transition duration in milliseconds. Pass `0` to disable animation. |
19
19
  | `forceOpened` | `boolean` | `false` | When `true`, the panel is always open. The toggle icon is hidden and clicks do not close the panel. |
20
20
  | `contentIsOnTop` | `boolean` | `false` | When `true`, the content region is taken out of flow and absolutely positioned directly below the summary, raised above surrounding page content via `z-index` — instead of pushing layout down when it opens. Applies a `content-is-on-top` class to the root `.expanding-panel` element (not the content div). Also enables click-outside-to-close (see below) — not applied when `forceOpened` is `true`. |
@@ -132,11 +132,11 @@ The component wires ARIA automatically from the `name` prop:
132
132
 
133
133
  | Element | Attribute | Value |
134
134
  |---------|-----------|-------|
135
- | `<summary>` | `id` | `id-{name}-trigger` |
136
- | `<summary>` | `aria-controls` | `id-{name}-content` |
135
+ | `<summary>` | `id` | `{useId()}-trigger` |
136
+ | `<summary>` | `aria-controls` | `{useId()}-content` |
137
137
  | `<summary>` | `aria-expanded` | `true` / `false` |
138
- | content div | `id` | `id-{name}-content` |
139
- | content div | `aria-labelledby` | `id-{name}-trigger` |
138
+ | content div | `id` | `{useId()}-content` |
139
+ | content div | `aria-labelledby` | `{useId()}-trigger` |
140
140
  | content div | `role` | `region` |
141
141
 
142
142
  Always supply a meaningful `name` prop when using multiple panels on the same page to avoid duplicate IDs.
@@ -213,9 +213,10 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
213
213
 
214
214
  ## Notes
215
215
 
216
+ - 2026-10-20: element ids come from a per-panel `useId()` instead of `name`. Panels sharing a `name` (an exclusive group, ResponsiveHeader's overflow groups) all rendered the same `id-{name}-trigger`/`-content`, so every `aria-controls` pointed at the first panel. The same pass made the summary track `minmax(0, 1fr) auto` and added `overflow-wrap: anywhere` on the root, so a long unbroken summary wraps instead of pushing the caret out of the clipped summary. The `StressTest` story covers long, RTL, emoji and empty-slot cases.
216
217
  - The open/close animation targets `::details-content` (`height: 0 → auto`, `overflow: clip`) — no JS height measurement needed. Requires `interpolate-size: allow-keywords` for the height transition itself (Chromium only as of 2026; other browsers with `::details-content` support still toggle correctly, just instantly). WebKit has neither, and there is no automatic fallback — use `ExpandingPanelClassic` (see the Overview section above) for anything that needs to work correctly there.
217
218
  - `content-is-on-top` is applied to the root `.expanding-panel` element, not `.expanding-panel-content` — style overrides must scope through it, e.g. `.expanding-panel.my-panel .expanding-panel-content { ... }`.
218
219
  - When `forceOpened` is `true`, `open` stays `true` regardless of `v-model`, but `v-model` still updates internally on clicks (useful if you later set `forceOpened` back to `false`).
219
- - Group panels into a native accordion (only one open at a time) by passing the same `name` to multiple panels or use `AccordianCore` which handles this automatically.
220
+ - Group panels into a native accordion (only one open at a time) by passing the same `name` to multiple panels or use `AccordionGroup` which handles this automatically.
220
221
  - Auto-imported in Nuxt — no manual import needed.
221
222
  - File: `app/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue`
@@ -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.
@@ -0,0 +1,79 @@
1
+ ---
2
+ name: SparkleEffect
3
+ description: SparkleEffect decorative wrapper that sends sparkles along its rounded border and fades in a glow on hover or keyboard focus
4
+ type: reference
5
+ ---
6
+
7
+ # SparkleEffect
8
+
9
+ ## Overview
10
+
11
+ `SparkleEffect` wraps one element (a button, link or card) and, on hover or keyboard focus inside
12
+ it, sends small blurred sparkles travelling along the wrapper's border box while a soft glow fades in
13
+ behind the slot. Adapted from a Kevin Powell CodePen ("whimsical" button). It's a wrapper rather than
14
+ a utility class because the effect needs its own decorative elements.
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |------|------|---------|-------------|
20
+ | `count` | `number` | `5` | Number of sparkles. Floored and clamped to 1 to 12; a non-finite value falls back to 5. |
21
+ | `radius` | `string` | `undefined` | Written inline as `--sparkle-effect-radius`. Match the slotted element's corner radius. |
22
+ | `continuous` | `boolean` | `false` | Loop the sparkles for as long as the hover or focus lasts, instead of one burst per hover. Rendered as `data-continuous`. |
23
+ | `tag` | `"span" \| "div"` | `"span"` | `span` (inline-grid) for buttons and inline content, `div` (grid) for block content like cards. |
24
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root. |
25
+
26
+ ## Slots
27
+
28
+ | Slot | Description |
29
+ |---|---|
30
+ | `default` | The element that gets the effect. One element; the wrapper shrinks to it. |
31
+
32
+ ## Basic usage
33
+
34
+ ```vue
35
+ <SparkleEffect>
36
+ <InputButton button-text="Whimsy" is-pill />
37
+ </SparkleEffect>
38
+
39
+ <SparkleEffect continuous>
40
+ <InputButton button-text="Keep hovering" is-pill />
41
+ </SparkleEffect>
42
+
43
+ <SparkleEffect tag="div" radius="1.2rem" :count="7">
44
+ <NuxtLink to="/workshop" class="workshop-card">...</NuxtLink>
45
+ </SparkleEffect>
46
+ ```
47
+
48
+ ## Behaviour
49
+
50
+ - Renders the slotted content unchanged; the sparkles and glow are added around it.
51
+ - Renders `count` sparkles, floored and clamped to 1 to 12, with 5 for NaN or Infinity.
52
+ - The first five sparkles use tuned presets (lightness, hue shift, scale, drift, end point, speed)
53
+ from the original pen. Sparkles 6 to 12 reuse them with a further hue shift (35° per cycle), an end
54
+ point 13% further round the path and mirrored drift. Script writes them inline per sparkle, so there
55
+ is no `:nth-child` and slotted siblings can't shift them.
56
+ - Hover, or `:focus-visible` on anything inside, runs the effect. By default it's one burst per hover; with
57
+ `continuous` each sparkle loops at its own speed until the hover or focus ends. The slotted control keeps its own
58
+ focus ring.
59
+ - The glow sits behind the slot (`isolation: isolate` on the root, glow at `z-index: -1`), so it
60
+ works whatever stacking context the slotted element creates.
61
+ - Sparkles follow `--sparkle-effect-radius`: the wrapper can't read the child's radius.
62
+ - Sparkles are in an `aria-hidden`, `pointer-events: none` layer with nothing focusable.
63
+ - `prefers-reduced-motion: reduce` gives the glow only.
64
+ - Without `offset-path: border-box` (Safari before 17.2) no sparkles render; the glow still works.
65
+ Without relative colour syntax (Safari before 18) sparkles use the base colour unshifted.
66
+ - No effect while the slotted element is `:disabled` or `aria-disabled="true"` (InputButton
67
+ `readonly`).
68
+ - Hostile counts, long unbroken button labels, full-width block children, round icon buttons, nested
69
+ wrappers and an empty wrapper don't break layout (`StressTest` story).
70
+
71
+ ## Styling
72
+
73
+ Colour (defaults to `--theme-border`), sparkle size, sparkle blur (`1px`), duration, radius, glow blur and glow opacity
74
+ are public tokens; see `CONSUMER-STYLING.md`.
75
+
76
+ ## Stories
77
+
78
+ **Atoms/Effects/SparkleEffect**: Default (the original pen's lavender), ThemeColour, ManySparks,
79
+ Continuous, OnCard, Disabled, StressTest.
@@ -81,11 +81,12 @@ Each skill is a single markdown file named `<area>-<task>.md`.
81
81
  ├── alert-mask.md — AlertMask (renamed from AlertMaskCore): SVG border/fill sized to slotted content via ResizeObserver; config-prop geometry, --alert-mask-* colour tokens
82
82
  ├── alert-masked-content.md — AlertMaskedContent: AlertContentInner inside AlertMask's SVG mask (accent cut-out border + translucent fill); masked variant for DisplayToast/DisplayPrompt; --alert-masked-content-* colour tokens
83
83
  ├── animated-svg-text.md — AnimatedSvgText: inline SVG stroke-draw-then-fill animation, text slot, CSS token API
84
- ├── accordian-core.md — AccordianCore indexed dynamic slots (accordian-{n}-summary/icon/content), exclusive-open grouping
84
+ ├── accordion-group.md — AccordionGroup (renamed from AccordianCore): panels inferred from item-{n}-summary/content/icon slots, exclusive-open via shared name, unique per-panel ids, classic/modern variant, max-width token
85
+ ├── accordion-chat.md — AccordionChat: accordion as a chat (question bubbles right, rich answer bubbles left), avatar precedence item-{n}-avatar > #avatar > avatarLabel > none, built on ExpandingPanelClassic, full --accordion-chat-* token API (dark theme via tokens only)
85
86
  ├── eyebrow-text.md — EyebrowText props, usage patterns, styling
86
87
  ├── hero-text.md — HeroText props, usage patterns, styling
87
88
  ├── layout-grid-by-cols.md — LayoutGridByCols dynamic slots (item-{n}), column/gap/collapse props that write public --layout-grid-by-cols-* tokens (CSS-settable), section label
88
- ├── page-row.md — PageRow layout primitive: CSS grid named lines, nesting pattern, align prop, aria-labelledby, CSS token API
89
+ ├── page-row.md — PageRow layout primitive: CSS grid named lines, nesting pattern, align prop, data-variant hook, aria-labelledby, CSS token API
89
90
  ├── link-text.md — LinkText props, slots, usage patterns, styling
90
91
  ├── header-block.md — HeaderBlock: tagLevel/classLevel semantic-vs-visual heading decoupling, page-heading-N utility classes
91
92
  ├── text-block.md — TextBlock: vertical-rhythm text wrapper, --text-block-padding-block-start/-end tokens, heading-id slot prop for section/article aria-labelledby
@@ -119,30 +120,30 @@ Each skill is a single markdown file named `<area>-<task>.md`.
119
120
  ├── rotating-carousel-image.md — RotatingCarouselImage: 3D rotating image carousel, scroll-parallax tilt, focus/keyboard/hover pause, reduced-motion, CSS tokens
120
121
  ├── social-icons-list.md — SocialIconsList: data-driven social icon links, ISocialIcon type, logos: icon names (dark ones vanish on dark backgrounds; mono alternatives), showLabel + scoped icon/label slots, per-item brandColour (set brand-colour tokens on .social-icons-list-item), tile/circle/label tokens, four recipes (mono tiles, dark dock, pills, overlapping stack), unsafe hrefs skipped
121
122
  ├── display-qr-code.md — DisplayQrCode: QR code SVG from a string value, colour/size/variant/radius props, currentColor default
122
- ├── capture-qr-code.md — CaptureQrCode: live camera scanner, error state, visibility/route/KeepAlive lifecycle, media stream cleanup
123
- ├── decode-qr-code.md — DecodeQrCode: file picker + drag-and-drop image decoder, shared results list, CSS override points
124
- ├── auto-grid.md — AutoGrid: auto-fit responsive grid, $slots iteration, --auto-grid-gap/min-col-size-{small,default,large} tokens, is-responsive, semantic tag + aria
123
+ ├── capture-qr-code.md — CaptureQrCode: live camera scanner, localisable errorMessages, error slot, restarts when the tab is shown again, own-root media stream cleanup, wrapping results
124
+ ├── decode-qr-code.md — DecodeQrCode: file picker + drag-and-drop image decoder, shared results list, no-result/read-error messages, data-dropping state hook, CSS override points
125
+ ├── auto-grid.md — AutoGrid: auto-fit responsive grid, $slots iteration, --auto-grid-gap/min-col-size-{small,default,large} tokens, is-responsive (data-responsive hook), semantic tag + aria
125
126
  ├── display-avatar.md — DisplayAvatar (02.molecules): circular avatar with image/initials fallback (alt announced), broken-image fallback, data-size scale tokens, image-size, --display-avatar-* colours, chip badge, icon slot
126
127
  ├── profile-section.md — ProfileSection (02.molecules): picture + numbered profile-info-N slots, eyebrow/hero header (above, or over the picture top/bottom on a scrim via header-placement), profileLinks, container-query two-column layout, .location/.services highlight tokens
127
128
  ├── display-theme-switch.md — DisplayThemeSwitch: system/light/dark picker wrapping SegmentedControl, wired to useSettingsStore, labels/icons props, small sizing variant
128
129
  ├── card-core.md — CardCore: generic card container, dynamic named slots as rows, 4 variants, blurred backdrop layer, full CSS token API
129
- ├── action-menu.md — ActionMenu + ActionMenuItem: ellipsis trigger + anchored popover menu, indexed item-{n} slots, link/button items, full CSS token API
130
+ ├── action-menu.md — ActionMenu + ActionMenuItem: ellipsis trigger + anchored popover menu, indexed item-{n} slots, link/button items, max-width cap with wrapping labels + label line-clamp token, inline flip near the left edge, full CSS token API
130
131
  ├── select-menu.md — SelectMenu: v-model single-select listbox popover (ActionMenu's popover mechanics, InputSelect's selected-option semantics), icon/text/chevron trigger toggles, checkmark on selected option, options-array driven, full CSS token API
131
132
  ├── display-dialog.md — DisplayDialog: native <dialog> overlay, 5 variants (dialog/modal/confirm/alert/fullscreen), useDialogControls integration, InputButton close button, viewport-capped panel with wrapping title/content/footer, aria-label fallthrough when untitled, CSS token API
132
133
  ├── display-chip.md — DisplayChip: status indicator chip overlay, CSS trig positioning, circle/square shapes, status colours, icon/label content
133
134
  ├── display-pill.md — DisplayPill: pill/badge label with icon slot, 6 variants, 3 sizes, reversible order, --display-pill-* tokens (variants fall back to base colour/border/ring tokens); data-size/data-variant/data-reversed hooks (2026-10-08, were bare classes), label line-clamp token, -02 ring and outlined-monochrome recipes, StressTest story
134
- ├── carousel-flip.md — CarouselFlip: FLIP-animated carousel, carouselDataIds slot API, buttonLayout variants (sides/controls-flanking/controls-grouped-right/overlay), CSS tokens
135
- ├── samaritan-prompt.md — SamaritanPrompt + SamaritanPromptMixed: animated text prompts, typewriter/word-pulse effects, MessageConfig API, pause on hover, aria-live, CSS tokens
135
+ ├── carousel-flip.md — CarouselFlip: FLIP-animated carousel, carouselDataIds slot API, buttonLayout variants (sides/controls-flanking/controls-grouped-right/overlay/track-edges), button align/offset/edge-inset tokens, InputButton prev/next, prefixed classes + data-* state hooks, empty/single-item handling, CSS tokens
136
+ ├── samaritan-prompt.md — SamaritanPrompt + SamaritanPromptMixed: animated text prompts, typewriter/word-pulse effects, MessageConfig API, grapheme-safe typing, instant typing under reduced motion, pause on hover, aria-live, theme-default colours, wrapping + line-clamp, CSS tokens
136
137
  ├── display-toast.md — DisplayToast (standalone v-model) + DisplayToastProvider + useToastQueue (app-wide queue): stacking, FLIP dismiss, maxVisible, SemanticTheme × 4, masked SVG glass variant
137
138
  ├── display-prompt.md — DisplayPrompt: inline notification banner, SemanticTheme × 4, local vs parent-controlled dismiss, outlined modifier, CSS token override
138
139
  ├── expanding-panel-classic.md — ExpandingPanelClassic: grid-template-rows animation (no Baseline-2025 dependency), same API as ExpandingPanel, cross-browser animation parity trade-off
139
- ├── site-header.md — SiteHeader: PageRow + SkipLinks + ResponsiveHeader composition, #branding/#secondaryNavigation slots, dual styleClassPassthrough hooks
140
+ ├── site-header.md — SiteHeader: PageRow + SkipLinks + ResponsiveHeader composition, #branding/#secondaryNavigation slots, dual styleClassPassthrough hooks, sticky on the outer row, capped branding column
140
141
  ├── responsive-header.md — ResponsiveHeader: overflow-collapsing adaptive nav, measurement-pipeline gotchas (unsized icons, vw font-size drift), full CSS token API
141
142
  ├── navigation-items.md — NavigationItems: internal overflow-panel renderer for ResponsiveHeader, complement-visibility logic, not used standalone
142
143
  ├── cookie-consent-banner.md — CookieConsentBanner: fixed non-modal Accept/Reject banner built on AlertContent, driven by useCookieConsent; title/message slots, acceptLabel/rejectLabel/icon/showIcon/ariaLabel props, InputButton (inline) actions, position/button tokens (panel via --alert-content-*), teleported so tokens go on html/passthrough
143
144
  ├── display-banner.md — DisplayBanner: canvas/content stacked overlay banner, conditional slot wrappers, CSS token API
144
- ├── deep-expanding-menu.md — DeepExpandingMenu: anchor-positioned popover nav panels, browser support caveat, CSS token API
145
- ├── deep-expanding-menu-classic.md — DeepExpandingMenuClassic: <details>-based fallback nav, click-outside close, CSS token API
145
+ ├── deep-expanding-menu.md — DeepExpandingMenu: anchor-positioned popover nav panels, browser support caveat, ariaLabel, prefixed classes, wrapping/flip/scroll stress handling, CSS token API
146
+ ├── deep-expanding-menu-classic.md — DeepExpandingMenuClassic: <details>-based fallback nav, per-instance exclusive groups, outside-click/Escape/link-follow close, ariaLabel, prefixed classes, container-width panels, CSS token API
146
147
  ├── display-tooltip.md — DisplayTooltip: anchor-positioned popover trigger, browser support caveat, triggerIcon prop, type="button" trigger, hideTrigger removes it from tab order (data-hidden), CSS token API
147
148
  ├── display-tooltip-rich.md — DisplayTooltipRich (Material 3 rich tooltip, renamed from DisplayTooltipDefined 2026-10-18): structured title/body/action content with InputButton close button, per-field line-clamp tokens, composes DisplayTooltip
148
149
  ├── pop-over.md — PopOver: generic anchor-positioned disclosure panel, consumer-supplied trigger/content slots, placement prop, CSS token API
@@ -168,12 +169,13 @@ Each skill is a single markdown file named `<area>-<task>.md`.
168
169
  ├── sliding-toggle.md — SlidingToggle: single-choice segmented radiogroup with text labels of any width, measured sliding + hover indicators (useSlidingIndicator), roving tabindex/arrow keys, corner-shape token, CSS token API
169
170
  ├── entry-animation.md — EntryAnimation: scroll-driven entry animation wrapper (slide-in/zoom-reveal/exit-blur utility classes), skipAnimation for above-the-fold loop items, reduced-motion handled at the CSS layer
170
171
  ├── column-flow-grid.md — ColumnFlowGrid (renamed from MasonryGrid): CSS multi-column text-flow layout, named dynamic slots (no count/data prop), itemMinWidth/gap/unit sizing, CSS token API; not a true masonry, see "which one do I want?"
171
- ├── masonry-grid.md — MasonryGrid: real measured-height masonry (greedy shortest-column packing, animated resize) absorbed from the now-retired MasonryGridOrdered; named dynamic slots, fixedWidth/justify, CSS token API
172
+ ├── masonry-grid.md — MasonryGrid: real measured-height masonry (greedy shortest-column packing, animated resize, re-packs on any tile resize) absorbed from the now-retired MasonryGridOrdered; named dynamic slots, fixedWidth/justify, CSS token API
172
173
  ├── glowing-border.md — GlowingBorder: animated conic-gradient glow border, 5 colour variants, tag prop, full CSS token API for width/radius/surface/duration/per-variant colour stops, reduced-motion guard
174
+ ├── sparkle-effect.md — SparkleEffect: slot wrapper sending sparkles along its rounded border (offset-path) plus a glow on hover/focus-visible, count prop (1 to 12, preset table), continuous prop (loop while hovered/focused), radius prop/token, blur token, disabled suppression, reduced-motion and old-Safari glow-only fallback, CSS token API
173
175
  ├── section-parallax.md — SectionParallax: CSS fixed-background parallax section, iOS Safari limitation, when to use vs ScrollRevealImage, CSS token API, reduced-motion guard
174
176
  ├── container-glow.md — ContainerGlow (renamed from ContainerGlowCore): pointer-proximity glow-border cards, named dynamic slots (one card per slot), config-prop-driven layout/interaction, full CSS token API for static visuals, reduced-motion guard
175
- ├── dashboard-quad-grid.md — DashboardQuadGrid (renamed from LayoutGridA): fixed 4-slot dashboard grid, container-query breakpoints, CSS token API
176
- └── dashboard-stats-grid.md — DashboardStatsGrid (renamed from LayoutGridB): top-row panel cluster + bottom-row panel strip dashboard grid, topRowSlot1ItemCount/bottomRowItemCount, CSS token API
177
+ ├── dashboard-quad-grid.md — DashboardQuadGrid (renamed from LayoutGridA): fixed 4-slot dashboard grid, prefixed cell classes, container-query breakpoints, CSS token API
178
+ └── dashboard-stats-grid.md — DashboardStatsGrid (renamed from LayoutGridB): top-row panel cluster + bottom-row panel strip dashboard grid, topRowSlot1ItemCount/bottomRowItemCount (floored, clamped at 0), prefixed classes, CSS token API
177
179
  ```
178
180
 
179
181
  ## Skill file template