srcdev-nuxt-components 9.6.3 → 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 (45) hide show
  1. package/.claude/rules/consumer/vue-no-self-closing.md +3 -2
  2. package/.claude/skills/component-compliance-checklist.md +17 -3
  3. package/.claude/skills/component-inline-action-button.md +33 -2
  4. package/.claude/skills/components/cookie-consent-banner.md +9 -6
  5. package/.claude/skills/components/input-copy.md +47 -100
  6. package/.claude/skills/components/progress-tracker.md +140 -0
  7. package/.claude/skills/components/stepper-list.md +10 -3
  8. package/.claude/skills/components/tabbed-content.md +4 -0
  9. package/.claude/skills/composable-cookie-consent.md +1 -1
  10. package/.claude/skills/index.md +4 -3
  11. package/.claude/skills/storybook-add-story.md +14 -0
  12. package/.claude/skills/testing-add-unit-test.md +19 -0
  13. package/.claude/skills/theming-component-token-pattern.md +1 -1
  14. package/.vscode/srcdev-component-cookie-consent-banner.code-snippets +7 -5
  15. package/.vscode/srcdev-component-input-copy.code-snippets +24 -42
  16. package/.vscode/srcdev-component-progress-tracker.code-snippets +45 -0
  17. package/.vscode/srcdev-component-stepper-list.code-snippets +3 -2
  18. package/.vscode/srcdev-component-tabbed-content.code-snippets +1 -0
  19. package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +5 -1
  20. package/app/components/01.atoms/tabbed-content/TabbedContent.vue +2 -2
  21. package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +26 -0
  22. package/app/components/02.molecules/contact-section/tests/__snapshots__/ContactSection.spec.ts.snap +28 -28
  23. package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +13 -4
  24. package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +23 -14
  25. package/app/components/02.molecules/cookie-consent-banner/stories/CookieConsentBanner.stories.ts +106 -5
  26. package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +33 -11
  27. package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +82 -124
  28. package/app/components/02.molecules/input-copy/InputCopy.vue +110 -85
  29. package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +204 -47
  30. package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +147 -171
  31. package/app/components/02.molecules/progress-tracker/CONSUMER-STYLING.md +116 -0
  32. package/app/components/02.molecules/progress-tracker/ProgressTracker.vue +275 -0
  33. package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +424 -0
  34. package/app/components/02.molecules/progress-tracker/tests/ProgressTracker.spec.ts +253 -0
  35. package/app/components/02.molecules/progress-tracker/tests/__snapshots__/ProgressTracker.spec.ts.snap +46 -0
  36. package/app/components/02.molecules/stepper-list/CONSUMER-STYLING.md +42 -7
  37. package/app/components/02.molecules/stepper-list/StepperList.vue +42 -19
  38. package/app/components/02.molecules/stepper-list/stories/StepperList.stories.ts +288 -2
  39. package/app/components/02.molecules/stepper-list/tests/StepperList.spec.ts +161 -34
  40. package/app/components/02.molecules/stepper-list/tests/__snapshots__/StepperList.spec.ts.snap +40 -40
  41. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +7 -7
  42. package/app/types/components/cookie-consent-banner.d.ts +2 -0
  43. package/app/types/components/index.ts +1 -0
  44. package/app/types/components/progress-tracker.d.ts +6 -0
  45. package/package.json +1 -1
@@ -6,6 +6,7 @@ paths:
6
6
  # No self-closing non-void elements
7
7
 
8
8
  Always write explicit closing tags on non-void elements and slots: `<slot name="x"></slot>`,
9
- `<span></span>`. Never `<slot name="x" />`.
9
+ `<span></span>`. Never `<slot name="x" />`. Components are the opposite: self-close them when
10
+ they have no content (`<InputButton :button-text="label" />`, not `<InputButton ...></InputButton>`).
10
11
 
11
- Why: ESLint `vue/html-self-closing` rejects it.
12
+ Why: ESLint `vue/html-self-closing` is set to `normal: "never"`, `component: "always"`.
@@ -104,7 +104,8 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
104
104
  Sample copy in stories, and examples in the skill doc, use placeholders (a name like "Alex
105
105
  Morgan", a town like "Northtown"), never a real client's name, town, business or site: the
106
106
  stories and docs ship in the public package. **Existing component:** check for, and replace, any
107
- copy lifted from a consumer app (`ProfileSection` had both).
107
+ copy lifted from a consumer app (`ProfileSection` had both), in the stories, the snippet and the
108
+ skill doc (`InputCopy` had one in a story and one in a snippet).
108
109
  6a. **Storybook Controls-panel reactivity** — check every story `Template`/`render` function in
109
110
  the component's `stories/*.stories.ts` files for this bug: `@storybook/vue3` mounts the story
110
111
  component **once** and, on every Controls-panel change, mutates the same reactive `args`
@@ -129,7 +130,10 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
129
130
  number of items
130
131
  - emoji (including at the start of anything that takes initials or first letters), HTML-like
131
132
  text (must render as text), right-to-left text
132
- - 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)
133
137
  - broken image or link URLs
134
138
  Describe what to check in the story's docs description, and look at it at every
135
139
  `CanvasSwitcher` width. Fixes usually mean `overflow-wrap: anywhere`, `min-inline-size: 0` on
@@ -167,7 +171,9 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
167
171
  7. **Skill doc** — create or update `.claude/skills/components/<component-name>.md`, following the
168
172
  pattern of an existing one. A labelled `<Name>Field` wrapper (or any other wrapper) is documented
169
173
  in its control's doc under a "Variants" section, not in a doc of its own
170
- (`feedback_variants_deprecated_as_own_skill_docs`). **Existing component:** if you find a
174
+ (`feedback_variants_deprecated_as_own_skill_docs`). **Existing component:** check every
175
+ behavioural claim (event order and timing, what's disabled when, breakpoints) against the code,
176
+ not just the props table: `InputCopy`'s doc said `copy` fired before the clipboard write. **Existing component:** if you find a
171
177
  `variants/` subfolder, flatten it into the parent folder (`git mv` the `.vue` files plus their
172
178
  stories/tests up one level, fix relative imports); `pathPrefix: false` keeps the auto-import
173
179
  names unchanged. All existing `variants/` folders were flattened 2026-09-25.
@@ -176,6 +182,8 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
176
182
  9. **Accessibility** — check for gaps beyond what already-passing tests would catch:
177
183
  - Interactive controls (buttons, toggles, custom form-like widgets) have an accessible name —
178
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).
179
187
  - Keyboard behaviour matches what's actually implemented — no keydown case that only calls
180
188
  `event.preventDefault()` with a comment like "could add this later", and no screen-reader
181
189
  copy (visible or `sr-only`) that describes an interaction the component doesn't actually
@@ -250,6 +258,12 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
250
258
  the edits in private memory. Note that an inline-size container can't size itself from its
251
259
  content: say so in `CONSUMER-STYLING.md` (`ProfileSection`).
252
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).
264
+ - Action buttons are `InputButton` (`variant="inline"` when the wrapper styles them), not a raw
265
+ `<button>` (`component-inline-button.md`, `component-inline-action-button.md`). The ledger
266
+ doesn't check this, which is how `InputCopy` scored 5/5 with one (fixed 2026-10-15).
253
267
  - No `:deep()` in the component's `<style>`: these blocks are never scoped, so Vue leaves it
254
268
  as-is and the browser drops the whole rule (pitfall #27). Use a descendant selector, or set
255
269
  the child component's public tokens on an ancestor.
@@ -74,6 +74,37 @@ Target `.my-action-button.input-button` inside the wrapper to override InputButt
74
74
  }
75
75
  ```
76
76
 
77
- ## Real example
77
+ ## What `variant="inline"` still brings
78
78
 
79
- `InputCopyCore` — `app/components/forms/input-copy/InputCopyCore.vue`
79
+ `inline` adds no colours, but the base `.input-button` rule and its inner `.button-text` still
80
+ apply. Override what the wrapper doesn't want, on the `.my-action-button.input-button` selector:
81
+
82
+ - `box-sizing: content-box`: set `border-box` if the button has a `max-inline-size: 100%` or sits
83
+ in a wrapping flex row, or padding pushes it past its container.
84
+ - `min-height: var(--button-min-height)`: set `auto` to size from your own padding.
85
+ - `grid-auto-flow: column` layout, `overflow: hidden` and a `transition: all` (replace it with
86
+ your own `transition` list).
87
+ - `.button-text` takes `font-size: var(--button-font-size)` and
88
+ `text-transform: var(--button-text-transform)`: set `font-size: inherit; text-transform: none`
89
+ on `& .button-text` to keep the wrapper's type. It already wraps unbroken text
90
+ (`overflow-wrap: anywhere`, with `white-space` from `--input-button-text-white-space`).
91
+ - The label is the `buttonText` string prop; there's no label slot. A wrapper that had label
92
+ slots on a raw `<button>` turns them into string props when it switches (a breaking change).
93
+
94
+ `InputButton` always renders `data-theme` (default `"default"`). That value has no hue rule of
95
+ its own, so the button keeps the parent's `data-theme` palette and `--theme-*` reads inside it
96
+ still follow the wrapper's theme. Don't pass a theme just to keep it in step.
97
+
98
+ ## Temporary "done" states (copied, saved)
99
+
100
+ When the button briefly blocks repeat clicks after it succeeds, use `InputButton`'s `readonly` prop
101
+ (`aria-disabled` plus a click guard), not `disabled`: disabling the focused button drops keyboard
102
+ focus. Keep `disabled` for a genuinely unavailable control. If the button has an `aria-label`, a
103
+ changed visible label ("Copied!") isn't announced, because the aria-label wins; add a visually hidden
104
+ `role="status"` span carrying the message. `InputCopy` does both (2026-10-15).
105
+
106
+ ## Real examples
107
+
108
+ - `InputPasswordWithLabel`: `app/components/05.forms/input-text/InputPasswordWithLabel.vue` (show/hide toggle attached to the input)
109
+ - `CookieConsentBanner`: `app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue`
110
+ (standalone Accept/Reject actions, not attached to an input)
@@ -16,6 +16,8 @@ Fixed, non-modal banner built on `AlertContent` (see `alert-content.md`), shown
16
16
  |---|---|---|---|
17
17
  | `theme` | `SemanticTheme` | `"info"` | Passed to `AlertContent`: panel colours, accent stripe and the accept button, via `--theme-*`. |
18
18
  | `ariaLabel` | `string` | `"Cookie consent"` | Accessible name for the banner's `role="region"`. Pass translated copy in non-English apps. |
19
+ | `acceptLabel` | `string` | `"Accept"` | Accept button text (an `InputButton`'s `buttonText`). |
20
+ | `rejectLabel` | `string` | `"Reject"` | Reject button text. |
19
21
  | `icon` | `string` | `"material-symbols:cookie-outline"` | Iconify name for the alert icon (`AlertContent`'s `customIcon`). |
20
22
  | `showIcon` | `boolean` | `true` | Render the icon column. |
21
23
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root element. |
@@ -41,10 +43,8 @@ Resolution chain: **prop → app.config → hardcoded fallback**.
41
43
  |---|---|---|
42
44
  | `title` | Title line above the message | "Cookies" |
43
45
  | `message` | The consent message body | "This site uses cookies to understand how it's used. You can accept or reject them." |
44
- | `acceptLabel` | Accept button text | "Accept" |
45
- | `rejectLabel` | Reject button text | "Reject" |
46
46
 
47
- Per [[feedback_i18n_required]] in consuming apps, always fill these slots with `t()`-sourced copy rather than relying on the English defaults.
47
+ Per [[feedback_i18n_required]] in consuming apps, always fill these slots and the label props with `t()`-sourced copy rather than relying on the English defaults.
48
48
 
49
49
  ## Basic usage
50
50
 
@@ -61,11 +61,13 @@ useAnalytics();
61
61
  <div class="page-layout">
62
62
  <slot />
63
63
  <DisplayToastProvider position="top" alignment="right" :max-visible="3" />
64
- <CookieConsentBanner :aria-label="t('global.cookieConsent.ariaLabel')">
64
+ <CookieConsentBanner
65
+ :aria-label="t('global.cookieConsent.ariaLabel')"
66
+ :accept-label="t('global.cookieConsent.accept')"
67
+ :reject-label="t('global.cookieConsent.reject')"
68
+ >
65
69
  <template #title>{{ t("global.cookieConsent.title") }}</template>
66
70
  <template #message>{{ t("global.cookieConsent.message") }}</template>
67
- <template #acceptLabel>{{ t("global.cookieConsent.accept") }}</template>
68
- <template #rejectLabel>{{ t("global.cookieConsent.reject") }}</template>
69
71
  </CookieConsentBanner>
70
72
  </div>
71
73
  </template>
@@ -85,3 +87,4 @@ The banner is **teleported to `<body>`**, so set tokens on `:root`/`html`/`body`
85
87
  - To let a visitor change their mind later (e.g. from a cookie-policy page), call `useCookieConsent().rejectAll()` or clear the `privacy-notice-consent` cookie — the banner reappears since `status` returns to `"unset"` only once the cookie is gone; `rejectAll()` itself sets it to `"denied"`, which keeps the banner hidden but stops GA. Expose a dedicated "reset my choice" affordance if you want the banner itself to resurface.
86
88
  - 2026-09-27 migration: classes/tokens/test ids renamed `privacy-notice-banner` → `cookie-consent-banner`; `light-dark()` defaults replaced with light values plus an explicit text colour; single-use private pass-throughs inlined and the hardcoded spacing, button and colour values promoted to public tokens; `ariaLabel` prop (was a hardcoded "Cookie consent"); closed banner hidden from keyboard and screen readers; visible `:focus-visible` outline on the buttons; reduced-motion support; `styleClassPassthrough` now reactive. Added CONSUMER-STYLING.md and a snippet.
87
89
  - 2026-09-27 (later the same day): rebuilt on `AlertContent` (`:custom-icon`, `:show-icon`, title/message/actions slots). New `title` slot (default "Cookies") and `icon`/`showIcon` props. The panel now follows the semantic theme in light and dark mode instead of a fixed white panel, with a left accent stripe instead of a top border and right-aligned buttons. The banner's own panel tokens were removed in favour of `--alert-content-*`.
90
+ - 2026-10-15: `StressTest` story (worst-case copy: long German, unbroken strings and URLs, emoji/HTML-like text, RTL, empty title). **Breaking:** the buttons are now `InputButton variant="inline"`, so the `acceptLabel`/`rejectLabel` slots became string props of the same name (`:accept-label`, `:reject-label`); the closed state class `.closed` became `.cookie-consent-banner-closed`. The `--cookie-consent-banner-button-*`/`-accept-*`/`-reject-*` tokens are unchanged.
@@ -2,7 +2,9 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- `InputCopy` renders a readonly text input with a copy-to-clipboard button. Displays a copyable value (e.g., license key, API token) with optional description text below. Shows visual feedback ("Copied!") in the button after a successful copy. Emits `copy` and `copied` events for consumer-side toast/feedback handling.
5
+ `InputCopy` renders a readonly text input with a copy-to-clipboard button (an inline `InputButton`), with optional description text below. After a successful copy the button shows `copiedText` and `copiedIcon` for `copiedDuration` ms, a visually hidden status region announces `copiedText`, and the button stays focusable but `aria-disabled` so repeat clicks are ignored without losing focus. Emits `copy` and `copied` for consumer-side toast/feedback handling.
6
+
7
+ **Location**: `app/components/02.molecules/input-copy/InputCopy.vue`
6
8
 
7
9
  ---
8
10
 
@@ -10,53 +12,49 @@
10
12
 
11
13
  > **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:style-class-passthrough`.
12
14
 
13
- | Prop (template form) | Type | Default | Notes |
14
- | -------------------------- | ---------------------- | ---------------- | ------------------------------------------------------------ |
15
- | `:value` | `string` | (required) | The text to copy to clipboard (e.g., license key, API token).|
16
- | `:id` | `string` | auto-generated | Id applied to the input. Only pass one if something needs to target this input directly. |
17
- | `:name` | `string` | falls back to `id` | Name applied to the input. Falls back to the resolved `id` (auto-generated or explicit) when not provided. |
18
- | `:label` | `string` | (optional) | Short label for the field; used in aria-label if ariaLabel not set. |
19
- | `:aria-label` | `string` | (optional) | Custom aria-label for the input; overrides label. |
20
- | `:description` | `string` | (optional) | Helper text displayed below the input. |
21
- | `:button-text` | `string` | `"Copy"` | Label on the copy button. |
22
- | `:copied-text` | `string` | `"Copied!"` | Text shown in button after copy succeeds. |
23
- | `:copied-duration` | `number` | `2000` | Milliseconds the "Copied!" state persists before resetting. |
24
- | `:is-disabled` | `boolean` | `false` | Disables the copy button. |
25
- | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root element. |
15
+ | Prop (template form) | Type | Default | Notes |
16
+ | -------------------------- | -------------------- | ------------------ | ----- |
17
+ | `:value` | `string` | (required) | The text copied to the clipboard. |
18
+ | `:id` | `string` | auto-generated | Id on the input. Only pass one if something needs to target it. |
19
+ | `:name` | `string` | falls back to `id` | Name on the input. |
20
+ | `:label` | `string` | `undefined` | Accessible name for the input (not rendered visibly) and part of the default button aria-label. |
21
+ | `:aria-label` | `string` | `undefined` | Accessible name for the input; overrides `label`. |
22
+ | `:description` | `string` | `undefined` | Helper text below the input, linked with `aria-describedby`. |
23
+ | `:button-text` | `string` | `"Copy"` | Button label. |
24
+ | `:copied-text` | `string` | `"Copied!"` | Button label and status announcement after a copy. |
25
+ | `:button-aria-label` | `string` | `"Copy <label> to clipboard"` | Accessible name for the button. Pass translated copy in non-English apps. |
26
+ | `:icon` | `string` | `"lucide:copy"` | Iconify name for the resting icon. |
27
+ | `:copied-icon` | `string` | `"lucide:check"` | Iconify name shown after a copy. |
28
+ | `:copied-duration` | `number` | `2000` | Milliseconds the copied state lasts. |
29
+ | `:is-disabled` | `boolean` | `false` | Disables the copy button. |
30
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra classes on the root element. |
26
31
 
27
32
  ---
28
33
 
29
34
  ## Events
30
35
 
31
- | Event | Payload | Notes |
32
- | -------- | -------------------- | ---------------------------------------------------------- |
33
- | `copy` | `value: string` | Emitted when the copy button is clicked (before clipboard write). |
34
- | `copied` | `value: string` | Emitted after successful clipboard write; fires after `copy`. |
36
+ | Event | Payload | Notes |
37
+ | -------- | --------------- | ----- |
38
+ | `copy` | `value: string` | Emitted after the clipboard write succeeds. |
39
+ | `copied` | `value: string` | Emitted straight after `copy`, same payload. Neither fires if the write fails (it logs `console.error`). |
35
40
 
36
41
  ---
37
42
 
38
43
  ## Slots
39
44
 
40
- None. Use events to customize feedback behavior.
45
+ None.
41
46
 
42
47
  ---
43
48
 
44
- ## CSS Token Customization
45
-
46
- All `--input-copy-*` tokens can be overridden at global, page, or instance scope. See `CONSUMER-STYLING.md` in the component directory for full token documentation and examples.
49
+ ## Styling
47
50
 
48
- **Common tokens:**
49
- - `--input-copy-input-bg` — input field background
50
- - `--input-copy-input-border` — input field border
51
- - `--input-copy-button-bg` — copy button background
52
- - `--input-copy-button-bg-hover` — button background on hover
53
- - `--input-copy-description-color` — description text color
51
+ Tokens are `--input-copy-*` (layout gap, input, button, description including `--input-copy-description-line-clamp`), defaulting from `--theme-*`. Full list and defaults in `CONSUMER-STYLING.md` next to the component. The root is an inline-size container: below `48rem` of its own width the button shows only its icon.
54
52
 
55
53
  ---
56
54
 
57
- ## Usage Examples
55
+ ## Usage examples
58
56
 
59
- ### License key display (most common)
57
+ ### License key display
60
58
 
61
59
  ```vue
62
60
  <InputCopy
@@ -67,21 +65,20 @@ All `--input-copy-*` tokens can be overridden at global, page, or instance scope
67
65
  />
68
66
  ```
69
67
 
70
- ### API token with custom button text
68
+ ### Translated copy
71
69
 
72
70
  ```vue
73
71
  <InputCopy
74
72
  :value="apiToken"
75
- label="API token"
76
- description="Do not share this token with anyone."
77
- button-text="Copy token"
78
- copied-text="Token copied!"
79
- @copy="logCopy"
80
- @copied="logCopied"
73
+ :label="t('account.apiToken.label')"
74
+ :description="t('account.apiToken.description')"
75
+ :button-text="t('common.copy')"
76
+ :copied-text="t('common.copied')"
77
+ :button-aria-label="t('account.apiToken.copyAriaLabel')"
81
78
  />
82
79
  ```
83
80
 
84
- ### Disabled state (e.g., loading or error)
81
+ ### Disabled state
85
82
 
86
83
  ```vue
87
84
  <InputCopy
@@ -93,79 +90,29 @@ All `--input-copy-*` tokens can be overridden at global, page, or instance scope
93
90
  />
94
91
  ```
95
92
 
96
- ### With custom styling via CSS tokens
97
-
98
- ```vue
99
- <InputCopy
100
- :value="shareLink"
101
- label="Share link"
102
- description="Share this link with others"
103
- style="
104
- --input-copy-input-bg: #f0f0f0;
105
- --input-copy-button-bg: var(--brand-primary);
106
- --input-copy-button-text-color: white;
107
- "
108
- />
109
- ```
110
-
111
- ### With page-scoped styling
112
-
113
- In your page's `<style>` block:
93
+ ### Page-scoped styling
114
94
 
115
95
  ```css
116
- .success-page .input-copy {
117
- --input-copy-input-border: 2px solid var(--success-color);
118
- --input-copy-button-bg: var(--success-color);
119
- --input-copy-description-color: var(--slate-06);
96
+ .account-page .input-copy {
97
+ --input-copy-input-border: 0.2rem solid var(--theme-border);
98
+ --input-copy-button-bg: var(--theme-surface-inverted);
120
99
  }
121
100
  ```
122
101
 
123
- Then in the template:
124
-
125
- ```vue
126
- <div class="success-page">
127
- <InputCopy
128
- :value="licenseKey"
129
- label="Your license key"
130
- description="Copy this key to activate the component in your application."
131
- />
132
- </div>
133
- ```
134
-
135
- ### With toast feedback (consumer-side)
136
-
137
- ```vue
138
- <template>
139
- <InputCopy
140
- :value="apiKey"
141
- label="API key"
142
- @copied="handleCopied"
143
- />
144
- </template>
145
-
146
- <script setup>
147
- const handleCopied = (value: string) => {
148
- // Use your app's toast library
149
- useToast().success(`Copied: ${value.slice(0, 8)}...`);
150
- };
151
- </script>
152
- ```
153
-
154
102
  ---
155
103
 
156
104
  ## Accessibility
157
105
 
158
- - Input always has `aria-label` set from `label` or `aria-label` prop
159
- - Button has descriptive aria-label: "Copy [label] to clipboard"
160
- - Button is disabled during the "Copied!" feedback window to prevent repeated clicks
161
- - Icon-only display on mobile (<480px); text shown on desktop for clarity
162
- - Readonly input has `readonly` attribute; not editable
106
+ - The input's accessible name comes from `aria-label` or `label`; `description` is linked with `aria-describedby`.
107
+ - The button's accessible name is `buttonAriaLabel`, defaulting to "Copy [label] to clipboard". It's set as `aria-label`, so it overrides the visible label.
108
+ - A visually hidden `role="status"` region announces `copiedText` after a copy (the button's aria-label doesn't change, so screen readers wouldn't otherwise hear it).
109
+ - During the copied state the button is `aria-disabled`, not `disabled`, so keyboard focus stays on it.
110
+ - Visible `:focus-visible` rings on the input and the button.
163
111
 
164
112
  ---
165
113
 
166
114
  ## Notes
167
115
 
168
- - **Clipboard API**: Uses native `navigator.clipboard.writeText()`. Fails silently if unavailable (e.g., insecure context).
169
- - **Button text hide on mobile**: By default, only the copy icon displays on screens <480px. Text appears on desktop (≥480px).
170
- - **No toast built-in**: This component does not show toasts. Listen to `@copied` event and use your app's toast library for feedback.
171
- - **Manual reset**: The "Copied!" state automatically resets after `copied-duration` milliseconds. No manual reset needed.
116
+ - **Clipboard API**: uses `navigator.clipboard.writeText()`, which needs a secure context. A failure logs to the console and leaves the resting state.
117
+ - **No toast built-in**: listen to `@copied` and use the app's own toast (e.g. `DisplayToastProvider`).
118
+ - 2026-10-15 migration: raw `<button>` replaced with `InputButton variant="inline"`; new `buttonAriaLabel`, `icon`, `copiedIcon` props; status announcement, `aria-describedby`, copied state via `aria-disabled` instead of `disabled`; theme colour defaults and readable font sizes; container query replaces the 480px viewport media query; `.input-copy__button-text` became InputButton's `.button-text`; StressTest story and `--input-copy-description-line-clamp`.
@@ -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,
@@ -18,7 +18,7 @@
18
18
 
19
19
  ### 2. Render the banner
20
20
 
21
- Pair it with [[cookie-consent-banner]] (`CookieConsentBanner.vue`), registered once in the app's default layout — see that component's skill doc for placement and copy slots.
21
+ Pair it with [[cookie-consent-banner]] (`CookieConsentBanner.vue`), registered once in the app's default layout — see that component's skill doc for placement, copy slots and label props.
22
22
 
23
23
  ## Composable reference
24
24
 
@@ -99,14 +99,15 @@ 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
106
107
  ├── pricing-card.md — PricingCard: plan card with highlight badge, corner ribbon, feature list, #cta slot, heading-tag, line-clamp tokens
107
108
  ├── price-list.md — PriceList: service/menu price columns (dl rows), PriceListData type, headingTag/fromLabel props, CSS token API
108
109
  ├── opening-hours.md — OpeningHours: per-day data auto-grouped into ranges, split sessions, 24h/appointment, exceptions, Intl formatting, today highlight, JSON-LD, CSS token API
109
- ├── input-copy.md — InputCopy: readonly copy-to-clipboard input, visual feedback, Clipboard API, accessibility, CSS token API
110
+ ├── input-copy.md — InputCopy: readonly copy-to-clipboard input with an inline InputButton, copied state + status announcement, translatable labels/icons, container-query icon-only button, --input-copy-* tokens
110
111
  ├── banner-video.md — BannerVideo: full-width hero video banner, depth tier system, objectFit/objectPosition, playIcon/pauseIcon/toggle-icon slot, reduced-motion fallback, CSS tokens
111
112
  ├── grid-stack.md — GridStack: CSS Grid z-axis stacking, slot API, z-order rules, sizing, video+overlay and image+text patterns
112
113
  ├── scroll-reveal-frame.md — ScrollRevealFrame: generic parallax clipping frame, slot API, image grid pattern, public --scroll-reveal-frame-* tokens (props only write them when passed), browser support
@@ -136,7 +137,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
136
137
  ├── site-header.md — SiteHeader: PageRow + SkipLinks + ResponsiveHeader composition, #branding/#secondaryNavigation slots, dual styleClassPassthrough hooks
137
138
  ├── responsive-header.md — ResponsiveHeader: overflow-collapsing adaptive nav, measurement-pipeline gotchas (unsized icons, vw font-size drift), full CSS token API
138
139
  ├── navigation-items.md — NavigationItems: internal overflow-panel renderer for ResponsiveHeader, complement-visibility logic, not used standalone
139
- ├── cookie-consent-banner.md — CookieConsentBanner: fixed non-modal Accept/Reject banner built on AlertContent, driven by useCookieConsent; title/message/acceptLabel/rejectLabel slots, icon/showIcon/ariaLabel props, position/button tokens (panel via --alert-content-*), teleported so tokens go on html/passthrough
140
+ ├── cookie-consent-banner.md — CookieConsentBanner: fixed non-modal Accept/Reject banner built on AlertContent, driven by useCookieConsent; title/message slots, acceptLabel/rejectLabel/icon/showIcon/ariaLabel props, InputButton (inline) actions, position/button tokens (panel via --alert-content-*), teleported so tokens go on html/passthrough
140
141
  ├── display-banner.md — DisplayBanner: canvas/content stacked overlay banner, conditional slot wrappers, CSS token API
141
142
  ├── deep-expanding-menu.md — DeepExpandingMenu: anchor-positioned popover nav panels, browser support caveat, CSS token API
142
143
  ├── deep-expanding-menu-classic.md — DeepExpandingMenuClassic: <details>-based fallback nav, click-outside close, CSS token API