srcdev-nuxt-components 9.6.19 → 9.6.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/consumer/ids-from-useid.md +3 -2
- package/.claude/skills/component-compliance-checklist.md +5 -0
- 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/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/index.md +12 -12
- package/.claude/skills/storybook-add-story.md +25 -0
- package/.claude/skills/testing-add-unit-test.md +26 -0
- 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/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/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/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/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/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
|
@@ -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).
|
|
@@ -140,6 +140,11 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
|
|
|
140
140
|
`computed()`: Vue renders nothing for a fractional or negative range and only warns in dev
|
|
141
141
|
(`StepperList`, 2026-10-15)
|
|
142
142
|
- broken image or link URLs
|
|
143
|
+
- an empty list where script loops over the data: a `while` loop that walks or cycles the
|
|
144
|
+
items must still terminate, and an async loop must await something on every pass, or the
|
|
145
|
+
tab freezes. `SamaritanPromptMixed` (empty `messageConfigs` with `introDelay: 0`) and
|
|
146
|
+
`CarouselFlip` (empty `carouselDataIds`) both did, fixed 2026-10-21. Grep the script for
|
|
147
|
+
`while (` and check each one with zero and one item
|
|
143
148
|
- data a third-party child can't handle: a library that throws while rendering (an encoder, a
|
|
144
149
|
parser) breaks the whole page render. Catch it with `onErrorCaptured` and render a fallback
|
|
145
150
|
(`DisplayQrCode` threw "Data too long" on an over-capacity value until 2026-10-19). Check story
|
|
@@ -37,11 +37,13 @@ When filling from a data array, use a dynamic slot name in a `v-for`:
|
|
|
37
37
|
| Prop (template form) | Type | Default | Notes |
|
|
38
38
|
| -------------------------- | ------------------------------------------- | ------- | --------------------------------------------- |
|
|
39
39
|
| `tag` | `"div" \| "section" \| "article" \| "main"` | `"div"` | Use a semantic tag for page landmark regions. |
|
|
40
|
-
| `is-responsive` | `boolean` | `false` | Adds `.is-responsive
|
|
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.
|
|
@@ -11,8 +11,10 @@ type: reference
|
|
|
11
11
|
`<details>`/`<summary>`-based fallback for [`DeepExpandingMenu`](deep-expanding-menu.md) — same
|
|
12
12
|
props, same `ResponsiveHeaderNavItem` data shape, same visual result, but built without CSS
|
|
13
13
|
anchor-positioning or the Popover API. Each nav item with `childLinks` renders as a native
|
|
14
|
-
`<details>` element
|
|
15
|
-
`onClickOutside
|
|
14
|
+
`<details>` element. Groups in one menu share a per-instance `name` (from `useId()`), so opening
|
|
15
|
+
one closes the others. A click outside the menu (`@vueuse/core` `onClickOutside`), a click
|
|
16
|
+
elsewhere inside it, Escape (focus returns to the group's `<summary>`) or following a child link
|
|
17
|
+
closes the open group.
|
|
16
18
|
|
|
17
19
|
Prefer `DeepExpandingMenu` unless the modern implementation's browser support isn't acceptable for
|
|
18
20
|
your target audience — see its own docs for the caveat.
|
|
@@ -21,7 +23,8 @@ your target audience — see its own docs for the caveat.
|
|
|
21
23
|
|
|
22
24
|
| Prop | Type | Default | Description |
|
|
23
25
|
|------|------|---------|-------------|
|
|
24
|
-
| `tag` | `"div" \| "section" \| "nav"
|
|
26
|
+
| `tag` | `"div" \| "section" \| "nav"` | `"nav"` | Root element tag. `ul`/`ol` were dropped 2026-10-21: the root wraps a `<div>`, so they rendered invalid HTML. |
|
|
27
|
+
| `ariaLabel` | `string` | `undefined` | Accessible name on the root. Set it when the page has more than one `<nav>`. |
|
|
25
28
|
| `navLinks` | `ResponsiveHeaderNavItem[]` | `[]` | Top-level nav items. An item with `path` renders as a direct link; an item with `childLinks` renders as a `<details>` group. |
|
|
26
29
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
|
|
27
30
|
|
|
@@ -53,37 +56,35 @@ const navLinks: ResponsiveHeaderNavItem[] = [
|
|
|
53
56
|
|
|
54
57
|
## CSS custom properties
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
| `--deep-expanding-menu-classic-panel-shadow` | `0 0 1rem rgba(0, 0, 0, 0.1)` | Panel box-shadow |
|
|
73
|
-
| `--deep-expanding-menu-classic-panel-padding` | `1.2rem` | Panel padding |
|
|
74
|
-
| `--deep-expanding-menu-classic-panel-heading-colour` | `var(--slate-10)` | `childLinksTitle` heading colour |
|
|
75
|
-
| `--deep-expanding-menu-classic-panel-list-gap` | `1.2rem` | Gap between child link grid items |
|
|
76
|
-
| `--deep-expanding-menu-classic-group-link-colour` | `var(--slate-10)` | Child link text colour |
|
|
77
|
-
| `--deep-expanding-menu-classic-group-link-border-colour-hover` | `var(--slate-10)` | Child link bottom border colour on hover/focus |
|
|
78
|
-
|
|
79
|
-
See `CONSUMER-STYLING.md` for the full token API.
|
|
59
|
+
Public tokens are all `--deep-expanding-menu-classic-*`: item gap, link border/padding/hover
|
|
60
|
+
colour, focus ring, icon size, and the panel's offset, three container-width tiers, colours,
|
|
61
|
+
border, radius, shadow, padding, heading line clamp, list gap and child link colours. Colours
|
|
62
|
+
default from `--theme-text`, `--theme-border`, `--theme-surface-subtle` and `--theme-ring`. The
|
|
63
|
+
full table with defaults, the prefixed class names and the sizing model are in this component's
|
|
64
|
+
`CONSUMER-STYLING.md`.
|
|
65
|
+
|
|
66
|
+
## Data handling
|
|
67
|
+
|
|
68
|
+
- An item with `path` is a link; anything else is a `<details>` group. An empty `path` is a group.
|
|
69
|
+
- A group's heading renders only when `childLinksTitle` is non-empty, and its list only when
|
|
70
|
+
`childLinks` has items.
|
|
71
|
+
- Child links are keyed on index + name, so duplicate names are safe.
|
|
72
|
+
- Long or unbroken labels wrap, the top-level row wraps onto more lines, and each panel is capped
|
|
73
|
+
at the menu's width. A panel opens from its toggle's left edge and doesn't flip, so near the
|
|
74
|
+
right edge it can overrun the menu. The `StressTest` story covers all of these.
|
|
80
75
|
|
|
81
76
|
## Notes
|
|
82
77
|
|
|
83
78
|
- Auto-imported in Nuxt — no manual import needed.
|
|
84
|
-
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
- Until 2026-10-21 every instance used the fixed `name="navigation-group"`, so all menus on a page
|
|
80
|
+
formed one exclusive group (opening a panel in one menu closed another menu's), and the doc
|
|
81
|
+
wrongly said siblings didn't auto-close. Click-outside listeners were also bound per group in
|
|
82
|
+
`onMounted`, so groups added later never closed.
|
|
83
|
+
- 2026-10-21 migration: prefixed every structural class, moved colours to `--theme-*`, panel
|
|
84
|
+
offset to `calc(100% + 1rem)`, panel widths from viewport `@media` to `@container`, removed
|
|
85
|
+
the no-op `--deep-expanding-menu-classic-gap` and a `display: flex !important`, added
|
|
86
|
+
`ariaLabel`, Escape and link-follow closing, a `:focus-visible` ring, `role="list"`, an
|
|
87
|
+
`aria-hidden` caret, the heading line-clamp token and the `StressTest` story.
|
|
87
88
|
- 2026-09-07 migration: moved from an unplaced top-level folder (was `DeepExpandingMenuOld.vue`)
|
|
88
89
|
into `02.molecules/navigation/deep-expanding-menu-classic/`; converted options-style
|
|
89
90
|
`defineProps` to `interface Props` + `withDefaults`; switched its locally-declared
|
|
@@ -24,7 +24,8 @@ built on `<details>`/`<summary>`: same API and visual result, with no popover la
|
|
|
24
24
|
|
|
25
25
|
| Prop | Type | Default | Description |
|
|
26
26
|
|------|------|---------|-------------|
|
|
27
|
-
| `tag` | `"div" \| "section" \| "nav"
|
|
27
|
+
| `tag` | `"div" \| "section" \| "nav"` | `"nav"` | Root element tag. `ul`/`ol` were dropped 2026-10-21: the root wraps a `<div>`, so they rendered invalid HTML. |
|
|
28
|
+
| `ariaLabel` | `string` | `undefined` | Accessible name on the root. Set it when the page has more than one `<nav>`. |
|
|
28
29
|
| `navLinks` | `ResponsiveHeaderNavItem[]` | `[]` | Top-level nav items. An item with `path` renders as a direct link; an item with `childLinks` renders as a toggle + popover panel. |
|
|
29
30
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
|
|
30
31
|
|
|
@@ -56,26 +57,22 @@ const navLinks: ResponsiveHeaderNavItem[] = [
|
|
|
56
57
|
|
|
57
58
|
## CSS custom properties
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
| `--deep-expanding-menu-group-link-colour` | `var(--slate-10)` | Child link text colour |
|
|
76
|
-
| `--deep-expanding-menu-group-link-border-colour-hover` | `var(--slate-10)` | Child link bottom border colour on hover/focus |
|
|
77
|
-
|
|
78
|
-
See `CONSUMER-STYLING.md` for the full token API and what's not tokenised.
|
|
60
|
+
Public tokens are all `--deep-expanding-menu-*`: row gap, link border/padding/hover colour, focus
|
|
61
|
+
ring, icon size, and the panel's width, max height, colours, border, radius, shadow, padding,
|
|
62
|
+
heading line clamp, list gap and child link colours. Colours default from `--theme-text`,
|
|
63
|
+
`--theme-border`, `--theme-surface-subtle` and `--theme-ring`. The full table with defaults, the
|
|
64
|
+
prefixed class names and the state hooks are in this component's `CONSUMER-STYLING.md`.
|
|
65
|
+
|
|
66
|
+
## Data handling
|
|
67
|
+
|
|
68
|
+
- An item with `path` is a link; anything else is a group (toggle + panel). An empty `path` is a group.
|
|
69
|
+
- A group's heading renders only when `childLinksTitle` is non-empty, and its list only when
|
|
70
|
+
`childLinks` has items, so an empty group opens a panel with just its heading.
|
|
71
|
+
- Child links are keyed on index + name, so duplicate names are safe.
|
|
72
|
+
- Long or unbroken labels wrap (`overflow-wrap: anywhere`), the top-level row wraps onto more
|
|
73
|
+
lines, panel lists longer than `--deep-expanding-menu-panel-max-height` scroll, and a panel near
|
|
74
|
+
the right edge flips to align with its toggle's right edge where anchor positioning is supported.
|
|
75
|
+
The `StressTest` story covers all of these.
|
|
79
76
|
|
|
80
77
|
## Notes
|
|
81
78
|
|
|
@@ -92,6 +89,10 @@ See `CONSUMER-STYLING.md` for the full token API and what's not tokenised.
|
|
|
92
89
|
`aria-expanded` revealing ordinary links), not an ARIA menu. `role="menu"` was removed 2026-10-05
|
|
93
90
|
because it requires `menuitem` children and arrow-key handling the panels don't have, so screen
|
|
94
91
|
readers announced a menu that didn't behave like one. Don't add it back.
|
|
92
|
+
- 2026-10-21 migration: prefixed every structural class (`.inner`, `.navigation-*`, `.icon` became
|
|
93
|
+
`.deep-expanding-menu-*`), moved colour defaults to `--theme-*` tokens, added `ariaLabel`, a
|
|
94
|
+
`:focus-visible` ring, `role="list"` on the child list, an `aria-hidden` caret, the heading
|
|
95
|
+
line-clamp and max-height tokens, `position-try-fallbacks: flip-inline`, and the `StressTest` story.
|
|
95
96
|
- 2026-10-05: removed two never-referenced `@position-try` blocks (`--anchor-left`, and an invalid
|
|
96
97
|
`@position-try-fallbacks --anchor-right` at-rule).
|
|
97
98
|
- 2026-09-07 migration: moved from an unplaced top-level folder into
|
|
@@ -134,6 +134,8 @@ consumer's `<style scoped>` file, `.grid-stack__layer` rules need `:deep()`. Ful
|
|
|
134
134
|
|
|
135
135
|
## Notes
|
|
136
136
|
|
|
137
|
+
- The `StressTest` story stacks an overlay of unbroken, long and right-to-left text over an image layer.
|
|
138
|
+
|
|
137
139
|
- `pointer-events: none` on overlay layers (and `pointer-events: auto` on interactive children within them) is the standard pattern for overlays that shouldn't block interaction with layers behind them.
|
|
138
140
|
- There is no built-in `z-index` — stacking is handled purely by DOM order. If a consumer applies `z-index` on a layer for other reasons, be aware it creates a new stacking context.
|
|
139
141
|
- The slot name is used as the Vue `:key` on the layer wrapper, so slot names must be unique.
|
|
@@ -130,6 +130,8 @@ Tokens: `--layout-grid-by-cols-column-count`, `-single-col-below`, `-gap`, `-row
|
|
|
130
130
|
|
|
131
131
|
## Notes
|
|
132
132
|
|
|
133
|
+
- The `StressTest` story feeds every cell oversized content (an unbroken string and URL, long German, a 1600px image, emoji, right-to-left, an empty cell).
|
|
134
|
+
|
|
133
135
|
- Auto-imported in Nuxt — no manual import needed.
|
|
134
136
|
- Slot names can be anything — semantic or indexed. Document order determines render order.
|
|
135
137
|
- 2026-09-27 migration: `singleColBelow` never worked before (the breakpoint was a hardcoded `768px` container query); it does now. `gap`/`columnCount` moved from `v-bind()` to public tokens, and the layout props lost their defaults (defaults now live in CSS). A `section` without a `label` used to read the placeholder "If tag='section' then a label is required" to screen readers; it now renders nothing and relies on the composable's dev warning. `gap` must be a single length (compound values were claimed before but break the column calculation). Inner class `.layout-grid-inner` → `.layout-grid-by-cols-inner`; root no longer a `layoutGrid` size container. No consumer app used the component at the time.
|
|
@@ -98,9 +98,17 @@ See `CONSUMER-STYLING.md` in the component folder.
|
|
|
98
98
|
|
|
99
99
|
## Notes
|
|
100
100
|
|
|
101
|
+
- 2026-10-21: tiles re-pack when any tile changes height (a `ResizeObserver` per tile, throttled to
|
|
102
|
+
one `requestAnimationFrame`), not only when the container width changes, so a late-loading image
|
|
103
|
+
no longer overlaps the tile below. Slot names are read per render (they were a `computed` over
|
|
104
|
+
`useSlots()`, which missed slots added after mount), and tiles are read in DOM order from the
|
|
105
|
+
wrapper instead of a `v-for` ref array, which Vue doesn't keep in DOM order. The multi-column state
|
|
106
|
+
is `data-multiple-cols` and the private tokens are prefixed `--_masonry-grid-*`.
|
|
107
|
+
- The `StressTest` story mixes unbroken, long, image, emoji, right-to-left, empty and one-character tiles.
|
|
108
|
+
|
|
101
109
|
- **How positioning works**: on mount and on every measured resize, each item is read (`item.
|
|
102
110
|
offsetHeight`) in DOM order and placed at the top of whichever column currently has the least
|
|
103
|
-
accumulated height, via inline `--
|
|
111
|
+
accumulated height, via inline `--_masonry-grid-item-top`/`--_masonry-grid-item-left`/`--_masonry-grid-item-width` custom
|
|
104
112
|
properties the component sets directly (not part of the public token API — these are internal
|
|
105
113
|
positioning state, not a styling surface). Column steps are computed in real pixels
|
|
106
114
|
(`itemWidth + gap`) rather than equal percentage slots of the container — a percentage slot only
|
|
@@ -24,6 +24,9 @@ no extra wrapper divs needed.
|
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
27
|
+
The `variant` prop renders as `data-variant` on the root (a bare class like `.content` until
|
|
28
|
+
2026-10-21). The `StressTest` story nests every variant with oversized content.
|
|
29
|
+
|
|
27
30
|
## Track widths
|
|
28
31
|
|
|
29
32
|
| Variant | Max width | Typical use |
|
|
@@ -17,7 +17,9 @@ effects, styled as a monospace line over an underline with a pulsing cursor glyp
|
|
|
17
17
|
|
|
18
18
|
Effects:
|
|
19
19
|
|
|
20
|
-
- `typewriter` — characters typed one by one, held, deleted one by one.
|
|
20
|
+
- `typewriter` — characters typed one by one, held, deleted one by one. "Characters" are graphemes
|
|
21
|
+
(`splitGraphemes()` in `app/utils/graphemes.ts`, `Intl.Segmenter`), so an emoji with a skin tone
|
|
22
|
+
or a family emoji never shows half a glyph.
|
|
21
23
|
- `word-pulse` — the whole message fades in, holds, fades out.
|
|
22
24
|
|
|
23
25
|
Both use `useCancellableTimer` for all timing, so the loop exits cleanly on unmount.
|
|
@@ -31,8 +33,9 @@ Both use `useCancellableTimer` for all timing, so the loop exits cleanly on unmo
|
|
|
31
33
|
deletes or fades out, so a repeated message announces again.
|
|
32
34
|
- **Pause on hover** (WCAG 2.2.2): pointer over the root pauses the sequence where it is and the
|
|
33
35
|
cursor pulse; leaving resumes. The root gets `data-paused` while paused.
|
|
34
|
-
- `prefers-reduced-motion: reduce` removes the cursor pulse and the word-pulse fade transition
|
|
35
|
-
|
|
36
|
+
- `prefers-reduced-motion: reduce` removes the cursor pulse and the word-pulse fade transition, and
|
|
37
|
+
the typewriter shows each message whole instead of typing and deleting it (since 2026-10-21). The
|
|
38
|
+
message sequence keeps running (it's the content).
|
|
36
39
|
|
|
37
40
|
> **Changed 2026-09-27**: `SamaritanPrompt` previously had no live region (the visible text was read
|
|
38
41
|
> character by character, if at all) and no reduced-motion handling; both now match
|
|
@@ -123,9 +126,13 @@ props on the component tag are hyphenated.
|
|
|
123
126
|
|
|
124
127
|
Full token list and class hooks: `app/components/02.molecules/samaritan-prompt/CONSUMER-STYLING.md`.
|
|
125
128
|
Tokens are `--samaritan-prompt-*` (text/underline/cursor colours, font, sizes, gaps, pulse
|
|
126
|
-
duration
|
|
127
|
-
|
|
128
|
-
|
|
129
|
+
duration, white-space, line clamp). Colours default to `--theme-text` (text, underline) and
|
|
130
|
+
`--theme-accent` (cursor); set the three colour tokens to `#fff`, `#fff`, `#cc0000` for the classic
|
|
131
|
+
white-on-black look. Text wraps by default (`--samaritan-prompt-text-white-space: nowrap` restores
|
|
132
|
+
one line).
|
|
133
|
+
|
|
134
|
+
> **Changed 2026-10-21**: colour defaults were `#ffffff` text/underline and a `#cc0000` cursor on the
|
|
135
|
+
> assumption of a dark page, and text was always `nowrap`, so a long message overflowed.
|
|
129
136
|
|
|
130
137
|
The component brings no page layout; centre it from its parent.
|
|
131
138
|
|
|
@@ -134,7 +141,16 @@ The component brings no page layout; centre it from its parent.
|
|
|
134
141
|
|
|
135
142
|
## Notes
|
|
136
143
|
|
|
144
|
+
- Both stories files have a `StressTest` story (long German, unbroken URL, empty message, emoji,
|
|
145
|
+
HTML-like text, right-to-left, single character) with the `CanvasSwitcher` decorator.
|
|
146
|
+
|
|
137
147
|
- `SamaritanPromptMixed` reads `messageConfigs` at the start of each loop, so prop changes land on
|
|
138
|
-
the next loop.
|
|
148
|
+
the next loop. With an empty list it polls every 500 ms (or `introDelay`, if longer) and starts
|
|
149
|
+
once messages arrive; until 2026-10-21 an empty list with `introDelay: 0` spun forever and froze
|
|
150
|
+
the tab.
|
|
151
|
+
- `SamaritanPrompt` restarts when `effect` or the `messages` values change (compared by value, so
|
|
152
|
+
an inline array literal re-created on every parent render doesn't restart it). Until 2026-10-21 it
|
|
153
|
+
only restarted on `effect`, and an empty string in `messages` (or a list that shrank mid-cycle)
|
|
154
|
+
stopped the loop for good.
|
|
139
155
|
- The `Mono MMM 5` font is declared with `@font-face` in each component's unscoped style from
|
|
140
|
-
`/fonts/monoMMM_5.ttf` (`font-display: swap` in
|
|
156
|
+
`/fonts/monoMMM_5.ttf` (`font-display: swap` in both since 2026-10-21; `SamaritanPromptMixed` used `optional`).
|
|
@@ -23,7 +23,7 @@ Branding (logo/wordmark) is entirely consumer-authored via the `#branding` slot
|
|
|
23
23
|
| `pageRowVariant` | `"full" \| "popout" \| "content" \| "inset-content"` | `"content"` | Forwarded to the root `PageRow`'s `variant` prop. |
|
|
24
24
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes on the root `PageRow` — targets `--site-header-*` tokens (see `CONSUMER-STYLING.md`). |
|
|
25
25
|
| `navStyleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes on the nested `ResponsiveHeader` — targets its own `--responsive-header-*`/`--overflow-nav-*` tokens. |
|
|
26
|
-
| `homeNavAriaLabel` | `string` | `"Home
|
|
26
|
+
| `homeNavAriaLabel` | `string` | `"Home navigation"` | aria-label on the home/branding nav landmark — override for localisation. |
|
|
27
27
|
| `mainNavAriaLabel` | `string` | `"Main navigation"` | Forwarded to `ResponsiveHeader`'s `mainNavAriaLabel` — override for localisation. |
|
|
28
28
|
| `secondaryNavAriaLabel` | `string` | `"Secondary navigation"` | Forwarded to `ResponsiveHeader`'s `secondaryNavAriaLabel` — override for localisation. |
|
|
29
29
|
| `overflowMenuAriaLabel` | `string` | `"Overflow navigation menu"` | Forwarded to `ResponsiveHeader`'s `overflowMenuAriaLabel` — override for localisation. |
|
|
@@ -97,8 +97,22 @@ const responsiveNavLinks = {
|
|
|
97
97
|
|
|
98
98
|
---
|
|
99
99
|
|
|
100
|
+
## Styling and layout
|
|
101
|
+
|
|
102
|
+
Tokens, class names and the sticky recipe are in this component's `CONSUMER-STYLING.md`. Since
|
|
103
|
+
2026-10-21:
|
|
104
|
+
|
|
105
|
+
- `--site-header-position: sticky` (with `--site-header-sticky-offset`, `--site-header-z-index`)
|
|
106
|
+
applies to the outer `.site-header-row` (the `PageRow`). Before that it sat on the inner
|
|
107
|
+
`<header>`, whose parent is exactly its height, so the header never actually stuck.
|
|
108
|
+
- The branding column is `fit-content(var(--site-header-branding-max-width, 50%))` and the nav
|
|
109
|
+
column `minmax(0, 1fr)`, so a long brand wraps instead of squeezing the nav, and the nav can
|
|
110
|
+
shrink and collapse instead of overflowing. The `StressTest` story covers it.
|
|
111
|
+
- `.home-navigation` became `.site-header-home-nav`, and `homeNavAriaLabel` defaults to
|
|
112
|
+
`"Home navigation"` (was `"Home Navigation"`).
|
|
113
|
+
|
|
100
114
|
## Migration notes
|
|
101
115
|
|
|
102
|
-
- Consuming layouts previously assembling `PageRow` + `SkipLinks` + `ResponsiveHeader` (or the removed `TabNavigation`) by hand can switch to `SiteHeader` directly; the two `styleClassPassthrough` props map 1:1 onto the two independent passthrough hooks that pattern used.
|
|
116
|
+
- Consuming layouts previously assembling `PageRow` + `SkipLinks` + `ResponsiveHeader` (or the removed `TabNavigation`) by hand can switch to `SiteHeader` directly; the two `styleClassPassthrough` props map 1:1 onto the two independent passthrough hooks that pattern used. Their hand-built markup often uses a `.home-navigation` class of its own; SiteHeader's nav is `.site-header-home-nav`, so move any styles across.
|
|
103
117
|
- `TabNavigation` (removed 2026-10-07) took `NavItemData` (`{ main: [{ text, href, iconName, cssName }] }`, also removed), which is **not** the same shape as `ResponsiveHeaderProp` (`{ groupKey: [{ name, path, childLinksTitle, childLinks }] }`): migrating means reshaping the nav data, not just swapping the component. See "Migrating from TabNavigation" in `responsive-header.md`. Its `navAlign` prop has no `SiteHeader` equivalent since `ResponsiveHeader`'s layout is fixed (branding column + nav column).
|
|
104
118
|
- Requires the consuming layout's main/footer regions to carry `id="main-content"` / `id="footer-content"` — same requirement as using `SkipLinks` directly.
|