srcdev-nuxt-components 9.3.12 → 9.4.0

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 (62) hide show
  1. package/.claude/skills/components/cookie-consent-banner.md +88 -0
  2. package/.claude/skills/composable-cookie-consent.md +78 -0
  3. package/.claude/skills/composable-google-analytics.md +87 -0
  4. package/.claude/skills/index.md +5 -1
  5. package/.claude/skills/theming-component-token-pattern.md +126 -0
  6. package/app/app.config.ts +10 -1
  7. package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +10 -1
  8. package/app/components/01.atoms/cookie-consent-banner/CookieConsentBanner.vue +148 -0
  9. package/app/components/01.atoms/cookie-consent-banner/stories/CookieConsentBanner.stories.ts +93 -0
  10. package/app/components/01.atoms/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +118 -0
  11. package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +3 -3
  12. package/app/components/02.molecules/input-copy/InputCopy.vue +9 -3
  13. package/app/components/02.molecules/pricing-card/CONSUMER-STYLING.md +3 -3
  14. package/app/components/02.molecules/pricing-card/PricingCard.vue +6 -3
  15. package/app/components/05.forms/input-button/CONSUMER-STYLING.md +117 -0
  16. package/app/components/05.forms/input-button/InputButtonCore.vue +83 -37
  17. package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-secondary-chromium-darwin.png +0 -0
  18. package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-tertiary-chromium-darwin.png +0 -0
  19. package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-primary-pending-chromium-darwin.png +0 -0
  20. package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-secondary-pill-chromium-darwin.png +0 -0
  21. package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-tertiary-readonly-chromium-darwin.png +0 -0
  22. package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-chromium-darwin.png +0 -0
  23. package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-default-chromium-darwin.png +0 -0
  24. package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-error-chromium-darwin.png +0 -0
  25. package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-success-chromium-darwin.png +0 -0
  26. package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-warning-chromium-darwin.png +0 -0
  27. package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-primary-chromium-darwin.png +0 -0
  28. package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-secondary-chromium-darwin.png +0 -0
  29. package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-tertiary-chromium-darwin.png +0 -0
  30. package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/nuxt-icon-only-component-chromium-darwin.png +0 -0
  31. package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pending-chromium-darwin.png +0 -0
  32. package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pill-chromium-darwin.png +0 -0
  33. package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/state-readonly-chromium-darwin.png +0 -0
  34. package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/with-both-nuxt-icon-components-chromium-darwin.png +0 -0
  35. package/app/components/05.forms/input-button/stories/InputButtonCore.stories.ts +35 -0
  36. package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +68 -0
  37. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +19 -9
  38. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioCore.vue +8 -5
  39. package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioButton.stories.ts +93 -0
  40. package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioCore.stories.ts +75 -0
  41. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +41 -0
  42. package/app/components/05.forms/input-number/InputNumberCore.vue +12 -6
  43. package/app/components/05.forms/input-select/CONSUMER-STYLING.md +35 -29
  44. package/app/components/05.forms/input-select/InputSelectCore.vue +32 -20
  45. package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +69 -0
  46. package/app/components/05.forms/input-text/CONSUMER-STYLING.md +38 -48
  47. package/app/components/05.forms/input-text/InputTextCore.vue +35 -18
  48. package/app/components/05.forms/patterns/stories/ContactForm.stories.ts +184 -0
  49. package/app/components/05.forms/patterns/stories/LoginForm.stories.ts +125 -0
  50. package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +49 -0
  51. package/app/components/05.forms/toggle-switch/ToggleSwitchCore.vue +23 -16
  52. package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +72 -0
  53. package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitchCore.vue +42 -20
  54. package/app/components/05.forms/triple-toggle-switch/stories/TripleToggleSwitchCore.stories.ts +63 -0
  55. package/app/components/display-theme-switch/DisplayThemeSwitch.vue +13 -6
  56. package/app/composables/tests/useCookieConsent.spec.ts +87 -0
  57. package/app/composables/useCookieConsent.ts +54 -0
  58. package/app/composables/useGoogleAnalytics.ts +26 -0
  59. package/app/types/components/cookie-consent-banner.d.ts +8 -0
  60. package/app/types/components/index.ts +1 -0
  61. package/nuxt.config.ts +6 -0
  62. package/package.json +2 -1
@@ -0,0 +1,69 @@
1
+ import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
+ import { ref } from "vue";
3
+ import StorybookComponent from "../InputSelectCore.vue";
4
+ import type { IFormMultipleOptions } from "~/types/forms/types.forms.d";
5
+
6
+ interface InputSelectCoreStoryArgs {
7
+ id: string;
8
+ name: string;
9
+ placeholder: string;
10
+ required: boolean;
11
+ fieldHasError: boolean;
12
+ inputVariant: "normal" | "outlined" | "underlined";
13
+ }
14
+
15
+ export default {
16
+ title: "Components/Forms/Input Select/InputSelectCore",
17
+ component: StorybookComponent,
18
+ argTypes: {
19
+ id: { control: "text", table: { category: "Basic" } },
20
+ name: { control: "text", table: { category: "Basic" } },
21
+ placeholder: { control: "text", table: { category: "Basic" } },
22
+ required: { control: "boolean", table: { category: "Basic" } },
23
+ fieldHasError: { control: "boolean", table: { category: "States" } },
24
+ inputVariant: {
25
+ control: { type: "select" },
26
+ options: ["normal", "outlined", "underlined"],
27
+ table: { category: "Styling" },
28
+ },
29
+ },
30
+ args: {
31
+ id: "select-default",
32
+ name: "selectDefault",
33
+ placeholder: "Choose a service",
34
+ required: false,
35
+ fieldHasError: false,
36
+ inputVariant: "normal",
37
+ },
38
+ } as Meta<InputSelectCoreStoryArgs>;
39
+
40
+ const Template: StoryFn<InputSelectCoreStoryArgs> = (args) => ({
41
+ components: { StorybookComponent },
42
+ setup() {
43
+ const modelValue = ref<string | number>("");
44
+ const fieldData = ref<IFormMultipleOptions>({
45
+ data: [
46
+ { id: "1", name: "balayage", value: "balayage", label: "Balayage" },
47
+ { id: "2", name: "highlights", value: "highlights", label: "Highlights" },
48
+ { id: "3", name: "full-colour", value: "full-colour", label: "Full Colour" },
49
+ ],
50
+ total: 3,
51
+ skip: 0,
52
+ limit: 10,
53
+ });
54
+ return { args, modelValue, fieldData };
55
+ },
56
+ template: `
57
+ <div style="margin: 36px; max-width: 320px;">
58
+ <StorybookComponent v-bind="args" v-model="modelValue" v-model:field-data="fieldData" />
59
+ </div>
60
+ `,
61
+ });
62
+
63
+ export const Default = Template.bind({});
64
+
65
+ export const ErrorState = Template.bind({});
66
+ ErrorState.args = { fieldHasError: true };
67
+
68
+ export const Underlined = Template.bind({});
69
+ Underlined.args = { inputVariant: "underlined" };
@@ -1,60 +1,50 @@
1
1
  # InputTextCore — Consumer Styling Guide
2
2
 
3
- ## Two override paths
4
-
5
- Before 2026-08-22, `.input-text-wrapper` read the global `--theme-*` tokens directly
6
- (`--theme-input-surface`, `--theme-border`, `--theme-border-focus`, `--theme-input-surface-hover`).
7
- Overriding those globally (e.g. in a consumer app's unlayered `:root` block) should always have
8
- worked on pure CSS-cascade grounds — an unlayered `:root` declaration (specificity `0,1,0`) beats
9
- the layer's own `:where(html, [data-theme], [data-invalid])` declaration (specificity `0`)
10
- regardless of source/import order — but a consumer app hit a case where a global override wasn't
11
- visibly landing and the root cause wasn't pinned down (see `nuxt-components`'s own git history
12
- around the same date for a *different*, confirmed bug in a sibling app: wrapping consumer CSS in
13
- `@layer consumer` broke in production because SSR-inlined per-chunk `<style>` tags fix layer
14
- priority by first-reference document order, not the app's intended `@layer` statement order — sic
15
- that isn't what was happening here, since the consumer app's CSS was already unlayered, but it's
16
- the same class of "cascade behaves differently than a clean mental model predicts" issue).
17
-
18
- Rather than leave global-token overrides as the only lever, `.input-text-wrapper` now also
19
- exposes its own local tokens, one indirection step below the global ones:
3
+ ## Public token API
4
+
5
+ Every colour token below is `var(--input-text-*, {default})` — set the `--input-text-*` name to
6
+ override just this component; leave it unset and it inherits the shared `--theme-input-*`/
7
+ `--theme-*` token every other themed component also falls back to. See
8
+ `theming-component-token-pattern.md` for why this two-tier shape exists.
9
+
10
+ (Before 2026-08-25 this component used a private `--_input-text-*` naming scheme presented as a
11
+ "local override path" — that's gone. A private, underscore-prefixed custom property was never a
12
+ real override point; renaming it public with a real fallback is the actual fix, not a second
13
+ documented path to reach for.)
14
+
15
+ | Token | Falls back to | Controls |
16
+ |---|---|---|
17
+ | `--input-text-surface` | `var(--theme-input-surface)` | Wrapper background |
18
+ | `--input-text-surface-hover` | `var(--theme-input-surface-hover)` | Background of an embedded `InputButtonCore` slot on hover |
19
+ | `--input-text-border` | `var(--theme-border)` | Border colour (`.normal`/`.underlined`, resting) |
20
+ | `--input-text-border-hover` | `var(--theme-border-focus)` | Outline colour on hover (mouse) |
21
+ | `--input-text-border-focus` | `var(--theme-border-focus)` | Outline colour on `:focus-visible` (keyboard/assistive) |
22
+ | `--input-text-color` | `var(--theme-input-text-color-normal)` | Input text colour |
23
+ | `--input-text-placeholder-color` | `var(--theme-input-placeholder)` | Placeholder text colour |
24
+
25
+ Geometry (`--input-padding-*`, `--input-min-height`, `--input-font-size`,
26
+ `--form-element-*`, etc.) is shared across every `05.forms` component and untouched by this —
27
+ see `theming-form-geometry-tokens.md`.
20
28
 
21
- ```css
22
- --_input-text-surface: var(--theme-input-surface);
23
- --_input-text-surface-hover: var(--theme-input-surface-hover);
24
- --_input-text-border: var(--theme-border);
25
- --_input-text-border-focus: var(--theme-border-focus);
26
- ```
29
+ ---
27
30
 
28
- - **Global override** (affects every themed component, not just this one): redeclare
29
- `--theme-input-surface` / `--theme-border` / `--theme-border-focus` / `--theme-input-surface-hover`
30
- at whatever scope you'd normally set theme tokens (`:root`, `[data-theme]`, a page wrapper).
31
- - **Local override** (this component only, guaranteed to win — it's the last declaration before
32
- the property that consumes it, no global-token indirection to reason about):
31
+ ## Global theming — app-level CSS file
33
32
 
34
33
  ```css
35
- /* Unscoped consumer CSS, no :deep() needed */
36
- .input-text-wrapper {
37
- --_input-text-border: var(--slate-06);
38
- --_input-text-border-focus: var(--slate-04);
39
- --_input-text-surface: var(--slate-01);
34
+ :where(html) {
35
+ --input-text-surface: var(--rose-09);
36
+ --input-text-border: var(--rose-05);
37
+ --input-text-border-focus: var(--rose-03);
40
38
  }
41
39
  ```
42
40
 
43
- Prefer the local override when you only want to change this one component's look, or when you've
44
- hit a case (like the one above) where a global-token change isn't landing and you don't have time
45
- to dig into why — the local path is a strictly simpler cascade to reason about.
41
+ Only declare the tokens you want to change — everything else keeps inheriting the shared
42
+ `--theme-input-*`/`--theme-*` tokens.
46
43
 
47
44
  ---
48
45
 
49
- ## Other tokens
50
-
51
- - `--input-icon-slot-gap`, `--input-padding-inline`, `--input-padding-block`, `--input-min-height`,
52
- `--input-font-size`, `--form-element-border-width`, `--form-input-border-radius`,
53
- `--form-element-border-bottom-width-underlined`, `--form-element-outline-width(-focus)`,
54
- `--form-element-outline-offset-focus` — all generic form-geometry tokens shared across every
55
- `05.forms` component, declared in `setup/04.elements/forms/*.css`. Not component-local, so
56
- overriding one affects every form element, not just `InputTextCore`.
57
- - `--theme-input-text-color-normal`, `--theme-input-placeholder`,
58
- `--theme-input-placeholder-font-size` — still read directly from the global theme tokens, no
59
- local indirection yet. Follow the same pattern above (`--_input-text-*`) if a future consumer
60
- needs a local override for these.
46
+ ## Per-instance overrides
47
+
48
+ ```vue
49
+ <InputTextCore id="email" name="email" style="--input-text-border-focus: var(--gold-04);" />
50
+ ```
@@ -127,19 +127,25 @@ onMounted(() => {
127
127
  <style lang="css">
128
128
  @layer components {
129
129
  .input-text-wrapper {
130
- /* Local overrides, one indirection step below the global --theme-* tokens they default from
131
- — a consumer can target `.input-text-wrapper { --_input-text-border: ...; }` directly for
132
- a guaranteed-specific override, instead of relying solely on redefining the global token.
133
- See CONSUMER-STYLING.md. */
134
- --_input-text-surface: var(--theme-input-surface);
135
- --_input-text-surface-hover: var(--theme-input-surface-hover);
136
- --_input-text-border: var(--theme-border);
137
- --_input-text-border-focus: var(--theme-border-focus);
130
+ /* Public --input-text-* tokens, inline-fallback to the shared --theme-* tokens (see
131
+ theming-component-token-pattern.md) — overriding one here doesn't touch every other themed
132
+ input/control that also reads --theme-input-surface/--theme-border. --_surface etc. are
133
+ private locals purely so the rest of this rule can reuse the resolved value without
134
+ repeating the fallback chain at every property — they are not themselves an override point,
135
+ see CONSUMER-STYLING.md. */
136
+ --_surface: var(--input-text-surface, var(--theme-input-surface));
137
+ --_surface-hover: var(--input-text-surface-hover, var(--theme-input-surface-hover));
138
+ --_border: var(--input-text-border, var(--theme-border));
139
+ /* Split so hover (mouse) and :focus-visible (keyboard/assistive) can diverge — both default
140
+ to the same --theme-border-focus today, identical appearance to before this token existed,
141
+ but each now has its own override point. */
142
+ --_border-hover: var(--input-text-border-hover, var(--theme-border-focus));
143
+ --_border-focus: var(--input-text-border-focus, var(--theme-border-focus));
138
144
 
139
145
  display: flex;
140
146
  align-items: center;
141
147
  gap: var(--input-icon-slot-gap);
142
- background-color: var(--_input-text-surface);
148
+ background-color: var(--_surface);
143
149
  overflow: hidden;
144
150
  transition: all var(--theme-form-transition-duration) ease-in-out;
145
151
 
@@ -147,25 +153,25 @@ onMounted(() => {
147
153
  position: relative;
148
154
 
149
155
  &.normal {
150
- border: var(--form-element-border-width) solid var(--_input-text-border);
156
+ border: var(--form-element-border-width) solid var(--_border);
151
157
  border-radius: var(--form-input-border-radius);
152
158
  outline: var(--form-element-outline-width) solid transparent;
153
159
 
154
160
  padding-inline: var(--input-padding-inline);
155
161
 
156
162
  &:has(input:is(:hover), button:is(:hover)) {
157
- outline: var(--form-element-outline-width-focus) solid var(--_input-text-border-focus);
163
+ outline: var(--form-element-outline-width-focus) solid var(--_border-hover);
158
164
  outline-offset: var(--form-element-outline-offset-focus);
159
165
  }
160
166
 
161
167
  &:has(input:focus-visible, button:focus-visible) {
162
- outline: var(--form-element-outline-width-focus) solid var(--_input-text-border-focus);
168
+ outline: var(--form-element-outline-width-focus) solid var(--_border-focus);
163
169
  outline-offset: var(--form-element-outline-offset-focus);
164
170
  }
165
171
  }
166
172
 
167
173
  &.underlined {
168
- border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--_input-text-border);
174
+ border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--_border);
169
175
  padding-inline: var(--input-padding-inline);
170
176
  }
171
177
 
@@ -180,17 +186,28 @@ onMounted(() => {
180
186
  box-sizing: content-box;
181
187
 
182
188
  .input-button-core {
183
- background-color: var(--_input-text-surface);
189
+ /* variant="inline" ships with zero border/colour styling by design (see
190
+ component-inline-action-button.md) — the parent supplies all of it. This rule was
191
+ always missing the border/colour half of that contract, only ever setting
192
+ background-color; harmless before InputButtonCore's flat-button redesign, which
193
+ removed the unscoped base rule that used to accidentally give every variant a real
194
+ border regardless. Without its own border/color/outline reset, this button now falls
195
+ through to the browser's UA default <button> chrome (a stark outset border and
196
+ black/white text) instead of sitting flush with the input. */
197
+ background-color: var(--_surface);
198
+ color: var(--input-text-color, var(--theme-input-text-color-normal));
199
+ border: none;
200
+ outline: none;
184
201
  aspect-ratio: 1;
185
202
  border-radius: 0;
186
203
  width: var(--input-min-height);
187
204
 
188
205
  &:hover {
189
- background-color: var(--_input-text-surface-hover);
206
+ background-color: var(--_surface-hover);
190
207
  }
191
208
 
192
209
  &:is(:focus-visible) {
193
- outline: var(--form-element-outline-width-focus) solid var(--_input-text-border-focus);
210
+ outline: var(--form-element-outline-width-focus) solid var(--_border-focus);
194
211
  outline-offset: -4px;
195
212
  }
196
213
 
@@ -207,7 +224,7 @@ onMounted(() => {
207
224
  touch-action: manipulation;
208
225
  flex-grow: 1;
209
226
 
210
- color: var(--theme-input-text-color-normal);
227
+ color: var(--input-text-color, var(--theme-input-text-color-normal));
211
228
  font-family: var(--font-family);
212
229
  font-size: var(--input-font-size);
213
230
 
@@ -219,7 +236,7 @@ onMounted(() => {
219
236
  min-height: var(--input-min-height);
220
237
 
221
238
  &::placeholder {
222
- color: var(--theme-input-placeholder);
239
+ color: var(--input-text-placeholder-color, var(--theme-input-placeholder));
223
240
  font-size: var(--theme-input-placeholder-font-size);
224
241
  font-style: italic;
225
242
  line-height: 1;
@@ -0,0 +1,184 @@
1
+ import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
+ import { reactive, ref } from "vue";
3
+ import InputTextWithLabel from "../../input-text/variants/InputTextWithLabel.vue";
4
+ import InputSelectWithLabel from "../../input-select/variants/InputSelectWithLabel.vue";
5
+ import InputTextareaWithLabel from "../../input-textarea/variants/InputTextareaWithLabel.vue";
6
+ import MultipleCheckboxes from "../../input-checkbox/MultipleCheckboxes.vue";
7
+ import InputButtonCore from "../../input-button/InputButtonCore.vue";
8
+ import FormField from "../../form-field/FormField.vue";
9
+ import HeroText from "../../../01.atoms/text-blocks/hero-text/HeroText.vue";
10
+ import type { IFormMultipleOptions, InputUiVariant } from "~/types/forms/types.forms.d";
11
+
12
+ interface ContactFormStoryArgs {
13
+ inputVariant: InputUiVariant;
14
+ }
15
+
16
+ // Demo composition, not a component of its own — a richer variant of the pattern the real
17
+ // luxury-locs-by-natasha-nuxt3 /contact page uses (see that repo's app/pages/contact.vue): same
18
+ // name/phone/email + multi-select services + comments shape, plus an InputSelectWithLabel
19
+ // "How did you hear about us?" field that page doesn't have, to also exercise the select
20
+ // component alongside the others in one composed form. No validation wiring
21
+ // (useZodValidation/zod) here — that's the consuming app's concern, this is a visual/composition
22
+ // reference only.
23
+ export default {
24
+ title: "Patterns/Contact Form",
25
+ argTypes: {
26
+ inputVariant: {
27
+ control: { type: "select" },
28
+ options: ["normal", "outlined", "underlined"],
29
+ description: "Applied to every field in the form at once",
30
+ table: { category: "Styling" },
31
+ },
32
+ },
33
+ args: {
34
+ inputVariant: "normal",
35
+ },
36
+ } as Meta<ContactFormStoryArgs>;
37
+
38
+ const Template: StoryFn<ContactFormStoryArgs> = (args) => ({
39
+ components: {
40
+ InputTextWithLabel,
41
+ InputSelectWithLabel,
42
+ InputTextareaWithLabel,
43
+ MultipleCheckboxes,
44
+ InputButtonCore,
45
+ FormField,
46
+ HeroText,
47
+ },
48
+ setup() {
49
+ const state = reactive({
50
+ fullName: "",
51
+ telNumber: "",
52
+ emailAddress: "",
53
+ hearAboutUs: "",
54
+ services: [] as string[],
55
+ comments: "",
56
+ });
57
+
58
+ const hearAboutUsOptions = ref<IFormMultipleOptions>({
59
+ data: [
60
+ { id: "search", name: "hearAboutUs", value: "search", label: "Search engine" },
61
+ { id: "social", name: "hearAboutUs", value: "social", label: "Social media" },
62
+ { id: "referral", name: "hearAboutUs", value: "referral", label: "Friend referral" },
63
+ { id: "other", name: "hearAboutUs", value: "other", label: "Other" },
64
+ ],
65
+ total: 4,
66
+ skip: 0,
67
+ limit: 10,
68
+ });
69
+
70
+ const servicesOptions = ref<IFormMultipleOptions>({
71
+ data: [
72
+ { id: "balayage", name: "services", value: "balayage", label: "Balayage" },
73
+ { id: "highlights", name: "services", value: "highlights", label: "Highlights" },
74
+ { id: "full-colour", name: "services", value: "full-colour", label: "Full Colour" },
75
+ { id: "cut-style", name: "services", value: "cut-style", label: "Cut & Style" },
76
+ ],
77
+ total: 4,
78
+ skip: 0,
79
+ limit: 10,
80
+ });
81
+
82
+ return { args, state, hearAboutUsOptions, servicesOptions };
83
+ },
84
+ template: `
85
+ <div style="margin: 36px; max-width: 480px;">
86
+ <HeroText
87
+ tag="h2"
88
+ font-size="heading"
89
+ :text-content="[{ text: 'Get in touch', styleClass: 'normal' }]"
90
+ :style-class-passthrough="['mbe-20']"
91
+ />
92
+
93
+ <form @submit.prevent>
94
+ <FormField width="wide" :has-gutter="false">
95
+ <InputTextWithLabel
96
+ v-model="state.fullName"
97
+ type="text"
98
+ name="fullName"
99
+ label="Full name"
100
+ placeholder="eg. Jane Smith"
101
+ error-message=""
102
+ :required="true"
103
+ :input-variant="args.inputVariant"
104
+ />
105
+ </FormField>
106
+
107
+ <FormField width="wide" :has-gutter="false">
108
+ <InputTextWithLabel
109
+ v-model="state.telNumber"
110
+ type="tel"
111
+ inputmode="tel"
112
+ name="telNumber"
113
+ label="Telephone number"
114
+ placeholder="eg. 07700 900000"
115
+ error-message=""
116
+ :required="true"
117
+ :input-variant="args.inputVariant"
118
+ />
119
+ </FormField>
120
+
121
+ <FormField width="wide" :has-gutter="false">
122
+ <InputTextWithLabel
123
+ v-model="state.emailAddress"
124
+ type="email"
125
+ inputmode="email"
126
+ name="emailAddress"
127
+ label="Email address"
128
+ placeholder="eg. name@domain.com"
129
+ error-message=""
130
+ :required="true"
131
+ :input-variant="args.inputVariant"
132
+ />
133
+ </FormField>
134
+
135
+ <FormField width="wide" :has-gutter="false">
136
+ <InputSelectWithLabel
137
+ v-model="state.hearAboutUs"
138
+ v-model:field-data="hearAboutUsOptions"
139
+ name="hearAboutUs"
140
+ label="How did you hear about us?"
141
+ placeholder="Select an option"
142
+ error-message=""
143
+ :input-variant="args.inputVariant"
144
+ />
145
+ </FormField>
146
+
147
+ <FormField width="wide" :has-gutter="false">
148
+ <MultipleCheckboxes
149
+ v-model="state.services"
150
+ v-model:field-data="servicesOptions"
151
+ name="services"
152
+ legend="Services of interest"
153
+ label="Select all that apply"
154
+ error-message=""
155
+ options-layout="inline"
156
+ :is-button="true"
157
+ :input-variant="args.inputVariant"
158
+ >
159
+ <template #descriptionText>Choose one or more services you're interested in.</template>
160
+ </MultipleCheckboxes>
161
+ </FormField>
162
+
163
+ <FormField width="wide" :has-gutter="false">
164
+ <InputTextareaWithLabel
165
+ v-model="state.comments"
166
+ name="comments"
167
+ label="Comments"
168
+ placeholder="Tell us a bit more about what you're looking for…"
169
+ error-message=""
170
+ :input-variant="args.inputVariant"
171
+ >
172
+ <template #descriptionText>Optional — any extra information that might help us.</template>
173
+ </InputTextareaWithLabel>
174
+ </FormField>
175
+
176
+ <FormField width="wide" :has-gutter="false">
177
+ <InputButtonCore type="submit" variant="primary" button-text="Send message" />
178
+ </FormField>
179
+ </form>
180
+ </div>
181
+ `,
182
+ });
183
+
184
+ export const Default = Template.bind({});
@@ -0,0 +1,125 @@
1
+ import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
+ import { reactive } from "vue";
3
+ import InputTextWithLabel from "../../input-text/variants/InputTextWithLabel.vue";
4
+ import InputPasswordWithLabel from "../../input-text/variants/InputPasswordWithLabel.vue";
5
+ import InputCheckboxRadioWithLabel from "../../input-checkbox-radio/InputCheckboxRadioWithLabel.vue";
6
+ import InputButtonCore from "../../input-button/InputButtonCore.vue";
7
+ import FormField from "../../form-field/FormField.vue";
8
+ import GlassPanel from "../../../01.atoms/glass-panel/GlassPanel.vue";
9
+ import HeroText from "../../../01.atoms/text-blocks/hero-text/HeroText.vue";
10
+ import type { InputUiVariant } from "~/types/forms/types.forms.d";
11
+
12
+ interface LoginFormStoryArgs {
13
+ inputVariant: InputUiVariant;
14
+ }
15
+
16
+ // Demo composition, not a component of its own — exercises the real-world "login form" pattern
17
+ // (labelled text/password inputs, a single checkbox, primary + tertiary buttons) using the
18
+ // existing WithLabel wrapper components rather than the bare *Core components other stories use,
19
+ // since that's what a consuming app actually reaches for. No validation wiring (useZodValidation)
20
+ // here — that's the consuming app's concern, this is a visual/composition reference only.
21
+ export default {
22
+ title: "Patterns/Login Form",
23
+ argTypes: {
24
+ inputVariant: {
25
+ control: { type: "select" },
26
+ options: ["normal", "outlined", "underlined"],
27
+ description: "Applied to every field in the form at once",
28
+ table: { category: "Styling" },
29
+ },
30
+ },
31
+ args: {
32
+ inputVariant: "normal",
33
+ },
34
+ } as Meta<LoginFormStoryArgs>;
35
+
36
+ const Template: StoryFn<LoginFormStoryArgs> = (args) => ({
37
+ components: {
38
+ InputTextWithLabel,
39
+ InputPasswordWithLabel,
40
+ InputCheckboxRadioWithLabel,
41
+ InputButtonCore,
42
+ FormField,
43
+ GlassPanel,
44
+ HeroText,
45
+ },
46
+ setup() {
47
+ const state = reactive({
48
+ email: "",
49
+ password: "",
50
+ rememberMe: false,
51
+ });
52
+ return { args, state };
53
+ },
54
+ template: `
55
+ <div style="margin: 36px; max-width: 420px;">
56
+ <GlassPanel :style-class-passthrough="['login-panel']">
57
+ <div style="padding: 3.2rem;">
58
+ <HeroText
59
+ tag="h2"
60
+ font-size="heading"
61
+ :text-content="[{ text: 'Sign in', styleClass: 'normal' }]"
62
+ :style-class-passthrough="['mbe-20']"
63
+ />
64
+
65
+ <form @submit.prevent>
66
+ <FormField width="wide" :has-gutter="false">
67
+ <InputTextWithLabel
68
+ v-model="state.email"
69
+ type="email"
70
+ inputmode="email"
71
+ name="email"
72
+ label="Email address"
73
+ placeholder="you@example.com"
74
+ error-message=""
75
+ :required="true"
76
+ :input-variant="args.inputVariant"
77
+ />
78
+ </FormField>
79
+
80
+ <FormField width="wide" :has-gutter="false">
81
+ <InputPasswordWithLabel
82
+ v-model="state.password"
83
+ name="password"
84
+ label="Password"
85
+ placeholder="Enter your password"
86
+ error-message=""
87
+ :required="true"
88
+ :input-variant="args.inputVariant"
89
+ />
90
+ </FormField>
91
+
92
+ <FormField width="wide" :has-gutter="false">
93
+ <div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem;">
94
+ <InputCheckboxRadioWithLabel
95
+ v-model="state.rememberMe"
96
+ type="checkbox"
97
+ name="rememberMe"
98
+ label="Remember me"
99
+ :input-variant="args.inputVariant"
100
+ />
101
+ <InputButtonCore type="button" variant="tertiary" button-text="Forgot password?" />
102
+ </div>
103
+ </FormField>
104
+
105
+ <FormField width="wide" :has-gutter="false">
106
+ <!-- InputButtonCore keeps box-sizing: content-box, so width: 100% alone sizes only
107
+ the content box and padding/border overflow past it — box-sizing: border-box
108
+ here makes width: 100% mean the actual rendered width, not a demo-only quirk
109
+ worth fixing in the component itself since content-box is relied on elsewhere
110
+ (e.g. icon-only's aspect-ratio: 1). -->
111
+ <InputButtonCore
112
+ type="submit"
113
+ variant="primary"
114
+ button-text="Sign in"
115
+ style="width: 100%; box-sizing: border-box;"
116
+ />
117
+ </FormField>
118
+ </form>
119
+ </div>
120
+ </GlassPanel>
121
+ </div>
122
+ `,
123
+ });
124
+
125
+ export const Default = Template.bind({});
@@ -0,0 +1,49 @@
1
+ # ToggleSwitchCore — Consumer Styling Guide
2
+
3
+ ## Public token API
4
+
5
+ Every colour token below is `var(--toggle-switch-*, {default})` — set the `--toggle-switch-*`
6
+ name to override just this component; leave it unset and it inherits the shared `--theme-*` token
7
+ every other themed component also falls back to. See `theming-component-token-pattern.md` for why
8
+ this two-tier shape exists. Defaults are unchanged from before this migration — this is a
9
+ token-rename pass, not a redesign.
10
+
11
+ | Token | Falls back to | Controls |
12
+ |---|---|---|
13
+ | `--toggle-switch-surface` | `var(--theme-checkbox-symbol-surface)` | Track background (resting) |
14
+ | `--toggle-switch-surface-hover` | `var(--theme-surface-subtle)` | Track background on hover |
15
+ | `--toggle-switch-border` | `var(--theme-border)` | Track border colour |
16
+ | `--toggle-switch-border-focus` | `var(--theme-border-focus)` | Track outline colour on `:focus-visible` |
17
+ | `--toggle-switch-symbol-border` | `var(--theme-text)` | Thumb border colour |
18
+ | `--toggle-switch-symbol-surface-off` | `var(--theme-on-surface)` | Thumb background, unchecked |
19
+ | `--toggle-switch-symbol-surface-on` | `var(--theme-surface)` | Thumb background, checked |
20
+ | `--toggle-switch-icon-on-color` | `var(--theme-on-surface)` | "On" icon colour |
21
+ | `--toggle-switch-icon-off-color` | `var(--theme-surface)` | "Off" icon colour |
22
+
23
+ Geometry (`--input-toggle-*`, `--form-element-*`) is shared across every `05.forms` component and
24
+ untouched by this — see `theming-form-geometry-tokens.md`.
25
+
26
+ > Five private locals (`--_icon-on-opacity`, `--_icon-off-opacity`, `--_symbol-background-color`,
27
+ > `--_symbol-margin-inline-start`, `--_symbol-checked-offset`) were removed during this migration —
28
+ > declared but never actually read by any property in the component, confirmed by grep. Not part
29
+ > of any public API, so nothing to update if you were relying on them (you weren't — they had zero
30
+ > effect).
31
+
32
+ ---
33
+
34
+ ## Global theming — app-level CSS file
35
+
36
+ ```css
37
+ :where(html) {
38
+ --toggle-switch-symbol-surface-on: var(--rose-05);
39
+ --toggle-switch-border-focus: var(--rose-03);
40
+ }
41
+ ```
42
+
43
+ ---
44
+
45
+ ## Per-instance overrides
46
+
47
+ ```vue
48
+ <ToggleSwitchCore id="notifications" name="notifications" style="--toggle-switch-symbol-surface-on: var(--gold-06);" />
49
+ ```