srcdev-nuxt-components 9.6.8 → 9.6.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/component-compliance-checklist.md +12 -1
- package/.claude/skills/component-inline-action-button.md +1 -1
- package/.claude/skills/component-local-style-override.md +1 -1
- package/.claude/skills/component-naming.md +11 -11
- package/.claude/skills/components/container-glow.md +2 -2
- package/.claude/skills/components/form-field.md +2 -2
- package/.claude/skills/components/form-wrapper.md +1 -1
- package/.claude/skills/components/glowing-border.md +1 -1
- package/.claude/skills/components/input-description.md +7 -7
- package/.claude/skills/components/input-error.md +3 -3
- package/.claude/skills/components/input-label.md +7 -8
- package/.claude/skills/components/input-number.md +17 -17
- package/.claude/skills/components/input-select.md +153 -0
- package/.claude/skills/components/input-text.md +220 -0
- package/.claude/skills/components/input-textarea.md +147 -0
- package/.claude/skills/components/select-menu.md +4 -4
- package/.claude/skills/components/toggle-switch.md +143 -0
- package/.claude/skills/composable-zod-validation.md +1 -1
- package/.claude/skills/icon-sets.md +1 -1
- package/.claude/skills/icon-sizing.md +1 -1
- package/.claude/skills/index.md +5 -5
- package/.claude/skills/storybook-add-story.md +1 -1
- package/.claude/skills/theming-component-token-pattern.md +8 -8
- package/.claude/skills/theming-form-geometry-tokens.md +1 -1
- package/.vscode/srcdev-component-input-select.code-snippets +20 -14
- package/.vscode/srcdev-component-input-text.code-snippets +46 -40
- package/.vscode/srcdev-component-input-textarea.code-snippets +19 -14
- package/.vscode/srcdev-component-toggle-switch.code-snippets +19 -29
- package/app/assets/styles/setup/04.elements/forms/00.element-defaults.css +2 -2
- package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +1 -1
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +2 -2
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +4 -4
- package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +4 -4
- package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +1 -1
- package/app/components/05.forms/form-field/CONSUMER-STYLING.md +2 -2
- package/app/components/05.forms/input-button/stories/InputButton.stories.ts +3 -3
- package/app/components/05.forms/input-description/CONSUMER-STYLING.md +6 -6
- package/app/components/05.forms/input-label/CONSUMER-STYLING.md +5 -5
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +9 -5
- package/app/components/05.forms/input-number/InputNumber.vue +2 -2
- package/app/components/05.forms/input-select/CONSUMER-STYLING.md +80 -10
- package/app/components/05.forms/input-select/InputSelect.vue +198 -0
- package/app/components/05.forms/input-select/InputSelectField.vue +130 -0
- package/app/components/05.forms/input-select/stories/InputSelect.stories.ts +184 -0
- package/app/components/05.forms/input-select/stories/InputSelectField.stories.ts +187 -0
- package/app/components/05.forms/input-select/tests/{InputSelectCore.spec.ts → InputSelect.spec.ts} +75 -12
- package/app/components/05.forms/input-select/tests/InputSelectField.spec.ts +121 -0
- package/app/components/05.forms/input-text/CONSUMER-STYLING.md +85 -15
- package/app/components/05.forms/input-text/{InputPasswordWithLabel.vue → InputPasswordField.vue} +16 -19
- package/app/components/05.forms/input-text/{InputTextCore.vue → InputText.vue} +42 -78
- package/app/components/05.forms/input-text/{InputTextAsNumberWithLabel.vue → InputTextAsNumberField.vue} +66 -62
- package/app/components/05.forms/input-text/InputTextField.vue +168 -0
- package/app/components/05.forms/input-text/stories/InputPasswordField.stories.ts +163 -0
- package/app/components/05.forms/input-text/stories/InputText.stories.ts +194 -0
- package/app/components/05.forms/input-text/stories/InputTextAsNumberField.stories.ts +182 -0
- package/app/components/05.forms/input-text/stories/InputTextField.stories.ts +206 -0
- package/app/components/05.forms/input-text/tests/{InputPasswordWithLabel.spec.ts → InputPasswordField.spec.ts} +42 -3
- package/app/components/05.forms/input-text/tests/InputText.spec.ts +166 -0
- package/app/components/05.forms/input-text/tests/InputTextAsNumberField.spec.ts +135 -0
- package/app/components/05.forms/input-text/tests/InputTextField.spec.ts +130 -0
- package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +94 -12
- package/app/components/05.forms/input-textarea/InputTextarea.vue +165 -0
- package/app/components/05.forms/input-textarea/InputTextareaField.vue +159 -0
- package/app/components/05.forms/input-textarea/stories/InputTextarea.stories.ts +198 -0
- package/app/components/05.forms/input-textarea/stories/InputTextareaField.stories.ts +161 -0
- package/app/components/05.forms/input-textarea/tests/InputTextarea.spec.ts +149 -0
- package/app/components/05.forms/input-textarea/tests/InputTextareaField.spec.ts +122 -0
- package/app/components/05.forms/patterns/stories/ContactForm.stories.ts +13 -13
- package/app/components/05.forms/patterns/stories/LoginForm.stories.ts +6 -6
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +30 -32
- package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +71 -50
- package/app/components/05.forms/toggle-switch/{ToggleSwitchCore.vue → ToggleSwitch.vue} +58 -73
- package/app/components/05.forms/toggle-switch/{ToggleSwitchWithLabel.vue → ToggleSwitchField.vue} +65 -19
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitch.stories.ts +148 -0
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchField.stories.ts +176 -0
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitch.spec.ts +145 -0
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchField.spec.ts +155 -0
- package/package.json +1 -1
- package/.claude/skills/components/input-select-core.md +0 -151
- package/.claude/skills/components/input-text-core.md +0 -204
- package/.claude/skills/components/input-textarea-core.md +0 -154
- package/.claude/skills/components/toggle-switch-core.md +0 -169
- package/app/components/05.forms/input-select/InputSelectCore.vue +0 -188
- package/app/components/05.forms/input-select/InputSelectWithLabel.vue +0 -118
- package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +0 -69
- package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +0 -115
- package/app/components/05.forms/input-select/tests/InputSelectWithLabel.spec.ts +0 -87
- package/app/components/05.forms/input-text/InputTextWithLabel.vue +0 -154
- package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +0 -368
- package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +0 -480
- package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +0 -267
- package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +0 -319
- package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +0 -75
- package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +0 -60
- package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +0 -71
- package/app/components/05.forms/input-textarea/InputTextareaCore.vue +0 -159
- package/app/components/05.forms/input-textarea/InputTextareaWithLabel.vue +0 -136
- package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +0 -247
- package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +0 -458
- package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +0 -119
- package/app/components/05.forms/input-textarea/tests/InputTextareaWithLabel.spec.ts +0 -93
- package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabelInline.vue +0 -81
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +0 -251
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +0 -251
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +0 -283
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +0 -101
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabel.spec.ts +0 -133
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabelInline.spec.ts +0 -66
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// https://nuxt.com/docs/getting-started/testing#unit-testing
|
|
2
|
+
import { nextTick } from "vue";
|
|
3
|
+
import type { VueWrapper } from "@vue/test-utils";
|
|
4
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
5
|
+
import ComponentUnderTest from "../InputTextareaField.vue";
|
|
6
|
+
|
|
7
|
+
const initialPropsData = {
|
|
8
|
+
name: "message",
|
|
9
|
+
label: "Message",
|
|
10
|
+
errorMessage: "",
|
|
11
|
+
modelValue: "",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
|
|
15
|
+
const wrapperFactory = (propsData = {}, slots = {}) => {
|
|
16
|
+
const mockPropsData = { ...initialPropsData, ...propsData };
|
|
17
|
+
|
|
18
|
+
return mountSuspended(ComponentUnderTest, {
|
|
19
|
+
attachTo: document.body,
|
|
20
|
+
props: mockPropsData,
|
|
21
|
+
slots,
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
wrapper?.unmount();
|
|
27
|
+
vi.restoreAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("InputTextareaField", () => {
|
|
31
|
+
it("mounts without error", async () => {
|
|
32
|
+
wrapper = await wrapperFactory();
|
|
33
|
+
expect(wrapper.vm).toBeTruthy();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("renders .input-textarea-field as the root", async () => {
|
|
37
|
+
wrapper = await wrapperFactory();
|
|
38
|
+
expect(wrapper.classes()).toContain("input-textarea-field");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("renders the label text, linked to the textarea", async () => {
|
|
42
|
+
wrapper = await wrapperFactory({ label: "Feedback" });
|
|
43
|
+
const label = wrapper.find("label");
|
|
44
|
+
|
|
45
|
+
expect(label.text()).toContain("Feedback");
|
|
46
|
+
expect(label.attributes("for")).toBe(wrapper.find("textarea").attributes("id"));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("does not leak label or error-message onto the control's wrapper", async () => {
|
|
50
|
+
wrapper = await wrapperFactory({ label: "Feedback", errorMessage: "Oops" });
|
|
51
|
+
const control = wrapper.find(".input-textarea-wrapper");
|
|
52
|
+
|
|
53
|
+
expect(control.attributes("label")).toBeUndefined();
|
|
54
|
+
expect(control.attributes("error-message")).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("sets data-invalid on the root and links the error via aria-describedby", async () => {
|
|
58
|
+
wrapper = await wrapperFactory({ fieldHasError: true, errorMessage: "This field is required" });
|
|
59
|
+
|
|
60
|
+
expect(wrapper.attributes("data-invalid")).toBe("");
|
|
61
|
+
expect(wrapper.text()).toContain("This field is required");
|
|
62
|
+
expect(wrapper.find("textarea").attributes("aria-describedby")).toMatch(/-error-message$/);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("renders the descriptionText slot before the textarea by default", async () => {
|
|
66
|
+
wrapper = await wrapperFactory({}, { descriptionText: () => "Please provide more detail" });
|
|
67
|
+
const html = wrapper.html();
|
|
68
|
+
|
|
69
|
+
expect(wrapper.text()).toContain("Please provide more detail");
|
|
70
|
+
expect(html.indexOf("Please provide more detail")).toBeLessThan(html.indexOf("<textarea"));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("renders the description after the textarea for the outlined variant", async () => {
|
|
74
|
+
wrapper = await wrapperFactory({ inputVariant: "outlined" }, { descriptionText: () => "Hint text" });
|
|
75
|
+
const html = wrapper.html();
|
|
76
|
+
expect(html.indexOf("Hint text")).toBeGreaterThan(html.indexOf("<textarea"));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("renders the left/right slots when provided", async () => {
|
|
80
|
+
wrapper = await wrapperFactory(
|
|
81
|
+
{},
|
|
82
|
+
{
|
|
83
|
+
left: '<span data-testid="left-slot">📝</span>',
|
|
84
|
+
right: '<span data-testid="right-slot">✨</span>',
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(wrapper.find('[data-testid="left-slot"]').exists()).toBe(true);
|
|
89
|
+
expect(wrapper.find('[data-testid="right-slot"]').exists()).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("marks the field dirty once the value is non-empty", async () => {
|
|
93
|
+
wrapper = await wrapperFactory();
|
|
94
|
+
expect(wrapper.attributes("data-dirty")).toBeUndefined();
|
|
95
|
+
|
|
96
|
+
await wrapper.setProps({ modelValue: "hello" });
|
|
97
|
+
await nextTick();
|
|
98
|
+
expect(wrapper.attributes("data-dirty")).toBe("");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("sets data-input-variant on the root", async () => {
|
|
102
|
+
wrapper = await wrapperFactory({ inputVariant: "underlined" });
|
|
103
|
+
expect(wrapper.attributes("data-input-variant")).toBe("underlined");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("forwards readonly and autocomplete", async () => {
|
|
107
|
+
wrapper = await wrapperFactory({ readonly: true, autocomplete: "off" });
|
|
108
|
+
expect(wrapper.find("textarea").attributes("readonly")).toBeDefined();
|
|
109
|
+
expect(wrapper.find("textarea").attributes("autocomplete")).toBe("off");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("applies styleClassPassthrough to its root only and reacts to changes", async () => {
|
|
113
|
+
wrapper = await wrapperFactory({ styleClassPassthrough: "custom-textarea" });
|
|
114
|
+
expect(wrapper.classes()).toContain("custom-textarea");
|
|
115
|
+
expect(wrapper.find("textarea").classes()).not.toContain("custom-textarea");
|
|
116
|
+
|
|
117
|
+
await wrapper.setProps({ styleClassPassthrough: "other-textarea" });
|
|
118
|
+
await nextTick();
|
|
119
|
+
expect(wrapper.classes()).toContain("other-textarea");
|
|
120
|
+
expect(wrapper.classes()).not.toContain("custom-textarea");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
2
|
import { reactive, ref } from "vue";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
3
|
+
import InputTextField from "../../input-text/InputTextField.vue";
|
|
4
|
+
import InputSelectField from "../../input-select/InputSelectField.vue";
|
|
5
|
+
import InputTextareaField from "../../input-textarea/InputTextareaField.vue";
|
|
6
6
|
import MultipleCheckboxes from "../../input-checkbox/MultipleCheckboxes.vue";
|
|
7
7
|
import InputButton from "../../input-button/InputButton.vue";
|
|
8
8
|
import FormField from "../../form-field/FormField.vue";
|
|
@@ -14,7 +14,7 @@ interface ContactFormStoryArgs {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
// Demo composition, not a component of its own — a richer variant of a typical contact page
|
|
17
|
-
// pattern: name/phone/email + multi-select services + comments shape, plus an
|
|
17
|
+
// pattern: name/phone/email + multi-select services + comments shape, plus an InputSelectField
|
|
18
18
|
// "How did you hear about us?" field, to also exercise the select
|
|
19
19
|
// component alongside the others in one composed form. No validation wiring
|
|
20
20
|
// (useZodValidation/zod) here — that's the consuming app's concern, this is a visual/composition
|
|
@@ -36,9 +36,9 @@ export default {
|
|
|
36
36
|
|
|
37
37
|
const Template: StoryFn<ContactFormStoryArgs> = (args) => ({
|
|
38
38
|
components: {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
InputTextField,
|
|
40
|
+
InputSelectField,
|
|
41
|
+
InputTextareaField,
|
|
42
42
|
MultipleCheckboxes,
|
|
43
43
|
InputButton,
|
|
44
44
|
FormField,
|
|
@@ -91,7 +91,7 @@ const Template: StoryFn<ContactFormStoryArgs> = (args) => ({
|
|
|
91
91
|
|
|
92
92
|
<form @submit.prevent>
|
|
93
93
|
<FormField width="wide" :has-gutter="false">
|
|
94
|
-
<
|
|
94
|
+
<InputTextField
|
|
95
95
|
v-model="state.fullName"
|
|
96
96
|
type="text"
|
|
97
97
|
name="fullName"
|
|
@@ -104,7 +104,7 @@ const Template: StoryFn<ContactFormStoryArgs> = (args) => ({
|
|
|
104
104
|
</FormField>
|
|
105
105
|
|
|
106
106
|
<FormField width="wide" :has-gutter="false">
|
|
107
|
-
<
|
|
107
|
+
<InputTextField
|
|
108
108
|
v-model="state.telNumber"
|
|
109
109
|
type="tel"
|
|
110
110
|
inputmode="tel"
|
|
@@ -118,7 +118,7 @@ const Template: StoryFn<ContactFormStoryArgs> = (args) => ({
|
|
|
118
118
|
</FormField>
|
|
119
119
|
|
|
120
120
|
<FormField width="wide" :has-gutter="false">
|
|
121
|
-
<
|
|
121
|
+
<InputTextField
|
|
122
122
|
v-model="state.emailAddress"
|
|
123
123
|
type="email"
|
|
124
124
|
inputmode="email"
|
|
@@ -132,7 +132,7 @@ const Template: StoryFn<ContactFormStoryArgs> = (args) => ({
|
|
|
132
132
|
</FormField>
|
|
133
133
|
|
|
134
134
|
<FormField width="wide" :has-gutter="false">
|
|
135
|
-
<
|
|
135
|
+
<InputSelectField
|
|
136
136
|
v-model="state.hearAboutUs"
|
|
137
137
|
v-model:field-data="hearAboutUsOptions"
|
|
138
138
|
name="hearAboutUs"
|
|
@@ -160,7 +160,7 @@ const Template: StoryFn<ContactFormStoryArgs> = (args) => ({
|
|
|
160
160
|
</FormField>
|
|
161
161
|
|
|
162
162
|
<FormField width="wide" :has-gutter="false">
|
|
163
|
-
<
|
|
163
|
+
<InputTextareaField
|
|
164
164
|
v-model="state.comments"
|
|
165
165
|
name="comments"
|
|
166
166
|
label="Comments"
|
|
@@ -169,7 +169,7 @@ const Template: StoryFn<ContactFormStoryArgs> = (args) => ({
|
|
|
169
169
|
:input-variant="args.inputVariant"
|
|
170
170
|
>
|
|
171
171
|
<template #descriptionText>Optional — any extra information that might help us.</template>
|
|
172
|
-
</
|
|
172
|
+
</InputTextareaField>
|
|
173
173
|
</FormField>
|
|
174
174
|
|
|
175
175
|
<FormField width="wide" :has-gutter="false">
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
2
|
import { reactive } from "vue";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import InputTextField from "../../input-text/InputTextField.vue";
|
|
4
|
+
import InputPasswordField from "../../input-text/InputPasswordField.vue";
|
|
5
5
|
import InputCheckboxRadioField from "../../input-checkbox-radio/InputCheckboxRadioField.vue";
|
|
6
6
|
import InputButton from "../../input-button/InputButton.vue";
|
|
7
7
|
import FormField from "../../form-field/FormField.vue";
|
|
@@ -35,8 +35,8 @@ export default {
|
|
|
35
35
|
|
|
36
36
|
const Template: StoryFn<LoginFormStoryArgs> = (args) => ({
|
|
37
37
|
components: {
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
InputTextField,
|
|
39
|
+
InputPasswordField,
|
|
40
40
|
InputCheckboxRadioField,
|
|
41
41
|
InputButton,
|
|
42
42
|
FormField,
|
|
@@ -64,7 +64,7 @@ const Template: StoryFn<LoginFormStoryArgs> = (args) => ({
|
|
|
64
64
|
|
|
65
65
|
<form @submit.prevent>
|
|
66
66
|
<FormField width="wide" :has-gutter="false">
|
|
67
|
-
<
|
|
67
|
+
<InputTextField
|
|
68
68
|
v-model="state.email"
|
|
69
69
|
type="email"
|
|
70
70
|
inputmode="email"
|
|
@@ -78,7 +78,7 @@ const Template: StoryFn<LoginFormStoryArgs> = (args) => ({
|
|
|
78
78
|
</FormField>
|
|
79
79
|
|
|
80
80
|
<FormField width="wide" :has-gutter="false">
|
|
81
|
-
<
|
|
81
|
+
<InputPasswordField
|
|
82
82
|
v-model="state.password"
|
|
83
83
|
name="password"
|
|
84
84
|
label="Password"
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
2
|
import { reactive, ref } from "vue";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
3
|
+
import InputTextField from "../../input-text/InputTextField.vue";
|
|
4
|
+
import InputPasswordField from "../../input-text/InputPasswordField.vue";
|
|
5
|
+
import InputTextAsNumberField from "../../input-text/InputTextAsNumberField.vue";
|
|
6
6
|
import InputRangeField from "../../input-range/InputRangeField.vue";
|
|
7
7
|
import InputNumberField from "../../input-number/InputNumberField.vue";
|
|
8
8
|
import InputOtpField from "../../input-otp/InputOtpField.vue";
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import ToggleSwitchWithLabelInline from "../../toggle-switch/ToggleSwitchWithLabelInline.vue";
|
|
9
|
+
import InputTextareaField from "../../input-textarea/InputTextareaField.vue";
|
|
10
|
+
import InputSelectField from "../../input-select/InputSelectField.vue";
|
|
11
|
+
import ToggleSwitchField from "../../toggle-switch/ToggleSwitchField.vue";
|
|
13
12
|
import MultipleCheckboxes from "../../input-checkbox/MultipleCheckboxes.vue";
|
|
14
13
|
import SingleCheckbox from "../../input-checkbox/SingleCheckbox.vue";
|
|
15
14
|
import MultipleRadiobuttons from "../../input-radio/MultipleRadiobuttons.vue";
|
|
@@ -27,11 +26,11 @@ interface MigratedFieldsFormStoryArgs {
|
|
|
27
26
|
// Living reference, not a component of its own: every 05.forms component that scores 5/5 in the
|
|
28
27
|
// Component Ledger (.claude/component-ledger/audit.json) appears here, either as its own field or
|
|
29
28
|
// rendered inside one. As of 2026-10-04 every 05.forms component is 5/5, so all of them are in:
|
|
30
|
-
// - Own field:
|
|
31
|
-
// on-device keyboards),
|
|
32
|
-
//
|
|
33
|
-
// (InputNumber), InputOtpField (InputOtp),
|
|
34
|
-
// (
|
|
29
|
+
// - Own field: InputTextField (InputText; plus one per type/inputmode pair for checking
|
|
30
|
+
// on-device keyboards), InputPasswordField,
|
|
31
|
+
// InputTextAsNumberField, InputRangeField (InputRange), InputNumberField
|
|
32
|
+
// (InputNumber), InputOtpField (InputOtp), InputTextareaField (InputTextarea), InputSelectField
|
|
33
|
+
// (InputSelect), ToggleSwitchField, stacked and inline (ToggleSwitch),
|
|
35
34
|
// MultipleCheckboxes, SingleCheckbox, MultipleRadiobuttons, TripleToggleSwitch ("Theme
|
|
36
35
|
// preference": no Field wrapper or error state, so it sits under a plain caption).
|
|
37
36
|
// - Rendered inside other fields: FormWrapper (wraps the form), FormField (wraps every field),
|
|
@@ -60,16 +59,15 @@ export default {
|
|
|
60
59
|
|
|
61
60
|
const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
62
61
|
components: {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
InputTextField,
|
|
63
|
+
InputPasswordField,
|
|
64
|
+
InputTextAsNumberField,
|
|
66
65
|
InputRangeField,
|
|
67
66
|
InputNumberField,
|
|
68
67
|
InputOtpField,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
ToggleSwitchWithLabelInline,
|
|
68
|
+
InputTextareaField,
|
|
69
|
+
InputSelectField,
|
|
70
|
+
ToggleSwitchField,
|
|
73
71
|
MultipleCheckboxes,
|
|
74
72
|
SingleCheckbox,
|
|
75
73
|
MultipleRadiobuttons,
|
|
@@ -97,7 +95,7 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
97
95
|
terms: false,
|
|
98
96
|
});
|
|
99
97
|
|
|
100
|
-
// One
|
|
98
|
+
// One InputTextField per type/inputmode pair, for checking each on-screen keyboard on a real device.
|
|
101
99
|
const textVariants: { name: string; label: string; type: InputTypesText; inputmode: InputMode; placeholder: string }[] = [
|
|
102
100
|
{ name: "variantEmail", label: "Email", type: "email", inputmode: "email", placeholder: "eg. jane@example.com" },
|
|
103
101
|
{ name: "variantTel", label: "Phone", type: "tel", inputmode: "tel", placeholder: "eg. 07700 900123" },
|
|
@@ -251,7 +249,7 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
251
249
|
<FormWrapper width="medium">
|
|
252
250
|
<form novalidate @submit.prevent="submit">
|
|
253
251
|
<FormField width="wide" :has-gutter="false">
|
|
254
|
-
<
|
|
252
|
+
<InputTextField
|
|
255
253
|
v-model="state.fullName"
|
|
256
254
|
type="text"
|
|
257
255
|
name="fullName"
|
|
@@ -263,11 +261,11 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
263
261
|
:input-variant="args.inputVariant"
|
|
264
262
|
>
|
|
265
263
|
<template #descriptionText>As it appears on your ID</template>
|
|
266
|
-
</
|
|
264
|
+
</InputTextField>
|
|
267
265
|
</FormField>
|
|
268
266
|
|
|
269
267
|
<FormField width="wide" :has-gutter="false">
|
|
270
|
-
<
|
|
268
|
+
<InputPasswordField
|
|
271
269
|
v-model="state.password"
|
|
272
270
|
name="password"
|
|
273
271
|
label="Password"
|
|
@@ -277,11 +275,11 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
277
275
|
:input-variant="args.inputVariant"
|
|
278
276
|
>
|
|
279
277
|
<template #descriptionText>At least 8 characters</template>
|
|
280
|
-
</
|
|
278
|
+
</InputPasswordField>
|
|
281
279
|
</FormField>
|
|
282
280
|
|
|
283
281
|
<FormField v-for="variant in textVariants" :key="variant.name" width="wide" :has-gutter="false">
|
|
284
|
-
<
|
|
282
|
+
<InputTextField
|
|
285
283
|
v-model="textVariantValues[variant.name]"
|
|
286
284
|
:type="variant.type"
|
|
287
285
|
:inputmode="variant.inputmode"
|
|
@@ -292,7 +290,7 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
292
290
|
:input-variant="args.inputVariant"
|
|
293
291
|
>
|
|
294
292
|
<template #descriptionText>type="{{ variant.type }}", inputmode="{{ variant.inputmode }}"</template>
|
|
295
|
-
</
|
|
293
|
+
</InputTextField>
|
|
296
294
|
</FormField>
|
|
297
295
|
|
|
298
296
|
<FormField width="wide" :has-gutter="false">
|
|
@@ -341,7 +339,7 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
341
339
|
</FormField>
|
|
342
340
|
|
|
343
341
|
<FormField width="wide" :has-gutter="false">
|
|
344
|
-
<
|
|
342
|
+
<InputTextAsNumberField
|
|
345
343
|
v-model="state.guests"
|
|
346
344
|
name="guests"
|
|
347
345
|
label="Guests"
|
|
@@ -353,11 +351,11 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
353
351
|
>
|
|
354
352
|
<template #left><span aria-hidden="true">−</span></template>
|
|
355
353
|
<template #right><span aria-hidden="true">+</span></template>
|
|
356
|
-
</
|
|
354
|
+
</InputTextAsNumberField>
|
|
357
355
|
</FormField>
|
|
358
356
|
|
|
359
357
|
<FormField width="wide" :has-gutter="false">
|
|
360
|
-
<
|
|
358
|
+
<InputTextareaField
|
|
361
359
|
v-model="state.notes"
|
|
362
360
|
name="notes"
|
|
363
361
|
label="Notes"
|
|
@@ -369,7 +367,7 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
369
367
|
</FormField>
|
|
370
368
|
|
|
371
369
|
<FormField width="wide" :has-gutter="false">
|
|
372
|
-
<
|
|
370
|
+
<InputSelectField
|
|
373
371
|
v-model="state.colour"
|
|
374
372
|
v-model:field-data="colourOptions"
|
|
375
373
|
name="colour"
|
|
@@ -382,11 +380,11 @@ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
|
|
|
382
380
|
</FormField>
|
|
383
381
|
|
|
384
382
|
<FormField width="wide" :has-gutter="false">
|
|
385
|
-
<
|
|
383
|
+
<ToggleSwitchField v-model="state.subscribe" name="subscribe" label="Subscribe to updates" />
|
|
386
384
|
</FormField>
|
|
387
385
|
|
|
388
386
|
<FormField width="wide" :has-gutter="false">
|
|
389
|
-
<
|
|
387
|
+
<ToggleSwitchField v-model="state.reminders" name="reminders" label="Email me appointment reminders" layout="inline" />
|
|
390
388
|
</FormField>
|
|
391
389
|
|
|
392
390
|
<FormField width="wide" :has-gutter="false">
|
|
@@ -1,68 +1,98 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ToggleSwitch — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
> **Renamed 2026-10-16**: `ToggleSwitchCore` → `ToggleSwitch`, `ToggleSwitchWithLabel` →
|
|
4
|
+
> `ToggleSwitchField`, `ToggleSwitchWithLabelInline` → `ToggleSwitchField layout="inline"`. Class
|
|
5
|
+
> changes are listed under "State hooks" below.
|
|
2
6
|
|
|
3
7
|
## Public token API
|
|
4
8
|
|
|
5
9
|
Every colour token below is `var(--toggle-switch-*, {default})` — set the `--toggle-switch-*`
|
|
6
10
|
name to override just this component; leave it unset and it inherits the shared `--theme-*` token
|
|
7
11
|
every other themed component also falls back to. See `theming-component-token-pattern.md` for why
|
|
8
|
-
this two-tier shape exists.
|
|
9
|
-
|
|
12
|
+
this two-tier shape exists.
|
|
13
|
+
|
|
14
|
+
### Track and thumb
|
|
10
15
|
|
|
11
|
-
| Token |
|
|
16
|
+
| Token | Default | Controls |
|
|
12
17
|
|---|---|---|
|
|
13
18
|
| `--toggle-switch-surface` | `var(--theme-checkbox-symbol-surface)` | Track background (resting) |
|
|
14
19
|
| `--toggle-switch-surface-hover` | `var(--theme-surface-subtle)` | Track background on hover |
|
|
15
20
|
| `--toggle-switch-border` | `var(--theme-border)` | Track border colour |
|
|
16
21
|
| `--toggle-switch-border-focus` | `var(--theme-border-focus)` | Track outline colour on `:focus-visible` |
|
|
17
22
|
| `--toggle-switch-symbol-border` | `var(--theme-text)` | Thumb border colour |
|
|
18
|
-
| `--toggle-switch-symbol-surface-off` | `var(--theme-on-surface)` | Thumb background,
|
|
19
|
-
| `--toggle-switch-symbol-surface-on` | `var(--theme-surface)` | Thumb background,
|
|
23
|
+
| `--toggle-switch-symbol-surface-off` | `var(--theme-on-surface)` | Thumb background, off |
|
|
24
|
+
| `--toggle-switch-symbol-surface-on` | `var(--theme-surface)` | Thumb background, on |
|
|
20
25
|
| `--toggle-switch-icon-on-color` | `var(--theme-on-surface)` | "On" icon colour |
|
|
21
26
|
| `--toggle-switch-icon-off-color` | `var(--theme-surface)` | "Off" icon colour |
|
|
27
|
+
| `--toggle-switch-border-radius` | `100vw` (`round`), `0.4rem` (`round: false`) | Track and thumb shape |
|
|
28
|
+
| `--toggle-switch-track-width` | derived from the thumb size | Track width (see below) |
|
|
29
|
+
|
|
30
|
+
### Field
|
|
31
|
+
|
|
32
|
+
| Token | Default | Controls |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `--toggle-switch-field-inline-gap` | `1.2rem` | Space between label and switch with `layout="inline"` |
|
|
22
35
|
|
|
23
36
|
Geometry (`--input-toggle-*`, `--form-element-*`) is shared across every `05.forms` component and
|
|
24
37
|
untouched by this — see `theming-form-geometry-tokens.md`.
|
|
25
38
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
`--toggle-switch-border-radius` controls the track and thumb's shared `border-radius`. Left unset,
|
|
29
|
-
it defaults to `100vw` (pill shape) when the `round` prop is `true` (the default), or `0.4rem`
|
|
30
|
-
(a rounded-square shape) when `round` is `false` — see the `SquareShape` story. Set the token
|
|
31
|
-
directly to override with an exact value the two-state default doesn't produce.
|
|
39
|
+
Private (not public API): `--_border-radius` (from `round` and the token above, read by track and
|
|
40
|
+
thumb) and `--_track-width` (the derived width).
|
|
32
41
|
|
|
33
42
|
### Track width
|
|
34
43
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
with `--input-toggle-symbol-size` — resize the thumb and the track resizes to match, no separate
|
|
38
|
-
width to keep in sync:
|
|
44
|
+
Left unset, the track width is worked out from the shared `--input-toggle-*`/`--form-element-*`
|
|
45
|
+
geometry tokens, so it scales with `--input-toggle-symbol-size`:
|
|
39
46
|
|
|
40
47
|
```css
|
|
41
48
|
.my-compact-toggle {
|
|
42
49
|
--input-toggle-symbol-size: 2.2rem;
|
|
43
|
-
/* Track width now follows automatically — no --toggle-switch-track-width override needed. */
|
|
44
50
|
}
|
|
45
51
|
```
|
|
46
52
|
|
|
47
|
-
Only set `--toggle-switch-track-width`
|
|
48
|
-
fixed width independent of symbol size).
|
|
53
|
+
Only set `--toggle-switch-track-width` for a width the formula doesn't produce.
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## State hooks
|
|
58
|
+
|
|
59
|
+
| Attribute | On | When |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `data-checked` | `.toggle-switch` | The value equals `true-value` |
|
|
62
|
+
| `data-invalid` | `.toggle-switch`, `.toggle-switch-field` | `field-has-error` |
|
|
63
|
+
| `data-theme` | `.toggle-switch`, `.toggle-switch-field` | Always; `error` while `field-has-error` is set |
|
|
64
|
+
| `data-layout="stacked" \| "inline"` | `.toggle-switch-field` | Always, mirrors `layout` |
|
|
65
|
+
|
|
66
|
+
Inner elements: `.toggle-switch-wrapper`, `.toggle-switch-input` (the hidden checkbox),
|
|
67
|
+
`.toggle-switch-track`, `.toggle-switch-thumb`, `.toggle-switch-icon`, `.toggle-switch-icon-on`,
|
|
68
|
+
`.toggle-switch-icon-off`, `.toggle-switch-label` (the Field's `InputLabel`),
|
|
69
|
+
`.toggle-switch-description` (the Field's `InputDescription`).
|
|
70
|
+
|
|
71
|
+
The thumb slide is turned off under `prefers-reduced-motion: reduce`.
|
|
72
|
+
|
|
73
|
+
> **Changed 2026-10-16**: inner classes were generic names that a consumer's own CSS could hit,
|
|
74
|
+
> and state was a class. Use the new names:
|
|
75
|
+
>
|
|
76
|
+
> - `.toggle-switch-core` → `.toggle-switch`; `.toggle-switch-with-label` → `.toggle-switch-field`;
|
|
77
|
+
> `.toggle-switch-with-label-inline` → `.toggle-switch-field[data-layout="inline"]`
|
|
78
|
+
> - `.symbol-wrapper` → `.toggle-switch-track`; `.symbol` → `.toggle-switch-thumb`;
|
|
79
|
+
> `.symbol-icon` → `.toggle-switch-icon`; `.icon-on` / `.icon-off` → `.toggle-switch-icon-on` /
|
|
80
|
+
> `.toggle-switch-icon-off`; the bare `input` → `.toggle-switch-input`
|
|
81
|
+
> - `.symbol.checked` and `.symbol-icon.active` → `.toggle-switch[data-checked]`
|
|
82
|
+
> - the unused `.use-default-icons` class and the default icon's `.icon` class are gone
|
|
83
|
+
> - the Field's label class `input-switch-label` → `toggle-switch-label`
|
|
84
|
+
>
|
|
85
|
+
> Also: the inline layout's label wraps instead of `nowrap`, and the inline row now supports a
|
|
86
|
+
> description, an error and `required`.
|
|
55
87
|
|
|
56
88
|
---
|
|
57
89
|
|
|
58
|
-
## Description
|
|
90
|
+
## Description
|
|
59
91
|
|
|
60
|
-
`
|
|
92
|
+
`ToggleSwitchField`'s `descriptionText` / `descriptionHtml` slots render through
|
|
61
93
|
`InputDescription`, so they're styled by its `--input-description-*` tokens (see
|
|
62
|
-
`input-description/CONSUMER-STYLING.md`)
|
|
63
|
-
|
|
64
|
-
`--input-description-slot-margin-block-end` (default `0.8rem`). Scope an override to toggles only
|
|
65
|
-
with the `.toggle-switch-description` class it adds:
|
|
94
|
+
`input-description/CONSUMER-STYLING.md`). Scope an override to toggles with
|
|
95
|
+
`.toggle-switch-description`:
|
|
66
96
|
|
|
67
97
|
```css
|
|
68
98
|
.toggle-switch-description {
|
|
@@ -70,6 +100,8 @@ with the `.toggle-switch-description` class it adds:
|
|
|
70
100
|
}
|
|
71
101
|
```
|
|
72
102
|
|
|
103
|
+
---
|
|
104
|
+
|
|
73
105
|
## Global theming
|
|
74
106
|
|
|
75
107
|
```css
|
|
@@ -92,28 +124,17 @@ Set the tokens above on an element you own (a page or section class, or a class
|
|
|
92
124
|
### One instance
|
|
93
125
|
|
|
94
126
|
```vue
|
|
95
|
-
<
|
|
127
|
+
<ToggleSwitchField
|
|
128
|
+
v-model="notifications"
|
|
129
|
+
name="notifications"
|
|
130
|
+
label="Notifications"
|
|
131
|
+
style="--toggle-switch-symbol-surface-on: var(--gold-06);"
|
|
132
|
+
/>
|
|
96
133
|
```
|
|
97
134
|
|
|
98
135
|
---
|
|
99
136
|
|
|
100
|
-
##
|
|
101
|
-
|
|
102
|
-
- **`round` prop was previously dead** — `ToggleSwitchWithLabel`/`ToggleSwitchWithLabelInline`
|
|
103
|
-
both accepted and forwarded it, but `ToggleSwitchCore` never declared the prop or read it, so
|
|
104
|
-
the Storybook `SquareShape` story rendered identically to `Default`. Fixed by adding `round` to
|
|
105
|
-
`ToggleSwitchCore` and wiring it to the new `--toggle-switch-border-radius` token (see Shape
|
|
106
|
-
above).
|
|
107
|
-
- **`fieldHasError` didn't visually theme the component** — `ToggleSwitchCore` and
|
|
108
|
-
`ToggleSwitchWithLabel` both computed an error-aware theme value but bound `data-theme` to the
|
|
109
|
-
raw `theme` prop instead, so setting `fieldHasError` never applied this library's
|
|
110
|
-
`[data-theme="error"]` theme-token overrides (`_error.css`). Fixed by binding `data-theme` to the
|
|
111
|
-
computed value.
|
|
112
|
-
- Custom `iconOn`/`iconOff` slot content could render as a non-square box (e.g. a 16×16 SVG
|
|
113
|
-
rendering as ~16×20), throwing off centering inside the thumb — fixed by adding
|
|
114
|
-
`line-height: 0` to `.symbol-icon`, which collapses inline leading for any slotted content.
|
|
115
|
-
- Two dead, unread `--_transition-duration` private locals were removed from
|
|
116
|
-
`ToggleSwitchWithLabel`/`ToggleSwitchWithLabelInline` — nothing in either file's CSS or
|
|
117
|
-
`ToggleSwitchCore`'s ever read them (the actual transition durations are hardcoded `0.4s`
|
|
118
|
-
literals in `ToggleSwitchCore`).
|
|
137
|
+
## Class passthrough
|
|
119
138
|
|
|
139
|
+
`ToggleSwitch`'s `:style-class-passthrough` lands on its root `.toggle-switch`, and
|
|
140
|
+
`ToggleSwitchField`'s on its root `.toggle-switch-field`.
|