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.
- package/.claude/rules/consumer/consumer-overrides.md +17 -0
- package/.claude/rules/consumer/css-comment-slash.md +13 -0
- package/.claude/rules/consumer/css-native-nesting.md +13 -0
- package/.claude/rules/consumer/css-no-deep.md +13 -0
- package/.claude/rules/consumer/css-rem-base.md +11 -0
- package/.claude/rules/consumer/css-surface-text-pairing.md +13 -0
- package/.claude/rules/consumer/css-webkit-support.md +15 -0
- package/.claude/rules/consumer/explicit-vue-imports.md +11 -0
- package/.claude/rules/consumer/nuxt-img-dimensions.md +12 -0
- package/.claude/rules/consumer/srcdev-skills-location.md +9 -0
- package/.claude/rules/consumer/story-args.md +12 -0
- package/.claude/rules/consumer/story-register-components.md +12 -0
- package/.claude/rules/consumer/vue-auto-imports.md +14 -0
- package/.claude/rules/consumer/vue-no-self-closing.md +12 -0
- package/.claude/rules/consumer/vue-prop-hyphenation.md +11 -0
- package/.claude/rules/consumer/vue-props-pattern.md +13 -0
- package/.claude/rules/consumer/vue-slots-no-computed.md +14 -0
- package/.claude/rules/consumer/vue-video-source.md +12 -0
- package/.claude/skills/component-compliance-checklist.md +31 -3
- package/.claude/skills/component-inline-action-button.md +33 -2
- package/.claude/skills/component-naming.md +1 -1
- package/.claude/skills/components/cookie-consent-banner.md +9 -6
- package/.claude/skills/components/input-copy.md +47 -100
- package/.claude/skills/components/pricing-card.md +68 -84
- package/.claude/skills/composable-cookie-consent.md +1 -1
- package/.claude/skills/index.md +11 -5
- package/.claude/skills/new-app-scaffold.md +4 -2
- package/.claude/skills/setup-postinstall.md +5 -4
- package/.claude/skills/storybook-add-story.md +44 -0
- package/.claude/skills/testing-add-unit-test.md +19 -0
- package/.claude/skills/theming-component-token-pattern.md +47 -1
- package/.claude/skills/theming-partial-override.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/.vscode/srcdev-component-pricing-card.code-snippets +46 -84
- package/README.md +5 -5
- package/app/components/01.atoms/animations/scroll-reveal-frame/stories/ScrollRevealFrame.stories.ts +2 -0
- package/app/components/01.atoms/animations/scroll-reveal-image/stories/ScrollRevealImage.stories.ts +2 -0
- package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +4 -0
- package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +2 -0
- 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 +108 -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 -48
- package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +147 -171
- package/app/components/02.molecules/pricing-card/CONSUMER-STYLING.md +96 -149
- package/app/components/02.molecules/pricing-card/PricingCard.vue +84 -44
- package/app/components/02.molecules/pricing-card/stories/PricingCard.stories.ts +241 -104
- package/app/components/02.molecules/pricing-card/tests/PricingCard.spec.ts +149 -123
- package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +4 -0
- package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +4 -0
- package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabel.vue +1 -1
- package/app/composables/useAriaDescribedById.ts +1 -1
- package/app/types/components/cookie-consent-banner.d.ts +2 -0
- 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,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,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:**
|
|
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
|
-
##
|
|
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)
|
|
@@ -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 `
|
|
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
|
|
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`.
|