srcdev-nuxt-components 9.6.11 → 9.6.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/.claude/rules/consumer/ids-from-useid.md +2 -1
  2. package/.claude/rules/consumer/vfor-keys-from-data.md +14 -0
  3. package/.claude/skills/component-compliance-checklist.md +24 -1
  4. package/.claude/skills/component-naming.md +1 -0
  5. package/.claude/skills/components/banner-video.md +24 -30
  6. package/.claude/skills/components/content-docs.md +18 -5
  7. package/.claude/skills/components/display-dialog.md +16 -6
  8. package/.claude/skills/components/{display-tooltip-defined.md → display-tooltip-rich.md} +40 -42
  9. package/.claude/skills/components/display-tooltip.md +17 -6
  10. package/.claude/skills/components/form-field.md +15 -0
  11. package/.claude/skills/components/form-fieldset.md +18 -2
  12. package/.claude/skills/components/marquee-scroller.md +51 -14
  13. package/.claude/skills/components/skip-links.md +16 -0
  14. package/.claude/skills/composable-tooltips-guide.md +4 -4
  15. package/.claude/skills/index.md +6 -6
  16. package/.claude/skills/testing-add-unit-test.md +8 -0
  17. package/.vscode/srcdev-component-banner-video.code-snippets +38 -25
  18. package/.vscode/srcdev-component-content-docs.code-snippets +69 -60
  19. package/.vscode/srcdev-component-display-dialog.code-snippets +7 -5
  20. package/.vscode/srcdev-component-display-tooltip-rich.code-snippets +50 -0
  21. package/.vscode/srcdev-component-display-tooltip.code-snippets +31 -0
  22. package/.vscode/srcdev-component-form-field.code-snippets +7 -6
  23. package/.vscode/srcdev-component-form-fieldset.code-snippets +12 -12
  24. package/.vscode/srcdev-component-marquee-scroller.code-snippets +31 -28
  25. package/.vscode/srcdev-component-skip-links.code-snippets +1 -1
  26. package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +77 -54
  27. package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +103 -91
  28. package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +132 -3
  29. package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +190 -102
  30. package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +17 -13
  31. package/app/components/01.atoms/banner-video/BannerVideo.vue +51 -19
  32. package/app/components/01.atoms/banner-video/CONSUMER-STYLING.md +59 -40
  33. package/app/components/01.atoms/banner-video/stories/BannerVideo.stories.ts +88 -1
  34. package/app/components/01.atoms/banner-video/tests/BannerVideo.spec.ts +127 -78
  35. package/app/components/01.atoms/banner-video/tests/__snapshots__/BannerVideo.spec.ts.snap +12 -4
  36. package/app/components/01.atoms/content-wrappers/docs-pages/CONSUMER-STYLING.md +42 -14
  37. package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +86 -112
  38. package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +134 -8
  39. package/app/components/01.atoms/content-wrappers/docs-pages/tests/ContentDocs.spec.ts +131 -65
  40. package/app/components/01.atoms/display-dialog/CONSUMER-STYLING.md +28 -3
  41. package/app/components/01.atoms/display-dialog/DisplayDialog.vue +39 -31
  42. package/app/components/01.atoms/display-dialog/stories/DisplayDialog.stories.ts +92 -1
  43. package/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts +87 -41
  44. package/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap +5 -1
  45. package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +70 -24
  46. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +24 -12
  47. package/app/components/01.atoms/display-tooltip/stories/DisplayTooltip.stories.ts +89 -2
  48. package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +65 -18
  49. package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +3 -3
  50. package/app/components/01.atoms/navigation/skip-links/CONSUMER-STYLING.md +65 -19
  51. package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +20 -14
  52. package/app/components/01.atoms/navigation/skip-links/stories/SkipLinks.stories.ts +89 -20
  53. package/app/components/01.atoms/navigation/skip-links/tests/SkipLinks.spec.ts +70 -10
  54. package/app/components/02.molecules/display-tooltip-rich/CONSUMER-STYLING.md +110 -0
  55. package/app/components/02.molecules/display-tooltip-rich/DisplayTooltipRich.vue +148 -0
  56. package/app/components/02.molecules/display-tooltip-rich/stories/DisplayTooltipRich.stories.ts +254 -0
  57. package/app/components/02.molecules/display-tooltip-rich/tests/DisplayTooltipRich.spec.ts +156 -0
  58. package/app/components/02.molecules/display-tooltip-rich/tests/__snapshots__/DisplayTooltipRich.spec.ts.snap +22 -0
  59. package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +1 -0
  60. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +1 -1
  61. package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +1 -0
  62. package/app/components/05.forms/form-field/CONSUMER-STYLING.md +7 -0
  63. package/app/components/05.forms/form-field/stories/FormField.stories.ts +116 -2
  64. package/app/components/05.forms/form-fieldset/CONSUMER-STYLING.md +11 -1
  65. package/app/components/05.forms/form-fieldset/FormFieldset.vue +4 -2
  66. package/app/components/05.forms/form-fieldset/stories/FormFieldset.stories.ts +68 -4
  67. package/app/components/05.forms/form-fieldset/tests/FormFieldset.spec.ts +10 -0
  68. package/app/composables/tests/useTooltips.spec.ts +3 -3
  69. package/app/types/components/index.ts +1 -1
  70. package/package.json +1 -1
  71. package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +0 -29
  72. package/app/components/02.molecules/display-tooltip-defined/CONSUMER-STYLING.md +0 -53
  73. package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +0 -117
  74. package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +0 -129
  75. package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +0 -85
  76. package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +0 -17
  77. /package/app/types/components/{display-tooltip-defined.d.ts → display-tooltip-rich.d.ts} +0 -0
@@ -10,8 +10,9 @@ type: reference
10
10
 
11
11
  `MarqueeScroller` renders an infinite horizontal scroller for small repeating items — client
12
12
  logos, badges, testimonial avatars. The track is duplicated once so the loop is seamless
13
- (`marquee-group` renders twice: once real, once `aria-hidden="true"`), and the whole thing pauses
14
- automatically on hover, on keyboard focus, and when the user has `prefers-reduced-motion` set.
13
+ (`.marquee-scroller-group` renders twice: once real, once `aria-hidden="true"` and `inert`), and the
14
+ whole thing pauses automatically on hover, whenever focus is inside it, and when the user has
15
+ `prefers-reduced-motion` set.
15
16
 
16
17
  ## Props
17
18
 
@@ -24,11 +25,12 @@ automatically on hover, on keyboard focus, and when the user has `prefers-reduce
24
25
  | `ariaLabel` | `string` | `"Scrolling content"` | `aria-label` on the root region — override for localisation. |
25
26
  | `ariaDescription` | `string` | `"Use spacebar to pause or play the animation."` | Screen-reader-only instructions rendered inside the region — override for localisation. |
26
27
  | `showControls` | `boolean` | `false` | Shows a visible pause/play button. Recommended (WCAG 2.2.2, Pause/Stop/Hide) whenever content scrolls automatically and indefinitely. |
27
- | `respectReducedMotion` | `boolean` | `true` | Auto-pauses and disables the animation for users with `prefers-reduced-motion: reduce`, and pauses on keyboard focus. |
28
+ | `respectReducedMotion` | `boolean` | `true` | Auto-pauses and disables the animation for users with `prefers-reduced-motion: reduce` (and keeps it paused after focus leaves). Focus-pausing no longer depends on it. |
28
29
  | `playIcon` | `string` | `"mdi:play"` | Iconify icon name shown on the control button while paused. Ignored if the `toggle-icon` slot is used. |
29
30
  | `pauseIcon` | `string` | `"mdi:pause"` | Iconify icon name shown on the control button while playing. Ignored if the `toggle-icon` slot is used. |
30
- | `playLabel` | `string` | `"Play animation"` | Control button `aria-label` while paused — override for localisation. |
31
- | `pauseLabel` | `string` | `"Pause animation"` | Control button `aria-label` while playing — override for localisation. |
31
+ | `playLabel` | `string` | `"Play animation"` | Control button text (visually hidden, its accessible name) while paused — override for localisation. |
32
+ | `pauseLabel` | `string` | `"Pause animation"` | Control button text (visually hidden, its accessible name) while playing — override for localisation. |
33
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root `.marquee-scroller`, via `useStyleClassPassthrough` (reacts to prop changes). |
32
34
 
33
35
  Import the item types from the library's type exports when building `marqueeData`/`itemConfig`
34
36
  outside the component:
@@ -52,8 +54,10 @@ One dynamically-named slot per `marqueeData` entry, named after that item's `id`
52
54
  </MarqueeScroller>
53
55
  ```
54
56
 
55
- Each slot is rendered twice (once in the visible group, once in the `aria-hidden` duplicate group)
56
- so the loop wraps seamlessly — don't put anything with side effects or unique ids inside a slot.
57
+ Each slot is rendered twice (once in the visible group, once in the `aria-hidden`, `inert`
58
+ duplicate group) so the loop wraps seamlessly. Don't put anything with side effects or unique ids
59
+ inside a slot. Links are fine: only the real copy is focusable. Items are fixed-size boxes; text
60
+ wraps inside them (`overflow-wrap: anywhere`) but content taller than `itemConfig.height` overflows.
57
61
 
58
62
  `toggle-icon` (scoped with `{ isPaused: boolean }`) replaces the control button's icon entirely —
59
63
  use this instead of `playIcon`/`pauseIcon` when an Iconify name isn't enough (custom SVG, a
@@ -70,16 +74,25 @@ different icon set):
70
74
  ## Accessibility behaviour
71
75
 
72
76
  - The root is `role="region"` with `tabindex="0"` so it's independently focusable.
73
- - Spacebar toggles pause/play while the region is focused. Arrow keys do nothing — there is no
77
+ - Spacebar toggles pause/play while the region itself is focused (keydown from inside, e.g. on the
78
+ control button or a link, is left to that element). Arrow keys do nothing — there is no
74
79
  manual-stepping behaviour, so the default `ariaDescription` doesn't claim one (fixed 2026-09-08;
75
80
  it previously announced "arrow keys ... for manual control" with no implementation behind it).
76
- - Focusing the region pauses the animation when `respectReducedMotion` is `true` (the default);
77
- blurring resumes it unless the user prefers reduced motion.
81
+ - Focus anywhere inside the region (the region itself, or a link in an item) pauses the animation
82
+ and removes `aria-hidden` from the track, so the focused content is announced. Focus moving between
83
+ elements inside keeps it paused. When focus leaves, it resumes only if focus caused the pause and
84
+ the user doesn't prefer reduced motion: a pause chosen with the button or spacebar sticks.
85
+ Focus landing on the control button doesn't auto-pause, so one click pauses. (Changed 2026-10-17:
86
+ this used to listen for `focus`/`blur` on the region only, gated on `respectReducedMotion`, so
87
+ tabbing to a link in an item left it moving and `aria-hidden`.)
78
88
  - Hovering anywhere over the scroller pauses the track and applies
79
89
  `--marquee-scroller-group-hover-filter` (default `grayscale(1)`) to every item; hovering an
80
90
  individual item restores it via `--marquee-scroller-item-hover-filter` (default `grayscale(0)`).
81
91
  - `aria-live` switches to `"polite"` while paused so screen readers announce the paused content.
82
- - The duplicate track group is always `aria-hidden="true"`.
92
+ - The track is `aria-hidden="true"` while playing (attribute removed when paused). The duplicate
93
+ group is always `aria-hidden="true"` and `inert`, so its copies of any links can't be focused.
94
+ - The control button is an `InputButton` (`variant="inline"`, icon-only): `playLabel`/`pauseLabel`
95
+ is its visually hidden text rather than an `aria-label`.
83
96
  - All user-visible copy (`ariaLabel`, `ariaDescription`, `playLabel`, `pauseLabel`) is a plain
84
97
  string prop with an English default — there's no i18n framework dependency in this library, so
85
98
  pass translated strings from the consumer's own i18n solution.
@@ -110,11 +123,11 @@ theme); only its border width is (`--marquee-scroller-control-border-width`).
110
123
 
111
124
  ## Seamless loop math
112
125
 
113
- The track duplicates `marqueeData` into two `.marquee-group` children so the loop can wrap without
126
+ The track duplicates `marqueeData` into two `.marquee-scroller-group` children so the loop can wrap without
114
127
  a visible cut. The animation must shift the track by exactly one group's width plus its connecting
115
128
  gap — not `translateX(-50%)`, which overshoots short of the seam once the track's own `gap`
116
129
  between the two groups is added to its total width. This is computed as a private
117
- `--_track-shift: calc(itemCount * (itemWidth + gap))` token and used in the `marqueeMove`
130
+ `--_track-shift: calc(itemCount * (itemWidth + gap))` token and used in the `marquee-scroller-move`
118
131
  keyframe. Fixed 2026-09-08 — the original `-50%` version produced a small but visible gap/overlap
119
132
  at the loop point once more than a couple of items were in `marqueeData`.
120
133
 
@@ -125,6 +138,30 @@ section on a wide viewport), the loop briefly shows empty space before snapping
125
138
  the visible track only spans as wide as the content, not the container. On mount (and on
126
139
  container resize, via `ResizeObserver`), the component measures one rendered group's `scrollWidth`
127
140
  against the root element's `offsetWidth` and repeats `marqueeData` internally (`repeatCount`)
128
- enough times per `.marquee-group` so a single group's width always covers the container. This is
141
+ enough times per `.marquee-scroller-group` so a single group's width always covers the container. This is
129
142
  transparent to consumers — it doesn't change `marqueeData` or any prop, only how many times each
130
143
  item's slot is rendered internally. Fixed 2026-09-08.
144
+
145
+ The repeat count is capped at 50 copies per group, so tiny items in a huge container can't flood
146
+ the DOM (added 2026-10-17).
147
+
148
+ ## Storybook
149
+
150
+ `Atoms/Effects/MarqueeScroller`: `WithControls`, `FocusableItems` (links in items, for the focus
151
+ behaviour), and `StressTest` (24 items of hostile copy, a broken image, long localised labels, a
152
+ single item, tiny items hitting the repeat cap, and empty data). Stories use the `CanvasSwitcher`
153
+ decorator.
154
+
155
+ ## History
156
+
157
+ **2026-10-17** (stress-test pass, breaking class renames):
158
+
159
+ - Structural classes prefixed (`.marquee-scroller-item`, `-control`, `-track`, `-group`,
160
+ `-description`); bare `.reverse`/`.paused`/`.reduced-motion` became `data-*` attributes; keyframes
161
+ renamed `marquee-scroller-move`. See CONSUMER-STYLING.md for the full map.
162
+ - Raw `<button>` replaced with `InputButton variant="inline"` (icon-only slot).
163
+ - Focus-pausing reworked (focusin/focusout, see Accessibility behaviour); duplicate group `inert`.
164
+ - `prefers-reduced-motion` change listener is now removed on unmount.
165
+ - New `--marquee-scroller-focus-outline-colour` token (was bare `--theme-ring`).
166
+ - Added `styleClassPassthrough`. Removed the redundant `aspect-ratio: 1 / 1` on items (width and
167
+ height are both set, and it mis-centres on old Safari). Items wrap unbroken text.
@@ -59,6 +59,17 @@ in `.skip-links__home`. Only rendered when the slot is actually used.
59
59
  Each `href` must match the `id` of a real element on the page (e.g. `<main id="main-content">`) —
60
60
  the component does not create those anchors itself.
61
61
 
62
+ Data handling (safe to feed straight from a CMS):
63
+
64
+ - An entry with an empty or whitespace-only `href` or `label` is dropped: an empty link has no
65
+ target or no accessible name.
66
+ - When no entry survives, the `<nav>` isn't rendered at all, so there's no empty landmark.
67
+ - Two entries may share an `href`; each renders (keys include the index). Before 2026-10-18 they
68
+ were keyed by `href` alone, and reordering such a list left a stale extra link in the DOM.
69
+ - Labels render as text and wrap; the panel never exceeds the viewport width.
70
+
71
+ `SiteHeader` renders `SkipLinks` as its branding row, passing its `branding` slot into `homeLink`.
72
+
62
73
  ## Accessibility behaviour
63
74
 
64
75
  - Links are hidden via `opacity`/`transform`, not `display`/`visibility` — they stay in the tab
@@ -80,3 +91,8 @@ See `CONSUMER-STYLING.md` in this component's folder for the full `--skip-links-
80
91
  - Migrated 2026-09-15 from the unplaced `skip-links/` folder: the link set was previously
81
92
  hardcoded (two fixed `<a>` tags with hardcoded English copy) — now data-driven via `links` so
82
93
  consumers can add/remove targets and localise the copy without forking the component.
94
+ - 2026-10-18 migration: colours moved from hardcoded black/white to `--theme-surface-inverted` and
95
+ `--theme-on-surface` defaults (same high contrast, follows `data-theme`); z-index default
96
+ `1000` → `999999` (library overlay default); links wrap instead of `nowrap`, and the panel is
97
+ capped at the viewport width and anchored with `inset-inline-start`; invalid entries dropped, no
98
+ empty landmark, index-safe keys; StressTest story and a link to the SiteHeader story.
@@ -8,7 +8,7 @@
8
8
 
9
9
  ## Prerequisites
10
10
 
11
- - Built around the native Popover API (Chrome 114+, Firefox 125+, Safari 17+). With `DisplayTooltip`/`DisplayTooltipDefined` it also works on Safari 16, because those components fall back to JS open/close (`useAnchoredPopover`) and the guide drives them through their trigger buttons. Hand-written raw `[popover]` markup like the example below still needs the Popover API.
11
+ - Built around the native Popover API (Chrome 114+, Firefox 125+, Safari 17+). With `DisplayTooltip`/`DisplayTooltipRich` it also works on Safari 16, because those components fall back to JS open/close (`useAnchoredPopover`) and the guide drives them through their trigger buttons. Hand-written raw `[popover]` markup like the example below still needs the Popover API.
12
12
  - Popovers must have `id` attributes and corresponding trigger buttons with `popovertarget` and `popovertargetaction="toggle"` attributes
13
13
  - Close buttons inside each popover must have `popovertargetaction="hide"`
14
14
 
@@ -91,13 +91,13 @@ await startGuide()
91
91
  | `stopGuide()` | `() => void` | Close any open popovers and stop the guide |
92
92
  | `initializePopovers()` | `() => void` | Re-scan the container for `[popover]` elements; call if popovers are added dynamically |
93
93
 
94
- ## Example with DisplayTooltip / DisplayTooltipDefined
94
+ ## Example with DisplayTooltip / DisplayTooltipRich
95
95
 
96
96
  [`DisplayTooltip`](components/display-tooltip.md) and
97
- [`DisplayTooltipDefined`](components/display-tooltip-defined.md) already emit the
97
+ [`DisplayTooltipRich`](components/display-tooltip-rich.md) already emit the
98
98
  `[popover]`/`popovertarget`/`popovertargetaction` markup this composable expects, so no extra
99
99
  wiring is needed beyond passing their shared container's ref in. See the `GuidedTour` story in
100
- `DisplayTooltipDefined.stories.ts` for a full working example — it recreates the old
100
+ `DisplayTooltipRich.stories.ts` for a full working example — it recreates the old
101
101
  `app/pages/ui/tooltips.vue` demo page (removed 2026-08-23 when Storybook became the only demo
102
102
  surface), mixing one of each component as guide steps.
103
103
 
@@ -97,19 +97,19 @@ Each skill is a single markdown file named `<area>-<task>.md`.
97
97
  ├── service-summary.md — ServiceSummary props, summary-link slot, DisplayPill duration/price (renamed/stripped from ServicesSection — full mode is now ServiceDetail)
98
98
  ├── service-detail.md — ServiceDetail: full service detail page (hero banner, sticky sidebar booking card + related services, closing CTA), headerTag vs subheadingTag split, book-cta/sidebar-note/related-service/final-cta slots
99
99
  ├── breadcrumb.md — Breadcrumb: items (BreadcrumbItem[]) trail, link vs current-page text, CSS token API
100
- ├── skip-links.md — SkipLinks: focus-revealed accessibility skip-nav, links (SkipLink[]) data-driven, homeLink slot, CSS token API
100
+ ├── skip-links.md — SkipLinks: focus-revealed accessibility skip-nav, links (SkipLink[]) data-driven, homeLink slot, empty entries dropped, index-safe keys, theme-token colours, viewport-capped wrapping panel, CSS token API
101
101
  ├── tabbed-content.md — TabbedContent (was TabsCore): tablist + panels from indexed slots (itemCount), arrow-key nav, moving hover/active/underline indicators, CSS token API
102
102
  ├── contact-section.md — ContactSection props (stepperIndicatorSize pass-through), 3-item info+form layout, slot API
103
103
  ├── stepper-list.md — StepperList dynamic slots (item-{n}/indicator-{n}), props, measured connectors (gap, dashed image tokens), per-step state recipe
104
104
  ├── progress-tracker.md — ProgressTracker: steps (ProgressTrackerStep[]) + v-model current index over StepperList, header label + progressbar, current-step #step-{n} slot for wizard forms, focus on change, CSS token API
105
105
  ├── expanding-panel.md — ExpandingPanel v-model, forceOpened, contentIsOnTop overlay mode, slots (summary/icon/content), ARIA wiring, CSS token API
106
- ├── content-docs.md — ContentDocs docs-page shell: prop-driven docsNav/docsPageNav (not slots), DocsNavItem icons, container-width breakpoint behaviour, shared/per-side CSS token API
106
+ ├── content-docs.md — ContentDocs docs-page shell: prop-driven docsNav/docsPageNav (not slots), DocsNavItem icons, container-width breakpoint behaviour, shared/per-side CSS token API, content-docs-* classes, aria-current active state
107
107
  ├── glass-panel.md — GlassPanel: frosted-glass container, light-only token defaults in-component (light-dark() block removed from _default.css, no built-in dark glass), highlight behind content, section/article labelled via headingId slot prop, CSS token API
108
108
  ├── pricing-card.md — PricingCard: plan card with highlight badge, corner ribbon, feature list, #cta slot, heading-tag, line-clamp tokens
109
109
  ├── price-list.md — PriceList: service/menu price columns (dl rows), PriceListData type, headingTag/fromLabel props, CSS token API
110
110
  ├── opening-hours.md — OpeningHours: per-day data auto-grouped into ranges, split sessions, 24h/appointment, exceptions, Intl formatting, today highlight, JSON-LD, CSS token API
111
111
  ├── input-copy.md — InputCopy: readonly copy-to-clipboard input with an inline InputButton, copied state + status announcement, translatable labels/icons, container-query icon-only button, --input-copy-* tokens
112
- ├── banner-video.md — BannerVideo: full-width hero video banner, depth tier system, objectFit/objectPosition, playIcon/pauseIcon/toggle-icon slot, reduced-motion fallback, CSS tokens
112
+ ├── banner-video.md — BannerVideo: full-width hero video banner, depth tier system, objectFit/objectPosition, playIcon/pauseIcon/toggle-icon slot, playLabel/pauseLabel, reduced-motion fallback, CSS tokens
113
113
  ├── grid-stack.md — GridStack: CSS Grid z-axis stacking, slot API, z-order rules, sizing, video+overlay and image+text patterns
114
114
  ├── scroll-reveal-frame.md — ScrollRevealFrame: generic parallax clipping frame, slot API, image grid pattern, public --scroll-reveal-frame-* tokens (props only write them when passed), browser support
115
115
  ├── scroll-reveal-image.md — ScrollRevealImage: single-image parallax reveal, imgWidth/imgHeight, public focal-x/focal-y crop tokens, frame tokens settable from CSS
@@ -127,7 +127,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
127
127
  ├── card-core.md — CardCore: generic card container, dynamic named slots as rows, 4 variants, blurred backdrop layer, full CSS token API
128
128
  ├── action-menu.md — ActionMenu + ActionMenuItem: ellipsis trigger + anchored popover menu, indexed item-{n} slots, link/button items, full CSS token API
129
129
  ├── select-menu.md — SelectMenu: v-model single-select listbox popover (ActionMenu's popover mechanics, InputSelect's selected-option semantics), icon/text/chevron trigger toggles, checkmark on selected option, options-array driven, full CSS token API
130
- ├── display-dialog.md — DisplayDialog: native <dialog> overlay, 5 variants (dialog/modal/confirm/alert/fullscreen), useDialogControls integration, CSS token API
130
+ ├── display-dialog.md — DisplayDialog: native <dialog> overlay, 5 variants (dialog/modal/confirm/alert/fullscreen), useDialogControls integration, InputButton close button, viewport-capped panel with wrapping title/content/footer, aria-label fallthrough when untitled, CSS token API
131
131
  ├── display-chip.md — DisplayChip: status indicator chip overlay, CSS trig positioning, circle/square shapes, status colours, icon/label content
132
132
  ├── display-pill.md — DisplayPill: pill/badge label with icon slot, 6 variants, 3 sizes, reversible order, --display-pill-* tokens (variants fall back to base colour/border/ring tokens); data-size/data-variant/data-reversed hooks (2026-10-08, were bare classes), label line-clamp token, -02 ring and outlined-monochrome recipes, StressTest story
133
133
  ├── carousel-flip.md — CarouselFlip: FLIP-animated carousel, carouselDataIds slot API, buttonLayout variants (sides/controls-flanking/controls-grouped-right/overlay), CSS tokens
@@ -142,8 +142,8 @@ Each skill is a single markdown file named `<area>-<task>.md`.
142
142
  ├── display-banner.md — DisplayBanner: canvas/content stacked overlay banner, conditional slot wrappers, CSS token API
143
143
  ├── deep-expanding-menu.md — DeepExpandingMenu: anchor-positioned popover nav panels, browser support caveat, CSS token API
144
144
  ├── deep-expanding-menu-classic.md — DeepExpandingMenuClassic: <details>-based fallback nav, click-outside close, CSS token API
145
- ├── display-tooltip.md — DisplayTooltip: anchor-positioned popover trigger, browser support caveat, CSS token API
146
- ├── display-tooltip-defined.md — DisplayTooltipDefined: structured title/body/action tooltip content with close button, composes DisplayTooltip
145
+ ├── display-tooltip.md — DisplayTooltip: anchor-positioned popover trigger, browser support caveat, triggerIcon prop, type="button" trigger, hideTrigger removes it from tab order (data-hidden), CSS token API
146
+ ├── display-tooltip-rich.md — DisplayTooltipRich (Material 3 rich tooltip, renamed from DisplayTooltipDefined 2026-10-18): structured title/body/action content with InputButton close button, per-field line-clamp tokens, composes DisplayTooltip
147
147
  ├── pop-over.md — PopOver: generic anchor-positioned disclosure panel, consumer-supplied trigger/content slots, placement prop, CSS token API
148
148
  ├── input-text.md — InputText (bare control), InputTextField, InputPasswordField, InputTextAsNumberField: renamed from Core/WithLabel, data-* state hooks, readonly/autocomplete props, leak-free digit filtering, wrapper attr-leak/empty-description/passthrough fixes, rounded+clamped stepping, min/max pass-through, CSS token API, stress stories
149
149
  ├── input-button.md — InputButton (renamed from InputButtonCore 2026-09-28): button/NuxtLink/a; primary filled, secondary outlined, tertiary text-only, inline unstyled; data-* state hooks; shadow ring + border-colour hover; wrapping labels; icon slots; readonly (aria-disabled, activation blocked); tokens
@@ -338,6 +338,14 @@ const ids = host.findAll("input").map((input) => input.attributes("id"));
338
338
  expect(new Set(ids).size).toBe(ids.length);
339
339
  ```
340
340
 
341
+ ## Testing duplicate `v-for` keys
342
+
343
+ Duplicate keys don't show on first render, and Vue only warns (and only corrupts the DOM) inside
344
+ the keyed diff's unsorted middle section. A reorder that keeps the first or last item in place can
345
+ be resolved from the ends and never reaches it. Mount with two items sharing the repeatable field,
346
+ then `setProps` with the first and last items swapped, and assert the rendered labels in order
347
+ (`SkipLinks.spec.ts`). With an `href`-only key that test rendered five links instead of four.
348
+
341
349
  ## Mocking browser APIs
342
350
 
343
351
  ### Global constructors (ResizeObserver, IntersectionObserver, etc.)
@@ -4,12 +4,24 @@
4
4
  "scope": "vue,html",
5
5
  "body": [
6
6
  "<BannerVideo",
7
- " src=\"$1/videos/hero.mp4\"",
8
- " poster=\"$2/images/hero-poster.jpg\"",
9
- " alt=\"$3Studio interior\"",
10
- " :img-width=\"1920\"",
11
- " :img-height=\"1080\"",
12
- " depth=\"$4md\"",
7
+ " src=\"${1:/videos/hero.mp4}\"",
8
+ " poster=\"${2:/images/hero-poster.jpg}\"",
9
+ " alt=\"${3:Studio interior}\"",
10
+ " :img-width=\"${4:1920}\"",
11
+ " :img-height=\"${5:1080}\"",
12
+ " depth=\"${6|md,xs,sm,lg,xl|}\"",
13
+ "/>"
14
+ ]
15
+ },
16
+ "SRCDEV BannerVideo Localised Labels": {
17
+ "description": "BannerVideo with translated pause/play toggle labels (the button's visually hidden accessible name)",
18
+ "scope": "vue,html",
19
+ "body": [
20
+ "<BannerVideo",
21
+ " src=\"${1:/videos/hero.mp4}\"",
22
+ " poster=\"${2:/images/hero-poster.jpg}\"",
23
+ " :play-label=\"${3:t('banner.playVideo')}\"",
24
+ " :pause-label=\"${4:t('banner.pauseVideo')}\"",
13
25
  "/>"
14
26
  ]
15
27
  },
@@ -18,10 +30,10 @@
18
30
  "scope": "vue,html",
19
31
  "body": [
20
32
  "<BannerVideo",
21
- " src=\"$1/videos/hero.mp4\"",
22
- " poster=\"$2/images/hero-poster.jpg\"",
23
- " play-icon=\"$3mdi:play-circle\"",
24
- " pause-icon=\"$4mdi:pause-circle\"",
33
+ " src=\"${1:/videos/hero.mp4}\"",
34
+ " poster=\"${2:/images/hero-poster.jpg}\"",
35
+ " play-icon=\"${3:mdi:play-circle}\"",
36
+ " pause-icon=\"${4:mdi:pause-circle}\"",
25
37
  "/>"
26
38
  ]
27
39
  },
@@ -29,7 +41,7 @@
29
41
  "description": "BannerVideo with the toggle-icon slot fully replacing the toggle button's icon",
30
42
  "scope": "vue,html",
31
43
  "body": [
32
- "<BannerVideo src=\"$1/videos/hero.mp4\" poster=\"$2/images/hero-poster.jpg\">",
44
+ "<BannerVideo src=\"${1:/videos/hero.mp4}\" poster=\"${2:/images/hero-poster.jpg}\">",
33
45
  " <template #toggle-icon=\"{ isPlaying }\">",
34
46
  " $3",
35
47
  " </template>",
@@ -37,22 +49,23 @@
37
49
  ]
38
50
  },
39
51
  "SRCDEV BannerVideo CSS Override": {
40
- "description": "CSS override scaffold for BannerVideo — scope to a page or section class",
52
+ "description": "CSS override scaffold for BannerVideo — set tokens on a page or section class you own",
41
53
  "scope": "css",
42
54
  "body": [
43
- ".$1my-page {",
44
- " .banner-video {",
45
- " --banner-video-max-height-xs: ;",
46
- " --banner-video-max-height-sm: ;",
47
- " --banner-video-max-height-md: ;",
48
- " --banner-video-max-height-lg: ;",
49
- " --banner-video-max-height-xl: ;",
50
- " --banner-video-toggle-size: ;",
51
- " --banner-video-toggle-offset: ;",
52
- " --banner-video-toggle-background: ;",
53
- " --banner-video-toggle-background-hover: ;",
54
- " --banner-video-toggle-icon-color: ;",
55
- " }",
55
+ ".${1:my-page} {",
56
+ " --banner-video-max-height-xs: ;",
57
+ " --banner-video-max-height-sm: ;",
58
+ " --banner-video-max-height-md: ;",
59
+ " --banner-video-max-height-lg: ;",
60
+ " --banner-video-max-height-xl: ;",
61
+ " --banner-video-toggle-size: ;",
62
+ " --banner-video-toggle-offset: ;",
63
+ " --banner-video-toggle-background: ;",
64
+ " --banner-video-toggle-background-hover: ;",
65
+ " --banner-video-toggle-icon-color: ;",
66
+ " --banner-video-toggle-focus-outline-width: ;",
67
+ " --banner-video-toggle-focus-outline-color: ;",
68
+ " --banner-video-toggle-focus-outline-offset: ;",
56
69
  "}"
57
70
  ]
58
71
  }
@@ -4,49 +4,51 @@
4
4
  "scope": "vue,html",
5
5
  "body": [
6
6
  "<ContentDocs",
7
- " v-model:active-nav-item=\"$1activeNavItem\"",
8
- " v-model:active-page-nav-item=\"$2activePageNavItem\"",
9
- " :docs-nav-items=\"$3docsNavItems\"",
10
- " :docs-page-nav-items=\"$4docsPageNavItems\"",
7
+ " v-model:active-nav-item=\"${1:activeNavItem}\"",
8
+ " v-model:active-page-nav-item=\"${2:activePageNavItem}\"",
9
+ " :docs-nav-items=\"${3:docsNavItems}\"",
10
+ " :docs-page-nav-items=\"${4:docsPageNavItems}\"",
11
11
  ">",
12
12
  " <template #docsContent>",
13
- " <p>$5Page content goes here.</p>",
13
+ " <p>${5:Page content goes here.}</p>",
14
14
  " </template>",
15
15
  "</ContentDocs>"
16
16
  ]
17
17
  },
18
- "SRCDEV ContentDocs Custom Labels" : {
18
+ "SRCDEV ContentDocs Custom Labels": {
19
19
  "description": "ContentDocs with custom docsNav / docsPageNav panel headings",
20
20
  "scope": "vue,html",
21
21
  "body": [
22
22
  "<ContentDocs",
23
- " v-model:active-nav-item=\"$1activeNavItem\"",
24
- " v-model:active-page-nav-item=\"$2activePageNavItem\"",
25
- " :docs-nav-items=\"$3docsNavItems\"",
26
- " :docs-page-nav-items=\"$4docsPageNavItems\"",
27
- " docs-nav-label=\"$5Sections\"",
28
- " docs-page-nav-label=\"$6Contents\"",
23
+ " v-model:active-nav-item=\"${1:activeNavItem}\"",
24
+ " v-model:active-page-nav-item=\"${2:activePageNavItem}\"",
25
+ " :docs-nav-items=\"${3:docsNavItems}\"",
26
+ " :docs-page-nav-items=\"${4:docsPageNavItems}\"",
27
+ " docs-nav-label=\"${5:Sections}\"",
28
+ " docs-page-nav-label=\"${6:Contents}\"",
29
29
  ">",
30
30
  " <template #docsContent>",
31
- " <p>$7Page content goes here.</p>",
31
+ " <p>${7:Page content goes here.}</p>",
32
32
  " </template>",
33
33
  "</ContentDocs>"
34
34
  ]
35
35
  },
36
36
  "SRCDEV ContentDocs Localised Nav Labels": {
37
- "description": "ContentDocs with localised aria-labels on the docsNav/docsPageNav <nav> landmarks",
37
+ "description": "ContentDocs with localised headings and aria-labels on the docsNav/docsPageNav <nav> landmarks",
38
38
  "scope": "vue,html",
39
39
  "body": [
40
40
  "<ContentDocs",
41
- " v-model:active-nav-item=\"$1activeNavItem\"",
42
- " v-model:active-page-nav-item=\"$2activePageNavItem\"",
43
- " :docs-nav-items=\"$3docsNavItems\"",
44
- " :docs-page-nav-items=\"$4docsPageNavItems\"",
45
- " docs-nav-aria-label=\"${5:Docs navigation}\"",
46
- " docs-page-nav-aria-label=\"${6:On this page}\"",
41
+ " v-model:active-nav-item=\"${1:activeNavItem}\"",
42
+ " v-model:active-page-nav-item=\"${2:activePageNavItem}\"",
43
+ " :docs-nav-items=\"${3:docsNavItems}\"",
44
+ " :docs-page-nav-items=\"${4:docsPageNavItems}\"",
45
+ " :docs-nav-label=\"${5:t('docs.navLabel')}\"",
46
+ " :docs-page-nav-label=\"${6:t('docs.pageNavLabel')}\"",
47
+ " :docs-nav-aria-label=\"${7:t('docs.navAriaLabel')}\"",
48
+ " :docs-page-nav-aria-label=\"${8:t('docs.pageNavAriaLabel')}\"",
47
49
  ">",
48
50
  " <template #docsContent>",
49
- " <p>$7Page content goes here.</p>",
51
+ " <p>${9:Page content goes here.}</p>",
50
52
  " </template>",
51
53
  "</ContentDocs>"
52
54
  ]
@@ -56,11 +58,11 @@
56
58
  "scope": "typescript,vue",
57
59
  "body": [
58
60
  "const docsNavItems: DocsNavItem[] = [",
59
- " { label: \"$1Getting started\", to: \"$2/docs\" },",
61
+ " { label: \"${1:Getting started}\", to: \"${2:/docs}\" },",
60
62
  "];",
61
63
  "",
62
64
  "const docsPageNavItems: DocsNavItem[] = [",
63
- " { label: \"$3Overview\", to: \"$2/docs#overview\" },",
65
+ " { label: \"${3:Overview}\", to: \"${2:/docs}#overview\" },",
64
66
  "];",
65
67
  "",
66
68
  "const activeNavItem = ref<string | undefined>(docsNavItems[0]?.to);",
@@ -72,11 +74,11 @@
72
74
  "scope": "typescript,vue",
73
75
  "body": [
74
76
  "const docsNavItems: DocsNavItem[] = [",
75
- " { label: \"$1Getting started\", to: \"$2/docs\", icon: \"$3lucide:rocket\" },",
77
+ " { label: \"${1:Getting started}\", to: \"${2:/docs}\", icon: \"${3:lucide:rocket}\" },",
76
78
  "];",
77
79
  "",
78
80
  "const docsPageNavItems: DocsNavItem[] = [",
79
- " { label: \"$4Overview\", to: \"$2/docs#overview\", icon: \"$5lucide:list\" },",
81
+ " { label: \"${4:Overview}\", to: \"${2:/docs}#overview\", icon: \"${5:lucide:list}\" },",
80
82
  "];",
81
83
  "",
82
84
  "const activeNavItem = ref<string | undefined>(docsNavItems[0]?.to);",
@@ -87,13 +89,11 @@
87
89
  "description": "CSS override scaffold for ContentDocs nav-link icon tokens (gap, size, and start/end order)",
88
90
  "scope": "css",
89
91
  "body": [
90
- ".$1my-content-docs {",
91
- " .content-docs {",
92
- " --content-docs-link-icon-gap: $2;",
93
- " --content-docs-link-icon-size: $3;",
94
- " --content-docs-nav-link-icon-order: $4rtl; /* icon at the end */",
95
- " --content-docs-page-nav-link-icon-order: $5ltr; /* icon at the start (default) */",
96
- " }",
92
+ ".${1:my-content-docs} {",
93
+ " --content-docs-link-icon-gap: $2;",
94
+ " --content-docs-link-icon-size: $3;",
95
+ " --content-docs-nav-link-icon-order: ${4:rtl}; /* icon at the end */",
96
+ " --content-docs-page-nav-link-icon-order: ${5:ltr}; /* icon at the start (default) */",
97
97
  "}"
98
98
  ]
99
99
  },
@@ -101,27 +101,26 @@
101
101
  "description": "CSS override scaffold for ContentDocs heading/panel/link tokens, applied to both docsNav and docsPageNav at once",
102
102
  "scope": "css",
103
103
  "body": [
104
- ".$1my-content-docs {",
105
- " .content-docs {",
106
- " --content-docs-heading-font-size: $2;",
107
- " --content-docs-heading-color: $3;",
108
- " --content-docs-heading-bg: $4;",
109
- " --content-docs-heading-padding-block: $5;",
104
+ ".${1:my-content-docs} {",
105
+ " --content-docs-heading-font-size: $2;",
106
+ " --content-docs-heading-color: $3;",
107
+ " --content-docs-heading-bg: $4;",
108
+ " --content-docs-heading-padding-block: $5;",
110
109
  "",
111
- " --content-docs-panel-bg: $6;",
112
- " --content-docs-panel-padding-block: $7;",
113
- " --content-docs-panel-padding-inline: $8;",
110
+ " --content-docs-panel-bg: $6;",
111
+ " --content-docs-panel-padding-block: $7;",
112
+ " --content-docs-panel-padding-inline: $8;",
114
113
  "",
115
- " --content-docs-link-font-size: $9;",
116
- " --content-docs-link-color: $10;",
117
- " --content-docs-link-hover-bg: $11;",
118
- " --content-docs-link-active-bg: $12;",
119
- " --content-docs-link-active-color: $13;",
114
+ " --content-docs-link-font-size: $9;",
115
+ " --content-docs-link-color: $10;",
116
+ " --content-docs-link-hover-bg: $11;",
117
+ " --content-docs-link-active-bg: $12;",
118
+ " --content-docs-link-active-color: $13;",
119
+ " --content-docs-link-focus-outline-color: $14;",
120
120
  "",
121
- " --content-docs-nav-column-width: $14;",
122
- " --content-docs-page-nav-column-width: $15;",
123
- " --content-docs-page-nav-column-width-tablet: $16;",
124
- " }",
121
+ " --content-docs-nav-column-width: $15;",
122
+ " --content-docs-page-nav-column-width: $16;",
123
+ " --content-docs-page-nav-column-width-tablet: $17;",
125
124
  "}"
126
125
  ]
127
126
  },
@@ -129,17 +128,27 @@
129
128
  "description": "CSS override scaffold for independently theming docsNav vs docsPageNav heading/panel/link tokens",
130
129
  "scope": "css",
131
130
  "body": [
132
- ".$1my-content-docs {",
133
- " .content-docs {",
134
- " /* docsNav (primary) */",
135
- " --content-docs-nav-heading-color: $2;",
136
- " --content-docs-nav-panel-bg: $3;",
137
- " --content-docs-nav-link-active-bg: $4;",
131
+ ".${1:my-content-docs} {",
132
+ " /* docsNav (primary) */",
133
+ " --content-docs-nav-heading-color: $2;",
134
+ " --content-docs-nav-panel-bg: $3;",
135
+ " --content-docs-nav-link-active-bg: $4;",
138
136
  "",
139
- " /* docsPageNav (on-this-page / secondary) */",
140
- " --content-docs-page-nav-heading-color: $5;",
141
- " --content-docs-page-nav-panel-bg: $6;",
142
- " --content-docs-page-nav-link-active-bg: $7;",
137
+ " /* docsPageNav (on-this-page / secondary) */",
138
+ " --content-docs-page-nav-heading-color: $5;",
139
+ " --content-docs-page-nav-panel-bg: $6;",
140
+ " --content-docs-page-nav-link-active-bg: $7;",
141
+ "}"
142
+ ]
143
+ },
144
+ "SRCDEV ContentDocs CSS Override — Active Link": {
145
+ "description": "Style the active ContentDocs link beyond its tokens: select [aria-current=\"page\"], there is no .is-active class",
146
+ "scope": "css",
147
+ "body": [
148
+ ".${1:my-content-docs} {",
149
+ " .content-docs-nav-link[aria-current=\"page\"],",
150
+ " .content-docs-page-nav-link[aria-current=\"page\"] {",
151
+ " $2",
143
152
  " }",
144
153
  "}"
145
154
  ]
@@ -19,7 +19,7 @@
19
19
  ]
20
20
  },
21
21
  "SRCDEV DisplayDialog Confirm Template": {
22
- "description": "DisplayDialog confirm variant — content-sized, requires explicit action",
22
+ "description": "DisplayDialog confirm variant — compact (max 48rem), dismissable with Escape or outside-click",
23
23
  "scope": "vue,html",
24
24
  "body": [
25
25
  "<DisplayDialog",
@@ -114,10 +114,12 @@
114
114
  " --display-dialog-backdrop-blur: ;",
115
115
  " --display-dialog-transition-duration: ;",
116
116
  "",
117
- " .inner {",
118
- " .header {}",
119
- " .dialog-content {}",
120
- " .footer {}",
117
+ " --display-dialog-header-accent-color: ;",
118
+ "",
119
+ " .display-dialog-inner {",
120
+ " .display-dialog-header {}",
121
+ " .display-dialog-content {}",
122
+ " .display-dialog-footer {}",
121
123
  " }",
122
124
  " }",
123
125
  "}"
@@ -0,0 +1,50 @@
1
+ {
2
+ "SRCDEV DisplayTooltipRich Basic": {
3
+ "description": "DisplayTooltipRich — structured title/body/action tooltip",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<DisplayTooltipRich",
7
+ " :content-text=\"{",
8
+ " tooltipTitle: { tag: 'h4', text: '$1' },",
9
+ " tooltipContent: { tag: 'p', text: '$0' },",
10
+ " }\"",
11
+ "/>"
12
+ ]
13
+ },
14
+ "SRCDEV DisplayTooltipRich Localised Copy": {
15
+ "description": "DisplayTooltipRich with localised trigger/close-button copy and a custom trigger icon",
16
+ "scope": "vue,html",
17
+ "body": [
18
+ "<DisplayTooltipRich",
19
+ " :content-text=\"{",
20
+ " tooltipTitle: { tag: 'h4', text: '$1' },",
21
+ " tooltipContent: { tag: 'p', text: '$2' },",
22
+ " }\"",
23
+ " trigger-aria-label=\"${3:Toggle the popover}\"",
24
+ " close-button-text=\"${4:Close}\"",
25
+ " close-button-aria-label=\"${5:Close tool tip}\"",
26
+ " trigger-icon=\"${6:fa7-solid:circle-question}\"",
27
+ "/>"
28
+ ]
29
+ },
30
+ "SRCDEV DisplayTooltipRich CSS Override": {
31
+ "description": "CSS override scaffold for DisplayTooltipRich, scoped to a page or section class",
32
+ "scope": "css",
33
+ "body": [
34
+ ".${1:my-page} {",
35
+ " .display-tooltip-rich {",
36
+ " --display-tooltip-rich-content-gap: ;",
37
+ " --display-tooltip-rich-title-colour: ;",
38
+ " --display-tooltip-rich-title-font-size: ;",
39
+ " --display-tooltip-rich-title-line-clamp: ${2:none};",
40
+ " --display-tooltip-rich-body-font-size: ;",
41
+ " --display-tooltip-rich-body-line-clamp: ${3:none};",
42
+ " --display-tooltip-rich-action-colour: ;",
43
+ " --display-tooltip-rich-action-line-clamp: ${4:none};",
44
+ " --display-tooltip-popover-background-colour: ;",
45
+ " --display-tooltip-popover-text-colour: ;",
46
+ " }",
47
+ "}"
48
+ ]
49
+ }
50
+ }