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.
- package/.claude/skills/component-compliance-checklist.md +8 -2
- package/.claude/skills/component-dynamic-slots.md +3 -3
- package/.claude/skills/component-export-types.md +10 -16
- package/.claude/skills/component-naming.md +1 -1
- package/.claude/skills/components/alert-content.md +9 -1
- package/.claude/skills/components/container-glow.md +21 -8
- package/.claude/skills/components/display-avatar.md +13 -7
- package/.claude/skills/components/display-chip.md +37 -24
- package/.claude/skills/components/display-pill.md +58 -10
- package/.claude/skills/components/display-prompt.md +8 -5
- package/.claude/skills/components/display-toast.md +21 -4
- package/.claude/skills/components/glowing-border.md +20 -2
- package/.claude/skills/components/responsive-header.md +11 -1
- package/.claude/skills/components/select-menu.md +54 -5
- package/.claude/skills/components/site-header.md +2 -2
- package/.claude/skills/components/tabbed-content.md +300 -0
- package/.claude/skills/composable-anchor-scroll.md +16 -15
- package/.claude/skills/index.md +4 -4
- package/.claude/skills/storybook-add-story.md +53 -0
- package/.claude/skills/theming-status-tokens.md +60 -0
- package/.vscode/srcdev-component-display-avatar.code-snippets +2 -1
- package/.vscode/srcdev-component-display-chip.code-snippets +3 -2
- package/.vscode/srcdev-component-display-pill.code-snippets +10 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +6 -0
- package/.vscode/srcdev-component-select-menu.code-snippets +28 -0
- package/.vscode/{srcdev-component-tabs-core.code-snippets → srcdev-component-tabbed-content.code-snippets} +35 -16
- package/app/assets/styles/setup/03.theming/_status.css +24 -0
- package/app/assets/styles/setup/03.theming/index.css +1 -0
- package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +39 -35
- package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +25 -15
- package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +130 -24
- package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +45 -0
- package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +2 -2
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +76 -19
- package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +12 -10
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +201 -42
- package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +15 -4
- package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +1 -1
- package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +75 -9
- package/app/components/01.atoms/display-pill/DisplayPill.vue +57 -24
- package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +240 -16
- package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +43 -14
- package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +3 -3
- package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +201 -0
- package/app/components/01.atoms/tabbed-content/TabbedContent.vue +750 -0
- package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +279 -0
- package/app/components/01.atoms/tabbed-content/tests/TabbedContent.spec.ts +816 -0
- package/app/components/02.molecules/alert-content/AlertContent.vue +11 -2
- package/app/components/02.molecules/alert-content/AlertContentInner.vue +19 -0
- package/app/components/02.molecules/alert-content/CONSUMER-STYLING.md +17 -4
- package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +200 -17
- package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +33 -0
- package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +7 -5
- package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +6 -2
- package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +24 -3
- package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +19 -0
- package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +2 -1
- package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +52 -19
- package/app/components/02.molecules/display-chip/DisplayChip.vue +72 -54
- package/app/components/02.molecules/display-chip/stories/DisplayChip.stories.ts +103 -6
- package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +98 -29
- package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +8 -4
- package/app/components/02.molecules/prompt/CONSUMER-STYLING.md +9 -1
- package/app/components/02.molecules/prompt/DisplayPrompt.vue +2 -2
- package/app/components/02.molecules/prompt/stories/DisplayPrompt.stories.ts +208 -71
- package/app/components/02.molecules/prompt/tests/DisplayPrompt.spec.ts +19 -6
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +66 -8
- package/app/components/02.molecules/select-menu/SelectMenu.vue +141 -18
- package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +232 -2
- package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +151 -0
- package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +6 -2
- package/app/components/02.molecules/toast/CONSUMER-STYLING.md +91 -27
- package/app/components/02.molecules/toast/DisplayToast.vue +109 -50
- package/app/components/02.molecules/toast/DisplayToastProvider.vue +109 -45
- package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +23 -0
- package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +73 -0
- package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +132 -18
- package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +105 -39
- package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +38 -11
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +27 -12
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +35 -20
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +1 -1
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +1 -1
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +1 -1
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +3 -3
- package/app/composables/useTabs.ts +148 -206
- package/app/types/components/display-chip.d.ts +8 -4
- package/app/types/components/display-toast.d.ts +2 -0
- package/app/types/components/index.ts +0 -1
- package/app/types/components/select-menu.d.ts +2 -0
- package/app/utils/pausableTimeout.ts +41 -0
- package/nuxt.config.ts +1 -1
- package/package.json +1 -1
- package/.claude/skills/components/tab-navigation.md +0 -288
- package/.claude/skills/components/tabs-core.md +0 -187
- package/.vscode/srcdev-component-tab-navigation.code-snippets +0 -30
- package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +0 -120
- package/app/components/01.atoms/navigation/tabs/TabsCore.vue +0 -272
- package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +0 -199
- package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +0 -274
- package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +0 -191
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -550
- package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -259
- package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -379
- package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +0 -33
- package/app/composables/useNavCollapse.ts +0 -86
- 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
|
|
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
|
|
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.
|
|
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
|
|
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`
|
|
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
|
|
67
|
+
### Active state in ResponsiveHeader
|
|
68
68
|
|
|
69
|
-
`
|
|
70
|
-
just pass anchor
|
|
71
|
-
and on initial load (defaulting to the first
|
|
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.
|
|
78
|
+
### 1. ResponsiveHeader with a sticky site header
|
|
78
79
|
|
|
79
|
-
`
|
|
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
|
-
<
|
|
86
|
-
:nav-
|
|
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
|
|
96
|
+
const responsiveNavLinks = {
|
|
96
97
|
main: [
|
|
97
|
-
{
|
|
98
|
-
{
|
|
99
|
-
{
|
|
100
|
-
{
|
|
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 `
|
|
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 `
|
|
294
|
+
section nav alongside a `ResponsiveHeader` on the same page with different offsets.
|
package/.claude/skills/index.md
CHANGED
|
@@ -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
|
|
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
|
-
├──
|
|
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 (
|
|
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
|