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.
Files changed (111) hide show
  1. package/.claude/rules/consumer/ids-from-useid.md +2 -1
  2. package/.claude/rules/consumer/story-register-components.md +1 -1
  3. package/.claude/rules/consumer/vfor-keys-from-data.md +14 -0
  4. package/.claude/skills/component-compliance-checklist.md +24 -1
  5. package/.claude/skills/component-naming.md +1 -0
  6. package/.claude/skills/components/banner-video.md +24 -30
  7. package/.claude/skills/components/content-docs.md +18 -5
  8. package/.claude/skills/components/display-dialog.md +16 -6
  9. package/.claude/skills/components/display-qr-code.md +22 -4
  10. package/.claude/skills/components/{display-tooltip-defined.md → display-tooltip-rich.md} +40 -42
  11. package/.claude/skills/components/display-tooltip.md +17 -6
  12. package/.claude/skills/components/eyebrow-text.md +9 -5
  13. package/.claude/skills/components/header-block.md +13 -5
  14. package/.claude/skills/components/hero-text.md +13 -9
  15. package/.claude/skills/components/link-text.md +8 -5
  16. package/.claude/skills/components/pop-over.md +17 -3
  17. package/.claude/skills/components/skip-links.md +16 -0
  18. package/.claude/skills/composable-tooltips-guide.md +4 -4
  19. package/.claude/skills/index.md +6 -6
  20. package/.claude/skills/storybook-add-story.md +5 -2
  21. package/.claude/skills/testing-add-playwright.md +12 -7
  22. package/.claude/skills/testing-add-unit-test.md +8 -0
  23. package/.vscode/srcdev-component-banner-video.code-snippets +38 -25
  24. package/.vscode/srcdev-component-content-docs.code-snippets +69 -60
  25. package/.vscode/srcdev-component-display-dialog.code-snippets +7 -5
  26. package/.vscode/srcdev-component-display-qr-code.code-snippets +12 -1
  27. package/.vscode/srcdev-component-display-tooltip-rich.code-snippets +50 -0
  28. package/.vscode/srcdev-component-display-tooltip.code-snippets +31 -0
  29. package/.vscode/srcdev-component-hero-text.code-snippets +21 -5
  30. package/.vscode/srcdev-component-link-text.code-snippets +3 -3
  31. package/.vscode/srcdev-component-pop-over.code-snippets +28 -0
  32. package/.vscode/srcdev-component-skip-links.code-snippets +1 -1
  33. package/app/components/01.atoms/banner-video/BannerVideo.vue +51 -19
  34. package/app/components/01.atoms/banner-video/CONSUMER-STYLING.md +59 -40
  35. package/app/components/01.atoms/banner-video/stories/BannerVideo.stories.ts +88 -1
  36. package/app/components/01.atoms/banner-video/tests/BannerVideo.spec.ts +127 -78
  37. package/app/components/01.atoms/banner-video/tests/__snapshots__/BannerVideo.spec.ts.snap +12 -4
  38. package/app/components/01.atoms/content-wrappers/docs-pages/CONSUMER-STYLING.md +42 -14
  39. package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +86 -112
  40. package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +134 -8
  41. package/app/components/01.atoms/content-wrappers/docs-pages/tests/ContentDocs.spec.ts +131 -65
  42. package/app/components/01.atoms/display-dialog/CONSUMER-STYLING.md +28 -3
  43. package/app/components/01.atoms/display-dialog/DisplayDialog.vue +39 -31
  44. package/app/components/01.atoms/display-dialog/stories/DisplayDialog.stories.ts +92 -1
  45. package/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts +87 -41
  46. package/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap +5 -1
  47. package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +70 -24
  48. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +24 -12
  49. package/app/components/01.atoms/display-tooltip/stories/DisplayTooltip.stories.ts +89 -2
  50. package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +65 -18
  51. package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +3 -3
  52. package/app/components/01.atoms/navigation/skip-links/CONSUMER-STYLING.md +65 -19
  53. package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +20 -14
  54. package/app/components/01.atoms/navigation/skip-links/stories/SkipLinks.stories.ts +89 -20
  55. package/app/components/01.atoms/navigation/skip-links/tests/SkipLinks.spec.ts +70 -10
  56. package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +42 -9
  57. package/app/components/01.atoms/pop-over/PopOver.vue +28 -12
  58. package/app/components/01.atoms/pop-over/stories/PopOver.stories.ts +114 -2
  59. package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +35 -0
  60. package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +5 -1
  61. package/app/components/01.atoms/qr-code/CONSUMER-STYLING.md +16 -3
  62. package/app/components/01.atoms/qr-code/DisplayQrCode.vue +31 -0
  63. package/app/components/01.atoms/qr-code/stories/DisplayQrCode.stories.ts +190 -157
  64. package/app/components/01.atoms/qr-code/tests/DisplayQrCode.spec.ts +58 -0
  65. package/app/components/01.atoms/text-blocks/eyebrow-text/CONSUMER-STYLING.md +9 -1
  66. package/app/components/01.atoms/text-blocks/eyebrow-text/EyebrowText.vue +5 -4
  67. package/app/components/01.atoms/text-blocks/eyebrow-text/playwright/eyebrow-text.playwright.ts +1 -1
  68. package/app/components/01.atoms/text-blocks/eyebrow-text/stories/EyebrowText.stories.ts +104 -17
  69. package/app/components/01.atoms/text-blocks/eyebrow-text/tests/EyebrowText.spec.ts +68 -118
  70. package/app/components/01.atoms/text-blocks/eyebrow-text/tests/__snapshots__/EyebrowText.spec.ts.snap +8 -8
  71. package/app/components/01.atoms/text-blocks/header-block/CONSUMER-STYLING.md +49 -24
  72. package/app/components/01.atoms/text-blocks/header-block/HeaderBlock.vue +9 -1
  73. package/app/components/01.atoms/text-blocks/header-block/stories/HeaderBlock.stories.ts +96 -26
  74. package/app/components/01.atoms/text-blocks/header-block/tests/HeaderBlock.spec.ts +64 -48
  75. package/app/components/01.atoms/text-blocks/header-block/tests/__snapshots__/HeaderBlock.spec.ts.snap +2 -2
  76. package/app/components/01.atoms/text-blocks/hero-text/CONSUMER-STYLING.md +22 -6
  77. package/app/components/01.atoms/text-blocks/hero-text/HeroText.vue +32 -23
  78. package/app/components/01.atoms/text-blocks/hero-text/playwright/hero-text.playwright.ts +1 -1
  79. package/app/components/01.atoms/text-blocks/hero-text/stories/HeroText.stories.ts +176 -23
  80. package/app/components/01.atoms/text-blocks/hero-text/tests/HeroText.spec.ts +129 -123
  81. package/app/components/01.atoms/text-blocks/hero-text/tests/__snapshots__/HeroText.spec.ts.snap +2 -2
  82. package/app/components/01.atoms/text-blocks/link-text/CONSUMER-STYLING.md +17 -0
  83. package/app/components/01.atoms/text-blocks/link-text/LinkText.vue +9 -3
  84. package/app/components/01.atoms/text-blocks/link-text/stories/LinkText.stories.ts +96 -99
  85. package/app/components/01.atoms/text-blocks/link-text/tests/LinkText.spec.ts +84 -93
  86. package/app/components/02.molecules/display-tooltip-rich/CONSUMER-STYLING.md +110 -0
  87. package/app/components/02.molecules/display-tooltip-rich/DisplayTooltipRich.vue +148 -0
  88. package/app/components/02.molecules/display-tooltip-rich/stories/DisplayTooltipRich.stories.ts +254 -0
  89. package/app/components/02.molecules/display-tooltip-rich/tests/DisplayTooltipRich.spec.ts +156 -0
  90. package/app/components/02.molecules/display-tooltip-rich/tests/__snapshots__/DisplayTooltipRich.spec.ts.snap +22 -0
  91. package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +1 -0
  92. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +1 -1
  93. package/app/components/02.molecules/price-list/tests/__snapshots__/PriceList.spec.ts.snap +8 -8
  94. package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +1 -0
  95. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +15 -15
  96. package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +3 -3
  97. package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +3 -3
  98. package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +3 -3
  99. package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +1 -1
  100. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +9 -9
  101. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +9 -9
  102. package/app/composables/tests/useTooltips.spec.ts +3 -3
  103. package/app/types/components/index.ts +1 -1
  104. package/package.json +1 -1
  105. package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +0 -29
  106. package/app/components/02.molecules/display-tooltip-defined/CONSUMER-STYLING.md +0 -53
  107. package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +0 -117
  108. package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +0 -129
  109. package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +0 -85
  110. package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +0 -17
  111. /package/app/types/components/{display-tooltip-defined.d.ts → display-tooltip-rich.d.ts} +0 -0
@@ -27,7 +27,7 @@ Do not assume placeholder text or default content.
27
27
  ## Basic Usage
28
28
 
29
29
  ```vue
30
- <EyebrowText textContent="Interior Design" />
30
+ <EyebrowText text-content="Interior Design" />
31
31
  ```
32
32
 
33
33
  ## With Tag and Size
@@ -37,8 +37,8 @@ Use `tag="p"` for semantic paragraph context, or `tag="span"` when inline. Choos
37
37
  ```vue
38
38
  <EyebrowText
39
39
  tag="p"
40
- fontSize="small"
41
- textContent="Featured Collection"
40
+ font-size="small"
41
+ text-content="Featured Collection"
42
42
  />
43
43
  ```
44
44
 
@@ -47,10 +47,10 @@ Use `tag="p"` for semantic paragraph context, or `tag="span"` when inline. Choos
47
47
  EyebrowText is commonly placed directly above a `HeroText` heading:
48
48
 
49
49
  ```vue
50
- <EyebrowText tag="p" textContent="Our Services" />
50
+ <EyebrowText tag="p" text-content="Our Services" />
51
51
  <HeroText
52
52
  tag="h2"
53
- :textContent="[
53
+ :text-content="[
54
54
  { text: 'Crafted with', styleClass: 'normal' },
55
55
  { text: 'Care', styleClass: 'accent' },
56
56
  ]"
@@ -59,6 +59,8 @@ EyebrowText is commonly placed directly above a `HeroText` heading:
59
59
 
60
60
  ## Font Size Scale
61
61
 
62
+ The prop is rendered as `data-font-size` on the root (not a class).
63
+
62
64
  | Value | CSS variable |
63
65
  |-------|-------------|
64
66
  | `large` | `--eyebrow-text-large` (default `1.4rem`) |
@@ -110,3 +112,5 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
110
112
 
111
113
  - Component is auto-imported in Nuxt — no import needed.
112
114
  - `tag` defaults to `"div"` which is block-level. Use `"span"` when the eyebrow must be inline within a flow of text.
115
+ - Long text and unbroken strings wrap (`overflow-wrap: anywhere`). There is no line-clamp token: clamping needs `overflow: hidden`, which clips the italic overhang; `ServiceDetail` clamps its eyebrow itself with an allowance.
116
+ - 2026-10-19 migration: the size moved from a bare `large`/`medium`/`small` class to `data-font-size`; added wrapping, a `StressTest` story and parent-story links.
@@ -58,10 +58,11 @@ heading to have an accessible name. Bind the same id to both:
58
58
  </section>
59
59
  ```
60
60
 
61
- Several components in this library (`PageRow`, `LayoutGridByCols`,
62
- `ProfileSection`, `ServiceSummary`, `ServiceDetail`, `PageHeroHighlights`) already generate this id
63
- automatically via the `useAriaLabelledById` composable and expose it as a `heading-id` slot prop —
64
- when composing inside one of those, bind `HeaderBlock`'s `id` to that slot prop instead of writing
61
+ Three components in this library generate this id with the `useAriaLabelledById` composable and
62
+ expose it as a `heading-id` slot prop: `PageRow` (default slot), `ProfileSection` (`heroText` slot)
63
+ and `PageHeroHighlights` (`header` slot). (`LayoutGridByCols`, `ServiceSummary` and `ServiceDetail`
64
+ use the composable too, but label their own heading or an `sr-only` label, with no slot prop.) When
65
+ composing inside one of the three, bind `HeaderBlock`'s `id` to that slot prop instead of writing
65
66
  your own id:
66
67
 
67
68
  ```vue
@@ -72,7 +73,8 @@ your own id:
72
73
 
73
74
  ## Styling
74
75
 
75
- `HeaderBlock` has no CSS of its own — `classLevel` applies one of the global
76
+ The root always has the `.header-block` class, whose only style is `overflow-wrap: anywhere` (so
77
+ an unbroken word wraps instead of overflowing). `classLevel` applies one of the global
76
78
  `.page-heading-1`–`.page-heading-6` utility classes defined in
77
79
  `app/assets/styles/setup/05.typography/02.utility-classes/_font-classes-page-heading.css`, each
78
80
  setting `font-size` from a shared fluid type-scale token (`--step-3` through `--step-8`). All six
@@ -90,3 +92,9 @@ Moved to `01.atoms/text-blocks/header-block/`, alongside `EyebrowText`/`HeroText
90
92
  added the `id` prop in the same pass — several sections need their heading to carry a specific id
91
93
  for `aria-labelledby`, and `HeaderBlock` had no explicit way to receive one (matching `PageRow`'s
92
94
  existing `id?: string | null` + `:id` convention).
95
+
96
+ ## Migration note (2026-10-19)
97
+
98
+ Added the `.header-block` root class and `overflow-wrap: anywhere`, a `StressTest` story, a
99
+ story-only slot-text control and docs text. The spec now unmounts every wrapper. Corrected the list
100
+ of components that expose a `heading-id` slot prop.
@@ -45,7 +45,7 @@ interface TextConfig {
45
45
  ```vue
46
46
  <HeroText
47
47
  tag="h1"
48
- :textContent="[
48
+ :text-content="[
49
49
  { text: 'Designing', styleClass: 'normal' },
50
50
  { text: 'Artistry', styleClass: 'accent' },
51
51
  { text: 'at Home', styleClass: 'normal' },
@@ -61,8 +61,8 @@ interface TextConfig {
61
61
  <HeroText
62
62
  tag="h2"
63
63
  icon="lucide:sparkles"
64
- fontSize="heading"
65
- :textContent="[{ text: 'Featured', styleClass: 'accent' }]"
64
+ font-size="heading"
65
+ :text-content="[{ text: 'Featured', styleClass: 'accent' }]"
66
66
  />
67
67
  ```
68
68
 
@@ -74,8 +74,8 @@ Use `axis="vertical"` to stack text segments in a column instead of a row.
74
74
  <HeroText
75
75
  tag="h1"
76
76
  axis="vertical"
77
- fontSize="display"
78
- :textContent="[
77
+ font-size="display"
78
+ :text-content="[
79
79
  { text: 'Modern', styleClass: 'normal' },
80
80
  { text: 'Living', styleClass: 'accent' },
81
81
  ]"
@@ -96,7 +96,9 @@ Define these CSS custom properties in your consuming app to control sizes.
96
96
 
97
97
  ## Styling
98
98
 
99
- Override via `styleClassPassthrough` or a parent HOC `<style>` block targeting `.hero-text`.
99
+ Override via `styleClassPassthrough` or a parent HOC `<style>` block targeting `.hero-text`. Size
100
+ and axis are `data-font-size` and `data-axis` attributes on the root (not classes); each segment is a
101
+ `<span class="hero-text-segment">` plus its `styleClass`.
100
102
 
101
103
  Key CSS custom properties:
102
104
 
@@ -107,7 +109,6 @@ Key CSS custom properties:
107
109
  | `--hero-text-{scale}` | see CONSUMER-STYLING.md | Font size per `fontSize` prop value (the library theme sets them on `:root`; the component has matching fallbacks) |
108
110
  | `--hero-text-font-family` | `"Playfair Display"` | Font family of the whole component |
109
111
  | `--hero-text-margin` | `0` | Margin on the root element |
110
- | `--hero-text-horizontal-gap` | `0.5ch` | Gap between segments in `axis="horizontal"` mode |
111
112
  | `--hero-text-vertical-gap` | `0.4em` | Gap between segments in `axis="vertical"` mode |
112
113
  | `--hero-text-accent-offset` | `0.2em` | Underline-clearance offset on `.accent` spans (`padding-bottom` and the matching negative `margin-bottom`) |
113
114
 
@@ -115,7 +116,7 @@ Key CSS custom properties:
115
116
 
116
117
  ```css
117
118
  .my-page {
118
- .hero-text.axis-vertical {
119
+ .hero-text[data-axis="vertical"] {
119
120
  --hero-text-vertical-gap: 0.6em;
120
121
  }
121
122
  }
@@ -149,6 +150,9 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
149
150
 
150
151
  ## Notes
151
152
 
152
- - Text segments are trimmed and a trailing space is automatically appended between segments in horizontal axis — do not manually pad `text` values.
153
+ - Text segments are trimmed and a trailing space is automatically appended between segments — do not manually pad `text` values. Segments with blank or missing `text` (common in CMS data) are dropped.
154
+ - The icon is decorative (`aria-hidden`).
155
+ - Long text and unbroken strings wrap. No line-clamp token: clamping needs `overflow: hidden`, which clips the accent descender offset; `ServiceDetail` clamps its title itself.
156
+ - 2026-10-19 migration: size and axis moved from bare root classes to `data-font-size`/`data-axis`, segments from `.text-block-{n}` to `.hero-text-segment`; dropped the dead `--hero-text-horizontal-gap`; added a `StressTest` story.
153
157
  - The icon is sized to match the font size. At `subheading` scale it is `0.75em` (applied as `font-size`, since `width`/`height` on an `<Icon>` are overridden by `@nuxt/icon`).
154
158
  - Component is auto-imported in Nuxt — no import needed.
@@ -26,13 +26,13 @@ Both slots are optional. If neither is provided, no icon wrapper elements are re
26
26
  ## Basic Usage
27
27
 
28
28
  ```vue
29
- <LinkText to="/about" linkText="About Us" />
29
+ <LinkText to="/about" link-text="About Us" />
30
30
  ```
31
31
 
32
32
  ## With Left Icon (back/previous pattern)
33
33
 
34
34
  ```vue
35
- <LinkText to="/blog" linkText="Back to Blog">
35
+ <LinkText to="/blog" link-text="Back to Blog">
36
36
  <template #left>
37
37
  <Icon name="lucide:arrow-left" />
38
38
  </template>
@@ -42,7 +42,7 @@ Both slots are optional. If neither is provided, no icon wrapper elements are re
42
42
  ## With Right Icon (forward/more pattern)
43
43
 
44
44
  ```vue
45
- <LinkText to="/services" linkText="Learn More">
45
+ <LinkText to="/services" link-text="Learn More">
46
46
  <template #right>
47
47
  <Icon name="lucide:arrow-right" />
48
48
  </template>
@@ -52,7 +52,7 @@ Both slots are optional. If neither is provided, no icon wrapper elements are re
52
52
  ## Both Icons
53
53
 
54
54
  ```vue
55
- <LinkText to="/collection" linkText="Explore">
55
+ <LinkText to="/collection" link-text="Explore">
56
56
  <template #left>
57
57
  <Icon name="lucide:sparkles" />
58
58
  </template>
@@ -67,7 +67,7 @@ Both slots are optional. If neither is provided, no icon wrapper elements are re
67
67
  ```vue
68
68
  <LinkText
69
69
  to="https://example.com"
70
- linkText="Visit Site"
70
+ link-text="Visit Site"
71
71
  :external="true"
72
72
  target="_blank"
73
73
  >
@@ -132,3 +132,6 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
132
132
  - NuxtLink handles both internal (`/path`) and external (`https://...`) URLs automatically. Only set `external: true` if you need to force external handling on an internal-looking path.
133
133
  - The label is always rendered inside `.link-text__label` — target this class if you need to style the text independently of the icons.
134
134
  - DOM order is always: left icon → label → right icon.
135
+ - Long and unbroken labels wrap inside the column (the root is capped at `max-inline-size: 100%`). There is no line-clamp token: link text names its destination.
136
+ - For an icon-only link, pass an empty `link-text` and an `aria-label`; it falls through to the `<a>`.
137
+ - 2026-10-19 migration: label wrapping, rem focus ring, `StressTest` story, docs text; the spec now unmounts every wrapper.
@@ -28,8 +28,9 @@ and Escape dismissal.
28
28
  |------|------|---------|-------------|
29
29
  | `placement` | `"top" \| "right" \| "bottom" \| "left"` | `"right"` | Which side of the trigger the popover panel opens on. Each side has a `position-try-fallbacks` so the browser flips it automatically if it would overflow the viewport. |
30
30
  | `triggerAriaLabel` | `string` | `""` | aria-label on the trigger button — set this when the `trigger` slot is icon-only. Unset (no attribute) by default, since the trigger slot usually carries its own visible/accessible content. |
31
- | `popoverAriaLabel` | `string` | `""` | aria-label on the popover content region — set this when the `content` slot has no visible heading. |
32
- | `closeButtonAriaLabel` | `string` | `"Close"` | aria-label on the close button — override for localisation. |
31
+ | `popoverAriaLabel` | `string` | `""` | Names the popover panel and gives it `role="dialog"` (with no label it has no role, since a name on a generic element isn't announced) — set this when the `content` slot has no visible heading. |
32
+ | `closeButtonAriaLabel` | `string` | `"Close"` | The close button's accessible name (its `aria-label` and its visually hidden button text) — override for localisation. |
33
+ | `closeIcon` | `string` | `"lucide:x"` | Iconify name for the close button icon. |
33
34
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
34
35
 
35
36
  ## Slots
@@ -80,11 +81,20 @@ the only source of space above/below the slotted content — a plain `<p>` doesn
80
81
  does the same in JS. The trigger has `aria-expanded` in every browser.
81
82
  - On open, focus moves to the close button (the composable's `onOpen`) so keyboard users land
82
83
  inside the panel immediately.
84
+ - The close button is an icon-only `InputButton variant="inline"` with class
85
+ `pop-over-close-button`, pinned to the panel's block-start/inline-end corner, so it sits top left
86
+ in right-to-left text. The content slot's top padding keeps content clear of it.
87
+ - The panel is never wider than the viewport minus `3.2rem` (whatever `--pop-over-width` says) or
88
+ taller than `--pop-over-max-block-size` (default `calc(100dvh - 3.2rem)`); taller content scrolls
89
+ inside the panel. Unbroken strings in the trigger and panel wrap (`overflow-wrap: anywhere`).
83
90
 
84
91
  ## CSS custom properties
85
92
 
86
93
  See `CONSUMER-STYLING.md` for the full `--pop-over-*` token API (gap, trigger outline, panel
87
- border/background/text/shadow/width, content padding, and close-button tokens).
94
+ border/background/text/shadow/width/max-height, content padding, and close-button tokens).
95
+
96
+ No line-clamp tokens: all text arrives through the `trigger` and `content` slots, so the consumer
97
+ clamps it in their own markup.
88
98
 
89
99
  ## Notes
90
100
 
@@ -101,3 +111,7 @@ border/background/text/shadow/width, content padding, and close-button tokens).
101
111
  `@position-try` blocks commented out and unused; promoted every hardcoded value to a public CSS
102
112
  token; added tests, a Storybook story, this skill doc, and a VS Code snippet (all previously
103
113
  missing).
114
+ - 2026-10-19 migration: the close button became an `InputButton variant="inline"` (it was a raw
115
+ `<button>`) and gained a `closeIcon` prop; it now uses logical insets. The panel caps at the
116
+ viewport width and height and wraps unbroken strings; the trigger wraps too. Added a
117
+ `StressTest` story and per-story docs descriptions.
@@ -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
@@ -180,9 +180,12 @@ Playwright `STORY_BASE` slug. The slug is derived by lowercasing and replacing s
180
180
 
181
181
  | Title | STORY_BASE slug |
182
182
  | --------------------------------------------- | ------------------------------------------- |
183
- | `"Atoms/Text Blocks/HeroText"` | `atoms-text-blocks-herotext` |
183
+ | `"Atoms/Text Blocks/Hero Text"` | `atoms-text-blocks-hero-text` |
184
184
  | `"Components/Forms/Input Text/InputText"` | `components-forms-input-text-inputtext` |
185
185
 
186
+ Renaming a title changes the slug: update any `playwright/*.playwright.ts` `STORY_BASE` and any
187
+ story links (`?path=/story/...`) that point at it.
188
+
186
189
  ## argTypes control types
187
190
 
188
191
  | Prop type | `control` |
@@ -274,7 +277,7 @@ export const Default: Story = {
274
277
  Key points:
275
278
 
276
279
  - `computed` is **not** auto-imported in `.ts` story files — import it explicitly from `"vue"`.
277
- - Components aren't auto-registered in a story's `template` string either. Every component used there, including ones you put in the component's slots (`EyebrowText`, `HeroText`, `SocialIconsList`, ...), must be imported and listed in `components: {}`. Otherwise Vue renders it as an empty unknown element (`<eyebrowtext>`) with no error. ProfileSection's header and social-icons stories rendered blank this way until 2026-10-14.
280
+ - Components aren't auto-registered in a story's `template` string either. Every component used there, including ones you put in the component's slots (`EyebrowText`, `HeroText`, `SocialIconsList`, ...), must be imported and listed in `components: {}`. Otherwise Vue renders it as an empty unknown element (`<eyebrowtext>`) with no error. ProfileSection's header and social-icons stories rendered blank this way until 2026-10-14. Globally registered components are the exception and need no entry: `Icon` (`@nuxt/icon` registers it with `global: true`) and `NuxtLink`.
278
281
  - Props in a story's `template` string aren't type-checked either. An invalid value renders silently with no warning: ProfileSection's stories passed `font-size="large"` to `HeroText`, which only has `display`/`title`/`heading`/`subheading`/`label`, so the heading fell back to unstyled text. Check every literal prop value against the component's `interface Props`.
279
282
  - Extra args must be stripped before `v-bind` — spreading unknown keys onto a component makes them unknown HTML attributes.
280
283
  - CSS custom properties set via `:style` on the component root are picked up by `var()` in the component's scoped CSS.
@@ -99,13 +99,18 @@ test.describe("ComponentName — combinations", () => {
99
99
 
100
100
  ## Deriving the STORY_BASE slug
101
101
 
102
- Take the story `title` from the `.stories.ts` file, lowercase everything, remove spaces,
103
- and replace `/` with `-`:
104
-
105
- | Story `title` | `STORY_BASE` |
106
- | ------------------------------------------------- | ----------------------------------------------- |
107
- | `"Atoms/Text Blocks/HeroText"` | `atoms-text-blocks-herotext` |
108
- | `"Components/Forms/Input Button/InputButton"` | `components-forms-input-button-inputbuttoncore` |
102
+ Take the story `title` from the `.stories.ts` file, lowercase everything, and replace both spaces
103
+ and `/` with `-`:
104
+
105
+ | Story `title` | `STORY_BASE` |
106
+ | --------------------------------------------- | ------------------------------------------- |
107
+ | `"Atoms/Text Blocks/Hero Text"` | `atoms-text-blocks-hero-text` |
108
+ | `"Components/Forms/Input Button/InputButton"` | `components-forms-input-button-inputbutton` |
109
+
110
+ `STORY_BASE` is hardcoded, and nothing (Vitest, ESLint, the Component Ledger) checks it. When you
111
+ change a story `title`, update `STORY_BASE` in the component's `playwright/` folder in the same
112
+ change. EyebrowText and HeroText pointed at ids from an older title (`...-eyebrowtext`,
113
+ `...-herotext`) until 2026-10-19, so their suites loaded a missing story.
109
114
 
110
115
  The full story URL pattern is:
111
116
 
@@ -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
  }