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.
Files changed (108) hide show
  1. package/.claude/skills/component-compliance-checklist.md +12 -1
  2. package/.claude/skills/component-inline-action-button.md +1 -1
  3. package/.claude/skills/component-local-style-override.md +1 -1
  4. package/.claude/skills/component-naming.md +11 -11
  5. package/.claude/skills/components/container-glow.md +2 -2
  6. package/.claude/skills/components/form-field.md +2 -2
  7. package/.claude/skills/components/form-wrapper.md +1 -1
  8. package/.claude/skills/components/glowing-border.md +1 -1
  9. package/.claude/skills/components/input-description.md +7 -7
  10. package/.claude/skills/components/input-error.md +3 -3
  11. package/.claude/skills/components/input-label.md +7 -8
  12. package/.claude/skills/components/input-number.md +17 -17
  13. package/.claude/skills/components/input-select.md +153 -0
  14. package/.claude/skills/components/input-text.md +220 -0
  15. package/.claude/skills/components/input-textarea.md +147 -0
  16. package/.claude/skills/components/select-menu.md +4 -4
  17. package/.claude/skills/components/toggle-switch.md +143 -0
  18. package/.claude/skills/composable-zod-validation.md +1 -1
  19. package/.claude/skills/icon-sets.md +1 -1
  20. package/.claude/skills/icon-sizing.md +1 -1
  21. package/.claude/skills/index.md +5 -5
  22. package/.claude/skills/storybook-add-story.md +1 -1
  23. package/.claude/skills/theming-component-token-pattern.md +8 -8
  24. package/.claude/skills/theming-form-geometry-tokens.md +1 -1
  25. package/.vscode/srcdev-component-input-select.code-snippets +20 -14
  26. package/.vscode/srcdev-component-input-text.code-snippets +46 -40
  27. package/.vscode/srcdev-component-input-textarea.code-snippets +19 -14
  28. package/.vscode/srcdev-component-toggle-switch.code-snippets +19 -29
  29. package/app/assets/styles/setup/04.elements/forms/00.element-defaults.css +2 -2
  30. package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +1 -1
  31. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +2 -2
  32. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +4 -4
  33. package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +4 -4
  34. package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +1 -1
  35. package/app/components/05.forms/form-field/CONSUMER-STYLING.md +2 -2
  36. package/app/components/05.forms/input-button/stories/InputButton.stories.ts +3 -3
  37. package/app/components/05.forms/input-description/CONSUMER-STYLING.md +6 -6
  38. package/app/components/05.forms/input-label/CONSUMER-STYLING.md +5 -5
  39. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +9 -5
  40. package/app/components/05.forms/input-number/InputNumber.vue +2 -2
  41. package/app/components/05.forms/input-select/CONSUMER-STYLING.md +80 -10
  42. package/app/components/05.forms/input-select/InputSelect.vue +198 -0
  43. package/app/components/05.forms/input-select/InputSelectField.vue +130 -0
  44. package/app/components/05.forms/input-select/stories/InputSelect.stories.ts +184 -0
  45. package/app/components/05.forms/input-select/stories/InputSelectField.stories.ts +187 -0
  46. package/app/components/05.forms/input-select/tests/{InputSelectCore.spec.ts → InputSelect.spec.ts} +75 -12
  47. package/app/components/05.forms/input-select/tests/InputSelectField.spec.ts +121 -0
  48. package/app/components/05.forms/input-text/CONSUMER-STYLING.md +85 -15
  49. package/app/components/05.forms/input-text/{InputPasswordWithLabel.vue → InputPasswordField.vue} +16 -19
  50. package/app/components/05.forms/input-text/{InputTextCore.vue → InputText.vue} +42 -78
  51. package/app/components/05.forms/input-text/{InputTextAsNumberWithLabel.vue → InputTextAsNumberField.vue} +66 -62
  52. package/app/components/05.forms/input-text/InputTextField.vue +168 -0
  53. package/app/components/05.forms/input-text/stories/InputPasswordField.stories.ts +163 -0
  54. package/app/components/05.forms/input-text/stories/InputText.stories.ts +194 -0
  55. package/app/components/05.forms/input-text/stories/InputTextAsNumberField.stories.ts +182 -0
  56. package/app/components/05.forms/input-text/stories/InputTextField.stories.ts +206 -0
  57. package/app/components/05.forms/input-text/tests/{InputPasswordWithLabel.spec.ts → InputPasswordField.spec.ts} +42 -3
  58. package/app/components/05.forms/input-text/tests/InputText.spec.ts +166 -0
  59. package/app/components/05.forms/input-text/tests/InputTextAsNumberField.spec.ts +135 -0
  60. package/app/components/05.forms/input-text/tests/InputTextField.spec.ts +130 -0
  61. package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +94 -12
  62. package/app/components/05.forms/input-textarea/InputTextarea.vue +165 -0
  63. package/app/components/05.forms/input-textarea/InputTextareaField.vue +159 -0
  64. package/app/components/05.forms/input-textarea/stories/InputTextarea.stories.ts +198 -0
  65. package/app/components/05.forms/input-textarea/stories/InputTextareaField.stories.ts +161 -0
  66. package/app/components/05.forms/input-textarea/tests/InputTextarea.spec.ts +149 -0
  67. package/app/components/05.forms/input-textarea/tests/InputTextareaField.spec.ts +122 -0
  68. package/app/components/05.forms/patterns/stories/ContactForm.stories.ts +13 -13
  69. package/app/components/05.forms/patterns/stories/LoginForm.stories.ts +6 -6
  70. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +30 -32
  71. package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +71 -50
  72. package/app/components/05.forms/toggle-switch/{ToggleSwitchCore.vue → ToggleSwitch.vue} +58 -73
  73. package/app/components/05.forms/toggle-switch/{ToggleSwitchWithLabel.vue → ToggleSwitchField.vue} +65 -19
  74. package/app/components/05.forms/toggle-switch/stories/ToggleSwitch.stories.ts +148 -0
  75. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchField.stories.ts +176 -0
  76. package/app/components/05.forms/toggle-switch/tests/ToggleSwitch.spec.ts +145 -0
  77. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchField.spec.ts +155 -0
  78. package/package.json +1 -1
  79. package/.claude/skills/components/input-select-core.md +0 -151
  80. package/.claude/skills/components/input-text-core.md +0 -204
  81. package/.claude/skills/components/input-textarea-core.md +0 -154
  82. package/.claude/skills/components/toggle-switch-core.md +0 -169
  83. package/app/components/05.forms/input-select/InputSelectCore.vue +0 -188
  84. package/app/components/05.forms/input-select/InputSelectWithLabel.vue +0 -118
  85. package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +0 -69
  86. package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +0 -115
  87. package/app/components/05.forms/input-select/tests/InputSelectWithLabel.spec.ts +0 -87
  88. package/app/components/05.forms/input-text/InputTextWithLabel.vue +0 -154
  89. package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +0 -368
  90. package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +0 -480
  91. package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +0 -267
  92. package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +0 -319
  93. package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +0 -75
  94. package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +0 -60
  95. package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +0 -71
  96. package/app/components/05.forms/input-textarea/InputTextareaCore.vue +0 -159
  97. package/app/components/05.forms/input-textarea/InputTextareaWithLabel.vue +0 -136
  98. package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +0 -247
  99. package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +0 -458
  100. package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +0 -119
  101. package/app/components/05.forms/input-textarea/tests/InputTextareaWithLabel.spec.ts +0 -93
  102. package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabelInline.vue +0 -81
  103. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +0 -251
  104. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +0 -251
  105. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +0 -283
  106. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +0 -101
  107. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabel.spec.ts +0 -133
  108. 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 InputTextWithLabel from "../../input-text/InputTextWithLabel.vue";
4
- import InputSelectWithLabel from "../../input-select/InputSelectWithLabel.vue";
5
- import InputTextareaWithLabel from "../../input-textarea/InputTextareaWithLabel.vue";
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 InputSelectWithLabel
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
- InputTextWithLabel,
40
- InputSelectWithLabel,
41
- InputTextareaWithLabel,
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
- <InputTextWithLabel
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
- <InputTextWithLabel
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
- <InputTextWithLabel
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
- <InputSelectWithLabel
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
- <InputTextareaWithLabel
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
- </InputTextareaWithLabel>
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 InputTextWithLabel from "../../input-text/InputTextWithLabel.vue";
4
- import InputPasswordWithLabel from "../../input-text/InputPasswordWithLabel.vue";
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
- InputTextWithLabel,
39
- InputPasswordWithLabel,
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
- <InputTextWithLabel
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
- <InputPasswordWithLabel
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 InputTextWithLabel from "../../input-text/InputTextWithLabel.vue";
4
- import InputPasswordWithLabel from "../../input-text/InputPasswordWithLabel.vue";
5
- import InputTextAsNumberWithLabel from "../../input-text/InputTextAsNumberWithLabel.vue";
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 InputTextareaWithLabel from "../../input-textarea/InputTextareaWithLabel.vue";
10
- import InputSelectWithLabel from "../../input-select/InputSelectWithLabel.vue";
11
- import ToggleSwitchWithLabel from "../../toggle-switch/ToggleSwitchWithLabel.vue";
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: InputTextWithLabel (InputTextCore; plus one per type/inputmode pair for checking
31
- // on-device keyboards), InputPasswordWithLabel,
32
- // InputTextAsNumberWithLabel, InputRangeField (InputRange), InputNumberField
33
- // (InputNumber), InputOtpField (InputOtp), InputTextareaWithLabel (InputTextareaCore), InputSelectWithLabel
34
- // (InputSelectCore), ToggleSwitchWithLabel and ToggleSwitchWithLabelInline (ToggleSwitchCore),
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
- InputTextWithLabel,
64
- InputPasswordWithLabel,
65
- InputTextAsNumberWithLabel,
62
+ InputTextField,
63
+ InputPasswordField,
64
+ InputTextAsNumberField,
66
65
  InputRangeField,
67
66
  InputNumberField,
68
67
  InputOtpField,
69
- InputTextareaWithLabel,
70
- InputSelectWithLabel,
71
- ToggleSwitchWithLabel,
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 InputTextWithLabel per type/inputmode pair, for checking each on-screen keyboard on a real device.
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
- <InputTextWithLabel
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
- </InputTextWithLabel>
264
+ </InputTextField>
267
265
  </FormField>
268
266
 
269
267
  <FormField width="wide" :has-gutter="false">
270
- <InputPasswordWithLabel
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
- </InputPasswordWithLabel>
278
+ </InputPasswordField>
281
279
  </FormField>
282
280
 
283
281
  <FormField v-for="variant in textVariants" :key="variant.name" width="wide" :has-gutter="false">
284
- <InputTextWithLabel
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
- </InputTextWithLabel>
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
- <InputTextAsNumberWithLabel
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
- </InputTextAsNumberWithLabel>
354
+ </InputTextAsNumberField>
357
355
  </FormField>
358
356
 
359
357
  <FormField width="wide" :has-gutter="false">
360
- <InputTextareaWithLabel
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
- <InputSelectWithLabel
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
- <ToggleSwitchWithLabel v-model="state.subscribe" name="subscribe" label="Subscribe to updates" />
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
- <ToggleSwitchWithLabelInline v-model="state.reminders" name="reminders" label="Email me appointment reminders" />
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
- # ToggleSwitchCore — Consumer Styling Guide
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. Defaults are unchanged from before this migration — this is a
9
- token-rename pass, not a redesign.
12
+ this two-tier shape exists.
13
+
14
+ ### Track and thumb
10
15
 
11
- | Token | Falls back to | Controls |
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, unchecked |
19
- | `--toggle-switch-symbol-surface-on` | `var(--theme-surface)` | Thumb background, checked |
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
- ### Shape (`round` prop)
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
- `--toggle-switch-track-width` overrides the track's width directly. Left unset, the track derives
36
- its width from the shared `--input-toggle-*`/`--form-element-*` geometry tokens so it always scales
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` if you need a width the formula doesn't produce (e.g. a
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
- > Five private locals (`--_icon-on-opacity`, `--_icon-off-opacity`, `--_symbol-background-color`,
51
- > `--_symbol-margin-inline-start`, `--_symbol-checked-offset`) were removed during this migration —
52
- > declared but never actually read by any property in the component, confirmed by grep. Not part
53
- > of any public API, so nothing to update if you were relying on them (you weren't — they had zero
54
- > effect).
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 (ToggleSwitchWithLabel)
90
+ ## Description
59
91
 
60
- `ToggleSwitchWithLabel`'s `descriptionText` / `descriptionHtml` slots render through
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`), the same as every other labelled form field. The
63
- spacing between the description and the switch is
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
- <ToggleSwitchCore id="notifications" name="notifications" style="--toggle-switch-symbol-surface-on: var(--gold-06);" />
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
- ## Migration notes (2026-09-23)
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`.