srcdev-nuxt-components 9.6.4 → 9.6.5

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 (27) hide show
  1. package/.claude/skills/component-compliance-checklist.md +9 -1
  2. package/.claude/skills/components/progress-tracker.md +140 -0
  3. package/.claude/skills/components/stepper-list.md +10 -3
  4. package/.claude/skills/components/tabbed-content.md +4 -0
  5. package/.claude/skills/index.md +2 -1
  6. package/.claude/skills/storybook-add-story.md +6 -0
  7. package/.vscode/srcdev-component-progress-tracker.code-snippets +45 -0
  8. package/.vscode/srcdev-component-stepper-list.code-snippets +3 -2
  9. package/.vscode/srcdev-component-tabbed-content.code-snippets +1 -0
  10. package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +5 -1
  11. package/app/components/01.atoms/tabbed-content/TabbedContent.vue +2 -2
  12. package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +26 -0
  13. package/app/components/02.molecules/contact-section/tests/__snapshots__/ContactSection.spec.ts.snap +28 -28
  14. package/app/components/02.molecules/progress-tracker/CONSUMER-STYLING.md +116 -0
  15. package/app/components/02.molecules/progress-tracker/ProgressTracker.vue +275 -0
  16. package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +424 -0
  17. package/app/components/02.molecules/progress-tracker/tests/ProgressTracker.spec.ts +253 -0
  18. package/app/components/02.molecules/progress-tracker/tests/__snapshots__/ProgressTracker.spec.ts.snap +46 -0
  19. package/app/components/02.molecules/stepper-list/CONSUMER-STYLING.md +42 -7
  20. package/app/components/02.molecules/stepper-list/StepperList.vue +42 -19
  21. package/app/components/02.molecules/stepper-list/stories/StepperList.stories.ts +288 -2
  22. package/app/components/02.molecules/stepper-list/tests/StepperList.spec.ts +161 -34
  23. package/app/components/02.molecules/stepper-list/tests/__snapshots__/StepperList.spec.ts.snap +40 -40
  24. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +7 -7
  25. package/app/types/components/index.ts +1 -0
  26. package/app/types/components/progress-tracker.d.ts +6 -0
  27. package/package.json +1 -1
@@ -130,7 +130,10 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
130
130
  number of items
131
131
  - emoji (including at the start of anything that takes initials or first letters), HTML-like
132
132
  text (must render as text), right-to-left text
133
- - out-of-range or odd numbers (0, negative, above the maximum, fractional, very large)
133
+ - out-of-range or odd numbers (0, negative, above the maximum, fractional, very large). A
134
+ count prop used as a `v-for` range (`itemCount`) needs flooring and clamping in a
135
+ `computed()`: Vue renders nothing for a fractional or negative range and only warns in dev
136
+ (`StepperList`, 2026-10-15)
134
137
  - broken image or link URLs
135
138
  Describe what to check in the story's docs description, and look at it at every
136
139
  `CanvasSwitcher` width. Fixes usually mean `overflow-wrap: anywhere`, `min-inline-size: 0` on
@@ -179,6 +182,8 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
179
182
  9. **Accessibility** — check for gaps beyond what already-passing tests would catch:
180
183
  - Interactive controls (buttons, toggles, custom form-like widgets) have an accessible name —
181
184
  visible text, `aria-label`, or `aria-labelledby`.
185
+ - A `<ul>`/`<ol>` styled with `list-style: none` gets `role="list"`, or Safari/VoiceOver stops
186
+ announcing it as a list (`StepperList`, 2026-10-15).
182
187
  - Keyboard behaviour matches what's actually implemented — no keydown case that only calls
183
188
  `event.preventDefault()` with a comment like "could add this later", and no screen-reader
184
189
  copy (visible or `sr-only`) that describes an interaction the component doesn't actually
@@ -253,6 +258,9 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
253
258
  the edits in private memory. Note that an inline-size container can't size itself from its
254
259
  content: say so in `CONSUMER-STYLING.md` (`ProfileSection`).
255
260
  - Grid tracks that hold text are `minmax(0, …)` so an unbroken string can't widen the grid.
261
+ - Layout measured in script (connector lines, indicators) re-measures when any measured item
262
+ resizes or changes class, not only when the container resizes (`measured-layout-observers.md`,
263
+ pitfall #28).
256
264
  - Action buttons are `InputButton` (`variant="inline"` when the wrapper styles them), not a raw
257
265
  `<button>` (`component-inline-button.md`, `component-inline-action-button.md`). The ledger
258
266
  doesn't check this, which is how `InputCopy` scored 5/5 with one (fixed 2026-10-15).
@@ -0,0 +1,140 @@
1
+ # ProgressTracker Component
2
+
3
+ ## Overview
4
+
5
+ `ProgressTracker` (`app/components/02.molecules/progress-tracker/`) is a presentational progress
6
+ track for a multi-step flow: a header with "Step X of Y" and a progress bar, then the steps as a
7
+ `StepperList`. Steps before the `v-model` index are done (filled indicator with a tick), the model's
8
+ step is current (ringed number, content in a focus card) and later steps are upcoming (outlined
9
+ number, muted text). The current step's `#step-{n}` slot is where a parent puts a form or actions,
10
+ so it also works as an accordion-style n-step wizard. The parent owns all state; the component has
11
+ no events and never changes the model. Styling tokens:
12
+ `app/components/02.molecules/progress-tracker/CONSUMER-STYLING.md`.
13
+
14
+ ---
15
+
16
+ ## Props reference
17
+
18
+ > **Hyphenation rule**: write camelCase props hyphenated in templates: `:style-class-passthrough`.
19
+
20
+ | Prop (template form) | Type | Default | Notes |
21
+ |---|---|---|---|
22
+ | `v-model` | `number` | (required) | 0-based index of the current step. `steps.length` means every step is done. Floored, and clamped to `0`–`steps.length`. |
23
+ | `:steps` | `ProgressTrackerStep[]` | (required) | `{ title: string; body?: string }`, exported from `~/types/components`. An empty array renders nothing. |
24
+ | `heading-tag` | `"h2" \| "h3" \| "h4" \| "h5" \| "h6"` | `"h3"` | Tag for each step title. |
25
+ | `step-label` | `string` | `"Step {current} of {total}"` | Header text and the bar's `aria-valuetext`; `{current}` is 1-based. |
26
+ | `complete-label` | `string` | `"All steps complete"` | Header text once every step is done. |
27
+ | `done-label` | `string` | `"Completed"` | Screen-reader text appended to each done title (", Completed"). |
28
+ | `done-icon` | `string` | `"lucide:check"` | Iconify name for the tick. |
29
+ | `:focus-on-change` | `boolean` | `true` | Move focus when the model changes (see Behaviour). |
30
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Classes on the root `div`. |
31
+
32
+ ## Slots
33
+
34
+ | Slot | Props | Notes |
35
+ |---|---|---|
36
+ | `#step-{n}` | `{ step, index, headingId }` | Rendered **only while step `n` is current**, inside its focus card under the body. `headingId` is the step title's id, for `aria-labelledby` on a form. |
37
+
38
+ ---
39
+
40
+ ## Behaviour
41
+
42
+ - One row per step, with its title and body; a step without a body has no body paragraph.
43
+ - Steps before the model are done, the model's step is current, later steps are upcoming.
44
+ - The current title has `aria-current="step"`; done titles carry `done-label` as screen-reader
45
+ text; indicators are `aria-hidden`. The list is an `ol`, so position ("2 of 5") is announced too.
46
+ - The header shows `step-label` filled in, or `complete-label` when every step is done. It's a
47
+ polite live region, so a change is announced.
48
+ - The bar is `role="progressbar"` with `aria-valuemin="0"`, `aria-valuemax` = step count,
49
+ `aria-valuenow` = done count and `aria-valuetext` = the header label.
50
+ - Changing the model after mount moves focus to the new current title (`tabindex="-1"`), or to the
51
+ header label when complete, so keyboard and screen-reader users aren't dropped when a slot's
52
+ focused button unmounts. Never on first render. `:focus-on-change="false"` turns it off (e.g.
53
+ when the parent restores saved progress after mount).
54
+ - Done steps are plain text: there is nothing clickable in the component itself.
55
+ - The model is clamped: negative counts as 0, above `steps.length` counts as complete, fractions
56
+ are floored.
57
+ - Survives worst-case data (unbroken strings, long copy, 20+ steps, empty steps) without breaking
58
+ the layout; the header wraps the bar below the label when space runs out.
59
+ - Bar and state colour transitions are off under `prefers-reduced-motion: reduce`.
60
+
61
+ ---
62
+
63
+ ## Usage
64
+
65
+ ### Display only
66
+
67
+ ```vue
68
+ <ProgressTracker v-model="currentStep" :steps="onboardingSteps" />
69
+ ```
70
+
71
+ ### Wizard (parent-owned forms)
72
+
73
+ ```vue
74
+ <script setup lang="ts">
75
+ import type { ProgressTrackerStep } from "~/types/components";
76
+
77
+ const currentStep = ref(0);
78
+ const form = reactive({ name: "", email: "" });
79
+
80
+ const steps = computed<ProgressTrackerStep[]>(() => [
81
+ { title: "Your details", body: currentStep.value > 0 ? form.name : "Tell us who you are." },
82
+ { title: "Contact", body: currentStep.value > 1 ? form.email : "Where can we reach you?" },
83
+ { title: "Review and submit" },
84
+ ]);
85
+ </script>
86
+
87
+ <template>
88
+ <ProgressTracker v-model="currentStep" :steps="steps">
89
+ <template #step-0="{ headingId }">
90
+ <form :aria-labelledby="headingId" @submit.prevent="currentStep++">
91
+ <!-- fields, then a submit InputButton labelled Continue -->
92
+ </form>
93
+ </template>
94
+ <template #step-1="{ headingId }">
95
+ <form :aria-labelledby="headingId" @submit.prevent="currentStep++">
96
+ <!-- fields, a Back InputButton (@click="currentStep--") and Continue -->
97
+ </form>
98
+ </template>
99
+ <template #step-2="{ headingId }">
100
+ <form :aria-labelledby="headingId" @submit.prevent="currentStep = steps.length">
101
+ <!-- review, Back and Submit -->
102
+ </form>
103
+ </template>
104
+ </ProgressTracker>
105
+ </template>
106
+ ```
107
+
108
+ Validation, saving and what "complete" means are the parent's job. Pass a summary of each completed
109
+ step back in as its `body` so done steps show what was entered.
110
+
111
+ ---
112
+
113
+ ## Composition
114
+
115
+ - Built on `StepperList` (`tag="ol"`, `indicator-alignment="top"`), filling its `indicator-{n}` and
116
+ `item-{n}` slots. ProgressTracker sets StepperList's `--stepper-list-*` tokens on
117
+ `.progress-tracker-list` from its own, so an ancestor's `--stepper-list-*` values don't reach it.
118
+ - Connector colour per state uses `.stepper-list-item:has(.progress-tracker-indicator.is-done)`,
119
+ the per-step state recipe from StepperList's `CONSUMER-STYLING.md`.
120
+ - The focus card bleeds left by `--progress-tracker-card-padding-inline` so the current step's text
121
+ stays aligned with the other steps; the current indicator moves down by the card's block padding
122
+ to stay level with its title. Clearance between the current ring and the card is
123
+ `gap - card-padding-inline - ring-width` (4rem - 2rem - 0.6rem = 1.4rem by default). The first
124
+ defaults (3.2rem gap, 2.4rem padding) left 0.2rem and the ring touched the card, so change these
125
+ three together.
126
+
127
+ ---
128
+
129
+ ## Stories
130
+
131
+ Default, All Complete, Interactive (Mark complete), Wizard (parent-owned forms), Translated
132
+ Labels, White Card on a Panel, Stress Test. Title: `Molecules/ProgressTracker`.
133
+
134
+ ---
135
+
136
+ ## Notes
137
+
138
+ - Out of scope: clickable or back-navigable steps, a built-in complete event or validation,
139
+ horizontal layout, auto-advance.
140
+ - Auto-imported in Nuxt; import the `ProgressTrackerStep` type from `~/types/components`.
@@ -20,6 +20,7 @@
20
20
  | … | … |
21
21
 
22
22
  **Rules:**
23
+
23
24
  - Slots are always **zero-indexed**: first item = `item-0`, last item = `item-{itemCount - 1}`.
24
25
  - `item-{n}` slots are required if you want content in each row.
25
26
  - `indicator-{n}` slots are **optional**. When omitted, a CSS counter bubble is rendered automatically. When provided, the custom content replaces the counter entirely — the CSS counter is hidden for that item only.
@@ -32,7 +33,7 @@
32
33
 
33
34
  | Prop | Type | Default | Notes |
34
35
  |------|------|---------|-------|
35
- | `itemCount` | `number` | — | **Required.** Controls how many `<li>` elements are rendered and which slots exist. |
36
+ | `itemCount` | `number` | — | **Required.** Controls how many `<li>` elements are rendered and which slots exist. Floored to a whole number; negative or `NaN` renders no rows. |
36
37
  | `tag` | `"ul" \| "ol"` | `"ul"` | Use `"ol"` for sequentially meaningful content (recipes, instructions). |
37
38
  | `indicatorAlignment` | `"top" \| "center"` | `"top"` | Vertical alignment of the indicator relative to each item's content. `"center"` suits single-line items; `"top"` suits multi-line content. |
38
39
  | `indicatorVariant` | `"disc" \| "circle" \| "square"` | `"disc"` | Visual style of the auto-generated counter bubble. |
@@ -160,12 +161,14 @@ for full examples (global theming, page-scoped, and per-instance overrides).
160
161
  | `--stepper-list-counter-font-size` | `1.4rem` | Counter number font size |
161
162
  | `--stepper-list-counter-font-weight` | `600` | Counter number font weight |
162
163
  | `--stepper-list-connector-width` | `0.2rem` | Connector line thickness |
164
+ | `--stepper-list-connector-gap` | `0` | Space between each end of the connector line and its indicators |
163
165
 
164
166
  ### Colours
165
167
 
166
168
  | Token | Default | Controls |
167
169
  |---|---|---|
168
170
  | `--stepper-list-connector-color` | `currentColor` | Connector line colour |
171
+ | `--stepper-list-connector-image` | `none` | Image over the connector colour, e.g. a `repeating-linear-gradient()` for a dashed line |
169
172
  | `--stepper-list-icon-color` | `currentColor` | Icon colour for custom `indicator-icon` elements |
170
173
 
171
174
  ### Per-variant tokens
@@ -190,10 +193,10 @@ for full examples (global theming, page-scoped, and per-instance overrides).
190
193
  ## Connector behaviour
191
194
 
192
195
  - Connectors are drawn as `::after` pseudo-elements on each `<li>` except the last.
193
- - JS (`ResizeObserver`) measures the bottom of each indicator and the top of the next to set `--_connector-top` and `--_connector-height` precisely, keeping the line flush between bubbles regardless of content height.
196
+ - JS (`ResizeObserver`) measures the bottom of each indicator and the top of the next to set `--_connector-top` and `--_connector-height` precisely, keeping the line flush between bubbles regardless of content height. It also sets `--_connector-left` to the indicator's measured centre, so the line stays centred under a custom indicator wider than the counter bubble (before JS runs it falls back to half of `--_counter-size`).
194
197
  - Before JS runs (SSR / initial paint), CSS fallback values approximate the correct position for `indicatorAlignment="top"`.
195
198
  - If `connected="false"`, no connectors are rendered and JS measurement is skipped.
196
- - Connectors recalculate automatically when `itemCount`, `connected`, or `indicatorAlignment` props change.
199
+ - Connectors recalculate when `itemCount`, `connected` or `indicatorAlignment` change, when the list or any `<li>` resizes (a `ResizeObserver` on the list and every item), and when a `class` attribute or child nodes change anywhere inside the list (a `MutationObserver`). The last two cover a consumer restyling a step without the list changing size, e.g. ProgressTracker moving its focus card from one step to the next. Inline `style` changes are not watched, since the connector values themselves are written there.
197
200
 
198
201
  ---
199
202
 
@@ -222,6 +225,7 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
222
225
  /* --stepper-list-counter-font-size: 1.4rem; */
223
226
  /* --stepper-list-counter-font-weight: 600; */
224
227
  /* --stepper-list-connector-width: 0.2rem; */
228
+ /* --stepper-list-connector-gap: 0.6rem; */
225
229
 
226
230
  /* Counter bubble — disc variant */
227
231
  /* --stepper-list-counter-disc-background: var(--brand-primary); */
@@ -246,6 +250,7 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
246
250
 
247
251
  /* Connector line */
248
252
  /* --stepper-list-connector-color: var(--brand-primary); */
253
+ /* --stepper-list-connector-image: repeating-linear-gradient(to bottom, currentColor 0 0.4rem, transparent 0.4rem 0.8rem); */
249
254
  }
250
255
  }
251
256
  </style>
@@ -258,4 +263,6 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
258
263
  - Always keep `itemCount` in sync with the number of `#item-*` slots you provide — mismatches will render empty `<li>` rows.
259
264
  - Slot names are **zero-indexed** — `#item-0` not `#item-1`.
260
265
  - The `indicator-icon` class is defined inside the component CSS and sizes the SVG to `var(--_counter-size)` (driven by `indicatorSize` or `--stepper-list-counter-size`). Always add it to custom SVGs.
266
+ - Markup: root `<ul>`/`<ol>` with `role="list"` (Safari drops list semantics under `list-style: none`), each row `li.stepper-list-item`, content in `.stepper-list-content`. The content column is `minmax(0, 1fr)` with `overflow-wrap: anywhere`, so long unbroken strings wrap.
267
+ - Used by `ServiceDetail` for its process steps (circle variant, `tag="ol"`, item count from CMS data) and by `ProgressTracker`, which fills the indicator and item slots with done/current/upcoming states (skill `progress-tracker.md`).
261
268
  - Auto-imported in Nuxt — no manual import needed.
@@ -145,6 +145,10 @@ and the trailing edge catches up: the same stretch effect as before, symmetric b
145
145
  settle timer. CSS retargets each edge from its current position, so a quick second move can't
146
146
  pull an edge backwards. Hover and active have separate duration/delay variables.
147
147
 
148
+ `--tabs-hover-indicator-stretch` (default `1`) multiplies the hover pair's two edge delays in CSS,
149
+ so `0` turns the hover stretch into a plain slide (both edges move together over the same
150
+ duration). The active indicator and underline always stretch.
151
+
148
152
  This replaced a `translate` + `scale` model (a bar-wide box translated to the tab's left and
149
153
  scaled to its width, plus a timeout to settle after a stretch, and one shared
150
154
  `--_transition-duration`). Rightward moves animated both properties together in the settle step,
@@ -99,7 +99,8 @@ Each skill is a single markdown file named `<area>-<task>.md`.
99
99
  ├── skip-links.md — SkipLinks: focus-revealed accessibility skip-nav, links (SkipLink[]) data-driven, homeLink slot, CSS token API
100
100
  ├── tabbed-content.md — TabbedContent (was TabsCore): tablist + panels from indexed slots (itemCount), arrow-key nav, moving hover/active/underline indicators, CSS token API
101
101
  ├── contact-section.md — ContactSection props (stepperIndicatorSize pass-through), 3-item info+form layout, slot API
102
- ├── stepper-list.md — StepperList dynamic slots (item-{n}/indicator-{n}), props, connector behaviour
102
+ ├── stepper-list.md — StepperList dynamic slots (item-{n}/indicator-{n}), props, measured connectors (gap, dashed image tokens), per-step state recipe
103
+ ├── progress-tracker.md — ProgressTracker: steps (ProgressTrackerStep[]) + v-model current index over StepperList, header label + progressbar, current-step #step-{n} slot for wizard forms, focus on change, CSS token API
103
104
  ├── expanding-panel.md — ExpandingPanel v-model, forceOpened, contentIsOnTop overlay mode, slots (summary/icon/content), ARIA wiring, CSS token API
104
105
  ├── content-docs.md — ContentDocs docs-page shell: prop-driven docsNav/docsPageNav (not slots), DocsNavItem icons, container-width breakpoint behaviour, shared/per-side CSS token API
105
106
  ├── glass-panel.md — GlassPanel: frosted-glass container, light-only token defaults in-component (light-dark() block removed from _default.css, no built-in dark glass), highlight behind content, section/article labelled via headingId slot prop, CSS token API
@@ -242,6 +242,12 @@ export default meta;
242
242
  type Story = StoryObj<typeof ComponentName>; // ← still strict for individual stories
243
243
  ```
244
244
 
245
+ When the story assigns a model arg itself (a button doing `args.modelValue = index + 1`, a wizard's
246
+ Back/Continue), a type built from `InstanceType<typeof C>["$props"]` fails `vue-tsc` with "Cannot
247
+ assign to 'modelValue' because it is a read-only property". Re-declare it writable:
248
+ `Omit<InstanceType<typeof C>["$props"], "modelValue"> & { modelValue: number; ... }`
249
+ (`ProgressTracker.stories.ts`).
250
+
245
251
  Strip extra args before `v-bind` using a `useStorySetup` helper in `setup()`:
246
252
 
247
253
  ```ts
@@ -0,0 +1,45 @@
1
+ {
2
+ "SRCDEV ProgressTracker": {
3
+ "description": "ProgressTracker showing progress through steps (v-model is the 0-based current step)",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<ProgressTracker v-model=\"${1:currentStep}\" :steps=\"${2:steps}\" />"
7
+ ]
8
+ },
9
+ "SRCDEV ProgressTracker Wizard": {
10
+ "description": "ProgressTracker as a wizard: each step's form lives in its #step-{n} slot and drives the model",
11
+ "scope": "vue,html",
12
+ "body": [
13
+ "<ProgressTracker v-model=\"${1:currentStep}\" :steps=\"${2:steps}\">",
14
+ " <template #step-0=\"{ headingId }\">",
15
+ " <form :aria-labelledby=\"headingId\" @submit.prevent=\"$1++\">",
16
+ " $3",
17
+ " <InputButton type=\"submit\" button-text=\"Continue\" />",
18
+ " </form>",
19
+ " </template>",
20
+ " <template #step-1=\"{ headingId }\">",
21
+ " <form :aria-labelledby=\"headingId\" @submit.prevent=\"$1 = $2.length\">",
22
+ " $4",
23
+ " <InputButton button-text=\"Back\" variant=\"secondary\" @click=\"$1--\" />",
24
+ " <InputButton type=\"submit\" button-text=\"Submit\" />",
25
+ " </form>",
26
+ " </template>",
27
+ "</ProgressTracker>"
28
+ ]
29
+ },
30
+ "SRCDEV ProgressTracker CSS Override": {
31
+ "description": "CSS override scaffold for ProgressTracker tokens",
32
+ "scope": "css",
33
+ "body": [
34
+ ".$1my-page {",
35
+ " --progress-tracker-accent: $2;",
36
+ " --progress-tracker-track: $3;",
37
+ " --progress-tracker-card-surface: $4;",
38
+ " --progress-tracker-card-shadow: $5;",
39
+ " --progress-tracker-label-font-family: $6;",
40
+ " --progress-tracker-title-line-clamp: $7;",
41
+ " --progress-tracker-body-line-clamp: $8;",
42
+ "}"
43
+ ]
44
+ }
45
+ }
@@ -31,8 +31,9 @@
31
31
  ".$1my-page {",
32
32
  " --stepper-list-gap: $2;",
33
33
  " --stepper-list-connector-color: $3;",
34
- " --stepper-list-counter-disc-background: $4;",
35
- " --stepper-list-counter-disc-text: $5;",
34
+ " --stepper-list-connector-gap: $4;",
35
+ " --stepper-list-counter-disc-background: $5;",
36
+ " --stepper-list-counter-disc-text: $6;",
36
37
  "}"
37
38
  ]
38
39
  }
@@ -58,6 +58,7 @@
58
58
  " --tabs-underline-indicator-height: ;",
59
59
  " --tabs-hover-underline-indicator-colour: ;",
60
60
  " --tabs-hover-underline-indicator-height: ;",
61
+ " --tabs-hover-indicator-stretch: ;",
61
62
  " --tabs-list-item-opacity: ;",
62
63
  " --tabs-list-item-colour: ;",
63
64
  " --tabs-list-item-colour-selected: ;",
@@ -58,6 +58,7 @@ box.
58
58
  | `--tabs-underline-indicator-height` | `0.4rem` | Thickness of the underline (`axis="x"`), also its width on `axis="y"` |
59
59
  | `--tabs-hover-underline-indicator-colour` | `--tabs-underline-indicator-colour` | Colour of the thin underline/sideline that follows the hovered tab (`trackHover` and `trackIndicator`) |
60
60
  | `--tabs-hover-underline-indicator-height` | `0.1rem` | Its thickness; `0` hides it. Sits under the active underline where they overlap |
61
+ | `--tabs-hover-indicator-stretch` | `1` | Stretch of the hover highlight and hover underline as they move: `1` stretches (leading edge first, trailing edge catches up), `0` slides both edges together. Values in between shorten the stretch |
61
62
 
62
63
  ### More menu (`overflowMode="menu"`, `axis="x"`)
63
64
 
@@ -98,7 +99,10 @@ measured layout, per indicator (`active` / `hovered`): `--_{kind}-start` / `--_{
98
99
  insets from the bar's start and end along the axis), `--_{kind}-duration`,
99
100
  `--_{kind}-start-delay` / `--_{kind}-end-delay`; each indicator reads them through its own
100
101
  `--_start` / `--_end` / `--_duration` / `--_start-delay` / `--_end-delay`. Not public API; use the
101
- `transitionDuration` prop for indicator speed (one edge moves for that long, then the other).
102
+ `transitionDuration` prop for indicator speed (one edge moves for that long, then the other), and
103
+ `--tabs-hover-indicator-stretch` to turn the hover stretch off. The More menu also uses
104
+ `--_anchor-name` (a per-instance anchor name) and the `--_anchor-*` position values
105
+ `useAnchoredPopover` writes for browsers without CSS anchor positioning.
102
106
 
103
107
  > **Changed 2026-10-08**: indicators were a bar-wide box moved with `translate` and sized with
104
108
  > `scale` (`--_x-*`, `--_width-*`, `--_y-*`, a shared `--_transition-duration`), with a timer
@@ -354,8 +354,8 @@ watch(
354
354
  --_start: var(--_hovered-start, 0px);
355
355
  --_end: var(--_hovered-end, 100%);
356
356
  --_duration: var(--_hovered-duration, 0ms);
357
- --_start-delay: var(--_hovered-start-delay, 0ms);
358
- --_end-delay: var(--_hovered-end-delay, 0ms);
357
+ --_start-delay: calc(var(--_hovered-start-delay, 0ms) * var(--tabs-hover-indicator-stretch, 1));
358
+ --_end-delay: calc(var(--_hovered-end-delay, 0ms) * var(--tabs-hover-indicator-stretch, 1));
359
359
  }
360
360
 
361
361
  .tabbed-content-indicator-active,
@@ -219,6 +219,32 @@ export const IndicatorColours: Story = {
219
219
  }),
220
220
  };
221
221
 
222
+ export const HoverWithoutStretch: Story = {
223
+ name: "Hover Without Stretch",
224
+ args: { itemCount: 5 },
225
+ parameters: {
226
+ docs: {
227
+ description: {
228
+ story:
229
+ "--tabs-hover-indicator-stretch: 0 makes the hover highlight slide between tabs instead of stretching across and catching up. Clicking a tab still stretches the active indicator.",
230
+ },
231
+ },
232
+ },
233
+ render: (args) => ({
234
+ components: { TabbedContent },
235
+ setup() {
236
+ return { args };
237
+ },
238
+ template: `
239
+ <div style="--tabs-hover-indicator-stretch: 0">
240
+ <TabbedContent v-bind="args">
241
+ ${tabSlots}
242
+ </TabbedContent>
243
+ </div>
244
+ `,
245
+ }),
246
+ };
247
+
222
248
  // ─── Stress test ──────────────────────────────────────────────────────────────
223
249
 
224
250
  const stressTabs = [
@@ -4,22 +4,22 @@ exports[`ContactSection > renders correct HTML structure with default props 1`]
4
4
  "<div class="contact-section">
5
5
  <div class="contact-section-inner">
6
6
  <div class="contact-section-info">
7
- <ul class="stepper-list">
8
- <li class="has-indicator indicator-top indicator-disc">
7
+ <ul role="list" class="stepper-list">
8
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
9
9
  <div class="stepper-list__indicator-custom"></div>
10
- <div>
10
+ <div class="stepper-list-content">
11
11
  <p class="page-body-normal">Default slot content for item 1</p>
12
12
  </div>
13
13
  </li>
14
- <li class="has-indicator indicator-top indicator-disc">
14
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
15
15
  <div class="stepper-list__indicator-custom"></div>
16
- <div>
16
+ <div class="stepper-list-content">
17
17
  <p class="page-body-normal">Default slot content for item 2</p>
18
18
  </div>
19
19
  </li>
20
- <li class="has-indicator indicator-top indicator-disc">
20
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
21
21
  <div class="stepper-list__indicator-custom"></div>
22
- <div>
22
+ <div class="stepper-list-content">
23
23
  <p class="page-body-normal">Default slot content for item 3</p>
24
24
  </div>
25
25
  </li>
@@ -34,22 +34,22 @@ exports[`ContactSection > renders correct HTML structure with form slot 1`] = `
34
34
  "<div class="contact-section">
35
35
  <div class="contact-section-inner">
36
36
  <div class="contact-section-info">
37
- <ul class="stepper-list">
38
- <li class="has-indicator indicator-top indicator-disc">
37
+ <ul role="list" class="stepper-list">
38
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
39
39
  <div class="stepper-list__indicator-custom"></div>
40
- <div>
40
+ <div class="stepper-list-content">
41
41
  <p class="page-body-normal">Default slot content for item 1</p>
42
42
  </div>
43
43
  </li>
44
- <li class="has-indicator indicator-top indicator-disc">
44
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
45
45
  <div class="stepper-list__indicator-custom"></div>
46
- <div>
46
+ <div class="stepper-list-content">
47
47
  <p class="page-body-normal">Default slot content for item 2</p>
48
48
  </div>
49
49
  </li>
50
- <li class="has-indicator indicator-top indicator-disc">
50
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
51
51
  <div class="stepper-list__indicator-custom"></div>
52
- <div>
52
+ <div class="stepper-list-content">
53
53
  <p class="page-body-normal">Default slot content for item 3</p>
54
54
  </div>
55
55
  </li>
@@ -66,22 +66,22 @@ exports[`ContactSection > renders correct HTML structure with styleClassPassthro
66
66
  "<div class="contact-section custom-class another-class">
67
67
  <div class="contact-section-inner">
68
68
  <div class="contact-section-info">
69
- <ul class="stepper-list">
70
- <li class="has-indicator indicator-top indicator-disc">
69
+ <ul role="list" class="stepper-list">
70
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
71
71
  <div class="stepper-list__indicator-custom"></div>
72
- <div>
72
+ <div class="stepper-list-content">
73
73
  <p class="page-body-normal">Default slot content for item 1</p>
74
74
  </div>
75
75
  </li>
76
- <li class="has-indicator indicator-top indicator-disc">
76
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
77
77
  <div class="stepper-list__indicator-custom"></div>
78
- <div>
78
+ <div class="stepper-list-content">
79
79
  <p class="page-body-normal">Default slot content for item 2</p>
80
80
  </div>
81
81
  </li>
82
- <li class="has-indicator indicator-top indicator-disc">
82
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
83
83
  <div class="stepper-list__indicator-custom"></div>
84
- <div>
84
+ <div class="stepper-list-content">
85
85
  <p class="page-body-normal">Default slot content for item 3</p>
86
86
  </div>
87
87
  </li>
@@ -96,18 +96,18 @@ exports[`ContactSection > renders correctly with all props and slots combined 1`
96
96
  "<section class="contact-section combined-class">
97
97
  <div class="contact-section-inner">
98
98
  <div class="contact-section-info">
99
- <ul class="stepper-list">
100
- <li class="has-indicator indicator-top indicator-disc">
99
+ <ul role="list" class="stepper-list">
100
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
101
101
  <div class="stepper-list__indicator-custom"><span class="icon">✓</span></div>
102
- <div><span>Step one</span></div>
102
+ <div class="stepper-list-content"><span>Step one</span></div>
103
103
  </li>
104
- <li class="has-indicator indicator-top indicator-disc">
104
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
105
105
  <div class="stepper-list__indicator-custom"></div>
106
- <div><span>Step two</span></div>
106
+ <div class="stepper-list-content"><span>Step two</span></div>
107
107
  </li>
108
- <li class="has-indicator indicator-top indicator-disc">
108
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
109
109
  <div class="stepper-list__indicator-custom"></div>
110
- <div><span>Step three</span></div>
110
+ <div class="stepper-list-content"><span>Step three</span></div>
111
111
  </li>
112
112
  </ul>
113
113
  </div>