srcdev-nuxt-components 9.5.2 → 9.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.claude/skills/colour-scheme-disable.md +1 -1
  2. package/.claude/skills/component-compliance-checklist.md +214 -0
  3. package/.claude/skills/component-inline-action-button.md +2 -2
  4. package/.claude/skills/components/canvas-switcher.md +5 -1
  5. package/.claude/skills/components/carousel-flip.md +3 -3
  6. package/.claude/skills/components/clipped-panel.md +2 -2
  7. package/.claude/skills/components/dashboard-quad-grid.md +2 -2
  8. package/.claude/skills/components/dashboard-stats-grid.md +1 -1
  9. package/.claude/skills/components/deep-expanding-menu-classic.md +1 -1
  10. package/.claude/skills/components/deep-expanding-menu.md +1 -1
  11. package/.claude/skills/components/eyebrow-text.md +6 -4
  12. package/.claude/skills/components/google-reviews.md +193 -0
  13. package/.claude/skills/components/input-number.md +4 -0
  14. package/.claude/skills/components/input-otp.md +120 -0
  15. package/.claude/skills/components/section-parallax.md +1 -1
  16. package/.claude/skills/components/select-menu.md +9 -5
  17. package/.claude/skills/components/services-card.md +25 -2
  18. package/.claude/skills/composable-colour-scheme.md +2 -0
  19. package/.claude/skills/index.md +5 -1
  20. package/.claude/skills/new-app-scaffold.md +1 -1
  21. package/.claude/skills/storybook-add-story.md +5 -0
  22. package/.claude/skills/theming-colour-ramps.md +6 -33
  23. package/.claude/skills/theming-component-token-pattern.md +30 -0
  24. package/.claude/skills/theming-dark-mode.md +243 -0
  25. package/.claude/skills/theming-override-default.md +6 -7
  26. package/.claude/skills/theming-partial-override.md +11 -11
  27. package/.vscode/srcdev-component-eyebrow-text.code-snippets +4 -0
  28. package/.vscode/srcdev-component-google-reviews.code-snippets +78 -0
  29. package/.vscode/srcdev-component-input-otp.code-snippets +47 -0
  30. package/.vscode/srcdev-component-select-menu.code-snippets +13 -0
  31. package/.vscode/srcdev-component-services-card.code-snippets +38 -7
  32. package/README.md +81 -41
  33. package/app/assets/styles/setup/01.config/_head.css +0 -9
  34. package/app/assets/styles/setup/03.theming/_default.css +29 -29
  35. package/app/assets/styles/setup/03.theming/_error.css +3 -3
  36. package/app/assets/styles/setup/03.theming/_theme-slots.css +13 -16
  37. package/app/assets/styles/setup/a11y/_variables.css +2 -2
  38. package/app/components/01.atoms/page-row/stories/PageRow.stories.ts +47 -13
  39. package/app/components/01.atoms/text-blocks/eyebrow-text/CONSUMER-STYLING.md +5 -1
  40. package/app/components/01.atoms/text-blocks/eyebrow-text/EyebrowText.vue +4 -1
  41. package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +7 -7
  42. package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +10 -0
  43. package/app/components/02.molecules/select-menu/SelectMenu.vue +15 -1
  44. package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +35 -0
  45. package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +9 -0
  46. package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +3 -3
  47. package/app/components/03.organisms/google-reviews/CONSUMER-STYLING.md +110 -0
  48. package/app/components/03.organisms/google-reviews/GoogleReviewCard.vue +161 -0
  49. package/app/components/03.organisms/google-reviews/GoogleReviews.vue +275 -0
  50. package/app/components/03.organisms/google-reviews/stories/GoogleReviews.stories.ts +323 -0
  51. package/app/components/03.organisms/google-reviews/tests/GoogleReviewCard.spec.ts +104 -0
  52. package/app/components/03.organisms/google-reviews/tests/GoogleReviews.spec.ts +232 -0
  53. package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviewCard.spec.ts.snap +9 -0
  54. package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviews.spec.ts.snap +43 -0
  55. package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +28 -3
  56. package/app/components/03.organisms/services/services-card/ServicesCard.vue +111 -18
  57. package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +122 -0
  58. package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +4 -1
  59. package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +100 -19
  60. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +12 -3
  61. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +1 -1
  62. package/app/components/05.forms/input-number/InputNumber.vue +2 -2
  63. package/app/components/05.forms/input-otp/CONSUMER-STYLING.md +59 -0
  64. package/app/components/05.forms/input-otp/InputOtp.vue +254 -0
  65. package/app/components/05.forms/input-otp/InputOtpField.vue +106 -0
  66. package/app/components/05.forms/input-otp/stories/InputOtp.stories.ts +100 -0
  67. package/app/components/05.forms/input-otp/stories/InputOtpField.stories.ts +163 -0
  68. package/app/components/05.forms/input-otp/tests/InputOtp.spec.ts +275 -0
  69. package/app/components/05.forms/input-otp/tests/InputOtpField.spec.ts +76 -0
  70. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +151 -33
  71. package/app/composables/tests/useAriaDescribedById.spec.ts +5 -5
  72. package/app/composables/useGoogleReviews.ts +4 -0
  73. package/app/types/components/google-reviews.d.ts +18 -0
  74. package/app/types/components/index.ts +1 -0
  75. package/nuxt.config.ts +9 -9
  76. package/package.json +9 -8
@@ -120,10 +120,10 @@ Create `app/assets/styles/setup/03.theming/_default.css`:
120
120
  --theme-chroma: var(--palette-gold-chroma);
121
121
 
122
122
  /* Page-level tokens — readable named steps, not raw oklch */
123
- --page-bg: light-dark(var(--slate-00), var(--slate-08));
124
- --colour-text-default: light-dark(var(--slate-09), var(--slate-01));
125
- --colour-text-accent: light-dark(var(--gold-09), var(--gold-04));
126
- --colour-text-eyebrow: light-dark(var(--gold-09), var(--gold-04));
123
+ --page-bg: var(--slate-00);
124
+ --colour-text-default: var(--slate-09);
125
+ --colour-text-accent: var(--gold-09);
126
+ --colour-text-eyebrow: var(--gold-09);
127
127
  }
128
128
  ```
129
129
 
@@ -172,8 +172,8 @@ export default defineNuxtConfig({
172
172
  The layer's `theme-ramp.css` declares the formula on `:where(html, [data-theme], [data-invalid])`.
173
173
  When your override sets `--theme-hue`/`--theme-chroma` on `:where(html)`, every `--colour-theme-*`
174
174
  step recalculates to your palette. Semantic slots (`--theme-surface`, `--theme-text`, etc.) are
175
- declared on that same selector using `light-dark()` — light/dark mode continues to work without
176
- any extra declarations.
175
+ declared on that same selector with light values only. The layer ships no dark scheme; to add
176
+ one, see `theming-dark-mode.md`.
177
177
 
178
178
  The generated `_gold.css` gives you a named alias for each step (`--gold-09` etc.) so your
179
179
  page-level token values stay readable. The values match what the formula would produce for
@@ -183,7 +183,6 @@ that hue/chroma pair.
183
183
 
184
184
  - Define `--colour-theme-0` through `--colour-theme-10` — the formula handles it
185
185
  - Redeclare `--theme-surface`, `--theme-border`, `--theme-ring` etc. — they auto-update
186
- - Write separate light/dark files — use `light-dark()` in a single file
187
186
  - Override any button or input component tokens — they all consume semantic slots
188
187
 
189
188
  ## rem sizing
@@ -88,8 +88,8 @@ Page-level tokens:
88
88
  ```css
89
89
  /* Make primary buttons darker/more saturated (step-7/9 defaults, step-9/7 hover) */
90
90
  :where(html) {
91
- --theme-surface: light-dark(oklch(48% 0.18 85), oklch(32% 0.20 85));
92
- --theme-surface-hover: light-dark(oklch(32% 0.18 85), oklch(48% 0.20 85));
91
+ --theme-surface: oklch(48% 0.18 85);
92
+ --theme-surface-hover: oklch(32% 0.18 85);
93
93
  }
94
94
  ```
95
95
 
@@ -97,9 +97,9 @@ Page-level tokens:
97
97
 
98
98
  ```css
99
99
  :where(html) {
100
- --theme-input-surface: light-dark(oklch(0.99 0 0), oklch(0.12 0.01 255));
101
- --theme-input-text-color-normal: light-dark(oklch(0.20 0.01 255), oklch(0.95 0.01 255));
102
- --theme-input-placeholder: light-dark(oklch(0.60 0.01 255), oklch(0.40 0.01 255));
100
+ --theme-input-surface: oklch(0.99 0 0);
101
+ --theme-input-text-color-normal: oklch(0.20 0.01 255);
102
+ --theme-input-placeholder: oklch(0.60 0.01 255);
103
103
  }
104
104
  ```
105
105
 
@@ -107,9 +107,9 @@ Page-level tokens:
107
107
 
108
108
  ```css
109
109
  :where(html) {
110
- --page-bg: light-dark(oklch(0.98 0 0), oklch(0.10 0 0));
111
- --colour-text-default: light-dark(oklch(0.20 0 0), oklch(0.93 0 0));
112
- --colour-text-accent: light-dark(oklch(0.30 0.16 85), oklch(0.65 0.18 85));
110
+ --page-bg: oklch(0.98 0 0);
111
+ --colour-text-default: oklch(0.20 0 0);
112
+ --colour-text-accent: oklch(0.30 0.16 85);
113
113
  }
114
114
  ```
115
115
 
@@ -140,8 +140,8 @@ To restrict an override to a specific section, scope to a wrapper class instead
140
140
 
141
141
  .hero-cta {
142
142
  /* Just these buttons use a different surface colour */
143
- --theme-surface: light-dark(oklch(48% 0.16 85), oklch(32% 0.18 85));
144
- --theme-surface-hover: light-dark(oklch(32% 0.14 85), oklch(48% 0.16 85));
143
+ --theme-surface: oklch(48% 0.16 85);
144
+ --theme-surface-hover: oklch(32% 0.14 85);
145
145
  }
146
146
  ```
147
147
 
@@ -160,6 +160,6 @@ To change how `data-theme="success"` looks without affecting other themes:
160
160
  ## Notes
161
161
 
162
162
  - Only override the tokens you actually need — everything else falls back to layer defaults
163
- - Use `light-dark()` in a single rule rather than separate `:where(html.light)` / `:where(html.dark)` blocks — the layer's `color-scheme` infrastructure handles mode switching
163
+ - The layer ships light values only (no `light-dark()`, for older iPad Safari). For dark mode, redeclare tokens under `html[data-color-scheme="dark"]` plus a `prefers-color-scheme` block for `auto`, or use `light-dark()` yourself if you accept the browser-support cost. Full setup in `theming-dark-mode.md`.
164
164
  - `--slate-*` neutral tokens come from the layer and do not need to be redefined unless you genuinely want different neutrals
165
165
  - For a full palette replacement (changing the layer default), use `theming-override-default.md` instead
@@ -13,6 +13,10 @@
13
13
  ".$1my-page {",
14
14
  " --eyebrow-text-bg-img: linear-gradient(135deg, $2, $3);",
15
15
  " --eyebrow-text-medium: $4;",
16
+ " --eyebrow-text-font-family: ;",
17
+ " --eyebrow-text-font-style: ;",
18
+ " --eyebrow-text-font-weight: ;",
19
+ " --eyebrow-text-letter-spacing: ;",
16
20
  "}"
17
21
  ]
18
22
  }
@@ -0,0 +1,78 @@
1
+ {
2
+ "SRCDEV GoogleReviews Basic": {
3
+ "description": "GoogleReviews fed by useGoogleReviews() (needs NUXT_GOOGLE_REVIEWS_API_KEY and NUXT_GOOGLE_REVIEWS_PLACE_ID)",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<GoogleReviews :data=\"${1:googleReviews}\" />"
7
+ ]
8
+ },
9
+ "SRCDEV GoogleReviews Script": {
10
+ "description": "Fetch Google reviews from the layer's server route",
11
+ "scope": "typescript,javascript,vue",
12
+ "body": [
13
+ "const { data: ${1:googleReviews} } = await useGoogleReviews();"
14
+ ]
15
+ },
16
+ "SRCDEV GoogleReviews With Heading": {
17
+ "description": "GoogleReviews labelled by its own heading, optionally filtered",
18
+ "scope": "vue,html",
19
+ "body": [
20
+ "<GoogleReviews :data=\"${1:googleReviews}\" :min-rating=\"${2:0}\">",
21
+ " <template #heading=\"{ headingId }\">",
22
+ " <h2 :id=\"headingId\">${3:What clients say}</h2>",
23
+ " </template>",
24
+ "</GoogleReviews>"
25
+ ]
26
+ },
27
+ "SRCDEV GoogleReviews Custom Card": {
28
+ "description": "GoogleReviews with each card rendered through the card slot",
29
+ "scope": "vue,html",
30
+ "body": [
31
+ "<GoogleReviews :data=\"${1:googleReviews}\">",
32
+ " <template #card=\"{ review }\">",
33
+ " <GoogleReviewCard :review=\"review\" :style-class-passthrough=\"['${2:my-review}']\" />",
34
+ " </template>",
35
+ "</GoogleReviews>"
36
+ ]
37
+ },
38
+ "SRCDEV GoogleReviews CSS Override": {
39
+ "description": "CSS override scaffold for GoogleReviews — scope to a page or section class. See CONSUMER-STYLING.md for the full token list.",
40
+ "scope": "css",
41
+ "body": [
42
+ ".$1my-page {",
43
+ " .google-reviews {",
44
+ " --google-reviews-gap: ;",
45
+ " --google-reviews-card-width: ;",
46
+ " --google-reviews-scroll-padding: ;",
47
+ " --google-reviews-scrollbar-width: ;",
48
+ " --google-reviews-scrollbar-thumb-colour: ;",
49
+ " --google-reviews-scrollbar-track-colour: ;",
50
+ " --google-reviews-summary-rating-font-size: ;",
51
+ " --google-reviews-focus-outline-width: ;",
52
+ " --google-reviews-focus-outline-colour: ;",
53
+ "",
54
+ " --google-reviews-card-gap: ;",
55
+ " --google-reviews-card-padding: ;",
56
+ " --google-reviews-card-border-width: ;",
57
+ " --google-reviews-card-border-colour: ;",
58
+ " --google-reviews-card-border-radius: ;",
59
+ " --google-reviews-card-surface: ;",
60
+ " --google-reviews-card-text-colour: ;",
61
+ " --google-reviews-avatar-surface: ;",
62
+ " --google-reviews-avatar-text-colour: ;",
63
+ " --google-reviews-author-font-weight: ;",
64
+ " --google-reviews-author-line-clamp: ;",
65
+ " --google-reviews-text-line-height: ;",
66
+ " --google-reviews-text-line-clamp: ;",
67
+ " --google-reviews-link-colour: ;",
68
+ "",
69
+ " --google-reviews-star-size: ;",
70
+ " --google-reviews-star-colour: ;",
71
+ " --google-reviews-star-empty-colour: ;",
72
+ " --google-reviews-meta-font-size: ;",
73
+ " --google-reviews-meta-text-colour: ;",
74
+ " }",
75
+ "}"
76
+ ]
77
+ }
78
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "SRCDEV InputOtp Basic": {
3
+ "description": "Bare InputOtp one-time-code boxes (no legend/description/error wrapper)",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<InputOtp",
7
+ " id=\"$1otp\"",
8
+ " v-model=\"$2code\"",
9
+ " name=\"$3code\"",
10
+ " :length=\"${4:6}\"",
11
+ " group-label=\"$5Verification code\"",
12
+ "/>"
13
+ ]
14
+ },
15
+ "SRCDEV InputOtpField Basic": {
16
+ "description": "InputOtpField: labelled one-time-code field with description and error support",
17
+ "scope": "vue,html",
18
+ "body": [
19
+ "<InputOtpField",
20
+ " v-model=\"$1code\"",
21
+ " name=\"$2code\"",
22
+ " label=\"$3Verification code\"",
23
+ " :length=\"${4:6}\"",
24
+ " autofocus",
25
+ " :error-message=\"$5errors.code\"",
26
+ " :field-has-error=\"!!$5errors.code\"",
27
+ " @complete=\"$6onComplete\"",
28
+ ">",
29
+ " <template #descriptionText>$7Enter the code we sent to your email.</template>",
30
+ "</InputOtpField>"
31
+ ]
32
+ },
33
+ "SRCDEV InputOtp CSS Override": {
34
+ "description": "CSS override scaffold for InputOtp tokens",
35
+ "scope": "css",
36
+ "body": [
37
+ ".$1my-page {",
38
+ " --input-otp-gap: $2;",
39
+ " --input-otp-box-inline-size: $3;",
40
+ " --input-otp-box-block-size: $4;",
41
+ " --input-otp-font-size: $5;",
42
+ " --input-otp-border: $6;",
43
+ " --input-otp-border-focus: $7;",
44
+ "}"
45
+ ]
46
+ }
47
+ }
@@ -35,6 +35,19 @@
35
35
  "/>"
36
36
  ]
37
37
  },
38
+ "SRCDEV SelectMenu Underlined": {
39
+ "description": "SelectMenu — text + chevron with an underlined trigger, matching InputSelect's underlined variant",
40
+ "scope": "vue,html",
41
+ "body": [
42
+ "<SelectMenu",
43
+ " v-model=\"${1:filterValue}\"",
44
+ " :options=\"${2:filterOptions}\"",
45
+ " label=\"${3:Choose a service}\"",
46
+ " :show-icon=\"false\"",
47
+ " input-variant=\"underlined\"",
48
+ "/>"
49
+ ]
50
+ },
38
51
  "SRCDEV SelectMenu Multi-Select": {
39
52
  "description": "SelectMenu — checkbox-per-option multi-select, v-model is an array",
40
53
  "scope": "vue,html",
@@ -25,6 +25,17 @@
25
25
  "<ServicesCard :service-data=\"$1service\" :href=\"`/services/${$1service.slug}`\" />"
26
26
  ]
27
27
  },
28
+ "SRCDEV ServicesCard Titles Over Image": {
29
+ "description": "ServicesCard with eyebrow and title overlaid on the image, over a scrim",
30
+ "scope": "vue,html",
31
+ "body": [
32
+ "<ServicesCard :service-data=\"$1service\" titles-within-image-wrapper>",
33
+ " <template #actions=\"{ serviceData }\">",
34
+ " <InputButton variant=\"secondary\" button-text=\"$2Enquire\" :href=\"`/services/${serviceData.slug}`\" />",
35
+ " </template>",
36
+ "</ServicesCard>"
37
+ ]
38
+ },
28
39
  "SRCDEV ServicesCard With Config Overrides": {
29
40
  "description": "ServicesCard with eyebrowConfig/heroConfig overrides",
30
41
  "scope": "vue,html",
@@ -76,8 +87,13 @@
76
87
  " --services-card-outline-colour-hover: ;",
77
88
  " --services-card-outline-offset: ;",
78
89
  " --services-card-outline-offset-hover: ;",
90
+ " --services-card-background-color-hover: ;",
91
+ " --services-card-transform-hover: ;",
79
92
  "",
80
93
  " --services-card-gap: ;",
94
+ " --services-card-background-color: ;",
95
+ " --services-card-padding-block: ;",
96
+ " --services-card-padding-inline: ;",
81
97
  " --image-wrapper-aspect-ratio: ;",
82
98
  " --image-wrapper-border-radius: ;",
83
99
  " --image-wrapper-padding-block: ;",
@@ -86,21 +102,36 @@
86
102
  " --details-wrapper-grid-gap: ;",
87
103
  " --details-wrapper-padding-block: ;",
88
104
  " --details-wrapper-padding-inline: ;",
105
+ " --eyebrow-text-padding-block: ;",
106
+ " --hero-text-padding-block: ;",
107
+ " --description-text-colour: ;",
89
108
  " --description-padding-block: ;",
90
109
  " --description-line-height: ;",
110
+ " --description-font-weight: ;",
91
111
  " --description-line-clamp: ;",
92
112
  " --meta-padding-block: ;",
113
+ " --meta-border-colour: ;",
114
+ " --meta-text-colour: ;",
115
+ " --meta-duration-text-colour: ;",
116
+ " --meta-price-text-colour: ;",
117
+ " --meta-duration-font-weight: ;",
118
+ " --meta-price-font-weight: ;",
119
+ " --meta-letter-spacing: ;",
120
+ " --meta-font-size: ;",
93
121
  " --meta-text-transform: ;",
94
122
  " --footer-padding-block: ;",
95
123
  " --footer-wrapper-grid-gap: ;",
96
124
  "",
97
- " /* Local-only tokens — page-scoped override only, no :root fallback */",
98
- " --_eyebrow-text-padding-block: ;",
99
- " --_hero-text-padding-block: ;",
100
- " --_description-text-colour: ;",
101
- " --_meta-border-colour: ;",
102
- " --_meta-text-colour: ;",
103
- " --_meta-font-size: ;",
125
+ " /* Titles over the image (titlesWithinImageWrapper) */",
126
+ " --image-wrapper-details-scrim-colour: ;",
127
+ " --image-wrapper-details-scrim: ;",
128
+ " --image-wrapper-details-text-colour: ;",
129
+ " --image-wrapper-details-eyebrow-text-colour: ;",
130
+ " --image-wrapper-details-gap: ;",
131
+ " --image-wrapper-details-eyebrow-text-padding-block: ;",
132
+ " --image-wrapper-details-eyebrow-text-padding-inline: ;",
133
+ " --image-wrapper-details-hero-text-padding-block: ;",
134
+ " --image-wrapper-details-hero-text-padding-inline: ;",
104
135
  " }",
105
136
  "}"
106
137
  ]
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## NOTE
8
8
 
9
- Althought this repo is public and feel free to do what you wish with it, this has been developed for use with websites we develop.
9
+ Although this repo is public and feel free to do what you wish with it, this has been developed for use with websites we develop.
10
10
 
11
11
  ## Install Nuxt Components layer
12
12
 
@@ -58,7 +58,7 @@ To ensure skills are always up to date, VSCode snippets are installed, and `nuxt
58
58
  2. `nuxt prepare` — generates Nuxt type declarations
59
59
  3. `setup:claude` — copies skills into `.claude/skills/srcdev-nuxt-components/`
60
60
 
61
- > **Note**: The snippet copy must run from your consumer app's postinstall, not the layer's. npm doesn't invoke postinstall hooks for dependencies, so each consumer app is responsible for copying snippets into its own `.vscode/` folder. If your app uses a standalone env flag for `nuxt prepare` (e.g. `NUXT_STANDALONE=true`), add it before `nuxt prepare`. This is project-specific — check your own `nuxt.config.ts` to confirm whether it is needed.
61
+ > **Note**: The snippet copy must run from your consumer app's postinstall, not the layer's. npm doesn't invoke postinstall hooks for dependencies, so each consumer app is responsible for copying snippets into its own `.vscode/` folder. Don't set `SRCDEV_STANDALONE` in a consumer app: that flag is only for running this layer on its own (it enables dev-only modules and the ramp watcher).
62
62
 
63
63
  ---
64
64
 
@@ -77,6 +77,31 @@ via `npm run setup:claude`.
77
77
 
78
78
  ---
79
79
 
80
+ ## Components
81
+
82
+ Components live in `app/components/`, grouped into tiers:
83
+
84
+ | Folder | Storybook title prefix |
85
+ | -------------- | ---------------------- |
86
+ | `01.atoms` | `Atoms/` |
87
+ | `02.molecules` | `Molecules/` |
88
+ | `03.organisms` | `Organisms/` |
89
+ | `04.templates` | `Templates/` |
90
+ | `05.forms` | `Forms/` |
91
+
92
+ Components are auto-imported with `pathPrefix: false`, so the tier folder never appears in the tag name.
93
+
94
+ **Naming**: a bare form control is `<Name>` (e.g. `InputNumber`) and its labelled wrapper is `<Name>Field` (e.g. `InputNumberField`). Older `Core` / `Default` / `WithLabel` suffixes are being renamed to this convention; see `.claude/skills/component-naming.md` for the backlog.
95
+
96
+ Each component ships with:
97
+
98
+ - **`CONSUMER-STYLING.md`** next to the `.vue` file, listing its public CSS custom properties and how to override them
99
+ - **a Storybook story**, the only place components are demonstrated (this repo has no demo pages)
100
+ - **a skill doc** in `.claude/skills/components/`, copied into your app by `npm run setup:claude`
101
+ - **a VS Code snippet** in `.vscode/srcdev-component-{name}.code-snippets`, copied into your app by `postinstall`
102
+
103
+ ---
104
+
80
105
  ## Consumer App Configuration
81
106
 
82
107
  Configuration options for apps extending this layer. All options go in the consumer's `nuxt.config.ts` under `runtimeConfig.public` and can also be set via environment variable.
@@ -108,6 +133,40 @@ When disabled, no `data-color-scheme` attribute is set on `<html>` and `useColou
108
133
 
109
134
  > See [.claude/skills/colour-scheme-disable.md](.claude/skills/colour-scheme-disable.md) for the full guide.
110
135
 
136
+ ### Analytics
137
+
138
+ Call `useAnalytics()` from a layout, page or component and use `trackEvent(name, params)` to fire events. Only Google Analytics (GA4) is implemented today; call sites never name the provider, so adding another later won't change them.
139
+
140
+ ```ts
141
+ // nuxt.config.ts
142
+ runtimeConfig: {
143
+ public: {
144
+ analytics: {
145
+ provider: "google-analytics",
146
+ googleAnalytics: { id: "G-XXXXXXXXXX" },
147
+ },
148
+ },
149
+ },
150
+ ```
151
+
152
+ Or via environment variable:
153
+
154
+ ```bash
155
+ NUXT_PUBLIC_ANALYTICS_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
156
+ ```
157
+
158
+ With no ID set, `trackEvent` does nothing.
159
+
160
+ ### WhatsApp
161
+
162
+ `useWhatsApp().openWhatsApp(fields)` opens a WhatsApp chat pre-filled with the given label/value pairs. Set the number in international format, without `+` or spaces:
163
+
164
+ ```bash
165
+ NUXT_PUBLIC_WHATSAPP_NUMBER=447700900000
166
+ ```
167
+
168
+ Without a number it logs a warning and does nothing.
169
+
111
170
  ---
112
171
 
113
172
  ## Colour System
@@ -131,7 +190,8 @@ computes the scale from the two params:
131
190
  Scale direction: **00 = lightest, 10 = darkest**. Chroma tapers at both ends and peaks at step 06.
132
191
 
133
192
  Nine semantic slots (`--theme-surface`, `--theme-accent`, `--theme-text`, `--theme-ring`, etc.) are derived from the
134
- scale using `light-dark()` and are shared by all components. Theme variants (`data-theme="success"`,
193
+ scale and are shared by all components. They ship light values only (no `light-dark()`, which older iPad
194
+ Safari lacks); dark mode is left to the consuming app, see `.claude/skills/theming-dark-mode.md`. Theme variants (`data-theme="success"`,
135
195
  `"warning"`, `"error"`) swap `--theme-hue` and `--theme-chroma` on their element; the formula
136
196
  re-evaluates locally so each themed element gets its own full palette without affecting the page.
137
197
 
@@ -222,8 +282,8 @@ In your `app/assets/styles/setup/02.colours/index.css` (create if it doesn't exi
222
282
  --theme-chroma: var(--palette-gold-chroma);
223
283
 
224
284
  /* Page-level tokens — readable named steps, not raw oklch */
225
- --colour-text-accent: light-dark(var(--gold-09), var(--gold-04));
226
- --colour-text-eyebrow: light-dark(var(--gold-09), var(--gold-04));
285
+ --colour-text-accent: var(--gold-09);
286
+ --colour-text-eyebrow: var(--gold-09);
227
287
  }
228
288
  ```
229
289
 
@@ -270,45 +330,13 @@ npm run check:ramps # CI: fail if generated CSS is out of date
270
330
  | Full ramp architecture, formula details, hue drift | `.claude/skills/theming-colour-ramps.md` |
271
331
  | Full palette swap for a consumer app | `.claude/skills/theming-override-default.md` |
272
332
  | Partial token override (palette shift, buttons, inputs) | `.claude/skills/theming-partial-override.md` |
333
+ | Add a dark scheme (layer ships light values only) | `.claude/skills/theming-dark-mode.md` |
273
334
  | Disable light/dark mode support | `.claude/skills/colour-scheme-disable.md` |
274
335
 
275
336
  Skills are available in your project after running `npm run setup:claude`.
276
337
 
277
338
  ---
278
339
 
279
- ## Known Build / Production Issues
280
-
281
- ### `ERR_MODULE_NOT_FOUND: vue/index.mjs` — 500 on every request (Node 22)
282
-
283
- **Symptom**: After `npm run build`, running `node .output/server/index.mjs` returns HTTP 500 on every page. The terminal shows:
284
-
285
- ```text
286
- Error [ERR_MODULE_NOT_FOUND]: Cannot find module
287
- '.output/server/node_modules/vue/index.mjs'
288
- Did you mean to import
289
- '.output/server/node_modules/.nitro/vue@3.5.34/dist/vue.cjs.prod.js'?
290
- ```
291
-
292
- **Root cause**: Nitro's dependency tracer externalises `vue` and copies it to `.output/server/node_modules/vue/`, but only traces the CJS build (`dist/vue.cjs.prod.js`). Vue's `package.json` exports map resolves the `"import"` + `"node"` condition (used by Node 22 when importing from an ES module) to `./index.mjs` — a file that was never copied. This is a Nitro tracing bug exposed by Node 22's stricter ESM condition matching, made more likely by `vue.runtimeCompiler: true` (which changes how Nuxt aliases Vue at build time, causing Nitro to fall back to externalising it as a node_modules package).
293
-
294
- **Fix** (already applied in this repo's `nuxt.config.ts`):
295
-
296
- ```ts
297
- nitro: {
298
- externals: {
299
- inline: ["vue", "@vue/runtime-core", "@vue/runtime-dom", "@vue/reactivity", "@vue/shared", "@vue/server-renderer"],
300
- },
301
- },
302
- ```
303
-
304
- This tells Nitro to bundle these packages inline rather than externalise them, which sidesteps the runtime resolution entirely.
305
-
306
- **Consumer apps**: This config is not gated behind `isStandalone`, so it is inherited automatically by any app that extends this layer. No consumer-side action is required.
307
-
308
- **When it's safe to remove**: If a future Nitro release fixes the dependency tracer so that all export-condition files are copied correctly, the `inline` list can be removed. Verify by checking that `.output/server/node_modules/vue/index.mjs` exists after a clean build without the config.
309
-
310
- ---
311
-
312
340
  ## Development Environment (`.vscode`)
313
341
 
314
342
  The `.vscode` directory contains Visual Studio Code configuration files to ensure a consistent development experience across the project:
@@ -428,7 +456,7 @@ npm run test:update
428
456
 
429
457
  ### Visual Regression Tests (Playwright)
430
458
 
431
- Runs pixel-level screenshot comparisons against Storybook. Requires Storybook to be running first.
459
+ Runs pixel-level screenshot comparisons against Storybook. Requires Storybook to be running first. Coverage is currently limited to a few components (`InputButton`, `HeroText`, `EyebrowText`, `ContentDocs`); specs live in each component's `playwright/` folder as `*.playwright.ts`.
432
460
 
433
461
  ```bash
434
462
  # 1. Start Storybook
@@ -462,12 +490,13 @@ npx playwright show-report
462
490
 
463
491
  ## Storybook
464
492
 
465
- Storybook is used for isolated component development and as the target for visual regression tests. It runs as a separate Vite-based dev server.
493
+ Storybook (v10, via `@nuxtjs/storybook`) is the only demo surface for this library, and the target for visual regression tests. The deployed build is at [storybook.srcdev.co.uk](https://storybook.srcdev.co.uk).
466
494
 
467
495
  ### Scripts
468
496
 
469
497
  ```bash
470
- # Start Storybook dev server (http://localhost:6006)
498
+ # Start Storybook dev server (http://localhost:6006). `npm run dev` does the same after
499
+ # regenerating the colour ramps; there is no `nuxt dev` entry point.
471
500
  npm run storybook
472
501
 
473
502
  # Build a static Storybook (outputs to storybook-static/)
@@ -483,6 +512,16 @@ npm run storybook:cache:clean
483
512
  > After clearing the cache, restart with `npm run storybook`. The cache clear is particularly
484
513
  > useful when changes inside `@layer` CSS blocks are not reflected in the running dev server.
485
514
 
515
+ ### MCP server for AI agents
516
+
517
+ `@storybook/addon-mcp` exposes the stories over MCP while the dev server is running, so a coding agent can look up components, their props and story examples instead of guessing. The endpoint is `http://localhost:6006/mcp`. To connect Claude Code:
518
+
519
+ ```bash
520
+ claude mcp add --transport http storybook http://localhost:6006/mcp
521
+ ```
522
+
523
+ See the [Storybook MCP docs](https://storybook.js.org/docs/next/ai/mcp/overview) for other clients.
524
+
486
525
  ### Fonts
487
526
 
488
527
  `@nuxt/fonts` is disabled in Storybook (detected via `process.env.STORYBOOK` in `nuxt.config.ts`).
@@ -492,5 +531,6 @@ Fonts are served instead from local files in `.storybook/public/_fonts/`, declar
492
531
  | ---------------- | ------ | -------------------------------------------- |
493
532
  | Poppins | TTF | `.storybook/public/_fonts/poppins/` |
494
533
  | Playfair Display | woff2 | `.storybook/public/_fonts/playfair-display/` |
534
+ | Mono MMM 5 | TTF | `public/fonts/` |
495
535
 
496
536
  To add a new font, see [.claude/skills/storybook-add-font.md](.claude/skills/storybook-add-font.md).
@@ -1,15 +1,6 @@
1
1
  html {
2
- color-scheme: light dark;
3
2
  interpolate-size: allow-keywords;
4
3
 
5
- &[data-color-scheme="light"] {
6
- color-scheme: light;
7
- }
8
-
9
- &[data-color-scheme="dark"] {
10
- color-scheme: dark;
11
- }
12
-
13
4
  font-size: 62.5%;
14
5
 
15
6
  transition:
@@ -6,10 +6,10 @@
6
6
  /* ===========================================
7
7
  GENERIC BODY VARIABLES
8
8
  =========================================== */
9
- --page-bg: light-dark(var(--slate-00), var(--slate-08));
10
- --colour-text-default: light-dark(var(--slate-09), var(--slate-01));
11
- --colour-text-accent: light-dark(var(--blue-09), var(--blue-05));
12
- --colour-text-eyebrow: light-dark(var(--blue-09), var(--blue-05));
9
+ --page-bg: var(--slate-00);
10
+ --colour-text-default: var(--slate-09);
11
+ --colour-text-accent: var(--blue-09);
12
+ --colour-text-eyebrow: var(--blue-09);
13
13
 
14
14
  /* ===========================================
15
15
  HERO / EYEBROW TEXT VARIABLES
@@ -21,46 +21,46 @@
21
21
  STEPPER LIST VARIABLES
22
22
  =========================================== */
23
23
  --stepper-list-counter-circle-background: transparent;
24
- --stepper-list-counter-circle-text: light-dark(var(--blue-09), var(--blue-02));
25
- --stepper-list-counter-circle-border: light-dark(var(--blue-09), var(--blue-02));
26
- --stepper-list-counter-disc-background: light-dark(var(--blue-01), var(--blue-02));
24
+ --stepper-list-counter-circle-text: var(--blue-09);
25
+ --stepper-list-counter-circle-border: var(--blue-09);
26
+ --stepper-list-counter-disc-background: var(--blue-01);
27
27
  --stepper-list-counter-disc-text: var(--blue-09);
28
- --stepper-list-counter-disc-border: light-dark(var(--blue-01), var(--blue-02));
28
+ --stepper-list-counter-disc-border: var(--blue-01);
29
29
  --stepper-list-counter-square-background: transparent;
30
- --stepper-list-counter-square-text: light-dark(var(--blue-09), var(--blue-02));
31
- --stepper-list-counter-square-border: light-dark(var(--blue-09), var(--blue-02));
32
- --stepper-list-connector-color: light-dark(var(--blue-01), var(--blue-02));
33
- --stepper-list-icon-color: light-dark(var(--blue-09), var(--blue-02));
30
+ --stepper-list-counter-square-text: var(--blue-09);
31
+ --stepper-list-counter-square-border: var(--blue-09);
32
+ --stepper-list-connector-color: var(--blue-01);
33
+ --stepper-list-icon-color: var(--blue-09);
34
34
 
35
35
  /* ===========================================
36
36
  TREATMENT CONSULTANT VARIABLES
37
37
  =========================================== */
38
- --treatment-consultant-primary-colour: light-dark(var(--amber-05), var(--amber-02));
39
- --treatment-consultant-primary-foreground: light-dark(var(--amber-09), var(--amber-02));
40
- --treatment-consultant-muted-colour: light-dark(var(--amber-03), var(--amber-05));
41
- --treatment-consultant-muted-foreground: light-dark(var(--amber-07), var(--amber-05));
42
- --treatment-consultant-background: light-dark(var(--amber-01), var(--amber-10));
43
- --treatment-consultant-foreground: light-dark(var(--amber-09), var(--amber-02));
44
- --treatment-consultant-border-colour: light-dark(var(--blue-02), var(--amber-03));
45
- --treatment-consultant-checked-surface-colour: light-dark(var(--green-03), var(--green-10));
46
- --treatment-consultant-checked-stroke-colour: light-dark(var(--green-09), var(--green-03));
47
- --treatment-consultant-conflict-surface-colour: light-dark(var(--sunset-03), var(--sunset-09));
48
- --treatment-consultant-conflict-stroke-colour: light-dark(var(--sunset-09), var(--sunset-03));
38
+ --treatment-consultant-primary-colour: var(--amber-05);
39
+ --treatment-consultant-primary-foreground: var(--amber-09);
40
+ --treatment-consultant-muted-colour: var(--amber-03);
41
+ --treatment-consultant-muted-foreground: var(--amber-07);
42
+ --treatment-consultant-background: var(--amber-01);
43
+ --treatment-consultant-foreground: var(--amber-09);
44
+ --treatment-consultant-border-colour: var(--blue-02);
45
+ --treatment-consultant-checked-surface-colour: var(--green-03);
46
+ --treatment-consultant-checked-stroke-colour: var(--green-09);
47
+ --treatment-consultant-conflict-surface-colour: var(--sunset-03);
48
+ --treatment-consultant-conflict-stroke-colour: var(--sunset-09);
49
49
 
50
50
  /* ===========================================
51
51
  ANCHOR LINK VARIABLES
52
52
  =========================================== */
53
- --colour-link-default: light-dark(var(--blue-10), var(--blue-02));
54
- --colour-link-hover: light-dark(var(--blue-09), var(--blue-03));
53
+ --colour-link-default: var(--blue-10);
54
+ --colour-link-hover: var(--blue-09);
55
55
 
56
56
  /* ===========================================
57
57
  STRUCTURAL INPUT TOKENS
58
58
  Neutral slate surface — independent of theme colour
59
59
  =========================================== */
60
- --theme-input-surface: light-dark(var(--slate-00), var(--slate-10));
61
- --theme-input-surface-hover: light-dark(var(--slate-01), var(--slate-09));
62
- --theme-input-text-color-normal: light-dark(var(--slate-09), var(--slate-01));
63
- --theme-input-placeholder: light-dark(var(--slate-05), var(--slate-04));
60
+ --theme-input-surface: var(--slate-00);
61
+ --theme-input-surface-hover: var(--slate-01);
62
+ --theme-input-text-color-normal: var(--slate-09);
63
+ --theme-input-placeholder: var(--slate-05);
64
64
  --theme-checkbox-symbol-surface: var(--theme-input-surface);
65
65
  --input-error-margin-block-start: 0.25rem;
66
66
  }
@@ -8,7 +8,7 @@
8
8
  These are always error-red regardless of nesting context.
9
9
  =========================================== */
10
10
  --input-error-color: white;
11
- --theme-error-surface: light-dark(var(--red-06), var(--red-07));
12
- --theme-error-border: light-dark(var(--red-06), var(--red-07));
13
- --theme-error-outline: light-dark(var(--red-01), var(--red-05));
11
+ --theme-error-surface: var(--red-06);
12
+ --theme-error-border: var(--red-06);
13
+ --theme-error-outline: var(--red-01);
14
14
  }