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
@@ -2,7 +2,11 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- `PricingCard` renders a SaaS-style pricing plan card with plan name, price, description, feature list, and call-to-action button. Supports "Most Popular" highlight state with optional badge. Features are highly customizable via CSS token overrides, and the CTA button can be customized via the `#cta` slot for custom variants and themes.
5
+ `PricingCard` (`app/components/02.molecules/pricing-card/`) renders a pricing plan card: plan name,
6
+ price, billing period, description, feature list and a call-to-action button. `is-highlighted`
7
+ adds a "Most Popular" badge plus a scaled, emphasised border; `ribbon-text` adds a diagonal corner
8
+ ribbon. The CTA can be replaced through the `#cta` slot. Styling tokens:
9
+ `app/components/02.molecules/pricing-card/CONSUMER-STYLING.md`.
6
10
 
7
11
  ---
8
12
 
@@ -10,34 +14,42 @@
10
14
 
11
15
  > **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:style-class-passthrough`.
12
16
 
13
- | Prop (template form) | Type | Default | Notes |
14
- | -------------------------- | ----------------------------- | ------------ | ------------------------------------------------------------------ |
15
- | `tag` | `"div" \| "section" \| "article"` | `"article"` | Root element tag for semantic HTML. |
16
- | `:plan-name` | `string` | (required) | Plan/tier name (e.g., "Basic", "Professional", "Enterprise"). |
17
- | `:price` | `number` | (required) | Price amount (e.g., `99`, `4.99`). Currency symbol added in template. |
18
- | `:billing-period` | `string` | `"one-time"` | Billing frequency text (e.g., "monthly", "yearly"). |
19
- | `:description` | `string` | (optional) | Short plan description. |
20
- | `:features` | `string[]` | `[]` | Array of feature strings. Rendered with checkmark icons. |
21
- | `:is-highlighted` | `boolean` | `false` | Highlight state; applies "Most Popular" badge, border, shadow, and scale. |
22
- | `:cta-text` | `string` | `"Get started"` | Button label text. |
23
- | `:cta-disabled` | `boolean` | `false` | Disables the CTA button. |
24
- | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the card root. |
17
+ | Prop (template form) | Type | Default | Notes |
18
+ | -------------------------- | -------------------------------------- | ---------------- | ----- |
19
+ | `tag` | `"div" \| "section" \| "article"` | `"article"` | Root element. Always labelled by the plan name heading (`aria-labelledby`). |
20
+ | `heading-tag` | `"h2" \| "h3" \| "h4" \| "h5" \| "h6"` | `"h3"` | Heading level for the plan name; pick to fit the page outline. |
21
+ | `plan-name` | `string` | (required) | Plan name. |
22
+ | `:price` | `number` | (required) | Whole numbers show as-is, fractional ones to two decimals (`9.5` → `9.50`). A non-finite value shows nothing. |
23
+ | `currency-symbol` | `string` | `"$"` | Literal prefix; no locale formatting. Pass `""` for none. |
24
+ | `billing-period` | `string` | `"one-time"` | Shown after the price; `""` hides it. |
25
+ | `description` | `string` | `undefined` | Short plan description. |
26
+ | `:features` | `string[]` | `[]` | Rendered as a checkmark list (checkmark hidden from screen readers). |
27
+ | `:is-highlighted` | `boolean` | `false` | Badge, highlight border/shadow, and `scale(1.05)`. |
28
+ | `badge-text` | `string` | `"Most Popular"` | Badge copy; pass a translated string for i18n. |
29
+ | `cta-text` | `string` | `"Get started"` | Default button label. |
30
+ | `:cta-disabled` | `boolean` | `false` | Sets the default button to `readonly`/`aria-disabled`. |
31
+ | `ribbon-text` | `string` | `undefined` | Corner ribbon copy; unset for no ribbon. Caps at two lines (`--pricing-card-ribbon-line-clamp`). |
32
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra classes on the root. |
33
+
34
+ All visitor-facing copy (`badge-text`, `cta-text`, `billing-period`) is a prop, so pass strings from
35
+ your own i18n solution.
25
36
 
26
37
  ---
27
38
 
28
39
  ## Events
29
40
 
30
- | Event | Payload | Notes |
31
- | ------ | ---------- | ---------------------------------------- |
32
- | `select` | `planName: string` | Emitted when the CTA button is clicked. |
41
+ | Event | Payload | Notes |
42
+ | -------- | ------------------ | ----- |
43
+ | `select` | `planName: string` | Emitted when the default CTA is clicked, or when a `#cta` slot calls `onSelect`. |
33
44
 
34
45
  ---
35
46
 
36
47
  ## Slots
37
48
 
38
- ### Default slot (features)
49
+ ### `#features`
39
50
 
40
- Override the default feature list rendering. Receives no slot scope.
51
+ Replaces the default `<li>` list. Your content goes inside the component's `<ul>`, so render `<li>`
52
+ elements. `--pricing-card-feature-line-clamp` and the checkmark only apply to the built-in list.
41
53
 
42
54
  ```vue
43
55
  <PricingCard plan-name="Pro" :price="99">
@@ -47,97 +59,57 @@ Override the default feature list rendering. Receives no slot scope.
47
59
  </PricingCard>
48
60
  ```
49
61
 
50
- ### #cta slot
62
+ ### `#cta`
51
63
 
52
- Customize the CTA button. Default renders `InputButton` with theme support.
64
+ Replaces the default `InputButton`. Slot scope:
53
65
 
54
- **Slot scope:**
55
- - `ctaText: string` — button label from `:cta-text` prop
56
- - `isDisabled: boolean` — button disabled state from `:cta-disabled` prop
57
- - `planName: string` — plan name for context
58
- - `onSelect: () => void` — callback to trigger `select` event
66
+ - `ctaText: string` — from `cta-text`
67
+ - `isDisabled: boolean` — from `cta-disabled`
68
+ - `planName: string`
69
+ - `onSelect: () => void` — emits `select`
59
70
 
60
71
  ```vue
61
72
  <PricingCard plan-name="Pro" :price="99" @select="handleSelect">
62
73
  <template #cta="{ ctaText, isDisabled, onSelect }">
63
- <InputButton
64
- :button-text="ctaText"
65
- variant="secondary"
66
- theme="custom"
67
- :readonly="isDisabled"
68
- @click="onSelect"
69
- />
74
+ <InputButton :button-text="ctaText" variant="secondary" :readonly="isDisabled" @click="onSelect" />
70
75
  </template>
71
76
  </PricingCard>
72
77
  ```
73
78
 
74
79
  ---
75
80
 
76
- ## CSS Token Customization
81
+ ## Styling
77
82
 
78
- All `--pricing-card-*` tokens can be overridden at global, page, or instance scope. See `CONSUMER-STYLING.md` in the component directory for full token documentation and examples.
83
+ All `--pricing-card-*` tokens are listed in `CONSUMER-STYLING.md`. The common ones:
79
84
 
80
- **Common tokens:**
81
- - `--pricing-card-padding` — card internal spacing
82
- - `--pricing-card-border-radius` — card corners
83
- - `--pricing-card-cta-bg` — button background
84
- - `--pricing-card-highlight-border` — highlighted card border
85
- - `--pricing-card-name-color` — plan name text color
85
+ - `--pricing-card-padding`, `--pricing-card-border-radius`, `--pricing-card-background`
86
+ - `--pricing-card-highlight-border`, `--pricing-card-highlight-scale`
87
+ - `--pricing-card-name-line-clamp`, `--pricing-card-description-line-clamp`, `--pricing-card-feature-line-clamp` (default `none`)
88
+ - `--pricing-card-feature-icon-color` (checkmark colour)
89
+
90
+ The default CTA is styled with `InputButton`'s own tokens (`--input-button-primary-surface`, etc.),
91
+ set on the card or an ancestor. There are no `--pricing-card-cta-*` colour tokens: the old ones never
92
+ applied and were removed 2026-10-14. `--pricing-card-cta-align` still controls CTA alignment.
86
93
 
87
94
  ---
88
95
 
89
- ## Usage Examples
96
+ ## Usage examples
90
97
 
91
- ### Single card with default button
98
+ ### Single card
92
99
 
93
100
  ```vue
94
101
  <PricingCard
95
102
  plan-name="Basic"
96
103
  :price="29.99"
97
- billing-period="monthly"
104
+ currency-symbol="£"
105
+ billing-period="per month"
98
106
  description="Perfect for getting started"
99
107
  :features="['Feature 1', 'Feature 2', 'Feature 3']"
100
- cta-text="Get started"
101
- @select="handleSelect"
102
- />
103
- ```
104
-
105
- ### Highlighted "Most Popular" card
106
-
107
- ```vue
108
- <PricingCard
109
- plan-name="Professional"
110
- :price="79.99"
111
- billing-period="monthly"
112
- :is-highlighted="true"
113
- :features="['All Basic features', 'Priority support', 'Advanced analytics']"
114
108
  @select="handleSelect"
115
109
  />
116
110
  ```
117
111
 
118
- ### Card with custom button variant
119
-
120
- ```vue
121
- <PricingCard
122
- plan-name="Enterprise"
123
- :price="199.99"
124
- billing-period="monthly"
125
- description="Everything you need"
126
- :features="['Unlimited access', 'Dedicated support', 'Custom integrations']"
127
- @select="handleSelect"
128
- >
129
- <template #cta="{ ctaText, isDisabled, onSelect }">
130
- <InputButton
131
- :button-text="ctaText"
132
- variant="primary"
133
- :readonly="isDisabled"
134
- @click="onSelect"
135
- />
136
- </template>
137
- </PricingCard>
138
- ```
139
-
140
- ### Three-column grid with token customization
112
+ ### Comparison grid
141
113
 
142
114
  ```vue
143
115
  <div class="pricing-grid">
@@ -148,16 +120,28 @@ All `--pricing-card-*` tokens can be overridden at global, page, or instance sco
148
120
  :price="plan.price"
149
121
  :is-highlighted="plan.id === 'pro'"
150
122
  :features="plan.features"
123
+ heading-tag="h2"
151
124
  @select="handleSelect"
152
- style="--pricing-card-padding: 2.5rem; --pricing-card-cta-bg: var(--brand-primary)"
153
125
  />
154
126
  </div>
155
127
  ```
156
128
 
129
+ ```css
130
+ .pricing-grid {
131
+ display: grid;
132
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 26rem), 1fr));
133
+ gap: 2.4rem;
134
+ }
135
+ ```
136
+
157
137
  ---
158
138
 
159
139
  ## Notes
160
140
 
161
- - Feature checkmarks are always rendered with a teal checkmark (`✓`). Customize the color via `--teal-06` or component tokens.
162
- - The `#cta` slot is preferred over CSS-only button customization when you need variant or theme changes.
163
- - Highlighted cards apply a 1.05x scale transform — consider z-index when nesting in flex/grid layouts.
141
+ - The highlight scale is a `transform`, so it doesn't change layout size. In a stacked single
142
+ column set `--pricing-card-highlight-scale: 1`, or the highlighted card overhangs.
143
+ - The badge sits centred on the top border (`translate: 0 -50%`), so leave some space above the
144
+ card (grid gap or padding).
145
+ - The plan name gets `--pricing-card-ribbon-clearance` (`6rem`) of right padding while a ribbon is
146
+ shown, so long names don't run underneath it.
147
+ - Stories: `Molecules/Pricing Card`, including `Stress Test (Worst-Case Data)`.
@@ -18,7 +18,7 @@
18
18
 
19
19
  ### 2. Render the banner
20
20
 
21
- Pair it with [[cookie-consent-banner]] (`CookieConsentBanner.vue`), registered once in the app's default layout — see that component's skill doc for placement and copy slots.
21
+ Pair it with [[cookie-consent-banner]] (`CookieConsentBanner.vue`), registered once in the app's default layout — see that component's skill doc for placement, copy slots and label props.
22
22
 
23
23
  ## Composable reference
24
24
 
@@ -7,10 +7,16 @@ Step-by-step guides for repeatable development tasks in this project.
7
7
  Copy skills into your project with:
8
8
 
9
9
  ```bash
10
- cp -r node_modules/srcdev-nuxt-components/.claude/skills .claude/skills/srcdev-nuxt-components
10
+ rm -rf .claude/skills/srcdev-nuxt-components && mkdir -p .claude/skills/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/skills/. .claude/skills/srcdev-nuxt-components
11
11
  ```
12
12
 
13
- Skills land in `.claude/skills/srcdev-nuxt-components/` — safe to re-run without overwriting your own skills.
13
+ Skills land in `.claude/skills/srcdev-nuxt-components/` — safe to re-run: it replaces only that folder, so skills dropped or renamed upstream are removed and your own skills are never touched.
14
+
15
+ The layer also ships a curated set of Claude Code rules in `.claude/rules/consumer/`. `npm run setup:claude` (see `setup-postinstall.md`) copies skills and rules together; to copy the rules by hand:
16
+
17
+ ```bash
18
+ rm -rf .claude/rules/srcdev-nuxt-components && mkdir -p .claude/rules/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/rules/consumer/. .claude/rules/srcdev-nuxt-components
19
+ ```
14
20
 
15
21
  ## Structure
16
22
 
@@ -97,10 +103,10 @@ Each skill is a single markdown file named `<area>-<task>.md`.
97
103
  ├── expanding-panel.md — ExpandingPanel v-model, forceOpened, contentIsOnTop overlay mode, slots (summary/icon/content), ARIA wiring, CSS token API
98
104
  ├── content-docs.md — ContentDocs docs-page shell: prop-driven docsNav/docsPageNav (not slots), DocsNavItem icons, container-width breakpoint behaviour, shared/per-side CSS token API
99
105
  ├── glass-panel.md — GlassPanel: frosted-glass container, light-only token defaults in-component (light-dark() block removed from _default.css, no built-in dark glass), highlight behind content, section/article labelled via headingId slot prop, CSS token API
100
- ├── pricing-card.md — PricingCard: SaaS-style plan card with highlight, feature list, #cta slot for button customization, CSS token API
106
+ ├── pricing-card.md — PricingCard: plan card with highlight badge, corner ribbon, feature list, #cta slot, heading-tag, line-clamp tokens
101
107
  ├── price-list.md — PriceList: service/menu price columns (dl rows), PriceListData type, headingTag/fromLabel props, CSS token API
102
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
103
- ├── input-copy.md — InputCopy: readonly copy-to-clipboard input, visual feedback, Clipboard API, accessibility, CSS token API
109
+ ├── input-copy.md — InputCopy: readonly copy-to-clipboard input with an inline InputButton, copied state + status announcement, translatable labels/icons, container-query icon-only button, --input-copy-* tokens
104
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
105
111
  ├── grid-stack.md — GridStack: CSS Grid z-axis stacking, slot API, z-order rules, sizing, video+overlay and image+text patterns
106
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
@@ -130,7 +136,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
130
136
  ├── site-header.md — SiteHeader: PageRow + SkipLinks + ResponsiveHeader composition, #branding/#secondaryNavigation slots, dual styleClassPassthrough hooks
131
137
  ├── responsive-header.md — ResponsiveHeader: overflow-collapsing adaptive nav, measurement-pipeline gotchas (unsized icons, vw font-size drift), full CSS token API
132
138
  ├── navigation-items.md — NavigationItems: internal overflow-panel renderer for ResponsiveHeader, complement-visibility logic, not used standalone
133
- ├── cookie-consent-banner.md — CookieConsentBanner: fixed non-modal Accept/Reject banner built on AlertContent, driven by useCookieConsent; title/message/acceptLabel/rejectLabel slots, icon/showIcon/ariaLabel props, position/button tokens (panel via --alert-content-*), teleported so tokens go on html/passthrough
139
+ ├── cookie-consent-banner.md — CookieConsentBanner: fixed non-modal Accept/Reject banner built on AlertContent, driven by useCookieConsent; title/message slots, acceptLabel/rejectLabel/icon/showIcon/ariaLabel props, InputButton (inline) actions, position/button tokens (panel via --alert-content-*), teleported so tokens go on html/passthrough
134
140
  ├── display-banner.md — DisplayBanner: canvas/content stacked overlay banner, conditional slot wrappers, CSS token API
135
141
  ├── deep-expanding-menu.md — DeepExpandingMenu: anchor-positioned popover nav panels, browser support caveat, CSS token API
136
142
  ├── deep-expanding-menu-classic.md — DeepExpandingMenuClassic: <details>-based fallback nav, click-outside close, CSS token API
@@ -44,7 +44,7 @@ to inform the CLAUDE.md and CSS tokens.
44
44
  "dev": "nuxt dev",
45
45
  "generate": "nuxt generate",
46
46
  "preview": "nuxt preview",
47
- "setup:claude": "mkdir -p .claude/skills/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/skills/. .claude/skills/srcdev-nuxt-components",
47
+ "setup:claude": "rm -rf .claude/skills/srcdev-nuxt-components .claude/rules/srcdev-nuxt-components && mkdir -p .claude/skills/srcdev-nuxt-components .claude/rules/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/skills/. .claude/skills/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/rules/consumer/. .claude/rules/srcdev-nuxt-components",
48
48
  "postinstall": "NUXT_STANDALONE=true nuxt prepare && npm run setup:claude",
49
49
  "lint": "eslint .",
50
50
  "lint:fix": "eslint . --fix"
@@ -71,7 +71,9 @@ to inform the CLAUDE.md and CSS tokens.
71
71
 
72
72
  > **`setup:claude` script**: `mkdir -p` is required — the destination folder won't exist on a
73
73
  > fresh clone. The trailing `/.` on the source path copies contents into the destination rather
74
- > than nesting a `skills` subfolder inside it.
74
+ > than nesting a `skills` subfolder inside it. The leading `rm -rf` clears only the two
75
+ > `srcdev-nuxt-components` folders, so skills and rules dropped or renamed upstream don't linger
76
+ > (a stale rule stays loaded); the app's own `.claude/skills/` and `.claude/rules/` are untouched.
75
77
 
76
78
  ### 3. Create `nuxt.config.ts`
77
79
 
@@ -5,7 +5,7 @@
5
5
  Prevent common "forgot to run after install" problems in a consuming app:
6
6
 
7
7
  1. `nuxt prepare` — generates Nuxt type declarations. Skipping it causes TypeScript errors after install or package updates.
8
- 2. `npm run setup:claude` — copies the latest `srcdev-nuxt-components` skills into `.claude/skills/srcdev-nuxt-components/`. Skipping it leaves Claude working from stale skill docs after a package update.
8
+ 2. `npm run setup:claude` — copies the latest `srcdev-nuxt-components` skills into `.claude/skills/srcdev-nuxt-components/` and its consumer rules into `.claude/rules/srcdev-nuxt-components/`. Skipping it leaves Claude working from stale skill docs and rules after a package update.
9
9
 
10
10
  Additionally, `srcdev-nuxt-components` automatically copies VSCode snippet files (`.code-snippets`) to your `.vscode/` folder during its own postinstall, so components' code snippets are available immediately.
11
11
 
@@ -17,7 +17,7 @@ A consumer app's `postinstall` script runs both automatically after every `npm i
17
17
 
18
18
  ```json
19
19
  "scripts": {
20
- "setup:claude": "mkdir -p .claude/skills/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/skills/. .claude/skills/srcdev-nuxt-components",
20
+ "setup:claude": "rm -rf .claude/skills/srcdev-nuxt-components .claude/rules/srcdev-nuxt-components && mkdir -p .claude/skills/srcdev-nuxt-components .claude/rules/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/skills/. .claude/skills/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/rules/consumer/. .claude/rules/srcdev-nuxt-components",
21
21
  "postinstall": "node node_modules/srcdev-nuxt-components/scripts/copy-snippets.mjs && nuxt prepare && npm run setup:claude"
22
22
  }
23
23
  ```
@@ -26,7 +26,7 @@ This postinstall hook does three things in order:
26
26
 
27
27
  1. Copies VSCode snippets from the layer to your `.vscode/` folder
28
28
  2. Runs `nuxt prepare` to generate type declarations
29
- 3. Runs `setup:claude` to copy skills into `.claude/skills/srcdev-nuxt-components/`
29
+ 3. Runs `setup:claude` to copy skills into `.claude/skills/srcdev-nuxt-components/` and rules into `.claude/rules/srcdev-nuxt-components/`
30
30
 
31
31
  ### 2. Check whether your app needs an env flag for nuxt prepare
32
32
 
@@ -49,6 +49,7 @@ From this point on, `npm install` and `npm ci` trigger both steps automatically.
49
49
  ## Notes
50
50
 
51
51
  - **Snippet copy must run in consumer app**: npm doesn't invoke postinstall hooks for dependencies. Each consumer app must explicitly call `scripts/copy-snippets.mjs` in its own postinstall to copy VSCode snippets to `.vscode/`. The layer's postinstall is used when the layer runs standalone, but consumers are responsible for their own `.vscode/` folder.
52
- - Skills land in `.claude/skills/srcdev-nuxt-components/` — safe to re-run without overwriting your own project's skills.
52
+ - Skills land in `.claude/skills/srcdev-nuxt-components/` and rules in `.claude/rules/srcdev-nuxt-components/` — safe to re-run. The script deletes and recreates only those two folders, so anything dropped or renamed upstream is removed (a stale rule would otherwise stay loaded forever); your own project's skills and rules are never touched.
53
+ - Only the layer's `.claude/rules/consumer/` set ships. The rest of its rules govern the library's own source (naming, ledger, demo pages) and would be wrong in an app.
53
54
  - `postinstall` also fires on `npm ci`, so CI environments get the skills and snippets too if they have a `.claude/` directory in the project.
54
55
  - If you do not want `postinstall` running in CI, guard it: `"postinstall": "[ \"$CI\" = \"true\" ] || (node node_modules/srcdev-nuxt-components/scripts/copy-snippets.mjs && nuxt prepare && npm run setup:claude)"`.
@@ -287,6 +287,37 @@ prop and try to pass it. Make it unmistakable:
287
287
 
288
288
  `PriceList.stories.ts` and `GoogleReviews.stories.ts` (line-clamp controls) follow this.
289
289
 
290
+ ## Docs page
291
+
292
+ `.storybook/preview.ts` sets `tags: ["autodocs"]` globally (2026-10-14), so every story file gets a
293
+ generated Docs page and the meta doesn't need the tag. Before that, only two files opted in, and
294
+ the earlier nightly `@nuxtjs/storybook` didn't render Docs pages at all, so a lot of existing
295
+ `docs.description` text had never been visible. Check the page whenever you add or change a story
296
+ (compliance checklist item 6c):
297
+
298
+ - `parameters.docs.description.component` in the meta: what the component is for and its main
299
+ props, slots and events. `docs.description.story` on a story: what that story shows.
300
+ - The Docs page renders every story inline in one document. Overlays (`position: fixed`), dialogs,
301
+ toasts, fullscreen canvases and scroll-driven effects break or overlap there. Render them in
302
+ their own iframe instead:
303
+
304
+ ```ts
305
+ parameters: {
306
+ docs: { story: { inline: false, height: "480px" } },
307
+ },
308
+ ```
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">`.
318
+ - Opt a file out with `tags: ["!autodocs"]` only when it isn't a component reference (tooling, or
319
+ a file with its own attached `.mdx` page, like `ColourRamps.stories.ts` with `SetupGuide.mdx`).
320
+
290
321
  ## Link to the stories of components that use this one
291
322
 
292
323
  When the component you're working on is a building block of other components (it's rendered
@@ -362,6 +393,19 @@ story: "See the [CookieConsentBanner](?path=/story/molecules-cookieconsentbanner
362
393
  controls that make sense (hide irrelevant ones with `Story.argTypes = { x: { table: { disable: true } } }`),
363
394
  or, for a fixed showcase (comparisons, all-themes grids, composition examples), add
364
395
  `controls: { disable: true }` to that story's `parameters`.
396
+ - **Sidebar order** comes from the `storySort` function in `.storybook/preview.ts`: top-level
397
+ groups in tier order, everything below them alphabetical, entries within one component in file
398
+ order with Docs first. Storybook doesn't run that function from the compiled file. It extracts
399
+ the function's source and `eval`s it as plain JavaScript, so any TypeScript inside it
400
+ (annotations, `as` casts) or a reference to something outside its body fails the whole preview
401
+ build with `SyntaxError: Unexpected token ':'` (found 2026-10-15). It stays annotation-free
402
+ under a `// @ts-expect-error`. Check an edit without starting Storybook:
403
+
404
+ ```bash
405
+ node --input-type=module -e 'import { getStorySortParameter } from "storybook/internal/csf-tools"; import fs from "node:fs"; console.log(typeof getStorySortParameter(fs.readFileSync(".storybook/preview.ts", "utf8")));'
406
+ ```
407
+
408
+ `function` means it parses; a syntax error is the same one Storybook would hit.
365
409
 
366
410
  ## Scroll/animation-driven effects need surrounding chrome, not just the bare component
367
411
 
@@ -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**.
@@ -42,6 +42,52 @@ overrides (2026-08-25):
42
42
  Tier 1 is the one deliberate exception. Everything else gets a real component-prefixed name with
43
43
  an inline fallback, even if that fallback happens to be a Tier 1 or Tier 2 token underneath.
44
44
 
45
+ ## Public vs private tokens
46
+
47
+ Rule: `.claude/rules/css-public-private-tokens.md`.
48
+
49
+ The public/private decision is about **consumer relevance, not reuse
50
+ count**. A value a consumer could plausibly want to customise — colours, spacing, sizes, radii,
51
+ durations, and any per-state variant of these (a hover outline width, a focus border colour) — is
52
+ a **public token**, consumed directly at its point of use: `border-radius: var(--component-name-border-radius, 0.5rem)`.
53
+ This holds even when it's only used once — single-use is not a reason to demote a genuinely
54
+ consumer-facing value to private. Per-state variants get their own public token that falls back
55
+ to the resting one, rather than being derived privately: `--component-name-outline-width-hover`
56
+ falling back to `--component-name-outline-width`, not a private var with no override hook.
57
+
58
+ A value with **no plausible consumer relevance** — genuine internal implementation detail, e.g.
59
+ something composed from a `v-bind()` JS value, or an intermediate that exists purely to avoid
60
+ repeating a computation — is a **private token**, always prefixed `--_x`. Prefix it even when
61
+ it's used only once: the underscore is a grep-able, at-a-glance "this is not public API" signal,
62
+ and that signal shouldn't depend on how many places the value happens to appear.
63
+
64
+ ```css
65
+ /* ✅ Single-use, consumer-facing — still a public token, consumed directly */
66
+ .component-name {
67
+ border-radius: var(--component-name-border-radius, 0.5rem);
68
+ transition: all var(--component-name-transition-duration, 300ms) ease;
69
+ }
70
+
71
+ /* ✅ Resting/hover pair — both public, hover falls back to resting; --_ only wraps the
72
+ genuinely-shared intermediate so the fallback chain isn't repeated */
73
+ .component-name {
74
+ --_border-colour: var(--component-name-border-colour, transparent);
75
+ border-color: var(--_border-colour);
76
+
77
+ &:hover {
78
+ border-color: var(--component-name-border-colour-hover, var(--_border-colour));
79
+ }
80
+ }
81
+
82
+ /* ✅ Private — internal only, no consumer would ever want to override this directly */
83
+ .component-name {
84
+ --_computed-offset: calc(v-bind(dynamicWidth) * 0.5px);
85
+ }
86
+ ```
87
+
88
+ CLAUDE.md pitfall #20 is the concrete case (`ServicesCard`'s private layer wrapped public tokens with
89
+ no composition or state-swap behind them: pure pass-through duplication, not genuine privacy).
90
+
45
91
  ## Naming convention
46
92
 
47
93
  `--{component-kebab-name}-{variant-if-any}-{property}[-state]`
@@ -138,7 +184,7 @@ raw value) chains through it:
138
184
 
139
185
  ```css
140
186
  /* InputCopy's copy button defaults to InputButton's primary look */
141
- --_button-bg: var(--input-copy-button-bg, var(--input-button-primary-surface, var(--theme-surface)));
187
+ background-color: var(--input-copy-button-bg, var(--input-button-primary-surface, var(--theme-surface)));
142
188
  ```
143
189
 
144
190
  Three tiers deep here: instance/global override → "look like the primary button" → the theme
@@ -47,7 +47,7 @@ affects buttons, inputs, prompts, and toasts simultaneously:
47
47
  | `--theme-border-focus` | `--colour-theme-4` / `3` | Focused border |
48
48
  | `--theme-ring` | `--colour-theme-1` / `9` | Focus ring (outline) |
49
49
  | `--theme-on-surface` | `--colour-theme-0` | Text/icon on filled/inverted surface (fixed light value in both modes — pair this with any dark-in-light-mode surface, not `--theme-text`) |
50
- | `--theme-text` | `--colour-theme-9` / `2` | Text on page, outline element text (assumes a *light* surface — don't pair with `--theme-surface-hover`/`-inverted`, see pitfall #13 in `Claude.md`) |
50
+ | `--theme-text` | `--colour-theme-9` / `2` | Text on page, outline element text (assumes a *light* surface — don't pair with `--theme-surface-hover`/`-inverted`, see pitfall #13 in `CLAUDE.md`) |
51
51
  | `--theme-text-inverted` | `--colour-theme-0` / `7` | Text on `--theme-surface-inverted` (fixed to a light value in light mode as of 2026-08-03 — was `--colour-theme-9`, identical to the surface it sits on) |
52
52
 
53
53
  Input-specific tokens:
@@ -10,11 +10,13 @@
10
10
  "description": "CookieConsentBanner with translated copy and accessible name",
11
11
  "scope": "vue,html",
12
12
  "body": [
13
- "<CookieConsentBanner :aria-label=\"${1:t('global.cookieConsent.ariaLabel')}\">",
14
- " <template #title>{{ ${2:t('global.cookieConsent.title')} }}</template>",
15
- " <template #message>{{ ${3:t('global.cookieConsent.message')} }}</template>",
16
- " <template #acceptLabel>{{ ${4:t('global.cookieConsent.accept')} }}</template>",
17
- " <template #rejectLabel>{{ ${5:t('global.cookieConsent.reject')} }}</template>",
13
+ "<CookieConsentBanner",
14
+ " :aria-label=\"${1:t('global.cookieConsent.ariaLabel')}\"",
15
+ " :accept-label=\"${2:t('global.cookieConsent.accept')}\"",
16
+ " :reject-label=\"${3:t('global.cookieConsent.reject')}\"",
17
+ ">",
18
+ " <template #title>{{ ${4:t('global.cookieConsent.title')} }}</template>",
19
+ " <template #message>{{ ${5:t('global.cookieConsent.message')} }}</template>",
18
20
  "</CookieConsentBanner>"
19
21
  ]
20
22
  },
@@ -1,28 +1,27 @@
1
1
  {
2
2
  "SRCDEV InputCopy Basic": {
3
- "description": "Basic InputCopy component with value and label",
3
+ "description": "InputCopy: readonly input with a copy-to-clipboard button",
4
4
  "scope": "vue,html",
5
5
  "body": [
6
6
  "<InputCopy",
7
- " value=\"$1sk_live_abc123xyz\"",
8
- " label=\"$2API key\"",
9
- " description=\"$3Copy this key to authenticate with our API.\"",
10
- " @copy=\"$4handleCopy\"",
7
+ " :value=\"${1:licenseKey}\"",
8
+ " label=\"${2:License key}\"",
9
+ " description=\"${3:Copy this key to activate the component in your application.}\"",
10
+ " @copied=\"${4:handleCopied}\"",
11
11
  "/>"
12
12
  ]
13
13
  },
14
- "SRCDEV InputCopy License Key": {
15
- "description": "InputCopy for displaying a license key on success page",
14
+ "SRCDEV InputCopy Translated": {
15
+ "description": "InputCopy with translated copy and accessible names",
16
16
  "scope": "vue,html",
17
17
  "body": [
18
18
  "<InputCopy",
19
- " :value=\"licenseKey\"",
20
- " label=\"License key\"",
21
- " description=\"Your license key is ready to use. Copy it to embed in your application.\"",
22
- " buttonText=\"Copy\"",
23
- " copiedText=\"Copied to clipboard!\"",
24
- " @copy=\"$1handleCopy\"",
25
- " @copied=\"$2showCopiedFeedback\"",
19
+ " :value=\"${1:value}\"",
20
+ " :label=\"${2:t('account.key.label')}\"",
21
+ " :description=\"${3:t('account.key.description')}\"",
22
+ " :button-text=\"${4:t('common.copy')}\"",
23
+ " :copied-text=\"${5:t('common.copied')}\"",
24
+ " :button-aria-label=\"${6:t('account.key.copyAriaLabel')}\"",
26
25
  "/>"
27
26
  ]
28
27
  },
@@ -31,25 +30,10 @@
31
30
  "scope": "vue,html",
32
31
  "body": [
33
32
  "<InputCopy",
34
- " :value=\"$1value\"",
35
- " label=\"$2Label\"",
36
- " :is-disabled=\"true\"",
37
- " :button-text=\"$3Unavailable\"",
38
- "/>"
39
- ]
40
- },
41
- "SRCDEV InputCopy in Page": {
42
- "description": "InputCopy with custom aria labels",
43
- "scope": "vue,html",
44
- "body": [
45
- "<InputCopy",
46
- " :value=\"$1value\"",
47
- " label=\"$2License key\"",
48
- " aria-label=\"$3Your unique GuideMyHair license identifier\"",
49
- " description=\"$4Keep this key safe. You'll need it to embed the widget.\"",
50
- " button-text=\"$5Copy key\"",
51
- " copied-text=\"$6Key copied!\"",
52
- " @copy=\"$7logCopy\"",
33
+ " :value=\"${1:value}\"",
34
+ " label=\"${2:Label}\"",
35
+ " :is-disabled=\"${3:true}\"",
36
+ " button-text=\"${4:Unavailable}\"",
53
37
  "/>"
54
38
  ]
55
39
  },
@@ -57,15 +41,13 @@
57
41
  "description": "CSS override scaffold for InputCopy tokens",
58
42
  "scope": "css",
59
43
  "body": [
60
- ".$1success-page {",
61
- " .input-copy {",
62
- " --input-copy-input-bg: $2;",
63
- " --input-copy-input-border: $3;",
64
- " --input-copy-input-border-radius: $4;",
65
- " --input-copy-button-bg: $5;",
66
- " --input-copy-button-bg-hover: $6;",
67
- " --input-copy-button-text-color: $7;",
68
- " }",
44
+ ".${1:account-page} .input-copy {",
45
+ " --input-copy-input-bg: ${2:var(--theme-input-surface)};",
46
+ " --input-copy-input-border: ${3:0.1rem solid var(--theme-border)};",
47
+ " --input-copy-button-bg: ${4:var(--theme-surface)};",
48
+ " --input-copy-button-bg-hover: ${5:var(--theme-surface-hover)};",
49
+ " --input-copy-button-text-color: ${6:var(--theme-on-surface)};",
50
+ " --input-copy-description-line-clamp: ${7:none};",
69
51
  "}"
70
52
  ]
71
53
  }