srcdev-nuxt-components 9.6.2 → 9.6.3

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 (44) 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 +11 -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 +23 -1
  20. package/.claude/skills/component-naming.md +1 -1
  21. package/.claude/skills/components/pricing-card.md +68 -84
  22. package/.claude/skills/index.md +9 -3
  23. package/.claude/skills/new-app-scaffold.md +4 -2
  24. package/.claude/skills/setup-postinstall.md +5 -4
  25. package/.claude/skills/storybook-add-story.md +36 -0
  26. package/.claude/skills/theming-component-token-pattern.md +46 -0
  27. package/.claude/skills/theming-partial-override.md +1 -1
  28. package/.vscode/srcdev-component-pricing-card.code-snippets +46 -84
  29. package/README.md +5 -5
  30. package/app/components/01.atoms/animations/scroll-reveal-frame/stories/ScrollRevealFrame.stories.ts +2 -0
  31. package/app/components/01.atoms/animations/scroll-reveal-image/stories/ScrollRevealImage.stories.ts +2 -0
  32. package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +4 -0
  33. package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +2 -0
  34. package/app/components/02.molecules/cookie-consent-banner/stories/CookieConsentBanner.stories.ts +2 -0
  35. package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +0 -1
  36. package/app/components/02.molecules/pricing-card/CONSUMER-STYLING.md +96 -149
  37. package/app/components/02.molecules/pricing-card/PricingCard.vue +84 -44
  38. package/app/components/02.molecules/pricing-card/stories/PricingCard.stories.ts +241 -104
  39. package/app/components/02.molecules/pricing-card/tests/PricingCard.spec.ts +149 -123
  40. package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +4 -0
  41. package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +4 -0
  42. package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabel.vue +1 -1
  43. package/app/composables/useAriaDescribedById.ts +1 -1
  44. 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,11 @@
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" />`.
10
+
11
+ Why: ESLint `vue/html-self-closing` rejects it.
@@ -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
 
@@ -149,6 +149,21 @@ The ledger can't see items 9, 10, 11 and 14, so check those by hand.
149
149
  with nothing to stress (animation wrappers, decorative containers that only take a slot and
150
150
  tag/variant props, Storybook tooling) are exempt: they are listed in `stressStoryExempt` in
151
151
  `.claude/component-ledger/build.mjs`; add a new one there rather than writing a padding story.
152
+ 6c. **Storybook docs page** — every story file gets a generated Docs page: `tags: ["autodocs"]` is
153
+ set globally in `.storybook/preview.ts` (since 2026-10-14), so don't repeat it in the meta.
154
+ Open the component's Docs page and check that it reads well:
155
+ - A `parameters.docs.description.component` in the meta says what the component is for and
156
+ names its main props/slots/events. Per-story `docs.description.story` text explains what
157
+ that story shows (the Docs page is the only place either is visible).
158
+ - The props table is accurate. Every story-only arg has a `description` and a category, and
159
+ CSS-token args are labelled story-only (6b).
160
+ - Every story renders inline. A story that misbehaves on the shared page (fixed/sticky
161
+ overlays, `layout: "fullscreen"` canvases, dialogs, toasts, scroll effects, heavy stress
162
+ data) gets `parameters.docs.story: { inline: false, height: "<px>" }` so it renders in its
163
+ own iframe. Don't drop it from the page.
164
+ - A story file that isn't a component reference (Storybook tooling, a file that already has an
165
+ attached `.mdx` page) opts out with `tags: ["!autodocs"]`. `ColourRamps.stories.ts` does this.
166
+ See `storybook-add-story.md`, "Docs page".
152
167
  7. **Skill doc** — create or update `.claude/skills/components/<component-name>.md`, following the
153
168
  pattern of an existing one. A labelled `<Name>Field` wrapper (or any other wrapper) is documented
154
169
  in its control's doc under a "Variants" section, not in a doc of its own
@@ -235,3 +250,10 @@ The ledger can't see items 9, 10, 11 and 14, so check those by hand.
235
250
  the edits in private memory. Note that an inline-size container can't size itself from its
236
251
  content: say so in `CONSUMER-STYLING.md` (`ProfileSection`).
237
252
  - Grid tracks that hold text are `minmax(0, …)` so an unbroken string can't widen the grid.
253
+ - No `:deep()` in the component's `<style>`: these blocks are never scoped, so Vue leaves it
254
+ as-is and the browser drops the whole rule (pitfall #27). Use a descendant selector, or set
255
+ the child component's public tokens on an ancestor.
256
+ - No `font-size` (or font-size token default) under `1rem`. `html` is `62.5%`, so `1rem` is
257
+ 10px, and values like `0.75rem`/`0.9rem` are almost always copied from a 16px-base design
258
+ and render at 7.5-9px. `PricingCard` had both (fixed 2026-10-15); grep
259
+ `font-size:\s*(var\([^,]+,\s*)?0?\.[0-9]+rem` to find them.
@@ -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.
@@ -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)`.
@@ -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,7 +103,7 @@ 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
109
  ├── input-copy.md — InputCopy: readonly copy-to-clipboard input, visual feedback, Clipboard API, accessibility, 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)"`.