srcdev-nuxt-components 9.6.9 → 9.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/consumer/ids-from-useid.md +4 -2
- package/.claude/skills/component-compliance-checklist.md +24 -2
- package/.claude/skills/component-inline-action-button.md +5 -4
- package/.claude/skills/component-naming.md +8 -2
- package/.claude/skills/components/{alert-mask-core.md → alert-mask.md} +38 -23
- package/.claude/skills/components/alert-masked-content.md +3 -3
- package/.claude/skills/components/container-glow.md +1 -1
- package/.claude/skills/components/display-theme-switch.md +8 -8
- package/.claude/skills/components/input-button.md +26 -7
- package/.claude/skills/components/input-checkbox-radio.md +2 -0
- package/.claude/skills/components/input-description.md +5 -0
- package/.claude/skills/components/input-error.md +9 -2
- package/.claude/skills/components/input-label.md +8 -2
- package/.claude/skills/components/input-number.md +2 -0
- package/.claude/skills/components/input-otp.md +2 -0
- package/.claude/skills/components/input-range.md +2 -0
- package/.claude/skills/components/input-select.md +2 -0
- package/.claude/skills/components/input-text.md +2 -0
- package/.claude/skills/components/input-textarea.md +2 -0
- package/.claude/skills/components/pending-effect.md +5 -4
- package/.claude/skills/components/{triple-toggle-switch.md → segmented-control.md} +34 -19
- package/.claude/skills/components/services-card.md +1 -1
- package/.claude/skills/components/tabbed-content.md +1 -1
- package/.claude/skills/components/toggle-switch.md +2 -0
- package/.claude/skills/css-nesting-conventions.md +22 -1
- package/.claude/skills/index.md +5 -5
- package/.claude/skills/storybook-add-story.md +3 -1
- package/.claude/skills/theming-component-token-pattern.md +24 -0
- package/.claude/skills/theming-form-geometry-tokens.md +1 -1
- package/.claude/skills/theming-partial-override.md +1 -1
- package/.vscode/srcdev-component-alert-mask.code-snippets +41 -0
- package/.vscode/{srcdev-component-triple-toggle-switch.code-snippets → srcdev-component-segmented-control.code-snippets} +6 -7
- package/app/assets/styles/setup/03.theming/_theme-slots.css +1 -1
- package/app/assets/styles/setup/04.elements/forms/03.generic-input-geometry.css +4 -0
- package/app/components/01.atoms/alert-mask/AlertMask.vue +194 -0
- package/app/components/01.atoms/alert-mask/CONSUMER-STYLING.md +57 -42
- package/app/components/01.atoms/alert-mask/stories/AlertMask.stories.ts +198 -0
- package/app/components/01.atoms/alert-mask/tests/AlertMask.spec.ts +233 -0
- package/app/components/02.molecules/alert-masked-content/AlertMaskedContent.vue +2 -2
- package/app/components/02.molecules/alert-masked-content/CONSUMER-STYLING.md +4 -4
- package/app/components/02.molecules/alert-masked-content/stories/AlertMaskedContent.stories.ts +60 -0
- package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +12 -12
- package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +3 -3
- package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +1 -1
- package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +1 -1
- package/app/components/02.molecules/display-theme-switch/CONSUMER-STYLING.md +9 -9
- package/app/components/02.molecules/display-theme-switch/DisplayThemeSwitch.vue +16 -16
- package/app/components/02.molecules/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +53 -1
- package/app/components/02.molecules/display-theme-switch/tests/DisplayThemeSwitch.spec.ts +8 -8
- package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +4 -4
- package/app/components/02.molecules/input-copy/InputCopy.vue +2 -2
- package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +2 -2
- package/app/components/03.organisms/google-reviews/tests/GoogleReviews.spec.ts +4 -4
- package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +2 -2
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +2 -2
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +6 -6
- package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +8 -0
- package/app/components/05.forms/form-errors/InputError.vue +43 -37
- package/app/components/05.forms/form-errors/stories/InputError.stories.ts +79 -1
- package/app/components/05.forms/form-errors/tests/InputError.spec.ts +29 -1
- package/app/components/05.forms/input-button/CONSUMER-STYLING.md +80 -47
- package/app/components/05.forms/input-button/InputButton.vue +52 -100
- package/app/components/05.forms/input-button/playwright/input-button.playwright.ts +1 -1
- package/app/components/05.forms/input-button/stories/InputButton.stories.ts +145 -8
- package/app/components/05.forms/input-button/tests/InputButton.spec.ts +76 -46
- package/app/components/05.forms/input-button/tests/__snapshots__/InputButton.spec.ts.snap +17 -17
- package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +9 -1
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadio.vue +8 -0
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +2 -2
- package/app/components/05.forms/input-description/CONSUMER-STYLING.md +14 -2
- package/app/components/05.forms/input-description/InputDescription.vue +2 -1
- package/app/components/05.forms/input-description/stories/InputDescription.stories.ts +70 -4
- package/app/components/05.forms/input-description/tests/InputDescription.spec.ts +2 -1
- package/app/components/05.forms/input-label/CONSUMER-STYLING.md +10 -2
- package/app/components/05.forms/input-label/InputLabel.vue +1 -0
- package/app/components/05.forms/input-label/stories/InputLabel.stories.ts +68 -2
- package/app/components/05.forms/input-label/tests/InputLabel.spec.ts +13 -3
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-number/InputNumber.vue +4 -5
- package/app/components/05.forms/input-otp/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-otp/InputOtp.vue +2 -2
- package/app/components/05.forms/input-range/InputRange.vue +1 -1
- package/app/components/05.forms/input-select/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-select/InputSelect.vue +14 -5
- package/app/components/05.forms/input-text/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-text/InputText.vue +4 -4
- package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-textarea/InputTextarea.vue +2 -3
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +4 -4
- package/app/components/05.forms/pending-effect/CONSUMER-STYLING.md +7 -3
- package/app/components/05.forms/pending-effect/PendingEffect.vue +3 -3
- package/app/components/05.forms/pending-effect/tests/PendingEffect.spec.ts +1 -1
- package/app/components/05.forms/segmented-control/CONSUMER-STYLING.md +125 -0
- package/app/components/05.forms/segmented-control/SegmentedControl.vue +231 -0
- package/app/components/05.forms/segmented-control/stories/SegmentedControl.stories.ts +215 -0
- package/app/components/05.forms/segmented-control/tests/SegmentedControl.spec.ts +202 -0
- package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +7 -0
- package/app/components/05.forms/toggle-switch/ToggleSwitch.vue +2 -0
- package/app/types/components/index.ts +1 -1
- package/package.json +1 -1
- package/.vscode/srcdev-component-alert-mask-core.code-snippets +0 -31
- package/app/components/01.atoms/alert-mask/AlertMaskCore.vue +0 -166
- package/app/components/01.atoms/alert-mask/stories/AlertMaskCore.stories.ts +0 -105
- package/app/components/01.atoms/alert-mask/tests/AlertMaskCore.spec.ts +0 -342
- package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +0 -99
- package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitch.vue +0 -251
- package/app/components/05.forms/triple-toggle-switch/stories/TripleToggleSwitch.stories.ts +0 -104
- package/app/components/05.forms/triple-toggle-switch/tests/TripleToggleSwitch.spec.ts +0 -138
- /package/app/types/components/{alert-mask-core.d.ts → alert-mask.d.ts} +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
-
import { computed } from "vue";
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
3
|
import StorybookComponent from "../InputLabel.vue";
|
|
4
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
4
5
|
import type { InputUiVariant, InputLabelIndicator } from "~/types/forms/types.forms";
|
|
6
|
+
import type { MediaCanvas } from "~/types/components";
|
|
5
7
|
|
|
6
8
|
interface InputLabelStoryArgs {
|
|
7
9
|
id: string;
|
|
@@ -109,8 +111,43 @@ export default {
|
|
|
109
111
|
textLabel: "Email address",
|
|
110
112
|
htmlLabel: "",
|
|
111
113
|
},
|
|
114
|
+
decorators: [
|
|
115
|
+
(story, context) => ({
|
|
116
|
+
components: { story, CanvasSwitcher },
|
|
117
|
+
setup() {
|
|
118
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
119
|
+
return { canvasName };
|
|
120
|
+
},
|
|
121
|
+
template: `
|
|
122
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
123
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
124
|
+
</div>
|
|
125
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
126
|
+
<story />
|
|
127
|
+
</div>
|
|
128
|
+
`,
|
|
129
|
+
}),
|
|
130
|
+
],
|
|
131
|
+
parameters: {
|
|
132
|
+
layout: "fullscreen",
|
|
133
|
+
docs: {
|
|
134
|
+
description: {
|
|
135
|
+
component:
|
|
136
|
+
"The `<label>` every labelled field wrapper renders. Label copy goes in the `textLabel` and `htmlLabel` slots (html first), `id` becomes its `for`, and `indicator` with `required` adds a required or optional marker (falling back to `app.config` `srcdev.inputLabel`). `field-has-error` and `input-variant` are styling hooks only. The control in these stories is a plain input; see [InputTextField](?path=/story/components-forms-input-text-inputtextfield--required) for it inside a real field.",
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
112
140
|
} as Meta<InputLabelStoryArgs>;
|
|
113
141
|
|
|
142
|
+
// Links in the canvas open in the Storybook manager (target="_top"), not inside the preview iframe.
|
|
143
|
+
const storyNoteStyle = "margin: 0 0 1.6rem; font-size: 1.4rem;";
|
|
144
|
+
const fieldNote = `
|
|
145
|
+
<p style="${storyNoteStyle}">
|
|
146
|
+
Every labelled field wrapper renders this label and forwards its own <code>required</code> prop. See it in a real field in the
|
|
147
|
+
<a href="/?path=/story/components-forms-input-text-inputtextfield--required" target="_top">InputTextField</a> story.
|
|
148
|
+
</p>
|
|
149
|
+
`;
|
|
150
|
+
|
|
114
151
|
const Template: StoryFn<InputLabelStoryArgs> = (args) => ({
|
|
115
152
|
components: { StorybookComponent },
|
|
116
153
|
setup() {
|
|
@@ -133,7 +170,8 @@ const Template: StoryFn<InputLabelStoryArgs> = (args) => ({
|
|
|
133
170
|
return { args, componentArgs, tokenStyles };
|
|
134
171
|
},
|
|
135
172
|
template: `
|
|
136
|
-
<div style="max-width: 48rem;
|
|
173
|
+
<div style="max-width: 48rem; font-size: 1.6rem;" :style="tokenStyles">
|
|
174
|
+
${fieldNote}
|
|
137
175
|
<StorybookComponent v-bind="componentArgs">
|
|
138
176
|
<template v-if="args.htmlLabel" #htmlLabel><span v-html="args.htmlLabel"></span></template>
|
|
139
177
|
<template v-if="args.textLabel" #textLabel>{{ args.textLabel }}</template>
|
|
@@ -150,17 +188,26 @@ const Template: StoryFn<InputLabelStoryArgs> = (args) => ({
|
|
|
150
188
|
});
|
|
151
189
|
|
|
152
190
|
export const TextLabel = Template.bind({});
|
|
191
|
+
TextLabel.parameters = {
|
|
192
|
+
docs: { description: { story: "Plain label text through the `textLabel` slot." } },
|
|
193
|
+
};
|
|
153
194
|
|
|
154
195
|
export const HtmlLabel = Template.bind({});
|
|
155
196
|
HtmlLabel.args = {
|
|
156
197
|
textLabel: "",
|
|
157
198
|
htmlLabel: 'Email address <small style="font-weight: normal;">(work or personal)</small>',
|
|
158
199
|
};
|
|
200
|
+
HtmlLabel.parameters = {
|
|
201
|
+
docs: { description: { story: "Rich markup through the `htmlLabel` slot." } },
|
|
202
|
+
};
|
|
159
203
|
|
|
160
204
|
export const TextAndHtml = Template.bind({});
|
|
161
205
|
TextAndHtml.args = {
|
|
162
206
|
htmlLabel: "<strong>Work</strong> ",
|
|
163
207
|
};
|
|
208
|
+
TextAndHtml.parameters = {
|
|
209
|
+
docs: { description: { story: "Both slots filled: `htmlLabel` renders first, then `textLabel`." } },
|
|
210
|
+
};
|
|
164
211
|
|
|
165
212
|
export const MarkRequired = Template.bind({});
|
|
166
213
|
MarkRequired.args = { indicator: "required", required: true };
|
|
@@ -234,3 +281,22 @@ CustomTokens.decorators = [
|
|
|
234
281
|
`,
|
|
235
282
|
}),
|
|
236
283
|
];
|
|
284
|
+
|
|
285
|
+
export const StressTest = Template.bind({});
|
|
286
|
+
StressTest.storyName = "Stress Test (Worst-Case Data)";
|
|
287
|
+
StressTest.args = {
|
|
288
|
+
textLabel:
|
|
289
|
+
"📧 Elektronischepostadressefürdiebenachrichtigungüberbestellungen https://example.com/a/very/long/unbroken/url/for/the/label <b>not bold</b> البريد الإلكتروني",
|
|
290
|
+
indicator: "optional",
|
|
291
|
+
required: false,
|
|
292
|
+
optionalText: "(freiwilligeAngabekeinePflichtfeldbitteleerlassenwennnichtzutreffend)",
|
|
293
|
+
};
|
|
294
|
+
StressTest.parameters = {
|
|
295
|
+
initialCanvas: "mobileCanvas",
|
|
296
|
+
docs: {
|
|
297
|
+
description: {
|
|
298
|
+
story:
|
|
299
|
+
"Long unbroken German, a long URL, emoji, HTML-like and right-to-left text in the label, plus an unbroken optional marker. Check at every canvas width: the label and the marker wrap inside the dashed outline, markup renders as text, and the marker stays after the text in RTL. Switch `indicator` to required and turn `required` on to check the required marker. Label copy arrives through slots, so there is no line-clamp token: clamp your own slot markup if you need to.",
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
};
|
|
@@ -1,14 +1,24 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
2
2
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
3
|
import InputLabel from "../InputLabel.vue";
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
5
|
+
const mounted: { unmount: () => void }[] = [];
|
|
6
|
+
|
|
7
|
+
const createWrapper = async (props = {}, slots = {}) => {
|
|
8
|
+
const wrapper = await mountSuspended(InputLabel, {
|
|
7
9
|
props: { id: "test-input", ...props },
|
|
8
10
|
slots,
|
|
9
11
|
});
|
|
12
|
+
mounted.push(wrapper);
|
|
13
|
+
return wrapper;
|
|
14
|
+
};
|
|
10
15
|
|
|
11
16
|
describe("InputLabel", () => {
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
mounted.splice(0).forEach((wrapper) => wrapper.unmount());
|
|
19
|
+
vi.restoreAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
12
22
|
describe("rendering", () => {
|
|
13
23
|
it("renders a label linked to the control id", async () => {
|
|
14
24
|
const wrapper = await createWrapper({ id: "email-input" });
|
|
@@ -18,7 +18,8 @@ token, same as `InputText`.
|
|
|
18
18
|
| `--input-number-surface` | `var(--theme-input-surface)` | Wrapper, input, and embedded-button background |
|
|
19
19
|
| `--input-number-surface-hover` | `var(--theme-input-surface-hover)` | Embedded left/right button background on hover |
|
|
20
20
|
| `--input-number-border` | `var(--theme-border)` | Border/divider colour (`normal`/`underlined` variant border, and the divider between the input and an embedded button) |
|
|
21
|
-
| `--input-number-border-hover` |
|
|
21
|
+
| `--input-number-border-hover` | the resolved border | 1px wrapper outline colour on mouse hover over the input or either step button (`normal` variant only) |
|
|
22
|
+
| `--input-number-shadow` | `var(--form-element-shadow-colour, var(--colour-theme-2))` | Resting shadow ring outside the border (`normal` only). Width is the global `--form-element-shadow-width` (`0.2rem`; `0` removes it) |
|
|
22
23
|
| `--input-number-border-focus` | `var(--theme-border-focus)` | Wrapper outline colour on `:focus-visible` (`normal` variant), and the embedded button's own `:focus-visible` outline |
|
|
23
24
|
| `--input-number-text-color` | `var(--theme-input-text-color-normal)` | Input text colour |
|
|
24
25
|
| `--input-number-placeholder-color` | `var(--theme-input-placeholder)` | Placeholder text colour |
|
|
@@ -33,6 +34,11 @@ and `step` and capped at `12em`).
|
|
|
33
34
|
|
|
34
35
|
---
|
|
35
36
|
|
|
37
|
+
> **Changed 2026-10-17**: bordered controls carry a light shadow ring (`box-shadow`, theme step 2,
|
|
38
|
+
> so red when invalid), and hover draws a 1px outline in the border colour with no offset, over the
|
|
39
|
+
> ring's inner edge. Hover used to draw a 2px outline at a 2px offset in the focus colour. Focus is
|
|
40
|
+
> unchanged and sits just outside the ring.
|
|
41
|
+
|
|
36
42
|
## State hooks
|
|
37
43
|
|
|
38
44
|
| Attribute | On | When |
|
|
@@ -123,11 +123,10 @@ const inputmode = computed(() => {
|
|
|
123
123
|
border: var(--form-element-border-width) solid var(--_border);
|
|
124
124
|
border-radius: var(--form-input-border-radius);
|
|
125
125
|
outline: var(--form-element-outline-width) solid transparent;
|
|
126
|
+
box-shadow: 0 0 0 var(--form-element-shadow-width) var(--input-number-shadow, var(--form-element-shadow-colour, var(--colour-theme-2)));
|
|
126
127
|
|
|
127
128
|
&:has(input:hover, button:hover) {
|
|
128
|
-
outline: var(--
|
|
129
|
-
var(--input-number-border-hover, var(--theme-border-focus));
|
|
130
|
-
outline-offset: var(--form-element-outline-offset-focus);
|
|
129
|
+
outline-color: var(--input-number-border-hover, var(--_border));
|
|
131
130
|
}
|
|
132
131
|
|
|
133
132
|
&:has(input:focus-visible, button:focus-visible) {
|
|
@@ -167,8 +166,8 @@ const inputmode = computed(() => {
|
|
|
167
166
|
outline-offset: -4px;
|
|
168
167
|
}
|
|
169
168
|
|
|
170
|
-
|
|
171
|
-
.
|
|
169
|
+
&[data-icon-only] {
|
|
170
|
+
& .input-button-icon {
|
|
172
171
|
margin: 0;
|
|
173
172
|
}
|
|
174
173
|
}
|
|
@@ -18,7 +18,8 @@ for the two-tier shape.
|
|
|
18
18
|
| `--input-otp-surface` | `var(--theme-input-surface)` | Box background |
|
|
19
19
|
| `--input-otp-text-color` | `var(--theme-input-text-color-normal)` | Digit colour |
|
|
20
20
|
| `--input-otp-border` | `var(--theme-border)` | Box border (`normal`) or bottom border (`underlined`) |
|
|
21
|
-
| `--input-otp-border-hover` |
|
|
21
|
+
| `--input-otp-border-hover` | the resolved border | 1px outline colour on hover (`normal` variant) |
|
|
22
|
+
| `--input-otp-shadow` | `var(--form-element-shadow-colour, var(--colour-theme-2))` | Resting shadow ring outside the border (each box, `normal` only). Width is the global `--form-element-shadow-width` (`0.2rem`; `0` removes it) |
|
|
22
23
|
| `--input-otp-border-focus` | `var(--theme-border-focus)` | Outline colour on `:focus-visible`, and the bottom border colour on focus (`underlined`) |
|
|
23
24
|
|
|
24
25
|
### Field (`InputOtpField`)
|
|
@@ -35,6 +36,11 @@ Border/outline widths and transition duration are the shared form geometry token
|
|
|
35
36
|
Private tokens (not public API): `--_border` and `--_border-focus` on `.input-otp`, which only
|
|
36
37
|
resolve the two public border tokens once for reuse across the variant rules.
|
|
37
38
|
|
|
39
|
+
> **Changed 2026-10-17**: bordered controls carry a light shadow ring (`box-shadow`, theme step 2,
|
|
40
|
+
> so red when invalid), and hover draws a 1px outline in the border colour with no offset, over the
|
|
41
|
+
> ring's inner edge. Hover used to draw a 2px outline at a 2px offset in the focus colour. Focus is
|
|
42
|
+
> unchanged and sits just outside the ring.
|
|
43
|
+
|
|
38
44
|
## State hooks
|
|
39
45
|
|
|
40
46
|
| Hook | Where | Meaning |
|
|
@@ -230,10 +230,10 @@ function onFocus(event: FocusEvent) {
|
|
|
230
230
|
border: var(--form-element-border-width) solid var(--_border);
|
|
231
231
|
border-radius: var(--input-otp-border-radius, var(--form-input-border-radius));
|
|
232
232
|
outline: var(--form-element-outline-width) solid transparent;
|
|
233
|
+
box-shadow: 0 0 0 var(--form-element-shadow-width) var(--input-otp-shadow, var(--form-element-shadow-colour, var(--colour-theme-2)));
|
|
233
234
|
|
|
234
235
|
&:hover {
|
|
235
|
-
outline: var(--
|
|
236
|
-
outline-offset: var(--form-element-outline-offset-focus);
|
|
236
|
+
outline-color: var(--input-otp-border-hover, var(--_border));
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
&:focus-visible {
|
|
@@ -17,7 +17,8 @@ to override just this component; leave it unset and it inherits the shared `--th
|
|
|
17
17
|
| `--input-select-surface` | `var(--theme-input-surface)` | Wrapper/select background |
|
|
18
18
|
| `--input-select-text-color` | `var(--theme-input-text-color-normal)` | Select text colour (added 2026-10-16; it inherited the page colour before) |
|
|
19
19
|
| `--input-select-border` | `var(--theme-border)` | Border colour (`normal`/`underlined` variants and the open picker, resting) |
|
|
20
|
-
| `--input-select-border-hover` | `var(--theme-border-focus)` | Outline colour on hover (mouse) |
|
|
20
|
+
| `--input-select-border-hover` | the resolved border (`normal`), `var(--theme-border-focus)` (`underlined`) | Outline colour on hover (mouse): 1px with no offset on `normal`, 2px at a 2px offset on `underlined` |
|
|
21
|
+
| `--input-select-shadow` | `var(--form-element-shadow-colour, var(--colour-theme-2))` | Resting shadow ring outside the border (`normal` only). Width is the global `--form-element-shadow-width` (`0.2rem`; `0` removes it) |
|
|
21
22
|
| `--input-select-border-focus` | `var(--theme-border-focus)` | Outline colour on `:focus-visible` (keyboard/assistive), and the open picker's outline |
|
|
22
23
|
|
|
23
24
|
### Options
|
|
@@ -41,6 +42,11 @@ shows and ignores them.
|
|
|
41
42
|
|
|
42
43
|
---
|
|
43
44
|
|
|
45
|
+
> **Changed 2026-10-17**: bordered controls carry a light shadow ring (`box-shadow`, theme step 2,
|
|
46
|
+
> so red when invalid), and hover draws a 1px outline in the border colour with no offset, over the
|
|
47
|
+
> ring's inner edge. Hover used to draw a 2px outline at a 2px offset in the focus colour. Focus is
|
|
48
|
+
> unchanged and sits just outside the ring.
|
|
49
|
+
|
|
44
50
|
## State hooks
|
|
45
51
|
|
|
46
52
|
| Attribute | On | When |
|
|
@@ -112,16 +112,25 @@ watch(
|
|
|
112
112
|
border: var(--form-element-border-width) solid var(--_border);
|
|
113
113
|
border-radius: var(--form-input-border-radius);
|
|
114
114
|
outline: var(--form-element-outline-width) solid transparent;
|
|
115
|
+
box-shadow: 0 0 0 var(--form-element-shadow-width) var(--input-select-shadow, var(--form-element-shadow-colour, var(--colour-theme-2)));
|
|
116
|
+
|
|
117
|
+
&:has(:hover) {
|
|
118
|
+
outline-color: var(--input-select-border-hover, var(--_border));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
&:has(select:focus-visible) {
|
|
122
|
+
outline-color: var(--_border-focus);
|
|
123
|
+
}
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
&[data-input-variant="underlined"] {
|
|
118
127
|
border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--_border);
|
|
119
|
-
}
|
|
120
128
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
129
|
+
&:has(:hover) {
|
|
130
|
+
outline: var(--form-element-outline-width-focus) solid
|
|
131
|
+
var(--input-select-border-hover, var(--theme-border-focus));
|
|
132
|
+
outline-offset: var(--form-element-outline-offset-focus);
|
|
133
|
+
}
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
&:has(select:focus-visible) {
|
|
@@ -17,7 +17,8 @@ override just this component; leave it unset and it inherits the shared `--theme
|
|
|
17
17
|
| `--input-text-surface` | `var(--theme-input-surface)` | Wrapper background, and slot `InputButton` background |
|
|
18
18
|
| `--input-text-surface-hover` | `var(--theme-input-surface-hover)` | Slot `InputButton` background on hover |
|
|
19
19
|
| `--input-text-border` | `var(--theme-border)` | Border colour (`normal`/`underlined`, resting), and the stepper's button dividers |
|
|
20
|
-
| `--input-text-border-hover` |
|
|
20
|
+
| `--input-text-border-hover` | the resolved border | 1px outline colour on hover (`normal` only) |
|
|
21
|
+
| `--input-text-shadow` | `var(--form-element-shadow-colour, var(--colour-theme-2))` | Resting shadow ring outside the border (`normal` only). Width is the global `--form-element-shadow-width` (`0.2rem`; `0` removes it) |
|
|
21
22
|
| `--input-text-border-focus` | `var(--theme-border-focus)` | Outline colour on `:focus-visible` (`normal` only), and a slot button's own focus outline |
|
|
22
23
|
| `--input-text-color` | `var(--theme-input-text-color-normal)` | Input text and slot button colour, and autofilled text (global rule) |
|
|
23
24
|
| `--input-text-placeholder-color` | `var(--theme-input-placeholder)` | Placeholder text colour |
|
|
@@ -31,6 +32,11 @@ read in several places.
|
|
|
31
32
|
|
|
32
33
|
---
|
|
33
34
|
|
|
35
|
+
> **Changed 2026-10-17**: bordered controls carry a light shadow ring (`box-shadow`, theme step 2,
|
|
36
|
+
> so red when invalid), and hover draws a 1px outline in the border colour with no offset, over the
|
|
37
|
+
> ring's inner edge. Hover used to draw a 2px outline at a 2px offset in the focus colour. Focus is
|
|
38
|
+
> unchanged and sits just outside the ring.
|
|
39
|
+
|
|
34
40
|
## State hooks
|
|
35
41
|
|
|
36
42
|
| Attribute | On | When |
|
|
@@ -131,12 +131,12 @@ watch(
|
|
|
131
131
|
border: var(--form-element-border-width) solid var(--_border);
|
|
132
132
|
border-radius: var(--form-input-border-radius);
|
|
133
133
|
outline: var(--form-element-outline-width) solid transparent;
|
|
134
|
+
box-shadow: 0 0 0 var(--form-element-shadow-width) var(--input-text-shadow, var(--form-element-shadow-colour, var(--colour-theme-2)));
|
|
134
135
|
|
|
135
136
|
padding-inline: var(--input-padding-inline);
|
|
136
137
|
|
|
137
138
|
&:has(input:is(:hover), button:is(:hover)) {
|
|
138
|
-
outline: var(--
|
|
139
|
-
outline-offset: var(--form-element-outline-offset-focus);
|
|
139
|
+
outline-color: var(--input-text-border-hover, var(--_border));
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
&:has(input:focus-visible, button:focus-visible) {
|
|
@@ -180,8 +180,8 @@ watch(
|
|
|
180
180
|
outline-offset: -4px;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
|
|
184
|
-
.
|
|
183
|
+
&[data-icon-only] {
|
|
184
|
+
& .input-button-icon {
|
|
185
185
|
margin: 0;
|
|
186
186
|
}
|
|
187
187
|
}
|
|
@@ -18,7 +18,8 @@ This shape mirrors `InputText`'s token set (see its own CONSUMER-STYLING.md).
|
|
|
18
18
|
|---|---|---|
|
|
19
19
|
| `--input-textarea-surface` | `var(--theme-input-surface)` | Wrapper background |
|
|
20
20
|
| `--input-textarea-border` | `var(--theme-border)` | Border colour (`normal`/`underlined` variant border) |
|
|
21
|
-
| `--input-textarea-border-hover` |
|
|
21
|
+
| `--input-textarea-border-hover` | the resolved border | 1px wrapper outline colour on mouse hover (`normal` only) |
|
|
22
|
+
| `--input-textarea-shadow` | `var(--form-element-shadow-colour, var(--colour-theme-2))` | Resting shadow ring outside the border (`normal` only). Width is the global `--form-element-shadow-width` (`0.2rem`; `0` removes it) |
|
|
22
23
|
| `--input-textarea-border-focus` | `var(--theme-border-focus)` | Wrapper outline colour on `:focus-visible` (`normal` only) |
|
|
23
24
|
| `--input-textarea-text-color` | `var(--theme-input-text-color-normal)` | Textarea text colour |
|
|
24
25
|
| `--input-textarea-placeholder-color` | `var(--theme-input-placeholder)` | Placeholder text colour |
|
|
@@ -41,6 +42,11 @@ Private (not public API): `--_border`, resolved once and read by both border var
|
|
|
41
42
|
|
|
42
43
|
---
|
|
43
44
|
|
|
45
|
+
> **Changed 2026-10-17**: bordered controls carry a light shadow ring (`box-shadow`, theme step 2,
|
|
46
|
+
> so red when invalid), and hover draws a 1px outline in the border colour with no offset, over the
|
|
47
|
+
> ring's inner edge. Hover used to draw a 2px outline at a 2px offset in the focus colour. Focus is
|
|
48
|
+
> unchanged and sits just outside the ring.
|
|
49
|
+
|
|
44
50
|
## State hooks
|
|
45
51
|
|
|
46
52
|
| Attribute | On | When |
|
|
@@ -105,11 +105,10 @@ watch(
|
|
|
105
105
|
border: var(--form-element-border-width) solid var(--_border);
|
|
106
106
|
border-radius: var(--form-input-border-radius);
|
|
107
107
|
outline: var(--form-element-outline-width) solid transparent;
|
|
108
|
+
box-shadow: 0 0 0 var(--form-element-shadow-width) var(--input-textarea-shadow, var(--form-element-shadow-colour, var(--colour-theme-2)));
|
|
108
109
|
|
|
109
110
|
&:has(textarea:is(:hover)) {
|
|
110
|
-
outline: var(--
|
|
111
|
-
var(--input-textarea-border-hover, var(--theme-border-focus));
|
|
112
|
-
outline-offset: var(--form-element-outline-offset-focus);
|
|
111
|
+
outline-color: var(--input-textarea-border-hover, var(--_border));
|
|
113
112
|
}
|
|
114
113
|
|
|
115
114
|
&:has(textarea:focus-visible) {
|
|
@@ -12,7 +12,7 @@ import ToggleSwitchField from "../../toggle-switch/ToggleSwitchField.vue";
|
|
|
12
12
|
import MultipleCheckboxes from "../../input-checkbox/MultipleCheckboxes.vue";
|
|
13
13
|
import SingleCheckbox from "../../input-checkbox/SingleCheckbox.vue";
|
|
14
14
|
import MultipleRadiobuttons from "../../input-radio/MultipleRadiobuttons.vue";
|
|
15
|
-
import
|
|
15
|
+
import SegmentedControl from "../../segmented-control/SegmentedControl.vue";
|
|
16
16
|
import InputButton from "../../input-button/InputButton.vue";
|
|
17
17
|
import FormField from "../../form-field/FormField.vue";
|
|
18
18
|
import FormWrapper from "../../form-wrapper/FormWrapper.vue";
|
|
@@ -31,7 +31,7 @@ interface MigratedFieldsFormStoryArgs {
|
|
|
31
31
|
// InputTextAsNumberField, InputRangeField (InputRange), InputNumberField
|
|
32
32
|
// (InputNumber), InputOtpField (InputOtp), InputTextareaField (InputTextarea), InputSelectField
|
|
33
33
|
// (InputSelect), ToggleSwitchField, stacked and inline (ToggleSwitch),
|
|
34
|
-
// MultipleCheckboxes, SingleCheckbox, MultipleRadiobuttons,
|
|
34
|
+
// MultipleCheckboxes, SingleCheckbox, MultipleRadiobuttons, SegmentedControl ("Theme
|
|
35
35
|
// preference": no Field wrapper or error state, so it sits under a plain caption).
|
|
36
36
|
// - Rendered inside other fields: FormWrapper (wraps the form), FormField (wraps every field),
|
|
37
37
|
// FormFieldset (checkbox/radio groups), InputLabel, InputDescription (the "Full name" and
|
|
@@ -71,7 +71,7 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
71
71
|
MultipleCheckboxes,
|
|
72
72
|
SingleCheckbox,
|
|
73
73
|
MultipleRadiobuttons,
|
|
74
|
-
|
|
74
|
+
SegmentedControl,
|
|
75
75
|
InputButton,
|
|
76
76
|
FormField,
|
|
77
77
|
FormWrapper,
|
|
@@ -418,7 +418,7 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
418
418
|
<FormField width="wide" :has-gutter="false">
|
|
419
419
|
<div style="display: grid; gap: 0.8rem; justify-items: start;">
|
|
420
420
|
<span>Theme preference</span>
|
|
421
|
-
<
|
|
421
|
+
<SegmentedControl
|
|
422
422
|
v-model="state.themePreference"
|
|
423
423
|
v-model:field-data="themeOptions"
|
|
424
424
|
name="themePreference"
|
|
@@ -8,8 +8,12 @@ directly: `InputButton` renders it when `has-pending-effect` is set, and it anim
|
|
|
8
8
|
<InputButton button-text="Save" has-pending-effect :is-pending="saving" />
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
The CSS keys off the host button's
|
|
12
|
-
|
|
11
|
+
The CSS keys off the host button's state attributes: `.input-button[data-pending-effect]` (added by
|
|
12
|
+
`has-pending-effect`), `[data-pending]`, and `[data-icon-only]`. The SVG itself is
|
|
13
|
+
`.pending-effect-container`.
|
|
14
|
+
|
|
15
|
+
> **Changed 2026-10-17**: these were the classes `.pending-effect`, `.is-pending` and `.icon-only`
|
|
16
|
+
> until `InputButton` moved its state to `data-*` attributes.
|
|
13
17
|
|
|
14
18
|
The line sits outside the button's edge, so nothing around it may clip overflow. `InputButton`
|
|
15
19
|
sets `overflow: visible` on itself when `has-pending-effect` is on, but an ancestor with
|
|
@@ -50,7 +54,7 @@ positioning maths) and `--_offset` (the resolved `--pending-effect-offset`).
|
|
|
50
54
|
## Reduced motion
|
|
51
55
|
|
|
52
56
|
Under `prefers-reduced-motion: reduce` the effect stays hidden. The host button still gets
|
|
53
|
-
`pointer-events: none` while
|
|
57
|
+
`pointer-events: none` while `[data-pending]` is set, from this component's CSS, so it can't be
|
|
54
58
|
clicked twice.
|
|
55
59
|
|
|
56
60
|
---
|
|
@@ -34,7 +34,7 @@ watch(
|
|
|
34
34
|
|
|
35
35
|
<style lang="css">
|
|
36
36
|
@layer components {
|
|
37
|
-
.pending-effect {
|
|
37
|
+
.input-button[data-pending-effect] {
|
|
38
38
|
position: relative;
|
|
39
39
|
|
|
40
40
|
.pending-effect-container {
|
|
@@ -72,14 +72,14 @@ watch(
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
&[data-icon-only] .pending-effect-container {
|
|
76
76
|
.pending-effect-blur,
|
|
77
77
|
.pending-effect-line {
|
|
78
78
|
rx: var(--pending-effect-border-radius-icon-only, 100vw);
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
&[data-pending] {
|
|
83
83
|
pointer-events: none;
|
|
84
84
|
cursor: wait;
|
|
85
85
|
|
|
@@ -54,7 +54,7 @@ describe("PendingEffect", () => {
|
|
|
54
54
|
props: { buttonText: "Save", hasPendingEffect: true },
|
|
55
55
|
});
|
|
56
56
|
expect(withEffect.find(".pending-effect-container").exists()).toBe(true);
|
|
57
|
-
expect(withEffect.find("button").
|
|
57
|
+
expect(withEffect.find("button").attributes("data-pending-effect")).toBe("");
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
it("passes the button theme through", async () => {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# SegmentedControl — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
> **Renamed 2026-10-17**: `TripleToggleSwitch` → `SegmentedControl`, since it handles any number
|
|
4
|
+
> of options. Every class and token changed prefix: `.triple-toggle-switch*` → `.segmented-control*`
|
|
5
|
+
> and `--triple-toggle-switch-*` → `--segmented-control-*`, with the rest of each name unchanged.
|
|
6
|
+
>
|
|
7
|
+
> **Renamed 2026-09-27**: `TripleToggleSwitchCore` → `TripleToggleSwitch`. The root class was
|
|
8
|
+
> already `.triple-toggle-switch`, and no token names changed.
|
|
9
|
+
|
|
10
|
+
## Public token API
|
|
11
|
+
|
|
12
|
+
Colour tokens fall back to the shared `--theme-*` tokens every other themed component uses, so
|
|
13
|
+
leave them unset to follow the site theme. See `theming-component-token-pattern.md`.
|
|
14
|
+
|
|
15
|
+
### Wrapper
|
|
16
|
+
|
|
17
|
+
| Token | Default | Controls |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| `--segmented-control-surface` | `var(--theme-input-surface)` | Wrapper background |
|
|
20
|
+
| `--segmented-control-border` | `var(--theme-border)` | Wrapper border colour |
|
|
21
|
+
| `--segmented-control-border-hover` | the resolved border | 1px wrapper outline colour on hover |
|
|
22
|
+
| `--segmented-control-shadow` | `var(--form-element-shadow-colour, var(--colour-theme-2))` | Resting shadow ring outside the border (on the wrapper). Width is the global `--form-element-shadow-width` (`0.2rem`; `0` removes it) |
|
|
23
|
+
| `--segmented-control-ring-focus` | `var(--theme-ring)` | Wrapper outline colour while a radio has `:focus-visible` |
|
|
24
|
+
| `--segmented-control-padding` | `0.6rem` | Wrapper padding |
|
|
25
|
+
| `--segmented-control-gap` | `1rem` | Gap between the options (also drives the marker's position) |
|
|
26
|
+
|
|
27
|
+
### Selected-option marker
|
|
28
|
+
|
|
29
|
+
| Token | Default | Controls |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `--segmented-control-marker-surface` | `var(--theme-surface)` | Marker background when the selected value isn't `system`/`light`/`dark` |
|
|
32
|
+
| `--segmented-control-marker-border` | `var(--theme-text)` | Marker border colour |
|
|
33
|
+
| `--segmented-control-marker-gradient-system` | green `radial-gradient(...)` | Marker `background-image` when `system` is selected |
|
|
34
|
+
| `--segmented-control-marker-gradient-light` | red/orange `radial-gradient(...)` | Marker `background-image` when `light` is selected |
|
|
35
|
+
| `--segmented-control-marker-gradient-dark` | black/grey `radial-gradient(...)` | Marker `background-image` when `dark` is selected |
|
|
36
|
+
|
|
37
|
+
> **Changed 2026-09-27**: the three marker gradients were hardcoded literals with no override hook.
|
|
38
|
+
> They're now public tokens; the defaults are unchanged. Any `<image>` value works
|
|
39
|
+
> (`linear-gradient(...)`, `url(...)`, `none`).
|
|
40
|
+
|
|
41
|
+
### Options
|
|
42
|
+
|
|
43
|
+
| Token | Default | Controls |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `--segmented-control-option-padding` | `0.5rem` | Padding inside each option circle |
|
|
46
|
+
| `--segmented-control-icon-size` | `2rem` | Icon `font-size` (and the hidden radio's hit area) |
|
|
47
|
+
| `--segmented-control-option-border` | `#00000025` | Each option's resting border colour |
|
|
48
|
+
| `--segmented-control-option-border-hover` | `var(--theme-text)` | Option outline colour on hover |
|
|
49
|
+
| `--segmented-control-option-ring-focus` | `var(--theme-ring)` | Option outline colour on `:focus-visible` |
|
|
50
|
+
| `--segmented-control-option-icon-color` | `var(--theme-text)` | Icon colour, resting |
|
|
51
|
+
| `--segmented-control-option-icon-color-active` | `var(--theme-on-surface)` | Icon colour, selected |
|
|
52
|
+
|
|
53
|
+
> **Changed 2026-10-17**: the marker surface defaulted to `--theme-input-surface` (white) while
|
|
54
|
+
> the selected icon defaulted to white too, so any value other than `system`/`light`/`dark` showed
|
|
55
|
+
> a white icon on a white marker. The marker now defaults to `--theme-surface` and the icons to
|
|
56
|
+
> `--theme-text`/`--theme-on-surface`, replacing the raw `--slate-10`/`--slate-00` palette tokens
|
|
57
|
+
> (close to the same colours on the default theme).
|
|
58
|
+
|
|
59
|
+
Also read (shared, not component-specific): `--form-element-border-width`,
|
|
60
|
+
`--form-element-outline-width`, `--theme-form-transition-duration`. See
|
|
61
|
+
`theming-form-geometry-tokens.md`.
|
|
62
|
+
|
|
63
|
+
Private tokens (not public API): `--_form-items-gap`, `--_scheme-icon-font-size` (each read in
|
|
64
|
+
several places, fed by the public gap/icon-size tokens) and `--_select-scheme-group-background-color`/
|
|
65
|
+
`--_select-scheme-group-background-image` (swapped per selected value).
|
|
66
|
+
|
|
67
|
+
> **Changed 2026-10-17**: bordered controls carry a light shadow ring (`box-shadow`, theme step 2,
|
|
68
|
+
> so red when invalid), and hover draws a 1px outline in the border colour with no offset, over the
|
|
69
|
+
> ring's inner edge. Hover used to draw a 2px outline at a 2px offset in the focus colour. Focus is
|
|
70
|
+
> unchanged and sits just outside the ring.
|
|
71
|
+
|
|
72
|
+
## State hooks
|
|
73
|
+
|
|
74
|
+
- **Selected value**: the marker gradients key off `:has(input[value="system" | "light" | "dark"]:checked)`
|
|
75
|
+
on the root. These three values are hardcoded, a legacy of the one real consumer
|
|
76
|
+
(`DisplayThemeSwitch`). Any other value shows `--segmented-control-marker-surface` instead.
|
|
77
|
+
- **Each option** (`.segmented-control-option`, the `<label>`) carries `data-value` (the
|
|
78
|
+
option's value) and `data-checked` when selected. Target one with
|
|
79
|
+
`.segmented-control-option[data-value="light"]`.
|
|
80
|
+
- **Root** has `data-has-selection` when the value matches an option; without it the marker is
|
|
81
|
+
hidden.
|
|
82
|
+
- **Theme**: `data-theme` on the root, from the `theme` prop.
|
|
83
|
+
- **Inner classes**: `.segmented-control-wrapper`, `.segmented-control-marker-layer`,
|
|
84
|
+
`.segmented-control-marker`, `.segmented-control-options` (the `role="radiogroup"`),
|
|
85
|
+
`.segmented-control-option`, `.segmented-control-option-icon`,
|
|
86
|
+
`.segmented-control-option-input`.
|
|
87
|
+
|
|
88
|
+
> **Changed 2026-10-17**: the inner classes were generic (`.selected-option-marker-wrapper`,
|
|
89
|
+
> `.selected-option-marker`, `.option-group-wrapper`, `.option-group`, `.option-icon`,
|
|
90
|
+
> `.option-input`) and collided with consumer CSS. State moved too: `.option-icon.active` →
|
|
91
|
+
> `.segmented-control-option[data-checked]`, `.option-icon.light` (the option id as a class)
|
|
92
|
+
> → `.segmented-control-option[data-value="light"]`, and `.selected-option-marker.show` is gone:
|
|
93
|
+
> the marker is positioned in CSS from the first paint, no measuring.
|
|
94
|
+
|
|
95
|
+
## Motion
|
|
96
|
+
|
|
97
|
+
The marker slides between options over the `stepAnimationDuration` prop (default `250ms`). Under
|
|
98
|
+
`prefers-reduced-motion: reduce` the slide transition is removed and the marker jumps.
|
|
99
|
+
|
|
100
|
+
## Global theming
|
|
101
|
+
|
|
102
|
+
```css
|
|
103
|
+
:where(html) {
|
|
104
|
+
--segmented-control-surface: var(--rose-09);
|
|
105
|
+
--segmented-control-option-ring-focus: var(--rose-04);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Local overrides
|
|
110
|
+
|
|
111
|
+
Set the tokens above on an element you own (a page or section class, or a class added with
|
|
112
|
+
`:style-class-passthrough`): they inherit down into the component. Keep the block **unlayered** (no
|
|
113
|
+
`@layer` wrapper) so it beats the library's `@layer components`. If your own file uses
|
|
114
|
+
`<style scoped>`, tokens set on your element still work, but selectors that reach inside the
|
|
115
|
+
component need `:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
|
|
116
|
+
|
|
117
|
+
**Caveat:** when used through `DisplayThemeSwitch`, that wrapper re-declares the four sizing tokens
|
|
118
|
+
(`--segmented-control-gap`, `-padding`, `-option-padding`, `-icon-size`) on the root element, so
|
|
119
|
+
setting them on an ancestor has no effect there. Set them on the component itself (via
|
|
120
|
+
`style` or a passthrough class) instead. Used directly, `SegmentedControl` re-declares nothing.
|
|
121
|
+
|
|
122
|
+
## Class passthrough
|
|
123
|
+
|
|
124
|
+
`style-class-passthrough` adds classes to the root `.segmented-control` element, alongside
|
|
125
|
+
`data-theme`. `DisplayThemeSwitch` uses it for its `colour-scheme-select` and `small` classes.
|