srcdev-nuxt-components 9.6.17 → 9.6.19

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 (59) hide show
  1. package/.claude/rules/consumer/css-relative-colour-calc.md +15 -0
  2. package/.claude/rules/consumer/vue-dynamic-arg-no-spaces.md +16 -0
  3. package/.claude/skills/component-compliance-checklist.md +6 -1
  4. package/.claude/skills/component-dynamic-slots.md +2 -2
  5. package/.claude/skills/component-naming.md +1 -1
  6. package/.claude/skills/components/accordion-chat.md +99 -0
  7. package/.claude/skills/components/accordion-group.md +98 -0
  8. package/.claude/skills/components/action-menu.md +11 -5
  9. package/.claude/skills/components/expanding-panel-classic.md +9 -7
  10. package/.claude/skills/components/expanding-panel.md +9 -8
  11. package/.claude/skills/components/responsive-header.md +1 -1
  12. package/.claude/skills/components/sparkle-effect.md +79 -0
  13. package/.claude/skills/index.md +4 -2
  14. package/.claude/skills/storybook-add-story.md +8 -0
  15. package/.claude/skills/using-component-skills.md +1 -1
  16. package/.vscode/srcdev-component-accordion-chat.code-snippets +42 -0
  17. package/.vscode/srcdev-component-accordion-group.code-snippets +38 -0
  18. package/.vscode/srcdev-component-action-menu.code-snippets +3 -3
  19. package/.vscode/srcdev-component-sparkle-effect.code-snippets +33 -0
  20. package/app/components/01.atoms/animations/sparkle-effect/CONSUMER-STYLING.md +75 -0
  21. package/app/components/01.atoms/animations/sparkle-effect/SparkleEffect.vue +213 -0
  22. package/app/components/01.atoms/animations/sparkle-effect/stories/SparkleEffect.stories.ts +303 -0
  23. package/app/components/01.atoms/animations/sparkle-effect/tests/SparkleEffect.spec.ts +150 -0
  24. package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +1 -1
  25. package/app/components/02.molecules/action-menu/ActionMenu.vue +6 -2
  26. package/app/components/02.molecules/action-menu/ActionMenuItem.vue +6 -3
  27. package/app/components/02.molecules/action-menu/CONSUMER-STYLING.md +11 -4
  28. package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +130 -18
  29. package/app/components/02.molecules/expandable/accordion-chat/AccordionChat.vue +227 -0
  30. package/app/components/02.molecules/expandable/accordion-chat/CONSUMER-STYLING.md +150 -0
  31. package/app/components/02.molecules/expandable/accordion-chat/stories/AccordionChat.stories.ts +347 -0
  32. package/app/components/02.molecules/expandable/accordion-chat/tests/AccordionChat.spec.ts +205 -0
  33. package/app/components/02.molecules/expandable/accordion-chat/tests/__snapshots__/AccordionChat.spec.ts.snap +40 -0
  34. package/app/components/02.molecules/expandable/{accordian/AccordianCore.vue → accordion-group/AccordionGroup.vue} +32 -20
  35. package/app/components/02.molecules/expandable/accordion-group/CONSUMER-STYLING.md +66 -0
  36. package/app/components/02.molecules/expandable/accordion-group/stories/AccordionGroup.stories.ts +238 -0
  37. package/app/components/02.molecules/expandable/accordion-group/tests/AccordionGroup.spec.ts +172 -0
  38. package/app/components/02.molecules/expandable/accordion-group/tests/__snapshots__/AccordionGroup.spec.ts.snap +45 -0
  39. package/app/components/02.molecules/expandable/expanding-panel/CONSUMER-STYLING.md +1 -0
  40. package/app/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue +11 -6
  41. package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +82 -1
  42. package/app/components/02.molecules/expandable/expanding-panel/tests/ExpandingPanel.spec.ts +24 -30
  43. package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +8 -8
  44. package/app/components/02.molecules/expandable/expanding-panel-classic/CONSUMER-STYLING.md +1 -0
  45. package/app/components/02.molecules/expandable/expanding-panel-classic/ExpandingPanelClassic.vue +10 -5
  46. package/app/components/02.molecules/expandable/expanding-panel-classic/stories/ExpandingPanelClassic.stories.ts +83 -1
  47. package/app/components/02.molecules/expandable/expanding-panel-classic/tests/ExpandingPanelClassic.spec.ts +24 -30
  48. package/app/components/02.molecules/expandable/expanding-panel-classic/tests/__snapshots__/ExpandingPanelClassic.spec.ts.snap +8 -8
  49. package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +6 -2
  50. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +21 -0
  51. package/app/components/03.organisms/responsive-header/tests/__snapshots__/NavigationItems.spec.ts.snap +2 -2
  52. package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +2 -2
  53. package/package.json +1 -1
  54. package/.claude/skills/components/accordian-core.md +0 -180
  55. package/.vscode/srcdev-component-accordian-core.code-snippets +0 -74
  56. package/app/components/02.molecules/expandable/accordian/CONSUMER-STYLING.md +0 -100
  57. package/app/components/02.molecules/expandable/accordian/stories/AccordianCore.stories.ts +0 -272
  58. package/app/components/02.molecules/expandable/accordian/tests/AccordianCore.spec.ts +0 -257
  59. package/app/components/02.molecules/expandable/accordian/tests/__snapshots__/AccordianCore.spec.ts.snap +0 -95
@@ -0,0 +1,15 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ - "app/**/*.css"
5
+ ---
6
+
7
+ # No `calc()` on relative-colour channels in a stylesheet
8
+
9
+ Never write `calc()` over a relative-colour channel keyword in a `<style>` block or CSS file
10
+ (`oklch(from var(--x) l c calc(h + 50))`). When a value like that is needed, set the whole colour as
11
+ a custom property from script (an inline `:style`) and read it with `var()` in the CSS.
12
+
13
+ Why: the build's PostCSS `calc()` parser doesn't know channel keywords (`h`, `l`, `c`) and warns
14
+ "Lexical error ... Unrecognized text" on every build, even though browsers accept it. Inline styles
15
+ never go through PostCSS (`SparkleEffect`, 2026-10-20).
@@ -0,0 +1,16 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ - "app/**/*.stories.ts"
5
+ ---
6
+
7
+ # No spaces inside a dynamic slot or argument name
8
+
9
+ Never put a space inside `#[...]`, `:[...]` or `@[...]`: write `` #[`item-${i}-summary`] ``, not
10
+ `` #[`item-${i + 6}-summary`] ``. When the name needs arithmetic, build it in script and bind it
11
+ (`#[item.summarySlot]`).
12
+
13
+ Why: an HTML attribute name ends at the first space, so the template compiler sees a cut-off
14
+ expression ("Missing } in template expression"). In a story's template string it only fails when
15
+ Storybook renders the story. Detail: skill `storybook-add-story.md` (srcdev-nuxt-components),
16
+ "Dynamic slot names in template strings".
@@ -121,7 +121,9 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
121
121
  template directly to the live object (`v-model="args.modelValue"`, `v-bind="componentArgs"`
122
122
  where `componentArgs` is a `computed()` that strips only genuinely non-prop extra args) rather
123
123
  than copying values out in `setup()` — a destructure/spread from `args` is only safe when it's
124
- inside a `computed()`. The ledger's `story_args_bug` column flags this automatically (any
124
+ inside a `computed()`. Binding raw `args` (`v-bind="args"`) when the meta has story-only args is the
125
+ same bug class in a different shape: the extras land on the root as HTML attributes (`ActionMenu`'s
126
+ `itemcount="5"`, 2026-10-20), and the ledger doesn't see it. The ledger's `story_args_bug` column flags the destructure case automatically (any
125
127
  `{...} = args;` spread not wrapped in a `computed()`); still eyeball each story file yourself,
126
128
  since a differently-shaped variant of the same mistake may not match that exact heuristic.
127
129
  6b. **Stress-test story** — add a `StressTest` story ("Stress Test (Worst-Case Data)") that
@@ -309,6 +311,9 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
309
311
  - Element ids come from `useId()`, never from data values (rule `consumer/ids-from-useid.md`).
310
312
  Call `useId()` once at setup, never inside a `computed` (`DisplayTooltip`, `DisplayTooltipRich`,
311
313
  2026-10-18).
314
+ - A wrapper forwards a slot to its child only when the consumer filled it
315
+ (`<template v-if="slots[name]" #icon>`). An always-present `<template #icon>` replaces the child's
316
+ fallback with nothing: `AccordionGroup` hid the panels' default caret this way until 2026-10-20.
312
317
  - A wrapper passes its child only props the child declares. Anything else falls through as an
313
318
  HTML attribute on the child's root: `InputTextField` put `label="…"` and
314
319
  `error-message="…"` on `.input-text-wrapper` (fixed 2026-10-16). Check the child root's
@@ -91,7 +91,7 @@ predictable pattern (`item-0`, `item-1`, …).
91
91
  | Reason to keep `itemCount` | Example |
92
92
 
93
93
  |---|---|
94
- | Multiple slot types per item must be grouped into one rendered element | `AccordianCore` — `summary`/`icon`/`content` per `<ExpandingPanel>` |
94
+ | Multiple slot types per item must be grouped into one rendered element | `AccordionGroup` — `summary`/`icon`/`content` per panel |
95
95
  | Two parallel loops must stay in sync for aria linking | `TabbedContent` — trigger `<button>` loop + panel `<div>` loop linked by id |
96
96
  | `itemCount` drives non-slot logic (z-index, CSS scope, counters) | `WipeAwayVertical` — `z-index: itemCount - key`, `timelineScope` computed |
97
97
  | `$slots` inspection per iteration is needed to conditionally render | `StepperList` — checks `$slots[indicator-N]` to toggle class per `<li>` |
@@ -131,7 +131,6 @@ Some components pair multiple slot types per index (e.g. TabbedContent):
131
131
  ### Reference components (indexed dynamic slots)
132
132
 
133
133
  - `app/components/01.atoms/tabbed-content/TabbedContent.vue`
134
- - `app/components/accordian/AccordianCore.vue`
135
134
  - `app/components/02.molecules/stepper-list/StepperList.vue`
136
135
  - `app/components/01.atoms/animations/view-timeline/WipeAwayVertical.vue`
137
136
 
@@ -200,6 +199,7 @@ No count prop needed — just add slots:
200
199
  ### Examples (prefixed slot inference)
201
200
 
202
201
  - `app/components/02.molecules/profile-section/ProfileSection.vue`
202
+ - `app/components/02.molecules/expandable/accordion-group/AccordionGroup.vue` (three slot kinds per item; count from `item-{n}-summary`)
203
203
 
204
204
  ---
205
205
 
@@ -107,7 +107,7 @@ external consumer, free to rename. Run `/check-component-usage` for the current
107
107
  | ~~AlertMaskCore~~ | ✅ `AlertMask` (2026-10-17) | - |
108
108
  | ~~DisplayTooltipDefined~~ | ✅ `DisplayTooltipRich` (2026-10-18): Material 3 "rich tooltip", the implementation word "Defined" said nothing about what it is | - |
109
109
  | CardCore | **decide**: `Card` is single-word and collision-prone; prefer a prefixed name (`Display*`, `Content*`) | yes |
110
- | AccordianCore | **decide**: also fixes the "Accordian" misspelling; bare `Accordion` is single-word and collision-prone, and it is built on `<details>`/`<summary>` | yes |
110
+ | ~~AccordianCore~~ | ✅ `AccordionGroup` (2026-10-20) | yes |
111
111
  | ~~TabsCore~~ | ✅ `TabbedContent` (2026-10-07) | - |
112
112
 
113
113
  The `input-checkbox` family also has `MultipleCheckboxes`/`SingleCheckbox` on top of the Core; look
@@ -0,0 +1,99 @@
1
+ ---
2
+ name: AccordionChat
3
+ description: AccordionChat renders an accordion as a chat conversation, question bubbles on the right and rich answer bubbles with an optional avatar on the left
4
+ type: reference
5
+ ---
6
+
7
+ # AccordionChat
8
+
9
+ ## Overview
10
+
11
+ `AccordionChat` is an accordion drawn as a chat: each panel's summary is a question bubble on the
12
+ right, and its content is an answer bubble on the left, with an optional avatar. Use it for FAQs or
13
+ "ask us" sections where a conversational look suits the site. It's purely presentational: the only
14
+ interaction is the normal accordion open and close. Built on `ExpandingPanelClassic` (works and
15
+ animates in every browser), with the same slot names as `AccordionGroup`, so the two swap easily.
16
+
17
+ ## Props
18
+
19
+ | Prop | Type | Default | Notes |
20
+ |---|---|---|---|
21
+ | `name` | `string` | `undefined` | Shared `<details>` name: only one answer open at a time. |
22
+ | `animationDuration` | `number` | `300` | Row-height animation in ms, passed to every panel. |
23
+ | `avatarLabel` | `string` | `""` | Initials for the avatar circle; trimmed, then cut to the first two graphemes (emoji stay whole). |
24
+ | `showAvatar` | `boolean` | `true` | `false` removes the avatar column even when an avatar slot is filled. |
25
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root. |
26
+
27
+ ## Slots
28
+
29
+ | Slot | Purpose |
30
+ |---|---|
31
+ | `item-{n}-summary` | Question text. **Its presence creates panel `n`** (prefixed slot inference, same as `AccordionGroup`). |
32
+ | `item-{n}-content` | Answer bubble content, any markup. Without it the panel has no answer bubble. |
33
+ | `item-{n}-icon` | Optional; replaces the default `+` circle for that panel. |
34
+ | `item-{n}-avatar` | Optional; the avatar for that panel only. |
35
+ | `avatar` | Optional; the avatar for every panel (logo image, `<Icon>`). |
36
+
37
+ Avatar precedence per panel: `item-{n}-avatar`, then `#avatar`, then `avatarLabel`, then none (no
38
+ avatar column; the bubble sits flush left).
39
+
40
+ ## Basic usage
41
+
42
+ ```vue
43
+ <AccordionChat name="faq" avatar-label="AM">
44
+ <template #item-0-summary>Do you offer gift vouchers?</template>
45
+ <template #item-0-content><p>Yes, in any amount, valid for twelve months.</p></template>
46
+
47
+ <template #item-1-summary>Is there parking nearby?</template>
48
+ <template #item-1-content><p>Free on-street parking on Northtown Road.</p></template>
49
+ </AccordionChat>
50
+ ```
51
+
52
+ With a logo avatar:
53
+
54
+ ```vue
55
+ <AccordionChat name="faq">
56
+ <template #avatar><NuxtImg src="/logo-mark.svg" alt="" width="30" height="30" /></template>
57
+ ...
58
+ </AccordionChat>
59
+ ```
60
+
61
+ ## Behaviour
62
+
63
+ - One panel per `item-{n}-summary` slot, in numeric order, gaps allowed; panels follow slots added
64
+ or removed after mount.
65
+ - The summary renders in the question bubble and the content in the answer bubble; a panel without
66
+ a content slot has no answer bubble.
67
+ - The default icon is a `+` in a circle that rotates to `×` when open; `item-{n}-icon` replaces it.
68
+ - Avatar precedence is panel slot, group slot, label, none; `showAvatar=false` removes the column.
69
+ - The avatar label is cut to two graphemes.
70
+ - A shared `name` keeps one answer open at a time.
71
+ - `aria-expanded`, `aria-controls`/`aria-labelledby` and unique ids come from
72
+ `ExpandingPanelClassic` unchanged.
73
+ - The avatar is always `aria-hidden`: it repeats beside every answer, so it's decoration.
74
+ - The question bubble shows a focus ring on keyboard focus.
75
+ - Reduced motion turns off the answer entrance animation and the icon rotation transition.
76
+ - Every colour, size, radius, font and duration is a token, so a fully different look (the Dark
77
+ Theme story) needs no extra CSS.
78
+ - Long unbroken text, long copy, emoji, HTML-like text, RTL (tails and sides flip), an over-wide
79
+ image, a long or emoji avatar label, a panel with no content and many panels all hold their layout
80
+ (`StressTest` story).
81
+
82
+ ## Styling
83
+
84
+ All tokens are `--accordion-chat-*`, defaulting to `--theme-*` colours; see `CONSUMER-STYLING.md`
85
+ (includes a dark serif recipe). The bubble is a wrapper inside the panel's `#content` slot, never the
86
+ panel's `.inner`.
87
+
88
+ ## Stories
89
+
90
+ **Molecules/Expandable/AccordionChat**: Default, Theme Defaults, Dark Theme, Logo Avatar, Icon
91
+ Avatar, No Avatar, Per-Panel Avatar Override, Exclusive Open, Rich Answer Content, Stress Test.
92
+
93
+ ## Notes
94
+
95
+ - Brief: a chat-styled accordion with question/answer bubbles. A typing indicator was considered and
96
+ dropped (2026-10-20): the component has no interactions beyond open and close.
97
+ - The question bubble restyles `ExpandingPanelClassic`'s `summary` with a deeper selector
98
+ (`.accordion-chat .accordion-chat-item.expanding-panel-classic .expanding-panel-classic-summary`)
99
+ so it beats the panel's own rules without editing the panel.
@@ -0,0 +1,98 @@
1
+ ---
2
+ name: AccordionGroup
3
+ description: AccordionGroup renders a group of expanding panels from item-{n}-summary/content/icon slots, optionally exclusive-open via a shared name
4
+ type: reference
5
+ ---
6
+
7
+ # AccordionGroup
8
+
9
+ > **Renamed 2026-10-20:** `AccordianCore` → `AccordionGroup` (also fixes the "Accordian"
10
+ > misspelling); root class `.display-accordian` → `.accordion-group`; panel class `.accordian-item`
11
+ > → `.accordion-group-item`; slots `accordian-{n}-*` → `item-{n}-*`. The `itemCount` prop is
12
+ > gone: the group counts its `item-{n}-summary` slots itself. Folder
13
+ > `02.molecules/expandable/accordion-group/`.
14
+
15
+ ## Overview
16
+
17
+ `AccordionGroup` renders one expanding panel per `item-{n}-summary` slot: `ExpandingPanelClassic`
18
+ by default, or `ExpandingPanel` with `variant="modern"`. Pass `name` to make it exclusive (the
19
+ browser keeps one `<details>` in a shared-name group open). Typical use: FAQs, product details,
20
+ terms sections.
21
+
22
+ ## Props
23
+
24
+ | Prop | Type | Default | Notes |
25
+ |---|---|---|---|
26
+ | `name` | `string` | `undefined` | Shared `name` on every panel's `<details>`; makes the group exclusive-open. Unset, each panel gets its own generated name and opens independently. |
27
+ | `animationDuration` | `number` | `300` | Milliseconds, passed to every panel and to the group's own margin/radius transition. |
28
+ | `variant` | `"classic" \| "modern"` | `"classic"` | `modern` relies on `::details-content`, which WebKit lacks (pitfall #19); keep `classic` unless you've confirmed your audience. |
29
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root. |
30
+
31
+ ## Slots
32
+
33
+ Prefixed slot inference (`component-dynamic-slots.md`, pattern 3): no count prop.
34
+
35
+ | Slot | Purpose |
36
+ |---|---|
37
+ | `item-{n}-summary` | The panel's clickable summary. **Its presence creates panel `n`.** |
38
+ | `item-{n}-content` | The panel's content. |
39
+ | `item-{n}-icon` | Optional. Replaces the panel's default caret; omit it to keep the caret. |
40
+
41
+ `n` is any non-negative integer. Panels render in numeric order, and gaps are fine (`item-0`,
42
+ `item-2`, `item-10`). An `item-{n}-content` or `-icon` slot with no matching summary renders
43
+ nothing.
44
+
45
+ ## Basic usage
46
+
47
+ ```vue
48
+ <AccordionGroup name="faq">
49
+ <template #item-0-summary>How long does a first appointment take?</template>
50
+ <template #item-0-content><p>About an hour.</p></template>
51
+
52
+ <template #item-1-summary>Can I change a booking?</template>
53
+ <template #item-1-content><p>Yes, up to 24 hours before.</p></template>
54
+ </AccordionGroup>
55
+ ```
56
+
57
+ From data:
58
+
59
+ ```vue
60
+ <AccordionGroup name="faq">
61
+ <template v-for="(faq, i) in faqs" :key="`s${i}`" #[`item-${i}-summary`]>{{ faq.question }}</template>
62
+ <template v-for="(faq, i) in faqs" :key="`c${i}`" #[`item-${i}-content`]><p>{{ faq.answer }}</p></template>
63
+ </AccordionGroup>
64
+ ```
65
+
66
+ ## Behaviour
67
+
68
+ - One panel per `item-{n}-summary` slot, in numeric order; adding or removing a summary slot after
69
+ mount adds or removes its panel (slots are read per render, not in a `computed`).
70
+ - With `name`, opening a panel closes the others; without it, panels open independently.
71
+ - Every panel has its own unique ids (`useId()`), so `aria-controls`/`aria-labelledby` pair each
72
+ summary with its own content region even when the panels share a `name`.
73
+ - A panel with no `item-{n}-icon` slot keeps its default caret.
74
+ - Only props the panels declare are passed to them (no stray attributes on panel roots).
75
+ - `variant` is group-wide; every panel renders the same component.
76
+ - Long unbroken summaries and content wrap rather than overflowing or pushing the caret out; RTL,
77
+ emoji and HTML-like text render correctly; an empty group renders an empty wrapper
78
+ (`StressTest` story).
79
+
80
+ ## Styling
81
+
82
+ One token of its own, `--accordion-group-max-width` (default `60rem`); everything else is the
83
+ active panel's tokens. See `CONSUMER-STYLING.md`.
84
+
85
+ ## Stories
86
+
87
+ **Molecules/Expandable/AccordionGroup**: Default, ExclusiveOpen, ModernVariant, CustomIcons,
88
+ SlowAnimation, NoPanels, StressTest.
89
+
90
+ ## Notes
91
+
92
+ - 2026-10-20 migration fixes, beyond the rename: the group passed `icon-size="medium"`, which
93
+ neither panel declares, so it landed on every panel root as an HTML attribute; it always filled
94
+ the panel's `#icon` slot, so the default caret vanished unless every item had an icon slot; it had
95
+ no `styleClassPassthrough` watch; the `600px` width is now a token. The same pass fixed two
96
+ bugs in the panels themselves: ids built from `name` (shared-name panels all had the same
97
+ `id-{name}-trigger`/`-content`, which also affected ResponsiveHeader's overflow groups) and a plain
98
+ `1fr` summary track that let a long word push the caret out.
@@ -141,11 +141,17 @@ Quick reference:
141
141
  `items[0]`; `ArrowUp` should focus `items[items.length - 1]`. Without the guard, the modulo
142
142
  formula gives `items[n-2]` for `ArrowUp` — the second-to-last item instead of the last.
143
143
  - **Right-aligned by default** — the menu's right edge aligns with the trigger's right edge
144
- (`right: anchor(right)`). Flips above the trigger near the bottom of the viewport
145
- (`position-try-fallbacks: flip-block`).
144
+ (`right: anchor(right)`). Flips above the trigger near the bottom of the viewport, and opens
145
+ rightwards from a trigger near the left edge (`position-try-fallbacks: flip-block, flip-inline,
146
+ flip-block flip-inline`; anchor-positioning browsers only, the JS fallback flips block only).
147
+ - **Width and long labels** — the menu sizes to its longest label between
148
+ `--action-menu-popover-min-width` (20rem) and `--action-menu-popover-max-width`
149
+ (`min(32rem, 100vw - 3.2rem)`); labels wrap at the ceiling (`overflow-wrap: anywhere`, label
150
+ track `minmax(0, 1fr)`). `--action-menu-item-label-line-clamp` (default `none`) caps them. Until
151
+ 2026-10-20 labels were `nowrap` and widened the menu without limit. `StressTest` story.
146
152
  - **`anchorName` format** — internally generated as `--action-menu-anchor-{id}` (a valid CSS
147
153
  `<dashed-ident>`). Set via a CSS custom property on the root element so both the trigger's
148
154
  `anchor-name` and the popover's `position-anchor` can reference the same value.
149
- - **Dynamic slots stability** — `item-{n}` slots enforce that only `ActionMenuItem` content
150
- enters the list; arbitrary HTML inside the popover is not supported and will break the ARIA
151
- `menu` / `menuitem` pattern.
155
+ - **One `ActionMenuItem` per slot** — nothing enforces it, but anything else inside an `item-{n}`
156
+ slot breaks the ARIA `menu` / `menuitem` pattern (keyboard navigation only finds
157
+ `[role="menuitem"]`).
@@ -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`
@@ -222,7 +222,7 @@ All tokens are read via `var(--token, default)` — see the full list in the com
222
222
  Defaults are for a **light** header (changed 2026-10-07): panels use `--page-bg`, borders and hover tints are `color-mix()`es of `--theme-text`, the overflow button is transparent with a `--theme-border-focus` hover outline. They used to assume a dark bar (`Canvas`, translucent white borders and tints). A dark header sets the tokens.
223
223
  | `--responsive-nav-decorator-indicator-color` / `--responsive-nav-decorator-hovered-*` | `currentColor` / inherits | The sliding active/hover indicator bar under the main nav. |
224
224
  | `--responsive-nav-decorator-indicator-offset` | `2px` | Distance of the active indicator below the active item's bottom edge; negative raises it. |
225
- | `--responsive-nav-decorator-indicator-height` / `--responsive-nav-decorator-hovered-border-radius` / `--responsive-nav-decorator-transition-duration` | `2px` / `4px` / `200ms` | Active indicator thickness, hover highlight radius, and how long both take to slide. |
225
+ | `--responsive-nav-decorator-indicator-height` / `--responsive-nav-decorator-hovered-border-radius` / `--responsive-nav-decorator-transition-duration` | `2px` / `4px` / `200ms` | Active indicator thickness, hover highlight radius, and how long both take to slide (they jump under `prefers-reduced-motion`; the chevron and overflow button don't animate their movement either). |
226
226
 
227
227
  ---
228
228
 
@@ -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,7 +81,8 @@ 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
@@ -126,7 +127,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
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
@@ -170,6 +171,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
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
172
  ├── 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
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
177
  ├── dashboard-quad-grid.md — DashboardQuadGrid (renamed from LayoutGridA): fixed 4-slot dashboard grid, container-query breakpoints, CSS token API
@@ -313,6 +313,14 @@ prop and try to pass it. Make it unmistakable:
313
313
 
314
314
  `PriceList.stories.ts` and `GoogleReviews.stories.ts` (line-clamp controls) follow this.
315
315
 
316
+ ## Dynamic slot names in template strings
317
+
318
+ A dynamic slot argument can't contain spaces: `` #[`item-${i + 6}-summary`] `` is cut at the first space
319
+ (HTML attribute names end there) and the story fails with "Missing } in template expression". It
320
+ only shows when Storybook renders the story, since the template is a string. Write the expression
321
+ without spaces (`` #[`item-${i}-summary`] ``), or build the names in `setup()` and bind them:
322
+ `#[item.summarySlot]` (AccordionChat's StressTest, 2026-10-20).
323
+
316
324
  ## Docs page
317
325
 
318
326
  `.storybook/preview.ts` sets `tags: ["autodocs"]` globally (2026-10-14), so every story file gets a
@@ -47,7 +47,7 @@ Component skills land in `.claude/skills/srcdev-nuxt-components/components/`:
47
47
  │ ├── pricing-card.md — SaaS pricing plan card
48
48
  │ ├── input-copy.md — copy-to-clipboard input
49
49
  │ ├── expanding-panel.md — single expand/collapse panel
50
- │ ├── accordian-core.md — native accordion (grouped panels)
50
+ │ ├── accordion-group.md — native accordion (grouped panels)
51
51
  │ ├── display-avatar.md — circular avatar component
52
52
  │ ├── card-core.md — generic card container
53
53
  │ ├── auto-grid.md — responsive auto-fit grid
@@ -0,0 +1,42 @@
1
+ {
2
+ "SRCDEV AccordionChat": {
3
+ "description": "AccordionChat: questions as right-hand bubbles, answers as left-hand bubbles with an avatar",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<AccordionChat name=\"${1:faq}\" avatar-label=\"${2:AM}\">",
7
+ " <template #item-0-summary>${3:Question one?}</template>",
8
+ " <template #item-0-content><p>${4:Answer one.}</p></template>",
9
+ "",
10
+ " <template #item-1-summary>${5:Question two?}</template>",
11
+ " <template #item-1-content><p>${6:Answer two.}</p></template>",
12
+ "</AccordionChat>"
13
+ ]
14
+ },
15
+ "SRCDEV AccordionChat Looped": {
16
+ "description": "AccordionChat driven by an array via dynamic item-{n} slot names",
17
+ "scope": "vue,html",
18
+ "body": [
19
+ "<AccordionChat name=\"${1:faq}\" avatar-label=\"${2:AM}\">",
20
+ " <template v-for=\"(item, i) in ${3:items}\" :key=\"`s\\${i}`\" #[`item-\\${i}-summary`]>{{ item.${4:question} }}</template>",
21
+ " <template v-for=\"(item, i) in ${3:items}\" :key=\"`c\\${i}`\" #[`item-\\${i}-content`]><p>{{ item.${5:answer} }}</p></template>",
22
+ "</AccordionChat>"
23
+ ]
24
+ },
25
+ "SRCDEV AccordionChat CSS Override": {
26
+ "description": "AccordionChat colour and type tokens scoped to a page or section class",
27
+ "scope": "css",
28
+ "body": [
29
+ ".${1:faq-section} {",
30
+ " --accordion-chat-question-surface: ${2:#ececec};",
31
+ " --accordion-chat-question-colour: ${3:#1d1d1f};",
32
+ " --accordion-chat-question-surface-open: ${4:#1d1d1f};",
33
+ " --accordion-chat-question-colour-open: ${5:#ffffff};",
34
+ " --accordion-chat-answer-surface: ${6:#ffffff};",
35
+ " --accordion-chat-answer-border: ${7:#e6e4e1};",
36
+ " --accordion-chat-answer-colour: ${8:#2a2a2c};",
37
+ " --accordion-chat-avatar-surface: ${9:#1d1d1f};",
38
+ " --accordion-chat-avatar-colour: ${10:#ffffff};",
39
+ "}"
40
+ ]
41
+ }
42
+ }