srcdev-nuxt-components 9.5.3 → 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.
- package/.claude/skills/colour-scheme-disable.md +1 -1
- package/.claude/skills/component-compliance-checklist.md +214 -0
- package/.claude/skills/component-inline-action-button.md +2 -2
- package/.claude/skills/components/carousel-flip.md +3 -3
- package/.claude/skills/components/clipped-panel.md +2 -2
- package/.claude/skills/components/dashboard-quad-grid.md +2 -2
- package/.claude/skills/components/dashboard-stats-grid.md +1 -1
- package/.claude/skills/components/deep-expanding-menu-classic.md +1 -1
- package/.claude/skills/components/deep-expanding-menu.md +1 -1
- package/.claude/skills/components/google-reviews.md +193 -0
- package/.claude/skills/components/input-number.md +4 -0
- package/.claude/skills/components/input-otp.md +120 -0
- package/.claude/skills/components/section-parallax.md +1 -1
- package/.claude/skills/components/select-menu.md +9 -5
- package/.claude/skills/composable-colour-scheme.md +2 -0
- package/.claude/skills/index.md +5 -1
- package/.claude/skills/new-app-scaffold.md +1 -1
- package/.claude/skills/theming-colour-ramps.md +6 -33
- package/.claude/skills/theming-component-token-pattern.md +30 -0
- package/.claude/skills/theming-dark-mode.md +243 -0
- package/.claude/skills/theming-override-default.md +6 -7
- package/.claude/skills/theming-partial-override.md +11 -11
- package/.vscode/srcdev-component-google-reviews.code-snippets +78 -0
- package/.vscode/srcdev-component-input-otp.code-snippets +47 -0
- package/.vscode/srcdev-component-select-menu.code-snippets +13 -0
- package/README.md +81 -41
- package/app/assets/styles/setup/01.config/_head.css +0 -9
- package/app/assets/styles/setup/03.theming/_default.css +29 -29
- package/app/assets/styles/setup/03.theming/_error.css +3 -3
- package/app/assets/styles/setup/03.theming/_theme-slots.css +13 -16
- package/app/assets/styles/setup/a11y/_variables.css +2 -2
- package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +7 -7
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +10 -0
- package/app/components/02.molecules/select-menu/SelectMenu.vue +15 -1
- package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +35 -0
- package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +9 -0
- package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +3 -3
- package/app/components/03.organisms/google-reviews/CONSUMER-STYLING.md +110 -0
- package/app/components/03.organisms/google-reviews/GoogleReviewCard.vue +161 -0
- package/app/components/03.organisms/google-reviews/GoogleReviews.vue +275 -0
- package/app/components/03.organisms/google-reviews/stories/GoogleReviews.stories.ts +323 -0
- package/app/components/03.organisms/google-reviews/tests/GoogleReviewCard.spec.ts +104 -0
- package/app/components/03.organisms/google-reviews/tests/GoogleReviews.spec.ts +232 -0
- package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviewCard.spec.ts.snap +9 -0
- package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviews.spec.ts.snap +43 -0
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +1 -1
- package/app/components/05.forms/input-number/InputNumber.vue +2 -2
- package/app/components/05.forms/input-otp/CONSUMER-STYLING.md +59 -0
- package/app/components/05.forms/input-otp/InputOtp.vue +254 -0
- package/app/components/05.forms/input-otp/InputOtpField.vue +106 -0
- package/app/components/05.forms/input-otp/stories/InputOtp.stories.ts +100 -0
- package/app/components/05.forms/input-otp/stories/InputOtpField.stories.ts +163 -0
- package/app/components/05.forms/input-otp/tests/InputOtp.spec.ts +275 -0
- package/app/components/05.forms/input-otp/tests/InputOtpField.spec.ts +76 -0
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +151 -33
- package/app/composables/useGoogleReviews.ts +4 -0
- package/app/types/components/google-reviews.d.ts +18 -0
- package/app/types/components/index.ts +1 -0
- package/nuxt.config.ts +9 -9
- package/package.json +9 -8
|
@@ -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",
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## NOTE
|
|
8
8
|
|
|
9
|
-
|
|
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.
|
|
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
|
|
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:
|
|
226
|
-
--colour-text-eyebrow:
|
|
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
|
|
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).
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
/* ===========================================
|
|
7
7
|
GENERIC BODY VARIABLES
|
|
8
8
|
=========================================== */
|
|
9
|
-
--page-bg:
|
|
10
|
-
--colour-text-default:
|
|
11
|
-
--colour-text-accent:
|
|
12
|
-
--colour-text-eyebrow:
|
|
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:
|
|
25
|
-
--stepper-list-counter-circle-border:
|
|
26
|
-
--stepper-list-counter-disc-background:
|
|
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:
|
|
28
|
+
--stepper-list-counter-disc-border: var(--blue-01);
|
|
29
29
|
--stepper-list-counter-square-background: transparent;
|
|
30
|
-
--stepper-list-counter-square-text:
|
|
31
|
-
--stepper-list-counter-square-border:
|
|
32
|
-
--stepper-list-connector-color:
|
|
33
|
-
--stepper-list-icon-color:
|
|
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:
|
|
39
|
-
--treatment-consultant-primary-foreground:
|
|
40
|
-
--treatment-consultant-muted-colour:
|
|
41
|
-
--treatment-consultant-muted-foreground:
|
|
42
|
-
--treatment-consultant-background:
|
|
43
|
-
--treatment-consultant-foreground:
|
|
44
|
-
--treatment-consultant-border-colour:
|
|
45
|
-
--treatment-consultant-checked-surface-colour:
|
|
46
|
-
--treatment-consultant-checked-stroke-colour:
|
|
47
|
-
--treatment-consultant-conflict-surface-colour:
|
|
48
|
-
--treatment-consultant-conflict-stroke-colour:
|
|
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:
|
|
54
|
-
--colour-link-hover:
|
|
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:
|
|
61
|
-
--theme-input-surface-hover:
|
|
62
|
-
--theme-input-text-color-normal:
|
|
63
|
-
--theme-input-placeholder:
|
|
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:
|
|
12
|
-
--theme-error-border:
|
|
13
|
-
--theme-error-outline:
|
|
11
|
+
--theme-error-surface: var(--red-06);
|
|
12
|
+
--theme-error-border: var(--red-06);
|
|
13
|
+
--theme-error-outline: var(--red-01);
|
|
14
14
|
}
|
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
/* Theme semantic slots — 8 shared colour roles used by all themed components.
|
|
2
2
|
Declared on the same selector as theme-ramp.css so the formula re-evaluates
|
|
3
|
-
per [data-theme] element rather than being inherited from html.
|
|
3
|
+
per [data-theme] element rather than being inherited from html.
|
|
4
|
+
Light values only, no light-dark(): dark mode is the consumer app's call. */
|
|
4
5
|
:where(html, [data-theme], [data-invalid]) {
|
|
5
|
-
--theme-surface:
|
|
6
|
-
--theme-surface-inverted:
|
|
7
|
-
--theme-surface-hover:
|
|
8
|
-
--theme-accent:
|
|
9
|
-
--theme-surface-subtle:
|
|
10
|
-
--theme-border:
|
|
11
|
-
--theme-border-focus:
|
|
12
|
-
--theme-ring:
|
|
6
|
+
--theme-surface: var(--colour-theme-7);
|
|
7
|
+
--theme-surface-inverted: var(--colour-theme-9);
|
|
8
|
+
--theme-surface-hover: var(--colour-theme-9);
|
|
9
|
+
--theme-accent: var(--colour-theme-5);
|
|
10
|
+
--theme-surface-subtle: var(--colour-theme-1);
|
|
11
|
+
--theme-border: var(--colour-theme-6);
|
|
12
|
+
--theme-border-focus: var(--colour-theme-4);
|
|
13
|
+
--theme-ring: var(--colour-theme-1);
|
|
13
14
|
--theme-on-surface: var(--colour-theme-0);
|
|
14
|
-
--theme-text:
|
|
15
|
-
/* Light
|
|
16
|
-
|
|
17
|
-
color), so secondary-button text was invisible (dark-on-dark) in light mode. Should be a
|
|
18
|
-
light step to contrast against that dark inverted surface, matching how --theme-on-surface
|
|
19
|
-
(line 13) contrasts against --theme-surface the same way in both modes. */
|
|
20
|
-
--theme-text-inverted: light-dark(var(--colour-theme-0), var(--colour-theme-7));
|
|
15
|
+
--theme-text: var(--colour-theme-9);
|
|
16
|
+
/* Light step: pairs with the dark --theme-surface-inverted (InputButton .secondary). */
|
|
17
|
+
--theme-text-inverted: var(--colour-theme-0);
|
|
21
18
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
:where(html) {
|
|
2
2
|
--focus-box-shadow-colour-off: transparent;
|
|
3
|
-
--focus-box-shadow-colour-on:
|
|
3
|
+
--focus-box-shadow-colour-on: var(--blue-10);
|
|
4
4
|
--form-focus-box-shadow: 0 0 0 0.3rem;
|
|
5
5
|
|
|
6
6
|
--box-shadow-off: 0 0 0 0.3rem transparent;
|
|
7
|
-
--box-shadow-on: 0 0 0 0.3rem
|
|
7
|
+
--box-shadow-on: 0 0 0 0.3rem var(--blue-10);
|
|
8
8
|
}
|
|
@@ -218,7 +218,7 @@ export const InContext: Story = {
|
|
|
218
218
|
align-items: center;
|
|
219
219
|
gap: 1.2rem;
|
|
220
220
|
padding: 1.2rem 1.6rem;
|
|
221
|
-
border-bottom: 0.1rem solid
|
|
221
|
+
border-bottom: 0.1rem solid rgb(128 128 128 / 0.35);
|
|
222
222
|
"
|
|
223
223
|
>
|
|
224
224
|
<div
|
|
@@ -226,7 +226,7 @@ export const InContext: Story = {
|
|
|
226
226
|
width: 3.6rem;
|
|
227
227
|
height: 3.6rem;
|
|
228
228
|
border-radius: 50%;
|
|
229
|
-
background:
|
|
229
|
+
background: rgb(128 128 128 / 0.35);
|
|
230
230
|
display: grid;
|
|
231
231
|
place-items: center;
|
|
232
232
|
font-size: 1.4rem;
|
|
@@ -290,17 +290,17 @@ export const ItemCoreStandalone: Story = {
|
|
|
290
290
|
template: `
|
|
291
291
|
<div style="padding: 4rem; max-width: 32rem; margin: 0 auto;">
|
|
292
292
|
<p style="margin: 0 0 1.2rem; font-size: 1.2rem; opacity: 0.5; text-transform: uppercase; letter-spacing: 0.08em;">Button items</p>
|
|
293
|
-
<div style="border: 0.1rem solid
|
|
293
|
+
<div style="border: 0.1rem solid rgb(128 128 128 / 0.35); border-radius: 0.8rem; overflow: hidden;">
|
|
294
294
|
<ActionMenuItem
|
|
295
295
|
label="Edit"
|
|
296
|
-
style="border-bottom: 0.1rem solid
|
|
296
|
+
style="border-bottom: 0.1rem solid rgb(128 128 128 / 0.35);"
|
|
297
297
|
@click="lastClick = 'Edit'"
|
|
298
298
|
>
|
|
299
299
|
<template #icon><Icon name="lucide:pencil" /></template>
|
|
300
300
|
</ActionMenuItem>
|
|
301
301
|
<ActionMenuItem
|
|
302
302
|
label="Duplicate"
|
|
303
|
-
style="border-bottom: 0.1rem solid
|
|
303
|
+
style="border-bottom: 0.1rem solid rgb(128 128 128 / 0.35);"
|
|
304
304
|
@click="lastClick = 'Duplicate'"
|
|
305
305
|
>
|
|
306
306
|
<template #icon><Icon name="lucide:copy" /></template>
|
|
@@ -314,11 +314,11 @@ export const ItemCoreStandalone: Story = {
|
|
|
314
314
|
</div>
|
|
315
315
|
|
|
316
316
|
<p style="margin: 2.4rem 0 1.2rem; font-size: 1.2rem; opacity: 0.5; text-transform: uppercase; letter-spacing: 0.08em;">Link items</p>
|
|
317
|
-
<div style="border: 0.1rem solid
|
|
317
|
+
<div style="border: 0.1rem solid rgb(128 128 128 / 0.35); border-radius: 0.8rem; overflow: hidden;">
|
|
318
318
|
<ActionMenuItem
|
|
319
319
|
label="Internal link (/about)"
|
|
320
320
|
href="/about"
|
|
321
|
-
style="border-bottom: 0.1rem solid
|
|
321
|
+
style="border-bottom: 0.1rem solid rgb(128 128 128 / 0.35);"
|
|
322
322
|
>
|
|
323
323
|
<template #icon><Icon name="lucide:arrow-up-right" /></template>
|
|
324
324
|
</ActionMenuItem>
|
|
@@ -14,6 +14,7 @@ global token so it matches the rest of the design system out of the box.
|
|
|
14
14
|
| `--select-menu-trigger-padding-block` | `0.8rem` | Trigger vertical padding |
|
|
15
15
|
| `--select-menu-trigger-padding-inline` | `1.2rem` | Trigger horizontal padding |
|
|
16
16
|
| `--select-menu-trigger-border-width` | `0.1rem` | Trigger border thickness |
|
|
17
|
+
| `--select-menu-trigger-border-width-underlined` | `var(--form-element-border-bottom-width-underlined, 0.3rem)` | Bottom border thickness in the `underlined` variant |
|
|
17
18
|
| `--select-menu-trigger-border` | `var(--theme-border)` | Trigger border colour (rest state) |
|
|
18
19
|
| `--select-menu-trigger-border-focus` | `var(--theme-border-focus)` | Trigger border colour on hover/focus |
|
|
19
20
|
| `--select-menu-trigger-border-radius` | `0.5rem` | Trigger corner rounding |
|
|
@@ -54,6 +55,15 @@ global token so it matches the rest of the design system out of the box.
|
|
|
54
55
|
|
|
55
56
|
---
|
|
56
57
|
|
|
58
|
+
## State hooks
|
|
59
|
+
|
|
60
|
+
| Hook | Where | Meaning |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `.normal` / `.underlined` | `.select-menu` root | The `inputVariant` prop. `underlined` removes the trigger's top and side borders and its corner radius, matching `InputSelect`'s underlined variant. |
|
|
63
|
+
| `:has(.select-menu-popover:popover-open)` | `.select-menu` root | Menu open (rotates the chevron). |
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
57
67
|
## Global theming
|
|
58
68
|
|
|
59
69
|
Create `assets/styles/setup/07.components/select-menu.css` in the consuming app and set tokens
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="select-menu" :class="[elementClasses]" :style="`--_anchor-name: ${anchorName}`">
|
|
2
|
+
<div class="select-menu" :class="[inputVariant, elementClasses]" :style="`--_anchor-name: ${anchorName}`">
|
|
3
3
|
<button
|
|
4
4
|
ref="triggerRef"
|
|
5
5
|
:popovertarget="menuId"
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
|
|
62
62
|
<script setup lang="ts">
|
|
63
63
|
import type { SelectMenuOption } from "~/types/components/select-menu";
|
|
64
|
+
import type { InputUiVariant } from "~/types/forms/types.forms";
|
|
64
65
|
|
|
65
66
|
interface Props {
|
|
66
67
|
/** The full list of selectable options. */
|
|
@@ -79,6 +80,8 @@ interface Props {
|
|
|
79
80
|
multiple?: boolean;
|
|
80
81
|
/** In multiple mode, update the trigger text to a comma-separated list of the currently checked options instead of leaving it fixed on placeholder/label. No effect outside multiple mode. */
|
|
81
82
|
showSelectionInTrigger?: boolean;
|
|
83
|
+
/** Trigger border style, matching InputSelect: `normal` (bordered box) or `underlined` (bottom border only). */
|
|
84
|
+
inputVariant?: InputUiVariant;
|
|
82
85
|
styleClassPassthrough?: string | string[];
|
|
83
86
|
}
|
|
84
87
|
|
|
@@ -89,6 +92,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
89
92
|
showChevron: true,
|
|
90
93
|
multiple: false,
|
|
91
94
|
showSelectionInTrigger: false,
|
|
95
|
+
inputVariant: "normal",
|
|
92
96
|
styleClassPassthrough: () => [],
|
|
93
97
|
});
|
|
94
98
|
|
|
@@ -273,6 +277,16 @@ watch(
|
|
|
273
277
|
}
|
|
274
278
|
}
|
|
275
279
|
|
|
280
|
+
&.underlined .select-menu-trigger {
|
|
281
|
+
border-block-start-width: 0;
|
|
282
|
+
border-inline-width: 0;
|
|
283
|
+
border-block-end-width: var(
|
|
284
|
+
--select-menu-trigger-border-width-underlined,
|
|
285
|
+
var(--form-element-border-bottom-width-underlined, 0.3rem)
|
|
286
|
+
);
|
|
287
|
+
border-radius: 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
276
290
|
&:has(.select-menu-popover:popover-open) .select-menu-trigger-chevron {
|
|
277
291
|
transform: rotate(180deg);
|
|
278
292
|
}
|
|
@@ -14,6 +14,7 @@ interface StoryArgs {
|
|
|
14
14
|
showChevron?: boolean;
|
|
15
15
|
multiple?: boolean;
|
|
16
16
|
showSelectionInTrigger?: boolean;
|
|
17
|
+
inputVariant?: "normal" | "underlined";
|
|
17
18
|
modelValue?: string | number | (string | number)[];
|
|
18
19
|
styleClassPassthrough?: string | string[];
|
|
19
20
|
}
|
|
@@ -57,6 +58,12 @@ const meta: Meta<StoryArgs> = {
|
|
|
57
58
|
description: "In multiple mode, update the trigger text to a comma-separated list of the currently checked options instead of leaving it fixed on placeholder/label. No effect outside multiple mode.",
|
|
58
59
|
table: { category: "Content" },
|
|
59
60
|
},
|
|
61
|
+
inputVariant: {
|
|
62
|
+
control: { type: "select" },
|
|
63
|
+
options: ["normal", "underlined"],
|
|
64
|
+
description: "Trigger border style, matching InputSelect: bordered box or bottom border only.",
|
|
65
|
+
table: { category: "Styling" },
|
|
66
|
+
},
|
|
60
67
|
styleClassPassthrough: {
|
|
61
68
|
table: { disable: true },
|
|
62
69
|
},
|
|
@@ -66,6 +73,7 @@ const meta: Meta<StoryArgs> = {
|
|
|
66
73
|
showIcon: true,
|
|
67
74
|
showLabel: true,
|
|
68
75
|
showChevron: true,
|
|
76
|
+
inputVariant: "normal",
|
|
69
77
|
},
|
|
70
78
|
parameters: {
|
|
71
79
|
docs: {
|
|
@@ -357,3 +365,30 @@ export const CustomTokens: Story = {
|
|
|
357
365
|
`,
|
|
358
366
|
}),
|
|
359
367
|
};
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Underlined — bottom border only, matching InputSelect's `underlined` variant
|
|
371
|
+
* so a SelectMenu can sit next to underlined form fields.
|
|
372
|
+
*/
|
|
373
|
+
export const Underlined: Story = {
|
|
374
|
+
args: {
|
|
375
|
+
label: "Choose a service",
|
|
376
|
+
showIcon: false,
|
|
377
|
+
inputVariant: "underlined",
|
|
378
|
+
},
|
|
379
|
+
render: (args) => ({
|
|
380
|
+
components: { SelectMenu },
|
|
381
|
+
setup() {
|
|
382
|
+
const value = ref<string | undefined>(undefined);
|
|
383
|
+
return { args, value, serviceOptions };
|
|
384
|
+
},
|
|
385
|
+
template: `
|
|
386
|
+
<div style="padding: 4rem 8rem;">
|
|
387
|
+
<SelectMenu v-bind="args" v-model="value" :options="serviceOptions" />
|
|
388
|
+
<p style="margin: 1.6rem 0 0; font-size: 1.3rem; opacity: 0.6;">
|
|
389
|
+
modelValue: <strong>{{ value === undefined ? 'undefined' : value }}</strong>
|
|
390
|
+
</p>
|
|
391
|
+
</div>
|
|
392
|
+
`,
|
|
393
|
+
}),
|
|
394
|
+
};
|
|
@@ -89,6 +89,15 @@ describe("SelectMenu", () => {
|
|
|
89
89
|
expect(wrapper.find(".select-menu-trigger").attributes("aria-haspopup")).toBe("listbox");
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
it("applies the normal variant class by default and underlined when set", async () => {
|
|
93
|
+
wrapper = await createWrapper();
|
|
94
|
+
expect(wrapper.find(".select-menu").classes()).toContain("normal");
|
|
95
|
+
|
|
96
|
+
await wrapper.setProps({ inputVariant: "underlined" });
|
|
97
|
+
expect(wrapper.find(".select-menu").classes()).toContain("underlined");
|
|
98
|
+
expect(wrapper.find(".select-menu").classes()).not.toContain("normal");
|
|
99
|
+
});
|
|
100
|
+
|
|
92
101
|
it("uses label prop as aria-label", async () => {
|
|
93
102
|
wrapper = await createWrapper({ label: "Filter by service" });
|
|
94
103
|
expect(wrapper.find(".select-menu-trigger").attributes("aria-label")).toBe("Filter by service");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
3
|
exports[`SelectMenu > Snapshots > default 1`] = `
|
|
4
|
-
"<div class="select-menu" style="--_anchor-name: --select-menu-anchor-v-0-0;"><button popovertarget="select-menu-v-0-0" popovertargetaction="toggle" type="button" class="select-menu-trigger" aria-label="Language" aria-haspopup="listbox" aria-expanded="false">
|
|
4
|
+
"<div class="select-menu normal" style="--_anchor-name: --select-menu-anchor-v-0-0;"><button popovertarget="select-menu-v-0-0" popovertargetaction="toggle" type="button" class="select-menu-trigger" aria-label="Language" aria-haspopup="listbox" aria-expanded="false">
|
|
5
5
|
<!--v-if--><span class="select-menu-trigger-label">Language</span><span class="iconify i-lucide:chevron-down select-menu-trigger-chevron" aria-hidden="true"></span>
|
|
6
6
|
</button>
|
|
7
7
|
<div id="select-menu-v-0-0" popover="" class="select-menu-popover">
|
|
@@ -15,7 +15,7 @@ exports[`SelectMenu > Snapshots > default 1`] = `
|
|
|
15
15
|
`;
|
|
16
16
|
|
|
17
17
|
exports[`SelectMenu > Snapshots > icon-only trigger 1`] = `
|
|
18
|
-
"<div class="select-menu" style="--_anchor-name: --select-menu-anchor-v-0-0;"><button popovertarget="select-menu-v-0-0" popovertargetaction="toggle" type="button" class="select-menu-trigger" aria-label="Language" aria-haspopup="listbox" aria-expanded="false"><span class="iconify i-flag:gb-4x3 select-menu-trigger-icon" aria-hidden="true"></span>
|
|
18
|
+
"<div class="select-menu normal" style="--_anchor-name: --select-menu-anchor-v-0-0;"><button popovertarget="select-menu-v-0-0" popovertargetaction="toggle" type="button" class="select-menu-trigger" aria-label="Language" aria-haspopup="listbox" aria-expanded="false"><span class="iconify i-flag:gb-4x3 select-menu-trigger-icon" aria-hidden="true"></span>
|
|
19
19
|
<!--v-if-->
|
|
20
20
|
<!--v-if-->
|
|
21
21
|
</button>
|
|
@@ -30,7 +30,7 @@ exports[`SelectMenu > Snapshots > icon-only trigger 1`] = `
|
|
|
30
30
|
`;
|
|
31
31
|
|
|
32
32
|
exports[`SelectMenu > Snapshots > with a selected value 1`] = `
|
|
33
|
-
"<div class="select-menu" style="--_anchor-name: --select-menu-anchor-v-0-0;"><button popovertarget="select-menu-v-0-0" popovertargetaction="toggle" type="button" class="select-menu-trigger" aria-label="Language" aria-haspopup="listbox" aria-expanded="false"><span class="iconify i-flag:fr-4x3 select-menu-trigger-icon" aria-hidden="true"></span><span class="select-menu-trigger-label">Français</span><span class="iconify i-lucide:chevron-down select-menu-trigger-chevron" aria-hidden="true"></span></button>
|
|
33
|
+
"<div class="select-menu normal" style="--_anchor-name: --select-menu-anchor-v-0-0;"><button popovertarget="select-menu-v-0-0" popovertargetaction="toggle" type="button" class="select-menu-trigger" aria-label="Language" aria-haspopup="listbox" aria-expanded="false"><span class="iconify i-flag:fr-4x3 select-menu-trigger-icon" aria-hidden="true"></span><span class="select-menu-trigger-label">Français</span><span class="iconify i-lucide:chevron-down select-menu-trigger-chevron" aria-hidden="true"></span></button>
|
|
34
34
|
<div id="select-menu-v-0-0" popover="" class="select-menu-popover">
|
|
35
35
|
<ul class="select-menu-list" role="listbox" aria-label="Language">
|
|
36
36
|
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:gb-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">English</span></li>
|