srcdev-nuxt-components 9.6.12 → 9.6.14
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/story-register-components.md +1 -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-qr-code.md +22 -4
- 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/eyebrow-text.md +9 -5
- package/.claude/skills/components/header-block.md +13 -5
- package/.claude/skills/components/hero-text.md +13 -9
- package/.claude/skills/components/link-text.md +8 -5
- package/.claude/skills/components/pop-over.md +17 -3
- 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/storybook-add-story.md +5 -2
- package/.claude/skills/testing-add-playwright.md +12 -7
- 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-qr-code.code-snippets +12 -1
- 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-hero-text.code-snippets +21 -5
- package/.vscode/srcdev-component-link-text.code-snippets +3 -3
- package/.vscode/srcdev-component-pop-over.code-snippets +28 -0
- package/.vscode/srcdev-component-skip-links.code-snippets +1 -1
- 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/01.atoms/pop-over/CONSUMER-STYLING.md +42 -9
- package/app/components/01.atoms/pop-over/PopOver.vue +28 -12
- package/app/components/01.atoms/pop-over/stories/PopOver.stories.ts +114 -2
- package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +35 -0
- package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +5 -1
- package/app/components/01.atoms/qr-code/CONSUMER-STYLING.md +16 -3
- package/app/components/01.atoms/qr-code/DisplayQrCode.vue +31 -0
- package/app/components/01.atoms/qr-code/stories/DisplayQrCode.stories.ts +190 -157
- package/app/components/01.atoms/qr-code/tests/DisplayQrCode.spec.ts +58 -0
- package/app/components/01.atoms/text-blocks/eyebrow-text/CONSUMER-STYLING.md +9 -1
- package/app/components/01.atoms/text-blocks/eyebrow-text/EyebrowText.vue +5 -4
- package/app/components/01.atoms/text-blocks/eyebrow-text/playwright/eyebrow-text.playwright.ts +1 -1
- package/app/components/01.atoms/text-blocks/eyebrow-text/stories/EyebrowText.stories.ts +104 -17
- package/app/components/01.atoms/text-blocks/eyebrow-text/tests/EyebrowText.spec.ts +68 -118
- package/app/components/01.atoms/text-blocks/eyebrow-text/tests/__snapshots__/EyebrowText.spec.ts.snap +8 -8
- package/app/components/01.atoms/text-blocks/header-block/CONSUMER-STYLING.md +49 -24
- package/app/components/01.atoms/text-blocks/header-block/HeaderBlock.vue +9 -1
- package/app/components/01.atoms/text-blocks/header-block/stories/HeaderBlock.stories.ts +96 -26
- package/app/components/01.atoms/text-blocks/header-block/tests/HeaderBlock.spec.ts +64 -48
- package/app/components/01.atoms/text-blocks/header-block/tests/__snapshots__/HeaderBlock.spec.ts.snap +2 -2
- package/app/components/01.atoms/text-blocks/hero-text/CONSUMER-STYLING.md +22 -6
- package/app/components/01.atoms/text-blocks/hero-text/HeroText.vue +32 -23
- package/app/components/01.atoms/text-blocks/hero-text/playwright/hero-text.playwright.ts +1 -1
- package/app/components/01.atoms/text-blocks/hero-text/stories/HeroText.stories.ts +176 -23
- package/app/components/01.atoms/text-blocks/hero-text/tests/HeroText.spec.ts +129 -123
- package/app/components/01.atoms/text-blocks/hero-text/tests/__snapshots__/HeroText.spec.ts.snap +2 -2
- package/app/components/01.atoms/text-blocks/link-text/CONSUMER-STYLING.md +17 -0
- package/app/components/01.atoms/text-blocks/link-text/LinkText.vue +9 -3
- package/app/components/01.atoms/text-blocks/link-text/stories/LinkText.stories.ts +96 -99
- package/app/components/01.atoms/text-blocks/link-text/tests/LinkText.spec.ts +84 -93
- 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/02.molecules/price-list/tests/__snapshots__/PriceList.spec.ts.snap +8 -8
- package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +1 -0
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +15 -15
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +3 -3
- package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +3 -3
- package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +3 -3
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +1 -1
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +9 -9
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +9 -9
- 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
|
|
@@ -6,7 +6,7 @@ paths:
|
|
|
6
6
|
# Register every component a story template uses
|
|
7
7
|
|
|
8
8
|
Every component in a story's `template` string, including ones placed in the component's slots, must
|
|
9
|
-
be listed in that story's `components: {}`.
|
|
9
|
+
be listed in that story's `components: {}`. Globally registered ones (`Icon`, `NuxtLink`) are the exception.
|
|
10
10
|
|
|
11
11
|
Why: components aren't auto-registered in story templates; unregistered ones render as unknown tags.
|
|
12
12
|
Detail: skill `storybook-add-story.md` (srcdev-nuxt-components).
|
|
@@ -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".
|
|
@@ -138,6 +138,11 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
|
|
|
138
138
|
`computed()`: Vue renders nothing for a fractional or negative range and only warns in dev
|
|
139
139
|
(`StepperList`, 2026-10-15)
|
|
140
140
|
- broken image or link URLs
|
|
141
|
+
- data a third-party child can't handle: a library that throws while rendering (an encoder, a
|
|
142
|
+
parser) breaks the whole page render. Catch it with `onErrorCaptured` and render a fallback
|
|
143
|
+
(`DisplayQrCode` threw "Data too long" on an over-capacity value until 2026-10-19). Check story
|
|
144
|
+
control ranges against the library's real range too: its `radius` control ran to 20 while the
|
|
145
|
+
library clamps to 0-1
|
|
141
146
|
Describe what to check in the story's docs description, and look at it at every
|
|
142
147
|
`CanvasSwitcher` width. Fixes usually mean `overflow-wrap: anywhere`, `min-inline-size: 0` on
|
|
143
148
|
flex/grid children, `Array.from(str)` instead of `str[0]`, and `clamp()` on values
|
|
@@ -197,6 +202,9 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
|
|
|
197
202
|
- A toast, banner or status message doesn't `.focus()` itself when it appears (rule
|
|
198
203
|
`consumer/a11y-no-focus-steal.md`).
|
|
199
204
|
- Focus is visible (`:focus-visible`, not a suppressed outline) on anything focusable.
|
|
205
|
+
- An element hidden visually but kept in the DOM (zero width, `opacity: 0`, kept as an anchor)
|
|
206
|
+
also leaves the tab order and the accessibility tree (`tabindex="-1"`, `aria-hidden`), or it's
|
|
207
|
+
an invisible tab stop (`DisplayTooltip`'s `hideTrigger`, fixed 2026-10-18).
|
|
200
208
|
- `aria-invalid` is bound as `fieldHasError ? 'true' : undefined`, never the boolean itself:
|
|
201
209
|
`:aria-invalid="fieldHasError"` renders `aria-invalid="false"` on every valid field
|
|
202
210
|
(`InputTextarea`, `ToggleSwitch`, fixed 2026-10-16). Same for an empty `aria-describedby`.
|
|
@@ -264,7 +272,20 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
|
|
|
264
272
|
- A `position: fixed`/`sticky` overlay exposes a public z-index token defaulting to `999999`
|
|
265
273
|
(pitfall #17).
|
|
266
274
|
- No utility class from `06.utility-classes` in the component's own markup unless it's used
|
|
267
|
-
for what that class actually means (pitfall #18, `mi-12`).
|
|
275
|
+
for what that class actually means (pitfall #18, `mi-12`). And no class the layer doesn't
|
|
276
|
+
define at all: `DisplayTooltipRich` carried `subtitle-sm`, `body-sm` and `input-value`, which
|
|
277
|
+
did nothing here but would pick up a consumer's own CSS (removed 2026-10-18). Grep
|
|
278
|
+
`app/assets/styles` for each non-prefixed class.
|
|
279
|
+
- A class this component styles that another component also styles (a class passed to a child,
|
|
280
|
+
or a child's class the parent restyles): compare the two selectors' specificity. An `all: unset`
|
|
281
|
+
in the higher one silently resets every layout property the other sets: `DisplayTooltip`'s
|
|
282
|
+
close-button reset stretched `DisplayTooltipRich`'s right-aligned button full width for six
|
|
283
|
+
weeks (fixed 2026-10-18).
|
|
284
|
+
- A `v-for` over consumer or CMS data is keyed on something guaranteed unique; a data field
|
|
285
|
+
that can repeat (`href`, `to`, a label) is combined with the index (rule
|
|
286
|
+
`consumer/vfor-keys-from-data.md`).
|
|
287
|
+
- Every raw `<button>` has an explicit `type` (ESLint `vue/html-button-has-type`, on since
|
|
288
|
+
2026-10-18), or it submits any form a consumer renders it inside.
|
|
268
289
|
- Nothing load-bearing relies on a CSS feature WebKit lacks (pitfall #19).
|
|
269
290
|
- No `computed()` over `useSlots()`; use a function called from the template (pitfall #25).
|
|
270
291
|
- Values derived from props are `computed()`, not plain consts frozen at mount.
|
|
@@ -286,6 +307,8 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
|
|
|
286
307
|
defaults to its min-content width and overflows a narrow column (`MultipleCheckboxes`,
|
|
287
308
|
2026-10-16).
|
|
288
309
|
- Element ids come from `useId()`, never from data values (rule `consumer/ids-from-useid.md`).
|
|
310
|
+
Call `useId()` once at setup, never inside a `computed` (`DisplayTooltip`, `DisplayTooltipRich`,
|
|
311
|
+
2026-10-18).
|
|
289
312
|
- A wrapper passes its child only props the child declares. Anything else falls through as an
|
|
290
313
|
HTML attribute on the child's root: `InputTextField` put `label="…"` and
|
|
291
314
|
`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);
|
|
@@ -10,10 +10,10 @@ Renders a styled QR code SVG from a string value. Supports colour, size, corner
|
|
|
10
10
|
|------|------|---------|-------------|
|
|
11
11
|
| `qr-value` | `string` | required | The string to encode into the QR code |
|
|
12
12
|
| `variant` | `QrCodeVariant` | `{ inner: "default", marker: "default", pixel: "default" }` | Shape variants for inner markers, outer markers, and pixels |
|
|
13
|
-
| `radius` | `number` | `0` | Corner rounding
|
|
13
|
+
| `radius` | `number` | `0` | Corner rounding for the rounded, circle and dots shapes, 0 to 1 (the library clamps values outside) |
|
|
14
14
|
| `black-color` | `string` | `"currentColor"` | Colour for the dark modules |
|
|
15
15
|
| `white-color` | `string` | `"transparent"` | Colour for the light modules |
|
|
16
|
-
| `size` | `string` | `"256px"` | Width of the QR code (height matches via `aspect-ratio: 1 / 1`) |
|
|
16
|
+
| `size` | `string` | `"256px"` | Width of the QR code (height matches via `aspect-ratio: 1 / 1`), never wider than its container |
|
|
17
17
|
| `aria-label` | `string` | `"QR code"` | Accessible name for the QR image (root has `role="img"`). Pass something specific and translated, e.g. "Scan to book an appointment". |
|
|
18
18
|
| `style-class-passthrough` | `string \| string[]` | `[]` | Extra classes on the root element |
|
|
19
19
|
|
|
@@ -22,7 +22,7 @@ Renders a styled QR code SVG from a string value. Supports colour, size, corner
|
|
|
22
22
|
```typescript
|
|
23
23
|
import type { QrCodeVariant } from "srcdev-nuxt-components/types/components";
|
|
24
24
|
|
|
25
|
-
// Each field accepts: "default" | "circle" | "rounded" | "dots" | "
|
|
25
|
+
// Each field accepts: "default" | "circle" | "rounded" | "dots" | "pixelated"
|
|
26
26
|
const variant: QrCodeVariant = {
|
|
27
27
|
inner: "circle",
|
|
28
28
|
marker: "rounded",
|
|
@@ -30,6 +30,21 @@ const variant: QrCodeVariant = {
|
|
|
30
30
|
};
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
## Slots and events
|
|
34
|
+
|
|
35
|
+
| Name | Kind | Description |
|
|
36
|
+
|------|------|-------------|
|
|
37
|
+
| `fallback` | slot | Rendered in place of the code when `qr-value` is too long to encode (about 2.9KB at the lowest error correction, less at higher). Empty by default, leaving a blank box of the same size. |
|
|
38
|
+
| `error` | event | `(error: Error)`, emitted once when encoding fails. Resets when `qr-value` changes. |
|
|
39
|
+
|
|
40
|
+
```vue
|
|
41
|
+
<DisplayQrCode :qr-value="bookingUrl" @error="reportQrError">
|
|
42
|
+
<template #fallback>
|
|
43
|
+
<a :href="bookingUrl">Open the booking page</a>
|
|
44
|
+
</template>
|
|
45
|
+
</DisplayQrCode>
|
|
46
|
+
```
|
|
47
|
+
|
|
33
48
|
## CSS token
|
|
34
49
|
|
|
35
50
|
`--display-qr-code-size` overrides the `size` prop when set (e.g. on a page wrapper). Full reference: `app/components/01.atoms/qr-code/CONSUMER-STYLING.md`.
|
|
@@ -39,6 +54,7 @@ const variant: QrCodeVariant = {
|
|
|
39
54
|
| Class | Where | Description |
|
|
40
55
|
|-------|-------|-------------|
|
|
41
56
|
| `.display-qr-code` | root | always present |
|
|
57
|
+
| `.display-qr-code-fallback` | root | only when the value can't be encoded (with `data-error`) |
|
|
42
58
|
|
|
43
59
|
## Usage
|
|
44
60
|
|
|
@@ -63,7 +79,9 @@ Custom size, colour, and rounded pixels:
|
|
|
63
79
|
|
|
64
80
|
## Notes
|
|
65
81
|
|
|
66
|
-
- The component renders directly as the `Qrcode` element from `nuxt-qrcode` — there is no wrapper `<div>`. The `.display-qr-code` class is applied to the `Qrcode` root, which renders as an `<svg>`.
|
|
82
|
+
- The component renders directly as the `Qrcode` element from `nuxt-qrcode` — there is no wrapper `<div>`. The `.display-qr-code` class is applied to the `Qrcode` root, which renders as an `<svg>`. When encoding fails it renders a `<span class="display-qr-code display-qr-code-fallback">` instead.
|
|
83
|
+
- Encoding errors are caught with `onErrorCaptured`, so an over-capacity value from a CMS can't break the page render. `nuxt-qrcode` must be installed in the consuming app.
|
|
67
84
|
- `size` is applied via CSS `v-bind()` — pass any valid CSS length value (`px`, `rem`, `%`, etc.).
|
|
68
85
|
- `black-color` defaults to `currentColor`, so the QR code inherits the surrounding text colour automatically unless overridden.
|
|
69
86
|
- `white-color` defaults to `transparent`, making the background of the QR code see-through by default.
|
|
87
|
+
- 2026-10-19 migration: added the `fallback` slot and `error` event, capped the width at the container, corrected the variant list (`pixelated`, no `classy`) and added a `StressTest` story; the story's radius control now uses the real 0 to 1 range.
|
|
@@ -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
|