srcdev-nuxt-components 9.4.9 → 9.4.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.
Files changed (178) hide show
  1. package/.claude/commands/migrate-component.md +38 -6
  2. package/.claude/component-ledger/audit.json +1 -1
  3. package/.claude/component-ledger/build.mjs +26 -0
  4. package/.claude/component-ledger/output.html +8 -3
  5. package/.claude/component-ledger/template.html +7 -2
  6. package/.claude/skills/component-dynamic-slots.md +1 -1
  7. package/.claude/skills/components/alert-mask-core.md +84 -0
  8. package/.claude/skills/components/canvas-switcher.md +1 -1
  9. package/.claude/skills/components/carousel-flip.md +112 -168
  10. package/.claude/skills/components/container-glow.md +123 -0
  11. package/.claude/skills/components/dashboard-quad-grid.md +98 -0
  12. package/.claude/skills/components/dashboard-stats-grid.md +103 -0
  13. package/.claude/skills/components/display-theme-switch.md +60 -0
  14. package/.claude/skills/components/glowing-border.md +66 -0
  15. package/.claude/skills/components/input-number-core.md +214 -0
  16. package/.claude/skills/components/input-range-core.md +142 -0
  17. package/.claude/skills/components/input-select-core.md +151 -0
  18. package/.claude/skills/components/input-text-core.md +16 -0
  19. package/.claude/skills/components/input-textarea-core.md +154 -0
  20. package/.claude/skills/components/section-parallax.md +32 -10
  21. package/.claude/skills/components/select-menu.md +195 -0
  22. package/.claude/skills/components/toggle-switch-core.md +150 -0
  23. package/.claude/skills/components/ui-block-decorated.md +56 -0
  24. package/.claude/skills/components/wipe-away-vertical.md +98 -0
  25. package/.claude/skills/icon-sets.md +1 -1
  26. package/.claude/skills/index.md +15 -1
  27. package/.vscode/css-custom-data.json +38 -0
  28. package/.vscode/settings.json +3 -0
  29. package/.vscode/srcdev-component-alert-mask-core.code-snippets +31 -0
  30. package/.vscode/srcdev-component-carousel-flip.code-snippets +41 -0
  31. package/.vscode/srcdev-component-container-glow.code-snippets +33 -0
  32. package/.vscode/srcdev-component-dashboard-quad-grid.code-snippets +36 -0
  33. package/.vscode/srcdev-component-dashboard-stats-grid.code-snippets +35 -0
  34. package/.vscode/srcdev-component-display-theme-switch.code-snippets +27 -0
  35. package/.vscode/srcdev-component-glowing-border.code-snippets +23 -0
  36. package/.vscode/srcdev-component-input-number.code-snippets +65 -0
  37. package/.vscode/srcdev-component-input-range.code-snippets +85 -0
  38. package/.vscode/srcdev-component-input-select.code-snippets +42 -0
  39. package/.vscode/srcdev-component-input-textarea.code-snippets +40 -0
  40. package/.vscode/srcdev-component-section-parallax.code-snippets +27 -0
  41. package/.vscode/srcdev-component-select-menu.code-snippets +65 -0
  42. package/.vscode/srcdev-component-toggle-switch.code-snippets +49 -0
  43. package/.vscode/srcdev-component-ui-block-decorated.code-snippets +24 -0
  44. package/.vscode/srcdev-component-wipe-away-vertical.code-snippets +25 -0
  45. package/README.md +18 -37
  46. package/app/components/{alert-mask → 01.atoms/alert-mask}/AlertMaskCore.vue +16 -17
  47. package/app/components/01.atoms/alert-mask/CONSUMER-STYLING.md +46 -0
  48. package/app/components/01.atoms/alert-mask/stories/AlertMaskCore.stories.ts +79 -0
  49. package/app/components/{alert-mask → 01.atoms/alert-mask}/tests/AlertMaskCore.spec.ts +10 -10
  50. package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +86 -0
  51. package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +299 -0
  52. package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +175 -0
  53. package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +131 -0
  54. package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +14 -0
  55. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +61 -0
  56. package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +132 -0
  57. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +165 -0
  58. package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +94 -0
  59. package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +3 -0
  60. package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +1 -1
  61. package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +1 -1
  62. package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +1 -1
  63. package/app/components/01.atoms/animations/section-parallax/CONSUMER-STYLING.md +44 -0
  64. package/app/components/{parallax → 01.atoms/animations/section-parallax}/SectionParallax.vue +11 -10
  65. package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +98 -0
  66. package/app/components/01.atoms/animations/section-parallax/tests/SectionParallax.spec.ts +102 -0
  67. package/app/components/01.atoms/animations/section-parallax/tests/__snapshots__/SectionParallax.spec.ts.snap +9 -0
  68. package/app/components/01.atoms/animations/view-timeline/CONSUMER-STYLING.md +70 -0
  69. package/app/components/01.atoms/animations/view-timeline/WipeAwayVertical.vue +181 -0
  70. package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +269 -0
  71. package/app/components/01.atoms/animations/view-timeline/tests/WipeAwayVertical.spec.ts +127 -0
  72. package/app/components/01.atoms/canvas-switcher/CONSUMER-STYLING.md +3 -3
  73. package/app/components/01.atoms/canvas-switcher/CanvasSwitcher.vue +2 -2
  74. package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +1 -1
  75. package/app/components/01.atoms/card/CardCore.vue +4 -4
  76. package/app/components/01.atoms/clipped-panel/CONSUMER-STYLING.md +2 -2
  77. package/app/components/01.atoms/clipped-panel/ClippedPanel.vue +2 -2
  78. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +42 -0
  79. package/app/components/{layout-grids/LayoutGridA.vue → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue} +23 -13
  80. package/app/components/{layout-grids/stories/LayoutGridA.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts} +151 -23
  81. package/app/components/{layout-grids/tests/LayoutGridA.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/tests/DashboardQuadGrid.spec.ts} +25 -25
  82. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +41 -0
  83. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +133 -0
  84. package/app/components/{layout-grids/stories/LayoutGridB.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/stories/DashboardStatsGrid.stories.ts} +24 -24
  85. package/app/components/{layout-grids/tests/LayoutGridB.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/tests/DashboardStatsGrid.spec.ts} +23 -23
  86. package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +8 -8
  87. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +8 -8
  88. package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +6 -6
  89. package/app/components/01.atoms/pop-over/PopOver.vue +6 -6
  90. package/app/components/01.atoms/ui-block-decorated/CONSUMER-STYLING.md +39 -0
  91. package/app/components/01.atoms/ui-block-decorated/UiBlockDecorated.vue +141 -0
  92. package/app/components/01.atoms/ui-block-decorated/stories/UiBlockDecorated.stories.ts +148 -0
  93. package/app/components/01.atoms/ui-block-decorated/tests/UiBlockDecorated.spec.ts +80 -0
  94. package/app/components/01.atoms/ui-block-decorated/tests/__snapshots__/UiBlockDecorated.spec.ts.snap +3 -0
  95. package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +6 -6
  96. package/app/components/02.molecules/display-theme-switch/CONSUMER-STYLING.md +34 -0
  97. package/app/components/02.molecules/display-theme-switch/DisplayThemeSwitch.vue +102 -0
  98. package/app/components/02.molecules/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +115 -0
  99. package/app/components/02.molecules/display-theme-switch/tests/DisplayThemeSwitch.spec.ts +86 -0
  100. package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +1 -1
  101. package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +1 -1
  102. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +1 -1
  103. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +1 -1
  104. package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +168 -0
  105. package/app/components/02.molecules/select-menu/SelectMenu.vue +384 -0
  106. package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +359 -0
  107. package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +476 -0
  108. package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +42 -0
  109. package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +46 -0
  110. package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/CarouselFlip.vue +63 -42
  111. package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/stories/CarouselFlip.stories.ts +114 -29
  112. package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/tests/CarouselFlip.spec.ts +80 -0
  113. package/app/components/05.forms/form-field/FormField.vue +1 -1
  114. package/app/components/05.forms/form-wrapper/FormWrapper.vue +1 -1
  115. package/app/components/05.forms/input-button/CONSUMER-STYLING.md +2 -7
  116. package/app/components/05.forms/input-button/InputButtonCore.vue +1 -5
  117. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +26 -6
  118. package/app/components/05.forms/input-number/InputNumberCore.vue +125 -85
  119. package/app/components/05.forms/input-number/stories/InputNumberCore.stories.ts +92 -0
  120. package/app/components/05.forms/input-number/tests/InputNumberCore.spec.ts +119 -0
  121. package/app/components/05.forms/input-number/variants/InputNumberDefault.vue +30 -22
  122. package/app/components/05.forms/input-number/variants/stories/InputNumberDefault.stories.ts +123 -0
  123. package/app/components/05.forms/input-number/variants/tests/InputNumberDefault.spec.ts +143 -0
  124. package/app/components/05.forms/input-range/CONSUMER-STYLING.md +45 -0
  125. package/app/components/05.forms/input-range/InputRangeCore.vue +79 -143
  126. package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +126 -0
  127. package/app/components/05.forms/input-range/tests/InputRangeCore.spec.ts +124 -0
  128. package/app/components/05.forms/input-range/variants/InputRangeDefault.vue +9 -32
  129. package/app/components/05.forms/input-range/variants/stories/InputRangeDefault.stories.ts +108 -0
  130. package/app/components/05.forms/input-range/variants/tests/InputRangeDefault.spec.ts +140 -0
  131. package/app/components/05.forms/input-select/InputSelectCore.vue +23 -30
  132. package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +2 -2
  133. package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +115 -0
  134. package/app/components/05.forms/input-select/tests/InputSelectCore.spec.ts +129 -0
  135. package/app/components/05.forms/input-select/variants/InputSelectWithLabel.vue +1 -1
  136. package/app/components/05.forms/input-select/variants/tests/InputSelectWithLabel.spec.ts +87 -0
  137. package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +14 -0
  138. package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +1 -1
  139. package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +55 -0
  140. package/app/components/05.forms/input-textarea/InputTextareaCore.vue +24 -16
  141. package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +1 -1
  142. package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +119 -0
  143. package/app/components/05.forms/input-textarea/variants/InputTextareaWithLabel.vue +3 -13
  144. package/app/components/05.forms/input-textarea/variants/tests/InputTextareaWithLabel.spec.ts +93 -0
  145. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +220 -0
  146. package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +29 -0
  147. package/app/components/05.forms/toggle-switch/ToggleSwitchCore.vue +120 -119
  148. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +1 -1
  149. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +1 -1
  150. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +2 -2
  151. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +101 -0
  152. package/app/components/05.forms/toggle-switch/variants/ToggleSwitchWithLabel.vue +3 -5
  153. package/app/components/05.forms/toggle-switch/variants/ToggleSwitchWithLabelInline.vue +1 -2
  154. package/app/components/05.forms/toggle-switch/variants/tests/ToggleSwitchWithLabel.spec.ts +77 -0
  155. package/app/components/05.forms/toggle-switch/variants/tests/ToggleSwitchWithLabelInline.spec.ts +66 -0
  156. package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +20 -6
  157. package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitchCore.vue +12 -10
  158. package/app/types/components/container-glow.d.ts +8 -0
  159. package/app/types/components/index.ts +2 -1
  160. package/app/types/components/select-menu.d.ts +5 -0
  161. package/package.json +3 -2
  162. package/.vscode/srcdev-component-toggle-switch-core.code-snippets +0 -13
  163. package/app/components/05.forms/input-range-fancy/InputRangeFancyCore.vue +0 -426
  164. package/app/components/05.forms/input-range-fancy/InputRangeFancyWithLabel.vue +0 -94
  165. package/app/components/carousels/CarouselBasic.vue +0 -317
  166. package/app/components/carousels/CarouselInfinite.vue +0 -358
  167. package/app/components/container-glow/ContainerGlowCore.vue +0 -296
  168. package/app/components/container-glow/stories/ContainerGlowCore.stories.ts +0 -337
  169. package/app/components/display-theme-switch/DisplayThemeSwitch.vue +0 -189
  170. package/app/components/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +0 -154
  171. package/app/components/glowing-border/GlowingBorder.vue +0 -142
  172. package/app/components/glowing-border/stories/GlowingBorder.stories.ts +0 -141
  173. package/app/components/layout-grids/LayoutGridB.vue +0 -134
  174. package/app/components/test-storybook/TestStorybook.vue +0 -49
  175. package/app/components/test-storybook/stories/TestStorybook.stories.ts +0 -28
  176. package/app/components/ui-block-decorated/UiBlockDecorated.vue +0 -167
  177. package/app/components/view-timeline/WipeAwayVertical.vue +0 -180
  178. package/app/types/components/carousel-basic.d.ts +0 -19
@@ -0,0 +1,220 @@
1
+ import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
+ import { reactive } from "vue";
3
+ import InputTextWithLabel from "../../input-text/variants/InputTextWithLabel.vue";
4
+ import InputRangeDefault from "../../input-range/variants/InputRangeDefault.vue";
5
+ import InputNumberDefault from "../../input-number/variants/InputNumberDefault.vue";
6
+ import InputTextareaWithLabel from "../../input-textarea/variants/InputTextareaWithLabel.vue";
7
+ import InputSelectWithLabel from "../../input-select/variants/InputSelectWithLabel.vue";
8
+ import ToggleSwitchWithLabel from "../../toggle-switch/variants/ToggleSwitchWithLabel.vue";
9
+ import InputButtonCore from "../../input-button/InputButtonCore.vue";
10
+ import FormField from "../../form-field/FormField.vue";
11
+ import HeroText from "../../../01.atoms/text-blocks/hero-text/HeroText.vue";
12
+ import type { InputUiVariant, IFormMultipleOptions } from "~/types/forms/types.forms.d";
13
+
14
+ interface MigratedFieldsFormStoryArgs {
15
+ inputVariant: InputUiVariant;
16
+ }
17
+
18
+ // Living reference, not a component of its own — one field per 05.forms component group that
19
+ // currently scores 5/5 in the Component Ledger (.claude/component-ledger/audit.json: tier folder,
20
+ // tests, story, skill doc, CONSUMER-STYLING.md, VS Code snippet). As of 2026-09-23 that's
21
+ // InputTextCore (via InputTextWithLabel), InputRangeCore (via InputRangeDefault), InputNumberCore
22
+ // (via InputNumberDefault), InputTextareaCore (via InputTextareaWithLabel), InputSelectCore
23
+ // (via InputSelectWithLabel), and ToggleSwitchCore (via ToggleSwitchWithLabel) — every other
24
+ // 05.forms component (checkbox, radio, ...) is still mid-migration.
25
+ // Add a field here each time /migrate-component brings another 05.forms component up to 5/5, so
26
+ // this story doubles as a visible migration-progress tracker rather than living only in the
27
+ // ledger's HTML output. No validation wiring (useZodValidation/zod) here — that's the consuming
28
+ // app's concern, this is a visual/composition reference only.
29
+ export default {
30
+ title: "Patterns/Migrated Fields Form",
31
+ argTypes: {
32
+ inputVariant: {
33
+ control: { type: "select" },
34
+ options: ["normal", "outlined", "underlined"],
35
+ description: "Applied to every text-like field in the form at once",
36
+ table: { category: "Styling" },
37
+ },
38
+ },
39
+ args: {
40
+ inputVariant: "normal",
41
+ },
42
+ } as Meta<MigratedFieldsFormStoryArgs>;
43
+
44
+ const Template: StoryFn<MigratedFieldsFormStoryArgs> = (args) => ({
45
+ components: {
46
+ InputTextWithLabel,
47
+ InputRangeDefault,
48
+ InputNumberDefault,
49
+ InputTextareaWithLabel,
50
+ InputSelectWithLabel,
51
+ ToggleSwitchWithLabel,
52
+ InputButtonCore,
53
+ FormField,
54
+ HeroText,
55
+ },
56
+ setup() {
57
+ const state = reactive({
58
+ fullName: "",
59
+ budget: 50,
60
+ quantity: 1,
61
+ notes: "",
62
+ colour: "",
63
+ subscribe: false,
64
+ });
65
+
66
+ const colourOptions = reactive<IFormMultipleOptions>({
67
+ data: [
68
+ { id: "1", name: "red", value: "red", label: "Red" },
69
+ { id: "2", name: "blue", value: "blue", label: "Blue" },
70
+ { id: "3", name: "green", value: "green", label: "Green" },
71
+ ],
72
+ total: 3,
73
+ skip: 0,
74
+ limit: 10,
75
+ });
76
+
77
+ // Demo-only "validation" — a real consuming app would wire this through something like
78
+ // useZodValidation instead (see this file's top comment). Kept deliberately simple so the
79
+ // Continue button has an obvious, reliable way to trigger each field's error state for
80
+ // exercising InputTextWithLabel/InputRangeDefault/InputNumberDefault/InputTextareaWithLabel/
81
+ // InputSelectWithLabel's error UI in Storybook.
82
+ const errors = reactive({
83
+ fullName: "",
84
+ budget: "",
85
+ quantity: "",
86
+ notes: "",
87
+ colour: "",
88
+ });
89
+
90
+ const validate = () => {
91
+ errors.fullName = state.fullName.trim() ? "" : "Full name is required";
92
+ errors.budget = state.budget >= 100 ? "" : "Budget must be at least £100";
93
+ errors.quantity = state.quantity >= 1 ? "" : "Quantity must be at least 1";
94
+ errors.notes = state.notes.trim() ? "" : "Notes are required";
95
+ errors.colour = state.colour ? "" : "Please choose a colour";
96
+ };
97
+
98
+ const clearErrors = () => {
99
+ errors.fullName = "";
100
+ errors.budget = "";
101
+ errors.quantity = "";
102
+ errors.notes = "";
103
+ errors.colour = "";
104
+ };
105
+
106
+ return { args, state, errors, validate, clearErrors, colourOptions };
107
+ },
108
+ template: `
109
+ <div style="margin: 36px; max-width: 480px;">
110
+ <HeroText
111
+ tag="h2"
112
+ font-size="heading"
113
+ :text-content="[{ text: 'Migrated fields', styleClass: 'normal' }]"
114
+ :style-class-passthrough="['mbe-20']"
115
+ />
116
+ <p style="margin: 0 0 2rem 0; color: #475569; font-size: 1.4rem;">
117
+ One field per 05.forms component that's fully migrated (5/5 on the Component Ledger).
118
+ Click Continue with an empty name, a budget under £100, a quantity under 1, empty notes,
119
+ or no colour chosen to see the error states.
120
+ </p>
121
+
122
+ <form novalidate @submit.prevent="validate">
123
+ <FormField width="wide" :has-gutter="false">
124
+ <InputTextWithLabel
125
+ v-model="state.fullName"
126
+ type="text"
127
+ name="fullName"
128
+ label="Full name"
129
+ placeholder="eg. Jane Smith"
130
+ :error-message="errors.fullName"
131
+ :field-has-error="!!errors.fullName"
132
+ :required="true"
133
+ :input-variant="args.inputVariant"
134
+ />
135
+ </FormField>
136
+
137
+ <FormField width="wide" :has-gutter="false">
138
+ <InputRangeDefault
139
+ v-model="state.budget"
140
+ name="budget"
141
+ label="Budget (£)"
142
+ :min="0"
143
+ :max="500"
144
+ :step="10"
145
+ :error-message="errors.budget"
146
+ :field-has-error="!!errors.budget"
147
+ >
148
+ <template #left><span aria-hidden="true">−</span></template>
149
+ <template #right><span aria-hidden="true">+</span></template>
150
+ </InputRangeDefault>
151
+ </FormField>
152
+
153
+ <FormField width="wide" :has-gutter="false">
154
+ <InputNumberDefault
155
+ v-model="state.quantity"
156
+ name="quantity"
157
+ label="Quantity"
158
+ :min="1"
159
+ :max="10"
160
+ :error-message="errors.quantity"
161
+ :field-has-error="!!errors.quantity"
162
+ :input-variant="args.inputVariant"
163
+ >
164
+ <template #left><span aria-hidden="true">−</span></template>
165
+ <template #right><span aria-hidden="true">+</span></template>
166
+ </InputNumberDefault>
167
+ </FormField>
168
+
169
+ <FormField width="wide" :has-gutter="false">
170
+ <InputTextareaWithLabel
171
+ v-model="state.notes"
172
+ name="notes"
173
+ label="Notes"
174
+ placeholder="Anything else we should know?"
175
+ :error-message="errors.notes"
176
+ :field-has-error="!!errors.notes"
177
+ :input-variant="args.inputVariant"
178
+ />
179
+ </FormField>
180
+
181
+ <FormField width="wide" :has-gutter="false">
182
+ <InputSelectWithLabel
183
+ v-model="state.colour"
184
+ v-model:field-data="colourOptions"
185
+ name="colour"
186
+ label="Favourite colour"
187
+ placeholder="Choose a colour"
188
+ :error-message="errors.colour"
189
+ :field-has-error="!!errors.colour"
190
+ :input-variant="args.inputVariant"
191
+ />
192
+ </FormField>
193
+
194
+ <FormField width="wide" :has-gutter="false">
195
+ <ToggleSwitchWithLabel v-model="state.subscribe" name="subscribe" label="Subscribe to updates" />
196
+ </FormField>
197
+
198
+ <FormField width="wide" :has-gutter="false">
199
+ <div style="display: flex; gap: 1.2rem;">
200
+ <InputButtonCore type="submit" variant="primary" button-text="Continue" />
201
+ <InputButtonCore type="button" variant="tertiary" button-text="Clear errors" @click="clearErrors" />
202
+ </div>
203
+ </FormField>
204
+ </form>
205
+
206
+ <div
207
+ style="margin-top: 2rem; padding: 1.6rem; border-radius: 0.8rem; background: #f8fafc; font-family: monospace; font-size: 1.3rem;"
208
+ >
209
+ <div>fullName: {{ state.fullName || '""' }}</div>
210
+ <div>budget: £{{ state.budget }}</div>
211
+ <div>quantity: {{ state.quantity }}</div>
212
+ <div>notes: {{ state.notes || '""' }}</div>
213
+ <div>colour: {{ state.colour || '""' }}</div>
214
+ <div>subscribe: {{ state.subscribe }}</div>
215
+ </div>
216
+ </div>
217
+ `,
218
+ });
219
+
220
+ export const Default = Template.bind({});
@@ -23,6 +23,13 @@ token-rename pass, not a redesign.
23
23
  Geometry (`--input-toggle-*`, `--form-element-*`) is shared across every `05.forms` component and
24
24
  untouched by this — see `theming-form-geometry-tokens.md`.
25
25
 
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.
32
+
26
33
  ### Track width
27
34
 
28
35
  `--toggle-switch-track-width` overrides the track's width directly. Left unset, the track derives
@@ -64,3 +71,25 @@ fixed width independent of symbol size).
64
71
  ```vue
65
72
  <ToggleSwitchCore id="notifications" name="notifications" style="--toggle-switch-symbol-surface-on: var(--gold-06);" />
66
73
  ```
74
+
75
+ ---
76
+
77
+ ## Migration notes (2026-09-23)
78
+
79
+ - **`round` prop was previously dead** — `ToggleSwitchWithLabel`/`ToggleSwitchWithLabelInline`
80
+ both accepted and forwarded it, but `ToggleSwitchCore` never declared the prop or read it, so
81
+ the Storybook `SquareShape` story rendered identically to `Default`. Fixed by adding `round` to
82
+ `ToggleSwitchCore` and wiring it to the new `--toggle-switch-border-radius` token (see Shape
83
+ above).
84
+ - **`fieldHasError` didn't visually theme the component** — `ToggleSwitchCore` and
85
+ `ToggleSwitchWithLabel` both computed an error-aware theme value but bound `data-theme` to the
86
+ raw `theme` prop instead, so setting `fieldHasError` never applied this library's
87
+ `[data-theme="error"]` theme-token overrides (`_error.css`). Fixed by binding `data-theme` to the
88
+ computed value.
89
+ - Custom `iconOn`/`iconOff` slot content could render as a non-square box (e.g. a 16×16 SVG
90
+ rendering as ~16×20), throwing off centering inside the thumb — fixed by adding
91
+ `line-height: 0` to `.symbol-icon`, which collapses inline leading for any slotted content.
92
+ - Two dead, unread `--_transition-duration` private locals were removed from
93
+ `ToggleSwitchWithLabel`/`ToggleSwitchWithLabelInline` — nothing in either file's CSS or
94
+ `ToggleSwitchCore`'s ever read them (the actual transition durations are hardcoded `0.4s`
95
+ literals in `ToggleSwitchCore`).
@@ -1,6 +1,6 @@
1
1
  <template>
2
- <div class="toggle-switch-core" :class="elementClasses" :data-theme="theme">
3
- <div class="toggle-switch-wrapper" :class="wrapperClasses" :for="inputId" @click="toggleSwitchValue">
2
+ <div class="toggle-switch-core" :class="elementClasses" :data-theme="formUiTheme">
3
+ <div class="toggle-switch-wrapper" :class="wrapperClasses" @click="toggleSwitchValue">
4
4
  <input
5
5
  :id="inputId"
6
6
  v-model="modelValue"
@@ -44,6 +44,7 @@ interface Props {
44
44
  falseValue?: string | number | boolean;
45
45
  styleClassPassthrough?: string | string[];
46
46
  theme?: FormUiTheme;
47
+ round?: boolean;
47
48
  ariaDescribedby?: string;
48
49
  }
49
50
 
@@ -54,17 +55,20 @@ const props = withDefaults(defineProps<Props>(), {
54
55
  falseValue: false,
55
56
  styleClassPassthrough: () => [],
56
57
  theme: "default",
58
+ round: true,
57
59
  ariaDescribedby: "",
58
60
  });
59
61
 
60
62
  const slots = useSlots();
61
63
  const useDefaultIcons = computed(() => !slots.iconOn && !slots.iconOff);
62
64
 
63
- const FormUiTheme = computed(() => {
65
+ const formUiTheme = computed(() => {
64
66
  return props.fieldHasError ? "error" : props.theme;
65
67
  });
66
68
 
67
- const modelValue = defineModel<string | number | boolean>();
69
+ const roundedRadiusDefault = computed(() => (props.round ? "100vw" : "0.4rem"));
70
+
71
+ const modelValue = defineModel<string | number | boolean>({ required: true });
68
72
  const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
69
73
 
70
74
  const inputId = computed(() => `toggle-switch-${props.id}`);
@@ -90,156 +94,153 @@ const toggleSwitchValue = () => {
90
94
 
91
95
  <style lang="css">
92
96
  @layer components {
93
- .toggle-switch-core {
94
- .toggle-switch-label {
95
- display: block;
96
- }
97
+ .toggle-switch-core {
98
+ .toggle-switch-label {
99
+ display: block;
100
+ }
97
101
 
98
- .toggle-switch-wrapper {
99
- /* --_icon-font-size, --_switch-padding, --_toggle-symbol-border-width are the only locals
102
+ .toggle-switch-wrapper {
103
+ /* --_icon-font-size, --_switch-padding, --_toggle-symbol-border-width are the only locals
100
104
  actually read anywhere below — --_icon-on-opacity, --_icon-off-opacity,
101
105
  --_symbol-background-color, --_symbol-margin-inline-start, --_symbol-checked-offset were
102
106
  declared here but never referenced by any property in this file; removed as dead code
103
107
  rather than migrated, since migrating an unread token would just create a new unread
104
108
  public one. */
105
- --_icon-font-size: 2.4rem;
106
- --_switch-padding: 0.2rem;
107
- --_toggle-symbol-border-width: 0.1rem;
109
+ --_icon-font-size: 2.4rem;
110
+ --_switch-padding: 0.2rem;
111
+ --_toggle-symbol-border-width: 0.1rem;
108
112
 
109
- /* Public --toggle-switch-* tokens, inline-fallback to the shared --theme-* tokens (see
113
+ /* Public --toggle-switch-* tokens, inline-fallback to the shared --theme-* tokens (see
110
114
  theming-component-token-pattern.md) — overriding one here doesn't touch every other
111
115
  themed input/control that also reads --theme-checkbox-symbol-surface/--theme-border. */
112
- --_surface: var(--toggle-switch-surface, var(--theme-checkbox-symbol-surface));
113
- --_surface-hover: var(--toggle-switch-surface-hover, var(--theme-surface-subtle));
114
- --_border: var(--toggle-switch-border, var(--theme-border));
115
- --_border-focus: var(--toggle-switch-border-focus, var(--theme-border-focus));
116
-
117
- display: flex;
118
- flex-direction: column;
119
-
120
- input {
121
- height: 0;
122
- width: 0;
123
- opacity: 0;
124
-
125
- &:focus-visible {
126
- + .symbol-wrapper {
127
- outline: var(--form-element-outline-width-focus) solid var(--_border-focus);
128
- outline-offset: var(--form-element-outline-offset-focus);
116
+ --_surface: var(--toggle-switch-surface, var(--theme-checkbox-symbol-surface));
117
+ --_surface-hover: var(--toggle-switch-surface-hover, var(--theme-surface-subtle));
118
+ --_border: var(--toggle-switch-border, var(--theme-border));
119
+ --_border-focus: var(--toggle-switch-border-focus, var(--theme-border-focus));
120
+ --_border-radius: var(--toggle-switch-border-radius, v-bind(roundedRadiusDefault));
121
+
122
+ display: flex;
123
+ flex-direction: column;
124
+
125
+ input {
126
+ height: 0;
127
+ width: 0;
128
+ opacity: 0;
129
+
130
+ &:focus-visible {
131
+ + .symbol-wrapper {
132
+ outline: var(--form-element-outline-width-focus) solid var(--_border-focus);
133
+ outline-offset: var(--form-element-outline-offset-focus);
134
+ }
129
135
  }
130
136
  }
131
- }
132
137
 
133
- .symbol-wrapper {
134
- /* Geometry */
138
+ .symbol-wrapper {
139
+ /* Geometry */
135
140
 
136
- /* Track width has no direct public token — it's derived from the same geometry tokens
141
+ /* Track width has no direct public token — it's derived from the same geometry tokens
137
142
  that size the thumb, so the track always scales with --input-toggle-symbol-size instead
138
143
  of clipping/floating it. Breakdown at the default 0.1rem tokens (= 72px total):
139
144
  2×symbol-size (6rem) + 2×form-element-border-width (0.2rem, wrapper border) +
140
145
  2×wrapper-padding (0.2rem) + 3×symbol-outline-width (0.3rem) +
141
146
  3×symbol-border-width (0.3rem) + 2×symbol-margin (0.2rem). Override
142
147
  --toggle-switch-track-width directly if you need a value the formula doesn't produce. */
143
- --_track-width: var(
144
- --toggle-switch-track-width,
145
- calc(
146
- (2 * var(--input-toggle-symbol-size)) + (2 * var(--form-element-border-width)) +
147
- (2 * var(--input-toggle-wrapper-padding)) + (3 * var(--input-toggle-symbol-outline-width)) +
148
- (3 * var(--input-toggle-symbol-border-width)) + (2 * var(--input-toggle-symbol-margin))
149
- )
150
- );
151
-
152
- display: inline-flex;
153
- align-items: center;
154
- justify-content: start;
155
- position: relative;
156
- transition:
157
- background-color var(--theme-form-transition-duration) linear,
158
- border-color var(--theme-form-transition-duration) linear,
159
- outline var(--theme-form-transition-duration) linear,
160
- outline-offset var(--theme-form-transition-duration) linear,
161
- outline var(--theme-form-transition-duration) linear;
162
-
163
- /* UI */
164
- background-color: var(--_surface);
165
- border: var(--form-element-border-width) solid var(--_border);
166
- outline: var(--form-element-outline-width) solid transparent;
167
- border-radius: 100vw;
168
- width: var(--_track-width);
169
- padding: calc(var(--input-toggle-wrapper-padding) + var(--input-toggle-symbol-outline-width));
170
-
171
- &:hover {
172
- background-color: var(--_surface-hover);
173
- cursor: pointer;
174
- }
175
-
176
- .symbol {
177
- /* Geometry */
178
-
179
- display: inline-grid;
180
- grid-template-areas: "icon";
181
- place-content: center;
182
- /* position: absolute; */
183
- overflow: hidden;
184
- translate: 0 0;
185
-
186
- aspect-ratio: 1/1;
148
+ --_track-width: var(
149
+ --toggle-switch-track-width,
150
+ calc(
151
+ (2 * var(--input-toggle-symbol-size)) + (2 * var(--form-element-border-width)) +
152
+ (2 * var(--input-toggle-wrapper-padding)) + (3 * var(--input-toggle-symbol-outline-width)) +
153
+ (3 * var(--input-toggle-symbol-border-width)) + (2 * var(--input-toggle-symbol-margin))
154
+ )
155
+ );
156
+
157
+ display: inline-flex;
158
+ align-items: center;
159
+ justify-content: start;
160
+ position: relative;
161
+ transition:
162
+ background-color var(--theme-form-transition-duration) linear,
163
+ border-color var(--theme-form-transition-duration) linear,
164
+ outline var(--theme-form-transition-duration) linear,
165
+ outline-offset var(--theme-form-transition-duration) linear,
166
+ outline var(--theme-form-transition-duration) linear;
187
167
 
188
168
  /* UI */
189
- /* padding: calc(calc(var(--input-toggle-symbol-size) - var(--_icon-font-size)) / 2); */
190
- border: var(--input-toggle-symbol-border-width) solid var(--toggle-switch-symbol-border, var(--theme-text));
191
- outline: var(--input-toggle-symbol-outline-width) solid transparent;
192
- border-radius: 100vw;
193
- background-color: var(--toggle-switch-symbol-surface-off, var(--theme-on-surface));
194
- transition:
195
- translate 0.4s ease,
196
- background-color 0.4s linear;
197
-
198
- &.checked {
199
- background-color: var(--toggle-switch-symbol-surface-on, var(--theme-surface));
200
- /* left: calc(
201
- 100% - var(--input-toggle-symbol-size) - var(--_switch-padding) - (2 * var(--_toggle-symbol-border-width))
202
- ); */
203
- /* translate: calc(var(--input-toggle-symbol-size) / 2) 0; */
204
- /* translate: 1.6rem 0; */
205
- translate: calc(
206
- var(--input-toggle-symbol-size) + var(--input-toggle-symbol-border-width) +
207
- var(--input-toggle-symbol-outline-width)
208
- )
209
- 0;
169
+ background-color: var(--_surface);
170
+ border: var(--form-element-border-width) solid var(--_border);
171
+ outline: var(--form-element-outline-width) solid transparent;
172
+ border-radius: var(--_border-radius);
173
+ width: var(--_track-width);
174
+ padding: calc(var(--input-toggle-wrapper-padding) + var(--input-toggle-symbol-outline-width));
175
+
176
+ &:hover {
177
+ background-color: var(--_surface-hover);
178
+ cursor: pointer;
210
179
  }
211
180
 
212
- .symbol-icon {
181
+ .symbol {
213
182
  /* Geometry */
214
183
 
215
- display: grid;
216
- grid-area: icon;
184
+ display: inline-grid;
185
+ grid-template-areas: "icon";
217
186
  place-content: center;
218
- opacity: 0;
187
+ overflow: hidden;
188
+ translate: 0 0;
189
+
190
+ aspect-ratio: 1/1;
219
191
 
220
192
  /* UI */
221
- width: var(--input-toggle-symbol-size);
222
- height: var(--input-toggle-symbol-size);
223
- margin: var(--input-toggle-symbol-margin);
193
+ /* padding: calc(calc(var(--input-toggle-symbol-size) - var(--_icon-font-size)) / 2); */
194
+ border: var(--input-toggle-symbol-border-width) solid var(--toggle-switch-symbol-border, var(--theme-text));
195
+ outline: var(--input-toggle-symbol-outline-width) solid transparent;
196
+ border-radius: var(--_border-radius);
197
+ background-color: var(--toggle-switch-symbol-surface-off, var(--theme-on-surface));
224
198
  transition:
225
- opacity var(--theme-form-transition-duration),
226
- color var(--theme-form-transition-duration);
227
-
228
- &.icon-on {
229
- color: var(--toggle-switch-icon-on-color, var(--theme-on-surface));
230
- }
231
-
232
- &.icon-off {
233
- color: var(--toggle-switch-icon-off-color, var(--theme-surface));
199
+ translate 0.4s ease,
200
+ background-color 0.4s linear;
201
+
202
+ &.checked {
203
+ background-color: var(--toggle-switch-symbol-surface-on, var(--theme-surface));
204
+ translate: calc(
205
+ var(--input-toggle-symbol-size) + var(--input-toggle-symbol-border-width) +
206
+ var(--input-toggle-symbol-outline-width)
207
+ )
208
+ 0;
234
209
  }
235
210
 
236
- &.active {
237
- opacity: 1;
211
+ .symbol-icon {
212
+ /* Geometry */
213
+
214
+ display: grid;
215
+ grid-area: icon;
216
+ place-content: center;
217
+ /* Removes line-box leading so slotted icon content sizes to its own intrinsic height. */
218
+ line-height: 0;
219
+ opacity: 0;
220
+
221
+ /* UI */
222
+ width: var(--input-toggle-symbol-size);
223
+ height: var(--input-toggle-symbol-size);
224
+ margin: var(--input-toggle-symbol-margin);
225
+ transition:
226
+ opacity var(--theme-form-transition-duration),
227
+ color var(--theme-form-transition-duration);
228
+
229
+ &.icon-on {
230
+ color: var(--toggle-switch-icon-on-color, var(--theme-on-surface));
231
+ }
232
+
233
+ &.icon-off {
234
+ color: var(--toggle-switch-icon-off-color, var(--theme-surface));
235
+ }
236
+
237
+ &.active {
238
+ opacity: 1;
239
+ }
238
240
  }
239
241
  }
240
242
  }
241
243
  }
242
244
  }
243
245
  }
244
- }
245
246
  </style>
@@ -75,7 +75,7 @@ export default {
75
75
  // Styling
76
76
  theme: {
77
77
  control: { type: "select" },
78
- options: ["primary", "secondary", "tertiary", "ghost", "error", "success", "warning"],
78
+ options: ["default", "success", "error", "warning"],
79
79
  description: "Toggle switch theme",
80
80
  table: {
81
81
  category: "Styling",
@@ -83,7 +83,7 @@ export default {
83
83
  // Styling
84
84
  theme: {
85
85
  control: { type: "select" },
86
- options: ["primary", "secondary", "tertiary", "ghost", "error", "success", "warning"],
86
+ options: ["default", "success", "error", "warning"],
87
87
  description: "Toggle switch theme",
88
88
  table: {
89
89
  category: "Styling",
@@ -64,7 +64,7 @@ export default {
64
64
  // Styling
65
65
  theme: {
66
66
  control: { type: "select" },
67
- options: ["primary", "secondary", "tertiary", "ghost", "error", "success", "warning"],
67
+ options: ["default", "success", "error", "warning"],
68
68
  description: "Toggle switch theme",
69
69
  table: {
70
70
  category: "Styling",
@@ -243,7 +243,7 @@ export const InlineFormExample: StoryFn = () => ({
243
243
  name="darkMode"
244
244
  label="Dark mode"
245
245
  label-weight="normal"
246
- theme="secondary"
246
+ theme="default"
247
247
  />
248
248
 
249
249
  <StorybookComponent