srcdev-nuxt-components 9.6.2 → 9.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/consumer/consumer-overrides.md +17 -0
- package/.claude/rules/consumer/css-comment-slash.md +13 -0
- package/.claude/rules/consumer/css-native-nesting.md +13 -0
- package/.claude/rules/consumer/css-no-deep.md +13 -0
- package/.claude/rules/consumer/css-rem-base.md +11 -0
- package/.claude/rules/consumer/css-surface-text-pairing.md +13 -0
- package/.claude/rules/consumer/css-webkit-support.md +15 -0
- package/.claude/rules/consumer/explicit-vue-imports.md +11 -0
- package/.claude/rules/consumer/nuxt-img-dimensions.md +12 -0
- package/.claude/rules/consumer/srcdev-skills-location.md +9 -0
- package/.claude/rules/consumer/story-args.md +12 -0
- package/.claude/rules/consumer/story-register-components.md +12 -0
- package/.claude/rules/consumer/vue-auto-imports.md +14 -0
- package/.claude/rules/consumer/vue-no-self-closing.md +12 -0
- package/.claude/rules/consumer/vue-prop-hyphenation.md +11 -0
- package/.claude/rules/consumer/vue-props-pattern.md +13 -0
- package/.claude/rules/consumer/vue-slots-no-computed.md +14 -0
- package/.claude/rules/consumer/vue-video-source.md +12 -0
- package/.claude/skills/component-compliance-checklist.md +31 -3
- package/.claude/skills/component-inline-action-button.md +33 -2
- package/.claude/skills/component-naming.md +1 -1
- package/.claude/skills/components/cookie-consent-banner.md +9 -6
- package/.claude/skills/components/input-copy.md +47 -100
- package/.claude/skills/components/pricing-card.md +68 -84
- package/.claude/skills/composable-cookie-consent.md +1 -1
- package/.claude/skills/index.md +11 -5
- package/.claude/skills/new-app-scaffold.md +4 -2
- package/.claude/skills/setup-postinstall.md +5 -4
- package/.claude/skills/storybook-add-story.md +44 -0
- package/.claude/skills/testing-add-unit-test.md +19 -0
- package/.claude/skills/theming-component-token-pattern.md +47 -1
- package/.claude/skills/theming-partial-override.md +1 -1
- package/.vscode/srcdev-component-cookie-consent-banner.code-snippets +7 -5
- package/.vscode/srcdev-component-input-copy.code-snippets +24 -42
- package/.vscode/srcdev-component-pricing-card.code-snippets +46 -84
- package/README.md +5 -5
- package/app/components/01.atoms/animations/scroll-reveal-frame/stories/ScrollRevealFrame.stories.ts +2 -0
- package/app/components/01.atoms/animations/scroll-reveal-image/stories/ScrollRevealImage.stories.ts +2 -0
- package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +4 -0
- package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +2 -0
- package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +13 -4
- package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +23 -14
- package/app/components/02.molecules/cookie-consent-banner/stories/CookieConsentBanner.stories.ts +108 -5
- package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +33 -11
- package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +82 -124
- package/app/components/02.molecules/input-copy/InputCopy.vue +110 -85
- package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +204 -48
- package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +147 -171
- package/app/components/02.molecules/pricing-card/CONSUMER-STYLING.md +96 -149
- package/app/components/02.molecules/pricing-card/PricingCard.vue +84 -44
- package/app/components/02.molecules/pricing-card/stories/PricingCard.stories.ts +241 -104
- package/app/components/02.molecules/pricing-card/tests/PricingCard.spec.ts +149 -123
- package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +4 -0
- package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +4 -0
- package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabel.vue +1 -1
- package/app/composables/useAriaDescribedById.ts +1 -1
- package/app/types/components/cookie-consent-banner.d.ts +2 -0
- package/package.json +2 -1
|
@@ -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)`.
|
|
@@ -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
|
|
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
|
|
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,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:
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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 `
|
|
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
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
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": "
|
|
3
|
+
"description": "InputCopy: readonly input with a copy-to-clipboard button",
|
|
4
4
|
"scope": "vue,html",
|
|
5
5
|
"body": [
|
|
6
6
|
"<InputCopy",
|
|
7
|
-
" value=\"$
|
|
8
|
-
" label=\"$
|
|
9
|
-
" description=\"$
|
|
10
|
-
" @
|
|
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
|
|
15
|
-
"description": "InputCopy
|
|
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=\"
|
|
20
|
-
" label=\"
|
|
21
|
-
" description=\"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
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=\"$
|
|
35
|
-
" label=\"$
|
|
36
|
-
" :is-disabled=\"true\"",
|
|
37
|
-
"
|
|
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
|
-
".$
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
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
|
}
|