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,68 @@
1
+ # InputCheckboxRadioCore / InputCheckboxRadioButton — Consumer Styling Guide
2
+
3
+ ## Public token API
4
+
5
+ Every colour token below is `var(--input-checkbox-*, {default})` — set the `--input-checkbox-*`
6
+ name to override just this component; leave it unset and it inherits the shared `--theme-*` slot
7
+ (or `--theme-checkbox-symbol-surface`/`--theme-input-surface` family) every other themed component
8
+ also falls back to. See `theming-component-token-pattern.md` for why this two-tier shape exists.
9
+
10
+ Defaults are unchanged from before this migration — this is a token-rename pass (giving each
11
+ property its own override point), not a redesign, unlike `InputButtonCore`'s flat/darker-hover
12
+ treatment. Geometry (`--input-checked-element-size`, `--form-element-*`, etc.) is separate and
13
+ untouched — see `theming-form-geometry-tokens.md`.
14
+
15
+ ### The control itself (`InputCheckboxRadioCore` — the square/circle)
16
+
17
+ | Token | Falls back to | Controls |
18
+ |---|---|---|
19
+ | `--input-checkbox-surface` | `var(--theme-checkbox-symbol-surface)` | Background |
20
+ | `--input-checkbox-border` | `var(--theme-border)` | Border colour |
21
+ | `--input-checkbox-border-focus` | `var(--theme-border-focus)` | Outline colour on `:focus-visible` (non-button style only — see note) |
22
+ | `--input-checkbox-icon-color` | `var(--theme-text)` | Checked-state icon colour |
23
+
24
+ > The `:focus-visible` outline only applies to the plain checkbox/radio style
25
+ > (`:not(.button)`) — `InputCheckboxRadioButton`'s pill/button style has its own focus treatment,
26
+ > below, via `:has(.input-checkbox-radio-core:focus-visible)` on the outer label.
27
+
28
+ ### The pill/button style (`InputCheckboxRadioButton` — e.g. a "Services of interest" option list)
29
+
30
+ | Token | Falls back to | Controls |
31
+ |---|---|---|
32
+ | `--input-checkbox-button-surface` | `var(--theme-input-surface)` | Background (resting) |
33
+ | `--input-checkbox-button-border` | `var(--theme-border)` | Border colour (resting and hover — unchanged between the two by default) |
34
+ | `--input-checkbox-button-surface-hover` | `var(--theme-input-surface-hover)` | Background on hover |
35
+ | `--input-checkbox-button-ring-hover` | `var(--theme-ring)` | Outline colour on hover |
36
+ | `--input-checkbox-button-surface-focus` | `var(--theme-surface-subtle)` | Background when the inner control has `:focus-visible` |
37
+ | `--input-checkbox-button-ring-focus` | `var(--theme-border-focus)` | Outline colour when the inner control has `:focus-visible` |
38
+ | `--input-checkbox-button-label-color` | `var(--colour-text-default)` | Option label text colour |
39
+ | `--input-checkbox-button-icon-color` | `var(--theme-text)` | Trailing decorator icon colour (the `+` in the corner) |
40
+
41
+ ---
42
+
43
+ ## Global theming — app-level CSS file
44
+
45
+ ```css
46
+ :where(html) {
47
+ --input-checkbox-button-surface: var(--rose-09);
48
+ --input-checkbox-button-surface-hover: var(--rose-08);
49
+ --input-checkbox-button-ring-focus: var(--rose-04);
50
+ }
51
+ ```
52
+
53
+ Only declare the tokens you want to change — everything else keeps inheriting the shared
54
+ `--theme-*`/`--theme-input-*` tokens.
55
+
56
+ ---
57
+
58
+ ## Per-instance overrides
59
+
60
+ ```vue
61
+ <InputCheckboxRadioButton
62
+ id="balayage"
63
+ type="checkbox"
64
+ name="services"
65
+ label="Balayage"
66
+ style="--input-checkbox-button-surface-hover: var(--gold-08);"
67
+ />
68
+ ```
@@ -100,23 +100,28 @@ const flexDirection = ref(props.direction);
100
100
  &.inline {
101
101
  --_white-space: nowrap;
102
102
  }
103
- background-color: var(--theme-input-surface);
104
- border: var(--form-element-border-width) solid var(--theme-border);
103
+
104
+ /* Public --input-checkbox-button-* tokens, inline-fallback to the shared theme tokens (see
105
+ theming-component-token-pattern.md) — overriding one here doesn't touch every other themed
106
+ input/control that also reads --theme-input-surface/--theme-border, etc. Defaults are
107
+ unchanged from before this migration — this is a token-rename pass, not a redesign. */
108
+ background-color: var(--input-checkbox-button-surface, var(--theme-input-surface));
109
+ border: var(--form-element-border-width) solid var(--input-checkbox-button-border, var(--theme-border));
105
110
  outline: var(--form-element-outline-width) solid transparent;
106
111
 
107
112
  box-shadow: 0.1rem 0.1rem 0.8rem 0.1rem transparent;
108
113
 
109
114
  &:hover {
110
- background-color: var(--theme-input-surface-hover);
111
- border-color: var(--theme-border);
112
- outline-color: var(--theme-ring);
115
+ background-color: var(--input-checkbox-button-surface-hover, var(--theme-input-surface-hover));
116
+ border-color: var(--input-checkbox-button-border, var(--theme-border));
117
+ outline-color: var(--input-checkbox-button-ring-hover, var(--theme-ring));
113
118
  outline-offset: var(--form-element-outline-offset-focus);
114
119
  cursor: pointer;
115
120
  }
116
121
 
117
122
  &:has(.input-checkbox-radio-core:focus-visible) {
118
- background-color: var(--theme-surface-subtle);
119
- outline-color: var(--theme-border-focus);
123
+ background-color: var(--input-checkbox-button-surface-focus, var(--theme-surface-subtle));
124
+ outline-color: var(--input-checkbox-button-ring-focus, var(--theme-border-focus));
120
125
  outline-offset: var(--form-element-outline-offset-focus);
121
126
  }
122
127
 
@@ -127,12 +132,17 @@ const flexDirection = ref(props.direction);
127
132
  .input-checkbox-radio-options-button-label {
128
133
  display: flex;
129
134
  flex-grow: 1;
130
- color: var(--colour-text-default);
135
+ color: var(--input-checkbox-button-label-color, var(--colour-text-default));
131
136
  font-size: var(--input-font-size);
132
137
  width: 100%;
133
138
  min-height: var(--input-element-line-height);
134
139
  align-items: center;
135
140
  justify-content: center;
141
+ /* justify-content only centers the label as a single block — once text actually wraps
142
+ (e.g. "Half Head Highlights"), that block fills the full available width and each
143
+ individual line falls back to its default left-alignment inside it. text-align is what
144
+ centers wrapped lines relative to each other. */
145
+ text-align: center;
136
146
  padding-block: 0.8rem;
137
147
  padding-inline: 0.8rem;
138
148
  white-space: var(--_white-space);
@@ -146,7 +156,7 @@ const flexDirection = ref(props.direction);
146
156
  display: flex;
147
157
  align-items: center;
148
158
  justify-content: center;
149
- color: var(--theme-text);
159
+ color: var(--input-checkbox-button-icon-color, var(--theme-text));
150
160
 
151
161
  .icon {
152
162
  height: var(--input-checkbox-decorator-icon-size);
@@ -112,8 +112,11 @@ const inputClasses = computed(() => [
112
112
  grid-template-areas: "element-stack";
113
113
  place-content: center;
114
114
 
115
- background-color: var(--theme-checkbox-symbol-surface);
116
- border: 0.1rem solid var(--theme-border);
115
+ /* Public --input-checkbox-* tokens, inline-fallback to the shared theme tokens (see
116
+ theming-component-token-pattern.md) — overriding one here doesn't touch every other
117
+ themed input/control that also reads --theme-checkbox-symbol-surface/--theme-border. */
118
+ background-color: var(--input-checkbox-surface, var(--theme-checkbox-symbol-surface));
119
+ border: 0.1rem solid var(--input-checkbox-border, var(--theme-border));
117
120
  outline: var(--form-element-outline-width) solid transparent;
118
121
 
119
122
  height: var(--input-checked-element-size);
@@ -139,7 +142,7 @@ const inputClasses = computed(() => [
139
142
 
140
143
  &:not(:is(.button)) {
141
144
  &:has(input:focus-visible) {
142
- outline: var(--form-element-outline-width-focus) solid var(--theme-border-focus);
145
+ outline: var(--form-element-outline-width-focus) solid var(--input-checkbox-border-focus, var(--theme-border-focus));
143
146
  outline-offset: var(--form-element-outline-offset-focus);
144
147
  }
145
148
  }
@@ -150,7 +153,7 @@ const inputClasses = computed(() => [
150
153
 
151
154
  .input-checked-icon-checked,
152
155
  .icon {
153
- color: var(--theme-text);
156
+ color: var(--input-checkbox-icon-color, var(--theme-text));
154
157
  }
155
158
  }
156
159
  }
@@ -164,7 +167,7 @@ const inputClasses = computed(() => [
164
167
 
165
168
  .input-checked-icon-checked,
166
169
  .icon {
167
- color: var(--theme-text);
170
+ color: var(--input-checkbox-icon-color, var(--theme-text));
168
171
  height: var(--input-checked-icon-size);
169
172
  width: var(--input-checked-icon-size);
170
173
  box-shadow: var(--_box-shadow);
@@ -0,0 +1,93 @@
1
+ import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
+ import { ref } from "vue";
3
+ import StorybookComponent from "../InputCheckboxRadioButton.vue";
4
+
5
+ interface InputCheckboxRadioButtonStoryArgs {
6
+ type: "checkbox" | "radio";
7
+ label: string;
8
+ isPill: boolean;
9
+ fieldHasError: boolean;
10
+ displayAsDisc: boolean;
11
+ }
12
+
13
+ export default {
14
+ title: "Components/Forms/Input Checkbox Radio/InputCheckboxRadioButton",
15
+ component: StorybookComponent,
16
+ argTypes: {
17
+ type: {
18
+ control: { type: "select" },
19
+ options: ["checkbox", "radio"],
20
+ table: { category: "Basic" },
21
+ },
22
+ label: { control: "text", table: { category: "Basic" } },
23
+ isPill: { control: "boolean", table: { category: "Styling" } },
24
+ fieldHasError: { control: "boolean", table: { category: "States" } },
25
+ displayAsDisc: { control: "boolean", table: { category: "Styling" } },
26
+ },
27
+ args: {
28
+ type: "checkbox",
29
+ label: "Balayage",
30
+ isPill: false,
31
+ fieldHasError: false,
32
+ displayAsDisc: false,
33
+ },
34
+ } as Meta<InputCheckboxRadioButtonStoryArgs>;
35
+
36
+ const Template: StoryFn<InputCheckboxRadioButtonStoryArgs> = (args) => ({
37
+ components: { StorybookComponent },
38
+ setup() {
39
+ const modelValue = ref(false);
40
+ return { args, modelValue };
41
+ },
42
+ template: `
43
+ <div style="margin: 36px; max-width: 260px;">
44
+ <StorybookComponent
45
+ id="single-option"
46
+ name="single"
47
+ :type="args.type"
48
+ :label="args.label"
49
+ :is-pill="args.isPill"
50
+ :field-has-error="args.fieldHasError"
51
+ :display-as-disc="args.displayAsDisc"
52
+ v-model="modelValue"
53
+ />
54
+ </div>
55
+ `,
56
+ });
57
+
58
+ export const Default = Template.bind({});
59
+
60
+ // "Services of interest" pattern — the actual multi-option checkbox-pill layout this component
61
+ // is normally used in (as opposed to Default's single isolated control).
62
+ const OptionGroupTemplate: StoryFn = () => ({
63
+ components: { StorybookComponent },
64
+ setup() {
65
+ const services = [
66
+ "Balayage",
67
+ "Highlights",
68
+ "Half Head Highlights",
69
+ "Full Colour",
70
+ "Lowlights",
71
+ "Toner & Gloss",
72
+ ];
73
+ const selected = ref<string[]>([]);
74
+ return { services, selected };
75
+ },
76
+ template: `
77
+ <div style="margin: 36px; max-width: 480px; display: grid; grid-template-columns: 1fr 1fr; gap: 0.8rem;">
78
+ <StorybookComponent
79
+ v-for="service in services"
80
+ :key="service"
81
+ :id="'service-' + service"
82
+ name="services"
83
+ type="checkbox"
84
+ :label="service"
85
+ :true-value="service"
86
+ :multiple-options="true"
87
+ v-model="selected"
88
+ />
89
+ </div>
90
+ `,
91
+ });
92
+
93
+ export const OptionGroup = OptionGroupTemplate.bind({});
@@ -0,0 +1,75 @@
1
+ import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
+ import { ref } from "vue";
3
+ import StorybookComponent from "../InputCheckboxRadioCore.vue";
4
+ import type { FormUiTheme } from "~/types/forms/types.forms.d";
5
+
6
+ interface InputCheckboxRadioCoreStoryArgs {
7
+ type: "checkbox" | "radio";
8
+ id: string;
9
+ name: string;
10
+ required: boolean;
11
+ theme: FormUiTheme;
12
+ fieldHasError: boolean;
13
+ displayAsDisc: boolean;
14
+ }
15
+
16
+ export default {
17
+ title: "Components/Forms/Input Checkbox Radio/InputCheckboxRadioCore",
18
+ component: StorybookComponent,
19
+ argTypes: {
20
+ type: {
21
+ control: { type: "select" },
22
+ options: ["checkbox", "radio"],
23
+ description: "Control type",
24
+ table: { category: "Basic" },
25
+ },
26
+ id: { control: "text", table: { category: "Basic" } },
27
+ name: { control: "text", table: { category: "Basic" } },
28
+ required: { control: "boolean", table: { category: "Basic" } },
29
+ theme: {
30
+ control: { type: "select" },
31
+ options: ["default", "success", "error", "warning"],
32
+ table: { category: "Styling" },
33
+ },
34
+ fieldHasError: { control: "boolean", table: { category: "States" } },
35
+ displayAsDisc: {
36
+ control: "boolean",
37
+ description: "Checkbox only — render as a filled disc instead of a check icon",
38
+ table: { category: "Styling" },
39
+ },
40
+ },
41
+ args: {
42
+ type: "checkbox",
43
+ id: "core-checkbox",
44
+ name: "coreCheckbox",
45
+ required: false,
46
+ theme: "default",
47
+ fieldHasError: false,
48
+ displayAsDisc: false,
49
+ },
50
+ } as Meta<InputCheckboxRadioCoreStoryArgs>;
51
+
52
+ const Template: StoryFn<InputCheckboxRadioCoreStoryArgs> = (args) => ({
53
+ components: { StorybookComponent },
54
+ setup() {
55
+ const modelValue = ref(false);
56
+ return { args, modelValue };
57
+ },
58
+ template: `
59
+ <div style="margin: 36px;">
60
+ <StorybookComponent v-bind="args" v-model="modelValue" />
61
+ </div>
62
+ `,
63
+ });
64
+
65
+ export const Checkbox = Template.bind({});
66
+ Checkbox.args = { type: "checkbox" };
67
+
68
+ export const Radio = Template.bind({});
69
+ Radio.args = { type: "radio" };
70
+
71
+ export const CheckboxAsDisc = Template.bind({});
72
+ CheckboxAsDisc.args = { type: "checkbox", displayAsDisc: true };
73
+
74
+ export const ErrorState = Template.bind({});
75
+ ErrorState.args = { type: "checkbox", fieldHasError: true, theme: "error" };
@@ -0,0 +1,41 @@
1
+ # InputNumberCore — Consumer Styling Guide
2
+
3
+ ## Public token API
4
+
5
+ Every colour token below is `var(--input-number-*, {default})` — set the `--input-number-*` name
6
+ to 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
+ | Token | Falls back to | Controls |
11
+ |---|---|---|
12
+ | `--input-number-surface` | `var(--theme-input-surface)` | Wrapper and input background |
13
+ | `--input-number-border` | `var(--theme-border)` | Border colour |
14
+ | `--input-number-text-color` | `var(--theme-input-text-color-normal)` | Input text colour |
15
+ | `--input-number-divider-color` | `var(--theme-input-surface-hover)` | Divider between the input and an embedded left/right `InputButtonCore` |
16
+
17
+ Geometry (`--form-textarea-padding-*`, `--input-font-size`, `--input-element-line-height`, etc.)
18
+ is shared across every `05.forms` component and untouched by this — see
19
+ `theming-form-geometry-tokens.md`.
20
+
21
+ ---
22
+
23
+ ## Global theming — app-level CSS file
24
+
25
+ ```css
26
+ :where(html) {
27
+ --input-number-surface: var(--rose-09);
28
+ --input-number-border: var(--rose-05);
29
+ }
30
+ ```
31
+
32
+ Only declare the tokens you want to change — everything else keeps inheriting the shared
33
+ `--theme-input-*`/`--theme-*` tokens.
34
+
35
+ ---
36
+
37
+ ## Per-instance overrides
38
+
39
+ ```vue
40
+ <InputNumberCore id="quantity" name="quantity" :min="1" :max="10" style="--input-number-border: var(--gold-04);" />
41
+ ```
@@ -66,14 +66,20 @@ const minLength = computed(() => `${props.max.toString().length + 1}em`);
66
66
  --_focus-box-shadow: var(--box-shadow-off);
67
67
  --_min-width: v-bind(minLength);
68
68
 
69
+ /* Public --input-number-* tokens, inline-fallback to the shared --theme-* tokens (see
70
+ theming-component-token-pattern.md) — overriding one here doesn't touch every other themed
71
+ input/control that also reads --theme-input-surface/--theme-border. */
72
+ --_surface: var(--input-number-surface, var(--theme-input-surface));
73
+ --_border: var(--input-number-border, var(--theme-border));
74
+
69
75
  display: flex;
70
76
  align-items: center;
71
77
 
72
78
  width: fit-content;
73
79
 
74
- background-color: var(--theme-input-surface);
80
+ background-color: var(--_surface);
75
81
  border-radius: var(--form-element-border-width);
76
- border: var(--form-element-border-width) solid var(--theme-border);
82
+ border: var(--form-element-border-width) solid var(--_border);
77
83
  outline: var(--form-element-outline-width) solid transparent;
78
84
  box-shadow: var(--_focus-box-shadow);
79
85
 
@@ -106,8 +112,8 @@ const minLength = computed(() => `${props.max.toString().length + 1}em`);
106
112
  outline: none;
107
113
  box-shadow: none;
108
114
 
109
- background-color: var(--theme-input-surface);
110
- color: var(--theme-input-text-color-normal);
115
+ background-color: var(--_surface);
116
+ color: var(--input-number-text-color, var(--theme-input-text-color-normal));
111
117
  font-family: var(--font-family);
112
118
  font-size: var(--input-font-size);
113
119
  line-height: var(--input-element-line-height);
@@ -144,12 +150,12 @@ const minLength = computed(() => `${props.max.toString().length + 1}em`);
144
150
 
145
151
  .left-slot {
146
152
  margin-inline-end: 0;
147
- border-right: 2px solid var(--theme-input-surface-hover);
153
+ border-right: 2px solid var(--input-number-divider-color, var(--theme-input-surface-hover));
148
154
  }
149
155
 
150
156
  .right-slot {
151
157
  margin-inline-end: 0;
152
- border-left: 2px solid var(--theme-input-surface-hover);
158
+ border-left: 2px solid var(--input-number-divider-color, var(--theme-input-surface-hover));
153
159
  }
154
160
  }
155
161
  }
@@ -1,42 +1,48 @@
1
1
  # InputSelectCore — Consumer Styling Guide
2
2
 
3
- ## Two override paths
3
+ ## Public token API
4
+
5
+ Every colour token below is `var(--input-select-*, {default})` — set the `--input-select-*` name
6
+ to 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-select-*` 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-select-surface` | `var(--theme-input-surface)` | Wrapper/select background |
18
+ | `--input-select-surface-hover` | `var(--theme-input-surface-hover)` | Option row background on hover |
19
+ | `--input-select-border` | `var(--theme-border)` | Border colour (`.normal`/`.underlined`/open picker, resting) |
20
+ | `--input-select-border-hover` | `var(--theme-border-focus)` | Outline colour on hover (mouse) |
21
+ | `--input-select-border-focus` | `var(--theme-border-focus)` | Outline colour on `:focus-visible` (keyboard/assistive) |
22
+
23
+ Geometry (`--input-padding-*`, `--input-min-height`, `--input-select-option-min-height`,
24
+ `--form-element-*`, etc.) is shared across every `05.forms` component and untouched by this — see
25
+ `theming-form-geometry-tokens.md`.
4
26
 
5
- Same public/private split as `InputTextCore` (see that component's `CONSUMER-STYLING.md` for the
6
- full rationale) — `.input-select-wrapper` now exposes local tokens, one indirection step below the
7
- global `--theme-*` ones they default from:
8
-
9
- ```css
10
- --_input-select-surface: var(--theme-input-surface);
11
- --_input-select-surface-hover: var(--theme-input-surface-hover);
12
- --_input-select-border: var(--theme-border);
13
- --_input-select-border-focus: var(--theme-border-focus);
14
- --_input-select-outline-color: var(--_input-select-border-focus);
15
- ```
27
+ ---
16
28
 
17
- - **Global override**: redeclare `--theme-input-surface` / `--theme-border` / `--theme-border-focus`
18
- / `--theme-input-surface-hover` wherever you'd normally set theme tokens.
19
- - **Local override** (this component only, guaranteed to win):
29
+ ## Global theming — app-level CSS file
20
30
 
21
31
  ```css
22
- .input-select-wrapper {
23
- --_input-select-border: var(--slate-06);
24
- --_input-select-border-focus: var(--slate-04);
25
- --_input-select-surface: var(--slate-01);
32
+ :where(html) {
33
+ --input-select-surface: var(--rose-09);
34
+ --input-select-border: var(--rose-05);
35
+ --input-select-border-focus: var(--rose-03);
26
36
  }
27
37
  ```
28
38
 
29
- ## Note on `--_input-select-outline-color`
30
-
31
- Before 2026-08-22 this was referenced (on the open-picker outline) but never declared anywhere —
32
- a dangling private token that silently resolved to the property's initial value (same failure mode
33
- as the `--theme-button-surface` typo documented in the layer's `CLAUDE.md`, pitfall list). It's now
34
- properly defined, defaulting to `--_input-select-border-focus`.
39
+ Only declare the tokens you want to change — everything else keeps inheriting the shared
40
+ `--theme-input-*`/`--theme-*` tokens.
35
41
 
36
42
  ---
37
43
 
38
- ## Other tokens
44
+ ## Per-instance overrides
39
45
 
40
- Generic form-geometry tokens (`--input-padding-inline`, `--input-min-height`, `--input-font-size`,
41
- `--form-element-border-width`, `--form-input-border-radius`, etc.) are shared across every
42
- `05.forms` component and declared in `setup/04.elements/forms/*.css` — not component-local.
46
+ ```vue
47
+ <InputSelectCore id="country" name="country" style="--input-select-border-focus: var(--gold-04);" />
48
+ ```
@@ -73,17 +73,24 @@ const fieldData = defineModel("fieldData") as Ref<IFormMultipleOptions>;
73
73
  <style lang="css">
74
74
  @layer components {
75
75
  .input-select-wrapper {
76
- /* Local overrides, one indirection step below the global --theme-* tokens they default from —
77
- a consumer can target `.input-select-wrapper { --_input-select-border: ...; }` directly for
78
- a guaranteed-specific override, instead of relying solely on redefining the global token.
79
- See CONSUMER-STYLING.md. */
80
- --_input-select-surface: var(--theme-input-surface);
81
- --_input-select-surface-hover: var(--theme-input-surface-hover);
82
- --_input-select-border: var(--theme-border);
83
- --_input-select-border-focus: var(--theme-border-focus);
84
- --_input-select-outline-color: var(--_input-select-border-focus);
85
-
86
- background-color: var(--_input-select-surface);
76
+ /* Public --input-select-* tokens, inline-fallback to the shared --theme-* tokens (see
77
+ theming-component-token-pattern.md) — overriding one here doesn't touch every other themed
78
+ input/control that also reads --theme-input-surface/--theme-border. --_surface etc. are
79
+ private locals purely so the rest of this rule can reuse the resolved value without
80
+ repeating the fallback chain at every property — they are not themselves an override point,
81
+ see CONSUMER-STYLING.md. --_outline-color stays private/computed: it's always just an alias
82
+ for --_border-focus with no independent meaning of its own. */
83
+ --_surface: var(--input-select-surface, var(--theme-input-surface));
84
+ --_surface-hover: var(--input-select-surface-hover, var(--theme-input-surface-hover));
85
+ --_border: var(--input-select-border, var(--theme-border));
86
+ /* Split so hover (mouse) and :focus-visible (keyboard/assistive) can diverge — both default to
87
+ the same --theme-border-focus today, identical appearance to before this token existed, but
88
+ each now has its own override point. */
89
+ --_border-hover: var(--input-select-border-hover, var(--theme-border-focus));
90
+ --_border-focus: var(--input-select-border-focus, var(--theme-border-focus));
91
+ --_outline-color: var(--_border-focus);
92
+
93
+ background-color: var(--_surface);
87
94
  overflow: hidden;
88
95
 
89
96
  z-index: 2;
@@ -91,18 +98,23 @@ const fieldData = defineModel("fieldData") as Ref<IFormMultipleOptions>;
91
98
  transition: all var(--theme-form-transition-duration) ease-in-out;
92
99
 
93
100
  &.normal {
94
- border: var(--form-element-border-width) solid var(--_input-select-border);
101
+ border: var(--form-element-border-width) solid var(--_border);
95
102
  border-radius: var(--form-input-border-radius);
96
103
  outline: var(--form-element-outline-width) solid transparent;
97
104
  }
98
105
 
99
106
  &.underlined {
100
- border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--_input-select-border);
101
- background-color: var(--_input-select-surface);
107
+ border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--_border);
108
+ background-color: var(--_surface);
102
109
  }
103
110
 
104
- &:has(select:focus-visible, :hover) {
105
- outline: var(--form-element-outline-width-focus) solid var(--_input-select-border-focus);
111
+ &:has(:hover) {
112
+ outline: var(--form-element-outline-width-focus) solid var(--_border-hover);
113
+ outline-offset: var(--form-element-outline-offset-focus);
114
+ }
115
+
116
+ &:has(select:focus-visible) {
117
+ outline: var(--form-element-outline-width-focus) solid var(--_border-focus);
106
118
  outline-offset: var(--form-element-outline-offset-focus);
107
119
  }
108
120
 
@@ -117,7 +129,7 @@ const fieldData = defineModel("fieldData") as Ref<IFormMultipleOptions>;
117
129
  content: '';
118
130
  width: 0.8em;
119
131
  height: 0.5em;
120
- background-color: var(--_input-select-border);
132
+ background-color: var(--_border);
121
133
  clip-path: polygon(100% 0%, 0 0%, 50% 100%);
122
134
  } */
123
135
 
@@ -140,8 +152,8 @@ const fieldData = defineModel("fieldData") as Ref<IFormMultipleOptions>;
140
152
 
141
153
  &:open::picker(select) {
142
154
  opacity: 1;
143
- border: var(--form-element-border-width) solid var(--_input-select-border);
144
- outline: var(--form-element-outline-width) solid var(--_input-select-outline-color);
155
+ border: var(--form-element-border-width) solid var(--_border);
156
+ outline: var(--form-element-outline-width) solid var(--_outline-color);
145
157
 
146
158
  @starting-style {
147
159
  opacity: 0;
@@ -168,7 +180,7 @@ const fieldData = defineModel("fieldData") as Ref<IFormMultipleOptions>;
168
180
  transition: all var(--theme-form-transition-duration) ease-in-out;
169
181
 
170
182
  &:hover {
171
- background-color: var(--_input-select-surface-hover);
183
+ background-color: var(--_surface-hover);
172
184
  }
173
185
 
174
186
  .input-select-core-option-decorator-icon {