srcdev-nuxt-components 9.6.18 → 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.
- package/.claude/rules/consumer/css-relative-colour-calc.md +15 -0
- package/.claude/rules/consumer/vue-dynamic-arg-no-spaces.md +16 -0
- package/.claude/skills/component-compliance-checklist.md +6 -1
- package/.claude/skills/component-dynamic-slots.md +2 -2
- package/.claude/skills/component-naming.md +1 -1
- package/.claude/skills/components/accordion-chat.md +99 -0
- package/.claude/skills/components/accordion-group.md +98 -0
- package/.claude/skills/components/action-menu.md +11 -5
- package/.claude/skills/components/expanding-panel-classic.md +9 -7
- package/.claude/skills/components/expanding-panel.md +9 -8
- package/.claude/skills/components/sparkle-effect.md +79 -0
- package/.claude/skills/index.md +4 -2
- package/.claude/skills/storybook-add-story.md +8 -0
- package/.claude/skills/using-component-skills.md +1 -1
- package/.vscode/srcdev-component-accordion-chat.code-snippets +42 -0
- package/.vscode/srcdev-component-accordion-group.code-snippets +38 -0
- package/.vscode/srcdev-component-action-menu.code-snippets +3 -3
- package/.vscode/srcdev-component-sparkle-effect.code-snippets +33 -0
- package/app/components/01.atoms/animations/sparkle-effect/CONSUMER-STYLING.md +75 -0
- package/app/components/01.atoms/animations/sparkle-effect/SparkleEffect.vue +213 -0
- package/app/components/01.atoms/animations/sparkle-effect/stories/SparkleEffect.stories.ts +303 -0
- package/app/components/01.atoms/animations/sparkle-effect/tests/SparkleEffect.spec.ts +150 -0
- package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +1 -1
- package/app/components/02.molecules/action-menu/ActionMenu.vue +6 -2
- package/app/components/02.molecules/action-menu/ActionMenuItem.vue +6 -3
- package/app/components/02.molecules/action-menu/CONSUMER-STYLING.md +11 -4
- package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +130 -18
- package/app/components/02.molecules/expandable/accordion-chat/AccordionChat.vue +227 -0
- package/app/components/02.molecules/expandable/accordion-chat/CONSUMER-STYLING.md +150 -0
- package/app/components/02.molecules/expandable/accordion-chat/stories/AccordionChat.stories.ts +347 -0
- package/app/components/02.molecules/expandable/accordion-chat/tests/AccordionChat.spec.ts +205 -0
- package/app/components/02.molecules/expandable/accordion-chat/tests/__snapshots__/AccordionChat.spec.ts.snap +40 -0
- package/app/components/02.molecules/expandable/{accordian/AccordianCore.vue → accordion-group/AccordionGroup.vue} +32 -20
- package/app/components/02.molecules/expandable/accordion-group/CONSUMER-STYLING.md +66 -0
- package/app/components/02.molecules/expandable/accordion-group/stories/AccordionGroup.stories.ts +238 -0
- package/app/components/02.molecules/expandable/accordion-group/tests/AccordionGroup.spec.ts +172 -0
- package/app/components/02.molecules/expandable/accordion-group/tests/__snapshots__/AccordionGroup.spec.ts.snap +45 -0
- package/app/components/02.molecules/expandable/expanding-panel/CONSUMER-STYLING.md +1 -0
- package/app/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue +11 -6
- package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +82 -1
- package/app/components/02.molecules/expandable/expanding-panel/tests/ExpandingPanel.spec.ts +24 -30
- package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +8 -8
- package/app/components/02.molecules/expandable/expanding-panel-classic/CONSUMER-STYLING.md +1 -0
- package/app/components/02.molecules/expandable/expanding-panel-classic/ExpandingPanelClassic.vue +10 -5
- package/app/components/02.molecules/expandable/expanding-panel-classic/stories/ExpandingPanelClassic.stories.ts +83 -1
- package/app/components/02.molecules/expandable/expanding-panel-classic/tests/ExpandingPanelClassic.spec.ts +24 -30
- package/app/components/02.molecules/expandable/expanding-panel-classic/tests/__snapshots__/ExpandingPanelClassic.spec.ts.snap +8 -8
- package/app/components/03.organisms/responsive-header/tests/__snapshots__/NavigationItems.spec.ts.snap +2 -2
- package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +2 -2
- package/package.json +1 -1
- package/.claude/skills/components/accordian-core.md +0 -180
- package/.vscode/srcdev-component-accordian-core.code-snippets +0 -74
- package/app/components/02.molecules/expandable/accordian/CONSUMER-STYLING.md +0 -100
- package/app/components/02.molecules/expandable/accordian/stories/AccordianCore.stories.ts +0 -272
- package/app/components/02.molecules/expandable/accordian/tests/AccordianCore.spec.ts +0 -257
- 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()`.
|
|
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 | `
|
|
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 |
|
|
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
|
-
- **
|
|
150
|
-
|
|
151
|
-
`
|
|
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 `
|
|
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()` |
|
|
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` | `
|
|
136
|
-
| `<summary>` | `aria-controls` | `
|
|
135
|
+
| `<summary>` | `id` | `{useId()}-trigger` |
|
|
136
|
+
| `<summary>` | `aria-controls` | `{useId()}-content` |
|
|
137
137
|
| `<summary>` | `aria-expanded` | `true` / `false` |
|
|
138
|
-
| content div | `id` | `
|
|
139
|
-
| content div | `aria-labelledby` | `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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()` |
|
|
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` | `
|
|
136
|
-
| `<summary>` | `aria-controls` | `
|
|
135
|
+
| `<summary>` | `id` | `{useId()}-trigger` |
|
|
136
|
+
| `<summary>` | `aria-controls` | `{useId()}-content` |
|
|
137
137
|
| `<summary>` | `aria-expanded` | `true` / `false` |
|
|
138
|
-
| content div | `id` | `
|
|
139
|
-
| content div | `aria-labelledby` | `
|
|
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 `
|
|
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`
|
|
@@ -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.
|
package/.claude/skills/index.md
CHANGED
|
@@ -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
|
-
├──
|
|
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
|
-
│ ├──
|
|
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
|
+
}
|