srcdev-nuxt-components 9.6.18 → 9.6.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/consumer/css-relative-colour-calc.md +15 -0
- package/.claude/rules/consumer/ids-from-useid.md +3 -2
- package/.claude/rules/consumer/vue-dynamic-arg-no-spaces.md +16 -0
- package/.claude/skills/component-compliance-checklist.md +11 -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/auto-grid.md +3 -1
- package/.claude/skills/components/capture-qr-code.md +9 -5
- package/.claude/skills/components/carousel-flip.md +41 -30
- package/.claude/skills/components/column-flow-grid.md +2 -0
- package/.claude/skills/components/dashboard-quad-grid.md +7 -2
- package/.claude/skills/components/dashboard-stats-grid.md +4 -0
- package/.claude/skills/components/decode-qr-code.md +7 -2
- package/.claude/skills/components/deep-expanding-menu-classic.md +31 -30
- package/.claude/skills/components/deep-expanding-menu.md +22 -21
- package/.claude/skills/components/expanding-panel-classic.md +9 -7
- package/.claude/skills/components/expanding-panel.md +9 -8
- package/.claude/skills/components/grid-stack.md +2 -0
- package/.claude/skills/components/layout-grid-by-cols.md +2 -0
- package/.claude/skills/components/masonry-grid.md +9 -1
- package/.claude/skills/components/page-row.md +3 -0
- package/.claude/skills/components/samaritan-prompt.md +24 -8
- package/.claude/skills/components/site-header.md +16 -2
- package/.claude/skills/components/sparkle-effect.md +79 -0
- package/.claude/skills/index.md +16 -14
- package/.claude/skills/storybook-add-story.md +33 -0
- package/.claude/skills/testing-add-unit-test.md +26 -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-capture-decode-qr-code.code-snippets +11 -2
- package/.vscode/srcdev-component-carousel-flip.code-snippets +22 -4
- package/.vscode/srcdev-component-deep-expanding-menu-classic.code-snippets +32 -2
- package/.vscode/srcdev-component-deep-expanding-menu.code-snippets +31 -1
- package/.vscode/srcdev-component-samaritan-prompt.code-snippets +5 -4
- package/.vscode/srcdev-component-site-header.code-snippets +13 -1
- 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/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +12 -2
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue +19 -17
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts +42 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/tests/DashboardQuadGrid.spec.ts +31 -44
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +13 -1
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +40 -31
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/stories/DashboardStatsGrid.stories.ts +47 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/tests/DashboardStatsGrid.spec.ts +51 -53
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/CONSUMER-STYLING.md +3 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/LayoutGridByCols.vue +4 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/stories/LayoutGridByCols.stories.ts +47 -2
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/LayoutGridByCols.spec.ts +36 -23
- package/app/components/01.atoms/grids/auto-grid/AutoGrid.vue +7 -3
- package/app/components/01.atoms/grids/auto-grid/CONSUMER-STYLING.md +1 -1
- package/app/components/01.atoms/grids/auto-grid/stories/AutoGrid.stories.ts +46 -1
- package/app/components/01.atoms/grids/auto-grid/tests/AutoGrid.spec.ts +40 -27
- package/app/components/01.atoms/grids/auto-grid/tests/__snapshots__/AutoGrid.spec.ts.snap +1 -1
- package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +9 -0
- package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +3 -2
- package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +46 -0
- package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +24 -11
- package/app/components/01.atoms/grids/grid-stack/CONSUMER-STYLING.md +3 -0
- package/app/components/01.atoms/grids/grid-stack/GridStack.vue +2 -0
- package/app/components/01.atoms/grids/grid-stack/stories/GridStack.stories.ts +41 -0
- package/app/components/01.atoms/grids/grid-stack/tests/GridStack.spec.ts +26 -13
- package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +15 -0
- package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +51 -31
- package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +48 -0
- package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +84 -29
- package/app/components/01.atoms/page-row/CONSUMER-STYLING.md +6 -1
- package/app/components/01.atoms/page-row/PageRow.vue +16 -15
- package/app/components/01.atoms/page-row/stories/PageRow.stories.ts +23 -0
- package/app/components/01.atoms/page-row/tests/PageRow.spec.ts +6 -5
- 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/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +63 -44
- package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +74 -37
- package/app/components/02.molecules/navigation/deep-expanding-menu/stories/DeepExpandingMenu.stories.ts +139 -19
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +90 -27
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +8 -8
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +58 -26
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +113 -57
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/stories/DeepExpandingMenuClassic.stories.ts +139 -19
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/DeepExpandingMenuClassic.spec.ts +140 -22
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/__snapshots__/DeepExpandingMenuClassic.spec.ts.snap +9 -9
- package/app/components/02.molecules/qr-code/CONSUMER-STYLING.md +22 -7
- package/app/components/02.molecules/qr-code/CaptureQrCode.vue +63 -71
- package/app/components/02.molecules/qr-code/DecodeQrCode.vue +47 -10
- package/app/components/02.molecules/qr-code/stories/CaptureQrCode.stories.ts +114 -26
- package/app/components/02.molecules/qr-code/stories/DecodeQrCode.stories.ts +115 -24
- package/app/components/02.molecules/qr-code/tests/CaptureQrCode.spec.ts +124 -51
- package/app/components/02.molecules/qr-code/tests/DecodeQrCode.spec.ts +74 -25
- package/app/components/02.molecules/samaritan-prompt/CONSUMER-STYLING.md +22 -10
- package/app/components/02.molecules/samaritan-prompt/SamaritanPrompt.vue +30 -12
- package/app/components/02.molecules/samaritan-prompt/SamaritanPromptMixed.vue +34 -10
- package/app/components/02.molecules/samaritan-prompt/stories/SamaritanPrompt.stories.ts +55 -4
- package/app/components/02.molecules/samaritan-prompt/stories/SamaritanPromptMixed.stories.ts +67 -7
- package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPrompt.spec.ts +113 -29
- package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPromptMixed.spec.ts +92 -45
- package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +82 -29
- package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +176 -113
- package/app/components/03.organisms/image-galleries/carousel-flip/stories/CarouselFlip.stories.ts +153 -23
- package/app/components/03.organisms/image-galleries/carousel-flip/tests/CarouselFlip.spec.ts +119 -63
- 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/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +8 -8
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +4 -4
- package/app/components/03.organisms/site-header/CONSUMER-STYLING.md +41 -23
- package/app/components/03.organisms/site-header/SiteHeader.vue +16 -21
- package/app/components/03.organisms/site-header/stories/SiteHeader.stories.ts +138 -0
- package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +53 -26
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +2 -2
- package/app/types/components/qr-code.d.ts +12 -0
- package/app/utils/graphemes.ts +8 -0
- 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/01.atoms/animations/entry/.nuxt/app.config.mjs +0 -21
- package/app/components/01.atoms/animations/entry/.nuxt/components.d.ts +0 -66
- package/app/components/01.atoms/animations/entry/.nuxt/imports.d.ts +0 -30
- package/app/components/01.atoms/animations/entry/.nuxt/manifest/meta/test.json +0 -1
- package/app/components/01.atoms/animations/entry/.nuxt/types/app.config.d.ts +0 -35
- package/app/components/01.atoms/animations/entry/.nuxt/types/build.d.ts +0 -23
- package/app/components/01.atoms/animations/entry/.nuxt/types/builder-env.d.ts +0 -1
- package/app/components/01.atoms/animations/entry/.nuxt/types/components.d.ts +0 -71
- package/app/components/01.atoms/animations/entry/.nuxt/types/imports.d.ts +0 -386
- package/app/components/01.atoms/animations/entry/.nuxt/types/middleware.d.ts +0 -11
- package/app/components/01.atoms/animations/entry/.nuxt/types/modules.d.ts +0 -79
- package/app/components/01.atoms/animations/entry/.nuxt/types/nitro-layouts.d.ts +0 -17
- package/app/components/01.atoms/animations/entry/.nuxt/types/nitro-nuxt.d.ts +0 -64
- package/app/components/01.atoms/animations/entry/.nuxt/types/plugins.d.ts +0 -29
- package/app/components/01.atoms/animations/entry/.nuxt/types/runtime-config.d.ts +0 -32
- package/app/components/01.atoms/animations/entry/.nuxt/types/shared-imports.d.ts +0 -10
- package/app/components/01.atoms/animations/entry/.nuxt/types/vue-shim.d.ts +0 -0
- package/app/components/01.atoms/animations/entry/.nuxtrc +0 -1
- package/app/components/01.atoms/animations/entry/node_modules/.cache/nuxt/chrome-workspace.json +0 -1
- package/app/components/01.atoms/animations/entry/node_modules/.cache/vite/client/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- 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
- package/app/components/03.organisms/responsive-header/tests/.nuxt/app.config.mjs +0 -21
- package/app/components/03.organisms/responsive-header/tests/.nuxt/components.d.ts +0 -66
- package/app/components/03.organisms/responsive-header/tests/.nuxt/imports.d.ts +0 -30
- package/app/components/03.organisms/responsive-header/tests/.nuxt/manifest/meta/test.json +0 -1
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/app.config.d.ts +0 -35
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/build.d.ts +0 -23
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/builder-env.d.ts +0 -1
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/components.d.ts +0 -71
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/imports.d.ts +0 -386
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/middleware.d.ts +0 -11
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/modules.d.ts +0 -79
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-layouts.d.ts +0 -17
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-nuxt.d.ts +0 -64
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/plugins.d.ts +0 -29
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/runtime-config.d.ts +0 -32
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/shared-imports.d.ts +0 -10
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/vue-shim.d.ts +0 -0
- package/app/components/03.organisms/responsive-header/tests/node_modules/.cache/nuxt/chrome-workspace.json +0 -1
|
@@ -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).
|
|
@@ -7,11 +7,12 @@ paths:
|
|
|
7
7
|
|
|
8
8
|
Build `id`/`for` pairs from `useId()` (plus a loop index for repeated items), never from props or
|
|
9
9
|
data values like `` `${name}-${item.value}` ``. A radio group's default `name` comes from
|
|
10
|
-
`useId()` too, never a fixed string. Call `useId()` once at setup and store the result; never call
|
|
10
|
+
`useId()` too, never a fixed string, and so does a `<details name>` exclusive group. Call `useId()` once at setup and store the result; never call
|
|
11
11
|
it inside a `computed`, which can re-run it.
|
|
12
12
|
|
|
13
13
|
Why: two instances sharing a `name` and values produce duplicate ids, and a value with spaces makes
|
|
14
14
|
an invalid one, so labels toggle the wrong control (`MultipleCheckboxes`, 2026-10-16). A fixed default radio `name` makes every instance on a page
|
|
15
|
-
one group, so picking in one clears the others (`SegmentedControl`, 2026-10-17)
|
|
15
|
+
one group, so picking in one clears the others (`SegmentedControl`, 2026-10-17); a fixed `<details>`
|
|
16
|
+
`name` did the same across every `DeepExpandingMenuClassic` on a page (fixed 2026-10-21). Test
|
|
16
17
|
uniqueness with both instances inside one host component: separate `mountSuspended` calls each
|
|
17
18
|
restart the `useId()` counter. Detail: skill `testing-add-unit-test.md` (srcdev-nuxt-components).
|
|
@@ -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
|
|
@@ -138,6 +140,11 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
|
|
|
138
140
|
`computed()`: Vue renders nothing for a fractional or negative range and only warns in dev
|
|
139
141
|
(`StepperList`, 2026-10-15)
|
|
140
142
|
- broken image or link URLs
|
|
143
|
+
- an empty list where script loops over the data: a `while` loop that walks or cycles the
|
|
144
|
+
items must still terminate, and an async loop must await something on every pass, or the
|
|
145
|
+
tab freezes. `SamaritanPromptMixed` (empty `messageConfigs` with `introDelay: 0`) and
|
|
146
|
+
`CarouselFlip` (empty `carouselDataIds`) both did, fixed 2026-10-21. Grep the script for
|
|
147
|
+
`while (` and check each one with zero and one item
|
|
141
148
|
- data a third-party child can't handle: a library that throws while rendering (an encoder, a
|
|
142
149
|
parser) breaks the whole page render. Catch it with `onErrorCaptured` and render a fallback
|
|
143
150
|
(`DisplayQrCode` threw "Data too long" on an over-capacity value until 2026-10-19). Check story
|
|
@@ -309,6 +316,9 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
|
|
|
309
316
|
- Element ids come from `useId()`, never from data values (rule `consumer/ids-from-useid.md`).
|
|
310
317
|
Call `useId()` once at setup, never inside a `computed` (`DisplayTooltip`, `DisplayTooltipRich`,
|
|
311
318
|
2026-10-18).
|
|
319
|
+
- A wrapper forwards a slot to its child only when the consumer filled it
|
|
320
|
+
(`<template v-if="slots[name]" #icon>`). An always-present `<template #icon>` replaces the child's
|
|
321
|
+
fallback with nothing: `AccordionGroup` hid the panels' default caret this way until 2026-10-20.
|
|
312
322
|
- A wrapper passes its child only props the child declares. Anything else falls through as an
|
|
313
323
|
HTML attribute on the child's root: `InputTextField` put `label="…"` and
|
|
314
324
|
`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"]`).
|
|
@@ -37,11 +37,13 @@ When filling from a data array, use a dynamic slot name in a `v-for`:
|
|
|
37
37
|
| Prop (template form) | Type | Default | Notes |
|
|
38
38
|
| -------------------------- | ------------------------------------------- | ------- | --------------------------------------------- |
|
|
39
39
|
| `tag` | `"div" \| "section" \| "article" \| "main"` | `"div"` | Use a semantic tag for page landmark regions. |
|
|
40
|
-
| `is-responsive` | `boolean` | `false` | Adds `.is-responsive
|
|
40
|
+
| `is-responsive` | `boolean` | `false` | Adds `data-responsive` (was the class `.is-responsive` until 2026-10-21): column minimum steps small → default → large via `@container` queries at 768px / 1024px. Needs a `container-type: inline-size` ancestor. |
|
|
41
41
|
| `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes on the root element. |
|
|
42
42
|
|
|
43
43
|
---
|
|
44
44
|
|
|
45
|
+
Each slotted item gets `min-inline-size: 0`, so an unbroken string can't widen its column. The `StressTest` story feeds every cell oversized content (an unbroken string and URL, long German, a 1600px image, emoji, right-to-left, an empty cell).
|
|
46
|
+
|
|
45
47
|
## CSS custom properties
|
|
46
48
|
|
|
47
49
|
Set these on the grid itself (`style`, passthrough class) or on any ancestor you own; they inherit
|
|
@@ -25,13 +25,14 @@ Camera access requires a secure context (HTTPS or localhost) and the user granti
|
|
|
25
25
|
|------|------|---------|-------------|
|
|
26
26
|
| `camera-stopped-label` | `string` | `"Camera stopped"` | Message while the camera is stopped (tab hidden, route leave) |
|
|
27
27
|
| `reset-camera-label` | `string` | `"Reset camera"` | Reset button text in the error state |
|
|
28
|
+
| `error-messages` | `CaptureQrCodeErrorMessages` | `{}` | Error copy keyed by error name (`NotAllowedError`, `NotFoundError`, `NotSupportedError`, `NotReadableError`, `OverconstrainedError`, `StreamApiNotSupportedError`, `InsecureContextError`, `StreamLoadTimeoutError`) plus `default`. Each key falls back to a built-in plain-English message; any other error name uses `default`. Type from `~/types/components`. |
|
|
28
29
|
| `style-class-passthrough` | `string \| string[]` | `[]` | Extra classes on the root element |
|
|
29
30
|
|
|
30
31
|
## Slots
|
|
31
32
|
|
|
32
33
|
| Slot | Scope | Description |
|
|
33
34
|
|------|-------|-------------|
|
|
34
|
-
| `#error` | `{ error, message, reset }` | Replaces the default error content (message + reset button). `error` is the `Error.name` (e.g. `NotAllowedError`), so you can show your own translated copy per case; call `reset()` to retry. |
|
|
35
|
+
| `#error` | `{ error, message, reset }` | Replaces the default error content (message + reset button). `message` is the resolved `errorMessages` text. `error` is the `Error.name` (e.g. `NotAllowedError`), so you can show your own translated copy per case; call `reset()` to retry. |
|
|
35
36
|
|
|
36
37
|
## CSS classes
|
|
37
38
|
|
|
@@ -40,15 +41,17 @@ Camera access requires a secure context (HTTPS or localhost) and the user granti
|
|
|
40
41
|
| `.capture-qr-code` | root | always present |
|
|
41
42
|
| `.capture-qr-code-stopped` | inner | shown when camera is off but no error |
|
|
42
43
|
| `.capture-qr-code-error` | inner | shown when an error has occurred |
|
|
43
|
-
| `.capture-qr-code-results` | inner | polite live region, always rendered; its `<ul>` appears once a code is decoded |
|
|
44
|
+
| `.capture-qr-code-results` | inner | polite live region, always rendered; its `.capture-qr-code-results-list` `<ul>` appears once a code is decoded |
|
|
45
|
+
| `.capture-qr-code-result` | `<li>` | one decoded value; wraps anywhere |
|
|
46
|
+
| `.capture-qr-code-error-message` | `<p>` | default error text |
|
|
44
47
|
|
|
45
48
|
## Behaviour
|
|
46
49
|
|
|
47
50
|
- Camera starts automatically on mount (`state.cameraOn = true`)
|
|
48
|
-
- When the page becomes hidden (tab switch, minimize), the camera stops and media streams are released
|
|
51
|
+
- When the page becomes hidden (tab switch, minimize), the camera stops and media streams are released; when it becomes visible again the camera restarts, unless an error is showing (until 2026-10-21 it stayed on "Camera stopped" with no way back)
|
|
49
52
|
- On route leave (`onBeforeRouteLeave`), the camera stops and streams are released
|
|
50
53
|
- On `<KeepAlive>` deactivate/activate, camera is stopped/restarted accordingly
|
|
51
|
-
- If the user denies camera permission or another error occurs, the `.capture-qr-code-error` state is shown with the
|
|
54
|
+
- If the user denies camera permission or another error occurs, the `.capture-qr-code-error` state is shown with a plain-language message (from `errorMessages`, not the library's raw `[Name]: message` text, which it showed until 2026-10-21) and a **Reset camera** button
|
|
52
55
|
- Detected QR codes replace the previous results — each scan is a fresh result set
|
|
53
56
|
- Results are displayed as a `<ul>` inside `.capture-qr-code-results`
|
|
54
57
|
|
|
@@ -91,4 +94,5 @@ The root element is a square (`aspect-ratio: 1 / 1`) by default. Override in a c
|
|
|
91
94
|
## Notes
|
|
92
95
|
|
|
93
96
|
- The component does not emit the detected values — results are rendered inline. To act on scan results in a parent, extend the component or use `nuxt-qrcode`'s `QrcodeStream` directly with `@detect`.
|
|
94
|
-
- Media streams are stopped
|
|
97
|
+
- Media streams are stopped by sweeping `<video>` elements inside the component's own root. Until 2026-10-21 it also swept every `<video>` in the document, which stopped any other camera or call stream on the page.
|
|
98
|
+
- 2026-10-21 migration: `errorMessages` prop, visible-again restart, own-root stream cleanup, wrapping results, `StressTest` story (simulation buttons drive the internal handlers, since Storybook can't fake a camera).
|
|
@@ -14,8 +14,7 @@ dynamic slots keyed by `carouselDataIds`. It supports swipe, arrow-key, marker-c
|
|
|
14
14
|
prev/next-button navigation, and four `buttonLayout` variants for positioning the prev/next
|
|
15
15
|
buttons relative to the marker controls.
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
default rendered output.
|
|
17
|
+
No consumer app uses it as of 2026-10-21 (an earlier note said one did; a scan found none).
|
|
19
18
|
|
|
20
19
|
## Props
|
|
21
20
|
|
|
@@ -27,12 +26,12 @@ default rendered output.
|
|
|
27
26
|
| `allowCarouselOverflow` | `boolean` | `false` | Allows items to overflow the item track horizontally instead of clipping. |
|
|
28
27
|
| `useFlipAnimation` | `boolean` | `false` | Changes the initial item offset used in the FLIP reorder calculation. |
|
|
29
28
|
| `useSpringEffect` | `boolean` | `false` | Uses `var(--spring-easing)` as the transition timing function instead of `ease`. |
|
|
30
|
-
| `buttonLayout` | `"sides" \| "controls-flanking" \| "controls-grouped-right" \| "overlay"` | `"sides"` | Where the prev/next buttons sit relative to the carousel/controls. |
|
|
29
|
+
| `buttonLayout` | `"sides" \| "controls-flanking" \| "controls-grouped-right" \| "overlay" \| "track-edges"` | `"sides"` | Where the prev/next buttons sit relative to the carousel/controls. |
|
|
31
30
|
| `showControls` | `boolean` | `true` | Shows/hides the marker (dot) controls bar. Prev/next buttons remain regardless. |
|
|
32
31
|
| `ariaLabel` | `string` | `"Image carousel"` | `aria-label` on the root region — override for localisation. |
|
|
33
32
|
| `itemsAriaLabel` | `string` | `"Carousel items"` | `aria-label` on the item group — override for localisation. |
|
|
34
|
-
| `previousLabel` | `string` | `"Go to previous item"` | Prev button
|
|
35
|
-
| `nextLabel` | `string` | `"Go to next item"` | Next button
|
|
33
|
+
| `previousLabel` | `string` | `"Go to previous item"` | Prev button accessible name (its visually hidden text) — override for localisation. |
|
|
34
|
+
| `nextLabel` | `string` | `"Go to next item"` | Next button accessible name (its visually hidden text) — override for localisation. |
|
|
36
35
|
| `jumpToItemLabel` | `string` | `"Jump to item"` | Marker button `aria-label` prefix — the 1-based index is appended (`"Jump to item 3"`). |
|
|
37
36
|
| `itemPositionAnnouncement` | `(current: number, total: number) => string` | `` (current, total) => `Item ${current} of ${total}` `` | Builds the screen-reader live-region text on navigation — override for localisation or a different phrasing. |
|
|
38
37
|
| `prevIcon` | `string` | `"ic:outline-keyboard-arrow-left"` | Iconify name for the previous button icon. Ignored if the `prev-icon` slot is used. |
|
|
@@ -64,10 +63,12 @@ One dynamically-named slot per `carouselDataIds` entry, named after that id:
|
|
|
64
63
|
## Navigation
|
|
65
64
|
|
|
66
65
|
- **Swipe** (touch): left swipe → next, right swipe → previous.
|
|
67
|
-
- **Keyboard**: `ArrowLeft`/`ArrowRight`
|
|
68
|
-
`tabindex="0"`
|
|
66
|
+
- **Keyboard**: `ArrowLeft`/`ArrowRight` while the item track (`tabindex="0"`) or a marker has
|
|
67
|
+
focus. The controls bar itself is no longer a tab stop (it was `tabindex="0"` with no name until
|
|
68
|
+
2026-10-21).
|
|
69
69
|
- **Marker click**: jumps directly to that item.
|
|
70
|
-
- **Prev/next buttons**:
|
|
70
|
+
- **Prev/next buttons**: `InputButton variant="inline"` with an `iconOnly` slot; rendered regardless
|
|
71
|
+
of `showControls`, but only when there are at least two items (the markers too).
|
|
71
72
|
- All navigation wraps around at the ends.
|
|
72
73
|
|
|
73
74
|
## `buttonLayout` variants
|
|
@@ -77,31 +78,35 @@ One dynamically-named slot per `carouselDataIds` entry, named after that id:
|
|
|
77
78
|
- `"controls-grouped-right"`: markers centred, prev/next buttons grouped to the right of them.
|
|
78
79
|
- `"overlay"`: markers float over the bottom edge of the item track instead of occupying their
|
|
79
80
|
own grid row.
|
|
81
|
+
- `"track-edges"` (added 2026-10-21): prev/next overlay the ends of the visible track, inset by
|
|
82
|
+
`--carousel-flip-button-edge-inset` (default: the edge-preview width plus `0.8rem`), so they sit
|
|
83
|
+
on the active frame. Use it with `allowCarouselOverflow`, where `"sides"` leaves the buttons at
|
|
84
|
+
the component box's edges, over the neighbouring items that peek out past it.
|
|
85
|
+
|
|
86
|
+
Placement tokens (added 2026-10-21): `--carousel-flip-button-align` (`start`/`center`/`end`,
|
|
87
|
+
vertical) and `--carousel-flip-button-inline-offset` (`sides` and `controls-flanking`: pushes each
|
|
88
|
+
button outward, negative pulls it inward; mirrors in RTL via `:dir(rtl)`). For the viewport edges
|
|
89
|
+
of a centred carousel of known width: `calc((100vw - <that width>) / 2 - 1.6rem)`. A percentage
|
|
90
|
+
won't work, since `translate` percentages resolve against the button.
|
|
80
91
|
|
|
81
92
|
## CSS token API
|
|
82
93
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
| `--carousel-flip-button-icon-size` | `2.4rem` | Prev/next button icon glyph size |
|
|
100
|
-
| `--carousel-flip-buttons-gap` | `1rem` | Gap between prev/next buttons in the `controls-grouped-right` layout |
|
|
101
|
-
| `--carousel-flip-overlay-controls-offset` | `1rem` | Distance of the controls bar from the bottom edge in the `overlay` layout |
|
|
102
|
-
| `--carousel-flip-focus-outline-width` | `0.2rem` | Marker button focus-visible outline width |
|
|
103
|
-
| `--carousel-flip-focus-outline-colour` | `var(--theme-ring)` | Marker button focus-visible outline colour |
|
|
104
|
-
| `--carousel-flip-focus-outline-offset` | `0.2rem` | Marker button focus-visible outline offset |
|
|
94
|
+
Full table, state hooks and class names: [CONSUMER-STYLING.md](../../app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md).
|
|
95
|
+
Tokens are `--carousel-flip-*`: gaps and max widths, edge preview, marker gap and active colour,
|
|
96
|
+
prev/next button colours, border, radius, padding and icon size/colour, and the focus outline.
|
|
97
|
+
Colours default from `--theme-surface`, `--theme-surface-subtle`, `--theme-border`,
|
|
98
|
+
`--theme-text` and `--theme-ring`.
|
|
99
|
+
|
|
100
|
+
## Data handling
|
|
101
|
+
|
|
102
|
+
- `itemCount` follows `carouselDataIds` after mount (it was frozen at the mount-time length until
|
|
103
|
+
2026-10-21, so adding items left the markers and announcement stale). When the list changes, the
|
|
104
|
+
layout is rebuilt and the active item resets to the first if it was removed.
|
|
105
|
+
- An empty `carouselDataIds` renders an empty track. Until 2026-10-21 it sent `reorderItems` into
|
|
106
|
+
an infinite loop on mount and froze the tab.
|
|
107
|
+
- Items are keyed on index + id, so a repeated id still renders (both items show that slot).
|
|
108
|
+
- Item content wraps (`min-inline-size: 0`, `overflow-wrap: anywhere`), the centre grid column is
|
|
109
|
+
`minmax(0, 1fr)`, and the markers wrap onto more rows. The `StressTest` story covers all of these.
|
|
105
110
|
|
|
106
111
|
## Accessibility
|
|
107
112
|
|
|
@@ -114,6 +119,12 @@ See [CONSUMER-STYLING.md](../../app/components/03.organisms/image-galleries/caro
|
|
|
114
119
|
|
|
115
120
|
## Notes
|
|
116
121
|
|
|
122
|
+
- 2026-10-21 migration: prefixed every structural class and moved state classes to `data-*`
|
|
123
|
+
attributes (see CONSUMER-STYLING.md), prev/next became `InputButton`, theme-token colour defaults,
|
|
124
|
+
`transitionSpeed` and the class passthrough became reactive, `aria-current` is omitted (not
|
|
125
|
+
`"false"`) on inactive items, and the spec lost a dead `#imports` mock of
|
|
126
|
+
`useStyleClassPassthrough` that never applied.
|
|
127
|
+
|
|
117
128
|
- Migrated from `app/components/carousels/CarouselFlip.vue` (unplaced, legacy location) to
|
|
118
129
|
`03.organisms/image-galleries/carousel-flip/` on 2026-09-21, alongside `SliderGallery`.
|
|
119
130
|
- Sibling components `CarouselBasic` and `CarouselInfinite` were deleted in the same migration —
|
|
@@ -82,6 +82,8 @@ properties, since they need JS-computed values rather than being purely CSS-driv
|
|
|
82
82
|
|
|
83
83
|
## Notes
|
|
84
84
|
|
|
85
|
+
- The `StressTest` story feeds every cell oversized content (an unbroken string and URL, long German, a 1600px image, emoji, right-to-left, an empty cell).
|
|
86
|
+
|
|
85
87
|
- **Renamed from `MasonryGrid`** (2026-09-13) — see **Which one do I want?** above for why. If you
|
|
86
88
|
have consumer code importing this as `MasonryGrid`, it needs updating to `ColumnFlowGrid`; the
|
|
87
89
|
props and behaviour are otherwise unchanged. CSS classes/tokens were renamed to match
|
|
@@ -50,6 +50,11 @@ column count instead, use `LayoutGridByCols` (fixed count) or `AutoGrid` (auto-f
|
|
|
50
50
|
|
|
51
51
|
---
|
|
52
52
|
|
|
53
|
+
## Cell classes
|
|
54
|
+
|
|
55
|
+
Each slot renders in a `.dashboard-quad-grid-cell` with `.dashboard-quad-grid-slot-1` to `-slot-4`
|
|
56
|
+
(`.slot1` to `.slot4` until 2026-10-21). The `StressTest` story feeds every cell oversized content (an unbroken string and URL, long German, a 1600px image, emoji, right-to-left, an empty cell).
|
|
57
|
+
|
|
53
58
|
## CSS token API
|
|
54
59
|
|
|
55
60
|
See [CONSUMER-STYLING.md](../../app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md).
|
|
@@ -59,10 +64,10 @@ See [CONSUMER-STYLING.md](../../app/components/01.atoms/content-wrappers/layout-
|
|
|
59
64
|
| `--dashboard-quad-grid-gap` | `2rem` | Gap between the four slot panels |
|
|
60
65
|
| `--dashboard-quad-grid-padding` | `2rem` | Padding inside each slot panel |
|
|
61
66
|
| `--dashboard-quad-grid-outline-width` | `0.1rem` | Slot panel outline width |
|
|
62
|
-
| `--dashboard-quad-grid-outline-colour` | `
|
|
67
|
+
| `--dashboard-quad-grid-outline-colour` | `var(--theme-border)` | Slot panel outline colour |
|
|
63
68
|
| `--dashboard-quad-grid-outline-offset` | `0` | Slot panel outline offset |
|
|
64
69
|
| `--dashboard-quad-grid-border-width` | `0` | Slot panel border width (off by default — the panel edge is drawn by the outline) |
|
|
65
|
-
| `--dashboard-quad-grid-border-colour` | `
|
|
70
|
+
| `--dashboard-quad-grid-border-colour` | `var(--theme-border)` | Slot panel border colour, only visible once `--dashboard-quad-grid-border-width` is set |
|
|
66
71
|
| `--dashboard-quad-grid-border-radius` | `0.5rem` | Slot panel corner radius |
|
|
67
72
|
| `--dashboard-quad-grid-rule-width` | `0.1rem` | Width of the grid gap rule line (CSS Gap Decorations, limited browser support) |
|
|
68
73
|
| `--dashboard-quad-grid-rule-colour` | `transparent` | Colour of the grid gap rule line, invisible by default |
|
|
@@ -32,6 +32,10 @@ top-row slots and overall shape are fixed.
|
|
|
32
32
|
|
|
33
33
|
---
|
|
34
34
|
|
|
35
|
+
The two counts are floored, and a negative or non-finite count renders no panels (until 2026-10-21 a
|
|
36
|
+
fractional count rendered none at all, since Vue skips a fractional `v-for` range). Element classes
|
|
37
|
+
are prefixed `.dashboard-stats-grid-*` (see CONSUMER-STYLING.md). The `StressTest` story feeds every cell oversized content (an unbroken string and URL, long German, a 1600px image, emoji, right-to-left, an empty cell).
|
|
38
|
+
|
|
35
39
|
## Props reference
|
|
36
40
|
|
|
37
41
|
| Prop (template form) | Type | Default | Notes |
|
|
@@ -25,6 +25,8 @@ No camera permission is required — this component works entirely with uploaded
|
|
|
25
25
|
|------|------|---------|-------------|
|
|
26
26
|
| `upload-label` | `string` | `"Upload a QR code image"` | Visible label wrapping the file input (its accessible name) |
|
|
27
27
|
| `drop-label` | `string` | `"Or drop a QR code image here"` | Instruction text inside the drop zone |
|
|
28
|
+
| `no-result-label` | `string` | `"No QR code was found in that image."` | Shown when an image decodes but contains no QR code. Empty string shows nothing |
|
|
29
|
+
| `read-error-label` | `string` | `"That file couldn't be read as an image."` | Shown when the drop zone reports an error (a dropped file or URL that isn't a readable image). Empty string shows nothing |
|
|
28
30
|
| `style-class-passthrough` | `string \| string[]` | `[]` | Extra classes on the root element |
|
|
29
31
|
|
|
30
32
|
## CSS classes
|
|
@@ -35,7 +37,8 @@ No camera permission is required — this component works entirely with uploaded
|
|
|
35
37
|
| `.decode-qr-code-capture` | file input | the `QrcodeCapture` file-picker element |
|
|
36
38
|
| `.decode-qr-code-dropzone` | drop zone | the `QrcodeDropZone` drag-and-drop area |
|
|
37
39
|
| `.decode-qr-code-upload` | label | wraps `.decode-qr-code-upload-label` and the file input |
|
|
38
|
-
| `.decode-qr-code-dropzone
|
|
40
|
+
| `.decode-qr-code-dropzone[data-dropping]` | drop zone | an image is being dragged over it (was `.is-dropping` until 2026-10-21) |
|
|
41
|
+
| `.decode-qr-code-status` | `<p>` | the no-result or (with `[data-error]`) read-error message |
|
|
39
42
|
| `.decode-qr-code-results` | inner | polite live region, always rendered; its `<ul>` appears once a code is decoded |
|
|
40
43
|
|
|
41
44
|
## Behaviour
|
|
@@ -43,7 +46,8 @@ No camera permission is required — this component works entirely with uploaded
|
|
|
43
46
|
- Two input methods are rendered side by side: a file picker (`.decode-qr-code-capture`) and a drag-and-drop zone (`.decode-qr-code-dropzone`)
|
|
44
47
|
- Both share the same `onDetect` handler — results are displayed in the same `.decode-qr-code-results` list regardless of input method
|
|
45
48
|
- Detected QR codes replace the previous results — each decode is a fresh result set
|
|
46
|
-
- If the decoded array is empty, the results `<ul>` isn't rendered
|
|
49
|
+
- If the decoded array is empty, the results `<ul>` isn't rendered and `noResultLabel` shows instead; a drop-zone error clears the results and shows `readErrorLabel`. Both go through the same polite live region. Before 2026-10-21 neither case gave any feedback.
|
|
50
|
+
- Results and labels wrap (`overflow-wrap: anywhere`), so a long decoded URL stays inside the component
|
|
47
51
|
- The drop zone has a dashed border and minimum height by default — style with `.decode-qr-code-dropzone` to customise
|
|
48
52
|
|
|
49
53
|
## Usage
|
|
@@ -87,4 +91,5 @@ With styling:
|
|
|
87
91
|
## Notes
|
|
88
92
|
|
|
89
93
|
- Like `CaptureQrCode`, results are rendered inline and not emitted. To act on results in a parent, extend the component or use `nuxt-qrcode`'s `QrcodeCapture` / `QrcodeDropZone` primitives directly.
|
|
94
|
+
- 2026-10-21 migration: `data-dropping` state hook, theme-token drop zone colours, status messages, wrapping, `StressTest` story (simulation buttons drive the internal handlers).
|
|
90
95
|
- The component renders both input methods unconditionally. If only one is needed, use the underlying `QrcodeCapture` or `QrcodeDropZone` primitives directly.
|