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.
- package/.claude/rules/consumer/ids-from-useid.md +2 -1
- package/.claude/rules/consumer/vfor-keys-from-data.md +14 -0
- package/.claude/skills/component-compliance-checklist.md +24 -1
- package/.claude/skills/component-naming.md +1 -0
- package/.claude/skills/components/banner-video.md +24 -30
- package/.claude/skills/components/content-docs.md +18 -5
- package/.claude/skills/components/display-dialog.md +16 -6
- package/.claude/skills/components/{display-tooltip-defined.md → display-tooltip-rich.md} +40 -42
- package/.claude/skills/components/display-tooltip.md +17 -6
- package/.claude/skills/components/form-field.md +15 -0
- package/.claude/skills/components/form-fieldset.md +18 -2
- package/.claude/skills/components/marquee-scroller.md +51 -14
- package/.claude/skills/components/skip-links.md +16 -0
- package/.claude/skills/composable-tooltips-guide.md +4 -4
- package/.claude/skills/index.md +6 -6
- package/.claude/skills/testing-add-unit-test.md +8 -0
- package/.vscode/srcdev-component-banner-video.code-snippets +38 -25
- package/.vscode/srcdev-component-content-docs.code-snippets +69 -60
- package/.vscode/srcdev-component-display-dialog.code-snippets +7 -5
- package/.vscode/srcdev-component-display-tooltip-rich.code-snippets +50 -0
- package/.vscode/srcdev-component-display-tooltip.code-snippets +31 -0
- package/.vscode/srcdev-component-form-field.code-snippets +7 -6
- package/.vscode/srcdev-component-form-fieldset.code-snippets +12 -12
- package/.vscode/srcdev-component-marquee-scroller.code-snippets +31 -28
- package/.vscode/srcdev-component-skip-links.code-snippets +1 -1
- package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +77 -54
- package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +103 -91
- package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +132 -3
- package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +190 -102
- package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +17 -13
- package/app/components/01.atoms/banner-video/BannerVideo.vue +51 -19
- package/app/components/01.atoms/banner-video/CONSUMER-STYLING.md +59 -40
- package/app/components/01.atoms/banner-video/stories/BannerVideo.stories.ts +88 -1
- package/app/components/01.atoms/banner-video/tests/BannerVideo.spec.ts +127 -78
- package/app/components/01.atoms/banner-video/tests/__snapshots__/BannerVideo.spec.ts.snap +12 -4
- package/app/components/01.atoms/content-wrappers/docs-pages/CONSUMER-STYLING.md +42 -14
- package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +86 -112
- package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +134 -8
- package/app/components/01.atoms/content-wrappers/docs-pages/tests/ContentDocs.spec.ts +131 -65
- package/app/components/01.atoms/display-dialog/CONSUMER-STYLING.md +28 -3
- package/app/components/01.atoms/display-dialog/DisplayDialog.vue +39 -31
- package/app/components/01.atoms/display-dialog/stories/DisplayDialog.stories.ts +92 -1
- package/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts +87 -41
- package/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap +5 -1
- package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +70 -24
- package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +24 -12
- package/app/components/01.atoms/display-tooltip/stories/DisplayTooltip.stories.ts +89 -2
- package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +65 -18
- package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +3 -3
- package/app/components/01.atoms/navigation/skip-links/CONSUMER-STYLING.md +65 -19
- package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +20 -14
- package/app/components/01.atoms/navigation/skip-links/stories/SkipLinks.stories.ts +89 -20
- package/app/components/01.atoms/navigation/skip-links/tests/SkipLinks.spec.ts +70 -10
- package/app/components/02.molecules/display-tooltip-rich/CONSUMER-STYLING.md +110 -0
- package/app/components/02.molecules/display-tooltip-rich/DisplayTooltipRich.vue +148 -0
- package/app/components/02.molecules/display-tooltip-rich/stories/DisplayTooltipRich.stories.ts +254 -0
- package/app/components/02.molecules/display-tooltip-rich/tests/DisplayTooltipRich.spec.ts +156 -0
- package/app/components/02.molecules/display-tooltip-rich/tests/__snapshots__/DisplayTooltipRich.spec.ts.snap +22 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +1 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +1 -1
- package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +1 -0
- package/app/components/05.forms/form-field/CONSUMER-STYLING.md +7 -0
- package/app/components/05.forms/form-field/stories/FormField.stories.ts +116 -2
- package/app/components/05.forms/form-fieldset/CONSUMER-STYLING.md +11 -1
- package/app/components/05.forms/form-fieldset/FormFieldset.vue +4 -2
- package/app/components/05.forms/form-fieldset/stories/FormFieldset.stories.ts +68 -4
- package/app/components/05.forms/form-fieldset/tests/FormFieldset.spec.ts +10 -0
- package/app/composables/tests/useTooltips.spec.ts +3 -3
- package/app/types/components/index.ts +1 -1
- package/package.json +1 -1
- package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +0 -29
- package/app/components/02.molecules/display-tooltip-defined/CONSUMER-STYLING.md +0 -53
- package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +0 -117
- package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +0 -129
- package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +0 -85
- package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +0 -17
- /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"` | `"
|
|
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
|
-
|
|
190
|
-
banner, overlaying the video. It calls `videoEl.pause()` / `.play()`
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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-
|
|
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**,
|
|
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
|
|
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
|
|
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`
|
|
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 (`
|
|
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
|
|
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
|
|
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` |
|
|
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"`) |
|
|
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` |
|
|
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
|
-
:
|
|
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:
|
|
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
|
-
#
|
|
7
|
+
# DisplayTooltipRich
|
|
8
8
|
|
|
9
9
|
## Overview
|
|
10
10
|
|
|
11
|
-
`
|
|
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;
|
|
23
|
-
| `contentText` | `TooltipContentText` | `{}` | Title/body/action content. Each field is optional
|
|
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
|
|
27
|
-
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes
|
|
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
|
-
<
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
-
|
|
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 [`
|
|
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` |
|
|
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 `
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
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
|