srcdev-nuxt-components 9.6.2 → 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 (58) hide show
  1. package/.claude/rules/consumer/consumer-overrides.md +17 -0
  2. package/.claude/rules/consumer/css-comment-slash.md +13 -0
  3. package/.claude/rules/consumer/css-native-nesting.md +13 -0
  4. package/.claude/rules/consumer/css-no-deep.md +13 -0
  5. package/.claude/rules/consumer/css-rem-base.md +11 -0
  6. package/.claude/rules/consumer/css-surface-text-pairing.md +13 -0
  7. package/.claude/rules/consumer/css-webkit-support.md +15 -0
  8. package/.claude/rules/consumer/explicit-vue-imports.md +11 -0
  9. package/.claude/rules/consumer/nuxt-img-dimensions.md +12 -0
  10. package/.claude/rules/consumer/srcdev-skills-location.md +9 -0
  11. package/.claude/rules/consumer/story-args.md +12 -0
  12. package/.claude/rules/consumer/story-register-components.md +12 -0
  13. package/.claude/rules/consumer/vue-auto-imports.md +14 -0
  14. package/.claude/rules/consumer/vue-no-self-closing.md +12 -0
  15. package/.claude/rules/consumer/vue-prop-hyphenation.md +11 -0
  16. package/.claude/rules/consumer/vue-props-pattern.md +13 -0
  17. package/.claude/rules/consumer/vue-slots-no-computed.md +14 -0
  18. package/.claude/rules/consumer/vue-video-source.md +12 -0
  19. package/.claude/skills/component-compliance-checklist.md +31 -3
  20. package/.claude/skills/component-inline-action-button.md +33 -2
  21. package/.claude/skills/component-naming.md +1 -1
  22. package/.claude/skills/components/cookie-consent-banner.md +9 -6
  23. package/.claude/skills/components/input-copy.md +47 -100
  24. package/.claude/skills/components/pricing-card.md +68 -84
  25. package/.claude/skills/composable-cookie-consent.md +1 -1
  26. package/.claude/skills/index.md +11 -5
  27. package/.claude/skills/new-app-scaffold.md +4 -2
  28. package/.claude/skills/setup-postinstall.md +5 -4
  29. package/.claude/skills/storybook-add-story.md +44 -0
  30. package/.claude/skills/testing-add-unit-test.md +19 -0
  31. package/.claude/skills/theming-component-token-pattern.md +47 -1
  32. package/.claude/skills/theming-partial-override.md +1 -1
  33. package/.vscode/srcdev-component-cookie-consent-banner.code-snippets +7 -5
  34. package/.vscode/srcdev-component-input-copy.code-snippets +24 -42
  35. package/.vscode/srcdev-component-pricing-card.code-snippets +46 -84
  36. package/README.md +5 -5
  37. package/app/components/01.atoms/animations/scroll-reveal-frame/stories/ScrollRevealFrame.stories.ts +2 -0
  38. package/app/components/01.atoms/animations/scroll-reveal-image/stories/ScrollRevealImage.stories.ts +2 -0
  39. package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +4 -0
  40. package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +2 -0
  41. package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +13 -4
  42. package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +23 -14
  43. package/app/components/02.molecules/cookie-consent-banner/stories/CookieConsentBanner.stories.ts +108 -5
  44. package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +33 -11
  45. package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +82 -124
  46. package/app/components/02.molecules/input-copy/InputCopy.vue +110 -85
  47. package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +204 -48
  48. package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +147 -171
  49. package/app/components/02.molecules/pricing-card/CONSUMER-STYLING.md +96 -149
  50. package/app/components/02.molecules/pricing-card/PricingCard.vue +84 -44
  51. package/app/components/02.molecules/pricing-card/stories/PricingCard.stories.ts +241 -104
  52. package/app/components/02.molecules/pricing-card/tests/PricingCard.spec.ts +149 -123
  53. package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +4 -0
  54. package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +4 -0
  55. package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabel.vue +1 -1
  56. package/app/composables/useAriaDescribedById.ts +1 -1
  57. package/app/types/components/cookie-consent-banner.d.ts +2 -0
  58. package/package.json +2 -1
@@ -0,0 +1,17 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ - "app/**/*.css"
5
+ ---
6
+
7
+ # Overriding srcdev-nuxt-components styles
8
+
9
+ Override a layer component through its public tokens (`--component-name-*`, listed in its
10
+ `CONSUMER-STYLING.md`) set on an ancestor you own. Never wrap override `<style>` blocks in a named
11
+ `@layer`, never set its private `--_` tokens, and override colours and geometry only, never
12
+ behaviour (`display`, `visibility`, `pointer-events`, `z-index`, animations). Applies in apps that
13
+ extend the layer, not to a component's own styles inside srcdev-nuxt-components.
14
+
15
+ Why: unlayered CSS always beats the layer's `@layer components`, while a named layer can be ordered
16
+ below it by SSR-inlined chunks; private tokens can change in any release.
17
+ Detail: skill `component-local-style-override.md` (srcdev-nuxt-components).
@@ -0,0 +1,13 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ - "app/**/*.css"
5
+ ---
6
+
7
+ # No `*/` inside CSS comments
8
+
9
+ Never join token names with a bare slash in a CSS comment (`--_form-*/--_scheme-icon-font-size`);
10
+ write "and" or space the slash.
11
+
12
+ Why: `*/` closes the comment early and yields a misleading "Unclosed string" error lines later.
13
+
@@ -0,0 +1,13 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ - "app/**/*.css"
5
+ ---
6
+
7
+ # Native CSS nesting, no Sass concatenation
8
+
9
+ Never write `&__child` or `&-modifier`. Use `& .block__child` or a top-level `.block__child {}` rule.
10
+ Nest child rules inside the component's root block.
11
+
12
+ Why: it's Sass syntax; esbuild turns `&__foo` into `:is(__foo)`, which silently matches nothing.
13
+ Detail: skill `css-nesting-conventions.md` (srcdev-nuxt-components).
@@ -0,0 +1,13 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ ---
5
+
6
+ # `:deep()` only in `<style scoped>`
7
+
8
+ Never use `:deep()` in an unscoped `<style>` block (srcdev-nuxt-components' own styles are never
9
+ scoped). There, reach a child component with a plain descendant selector (`& .input-button`), or
10
+ better, set the child's public tokens on an ancestor. In a `<style scoped>` block it works as normal.
11
+
12
+ Why: outside `<style scoped>` `:deep()` stays as an unknown pseudo-class and the browser silently
13
+ drops the whole rule.
@@ -0,0 +1,11 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ - "app/**/*.css"
5
+ ---
6
+
7
+ # 1rem = 10px
8
+
9
+ Calculate rem values on a 10px base (`html { font-size: 62.5% }`): `1.6rem` = 16px, `2.4rem` = 24px.
10
+
11
+ Why: the browser default 16px base gives wrong sizes.
@@ -0,0 +1,13 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ - "app/**/*.css"
5
+ ---
6
+
7
+ # Text on dark surfaces uses `--theme-on-surface`
8
+
9
+ Pair a dark/bold surface (`--theme-surface-hover`, `--theme-surface-inverted`) with
10
+ `--theme-on-surface`, never `--theme-text` or `--theme-text-inverted`.
11
+
12
+ Why: those text tokens assume a light surface, so the pairing renders dark-on-dark in light mode.
13
+ Detail: skill `theming-partial-override.md` (srcdev-nuxt-components).
@@ -0,0 +1,15 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ - "app/**/*.css"
5
+ ---
6
+
7
+ # Load-bearing CSS must work in WebKit / Safari 16
8
+
9
+ Never rely on a new CSS feature (`::details-content`, `@starting-style`, `appearance: base-select`,
10
+ anchor positioning, ...) for anything load-bearing unless Safari 16 has a working path; decorative
11
+ extras may degrade. Prefer routing to a proven alternative implementation over a CSS-only
12
+ `@supports` patch.
13
+
14
+ Why: WebKit silently drops unsupported rules, and Chrome's device emulation is still Blink so it
15
+ can't show the gap.
@@ -0,0 +1,11 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.spec.ts"
4
+ - "**/*.stories.ts"
5
+ ---
6
+
7
+ # Import Vue APIs explicitly in tests and stories
8
+
9
+ Always import `nextTick`, `computed`, `ref`, etc. from `"vue"` in `.spec.ts` and `.stories.ts` files.
10
+
11
+ Why: auto-imports only apply inside components.
@@ -0,0 +1,12 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ ---
5
+
6
+ # NuxtImg needs numeric width and height
7
+
8
+ Always give `NuxtImg` numeric `width` and `height` props, roughly the largest rendered pixel width
9
+ (pass a size prop through when it varies). Never `width="100%"`. CSS sizes the displayed image.
10
+
11
+ Why: without them the Vercel provider requests a width it doesn't allow (`w=1536`), and a
12
+ non-numeric width makes it request its largest screen width for every image.
@@ -0,0 +1,9 @@
1
+ # Where srcdev-nuxt-components skills live
2
+
3
+ A skill named as "skill `x.md` (srcdev-nuxt-components)" is at
4
+ `.claude/skills/srcdev-nuxt-components/x.md`. Per-component usage docs are in its `components/`
5
+ subfolder (`.claude/skills/srcdev-nuxt-components/components/<component-name>.md`). Inside the
6
+ srcdev-nuxt-components repo itself, the same files are directly under `.claude/skills/`.
7
+
8
+ Why: the layer's rules name skills rather than linking a path, so they read the same in the
9
+ library repo and in consuming apps.
@@ -0,0 +1,12 @@
1
+ ---
2
+ paths:
3
+ - "**/*.stories.ts"
4
+ ---
5
+
6
+ # Story args stay reactive
7
+
8
+ Never destructure, copy (`ref(args.x)`) or `watch` `args` at setup time. Strip non-prop extra args
9
+ inside a `computed` before `v-bind`, and type the meta as `Meta<StoryArgs>` when it has extra args.
10
+
11
+ Why: setup runs once, so copied args and watchers miss control changes, and unstripped extras become
12
+ unknown HTML attributes. Detail: skill `storybook-add-story.md` (srcdev-nuxt-components).
@@ -0,0 +1,12 @@
1
+ ---
2
+ paths:
3
+ - "**/*.stories.ts"
4
+ ---
5
+
6
+ # Register every component a story template uses
7
+
8
+ Every component in a story's `template` string, including ones placed in the component's slots, must
9
+ be listed in that story's `components: {}`.
10
+
11
+ Why: components aren't auto-registered in story templates; unregistered ones render as unknown tags.
12
+ Detail: skill `storybook-add-story.md` (srcdev-nuxt-components).
@@ -0,0 +1,14 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ - "app/composables/**/*.ts"
5
+ - "app/utils/**/*.ts"
6
+ ---
7
+
8
+ # Vue and Nuxt auto-imports
9
+
10
+ Never import `ref`, `reactive`, `computed`, `watch`, `nextTick`, `useState`, `useRoute`, `useRouter`
11
+ or anything from `~/composables/` in app code. Import only `type`s and external packages.
12
+
13
+ Why: Nuxt auto-imports them; manual imports are noise and drift from the rest of the codebase.
14
+ Test and story files are the exception (see `explicit-vue-imports.md`).
@@ -0,0 +1,12 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ ---
5
+
6
+ # No self-closing non-void elements
7
+
8
+ Always write explicit closing tags on non-void elements and slots: `<slot name="x"></slot>`,
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>`).
11
+
12
+ Why: ESLint `vue/html-self-closing` is set to `normal: "never"`, `component: "always"`.
@@ -0,0 +1,11 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ - "**/*.stories.ts"
5
+ ---
6
+
7
+ # Hyphenated props in templates
8
+
9
+ Always write camelCase props hyphenated in templates: `:item-count`, `:style-class-passthrough`.
10
+
11
+ Why: ESLint `vue/attribute-hyphenation` requires it.
@@ -0,0 +1,13 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ ---
5
+
6
+ # Props pattern
7
+
8
+ Always declare props as `interface Props` + `withDefaults(defineProps<Props>(), {...})`, with factory
9
+ functions for array/object defaults. Never use options-style `defineProps({ x: { type, default } })`.
10
+ Never declare `class` or `style` as props; hide them from Storybook controls via `argTypes` instead.
11
+
12
+ Why: typed props are the standard across srcdev-nuxt-components and its apps, and a
13
+ `class`/`style` prop breaks Vue's automatic `$attrs` inheritance.
@@ -0,0 +1,14 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ - "app/composables/**/*.ts"
5
+ ---
6
+
7
+ # Never derive state from slots in a computed
8
+
9
+ Never read `useSlots()` inside a `computed()`. Use a plain function called from the template
10
+ (`v-if="hasFooter()"`, `:aria-describedby="ariaDescribedby()"`), including anything derived from it.
11
+ Test by toggling a `v-if`'d slot in a host component after mount.
12
+
13
+ Why: the slots object isn't reactive, so the computed caches its first value and leaves split state
14
+ when a slot is added or removed after mount.
@@ -0,0 +1,12 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ ---
5
+
6
+ # Video src goes on a `<source>` child
7
+
8
+ Never bind `:src` on a `<video>`. Use `<source :src="src">` inside it, `:key="src"` on the video, and
9
+ an explicit `videoEl.load()` on mount and src change.
10
+
11
+ Why: Vue patching `:src` on client-side navigation silently skips the fetch.
12
+ Detail: skill `vue-video-autoplay.md` (srcdev-nuxt-components).
@@ -15,7 +15,7 @@ A component passes when its Component Ledger row (`node .claude/component-ledger
15
15
  `.claude/component-ledger/audit.json`) shows `score: 5`, a real tier, and `variants`, `legacy_props`,
16
16
  `story_args_bug`, `eslint_issues`, `redundant_priv_tokens`, `styling_doc_outdated` and `missing_stress_story`
17
17
  all `false`.
18
- The ledger can't see items 9, 10, 11 and 14, so check those by hand.
18
+ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
19
19
 
20
20
  ## The checklist
21
21
 
@@ -104,7 +104,8 @@ The ledger can't see items 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`
@@ -149,10 +150,27 @@ The ledger can't see items 9, 10, 11 and 14, so check those by hand.
149
150
  with nothing to stress (animation wrappers, decorative containers that only take a slot and
150
151
  tag/variant props, Storybook tooling) are exempt: they are listed in `stressStoryExempt` in
151
152
  `.claude/component-ledger/build.mjs`; add a new one there rather than writing a padding story.
153
+ 6c. **Storybook docs page** — every story file gets a generated Docs page: `tags: ["autodocs"]` is
154
+ set globally in `.storybook/preview.ts` (since 2026-10-14), so don't repeat it in the meta.
155
+ Open the component's Docs page and check that it reads well:
156
+ - A `parameters.docs.description.component` in the meta says what the component is for and
157
+ names its main props/slots/events. Per-story `docs.description.story` text explains what
158
+ that story shows (the Docs page is the only place either is visible).
159
+ - The props table is accurate. Every story-only arg has a `description` and a category, and
160
+ CSS-token args are labelled story-only (6b).
161
+ - Every story renders inline. A story that misbehaves on the shared page (fixed/sticky
162
+ overlays, `layout: "fullscreen"` canvases, dialogs, toasts, scroll effects, heavy stress
163
+ data) gets `parameters.docs.story: { inline: false, height: "<px>" }` so it renders in its
164
+ own iframe. Don't drop it from the page.
165
+ - A story file that isn't a component reference (Storybook tooling, a file that already has an
166
+ attached `.mdx` page) opts out with `tags: ["!autodocs"]`. `ColourRamps.stories.ts` does this.
167
+ See `storybook-add-story.md`, "Docs page".
152
168
  7. **Skill doc** — create or update `.claude/skills/components/<component-name>.md`, following the
153
169
  pattern of an existing one. A labelled `<Name>Field` wrapper (or any other wrapper) is documented
154
170
  in its control's doc under a "Variants" section, not in a doc of its own
155
- (`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
156
174
  `variants/` subfolder, flatten it into the parent folder (`git mv` the `.vue` files plus their
157
175
  stories/tests up one level, fix relative imports); `pathPrefix: false` keeps the auto-import
158
176
  names unchanged. All existing `variants/` folders were flattened 2026-09-25.
@@ -235,3 +253,13 @@ The ledger can't see items 9, 10, 11 and 14, so check those by hand.
235
253
  the edits in private memory. Note that an inline-size container can't size itself from its
236
254
  content: say so in `CONSUMER-STYLING.md` (`ProfileSection`).
237
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).
259
+ - No `:deep()` in the component's `<style>`: these blocks are never scoped, so Vue leaves it
260
+ as-is and the browser drops the whole rule (pitfall #27). Use a descendant selector, or set
261
+ the child component's public tokens on an ancestor.
262
+ - No `font-size` (or font-size token default) under `1rem`. `html` is `62.5%`, so `1rem` is
263
+ 10px, and values like `0.75rem`/`0.9rem` are almost always copied from a 16px-base design
264
+ and render at 7.5-9px. `PricingCard` had both (fixed 2026-10-15); grep
265
+ `font-size:\s*(var\([^,]+,\s*)?0?\.[0-9]+rem` to find them.
@@ -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)
@@ -58,7 +58,7 @@ stripping `Core`.
58
58
 
59
59
  1. `git mv` the `.vue` files, their stories and tests (and the skill doc if it's named after the old
60
60
  control name).
61
- 2. `sed` the old names across `app/`, `.claude/`, `.vscode/`, `MIGRATION.md` and `Claude.md`:
61
+ 2. `sed` the old names across `app/`, `.claude/`, `.vscode/`, `MIGRATION.md` and `CLAUDE.md`:
62
62
  component names, `import` paths, story titles/`*StoryArgs` interfaces, `describe()` names, and the
63
63
  root/base DOM class (update `CONSUMER-STYLING.md` and test selectors that use it).
64
64
  3. Add a "Renamed YYYY-MM-DD" note at the top of the skill doc listing old → new names and classes.
@@ -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`.