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.
- package/.claude/rules/consumer/vue-no-self-closing.md +3 -2
- package/.claude/skills/component-compliance-checklist.md +8 -2
- package/.claude/skills/component-inline-action-button.md +33 -2
- package/.claude/skills/components/cookie-consent-banner.md +9 -6
- package/.claude/skills/components/input-copy.md +47 -100
- package/.claude/skills/composable-cookie-consent.md +1 -1
- package/.claude/skills/index.md +2 -2
- package/.claude/skills/storybook-add-story.md +8 -0
- package/.claude/skills/testing-add-unit-test.md +19 -0
- package/.claude/skills/theming-component-token-pattern.md +1 -1
- package/.vscode/srcdev-component-cookie-consent-banner.code-snippets +7 -5
- package/.vscode/srcdev-component-input-copy.code-snippets +24 -42
- package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +13 -4
- package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +23 -14
- package/app/components/02.molecules/cookie-consent-banner/stories/CookieConsentBanner.stories.ts +106 -5
- package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +33 -11
- package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +82 -124
- package/app/components/02.molecules/input-copy/InputCopy.vue +110 -85
- package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +204 -47
- package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +147 -171
- package/app/types/components/cookie-consent-banner.d.ts +2 -0
- 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`
|
|
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:**
|
|
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
|
-
##
|
|
77
|
+
## What `variant="inline"` still brings
|
|
78
78
|
|
|
79
|
-
`
|
|
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
|
|
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
|
|
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
|
|
14
|
-
| -------------------------- |
|
|
15
|
-
| `:value` | `string`
|
|
16
|
-
| `:id` | `string`
|
|
17
|
-
| `:name` | `string`
|
|
18
|
-
| `:label` | `string`
|
|
19
|
-
| `:aria-label` | `string`
|
|
20
|
-
| `:description` | `string`
|
|
21
|
-
| `:button-text` | `string`
|
|
22
|
-
| `:copied-text` | `string`
|
|
23
|
-
| `:
|
|
24
|
-
| `:
|
|
25
|
-
| `:
|
|
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
|
|
32
|
-
| -------- |
|
|
33
|
-
| `copy` | `value: string`
|
|
34
|
-
| `copied` | `value: string`
|
|
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.
|
|
45
|
+
None.
|
|
41
46
|
|
|
42
47
|
---
|
|
43
48
|
|
|
44
|
-
##
|
|
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
|
-
|
|
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
|
|
55
|
+
## Usage examples
|
|
58
56
|
|
|
59
|
-
### License key display
|
|
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
|
-
###
|
|
68
|
+
### Translated copy
|
|
71
69
|
|
|
72
70
|
```vue
|
|
73
71
|
<InputCopy
|
|
74
72
|
:value="apiToken"
|
|
75
|
-
label="
|
|
76
|
-
description="
|
|
77
|
-
button-text="
|
|
78
|
-
copied-text="
|
|
79
|
-
|
|
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
|
|
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
|
-
###
|
|
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
|
-
.
|
|
117
|
-
--input-copy-input-border:
|
|
118
|
-
--input-copy-button-bg: var(--
|
|
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
|
-
-
|
|
159
|
-
-
|
|
160
|
-
-
|
|
161
|
-
-
|
|
162
|
-
-
|
|
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**:
|
|
169
|
-
- **
|
|
170
|
-
-
|
|
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
|
|
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
|
|
package/.claude/skills/index.md
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
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
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
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": "
|
|
3
|
+
"description": "InputCopy: readonly input with a copy-to-clipboard button",
|
|
4
4
|
"scope": "vue,html",
|
|
5
5
|
"body": [
|
|
6
6
|
"<InputCopy",
|
|
7
|
-
" value=\"$
|
|
8
|
-
" label=\"$
|
|
9
|
-
" description=\"$
|
|
10
|
-
" @
|
|
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
|
|
15
|
-
"description": "InputCopy
|
|
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=\"
|
|
20
|
-
" label=\"
|
|
21
|
-
" description=\"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
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=\"$
|
|
35
|
-
" label=\"$
|
|
36
|
-
" :is-disabled=\"true\"",
|
|
37
|
-
"
|
|
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
|
-
".$
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
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`
|
|
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.
|