srcdev-nuxt-components 9.6.0 → 9.6.1

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 (107) hide show
  1. package/.claude/skills/component-compliance-checklist.md +8 -2
  2. package/.claude/skills/component-dynamic-slots.md +3 -3
  3. package/.claude/skills/component-export-types.md +10 -16
  4. package/.claude/skills/component-naming.md +1 -1
  5. package/.claude/skills/components/alert-content.md +9 -1
  6. package/.claude/skills/components/container-glow.md +21 -8
  7. package/.claude/skills/components/display-avatar.md +13 -7
  8. package/.claude/skills/components/display-chip.md +37 -24
  9. package/.claude/skills/components/display-pill.md +58 -10
  10. package/.claude/skills/components/display-prompt.md +8 -5
  11. package/.claude/skills/components/display-toast.md +21 -4
  12. package/.claude/skills/components/glowing-border.md +20 -2
  13. package/.claude/skills/components/responsive-header.md +11 -1
  14. package/.claude/skills/components/select-menu.md +54 -5
  15. package/.claude/skills/components/site-header.md +2 -2
  16. package/.claude/skills/components/tabbed-content.md +300 -0
  17. package/.claude/skills/composable-anchor-scroll.md +16 -15
  18. package/.claude/skills/index.md +4 -4
  19. package/.claude/skills/storybook-add-story.md +53 -0
  20. package/.claude/skills/theming-status-tokens.md +60 -0
  21. package/.vscode/srcdev-component-display-avatar.code-snippets +2 -1
  22. package/.vscode/srcdev-component-display-chip.code-snippets +3 -2
  23. package/.vscode/srcdev-component-display-pill.code-snippets +10 -0
  24. package/.vscode/srcdev-component-responsive-header.code-snippets +6 -0
  25. package/.vscode/srcdev-component-select-menu.code-snippets +28 -0
  26. package/.vscode/{srcdev-component-tabs-core.code-snippets → srcdev-component-tabbed-content.code-snippets} +35 -16
  27. package/app/assets/styles/setup/03.theming/_status.css +24 -0
  28. package/app/assets/styles/setup/03.theming/index.css +1 -0
  29. package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +39 -35
  30. package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +25 -15
  31. package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +130 -24
  32. package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +45 -0
  33. package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +2 -2
  34. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +76 -19
  35. package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +12 -10
  36. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +201 -42
  37. package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +15 -4
  38. package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +1 -1
  39. package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +75 -9
  40. package/app/components/01.atoms/display-pill/DisplayPill.vue +57 -24
  41. package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +240 -16
  42. package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +43 -14
  43. package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +3 -3
  44. package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +201 -0
  45. package/app/components/01.atoms/tabbed-content/TabbedContent.vue +750 -0
  46. package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +279 -0
  47. package/app/components/01.atoms/tabbed-content/tests/TabbedContent.spec.ts +816 -0
  48. package/app/components/02.molecules/alert-content/AlertContent.vue +11 -2
  49. package/app/components/02.molecules/alert-content/AlertContentInner.vue +19 -0
  50. package/app/components/02.molecules/alert-content/CONSUMER-STYLING.md +17 -4
  51. package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +200 -17
  52. package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +33 -0
  53. package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +7 -5
  54. package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +6 -2
  55. package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +24 -3
  56. package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +19 -0
  57. package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +2 -1
  58. package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +52 -19
  59. package/app/components/02.molecules/display-chip/DisplayChip.vue +72 -54
  60. package/app/components/02.molecules/display-chip/stories/DisplayChip.stories.ts +103 -6
  61. package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +98 -29
  62. package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +8 -4
  63. package/app/components/02.molecules/prompt/CONSUMER-STYLING.md +9 -1
  64. package/app/components/02.molecules/prompt/DisplayPrompt.vue +2 -2
  65. package/app/components/02.molecules/prompt/stories/DisplayPrompt.stories.ts +208 -71
  66. package/app/components/02.molecules/prompt/tests/DisplayPrompt.spec.ts +19 -6
  67. package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +66 -8
  68. package/app/components/02.molecules/select-menu/SelectMenu.vue +141 -18
  69. package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +232 -2
  70. package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +151 -0
  71. package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +6 -2
  72. package/app/components/02.molecules/toast/CONSUMER-STYLING.md +91 -27
  73. package/app/components/02.molecules/toast/DisplayToast.vue +109 -50
  74. package/app/components/02.molecules/toast/DisplayToastProvider.vue +109 -45
  75. package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +23 -0
  76. package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +73 -0
  77. package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +132 -18
  78. package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +105 -39
  79. package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +38 -11
  80. package/app/components/03.organisms/responsive-header/NavigationItems.vue +27 -12
  81. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +35 -20
  82. package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +1 -1
  83. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +1 -1
  84. package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +1 -1
  85. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +3 -3
  86. package/app/composables/useTabs.ts +148 -206
  87. package/app/types/components/display-chip.d.ts +8 -4
  88. package/app/types/components/display-toast.d.ts +2 -0
  89. package/app/types/components/index.ts +0 -1
  90. package/app/types/components/select-menu.d.ts +2 -0
  91. package/app/utils/pausableTimeout.ts +41 -0
  92. package/nuxt.config.ts +1 -1
  93. package/package.json +1 -1
  94. package/.claude/skills/components/tab-navigation.md +0 -288
  95. package/.claude/skills/components/tabs-core.md +0 -187
  96. package/.vscode/srcdev-component-tab-navigation.code-snippets +0 -30
  97. package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +0 -120
  98. package/app/components/01.atoms/navigation/tabs/TabsCore.vue +0 -272
  99. package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +0 -199
  100. package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +0 -274
  101. package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +0 -191
  102. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -550
  103. package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -259
  104. package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -379
  105. package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +0 -33
  106. package/app/composables/useNavCollapse.ts +0 -86
  107. package/app/types/components/nav-item.d.ts +0 -11
@@ -17,7 +17,7 @@ a configurable root element via the `tag` prop.
17
17
  | Prop | Type | Default | Description |
18
18
  |------|------|---------|-------------|
19
19
  | `tag` | `"div" \| "p" \| "span" \| "section" \| "article" \| "aside" \| "header" \| "footer" \| "main" \| "nav" \| "ul" \| "ol"` | `"div"` | Root element tag. |
20
- | `variant` | `"subtle" \| "vivid" \| "silver" \| "steel" \| "green"` | `"subtle"` | Glow colour variant. |
20
+ | `variant` | `"subtle" \| "vivid" \| "silver" \| "steel" \| "green"` | `"subtle"` | Glow colour variant, rendered as `data-variant` on the root (not a class). |
21
21
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
22
22
 
23
23
  ## Slots
@@ -36,7 +36,7 @@ a configurable root element via the `tag` prop.
36
36
 
37
37
  ## Styling
38
38
 
39
- Border width, radius, surface colour, animation duration, and every variant's 5 gradient colour
39
+ Border width, radius, surface colour (defaults to `--theme-surface-subtle`), text colour (`--theme-text`), animation duration, and every variant's 5 gradient colour
40
40
  stops are public CSS custom properties — see `CONSUMER-STYLING.md` for the full token table.
41
41
 
42
42
  ```vue
@@ -48,6 +48,14 @@ stops are public CSS custom properties — see `CONSUMER-STYLING.md` for the ful
48
48
  </GlowingBorder>
49
49
  ```
50
50
 
51
+ ## Highlighting a single control
52
+
53
+ Wrap just an `InputButton` or `InputTextCore` so the glow becomes the control's border (a design
54
+ call-out for something "special"). Set `--glowing-border-radius` to the control's radius plus the
55
+ glow width, `--glowing-border-surface: transparent`, `--glowing-border-overflow: visible` (or the
56
+ control's focus ring is clipped), shrink-to-fit display, and `--input-text-border: transparent` for
57
+ the input. Full recipe in `CONSUMER-STYLING.md`; `HighlightedControls` story.
58
+
51
59
  ## Motion
52
60
 
53
61
  The glow animation stops automatically under `prefers-reduced-motion: reduce`.
@@ -64,3 +72,13 @@ The glow animation stops automatically under `prefers-reduced-motion: reduce`.
64
72
  override surface.
65
73
  - 2026-09-20: added `green` variant (vibrant, saturated greens: `#00ff87`, `#39ff14`, `#00c853`,
66
74
  `#64dd17`, `#00e676`), each stop overridable via `--glowing-border-green-color-{1-5}`.
75
+ - 2026-10-08 stress-test pass (`StressTest` story): the variant moved from a bare root class
76
+ (`.vivid`, `.green`...) to `data-variant`, since those names collide with common consumer
77
+ classes; `@keyframes glow` renamed to `glowing-border-rotate` for the same reason. Added
78
+ `overflow-wrap: anywhere` so long unbroken text wraps rather than being clipped by
79
+ `overflow: hidden`. `--glowing-border-surface` default moved from `canvas` to
80
+ `--theme-surface-subtle`, and a new `--glowing-border-text-colour` token defaults to
81
+ `--theme-text`. Stories gained story-only canvas and surface background controls with dark
82
+ values; the hardcoded dark stage was removed.
83
+ - 2026-10-08: added `--glowing-border-overflow` (default `hidden`) so a wrapped control's hover/focus
84
+ outlines can show, plus the `HighlightedControls` story and recipe.
@@ -132,6 +132,9 @@ Route links and anchor links can be mixed in the same data. See the "Same-Page A
132
132
 
133
133
  ## Migrating from TabNavigation
134
134
 
135
+ > `TabNavigation` was deprecated 2026-10-06 and **removed 2026-10-07**, along with its `NavItem` /
136
+ > `NavItemData` types and the `useNavCollapse` composable. Type the new data as `ResponsiveHeaderProp`.
137
+
135
138
  `ResponsiveHeader` covers everything `TabNavigation` does, and collapses progressively (only the items
136
139
  that don't fit move into the overflow menu) instead of all-or-nothing. To switch:
137
140
 
@@ -149,8 +152,12 @@ that don't fit move into the overflow menu) instead of all-or-nothing. To switch
149
152
  | `anchor-scroll-offset` | `anchor-scroll-offset` |
150
153
  | collapse everything to a burger when items don't fit | default is per-item overflow; add `collapse-at-main-nav-intersection` for the old all-or-nothing burger |
151
154
  | `--tab-nav-link-color` | `--responsive-header-link-color` |
155
+ | `--tab-nav-link-hover-color` / `--tab-nav-link-active-color` | `--responsive-header-link-color-hover` / `-active` (added 2026-10-07) |
156
+ | `--tab-nav-decorator-hovered-bg` | `--responsive-nav-decorator-hovered-bg` |
152
157
  | `--tab-nav-decorator-indicator-color` | `--responsive-nav-decorator-indicator-color` |
153
- | `--tab-nav-*` panel and burger tokens | the overflow tokens in `CONSUMER-STYLING.md` and NavigationItems' `--overflow-nav-*` |
158
+ | `--tab-nav-panel-link-color` / `-hover-color` / `-active-color` | `--overflow-nav-link-color` / `-color-hover` / `-color-active` |
159
+ | `--tab-nav-panel-bg` / `--tab-nav-panel-border-color` / `--tab-nav-panel-item-border` | `--responsive-header-overflow-nav-bg` / `--responsive-header-overflow-nav-border` (full shorthand) / `--overflow-nav-link-border-color` |
160
+ | `--tab-nav-burger-color` | `--responsive-header-overflow-btn-icon-color` |
154
161
 
155
162
  ```vue
156
163
  <!-- Before -->
@@ -203,9 +210,12 @@ All tokens are read via `var(--token, default)` — see the full list in the com
203
210
  |---|---|---|
204
211
  | `--responsive-header-link-font-size` | `inherit` | Nav-link font-size. **Set this to a fixed value** — see the measurement-pipeline note above for why leaving it `inherit` from a fluid ancestor is a footgun. |
205
212
  | `--responsive-header-link-color` | `inherit` | Link/summary text colour. |
213
+ | `--responsive-header-link-color-hover` / `-active` | the resting colour | Per-state link colour in the top bar (added 2026-10-07). Hover falls back to the item's current state. `--overflow-nav-link-color-hover` / `-active` do the same in the overflow panel. |
206
214
  | `--responsive-header-bg` / `--responsive-header-padding-*` / `--responsive-header-border*` | transparent / `0` / `none` | Root element theming. |
207
215
  | `--responsive-header-overflow-btn-*` | various | Overflow burger button sizing/colour. |
208
216
  | `--responsive-header-sub-nav-*` / `--responsive-header-overflow-nav-*` | various | Top-bar dropdown panel and overflow-panel container theming. |
217
+
218
+ Defaults are for a **light** header (changed 2026-10-07): panels use `--page-bg`, borders and hover tints are `color-mix()`es of `--theme-text`, the overflow button is transparent with a `--theme-border-focus` hover outline. They used to assume a dark bar (`Canvas`, translucent white borders and tints). A dark header sets the tokens.
209
219
  | `--responsive-nav-decorator-indicator-color` / `--responsive-nav-decorator-hovered-*` | `currentColor` / inherits | The sliding active/hover indicator bar under the main nav. |
210
220
 
211
221
  ---
@@ -21,14 +21,18 @@ open between picks.
21
21
 
22
22
  | Prop | Type | Default | Notes |
23
23
  |---|---|---|---|
24
- | `options` | `SelectMenuOption[]` | — | **Required.** `{ value: string \| number; label: string; icon?: string }[]`. |
24
+ | `options` | `SelectMenuOption[]` | — | **Required.** `{ value: string \| number; label: string; icon?: string; dotColor?: string }[]`. `dotColor` (any CSS colour) colours the trigger's status dot while that option is selected (`indicator="dot"`, single-select) and shows a matching dot beside the option in the list when it has no `icon`. |
25
25
  | `label` | `string` | — | **Required.** Accessible name for the trigger + listbox (`aria-label`). Also the trigger's fallback text when nothing is selected and no `placeholder` is set — this doubles as a visible category tag, e.g. `"Choose a service"`. |
26
26
  | `placeholder` | `string` | `undefined` | Trigger text shown when nothing is selected. Falls back to `label` when omitted. |
27
- | `showIcon` | `boolean` | `true` | Show the selected option's icon in the trigger. |
27
+ | `showIcon` | `boolean` | `true` | Show the leading icon in the trigger: the selected option's icon (single-select), otherwise `triggerIcon`. |
28
+ | `triggerIcon` | `string` | `undefined` | Fixed leading icon, e.g. a filter category icon. Shown in both modes; in single-select a selected option's own icon replaces it. |
29
+ | `indicator` | `"none" \| "count" \| "dot"` | `"none"` | Marker after the trigger text, visible only while something is selected; otherwise kept in the layout with `visibility: hidden` (`.select-menu-trigger-indicator-empty`) so the trigger width doesn't jump. `count` = badge with the number selected (for `multiple`); `dot` = status dot (for single-select filters, flags the filter as active). `aria-hidden`. |
30
+ | `selectedCountLabel` | `string` | `"{count} selected"` | Appended to the trigger's `aria-label` while the indicator shows (`"Category, 3 selected"`). `{count}` is replaced. Override for translation. |
28
31
  | `showLabel` | `boolean` | `true` | Show the selected option's label (or placeholder/label fallback) text in the trigger. Set `false` for an icon-only compact trigger. |
29
32
  | `showChevron` | `boolean` | `true` | Show the trailing chevron in the trigger. |
30
- | `multiple` | `boolean` | `false` | Allow selecting more than one option. Each option gets a checkbox indicator, `v-model` becomes an array, and picking an option leaves the popover open so more can be toggled. Trigger icon is not shown when `true` (no single option to represent). |
33
+ | `multiple` | `boolean` | `false` | Allow selecting more than one option. Each option gets a checkbox indicator, `v-model` becomes an array, and picking an option leaves the popover open so more can be toggled. No option icon in the trigger when `true` (no single option to represent); `triggerIcon` still shows. |
31
34
  | `showSelectionInTrigger` | `boolean` | `false` | Multiple mode only. When `true`, the trigger text updates to a comma-separated list of the currently checked options instead of staying fixed on `placeholder`/`label`. No effect outside `multiple`. |
35
+ | `reserveLabelWidth` | `boolean` | `false` | Single-select only. Renders hidden copies of the placeholder and every option label in the same grid cell as the visible text, so the trigger is always as wide as the longest one and doesn't resize when the selection changes. Off by default, since sometimes a trigger that hugs its text is wanted. No effect with `multiple`. |
32
36
  | `inputVariant` | `InputUiVariant` | `"normal"` | Trigger border style, same prop as `InputSelect`: `"normal"` (bordered box) or `"underlined"` (bottom border only, square corners), added as a class on the root. `"outlined"` has no CSS, same as the `05.forms` inputs. |
33
37
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root `<div>`. |
34
38
 
@@ -132,6 +136,39 @@ Set `showSelectionInTrigger` to opt out of that and have the trigger update inst
132
136
 
133
137
  ---
134
138
 
139
+ ## Filter trigger: icon + count or status dot
140
+
141
+ ```vue
142
+ <!-- Multi-select category filter: list icon, "Category", badge "3", chevron -->
143
+ <SelectMenu
144
+ v-model="categories"
145
+ :options="categoryOptions"
146
+ label="Category"
147
+ multiple
148
+ trigger-icon="lucide:list"
149
+ indicator="count"
150
+ />
151
+
152
+ <!-- Single-select status filter: dot appears once a status is picked, in that option's dotColor
153
+ statusOptions = [{ value: "active", label: "Active", dotColor: "var(--status-success)" }, ...] -->
154
+ <SelectMenu
155
+ v-model="status"
156
+ :options="statusOptions"
157
+ label="Status"
158
+ trigger-icon="lucide:circle-dot"
159
+ indicator="dot"
160
+ />
161
+ ```
162
+
163
+ The indicator is only visible while something is selected, so an unfiltered trigger looks plain,
164
+ but its space is always reserved so the trigger doesn't resize as the filter is set and cleared.
165
+ Per-option `dotColor` beats `--select-menu-trigger-dot-color`, which is the fallback (and the only
166
+ colour in `multiple` mode).
167
+ Badge/dot tokens (`--select-menu-trigger-count-*`, `--select-menu-trigger-dot-*`) are in
168
+ `CONSUMER-STYLING.md`.
169
+
170
+ ---
171
+
135
172
  ## Filter bar (multiple categories)
136
173
 
137
174
  `SelectMenu` is single-select per instance — a faceted filter bar is built by placing one instance
@@ -167,6 +204,13 @@ Quick reference:
167
204
  }
168
205
  ```
169
206
 
207
+ ### Long content
208
+
209
+ The root caps at its container width, so a long trigger label truncates with an ellipsis. The
210
+ popover caps at `--select-menu-popover-max-width` (default `calc(100vw - 3.2rem)`) and option labels
211
+ wrap; `--select-menu-item-label-line-clamp` (default `none`) caps them, `1` for single-line
212
+ ellipsis. The `StressTest` story covers this at every canvas width.
213
+
170
214
  ---
171
215
 
172
216
  ## Notes
@@ -188,7 +232,8 @@ Quick reference:
188
232
  drop the whole rule.
189
233
  Confirmed broken on a Safari 16 iPad before this (trigger rendered, menu never opened).
190
234
  - **Left-aligned popover** (`left: anchor(left)`) — unlike `ActionMenu` which right-aligns. Matches
191
- native `<select>` dropdown behaviour. Flips above the trigger near the bottom of the viewport.
235
+ native `<select>` dropdown behaviour. Flips above the trigger near the bottom of the viewport,
236
+ and to right-aligned when it would overflow the right edge (anchor-positioning browsers only).
192
237
  - **Keyboard navigation** follows the WAI-ARIA listbox pattern: `ArrowDown`/`ArrowUp` move between
193
238
  options (wraps around), `Home`/`End` jump to first/last, `Enter`/`Space` select the focused
194
239
  option, `Tab` closes without stealing focus back to the trigger (matches `ActionMenu`'s
@@ -209,7 +254,7 @@ Quick reference:
209
254
  weight doesn't resize the popover. Hover/focus has `-text-color-hover` and `-border-hover`, which
210
255
  fall back to the row's *current* state (selected or resting), not always to resting. There's no
211
256
  hover font-weight token on purpose: the width reserve only covers one alternate weight.
212
- - **Chevron rotation** keys off the trigger's `aria-expanded` (from the composable's `isOpen`), not
257
+ - **Chevron flip** (`scaleY(-1)`, was `rotate(180deg)` until 2026-10-07) keys off the trigger's `aria-expanded` (from the composable's `isOpen`), not
213
258
  `:has(...:popover-open)`, so it also works without the Popover API (changed 2026-10-05).
214
259
  - **No native `<select>`/`appearance: base-select` involved** — this is a from-scratch popover
215
260
  listbox, not a styled native select, so it doesn't inherit `InputSelectCore`'s WebKit
@@ -217,3 +262,7 @@ Quick reference:
217
262
  - **Not a form field** — `SelectMenu` has no `fieldHasError`/`theme`/`inputVariant` props like the
218
263
  `05.forms` inputs. Use `InputSelectCore` instead for an actual form field that needs validation
219
264
  state and native `<select>` semantics.
265
+ - **2026-10-08 stress-test pass**: root `max-inline-size: 100%` (a long trigger label overflowed
266
+ narrow containers instead of truncating), popover `--select-menu-popover-max-width` and
267
+ `flip-inline` fallbacks (a long option made the menu wider than the viewport), option labels wrap
268
+ with a `--select-menu-item-label-line-clamp` token instead of `nowrap`.
@@ -99,6 +99,6 @@ const responsiveNavLinks = {
99
99
 
100
100
  ## Migration notes
101
101
 
102
- - Consuming layouts previously assembling `PageRow` + `SkipLinks` + `ResponsiveHeader` (or `TabNavigation`) by hand can switch to `SiteHeader` directly; the two `styleClassPassthrough` props map 1:1 onto the two independent passthrough hooks that pattern used.
103
- - `TabNavigation`'s `NavItemData` shape (`{ main: [{ text, href, iconName, cssName }] }`) is **not** the same shape as `ResponsiveHeaderProp` (`{ groupKey: [{ name, path, childLinksTitle, childLinks }] }`) — migrating a `TabNavigation` consumer means reshaping the nav data, not just swapping the component. `TabNavigation`'s `navAlign` prop has no `SiteHeader` equivalent since `ResponsiveHeader`'s layout is fixed (branding column + nav column).
102
+ - Consuming layouts previously assembling `PageRow` + `SkipLinks` + `ResponsiveHeader` (or the removed `TabNavigation`) by hand can switch to `SiteHeader` directly; the two `styleClassPassthrough` props map 1:1 onto the two independent passthrough hooks that pattern used.
103
+ - `TabNavigation` (removed 2026-10-07) took `NavItemData` (`{ main: [{ text, href, iconName, cssName }] }`, also removed), which is **not** the same shape as `ResponsiveHeaderProp` (`{ groupKey: [{ name, path, childLinksTitle, childLinks }] }`): migrating means reshaping the nav data, not just swapping the component. See "Migrating from TabNavigation" in `responsive-header.md`. Its `navAlign` prop has no `SiteHeader` equivalent since `ResponsiveHeader`'s layout is fixed (branding column + nav column).
104
104
  - Requires the consuming layout's main/footer regions to carry `id="main-content"` / `id="footer-content"` — same requirement as using `SkipLinks` directly.
@@ -0,0 +1,300 @@
1
+ ---
2
+ name: TabbedContent
3
+ description: TabbedContent tab list/panel widget (was TabsCore) — indexed dynamic slots, arrow-key navigation, moving hover/active/underline indicators, CSS tokens
4
+ type: reference
5
+ ---
6
+
7
+ # TabbedContent
8
+
9
+ > **Renamed 2026-10-07:** `TabsCore` → `TabbedContent`, moved from `01.atoms/navigation/tabs/` to
10
+ > `01.atoms/tabbed-content/`. Root class `.tabs-core` → `.tabbed-content`; inner classes
11
+ > `.tabs-list` → `.tabbed-content-list`, `.tabs-list-item` → `.tabbed-content-trigger`,
12
+ > `.nav__hovered`/`.nav__active`/`.nav__active-indicator` → `.tabbed-content-indicator-hover`/
13
+ > `-active`/`-underline`, `.tab-content-wrapper` → `.tabbed-content-panels`, `.tab-content` →
14
+ > `.tabbed-content-panel`. Tokens keep their `--tabs-*` names. Slot names are unchanged.
15
+
16
+ ## Overview
17
+
18
+ `TabbedContent` renders a WAI-ARIA tablist and its panels from indexed slots. The number of tabs is
19
+ set via `itemCount`; each tab's trigger and content come from `tab-{n}-trigger`/`tab-{n}-content`
20
+ slots. Indexed slots (rather than named dynamic slots) are used deliberately: `itemCount` is needed
21
+ for ARIA linking across the two parallel loops (triggers and panels), not just the slot loop itself.
22
+
23
+ Moving decorators track pointer/keyboard state: a hover highlight, an active-tab highlight, a
24
+ thick underline/sideline under the active tab, and a thin one (`--tabs-hover-underline-indicator-height`,
25
+ default `0.1rem`, added 2026-10-07) following the hovered tab. `trackHover` / `trackActive` /
26
+ `trackIndicator` turn them off; the hover underline needs both `trackHover` and `trackIndicator`.
27
+
28
+ Not navigation: for page/site navigation use `ResponsiveHeader` (which replaced `TabNavigation`).
29
+
30
+ **Location**: `app/components/01.atoms/tabbed-content/`
31
+
32
+ ## Props
33
+
34
+ | Prop | Type | Default | Notes |
35
+ |------|------|---------|-------|
36
+ | `itemCount` | `number` | — | **Required.** Number of tabs; drives both slot loops. Rounded down; 0, negative or `NaN` renders no tabs. Can change after mount (the active tab is kept if it still exists, otherwise the first tab becomes active). |
37
+ | `axis` | `"x" \| "y"` | `"x"` | Horizontal row or vertical column. Also sets `aria-orientation` and which arrow keys move between tabs. Reactive. |
38
+ | `transitionDuration` | `number` | `200` | ms duration of the moving indicators. Negative values clamp to 0. Reactive. |
39
+ | `trackHover` | `boolean` | `true` | Moving hover highlight. Reactive. |
40
+ | `trackActive` | `boolean` | `true` | Moving active-tab highlight. Reactive. |
41
+ | `trackIndicator` | `boolean` | `true` | Moving underline/sideline. Reactive. |
42
+ | `overflowMode` | `"menu" \| "scroll"` | `"menu"` | `axis="x"` only. `menu`: tabs that don't fit collapse from the end into a "More" button and menu (added 2026-10-07). `scroll`: the tab row scrolls sideways inside itself. Reactive. |
43
+ | `ariaLabel` | `string` | `"Tabs"` | `aria-label` on the tablist; override for localisation. |
44
+ | `moreLabel` | `string` | `"More tabs"` | Accessible name of the More button and its menu; override for localisation. |
45
+ | `moreIcon` | `string` | `"lucide:ellipsis"` | Icon on the More button. |
46
+ | `moreActiveIcon` | `string` | `"lucide:chevron-down"` | Icon after the active tab's label on the More button while that tab is collapsed. |
47
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Classes on the root `.tabbed-content` (was the tab list before 2026-10-07). |
48
+
49
+ ## Slots
50
+
51
+ For each index `0..itemCount-1`:
52
+
53
+ - `tab-{n}-trigger` — content of the tab button. May contain icons or other elements; a click
54
+ anywhere inside the button activates the tab.
55
+ - `tab-{n}-content` — content of that tab's panel.
56
+
57
+ ```vue
58
+ <TabbedContent :item-count="2" aria-label="Product details">
59
+ <template #tab-0-trigger>Overview</template>
60
+ <template #tab-0-content>Overview content…</template>
61
+ <template #tab-1-trigger>Specs</template>
62
+ <template #tab-1-content>Specs content…</template>
63
+ </TabbedContent>
64
+ ```
65
+
66
+ ## Accessibility behaviour
67
+
68
+ - `role="tablist"` is a `<div>` that owns the `role="tab"` buttons directly (it used to be a
69
+ `<ul>` with plain `<li>`s in between, an ARIA required-children violation, with the indicator
70
+ `<div>`s injected into the list by JS). Indicators are `aria-hidden` `<span>`s rendered by the
71
+ template.
72
+ - `aria-orientation` follows `axis`.
73
+ - Each trigger has `aria-controls` pointing at its panel; each panel has `aria-labelledby` pointing
74
+ at its trigger. IDs are prefixed with `useId()`, so several instances on one page don't clash
75
+ (before 2026-10-07 every instance used `tab-0-trigger` etc.).
76
+ - Inactive panels carry the `hidden` attribute; the first panel is visible in the SSR markup.
77
+ Panels have `tabindex="0"` so keyboard users can reach scrollable panel content.
78
+ - Roving tabindex: only the active trigger has `tabindex="0"`.
79
+ - Arrow keys move focus and activate the target tab (automatic activation): `ArrowLeft`/`ArrowRight`
80
+ on `axis="x"`, `ArrowUp`/`ArrowDown` on `axis="y"`, wrapping at the ends; `Home`/`End` jump to
81
+ the first/last tab. Not mirrored for RTL.
82
+ - Focus ring: `--tabs-focus-ring-colour` (default `--theme-border-focus`) on triggers and panels.
83
+
84
+ ## Overflow menu (`overflowMode="menu"`, default, since 2026-10-07)
85
+
86
+ - On `axis="x"`, a ResizeObserver on the bar and the tablist measures the tabs (widths cached
87
+ while each is visible) and collapses the ones that don't fit, from the end, into a "More" button
88
+ at the end of the tab row (`--tabs-more-margin-inline-start: auto`; `0` puts it straight after
89
+ the last visible tab). Collapsed triggers get `hidden`, so they leave the tablist; the menu lists
90
+ them as `role="menuitemradio"` rows (re-rendering each tab's own `tab-{n}-trigger` slot), with
91
+ `aria-checked` on the active one. Picking one activates that tab and returns focus to the
92
+ More button.
93
+ - The More button is outside `role="tablist"` (`aria-haspopup="menu"`, `aria-label` from
94
+ `moreLabel`). While nothing has overflowed it stays in the DOM but idle (invisible,
95
+ `tabindex="-1"`) so its width can be measured.
96
+ - When the active tab is collapsed, the More button shows that tab's own trigger slot plus a
97
+ chevron (instead of `⋯`), gets `.is-active` and the active/hover indicators sit on it. A
98
+ ResizeObserver on the button re-measures, so if the wider button no longer fits, the tab before
99
+ it moves into the menu (and comes back when a visible tab is picked). The button has no
100
+ `aria-label`: `moreLabel` is visually hidden text inside it, so its name ("Shipping, More tabs")
101
+ always contains the visible label (WCAG 2.5.3). Trigger slot content renders up to three times
102
+ (tab, menu row, More button), so don't put `id`s in it.
103
+ - Indicators sit on the More button while the active tab is collapsed (`useTabs` swaps any
104
+ hidden tab for `[data-more-trigger]` when positioning). Arrow keys move between visible tabs only, and the first visible tab keeps `tabindex="0"` so
105
+ the tablist stays reachable with Tab.
106
+ - The popover uses `useAnchoredPopover` (Popover API + anchor positioning, with the Safari 16/17
107
+ fallbacks), like `SelectMenu` and `ActionMenu`. Menu keys: Arrow Up/Down (wrapping),
108
+ Home/End, Escape (light dismiss), Tab closes.
109
+ - Not on `axis="y"`: a column doesn't run out of width.
110
+
111
+ ## Layout under hostile data
112
+
113
+ - With `overflowMode="scroll"` the tab row scrolls sideways inside itself; the indicators scroll
114
+ with it. In both modes triggers never shrink, and wrap their label past
115
+ `--tabs-list-item-max-inline-size` (`24rem`).
116
+ - On `axis="y"` the tab column is capped at `--tabs-axis-y-list-max-inline-size` (`50%`).
117
+ - Panels wrap unbroken strings (`overflow-wrap: anywhere`).
118
+ - No line-clamp token: trigger and panel text is consumer slot markup, so the consumer controls it.
119
+
120
+ ## CSS custom properties
121
+
122
+ See `CONSUMER-STYLING.md` in the component folder for the full `--tabs-*` token API. Colour
123
+ defaults come from the `--theme-*` slots (changed 2026-10-07, were fixed `--slate-*` steps with a
124
+ dark panel).
125
+
126
+ ## useTabs
127
+
128
+ `app/composables/useTabs.ts` drives the behaviour and is only used by this component. Its nav ref
129
+ is `.tabbed-content-bar` (the indicators' positioning box); it also exposes `activeIndex` and
130
+ `activateTabByIndex()` for the More menu. It takes
131
+ `axis`, `transitionDuration` and `trackHover` as getters (read at use time, so prop changes apply),
132
+ and exposes `refreshTabs()`, which the component calls on mount and after any change to the tab
133
+ count, axis or indicator props. Click/hover handlers use `event.currentTarget` (the button), not
134
+ `event.target`, so clicking an icon inside a trigger works. Panels are matched to triggers by
135
+ `data-tab-index`, not by position in the `v-for` ref array, since Vue doesn't keep that array in
136
+ source order when the list changes.
137
+
138
+ ## Indicator motion: edge insets (2026-10-08)
139
+
140
+ `useTabs.placeIndicator(kind, animate)` writes each indicator's two edges as insets from the
141
+ bar's start/end along the axis (`--_{kind}-start` / `--_{kind}-end`), and the CSS transitions
142
+ `left`/`right` (or `top`/`bottom`) separately. Moving forward, the start edge gets a delay of one
143
+ duration (`--_{kind}-start-delay`); moving back, the end edge does. So the leading edge moves first
144
+ and the trailing edge catches up: the same stretch effect as before, symmetric both ways, with no
145
+ settle timer. CSS retargets each edge from its current position, so a quick second move can't
146
+ pull an edge backwards. Hover and active have separate duration/delay variables.
147
+
148
+ This replaced a `translate` + `scale` model (a bar-wide box translated to the tab's left and
149
+ scaled to its width, plus a timeout to settle after a stretch, and one shared
150
+ `--_transition-duration`). Rightward moves animated both properties together in the settle step,
151
+ and the indicator visibly stepped back mid-move (reported for hover, then n to n+x clicks); the
152
+ computed values were always correct, the composition wasn't. Avoid reintroducing a transform
153
+ pair for this.
154
+
155
+ ## Label colour under the sliding highlight (2026-10-08)
156
+
157
+ With `trackActive`, `useTabs` re-checks every animation frame while the active indicator
158
+ transitions (`trackCoverage`, for `transitionDuration` + 50ms) which visible tabs and the More
159
+ button the indicator (`[data-active-indicator]`) covers by at least half, and toggles
160
+ `data-under-active` on them (an attribute, not a class, so Vue's own class binding on the More
161
+ button can't wipe it). CSS under `.tracks-active` colours by that attribute rather than
162
+ `aria-selected`. The coverage window only ever extends (`coverageUntil`): a refresh landing
163
+ mid-move (e.g. the More button resizing straight after a click) used to restart it with zero
164
+ length, cancelling the per-frame checks while the highlight was still sliding and leaving stale
165
+ colours (dark selected label, invisible neighbour). A More-button resize also re-places both
166
+ indicators (`syncIndicators(true)`), since its box moves even when no tab changes sides, and an
167
+ overflow change refreshes with `refreshTabs(true)` so an in-flight move retargets instead of
168
+ snapping. This replaces the per-component timing trick `TripleToggleSwitch` uses (swap
169
+ colour at half the slide), which only works for single-step moves; a tab jump passes under
170
+ several labels.
171
+
172
+ ## Migration note (2026-10-07)
173
+
174
+ Fixed during the compliance pass that renamed it:
175
+
176
+ - Duplicate IDs across instances (above).
177
+ - `event.target` in the click handler: clicking an element inside a trigger set the active tab to
178
+ that inner element, so no panel matched and every panel was hidden.
179
+ - `axis`, `transitionDuration` and the `track*` flags were read once at setup, and a changed
180
+ `itemCount` was never picked up (new tabs had no roving tabindex and weren't navigable).
181
+ - Dark-on-dark panel content by default, near-invisible focus ring, `styleClassPassthrough` on
182
+ the wrong element, unprefixed inner class names (pitfall #16).
183
+ - A long label or many tabs pushed the page sideways.
184
+
185
+ ## Migration note (2026-09-15)
186
+
187
+ Previously lived at `tabs/TabsCore.vue` with options-style `defineProps`, no tier folder, and
188
+ several defects fixed in this pass:
189
+
190
+ - `querySelectorAll("[data-nav-item")` was missing its closing `]` — an invalid selector that
191
+ some browsers tolerate but that throws in strict DOM implementations (including this repo's test
192
+ environment). Fixed to `"[data-nav-item]"`.
193
+ - `aria-labelledby="channel-name"` was hardcoded on the tablist, pointing at an id that doesn't
194
+ exist anywhere in this library. Replaced with the new `ariaLabel` prop.
195
+ - `border-bottom`/`border-left` on the tab list referenced `--_tabs-border-bottom`, a private CSS
196
+ variable that was never set anywhere (dead reference — the border never rendered). Promoted to
197
+ the public `--tabs-nav-border` token with a real default.
198
+ - `tag`, `trackHover`, `trackActive`, and `trackIndicator` were all declared props with no effect
199
+ anywhere in the component or its composable (`useTabs`). `tag` had no coherent purpose (triggers
200
+ were always hardcoded `<button>`s) and was removed; the three `track*` flags were wired up to
201
+ actually gate their respective decorator elements, since removing them would have been a bigger
202
+ behavioural change than making them work as their names already implied.
203
+ - Almost every colour/spacing/typography value in the `<style>` block was hardcoded with no
204
+ override hook at all (not even a private token) — promoted to public `--tabs-*` tokens.
205
+
206
+ ## Performance notes (2026-09-15)
207
+
208
+ `useTabs` had a few real perf issues, fixed alongside the migration:
209
+
210
+ - **Layout thrashing**: `moveActiveIndicator`/`moveHoveredIndicator`/`setFinalActivePositions`/
211
+ `setFinalHoveredPositions` used to interleave `offsetLeft`/`offsetWidth`/etc. reads with
212
+ `style.setProperty` writes, forcing a synchronous reflow between each pair. All layout reads are
213
+ now batched into local variables before any writes happen.
214
+ - **`@mouseover` → `@mouseenter`**: the hover trigger listener used `mouseover`, which bubbles and
215
+ re-fires as the pointer crosses any child element inside the button. `mouseenter` fires exactly
216
+ once per tab entered.
217
+ - **Debounced settle timers**: `moveActiveIndicator`/`moveHoveredIndicator` schedule a `setTimeout`
218
+ to snap the indicator to its exact final position once the CSS transition ends. Previously each
219
+ call scheduled a new timeout without clearing the last one, so moving the pointer quickly across
220
+ several tabs stacked up redundant pending timeouts. Now the previous timeout is cleared before
221
+ scheduling the next, and both are cleared in `onUnmounted`.
222
+ - `handleTransitioningClass` called `.indexOf()` on the same two tabs repeatedly inside its loop
223
+ condition and body — now computed once per call.
224
+ - `previousActiveTab` used `useState("previousActiveTab", ...)` with a fixed, unscoped key — every
225
+ `TabsCore` instance on a page would have shared that one piece of state. Not actually a
226
+ performance issue, but a correctness bug found while auditing this file; fixed by switching to a
227
+ plain per-instance `ref`, since this state doesn't need SSR/hydration sharing.
228
+
229
+ ## Fixed: hover/active label text could become unreadable against the indicator (2026-09-15)
230
+
231
+ `--tabs-hover-indicator-text-colour` and `--tabs-active-indicator-text-colour` were set as `color`
232
+ on `.nav__hovered`/`.nav__active` — decorative, empty, absolutely-positioned divs with no text
233
+ content, so that `color` was dead CSS with no visible effect. The actual tab label
234
+ (`.tabs-list-item`) used one shared `--tabs-list-item-colour-selected` token for hover, active,
235
+ *and* transitioning alike, regardless of which of the two (potentially differently-coloured)
236
+ indicator backgrounds was actually behind it at that moment. A consumer overriding only the hover
237
+ indicator's background (or the active one) had no working lever to keep the label readable against
238
+ it, and could end up with text the same colour as the background sliding behind it — this was the
239
+ original defect noticed during this component's initial authoring, reported as "the button text
240
+ became the same colour as the track item background and became invisible."
241
+
242
+ Fixed by removing the dead `color` declarations from the two decorator divs and splitting
243
+ `.tabs-list-item`'s combined `:hover, [aria-selected="true"], .transitioning` rule so hover uses
244
+ `--tabs-hover-indicator-text-colour` and active uses `--tabs-list-item-colour-selected` falling
245
+ back to `--tabs-active-indicator-text-colour` — each state's text colour now tracks the background
246
+ actually behind it. See `CONSUMER-STYLING.md`'s "Tab-label text colour tracks the indicator behind
247
+ it" section: overriding an indicator background without its matching text-colour token can still
248
+ produce poor contrast (same tradeoff as any two independently-set colour tokens) — the fix makes
249
+ the tokens functional, it doesn't auto-compute contrast.
250
+
251
+ ## Fixed: intermediate tab labels could go invisible during a multi-tab jump (2026-09-15)
252
+
253
+ The first fix above wasn't the whole story. The exact repro that surfaced the rest of it: with tab
254
+ 1 active, hover then click tab 5 — the tabs in between briefly lost all visible text. Going the
255
+ other direction (tab 5 active, hover then click tab 1) looked fine.
256
+
257
+ Cause: `useTabs` had a `handleTransitioningClass` helper that added a `.transitioning` class to
258
+ every tab spanned by a jump (previously shared by the `[aria-selected="true"]` colour rule fixed
259
+ above), on the assumption the sliding indicator visually covers every spanned tab for the whole
260
+ transition — so it force-applied the active text colour to all of them, for the full
261
+ `transitionDuration`. It doesn't cover them the whole time: the indicator's "grow from anchor" CSS
262
+ transition (see `moveActiveIndicator`/`moveHoveredIndicator`) keeps the box's leading edge pinned
263
+ at the *old* tab's position on a forward move and only grows its width, so it only gradually sweeps
264
+ across later tabs — a tab several positions away isn't actually covered until *late* in the
265
+ transition. Recolouring it to the active text colour from the very start meant its label matched
266
+ neither its own background nor the not-yet-arrived indicator, going invisible. A backward move
267
+ animates position and width toward their final values together, so the box's rendered coverage is
268
+ more consistent throughout — which is why that direction looked fine and made the bug easy to miss
269
+ without a specific repro.
270
+
271
+ Fixed by removing `handleTransitioningClass` and the `.transitioning` class entirely — it had no
272
+ other purpose. Tabs spanned by a jump now keep their normal (inactive) styling throughout; only the
273
+ newly-active tab (`[aria-selected="true"]`) and the currently-hovered tab (`:hover`) get their
274
+ special colours. See `CONSUMER-STYLING.md`'s "Tabs spanned by a jump keep their normal styling".
275
+
276
+ ## Fixed: passed-over tab text still invisible under the active indicator specifically (2026-09-15)
277
+
278
+ Removing `.transitioning` (previous fix) uncovered a second, pre-existing defect that
279
+ `.transitioning`'s forced recolour had been accidentally masking the whole time: a non-active tab's
280
+ normal text colour (`--tabs-list-item-colour`) was **exactly the same value** as the active
281
+ indicator's background (`--tabs-active-indicator-colour`, same default) — this collision existed in
282
+ the very first authored version of this component, not something introduced by any migration
283
+ change. So whenever the active pill visually passes under (or briefly sits under) any tab that
284
+ isn't itself marked active — which happens on every transition, single-step or distant, in either
285
+ direction — that tab's text became literally the same colour as the background sliding beneath it.
286
+ `--tabs-hover-indicator-colour`'s default never had this problem since it doesn't coincide with the
287
+ inactive text default.
288
+
289
+ Fixed by changing `--tabs-active-indicator-colour`'s default to one step in from the extreme,
290
+ still clearly the boldest of the three indicator colours, but no longer numerically identical to
291
+ `--tabs-list-item-colour`'s default. `--tabs-active-indicator-text-colour`/
292
+ `--tabs-list-item-colour-selected` (used by the truly active tab) still contrast fine against the
293
+ new value. See `CONSUMER-STYLING.md` for the note on keeping these two tokens from colliding again
294
+ if you override either one.
295
+
296
+ ## Colour token defaults (2026-10-07)
297
+
298
+ Colour defaults were flat `--slate-*` steps from 2026-09-15 (when `light-dark()` pairs were
299
+ dropped) until 2026-10-07, when they moved to the `--theme-*` slots. Light-only, like the rest of
300
+ the library.
@@ -64,26 +64,27 @@ onMounted(() => {
64
64
  >{{ section.label }}</a>
65
65
  ```
66
66
 
67
- ### Active state in TabNavigation
67
+ ### Active state in ResponsiveHeader
68
68
 
69
- `TabNavigation` handles `activeHash` internally for hash nav items. No extra work needed —
70
- just pass anchor hrefs in `navItemData` and the active indicator moves automatically on click
71
- and on initial load (defaulting to the first hash item when the URL has no hash).
69
+ `ResponsiveHeader` handles `activeHash` internally for `#` nav items. No extra work needed —
70
+ just pass anchor paths in `responsiveNavLinks` and the active indicator moves automatically on
71
+ click and on initial load (defaulting to the first anchor item when the URL has no hash).
72
+ (`TabNavigation` did the same until it was removed on 2026-10-07.)
72
73
 
73
74
  ---
74
75
 
75
76
  ## Usage patterns
76
77
 
77
- ### 1. TabNavigation with a sticky site header
78
+ ### 1. ResponsiveHeader with a sticky site header
78
79
 
79
- `TabNavigation` accepts an `anchorScrollOffset` prop that is passed directly to `useAnchorScroll`.
80
+ `ResponsiveHeader` accepts an `anchorScrollOffset` prop that is passed directly to `useAnchorScroll`.
80
81
  Pass a getter so the live header height is read at scroll time — this stays correct if the
81
82
  header resizes on different viewports.
82
83
 
83
84
  ```vue
84
85
  <header ref="headerRef">
85
- <TabNavigation
86
- :nav-item-data="navItemData"
86
+ <ResponsiveHeader
87
+ :responsive-nav-links="responsiveNavLinks"
87
88
  :anchor-scroll-offset="() => headerRef?.offsetHeight ?? 0"
88
89
  />
89
90
  </header>
@@ -92,18 +93,18 @@ header resizes on different viewports.
92
93
  ```ts
93
94
  const headerRef = ref<HTMLElement | null>(null);
94
95
 
95
- const navItemData = {
96
+ const responsiveNavLinks = {
96
97
  main: [
97
- { text: "About", href: "#about" },
98
- { text: "Services", href: "#services" },
99
- { text: "Contact", href: "#contact" },
100
- { text: "Blog", href: "/blog" }, // route — NuxtLink handles as normal
98
+ { name: "About", path: "#about" },
99
+ { name: "Services", path: "#services" },
100
+ { name: "Contact", path: "#contact" },
101
+ { name: "Blog", path: "/blog" }, // route — NuxtLink handles as normal
101
102
  ],
102
103
  };
103
104
  ```
104
105
 
105
106
  **CSS alternative** — if you want all anchor links site-wide to respect the sticky header
106
- (not just the ones inside `TabNavigation`), add `scroll-padding-top` to `html` instead and
107
+ (not just the ones inside `ResponsiveHeader`), add `scroll-padding-top` to `html` instead and
107
108
  skip the prop:
108
109
 
109
110
  ```css
@@ -290,4 +291,4 @@ takes effect immediately on the next click.
290
291
  - **SSR** — both `handleNavClick` and `scrollToAnchor` guard with `import.meta.server` and return
291
292
  early. No special SSR setup is needed.
292
293
  - **Multiple instances** — each call to `useAnchorScroll` is independent. You can run a sticky
293
- section nav alongside a `TabNavigation` on the same page with different offsets.
294
+ section nav alongside a `ResponsiveHeader` on the same page with different offsets.
@@ -33,6 +33,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
33
33
  ├── theming-partial-override.md — override a specific colour token category (palette, buttons, inputs) without a full theme replacement
34
34
  ├── theming-form-geometry-tokens.md — non-colour form/button/input tokens (padding, border-radius, gaps): full inventory, why partial override doesn't need duplicating the source files
35
35
  ├── theming-component-token-pattern.md — standard shape for overridable component colours: public token + inline fallback, no bare var()/no private-only indirection; the three tiers (theme slots / cross-component families / component-scoped); line-clamp tokens for overflowing text; rollout status
36
+ ├── theming-status-tokens.md — global --status-* colours (success/warning/danger/info/neutral: solid + surface + text), which components read them, how to override once for the whole library
36
37
  ├── theming-dark-mode.md — add a dark scheme in a consumer app (layer is light-only): enable plumbing, full dark token set without light-dark(), light-dark() alternative, light-only pinning
37
38
  ├── colour-scheme-disable.md — disable light/dark scheme support in a consumer app
38
39
  ├── component-dynamic-slots.md — named dynamic slots ($slots iteration) vs indexed dynamic slots (itemCount pattern)
@@ -62,7 +63,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
62
63
  ├── composable-zod-validation.md — useZodValidation: schema-driven form validation, error binding, submit flow, API error push
63
64
  ├── composable-colour-scheme.md — useColourScheme: reactive light/dark/auto switching, localStorage persistence, runtime config
64
65
  ├── composable-dialog-controls.md — useDialogControls: single-call setup with config object, openDialog/closeDialog API, confirm/cancel callbacks
65
- ├── composable-anchor-scroll.md — useAnchorScroll: smooth anchor scrolling with reduced-motion support, dynamic offset, and TabNavigation integration
66
+ ├── composable-anchor-scroll.md — useAnchorScroll: smooth anchor scrolling with reduced-motion support, dynamic offset, and ResponsiveHeader integration
66
67
  ├── composable-tooltips-guide.md — useTooltipsGuide: sequential popover guide with auto-start, dismiss-to-advance, manual controls
67
68
  ├── composable-cookie-consent.md — useCookieConsent: unset/granted/denied state, cookie persistence, wraps @nuxt/scripts' useScriptTriggerConsent
68
69
  ├── composable-analytics.md — useAnalytics: provider-agnostic trackEvent/page-view tracking (google-analytics only implemented), consent-gated, single call site for setup + firing events
@@ -90,7 +91,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
90
91
  ├── 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
91
92
  ├── breadcrumb.md — Breadcrumb: items (BreadcrumbItem[]) trail, link vs current-page text, CSS token API
92
93
  ├── skip-links.md — SkipLinks: focus-revealed accessibility skip-nav, links (SkipLink[]) data-driven, homeLink slot, CSS token API
93
- ├── tabs-core.md — TabsCore: tablist from indexed slots (itemCount), arrow-key nav, moving hover/active/underline indicators, CSS token API
94
+ ├── tabbed-content.md — TabbedContent (was TabsCore): tablist + panels from indexed slots (itemCount), arrow-key nav, moving hover/active/underline indicators, CSS token API
94
95
  ├── contact-section.md — ContactSection props (stepperIndicatorSize pass-through), 3-item info+form layout, slot API
95
96
  ├── stepper-list.md — StepperList dynamic slots (item-{n}/indicator-{n}), props, connector behaviour
96
97
  ├── expanding-panel.md — ExpandingPanel v-model, forceOpened, contentIsOnTop overlay mode, slots (summary/icon/content), ARIA wiring, CSS token API
@@ -107,7 +108,6 @@ Each skill is a single markdown file named `<area>-<task>.md`.
107
108
  ├── wipe-away-vertical.md — WipeAwayVertical: scroll-driven vertical wipe-away effect, pure-CSS grid overlay (no JS), indexed dynamic slots, sticky-centering-via-calc gotcha, CSS tokens
108
109
  ├── marquee-scroller.md — MarqueeScroller: infinite logo/badge scroller, per-item dynamic slots (marqueeData id), hover/focus/keyboard pause, reduced-motion, CSS tokens
109
110
  ├── rotating-carousel-image.md — RotatingCarouselImage: 3D rotating image carousel, scroll-parallax tilt, focus/keyboard/hover pause, reduced-motion, CSS tokens
110
- ├── tab-navigation.md — TabNavigation (DEPRECATED, use ResponsiveHeader): horizontal tab nav with CSS anchor-positioning indicators, anchor scroll, burger collapse, full CSS token API
111
111
  ├── social-icons-list.md — SocialIconsList: data-driven social icon links, ISocialIcon type, logos: icon names, CSS tokens
112
112
  ├── display-qr-code.md — DisplayQrCode: QR code SVG from a string value, colour/size/variant/radius props, currentColor default
113
113
  ├── capture-qr-code.md — CaptureQrCode: live camera scanner, error state, visibility/route/KeepAlive lifecycle, media stream cleanup
@@ -120,7 +120,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
120
120
  ├── select-menu.md — SelectMenu: v-model single-select listbox popover (ActionMenu's popover mechanics, InputSelectCore's selected-option semantics), icon/text/chevron trigger toggles, checkmark on selected option, options-array driven, full CSS token API
121
121
  ├── display-dialog.md — DisplayDialog: native <dialog> overlay, 5 variants (dialog/modal/confirm/alert/fullscreen), useDialogControls integration, CSS token API
122
122
  ├── display-chip.md — DisplayChip: status indicator chip overlay, CSS trig positioning, circle/square shapes, status colours, icon/label content
123
- ├── display-pill.md — DisplayPill: pill/badge label with icon slot, 6 variants, 3 sizes, reversible order, --display-pill-* tokens (renamed from --theme-pill-*; variants fall back to base colour tokens)
123
+ ├── 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
124
124
  ├── carousel-flip.md — CarouselFlip: FLIP-animated carousel, carouselDataIds slot API, buttonLayout variants (sides/controls-flanking/controls-grouped-right/overlay), CSS tokens
125
125
  ├── samaritan-prompt.md — SamaritanPrompt + SamaritanPromptMixed: animated text prompts, typewriter/word-pulse effects, MessageConfig API, pause on hover, aria-live, CSS tokens
126
126
  ├── display-toast.md — DisplayToast (standalone v-model) + DisplayToastProvider + useToastQueue (app-wide queue): stacking, FLIP dismiss, maxVisible, SemanticTheme × 4, masked SVG glass variant