srcdev-nuxt-components 9.6.3 → 9.6.4

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 (22) hide show
  1. package/.claude/rules/consumer/vue-no-self-closing.md +3 -2
  2. package/.claude/skills/component-compliance-checklist.md +8 -2
  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/composable-cookie-consent.md +1 -1
  7. package/.claude/skills/index.md +2 -2
  8. package/.claude/skills/storybook-add-story.md +8 -0
  9. package/.claude/skills/testing-add-unit-test.md +19 -0
  10. package/.claude/skills/theming-component-token-pattern.md +1 -1
  11. package/.vscode/srcdev-component-cookie-consent-banner.code-snippets +7 -5
  12. package/.vscode/srcdev-component-input-copy.code-snippets +24 -42
  13. package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +13 -4
  14. package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +23 -14
  15. package/app/components/02.molecules/cookie-consent-banner/stories/CookieConsentBanner.stories.ts +106 -5
  16. package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +33 -11
  17. package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +82 -124
  18. package/app/components/02.molecules/input-copy/InputCopy.vue +110 -85
  19. package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +204 -47
  20. package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +147 -171
  21. package/app/types/components/cookie-consent-banner.d.ts +2 -0
  22. 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`
@@ -167,7 +168,9 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
167
168
  7. **Skill doc** — create or update `.claude/skills/components/<component-name>.md`, following the
168
169
  pattern of an existing one. A labelled `<Name>Field` wrapper (or any other wrapper) is documented
169
170
  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
171
+ (`feedback_variants_deprecated_as_own_skill_docs`). **Existing component:** check every
172
+ behavioural claim (event order and timing, what's disabled when, breakpoints) against the code,
173
+ not just the props table: `InputCopy`'s doc said `copy` fired before the clipboard write. **Existing component:** if you find a
171
174
  `variants/` subfolder, flatten it into the parent folder (`git mv` the `.vue` files plus their
172
175
  stories/tests up one level, fix relative imports); `pathPrefix: false` keeps the auto-import
173
176
  names unchanged. All existing `variants/` folders were flattened 2026-09-25.
@@ -250,6 +253,9 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
250
253
  the edits in private memory. Note that an inline-size container can't size itself from its
251
254
  content: say so in `CONSUMER-STYLING.md` (`ProfileSection`).
252
255
  - Grid tracks that hold text are `minmax(0, …)` so an unbroken string can't widen the grid.
256
+ - Action buttons are `InputButton` (`variant="inline"` when the wrapper styles them), not a raw
257
+ `<button>` (`component-inline-button.md`, `component-inline-action-button.md`). The ledger
258
+ doesn't check this, which is how `InputCopy` scored 5/5 with one (fixed 2026-10-15).
253
259
  - No `:deep()` in the component's `<style>`: these blocks are never scoped, so Vue leaves it
254
260
  as-is and the browser drops the whole rule (pitfall #27). Use a descendant selector, or set
255
261
  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`.
@@ -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
 
@@ -106,7 +106,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
106
106
  ├── pricing-card.md — PricingCard: plan card with highlight badge, corner ribbon, feature list, #cta slot, heading-tag, line-clamp tokens
107
107
  ├── price-list.md — PriceList: service/menu price columns (dl rows), PriceListData type, headingTag/fromLabel props, CSS token API
108
108
  ├── 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
109
+ ├── 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
110
  ├── 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
111
  ├── grid-stack.md — GridStack: CSS Grid z-axis stacking, slot API, z-order rules, sizing, video+overlay and image+text patterns
112
112
  ├── 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 +136,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
136
136
  ├── site-header.md — SiteHeader: PageRow + SkipLinks + ResponsiveHeader composition, #branding/#secondaryNavigation slots, dual styleClassPassthrough hooks
137
137
  ├── responsive-header.md — ResponsiveHeader: overflow-collapsing adaptive nav, measurement-pipeline gotchas (unsized icons, vw font-size drift), full CSS token API
138
138
  ├── 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
139
+ ├── 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
140
  ├── display-banner.md — DisplayBanner: canvas/content stacked overlay banner, conditional slot wrappers, CSS token API
141
141
  ├── deep-expanding-menu.md — DeepExpandingMenu: anchor-positioned popover nav panels, browser support caveat, CSS token API
142
142
  ├── deep-expanding-menu-classic.md — DeepExpandingMenuClassic: <details>-based fallback nav, click-outside close, CSS token API
@@ -307,6 +307,14 @@ the earlier nightly `@nuxtjs/storybook` didn't render Docs pages at all, so a lo
307
307
  },
308
308
  ```
309
309
 
310
+ - A fixed or teleported overlay (banner, toast, dialog) can't be narrowed by `CanvasSwitcher`: it
311
+ positions against the viewport and may not even live inside the decorator. Its `StressTest`
312
+ (compliance checklist item 6b) renders **one** instance, switches between hostile data sets with
313
+ a story-only select (`stressCase`, category `"Story only"`, description starting
314
+ `**Story control, not a prop.**`), tells the reader to use Storybook's viewport toolbar for narrow
315
+ screens, and sets `docs.story: { inline: false }`. `CookieConsentBanner` and `DisplayToast`
316
+ follow this. A teleported component doesn't inherit a wrapper's `dir`, so wrap RTL slot copy
317
+ in `<span dir="rtl">`.
310
318
  - Opt a file out with `tags: ["!autodocs"]` only when it isn't a component reference (tooling, or
311
319
  a file with its own attached `.mdx` page, like `ColourRamps.stories.ts` with `SetupGuide.mdx`).
312
320
 
@@ -201,6 +201,25 @@ await nextTick();
201
201
  vi.runAllTimers();
202
202
  ```
203
203
 
204
+ ### Native clicks are silently dropped under frozen time
205
+
206
+ Vue's event invoker ignores an event whose timestamp is at or before the moment its listener was
207
+ attached (a guard against an event that triggered the patch also firing the new listener). With
208
+ the clock frozen, a native `button.click()` or `dispatchEvent()` straight after mount carries the
209
+ same timestamp, so the handler never runs and the test fails with "called 0 times". Whether it
210
+ bites depends on render timing: `CookieConsentBanner`'s click tests passed with raw `<button>`s
211
+ and failed once they became `InputButton`s (2026-10-15). Either use test-utils'
212
+ `wrapper.trigger("click")` (which works, including on teleported content via `findComponent`), or
213
+ advance the clock first:
214
+
215
+ ```ts
216
+ // Vue ignores events stamped at or before its listener's attach time; fake timers freeze both.
217
+ function click(button: HTMLElement) {
218
+ vi.advanceTimersByTime(1);
219
+ button.click();
220
+ }
221
+ ```
222
+
204
223
  ## `mountSuspended` flushes component async setup — initial state may be past the first `await`
205
224
 
206
225
  `mountSuspended` calls `flushPromises()` internally, draining all pending microtasks including `await nextTick()` calls inside the component's async functions. If a component's async lifecycle starts with `await nextTick()` (e.g. to let a CSS `v-bind` update before beginning a transition), **the component will have already executed past it by the time `mountSuspended` returns**.
@@ -184,7 +184,7 @@ raw value) chains through it:
184
184
 
185
185
  ```css
186
186
  /* InputCopy's copy button defaults to InputButton's primary look */
187
- --_button-bg: var(--input-copy-button-bg, var(--input-button-primary-surface, var(--theme-surface)));
187
+ background-color: var(--input-copy-button-bg, var(--input-button-primary-surface, var(--theme-surface)));
188
188
  ```
189
189
 
190
190
  Three tiers deep here: instance/global override → "look like the primary button" → the theme
@@ -10,11 +10,13 @@
10
10
  "description": "CookieConsentBanner with translated copy and accessible name",
11
11
  "scope": "vue,html",
12
12
  "body": [
13
- "<CookieConsentBanner :aria-label=\"${1:t('global.cookieConsent.ariaLabel')}\">",
14
- " <template #title>{{ ${2:t('global.cookieConsent.title')} }}</template>",
15
- " <template #message>{{ ${3:t('global.cookieConsent.message')} }}</template>",
16
- " <template #acceptLabel>{{ ${4:t('global.cookieConsent.accept')} }}</template>",
17
- " <template #rejectLabel>{{ ${5:t('global.cookieConsent.reject')} }}</template>",
13
+ "<CookieConsentBanner",
14
+ " :aria-label=\"${1:t('global.cookieConsent.ariaLabel')}\"",
15
+ " :accept-label=\"${2:t('global.cookieConsent.accept')}\"",
16
+ " :reject-label=\"${3:t('global.cookieConsent.reject')}\"",
17
+ ">",
18
+ " <template #title>{{ ${4:t('global.cookieConsent.title')} }}</template>",
19
+ " <template #message>{{ ${5:t('global.cookieConsent.message')} }}</template>",
18
20
  "</CookieConsentBanner>"
19
21
  ]
20
22
  },
@@ -1,28 +1,27 @@
1
1
  {
2
2
  "SRCDEV InputCopy Basic": {
3
- "description": "Basic InputCopy component with value and label",
3
+ "description": "InputCopy: readonly input with a copy-to-clipboard button",
4
4
  "scope": "vue,html",
5
5
  "body": [
6
6
  "<InputCopy",
7
- " value=\"$1sk_live_abc123xyz\"",
8
- " label=\"$2API key\"",
9
- " description=\"$3Copy this key to authenticate with our API.\"",
10
- " @copy=\"$4handleCopy\"",
7
+ " :value=\"${1:licenseKey}\"",
8
+ " label=\"${2:License key}\"",
9
+ " description=\"${3:Copy this key to activate the component in your application.}\"",
10
+ " @copied=\"${4:handleCopied}\"",
11
11
  "/>"
12
12
  ]
13
13
  },
14
- "SRCDEV InputCopy License Key": {
15
- "description": "InputCopy for displaying a license key on success page",
14
+ "SRCDEV InputCopy Translated": {
15
+ "description": "InputCopy with translated copy and accessible names",
16
16
  "scope": "vue,html",
17
17
  "body": [
18
18
  "<InputCopy",
19
- " :value=\"licenseKey\"",
20
- " label=\"License key\"",
21
- " description=\"Your license key is ready to use. Copy it to embed in your application.\"",
22
- " buttonText=\"Copy\"",
23
- " copiedText=\"Copied to clipboard!\"",
24
- " @copy=\"$1handleCopy\"",
25
- " @copied=\"$2showCopiedFeedback\"",
19
+ " :value=\"${1:value}\"",
20
+ " :label=\"${2:t('account.key.label')}\"",
21
+ " :description=\"${3:t('account.key.description')}\"",
22
+ " :button-text=\"${4:t('common.copy')}\"",
23
+ " :copied-text=\"${5:t('common.copied')}\"",
24
+ " :button-aria-label=\"${6:t('account.key.copyAriaLabel')}\"",
26
25
  "/>"
27
26
  ]
28
27
  },
@@ -31,25 +30,10 @@
31
30
  "scope": "vue,html",
32
31
  "body": [
33
32
  "<InputCopy",
34
- " :value=\"$1value\"",
35
- " label=\"$2Label\"",
36
- " :is-disabled=\"true\"",
37
- " :button-text=\"$3Unavailable\"",
38
- "/>"
39
- ]
40
- },
41
- "SRCDEV InputCopy in Page": {
42
- "description": "InputCopy with custom aria labels",
43
- "scope": "vue,html",
44
- "body": [
45
- "<InputCopy",
46
- " :value=\"$1value\"",
47
- " label=\"$2License key\"",
48
- " aria-label=\"$3Your unique GuideMyHair license identifier\"",
49
- " description=\"$4Keep this key safe. You'll need it to embed the widget.\"",
50
- " button-text=\"$5Copy key\"",
51
- " copied-text=\"$6Key copied!\"",
52
- " @copy=\"$7logCopy\"",
33
+ " :value=\"${1:value}\"",
34
+ " label=\"${2:Label}\"",
35
+ " :is-disabled=\"${3:true}\"",
36
+ " button-text=\"${4:Unavailable}\"",
53
37
  "/>"
54
38
  ]
55
39
  },
@@ -57,15 +41,13 @@
57
41
  "description": "CSS override scaffold for InputCopy tokens",
58
42
  "scope": "css",
59
43
  "body": [
60
- ".$1success-page {",
61
- " .input-copy {",
62
- " --input-copy-input-bg: $2;",
63
- " --input-copy-input-border: $3;",
64
- " --input-copy-input-border-radius: $4;",
65
- " --input-copy-button-bg: $5;",
66
- " --input-copy-button-bg-hover: $6;",
67
- " --input-copy-button-text-color: $7;",
68
- " }",
44
+ ".${1:account-page} .input-copy {",
45
+ " --input-copy-input-bg: ${2:var(--theme-input-surface)};",
46
+ " --input-copy-input-border: ${3:0.1rem solid var(--theme-border)};",
47
+ " --input-copy-button-bg: ${4:var(--theme-surface)};",
48
+ " --input-copy-button-bg-hover: ${5:var(--theme-surface-hover)};",
49
+ " --input-copy-button-text-color: ${6:var(--theme-on-surface)};",
50
+ " --input-copy-description-line-clamp: ${7:none};",
69
51
  "}"
70
52
  ]
71
53
  }
@@ -6,6 +6,11 @@ colour, border, radius, padding, accent stripe, icon, title and message sizes, a
6
6
  `app/components/02.molecules/alert-content/CONSUMER-STYLING.md`. This guide covers what the banner
7
7
  adds: position, motion and the two buttons.
8
8
 
9
+ > **Changed 2026-10-15**: the closed state class `.closed` is now `.cookie-consent-banner-closed`,
10
+ > and the buttons are `InputButton`s (`variant="inline"`, so the banner's tokens still style them):
11
+ > each button is `.cookie-consent-banner-accept.input-button` / `.cookie-consent-banner-reject.input-button`
12
+ > with the label in an inner `.button-text`. The button tokens below are unchanged.
13
+ >
9
14
  > **Changed 2026-09-27**: rebuilt on `AlertContent`. The banner's own panel tokens
10
15
  > (`--cookie-consent-banner-padding`, `-gap`, `-border-radius`, `-border`, `-accent-border-width`,
11
16
  > `-background`, `-text-colour`, `-actions-gap`) are gone; use the matching `--alert-content-*`
@@ -41,7 +46,10 @@ adds: position, motion and the two buttons.
41
46
  | `--cookie-consent-banner-accept-text-colour` | `var(--theme-on-surface)` | Accept button text |
42
47
 
43
48
  The gap between the buttons and their alignment (right by default) are `AlertContent`'s
44
- `--alert-content-actions-gap` and `--alert-content-actions-justify`.
49
+ `--alert-content-actions-gap` and `--alert-content-actions-justify`. Long labels wrap inside the
50
+ buttons (`InputButton`'s `.button-text`), and the buttons wrap onto their own lines when the row is full.
51
+ The label keeps the banner's font size and case: `InputButton`'s `--button-font-size` and
52
+ `--button-text-transform` don't apply here.
45
53
 
46
54
  Private tokens (not public API): `--_gutter`, `--_transition-duration`, `--_accent` on the root,
47
55
  each resolving a public token once for reuse.
@@ -50,11 +58,12 @@ each resolving a public token once for reuse.
50
58
 
51
59
  - `data-theme` on the root and on the inner `.alert-content`, from the `theme` prop or `app.config`
52
60
  (default `info`). It drives every `--theme-*` default, for the panel and the buttons.
53
- - `.closed` on the root once consent is granted or denied. The banner fades out and becomes
61
+ - `.cookie-consent-banner-closed` on the root once consent is granted or denied. The banner fades out and becomes
54
62
  `visibility: hidden`, so its buttons leave the tab order and the accessibility tree.
55
63
  - Classes: `.cookie-consent-banner` > `.cookie-consent-banner-inner` (the `role="region"`) >
56
64
  `.alert-content` (see its guide for the inner structure); the buttons are
57
- `.cookie-consent-banner-reject` and `.cookie-consent-banner-accept` inside `.alert-content-actions`.
65
+ `InputButton`s with `.cookie-consent-banner-reject` and `.cookie-consent-banner-accept` (plus
66
+ `.input-button.inline`) inside `.alert-content-actions`.
58
67
  - `data-test-id`: `cookie-consent-banner`, `cookie-consent-banner-reject`, `cookie-consent-banner-accept`,
59
68
  plus `AlertContent`'s `alert-icon`, `alert-title`, `alert-content`, `alert-actions`.
60
69
 
@@ -91,5 +100,5 @@ The pre-2026-09-27 look (white panel, dark text, no tint), scoped to the banner:
91
100
  ## Class passthrough
92
101
 
93
102
  `style-class-passthrough` adds classes to the teleported root `.cookie-consent-banner`, next to
94
- `data-theme` and `.closed`. It's reachable in normal use and is the most reliable place to set
103
+ `data-theme` and `.cookie-consent-banner-closed`. It's reachable in normal use and is the most reliable place to set
95
104
  tokens for this component, since ancestor wrappers don't contain it.