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.
- 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 +11 -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 +23 -1
- package/.claude/skills/component-naming.md +1 -1
- package/.claude/skills/components/pricing-card.md +68 -84
- package/.claude/skills/index.md +9 -3
- package/.claude/skills/new-app-scaffold.md +4 -2
- package/.claude/skills/setup-postinstall.md +5 -4
- package/.claude/skills/storybook-add-story.md +36 -0
- package/.claude/skills/theming-component-token-pattern.md +46 -0
- package/.claude/skills/theming-partial-override.md +1 -1
- 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/stories/CookieConsentBanner.stories.ts +2 -0
- package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +0 -1
- 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/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,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,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 `
|
|
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
|
|
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
|
|
14
|
-
| -------------------------- |
|
|
15
|
-
| `tag` | `"div" \| "section" \| "article"`
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
| `:
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
| `:
|
|
23
|
-
| `:
|
|
24
|
-
|
|
|
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
|
|
31
|
-
|
|
|
32
|
-
| `select` | `planName: string` | Emitted when the CTA
|
|
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
|
-
###
|
|
49
|
+
### `#features`
|
|
39
50
|
|
|
40
|
-
|
|
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
|
-
###
|
|
62
|
+
### `#cta`
|
|
51
63
|
|
|
52
|
-
|
|
64
|
+
Replaces the default `InputButton`. Slot scope:
|
|
53
65
|
|
|
54
|
-
|
|
55
|
-
- `
|
|
56
|
-
- `
|
|
57
|
-
- `
|
|
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
|
-
##
|
|
81
|
+
## Styling
|
|
77
82
|
|
|
78
|
-
All `--pricing-card-*` tokens
|
|
83
|
+
All `--pricing-card-*` tokens are listed in `CONSUMER-STYLING.md`. The common ones:
|
|
79
84
|
|
|
80
|
-
|
|
81
|
-
- `--pricing-card-
|
|
82
|
-
- `--pricing-card-
|
|
83
|
-
- `--pricing-card-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
96
|
+
## Usage examples
|
|
90
97
|
|
|
91
|
-
### Single card
|
|
98
|
+
### Single card
|
|
92
99
|
|
|
93
100
|
```vue
|
|
94
101
|
<PricingCard
|
|
95
102
|
plan-name="Basic"
|
|
96
103
|
:price="29.99"
|
|
97
|
-
|
|
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
|
-
###
|
|
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
|
-
-
|
|
162
|
-
|
|
163
|
-
-
|
|
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)`.
|
package/.claude/skills/index.md
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
|
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)"`.
|