srcdev-nuxt-components 9.6.11 → 9.6.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/.claude/rules/consumer/ids-from-useid.md +2 -1
  2. package/.claude/rules/consumer/vfor-keys-from-data.md +14 -0
  3. package/.claude/skills/component-compliance-checklist.md +24 -1
  4. package/.claude/skills/component-naming.md +1 -0
  5. package/.claude/skills/components/banner-video.md +24 -30
  6. package/.claude/skills/components/content-docs.md +18 -5
  7. package/.claude/skills/components/display-dialog.md +16 -6
  8. package/.claude/skills/components/{display-tooltip-defined.md → display-tooltip-rich.md} +40 -42
  9. package/.claude/skills/components/display-tooltip.md +17 -6
  10. package/.claude/skills/components/form-field.md +15 -0
  11. package/.claude/skills/components/form-fieldset.md +18 -2
  12. package/.claude/skills/components/marquee-scroller.md +51 -14
  13. package/.claude/skills/components/skip-links.md +16 -0
  14. package/.claude/skills/composable-tooltips-guide.md +4 -4
  15. package/.claude/skills/index.md +6 -6
  16. package/.claude/skills/testing-add-unit-test.md +8 -0
  17. package/.vscode/srcdev-component-banner-video.code-snippets +38 -25
  18. package/.vscode/srcdev-component-content-docs.code-snippets +69 -60
  19. package/.vscode/srcdev-component-display-dialog.code-snippets +7 -5
  20. package/.vscode/srcdev-component-display-tooltip-rich.code-snippets +50 -0
  21. package/.vscode/srcdev-component-display-tooltip.code-snippets +31 -0
  22. package/.vscode/srcdev-component-form-field.code-snippets +7 -6
  23. package/.vscode/srcdev-component-form-fieldset.code-snippets +12 -12
  24. package/.vscode/srcdev-component-marquee-scroller.code-snippets +31 -28
  25. package/.vscode/srcdev-component-skip-links.code-snippets +1 -1
  26. package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +77 -54
  27. package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +103 -91
  28. package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +132 -3
  29. package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +190 -102
  30. package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +17 -13
  31. package/app/components/01.atoms/banner-video/BannerVideo.vue +51 -19
  32. package/app/components/01.atoms/banner-video/CONSUMER-STYLING.md +59 -40
  33. package/app/components/01.atoms/banner-video/stories/BannerVideo.stories.ts +88 -1
  34. package/app/components/01.atoms/banner-video/tests/BannerVideo.spec.ts +127 -78
  35. package/app/components/01.atoms/banner-video/tests/__snapshots__/BannerVideo.spec.ts.snap +12 -4
  36. package/app/components/01.atoms/content-wrappers/docs-pages/CONSUMER-STYLING.md +42 -14
  37. package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +86 -112
  38. package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +134 -8
  39. package/app/components/01.atoms/content-wrappers/docs-pages/tests/ContentDocs.spec.ts +131 -65
  40. package/app/components/01.atoms/display-dialog/CONSUMER-STYLING.md +28 -3
  41. package/app/components/01.atoms/display-dialog/DisplayDialog.vue +39 -31
  42. package/app/components/01.atoms/display-dialog/stories/DisplayDialog.stories.ts +92 -1
  43. package/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts +87 -41
  44. package/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap +5 -1
  45. package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +70 -24
  46. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +24 -12
  47. package/app/components/01.atoms/display-tooltip/stories/DisplayTooltip.stories.ts +89 -2
  48. package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +65 -18
  49. package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +3 -3
  50. package/app/components/01.atoms/navigation/skip-links/CONSUMER-STYLING.md +65 -19
  51. package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +20 -14
  52. package/app/components/01.atoms/navigation/skip-links/stories/SkipLinks.stories.ts +89 -20
  53. package/app/components/01.atoms/navigation/skip-links/tests/SkipLinks.spec.ts +70 -10
  54. package/app/components/02.molecules/display-tooltip-rich/CONSUMER-STYLING.md +110 -0
  55. package/app/components/02.molecules/display-tooltip-rich/DisplayTooltipRich.vue +148 -0
  56. package/app/components/02.molecules/display-tooltip-rich/stories/DisplayTooltipRich.stories.ts +254 -0
  57. package/app/components/02.molecules/display-tooltip-rich/tests/DisplayTooltipRich.spec.ts +156 -0
  58. package/app/components/02.molecules/display-tooltip-rich/tests/__snapshots__/DisplayTooltipRich.spec.ts.snap +22 -0
  59. package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +1 -0
  60. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +1 -1
  61. package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +1 -0
  62. package/app/components/05.forms/form-field/CONSUMER-STYLING.md +7 -0
  63. package/app/components/05.forms/form-field/stories/FormField.stories.ts +116 -2
  64. package/app/components/05.forms/form-fieldset/CONSUMER-STYLING.md +11 -1
  65. package/app/components/05.forms/form-fieldset/FormFieldset.vue +4 -2
  66. package/app/components/05.forms/form-fieldset/stories/FormFieldset.stories.ts +68 -4
  67. package/app/components/05.forms/form-fieldset/tests/FormFieldset.spec.ts +10 -0
  68. package/app/composables/tests/useTooltips.spec.ts +3 -3
  69. package/app/types/components/index.ts +1 -1
  70. package/package.json +1 -1
  71. package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +0 -29
  72. package/app/components/02.molecules/display-tooltip-defined/CONSUMER-STYLING.md +0 -53
  73. package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +0 -117
  74. package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +0 -129
  75. package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +0 -85
  76. package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +0 -17
  77. /package/app/types/components/{display-tooltip-defined.d.ts → display-tooltip-rich.d.ts} +0 -0
@@ -7,7 +7,8 @@ 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.
10
+ `useId()` too, never a fixed string. Call `useId()` once at setup and store the result; never call
11
+ it inside a `computed`, which can re-run it.
11
12
 
12
13
  Why: two instances sharing a `name` and values produce duplicate ids, and a value with spaces makes
13
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
@@ -0,0 +1,14 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ ---
5
+
6
+ # Key data-driven `v-for` lists on something unique
7
+
8
+ Key a `v-for` over prop, CMS or API data on a field guaranteed unique. When the only candidates
9
+ can repeat (`href`, `to`, a label), combine one with the index: `` :key="`${index}-${link.href}`" ``.
10
+
11
+ Why: duplicate keys break Vue's keyed diff. Reordering a list where two items shared an `href` left
12
+ a stale extra element in the DOM (`SkipLinks`, 2026-10-18); `ContentDocs` had the same bug with
13
+ nav items sharing a `to`. Vue only warns while patching an update, so first render looks fine.
14
+ Detail: skill `testing-add-unit-test.md` (srcdev-nuxt-components), "Testing duplicate `v-for` keys".
@@ -197,6 +197,9 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
197
197
  - A toast, banner or status message doesn't `.focus()` itself when it appears (rule
198
198
  `consumer/a11y-no-focus-steal.md`).
199
199
  - Focus is visible (`:focus-visible`, not a suppressed outline) on anything focusable.
200
+ - An element hidden visually but kept in the DOM (zero width, `opacity: 0`, kept as an anchor)
201
+ also leaves the tab order and the accessibility tree (`tabindex="-1"`, `aria-hidden`), or it's
202
+ an invisible tab stop (`DisplayTooltip`'s `hideTrigger`, fixed 2026-10-18).
200
203
  - `aria-invalid` is bound as `fieldHasError ? 'true' : undefined`, never the boolean itself:
201
204
  `:aria-invalid="fieldHasError"` renders `aria-invalid="false"` on every valid field
202
205
  (`InputTextarea`, `ToggleSwitch`, fixed 2026-10-16). Same for an empty `aria-describedby`.
@@ -204,6 +207,11 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
204
207
  `aria-hidden` (`ToggleSwitch`, 2026-10-16).
205
208
  - Live-updating or auto-advancing content (carousels, marquees, auto-dismissing toasts) has a
206
209
  way to pause it, per WCAG 2.2.2 — a visible control, or pause-on-hover/focus at minimum.
210
+ - Pause-on-focus listens for `focusin`/`focusout` on the root (with a `relatedTarget`
211
+ containment check), not `focus`/`blur`, which don't bubble: focusable content inside an
212
+ `aria-hidden` moving track otherwise gets focus while still hidden and moving. A looping
213
+ duplicate copy gets `inert`, and a focus-caused pause must not undo a pause the user chose
214
+ (`MarqueeScroller`, 2026-10-18).
207
215
  - `prefers-reduced-motion` is respected for any animation that isn't purely decorative.
208
216
  - A mode that hides a native control's own indicator (a range thumb, a checkbox box, a caret)
209
217
  draws its own visible state for the current value, and its interactive area isn't shrunk to a
@@ -259,7 +267,20 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
259
267
  - A `position: fixed`/`sticky` overlay exposes a public z-index token defaulting to `999999`
260
268
  (pitfall #17).
261
269
  - No utility class from `06.utility-classes` in the component's own markup unless it's used
262
- for what that class actually means (pitfall #18, `mi-12`).
270
+ for what that class actually means (pitfall #18, `mi-12`). And no class the layer doesn't
271
+ define at all: `DisplayTooltipRich` carried `subtitle-sm`, `body-sm` and `input-value`, which
272
+ did nothing here but would pick up a consumer's own CSS (removed 2026-10-18). Grep
273
+ `app/assets/styles` for each non-prefixed class.
274
+ - A class this component styles that another component also styles (a class passed to a child,
275
+ or a child's class the parent restyles): compare the two selectors' specificity. An `all: unset`
276
+ in the higher one silently resets every layout property the other sets: `DisplayTooltip`'s
277
+ close-button reset stretched `DisplayTooltipRich`'s right-aligned button full width for six
278
+ weeks (fixed 2026-10-18).
279
+ - A `v-for` over consumer or CMS data is keyed on something guaranteed unique; a data field
280
+ that can repeat (`href`, `to`, a label) is combined with the index (rule
281
+ `consumer/vfor-keys-from-data.md`).
282
+ - Every raw `<button>` has an explicit `type` (ESLint `vue/html-button-has-type`, on since
283
+ 2026-10-18), or it submits any form a consumer renders it inside.
263
284
  - Nothing load-bearing relies on a CSS feature WebKit lacks (pitfall #19).
264
285
  - No `computed()` over `useSlots()`; use a function called from the template (pitfall #25).
265
286
  - Values derived from props are `computed()`, not plain consts frozen at mount.
@@ -281,6 +302,8 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
281
302
  defaults to its min-content width and overflows a narrow column (`MultipleCheckboxes`,
282
303
  2026-10-16).
283
304
  - Element ids come from `useId()`, never from data values (rule `consumer/ids-from-useid.md`).
305
+ Call `useId()` once at setup, never inside a `computed` (`DisplayTooltip`, `DisplayTooltipRich`,
306
+ 2026-10-18).
284
307
  - A wrapper passes its child only props the child declares. Anything else falls through as an
285
308
  HTML attribute on the child's root: `InputTextField` put `label="…"` and
286
309
  `error-message="…"` on `.input-text-wrapper` (fixed 2026-10-16). Check the child root's
@@ -105,6 +105,7 @@ external consumer, free to rename. Run `/check-component-usage` for the current
105
105
  | ~~InputCheckboxRadioWithLabel~~ | ✅ `InputCheckboxRadioField` (2026-09-27) | - |
106
106
  | ~~ActionMenuItemCore~~ | ✅ `ActionMenuItem` (2026-09-27) | - |
107
107
  | ~~AlertMaskCore~~ | ✅ `AlertMask` (2026-10-17) | - |
108
+ | ~~DisplayTooltipDefined~~ | ✅ `DisplayTooltipRich` (2026-10-18): Material 3 "rich tooltip", the implementation word "Defined" said nothing about what it is | - |
108
109
  | CardCore | **decide**: `Card` is single-word and collision-prone; prefer a prefixed name (`Display*`, `Content*`) | yes |
109
110
  | AccordianCore | **decide**: also fixes the "Accordian" misspelling; bare `Accordion` is single-word and collision-prone, and it is built on `<details>`/`<summary>` | yes |
110
111
  | ~~TabsCore~~ | ✅ `TabbedContent` (2026-10-07) | - |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: BannerVideo
3
- description: BannerVideo full-width hero video banner — props, depth tier system, verticalPosition/horizontalPosition, playIcon/pauseIcon/toggle-icon slot, reduced-motion fallback, CSS tokens, consumer styling
3
+ description: BannerVideo full-width hero video banner — props, depth tier system, verticalPosition/horizontalPosition, playIcon/pauseIcon/toggle-icon slot, playLabel/pauseLabel, reduced-motion fallback, CSS tokens, consumer styling
4
4
  type: reference
5
5
  ---
6
6
 
@@ -25,21 +25,23 @@ The video uses `autoplay muted loop playsinline preload="auto"` attributes on th
25
25
  | `alt` | `string` | `""` | Alt text for the fallback `NuxtImg`. |
26
26
  | `imgWidth` | `number` | `1920` | Intrinsic width of the poster image — required for NuxtImg/IPX optimisation. |
27
27
  | `imgHeight` | `number` | `1080` | Intrinsic height of the poster image — required for NuxtImg/IPX optimisation. |
28
- | `tag` | `"section" \| "div" \| "header" \| "main" \| "article"` | `"section"` | HTML element rendered as the root. |
28
+ | `tag` | `"section" \| "div" \| "header" \| "main" \| "article"` | `"div"` | HTML element rendered as the root. Pass `aria-label` as a plain attribute (it falls through to the root) when the tag is a landmark that needs a name. |
29
29
  | `depth` | `"xs" \| "sm" \| "md" \| "lg" \| "xl"` | `"md"` | Responsive max-height tier. Each maps to a `clamp()` scale; override via `--banner-video-max-height-{depth}`. |
30
- | `aspectRatio` | `string` | `"21/9"` | CSS `aspect-ratio` of the container (e.g. `"16/9"`, `"21/9"`, `"4/3"`). |
30
+ | `aspectRatio` | `string` | `"21/9"` | CSS `aspect-ratio` of the container (e.g. `"16/9"`, `"21/9"`, `"4/3"`, `"1.5"`). Anything that isn't a positive number or `w/h` ratio falls back to `"21/9"`. |
31
31
  | `objectFit` | `"cover" \| "contain" \| "fill" \| "none" \| "scale-down"` | `"cover"` | How the video and fallback image fill the banner frame. |
32
32
  | `verticalPosition` | `"start" \| "center" \| "end"` | `"center"` | Vertical crop position. Maps to `align-self` on the video element and `object-position` Y on the fallback image. |
33
33
  | `horizontalPosition` | `"start" \| "center" \| "end"` | `"center"` | Horizontal crop position. Maps to `object-position` X on the fallback image. |
34
34
  | `playIcon` | `string` | `"mdi:play"` | Iconify icon name shown on the toggle button before playback starts. |
35
35
  | `pauseIcon` | `string` | `"mdi:pause"` | Iconify icon name shown on the toggle button once playback starts. |
36
+ | `playLabel` | `string` | `"Play background video"` | Toggle button label while paused (visually hidden; the button's accessible name). Override for localisation. |
37
+ | `pauseLabel` | `string` | `"Pause background video"` | Toggle button label while playing. Override for localisation. |
36
38
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
37
39
 
38
40
  ## Slots
39
41
 
40
42
  | Slot | Slot props | Purpose |
41
43
  |---|---|---|
42
- | `toggle-icon` | `{ isPlaying: boolean }` | Replaces the toggle button's icon entirely. Default renders `<Icon :name="isPlaying ? pauseIcon : playIcon" />` — use this slot instead of `playIcon`/`pauseIcon` when an Iconify name isn't enough (custom SVG, different icon set). |
44
+ | `toggle-icon` | `{ isPlaying: boolean }` | Replaces the toggle button's icon entirely. Default renders `<Icon :name="isPlaying ? pauseIcon : playIcon" />` — use this slot instead of `playIcon`/`pauseIcon` when an Iconify name isn't enough (custom SVG, different icon set). Content is clipped to the round button, so size it to `--banner-video-toggle-size`. The accessible name still comes from `playLabel`/`pauseLabel`. |
43
45
 
44
46
  ## Basic usage
45
47
 
@@ -180,18 +182,22 @@ Inline style tokens (set from props):
180
182
  | `--banner-video-toggle-background` | `oklch(0% 0 0 / 0.4)` | Button background |
181
183
  | `--banner-video-toggle-background-hover` | `oklch(0% 0 0 / 0.6)` | Button background on hover/focus |
182
184
  | `--banner-video-toggle-icon-color` | `white` | Icon colour |
185
+ | `--banner-video-toggle-focus-outline-width` | `2px` | Focus ring width |
186
+ | `--banner-video-toggle-focus-outline-color` | `currentColor` | Focus ring colour (the icon colour by default) |
187
+ | `--banner-video-toggle-focus-outline-offset` | `2px` | Focus ring offset |
183
188
 
184
189
  Full details, plus global/page-scoped/per-instance override patterns, live in the
185
190
  component's own `CONSUMER-STYLING.md`.
186
191
 
187
192
  ## Pause/play toggle
188
193
 
189
- A `<button class="banner-video__toggle">` is always rendered in the bottom-right corner of the
190
- banner, overlaying the video. It calls `videoEl.pause()` / `.play()` directly and its
191
- `aria-label` reflects actual playback state ("Pause background video" / "Play background video"),
192
- tracked via the video's native `play`/`pause` events rather than assumed — autoplay can be
193
- blocked by the browser, so the label starts as "Play background video" until a `play` event
194
- actually fires.
194
+ An icon-only `InputButton variant="inline"` (class `.banner-video-toggle`) is always rendered in the
195
+ bottom-right corner of the banner, overlaying the video. It calls `videoEl.pause()` / `.play()`
196
+ directly. Its accessible name is visually hidden text (`.input-button-text.sr-only`, not an
197
+ `aria-label`) that reflects actual playback state: `pauseLabel` while playing, `playLabel`
198
+ otherwise (defaults "Pause background video" / "Play background video"). State is tracked via the
199
+ video's native `play`/`pause` events rather than assumed: autoplay can be blocked by the browser,
200
+ so the label starts as `playLabel` until a `play` event actually fires.
195
201
 
196
202
  This exists to satisfy **WCAG 2.2.2 (Pause, Stop, Hide)**: content that autoplays and lasts more
197
203
  than 5 seconds needs an on-page mechanism to pause it. The `prefers-reduced-motion` fallback only
@@ -211,9 +217,11 @@ component, since `BannerVideo` has no slot for it.
211
217
  | Class | Applied when |
212
218
  |---|---|
213
219
  | `.banner-video` | Always — the root element |
214
- | `.video` | The `<video>` element |
215
- | `.fallback` | The `<NuxtImg>` fallback — hidden by default, shown via `prefers-reduced-motion` CSS |
216
- | `.banner-video__toggle` | The pause/play button — always rendered except under `prefers-reduced-motion: reduce` |
220
+ | `.banner-video-media` | The `<video>` element |
221
+ | `.banner-video-fallback` | The `<NuxtImg>` fallback — hidden by default, shown via `prefers-reduced-motion` CSS |
222
+ | `.banner-video-toggle` | The pause/play `InputButton` (also `.input-button`) — always rendered, hidden under `prefers-reduced-motion: reduce` |
223
+
224
+ Renamed 2026-10-18 from `.video`, `.fallback` and `.banner-video__toggle` (generic names collided with consumer CSS).
217
225
 
218
226
  ## Consumer styling
219
227
 
@@ -239,37 +247,23 @@ per-instance override patterns.
239
247
 
240
248
  `max-height` from the depth tier silently wins over `aspect-ratio` once the viewport is wide enough. For example, at `depth="md"` the max-height clamp caps at `56rem` — so changing `aspectRatio` from `"21/9"` to `"1/1"` produces no visible change on a wide desktop because `max-height` is the binding constraint.
241
249
 
242
- **If you need `aspect-ratio` to dominate**, override all depth tokens to a large fixed value on the parent:
250
+ **If you need `aspect-ratio` to dominate**, raise the active tier's token on the parent. Only that one is read: `depth` is always set, so the root always matches one `[data-depth]` rule.
243
251
 
244
252
  ```css
245
253
  .my-page {
246
- --banner-video-max-height-xs: 80rem;
247
- --banner-video-max-height-sm: 80rem;
248
- --banner-video-max-height-md: 80rem;
249
254
  --banner-video-max-height-lg: 80rem;
250
- --banner-video-max-height-xl: 80rem;
251
255
  }
252
256
  ```
253
257
 
254
- Or via inline `:style` on the parent element (useful for dev QA exploration):
255
-
256
258
  ```vue
257
- <div :style="{
258
- '--banner-video-max-height-xs': '80rem',
259
- '--banner-video-max-height-sm': '80rem',
260
- '--banner-video-max-height-md': '80rem',
261
- '--banner-video-max-height-lg': '80rem',
262
- '--banner-video-max-height-xl': '80rem',
263
- }">
259
+ <div class="my-page">
264
260
  <BannerVideo aspect-ratio="1/1" depth="lg" ... />
265
261
  </div>
266
262
  ```
267
263
 
268
- You must override **all five tier tokens** — overriding only the active depth token is not sufficient because the component resolves the token by name.
269
-
270
264
  ## Notes
271
265
 
272
266
  - `loading="eager"` and `decoding="async"` are hardcoded on the fallback `NuxtImg` — it is above the fold by definition.
273
267
  - The video has `autoplay muted loop playsinline preload="auto"` — intentional and not configurable. This component is for ambient background video only, not a media player — the pause/play toggle exists purely for WCAG 2.2.2 compliance, not as a general playback UI (no seek bar, volume, etc.).
274
- - `prefers-reduced-motion` is handled in CSS (`.video { display: none }` + `.fallback { display: block }`), not via JS.
268
+ - `prefers-reduced-motion` is handled in CSS (`.banner-video-media` and the toggle hidden, `.banner-video-fallback` shown), not via JS. The script still loads and plays the hidden video.
275
269
  - Storybook: the `"none"` image provider is active, so `poster` paths pass through unchanged. Always provide explicit `img-width` and `img-height` to avoid the `w=1536` fallback in deployed Storybook.
@@ -4,7 +4,7 @@
4
4
 
5
5
  `ContentDocs` is a docs-page layout shell: a primary nav column, a main content column, and an "on this page" nav column, arranged via CSS `@container` queries (not viewport breakpoints) so the layout adapts to the space actually available — correct even when a consumer page has its own decoration (e.g. a site-wide left nav) eating into the viewport.
6
6
 
7
- `docsNav` and `docsPageNav` are **not** slots — they're rendered internally as `ExpandingPanel` accordions driven by `docsNavItems`/`docsPageNavItems` props. Only `docsContent` remains a slot, since it's arbitrary page content. The two side panels auto force-open/collapse based on the component's own measured width via `useContainerBreakpoints`, and share a native `<details name>` accordion group only on mobile (see "Breakpoint behaviour" below).
7
+ `docsNav` and `docsPageNav` are **not** slots — they're rendered internally as collapsible panels (`ExpandingPanelClassic` by default, `ExpandingPanel` with `panel-variant="modern"`) driven by `docsNavItems`/`docsPageNavItems` props. Only `docsContent` remains a slot, since it's arbitrary page content. The two side panels auto force-open/collapse based on the component's own measured width via `useContainerBreakpoints`, and share a native `<details name>` accordion group only on mobile (see "Breakpoint behaviour" below).
8
8
 
9
9
  ---
10
10
 
@@ -13,7 +13,7 @@
13
13
  | Prop | Type | Default | Notes |
14
14
  |------|------|---------|-------|
15
15
  | `tag` | `"div" \| "section" \| "article" \| "main"` | `"div"` | Root element tag. |
16
- | `docsNavItems` | `DocsNavItem[]` | `[]` | Items rendered in the `docsNav` panel. Panel (and its `.docs-nav` wrapper) is omitted entirely when empty. |
16
+ | `docsNavItems` | `DocsNavItem[]` | `[]` | Items rendered in the `docsNav` panel. Panel (and its `.content-docs-nav` wrapper) is omitted entirely when empty. Items may share a `to` (keys include the index); every item matching the active `to` is marked active. |
17
17
  | `docsPageNavItems` | `DocsNavItem[]` | `[]` | Items rendered in the `docsPageNav` panel. Panel omitted entirely when empty. |
18
18
  | `docsNavLabel` | `string` | `"Navigation"` | Heading text for the `docsNav` panel's `#summary`. |
19
19
  | `docsPageNavLabel` | `string` | `"On this page"` | Heading text for the `docsPageNav` panel's `#summary`. |
@@ -113,7 +113,7 @@ In the template, `docsContent` is placed **before** `docsNav`/`docsPageNav` —
113
113
 
114
114
  ### Why the two panels share a `name` only on mobile
115
115
 
116
- Both panels are `ExpandingPanel`s using the native `<details name="...">` grouping feature, which makes same-named panels mutually exclusive (browser force-closes one when the other opens). That's the wanted behaviour on mobile (accordion — only one open at a time), but at tablet/desktop both panels must be open **simultaneously** — a shared name there would make the browser silently force-close one of them the moment both try to be open. So the component computes distinct names (`"docsNav"` / `"docsPageNav"`) once past mobile, and a shared name (`"docsPanelGroup"`) only while mobile.
116
+ Both panels are `ExpandingPanel`s using the native `<details name="...">` grouping feature, which makes same-named panels mutually exclusive (browser force-closes one when the other opens). That's the wanted behaviour on mobile (accordion — only one open at a time), but at tablet/desktop both panels must be open **simultaneously** — a shared name there would make the browser silently force-close one of them the moment both try to be open. So the component computes distinct names (`useId()` plus `-docsNav` / `-docsPageNav`) once past mobile, and the shared bare `useId()` value only while mobile. Ids come from `useId()`, so two `ContentDocs` on one page never share a group.
117
117
 
118
118
  ### Related fix in ExpandingPanel.vue
119
119
 
@@ -125,7 +125,19 @@ Building this component's forced-open/forced-closed cycling surfaced a real bug
125
125
 
126
126
  `DocsNavItem.icon` is optional (any icon name resolvable by `<Icon>`, e.g. Lucide set: `"lucide:rocket"`). Items without an icon still align correctly with icon-bearing items — the link is `display: grid` with a fixed-width icon column (`--content-docs-link-icon-size`), not `flex`, so an absent icon doesn't collapse the label leftward.
127
127
 
128
- To move the icon to the end of the link instead of the start, set `--content-docs-nav-link-icon-order` (or `--content-docs-page-nav-link-icon-order`) to `rtl` (default `ltr`). This uses a `direction` flip to mirror which physical side the fixed-width column renders on, rather than swapping `grid-column` values directly — swapping columns would put the label into the icon-sized track and squeeze it. The icon and label content reset `direction: ltr` internally so text/glyphs don't visually mirror.
128
+ To move the icon to the end of the link instead of the start, set `--content-docs-nav-link-icon-order` (or `--content-docs-page-nav-link-icon-order`) to `rtl` (default `ltr`). This uses a `direction` flip to mirror which physical side the fixed-width column renders on, rather than swapping `grid-column` values directly — swapping columns would put the label into the icon-sized track and squeeze it. The icon resets `direction: ltr` so its glyph doesn't mirror. The label uses `unicode-bidi: plaintext` with `text-align: start` instead, so it takes its direction from its own text: a right-to-left label still reads and aligns right-to-left. (Until 2026-10-18 the label forced `direction: ltr`, which broke right-to-left labels.)
129
+
130
+ ---
131
+
132
+ ## Classes and state hooks
133
+
134
+ Root `.content-docs`; grid `.content-docs-inner`; grid areas `.content-docs-nav`, `.content-docs-content`, `.content-docs-page-nav` (each `min-inline-size: 0`, tracks `minmax(0, 1fr)`, content area `overflow-wrap: break-word`); headings `.content-docs-nav-heading` / `.content-docs-page-nav-heading`; `<nav>`s `.content-docs-nav-list` / `.content-docs-page-nav-list` (their `<ul role="list">`); links `.content-docs-nav-link` / `.content-docs-page-nav-link` with `-icon` and `-label` children. The active link is selected by `[aria-current="page"]`; there is no `.is-active` class.
135
+
136
+ Renamed 2026-10-18 from the unprefixed `.docs-*` names (`.docs-nav`, `.docs-content`, `.docs-page-nav`, `.docs-nav-link`, ...), which collided with consumer CSS. Full map in `CONSUMER-STYLING.md`.
137
+
138
+ ## Worst-case data
139
+
140
+ Labels and headings wrap anywhere, so an unbroken label stays in its column. An empty `label` renders a thin link with no accessible name: filter those out of the data. Link labels have no line-clamp token: they name their destinations. See the `StressTest` story.
129
141
 
130
142
  ---
131
143
 
@@ -140,7 +152,8 @@ All `--content-docs-*` tokens can be overridden at global, page, or instance sco
140
152
  - `-bg` (default: `var(--page-bg)`), `-padding-block`, `-padding-inline`, `-border-radius`
141
153
 
142
154
  **Link tokens** (shared: `--content-docs-link-*`, per-side: `--content-docs-{nav,page-nav}-link-*`):
143
- - `-font-size`, `-padding-block`, `-padding-inline`, `-margin-block`, `-border-radius`, `-color`, `-bg`, `-hover-bg`, `-hover-color`, `-active-bg`, `-active-color`
155
+ - `-font-size`, `-padding-block`, `-padding-inline`, `-margin-block`, `-border-radius`, `-color`, `-bg`, `-hover-bg`, `-hover-color`, `-active-bg`, `-active-color` (active defaults to `var(--theme-surface)` / `var(--theme-on-surface)`; it was a fixed green pair before 2026-10-18)
156
+ - Shared only: `--content-docs-link-focus-outline-width` (`0.2rem`), `--content-docs-link-focus-outline-color` (`var(--theme-border-focus)`)
144
157
 
145
158
  **Column-width tokens** (fixed-width grid tracks at tablet/desktop):
146
159
  - `--content-docs-nav-column-width` (default `23rem`, desktop `docsNav` track)
@@ -14,14 +14,14 @@ open/close state management.
14
14
  |---|---|---|---|
15
15
  | `dataDialogId` | `string` | — | **Required.** Unique identifier rendered as `data-dialog-id` attribute. |
16
16
  | `variant` | `'dialog' \| 'modal' \| 'confirm' \| 'alert' \| 'fullscreen'` | `'dialog'` | Controls panel sizing and ARIA role. |
17
- | `theme` | `SemanticTheme` | `undefined` | Optional header accent — `data-theme` on `.display-dialog-header` only; adds a coloured bottom border and tints the close icon. |
17
+ | `theme` | `SemanticTheme` | `undefined` | Optional header accent: `data-theme` on `.display-dialog-header` only; adds a coloured bottom border and tints the close icon (`--display-dialog-header-accent-color`). |
18
18
  | `v-model` | `boolean` | — | Controls open/closed state. |
19
19
  | `allowContentScroll` | `boolean` | `false` | Enables independent scroll on `.display-dialog-content`. |
20
- | `lockViewport` | `boolean` | `true` | Adds/removes `lock` class on `<body>` on mount/close. |
20
+ | `lockViewport` | `boolean` | `true` | Locks body scroll (`useBodyLock`) on mount and releases it on unmount, not on close: render the dialog with `v-if`. |
21
21
  | `justifyDialog` | `'start' \| 'center' \| 'end'` | `'center'` | Horizontal position of panel within overlay. |
22
22
  | `alignDialog` | `'start' \| 'center' \| 'end'` | `'center'` | Vertical position of panel within overlay. |
23
23
  | `closeIcon` | `string` | app.config | Icon name for the header close button. Override per-instance or set globally via `app.config`. |
24
- | `closeLabel` | `string` | app.config (`"Close"`) | Screen-reader text on the icon-only close button. Pass a translated string, or set it site-wide via `app.config`. |
24
+ | `closeLabel` | `string` | app.config (`"Close"`) | Visually hidden text on the icon-only close button (an `InputButton`). Pass a translated string, or set it site-wide via `app.config`. |
25
25
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root `<dialog>` element. |
26
26
 
27
27
  ## Slots
@@ -35,16 +35,26 @@ open/close state management.
35
35
 
36
36
  `.display-dialog-footer` is omitted entirely when neither action button slot is provided.
37
37
 
38
+ ## Accessible name
39
+
40
+ The `#dialogTitle` slot names the dialog (`aria-labelledby` points at it). A dialog without a
41
+ title has no name, so pass `aria-label="..."`: it isn't a prop, it falls through to the root
42
+ `<dialog>`.
43
+
38
44
  ## Variants
39
45
 
40
46
  | Variant | Panel size | ARIA role | Dismiss on Escape/outside-click |
41
47
  |---|---|---|---|
42
48
  | `dialog` | `70dvh × min(75%, 72rem)` | `dialog` | Yes |
43
- | `modal` | Content-sized | `dialog` | Yes |
44
- | `confirm` | Content-sized | `dialog` | Yes |
49
+ | `modal` | Content-sized, capped at the viewport | `dialog` | Yes |
50
+ | `confirm` | `min(90%, 48rem)` wide, content height | `dialog` | Yes |
45
51
  | `alert` | `min(90%, 48rem)` | `alertdialog` | **No** — requires explicit action |
46
52
  | `fullscreen` | `100% × 100%` | `dialog` | Yes |
47
53
 
54
+ Every variant is capped at the overlay's size. Long titles and content wrap, footer buttons wrap onto
55
+ further rows, and content taller than the panel is clipped (or scrolls with `allowContentScroll`)
56
+ while the header and footer stay visible.
57
+
48
58
  ## Basic usage
49
59
 
50
60
  ```vue
@@ -137,7 +147,7 @@ CSS over per-instance overrides — dialogs are site-wide UI:
137
147
 
138
148
  ```css
139
149
  /* assets/styles/setup/07.components/display-dialog.css */
140
- :root {
150
+ :where(html) {
141
151
  --display-dialog-inner-border-radius: 1.2rem;
142
152
  --display-dialog-inner-background: var(--brand-surface);
143
153
  --display-dialog-inner-border: 0.1rem solid var(--brand-border);
@@ -1,30 +1,37 @@
1
1
  ---
2
- name: DisplayTooltipDefined
3
- description: DisplayTooltipDefined structured title/body/action tooltip content with close button, composes DisplayTooltip
2
+ name: DisplayTooltipRich
3
+ description: DisplayTooltipRich (Material 3 "rich tooltip", renamed from DisplayTooltipDefined) structured title/body/action tooltip content with close button, composes DisplayTooltip
4
4
  type: reference
5
5
  ---
6
6
 
7
- # DisplayTooltipDefined
7
+ # DisplayTooltipRich
8
8
 
9
9
  ## Overview
10
10
 
11
- `DisplayTooltipDefined` composes [`DisplayTooltip`](display-tooltip.md), supplying a structured
11
+ `DisplayTooltipRich` composes [`DisplayTooltip`](display-tooltip.md), supplying a structured
12
12
  title/body/action content shape (each rendered with a consumer-chosen tag) plus a built-in close
13
13
  button wired to the popover's `popovertarget` and to `DisplayTooltip`'s scoped `close()`, so it also
14
14
  closes the tooltip on Safari 16 (no Popover API). Use this when you want the standard tooltip content
15
15
  layout without hand-building the `tooltipContent` slot markup each time; use plain `DisplayTooltip`
16
16
  directly when you need full control over the popover's content.
17
17
 
18
+ The name follows Material 3's **rich tooltip** (subhead, supporting text, optional action, as
19
+ opposed to a plain one-line tooltip). Renamed from `DisplayTooltipDefined` on 2026-10-18: every
20
+ `display-tooltip-defined` class and `--display-tooltip-defined-*` token became `display-tooltip-rich`,
21
+ with the same suffixes and defaults. Older notes below use the new component name but keep the
22
+ class names of their date (`.popover-content-defined` is today's `.display-tooltip-rich-content`).
23
+
18
24
  ## Props
19
25
 
20
26
  | Prop | Type | Default | Description |
21
27
  |------|------|---------|-------------|
22
- | `tooltipId` | `string` | `""` | Base id for the popover. Prefixed with `nuxt-tooltip-` when provided; auto-generated via `useId()` (also prefixed) when omitted. |
23
- | `contentText` | `TooltipContentText` | `{}` | Title/body/action content. Each field is optional — omitted fields render nothing. |
28
+ | `tooltipId` | `string` | `""` | Base id for the popover. Prefixed with `nuxt-tooltip-` when provided; when empty, an id generated once at setup via `useId()` (also prefixed) is used. Reactive. |
29
+ | `contentText` | `TooltipContentText` | `{}` | Title/body/action content. Each field is optional: omitted fields render nothing. An empty `tag` falls back to `p` (title, body) or `span` (action). Text renders as text, never HTML. |
24
30
  | `triggerAriaLabel` | `string` | `"Toggle the popover"` | Forwarded to the underlying `DisplayTooltip`'s `triggerAriaLabel` — override for localisation. |
31
+ | `triggerIcon` | `string` | `undefined` | Forwarded to `DisplayTooltip`'s `triggerIcon` (its default question-mark icon when unset). |
25
32
  | `closeButtonText` | `string` | `"Close"` | Visible text on the close button — override for localisation. |
26
- | `closeButtonAriaLabel` | `string` | `"Close tool tip"` | aria-label on the close button — override for localisation. |
27
- | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the underlying `DisplayTooltip` root element. |
33
+ | `closeButtonAriaLabel` | `string` | `"Close tool tip"` | aria-label on the close button: override for localisation, and keep the visible `closeButtonText` inside it (WCAG 2.5.3, label in name). |
34
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the underlying `DisplayTooltip` root, which always also carries `display-tooltip-rich`. |
28
35
 
29
36
  `TooltipContentText` is exported from `~/types/components`:
30
37
 
@@ -61,57 +68,40 @@ const contentText: TooltipContentText = {
61
68
  </script>
62
69
 
63
70
  <template>
64
- <DisplayTooltipDefined :content-text="contentText" />
71
+ <DisplayTooltipRich :content-text="contentText" />
65
72
  </template>
66
73
  ```
67
74
 
75
+ ## Close button
76
+
77
+ An `InputButton` (`variant="inline"`, `type="button"`) with the `.display-tooltip-close-button`
78
+ class. It carries `popovertarget` and `popovertargetaction="hide"` (which `useTooltipsGuide` looks
79
+ for) and calls `DisplayTooltip`'s scoped `close()`, so it also works without the Popover API.
80
+
68
81
  ## CSS custom properties
69
82
 
70
- | Property | Default | Controls |
71
- |----------|---------|----------|
72
- | `--display-tooltip-defined-content-gap` | `1.2rem` | Gap between title/body/action/close-button |
73
- | `--display-tooltip-defined-title-colour` | `inherit` | `tooltipTitle` text colour |
74
- | `--display-tooltip-defined-title-margin-block` | `0 0` | `tooltipTitle` block margin |
75
- | `--display-tooltip-defined-title-font-size` | `1.6rem` | `tooltipTitle` font size |
76
- | `--display-tooltip-defined-title-font-weight` | `700` | `tooltipTitle` font weight |
77
- | `--display-tooltip-defined-title-line-height` | `120%` | `tooltipTitle` line height |
78
- | `--display-tooltip-defined-title-letter-spacing` | `normal` | `tooltipTitle` letter spacing |
79
- | `--display-tooltip-defined-body-colour` | `inherit` | `tooltipContent` text colour |
80
- | `--display-tooltip-defined-body-margin-block` | `0 0` | `tooltipContent` block margin |
81
- | `--display-tooltip-defined-body-font-size` | `1.3rem` | `tooltipContent` font size |
82
- | `--display-tooltip-defined-body-font-weight` | `inherit` | `tooltipContent` font weight |
83
- | `--display-tooltip-defined-body-line-height` | `140%` | `tooltipContent` line height |
84
- | `--display-tooltip-defined-body-letter-spacing` | `normal` | `tooltipContent` letter spacing |
85
- | `--display-tooltip-defined-action-colour` | `inherit` | `tooltipAction` text colour |
86
- | `--display-tooltip-defined-action-margin-block` | `0 0` | `tooltipAction` block margin |
87
- | `--display-tooltip-defined-action-font-size` | `1.4rem` | `tooltipAction` font size |
88
- | `--display-tooltip-defined-action-font-weight` | `700` | `tooltipAction` font weight |
89
- | `--display-tooltip-defined-action-line-height` | `130%` | `tooltipAction` line height |
90
- | `--display-tooltip-defined-action-letter-spacing` | `normal` | `tooltipAction` letter spacing |
91
- | `--display-tooltip-defined-close-button-margin-block` | `1.2rem 0` | Space above the close button |
92
-
93
- Also renders every `--display-tooltip-*` token from the underlying `DisplayTooltip` — see its
94
- CONSUMER-STYLING.md for the full list.
83
+ See `CONSUMER-STYLING.md` for the full `--display-tooltip-rich-*` list: per-field colour,
84
+ margin, font size and weight, line height, letter spacing and a `*-line-clamp` token (default
85
+ `none`) for the title, body and action, plus the content gap and the close button's top margin.
86
+ Every `--display-tooltip-*` token from the underlying `DisplayTooltip` applies too.
95
87
 
96
88
  ## Pairing with useTooltipsGuide
97
89
 
98
90
  For a sequential, auto-advancing "guide" walkthrough (the pattern used in some guide intros — show
99
91
  step 1, wait for dismissal, show step 2, ...), pair this component with
100
92
  [`useTooltipsGuide`](../composable-tooltips-guide.md): put a ref on the container wrapping your
101
- `DisplayTooltipDefined`/`DisplayTooltip` instances and pass it to the composable. The composable
93
+ `DisplayTooltipRich`/`DisplayTooltip` instances and pass it to the composable. The composable
102
94
  only needs native `[popover]`/`popovertarget` markup, which both components already emit, so no
103
95
  extra wiring is required beyond the container ref. See the `GuidedTour` story in
104
- `DisplayTooltipDefined.stories.ts` for a full working example (it recreates the old
105
- `app/pages/ui/tooltips.vue` demo page, removed 2026-08-23 when Storybook became the only demo
106
- surface).
96
+ `DisplayTooltipRich.stories.ts` for a full working example.
107
97
 
108
98
  ## Notes
109
99
 
110
100
  - Auto-imported in Nuxt — no manual import needed.
111
101
  - 2026-09-07 migration: moved from an unplaced top-level folder into
112
- `02.molecules/display-tooltip-defined/` (molecule tier, since it composes the `DisplayTooltip`
102
+ `02.molecules/display-tooltip-rich/` (molecule tier, since it composes the `DisplayTooltip`
113
103
  atom); converted options-style `defineProps` to `interface Props` + `withDefaults`; moved the
114
- inline `TooltipContentText` interface out to `~/types/components/display-tooltip-defined.d.ts`
104
+ inline `TooltipContentText` interface out to `~/types/components/display-tooltip-rich.d.ts`
115
105
  so consumers can import it (inline `.vue` types aren't importable); fixed a dead CSS selector —
116
106
  the stylesheet targeted `.popover .popover-content .popover-content-defined`, but the actual
117
107
  rendered classes are `.display-tooltip-popover`/`.display-tooltip-popover-content`, so the rule
@@ -121,7 +111,7 @@ surface).
121
111
  the title/body/action text had been rendering only by accident, via CSS falling back to the
122
112
  inherited colour when a `var()` reference is unresolvable, not by design.
123
113
  - 2026-09-07 (same day, follow-up): `.popover-content-defined` gained `display: flex;
124
- flex-direction: column;` and a `gap` (new `--display-tooltip-defined-content-gap` token), and
114
+ flex-direction: column;` and a `gap` (new `--display-tooltip-rich-content-gap` token), and
125
115
  its close button is now right-aligned (`align-self: flex-end`) rather than sitting inline after
126
116
  the action text — part of the same visual-polish pass as `DisplayTooltip` itself (see its skill
127
117
  doc). Note `align-self` only works because `.popover-content-defined` (the button's actual DOM
@@ -137,4 +127,12 @@ surface).
137
127
  `font-weight`, `line-height`, `letter-spacing`, and `margin-block` (all overridable, in case a
138
128
  consumer genuinely wants extra margin on top of `gap`) — rather than only `colour`. The close
139
129
  button gained its own `margin-block` token to separate it from the action text above it.
140
- - File: `app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue`
130
+ - 2026-10-18 migration: generic classes `.popover-content-defined`, `.tooltip-title`,
131
+ `.tooltip-body`, `.tooltip-action` renamed to `.display-tooltip-rich-content`, `-title`,
132
+ `-body`, `-action`, and the root gained `.display-tooltip-rich`; undefined utility classes
133
+ (`subtitle-sm`, `body-sm`, `input-value`) removed; the close button is now an `InputButton` (it
134
+ had no `type`, so it submitted surrounding forms), and its right alignment and margin finally
135
+ apply (`DisplayTooltip`'s `all: unset` close-button rule out-specified them, stretching it full
136
+ width); the generated id is created once at setup instead of inside a `computed`; new
137
+ `triggerIcon` prop, empty-tag fallback, `*-line-clamp` tokens and wrapping for worst-case content.
138
+ - File: `app/components/02.molecules/display-tooltip-rich/DisplayTooltipRich.vue`
@@ -11,7 +11,7 @@ type: reference
11
11
  `DisplayTooltip` renders a trigger button that opens an anchor-positioned popover panel, built on
12
12
  the native Popover API (`popover`/`popovertarget`) and CSS anchor-positioning
13
13
  (`anchor-name`/`position-anchor`/`anchor()`). For a structured title/body/action content shape
14
- with a built-in close button, see [`DisplayTooltipDefined`](display-tooltip-defined.md), which
14
+ with a built-in close button, see [`DisplayTooltipRich`](display-tooltip-rich.md), which
15
15
  composes this component.
16
16
 
17
17
  **Browser support:** since 2026-10-05 open/close and positioning go through the shared
@@ -25,8 +25,9 @@ closes in JS, with outside-click and Escape dismissal. The trigger has `aria-exp
25
25
  | Prop | Type | Default | Description |
26
26
  |------|------|---------|-------------|
27
27
  | `tooltipId` | `string` | `""` | Id linking the trigger button (`popovertarget`) to the popover panel (`id`). Auto-generated via `useId()` when omitted. |
28
- | `hideTrigger` | `boolean` | `false` | Visually hides the trigger button (kept in the DOM, not removed) — e.g. when an equivalent trigger is supplied via the `triggerContent` slot. |
28
+ | `hideTrigger` | `boolean` | `false` | Collapses the trigger button to zero width and removes it from the tab order and accessibility tree (`tabindex="-1"`, `aria-hidden`). It stays in the DOM as the panel's anchor: open the panel from your own button with `popovertarget="<tooltipId>"` (Popover API browsers only; Safari 16 has no way to open it then). |
29
29
  | `triggerAriaLabel` | `string` | `"Toggle the popover"` | aria-label on the trigger button — override for localisation. |
30
+ | `triggerIcon` | `string` | `"fa7-solid:circle-question"` | Iconify name for the trigger button icon. |
30
31
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
31
32
 
32
33
  ## Slots
@@ -52,7 +53,7 @@ closes in JS, with outside-click and Escape dismissal. The trigger has `aria-exp
52
53
  ## CSS custom properties
53
54
 
54
55
  See `CONSUMER-STYLING.md` for the full `--display-tooltip-*` token API (padding, gaps, icon size,
55
- popover width/colours/radius, and the close-button tokens `DisplayTooltipDefined` uses).
56
+ popover width/colours/radius, and the close-button tokens `DisplayTooltipRich` uses).
56
57
 
57
58
  ## Notes
58
59
 
@@ -61,9 +62,19 @@ popover width/colours/radius, and the close-button tokens `DisplayTooltipDefined
61
62
  `-line-height` (`1.4`), so a bare `<p>` reads as tooltip text without extra styling.
62
63
 
63
64
  - Auto-imported in Nuxt — no manual import needed.
64
- - `tooltipId` is only reactive at mount — changing it after mount does not regenerate the linked
65
- `popovertarget`/`id` pair reactively beyond Vue's normal re-render, since both derive from the
66
- same computed.
65
+ - `tooltipId` is reactive: changing it updates both `popovertarget` and the panel `id`. Clearing it
66
+ falls back to the id generated once at setup (`nuxt-tooltip-<useId>`), which stays stable.
67
+ - The trigger is `type="button"`, so it never submits a surrounding form. It stays a raw `<button>`
68
+ rather than `InputButton`, like `PopOver`'s: `useAnchoredPopover` needs the native element (it
69
+ carries `popovertarget` and `anchor-name`) in its ref.
70
+ - Long unbroken text wraps in the trigger row and the panel; the panel width is capped at the
71
+ viewport (`100vw - 3.2rem`).
72
+ - 2026-10-18 migration: root class `.display-tooltip-core` renamed to `.display-tooltip`; the
73
+ hidden trigger's generic `.hide` class replaced by `[data-hidden]`, plus `tabindex="-1"` and
74
+ `aria-hidden` so it's no longer an invisible tab stop; `type="button"` added (it submitted
75
+ surrounding forms); new `triggerIcon` prop; the generated id is created once at setup instead
76
+ of inside a `computed`; the close button's documented resting-outline token now works and its
77
+ hover styles apply on `:focus-visible`; wrapping and width cap for worst-case content.
67
78
  - 2026-09-07 migration: moved from an unplaced top-level folder into `01.atoms/display-tooltip/`;
68
79
  converted options-style `defineProps` to `interface Props` + `withDefaults`; fixed a real
69
80
  accessibility bug where the trigger button's hover/focus-visible state set `outline-offset` but
@@ -57,10 +57,25 @@ Full token table in `app/components/05.forms/form-field/CONSUMER-STYLING.md`. Pu
57
57
  Target state via `.form-field[data-width="wide"]`, `.form-field[data-has-gutter]`,
58
58
  `.form-field[data-invalid]`.
59
59
 
60
+ FormField doesn't change `overflow-wrap` or `min-inline-size` on its slot content: wrapping long
61
+ copy is the slotted control's job (every `*Field` component handles its own). Plain text slotted
62
+ directly needs its own wrapping rule.
63
+
64
+ ---
65
+
66
+ ## Storybook
67
+
68
+ `Components/Forms/Form Field/FormField`: `Default`, `AllWidths`, `NoGutter`, and `StressTest`
69
+ (each width, with and without gutter and error state, around an `InputTextField` with hostile
70
+ copy). Stories use the `CanvasSwitcher` decorator.
71
+
60
72
  ---
61
73
 
62
74
  ## History
63
75
 
76
+ **2026-10-17**: added the `StressTest` story, `CanvasSwitcher` decorator and Docs-page
77
+ descriptions; nothing in the component broke under it.
78
+
64
79
  **Migrated 2026-09-26** (0/5 → full compliance):
65
80
 
66
81
  - Width and gutter modifiers moved from bare classes (`.narrow`, `.medium`, `.wide`, `.has-gutter`)
@@ -21,8 +21,8 @@ ARIA semantics.
21
21
  | `:legend` | `string` | `""` | Legend text. The legend renders when this or the `legend` slot is given. |
22
22
  | `group-role` | `"group" \| "radiogroup"` | `"group"` | `"group"` renders no `role` attribute (a fieldset's native role is already `group`). `"radiogroup"` sets `role="radiogroup"`: use it for radio buttons only. |
23
23
  | `:required` | `boolean` | `false` | Sets `aria-required="true"` **only** when `group-role="radiogroup"`. `aria-required` isn't allowed on `role="group"`, so checkbox groups get nothing; put "(required)" in the legend text instead. |
24
- | `:field-has-error` | `boolean` | `false` | Sets `aria-invalid` on the fieldset. |
25
- | `:data-testid` | `string` | `""` | Passed to the fieldset. |
24
+ | `:field-has-error` | `boolean` | `false` | Sets `aria-invalid="true"` on the fieldset; the attribute is absent when `false`. |
25
+ | `:data-testid` | `string` | `""` | Passed to the fieldset; omitted when empty. |
26
26
  | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra classes on the fieldset, via `useStyleClassPassthrough` (reacts to prop changes). |
27
27
 
28
28
  ## Slots
@@ -54,10 +54,26 @@ content `--form-fieldset-content-margin-block|margin-inline`, legend
54
54
  `--form-fieldset-legend-color|font-size|font-weight|line-height|margin-block|margin-inline`. Error state hook:
55
55
  `.form-fieldset[aria-invalid="true"]`.
56
56
 
57
+ The fieldset sets `min-inline-size: 0` and the legend `overflow-wrap: anywhere`, so neither
58
+ overflows a narrow column. Slotted controls handle their own wrapping.
59
+
60
+ ---
61
+
62
+ ## Storybook
63
+
64
+ `Components/Forms/Form Fieldset/FormFieldset`: `Default`, `CheckboxGroup`, `WithError`, and
65
+ `StressTest` (hostile legend copy with the error state on). Stories use the `CanvasSwitcher`
66
+ decorator and link to the three components that render FormFieldset.
67
+
57
68
  ---
58
69
 
59
70
  ## History
60
71
 
72
+ **2026-10-17**: StressTest story, CanvasSwitcher and Docs-page descriptions added. Fixed: a
73
+ valid fieldset rendered `aria-invalid="false"`; an empty `dataTestid` rendered `data-testid=""`;
74
+ the fieldset had no `min-inline-size: 0` (each wrapping component set it on its own class), and
75
+ the legend didn't wrap unbroken copy.
76
+
61
77
  **Migrated 2026-09-26** (0/5 → full compliance):
62
78
 
63
79
  - **Role bug fixed:** `role="radiogroup"` used to be hardcoded, so screen readers announced