srcdev-nuxt-components 9.4.10 → 9.4.12

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 (94) hide show
  1. package/.claude/commands/migrate-component.md +18 -0
  2. package/.claude/component-ledger/audit.json +1 -1
  3. package/.claude/component-ledger/output.html +1 -1
  4. package/.claude/skills/components/input-number-core.md +214 -0
  5. package/.claude/skills/components/input-select-core.md +151 -0
  6. package/.claude/skills/components/input-text-core.md +16 -0
  7. package/.claude/skills/components/input-textarea-core.md +154 -0
  8. package/.claude/skills/components/select-menu.md +195 -0
  9. package/.claude/skills/components/toggle-switch-core.md +150 -0
  10. package/.claude/skills/index.md +5 -0
  11. package/.vscode/srcdev-component-input-number.code-snippets +65 -0
  12. package/.vscode/srcdev-component-input-select.code-snippets +42 -0
  13. package/.vscode/srcdev-component-input-textarea.code-snippets +40 -0
  14. package/.vscode/srcdev-component-select-menu.code-snippets +65 -0
  15. package/.vscode/srcdev-component-toggle-switch.code-snippets +49 -0
  16. package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +1 -1
  17. package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +1 -1
  18. package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +1 -1
  19. package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +1 -1
  20. package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +1 -1
  21. package/app/components/01.atoms/animations/section-parallax/CONSUMER-STYLING.md +1 -1
  22. package/app/components/01.atoms/animations/section-parallax/SectionParallax.vue +1 -1
  23. package/app/components/01.atoms/canvas-switcher/CONSUMER-STYLING.md +2 -2
  24. package/app/components/01.atoms/canvas-switcher/CanvasSwitcher.vue +2 -2
  25. package/app/components/01.atoms/card/CardCore.vue +4 -4
  26. package/app/components/01.atoms/clipped-panel/CONSUMER-STYLING.md +2 -2
  27. package/app/components/01.atoms/clipped-panel/ClippedPanel.vue +2 -2
  28. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +2 -2
  29. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue +2 -2
  30. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts +1 -1
  31. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +1 -1
  32. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +1 -1
  33. package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +8 -8
  34. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +8 -8
  35. package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +6 -6
  36. package/app/components/01.atoms/pop-over/PopOver.vue +6 -6
  37. package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +1 -1
  38. package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +1 -1
  39. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +1 -1
  40. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +1 -1
  41. package/app/components/02.molecules/pricing-card/CONSUMER-STYLING.md +11 -0
  42. package/app/components/02.molecules/pricing-card/PricingCard.vue +3 -1
  43. package/app/components/02.molecules/pricing-card/stories/PricingCard.stories.ts +2 -0
  44. package/app/components/02.molecules/pricing-card/tests/PricingCard.spec.ts +13 -0
  45. package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +168 -0
  46. package/app/components/02.molecules/select-menu/SelectMenu.vue +384 -0
  47. package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +359 -0
  48. package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +476 -0
  49. package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +42 -0
  50. package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +3 -3
  51. package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +3 -3
  52. package/app/components/03.organisms/image-galleries/carousel-flip/stories/CarouselFlip.stories.ts +18 -18
  53. package/app/components/05.forms/input-button/CONSUMER-STYLING.md +2 -7
  54. package/app/components/05.forms/input-button/InputButtonCore.vue +1 -5
  55. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +26 -6
  56. package/app/components/05.forms/input-number/InputNumberCore.vue +125 -85
  57. package/app/components/05.forms/input-number/stories/InputNumberCore.stories.ts +92 -0
  58. package/app/components/05.forms/input-number/tests/InputNumberCore.spec.ts +119 -0
  59. package/app/components/05.forms/input-number/variants/InputNumberDefault.vue +30 -22
  60. package/app/components/05.forms/input-number/variants/stories/InputNumberDefault.stories.ts +123 -0
  61. package/app/components/05.forms/input-number/variants/tests/InputNumberDefault.spec.ts +143 -0
  62. package/app/components/05.forms/input-range/CONSUMER-STYLING.md +2 -2
  63. package/app/components/05.forms/input-range/InputRangeCore.vue +2 -2
  64. package/app/components/05.forms/input-select/InputSelectCore.vue +23 -30
  65. package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +2 -2
  66. package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +115 -0
  67. package/app/components/05.forms/input-select/tests/InputSelectCore.spec.ts +129 -0
  68. package/app/components/05.forms/input-select/variants/InputSelectWithLabel.vue +1 -1
  69. package/app/components/05.forms/input-select/variants/tests/InputSelectWithLabel.spec.ts +87 -0
  70. package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +14 -0
  71. package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +1 -1
  72. package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +55 -0
  73. package/app/components/05.forms/input-textarea/InputTextareaCore.vue +24 -16
  74. package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +1 -1
  75. package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +119 -0
  76. package/app/components/05.forms/input-textarea/variants/InputTextareaWithLabel.vue +3 -13
  77. package/app/components/05.forms/input-textarea/variants/tests/InputTextareaWithLabel.spec.ts +93 -0
  78. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +95 -10
  79. package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +29 -0
  80. package/app/components/05.forms/toggle-switch/ToggleSwitchCore.vue +120 -119
  81. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +1 -1
  82. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +1 -1
  83. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +2 -2
  84. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +101 -0
  85. package/app/components/05.forms/toggle-switch/variants/ToggleSwitchWithLabel.vue +3 -5
  86. package/app/components/05.forms/toggle-switch/variants/ToggleSwitchWithLabelInline.vue +1 -2
  87. package/app/components/05.forms/toggle-switch/variants/tests/ToggleSwitchWithLabel.spec.ts +77 -0
  88. package/app/components/05.forms/toggle-switch/variants/tests/ToggleSwitchWithLabelInline.spec.ts +66 -0
  89. package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +4 -4
  90. package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitchCore.vue +6 -6
  91. package/app/types/components/index.ts +1 -0
  92. package/app/types/components/select-menu.d.ts +5 -0
  93. package/package.json +1 -1
  94. package/.vscode/srcdev-component-toggle-switch-core.code-snippets +0 -13
@@ -0,0 +1,195 @@
1
+ # SelectMenu
2
+
3
+ ## Overview
4
+
5
+ `SelectMenu` is a trigger-and-popover component for selecting from a list of options — a custom,
6
+ fully token-styled alternative to a native `<select>` or `InputSelectCore`, built for non-form
7
+ contexts (language switchers, category filters, faceted filter bars, multi-pick treatment lists).
8
+ It reuses the same Popover API + CSS anchor-positioning mechanism as `ActionMenu` (see that skill
9
+ doc), but its items carry a `value`/`label`/optional `icon`, and the whole thing is driven by
10
+ `v-model` instead of slotted actions. Defaults to single-select (checkmark, closes on pick); set
11
+ `multiple` for a checkbox-per-option multi-select where `v-model` is an array and the popover stays
12
+ open between picks.
13
+
14
+ **Location**: `app/components/02.molecules/select-menu/`
15
+
16
+ ---
17
+
18
+ ## Components
19
+
20
+ ### SelectMenu
21
+
22
+ | Prop | Type | Default | Notes |
23
+ |---|---|---|---|
24
+ | `options` | `SelectMenuOption[]` | — | **Required.** `{ value: string \| number; label: string; icon?: string }[]`. |
25
+ | `label` | `string` | — | **Required.** Accessible name for the trigger + listbox (`aria-label`). Also the trigger's fallback text when nothing is selected and no `placeholder` is set — this doubles as a visible category tag, e.g. `"Choose a service"`. |
26
+ | `placeholder` | `string` | `undefined` | Trigger text shown when nothing is selected. Falls back to `label` when omitted. |
27
+ | `showIcon` | `boolean` | `true` | Show the selected option's icon in the trigger. |
28
+ | `showLabel` | `boolean` | `true` | Show the selected option's label (or placeholder/label fallback) text in the trigger. Set `false` for an icon-only compact trigger. |
29
+ | `showChevron` | `boolean` | `true` | Show the trailing chevron in the trigger. |
30
+ | `multiple` | `boolean` | `false` | Allow selecting more than one option. Each option gets a checkbox indicator, `v-model` becomes an array, and picking an option leaves the popover open so more can be toggled. Trigger icon is not shown when `true` (no single option to represent). |
31
+ | `showSelectionInTrigger` | `boolean` | `false` | Multiple mode only. When `true`, the trigger text updates to a comma-separated list of the currently checked options instead of staying fixed on `placeholder`/`label`. No effect outside `multiple`. |
32
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root `<div>`. |
33
+
34
+ **v-model**
35
+
36
+ `defineModel<string | number | (string | number)[] | undefined>({ default: undefined })` — the
37
+ selected value(s). In single-select mode (default) it's the one selected option's `value`, or
38
+ `undefined` if nothing is selected — a valid, expected state that renders the placeholder. With
39
+ `multiple`, it's an array of selected values (`[]` when nothing is selected). `{ default: undefined }`
40
+ is this repo's fix for the `vue/require-default-prop` false positive on `defineModel`-declared
41
+ props that are legitimately optional — unlike `InputSelectCore`'s `modelValue`, which uses
42
+ `{ required: true }` because a native form field genuinely can't be meaningfully empty, this model
43
+ is optional by design (see that rule's `defineModel` gap noted in the repo's ledger `eslint_issues`
44
+ docs). For several independent single-select categories, place multiple `SelectMenu` instances
45
+ side by side rather than using `multiple` on one instance — see the "Filter bar" section below.
46
+
47
+ **Type import**
48
+
49
+ ```ts
50
+ import type { SelectMenuOption } from "~/types/components/select-menu";
51
+ // consuming apps: import type { SelectMenuOption } from "srcdev-nuxt-components";
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Basic usage
57
+
58
+ ```vue
59
+ <script setup lang="ts">
60
+ import type { SelectMenuOption } from "~/types/components/select-menu";
61
+
62
+ const language = ref<string>("en");
63
+ const languageOptions: SelectMenuOption[] = [
64
+ { value: "en", label: "English", icon: "flag:gb-4x3" },
65
+ { value: "fr", label: "Français", icon: "flag:fr-4x3" },
66
+ ];
67
+ </script>
68
+
69
+ <template>
70
+ <SelectMenu v-model="language" :options="languageOptions" label="Language" />
71
+ </template>
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Trigger content variants
77
+
78
+ | Variant | Usage |
79
+ |---|---|
80
+ | Icon-only (compact switcher, e.g. a flag in a tight header) | `:show-label="false" :show-chevron="false"` |
81
+ | Icon + chevron, no text (flag with a visible dropdown affordance) | `:show-label="false"` |
82
+ | Text + chevron (category filter, no per-option icons) | `:show-icon="false"` |
83
+ | Icon + text + chevron (full select) | Defaults — no overrides needed |
84
+
85
+ When nothing is selected, the trigger shows `placeholder` (or `label` if no placeholder is given)
86
+ — this is what makes it work as a filter-category tag, e.g. a `SelectMenu` with
87
+ `label="Choose a service"` and no `placeholder` shows "Choose a service" until an option is
88
+ picked, then swaps to the selected option's label.
89
+
90
+ ---
91
+
92
+ ## Multi-select (checkboxes)
93
+
94
+ Set `multiple` to turn every option into a checkbox row. `v-model` becomes an array, and clicking
95
+ an option toggles it without closing the popover:
96
+
97
+ ```vue
98
+ <script setup lang="ts">
99
+ const treatments = ref<string[]>([]);
100
+ const treatmentOptions = [
101
+ { value: "trim", label: "Trim" },
102
+ { value: "layers", label: "Layers" },
103
+ { value: "restyle", label: "Restyle" },
104
+ { value: "straightening", label: "Straightening" },
105
+ ];
106
+ </script>
107
+
108
+ <template>
109
+ <SelectMenu v-model="treatments" :options="treatmentOptions" label="Services required" multiple />
110
+ </template>
111
+ ```
112
+
113
+ Unlike single-select, the trigger text stays fixed by default on `placeholder`/`label` as a static
114
+ category tag regardless of the current selection — it doesn't update to list checked options, so
115
+ `label="Services required"` remains visible in the trigger the whole time. Use the checkboxes
116
+ inside the open popover to see what's currently selected.
117
+
118
+ Set `showSelectionInTrigger` to opt out of that and have the trigger update instead:
119
+
120
+ ```vue
121
+ <SelectMenu
122
+ v-model="treatments"
123
+ :options="treatmentOptions"
124
+ label="Services required"
125
+ multiple
126
+ show-selection-in-trigger
127
+ />
128
+ <!-- Trigger reads e.g. "Layers, Restyle" once those two are checked, falling back to
129
+ "Services required" again if everything is unchecked. -->
130
+ ```
131
+
132
+ ---
133
+
134
+ ## Filter bar (multiple categories)
135
+
136
+ `SelectMenu` is single-select per instance — a faceted filter bar is built by placing one instance
137
+ per category, each with its own `v-model`:
138
+
139
+ ```vue
140
+ <template>
141
+ <div style="display: flex; gap: 1.2rem;">
142
+ <SelectMenu label="Choose a service" v-model="service" :options="serviceOptions" />
143
+ <SelectMenu label="Choose a stylist" v-model="stylist" :options="stylistOptions" />
144
+ </div>
145
+ </template>
146
+ ```
147
+
148
+ ---
149
+
150
+ ## CSS token API
151
+
152
+ See `CONSUMER-STYLING.md` in the component folder for the full token reference. Default styling
153
+ targets the same visual language as the library's other select-like controls (border, background,
154
+ radius, focus ring drawn from `--theme-*` tokens).
155
+
156
+ Quick reference:
157
+
158
+ ```css
159
+ /* assets/styles/setup/07.components/select-menu.css */
160
+ :root {
161
+ --select-menu-trigger-border-radius: 999rem;
162
+ --select-menu-trigger-surface: var(--brand-surface);
163
+ --select-menu-popover-surface: var(--brand-surface);
164
+ --select-menu-item-surface-hover: var(--brand-surface-subtle);
165
+ --select-menu-item-check-color: var(--brand-accent);
166
+ }
167
+ ```
168
+
169
+ ---
170
+
171
+ ## Notes
172
+
173
+ - **Popover API + CSS anchor positioning** — same mechanism as `ActionMenu`. Broadly supported
174
+ (Chrome 114+, Firefox 125+, Safari 17+). No polyfill is included.
175
+ - **Left-aligned popover** (`left: anchor(left)`) — unlike `ActionMenu` which right-aligns. Matches
176
+ native `<select>` dropdown behaviour. Flips above the trigger near the bottom of the viewport.
177
+ - **Keyboard navigation** follows the WAI-ARIA listbox pattern: `ArrowDown`/`ArrowUp` move between
178
+ options (wraps around), `Home`/`End` jump to first/last, `Enter`/`Space` select the focused
179
+ option, `Tab` closes without stealing focus back to the trigger (matches `ActionMenu`'s
180
+ `handleKeydown` convention — see that skill doc's note on the `currentIndex === -1` guard, which
181
+ applies identically here). `Enter`/`Space` close the popover in single-select mode but only
182
+ toggle the checkbox (leaving it open) when `multiple` is set.
183
+ - **Opens focused on the current selection** — `handleToggle` focuses the first selected
184
+ `[role="option"]` if one exists (in `multiple` mode, the first checked item), otherwise the first
185
+ option (unlike `ActionMenu`, which always focuses the first item since it has no selection state).
186
+ - **`aria-multiselectable="true"`** is set on the listbox only when `multiple` is `true`.
187
+ - **Chevron rotation is pure CSS** via `:has(.select-menu-popover:popover-open)` on the root — no
188
+ JS state drives the visual. `isOpen` is still tracked internally, but only to set `aria-expanded`
189
+ on the trigger.
190
+ - **No native `<select>`/`appearance: base-select` involved** — this is a from-scratch popover
191
+ listbox, not a styled native select, so it doesn't inherit `InputSelectCore`'s WebKit
192
+ `appearance: base-select` constraints.
193
+ - **Not a form field** — `SelectMenu` has no `fieldHasError`/`theme`/`inputVariant` props like the
194
+ `05.forms` inputs. Use `InputSelectCore` instead for an actual form field that needs validation
195
+ state and native `<select>` semantics.
@@ -0,0 +1,150 @@
1
+ # ToggleSwitchCore Component
2
+
3
+ ## Overview
4
+
5
+ `ToggleSwitchCore` is the low-level pill-shaped (or square, via `round`) toggle switch primitive
6
+ for the `05.forms` toggle family. It renders a visually-hidden native `<input type="checkbox">`
7
+ (kept in the DOM for real keyboard/checkbox semantics) plus a styled track/thumb visual, with
8
+ optional icon slots shown inside the thumb for the on/off states. It has no label, description, or
9
+ error-message rendering of its own — that's composed by the `ToggleSwitchWithLabel` and
10
+ `ToggleSwitchWithLabelInline` variants below.
11
+
12
+ Most consumers should reach for **ToggleSwitchWithLabel** or **ToggleSwitchWithLabelInline** rather
13
+ than `ToggleSwitchCore` directly.
14
+
15
+ ---
16
+
17
+ ## Props reference
18
+
19
+ > **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:field-has-error`, `:style-class-passthrough`, `:aria-describedby`, `:true-value`, `:false-value`.
20
+
21
+ | Prop (template form) | Type | Default | Notes |
22
+ |------|------|---------|-------|
23
+ | `:id` | `string` | (required) | Combined with a fixed prefix to build the native input's `id` (`toggle-switch-${id}`). |
24
+ | `:name` | `string` | (required) | Applied to the native checkbox. |
25
+ | `:required` | `boolean` | `false` | Native `required`. |
26
+ | `:field-has-error` | `boolean` | `false` | Drives `aria-invalid` on the checkbox, and switches `data-theme` to `"error"` regardless of the `theme` prop's own value. |
27
+ | `:true-value` / `:false-value` | `string \| number \| boolean` | `true` / `false` | The two values `v-model` toggles between. |
28
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root element. |
29
+ | `:theme` | `FormUiTheme` | `"default"` | Sets `data-theme` on the root (superseded by `"error"` when `field-has-error` is true — see above). |
30
+ | `:round` | `boolean` | `true` | `true` renders a pill-shaped track/thumb (`border-radius: 100vw`); `false` renders a rounded-square shape (`border-radius: 0.4rem`). See the `--toggle-switch-border-radius` token in CONSUMER-STYLING.md to override the exact value either state produces. |
31
+ | `:aria-describedby` | `string` | `""` | Forwarded to the native checkbox. |
32
+
33
+ ### v-model
34
+
35
+ - `v-model` — `string | number | boolean`, **required** (`defineModel({ required: true })`, satisfying `vue/require-default-prop` — see `project_ledger_eslint_issues_column` in memory).
36
+
37
+ ### Slots
38
+
39
+ - `iconOn` / `iconOff` — replace the default `material-symbols:circle-outline` icon shown inside the thumb for each state. Providing either slot switches off the `use-default-icons` wrapper class (checked via `useSlots()`, so this only reacts to slot *presence*, not conditional slot content).
40
+
41
+ ---
42
+
43
+ ## CSS token API
44
+
45
+ See [CONSUMER-STYLING.md](../../app/components/05.forms/toggle-switch/CONSUMER-STYLING.md) for the
46
+ full table with defaults.
47
+
48
+ **Common tokens:**
49
+ - `--toggle-switch-surface` / `-surface-hover` — track background resting/hover, fall back to `--theme-checkbox-symbol-surface`/`--theme-surface-subtle`
50
+ - `--toggle-switch-border` / `-border-focus` — track border colour, and `:focus-visible` outline colour, fall back to `--theme-border`/`--theme-border-focus`
51
+ - `--toggle-switch-symbol-surface-off` / `-symbol-surface-on` — thumb background unchecked/checked
52
+ - `--toggle-switch-border-radius` — track and thumb shape; see the `round` prop above
53
+
54
+ ---
55
+
56
+ ## Fixed 2026-09-23: dead functionality and story bugs
57
+
58
+ `ToggleSwitchCore` (and its two variants) migrated to full compliance from a `variants:true`,
59
+ `eslint_issues:true`, score-3/5 state (no tests, no skill doc).
60
+
61
+ - **`round` prop was completely dead** — both variants declared and forwarded it to
62
+ `ToggleSwitchCore`, and Storybook's `SquareShape` story existed specifically to demonstrate
63
+ `round: false`, but `ToggleSwitchCore` never declared the prop at all, so it silently fell
64
+ through as an inert `round` DOM attribute on the root `<div>` and the track/thumb stayed pill-shaped
65
+ regardless. Fixed by adding `round` to `ToggleSwitchCore` and wiring it to a new
66
+ `--toggle-switch-border-radius` token (100vw when round, 0.4rem when square).
67
+ - **`fieldHasError` didn't switch the component's theme** — `ToggleSwitchCore` and
68
+ `ToggleSwitchWithLabel` both computed an error-aware theme value but bound `data-theme` to the
69
+ raw `theme` prop instead of that computed, so this library's `[data-theme="error"]` global
70
+ token overrides never applied when a consumer set `fieldHasError`. Fixed by binding `data-theme`
71
+ to the computed value in both files.
72
+ - **An invalid `for` attribute on a `<div>`** (`.toggle-switch-wrapper`) was removed — `for` is
73
+ only a valid HTML attribute on `<label>`/`<output>`, so it was inert dead markup; the real
74
+ label association is via `InputLabel`'s own `for`/`id` wiring in the variants.
75
+ - **`defineModel` had no default**, tripping `vue/require-default-prop` (a known false-positive
76
+ for `defineModel` — see `project_ledger_eslint_issues_column` in memory) — fixed with
77
+ `{ required: true }` across `ToggleSwitchCore` and both variants, since a checkbox genuinely
78
+ can't be meaningfully empty.
79
+ - **Two dead, unread `--_transition-duration` private tokens** (one per variant) were removed —
80
+ nothing in either variant's own CSS or `ToggleSwitchCore`'s ever read them; the actual
81
+ transition durations are hardcoded `0.4s` literals inside `ToggleSwitchCore`.
82
+ - **Custom `iconOn`/`iconOff` slot content could render as a non-square box, throwing off
83
+ centering** — `.symbol-icon` is `display: grid; place-content: center;`, and its single
84
+ auto-sized track shrink-wraps to the slotted content's own box. A plain inline element (e.g. a
85
+ `<span>` wrapping a 16×16 SVG, as in the `WithCustomIcons` stories) sizes to its line box's
86
+ leading rather than its intrinsic height, rendering as ~16×20 instead of 16×16. Fixed by adding
87
+ `line-height: 0` to `.symbol-icon`, which collapses that leading for whatever markup a consumer
88
+ slots in. The default icon (`@nuxt/icon`'s `Icon` component) wasn't affected, since iconify sets
89
+ its own sizing/`vertical-align` internally.
90
+ - **All three story files' `theme` control listed the wrong option set**
91
+ (`["primary", "secondary", "tertiary", "ghost", "error", "success", "warning"]`, none of which
92
+ match `FormUiTheme`) — fixed to the real `["default", "success", "error", "warning"]`.
93
+ `ToggleSwitchWithLabelInline.stories.ts`'s `InlineFormExample` story also passed the now-invalid
94
+ `theme="secondary"` directly — fixed to `"default"`.
95
+
96
+ ---
97
+
98
+ ## Variants
99
+
100
+ ### ToggleSwitchWithLabel
101
+
102
+ `ToggleSwitchWithLabel` (`variants/ToggleSwitchWithLabel.vue`) composes `ToggleSwitchCore` with
103
+ `InputLabel`, an optional `description` slot, and `InputError`.
104
+
105
+ **Additional props over ToggleSwitchCore:**
106
+
107
+ | Prop (template form) | Type | Default | Notes |
108
+ |------|------|---------|-------|
109
+ | `:label` | `string` | (required) | Rendered via `InputLabel`. |
110
+ | `:error-message` | `object \| string` | `""` | Rendered via `InputError` when `field-has-error` is true. |
111
+
112
+ Note this wrapper passes `id`/`name`/`required`/`field-has-error`/`true-value`/`false-value`/
113
+ `theme`/`round`/`aria-describedby` straight through to `ToggleSwitchCore`; `id` is a self-generated
114
+ `useId()` value, not a prop — consumers don't set it directly.
115
+
116
+ **Slots**: `description`, `iconOn`, `iconOff` (the latter two forwarded to `ToggleSwitchCore`).
117
+
118
+ ```vue
119
+ <ToggleSwitchWithLabel
120
+ name="notifications"
121
+ label="Enable notifications"
122
+ v-model="notificationsEnabled"
123
+ :error-message="errors.notifications"
124
+ :field-has-error="!!errors.notifications"
125
+ >
126
+ <template #description>Turn this on to receive notifications</template>
127
+ </ToggleSwitchWithLabel>
128
+ ```
129
+
130
+ ### ToggleSwitchWithLabelInline
131
+
132
+ `ToggleSwitchWithLabelInline` (`variants/ToggleSwitchWithLabelInline.vue`) composes
133
+ `ToggleSwitchCore` with `InputLabel` laid out inline (label and switch side by side, no
134
+ description/error-message support — this variant is for compact settings-row usage).
135
+
136
+ **Additional props over ToggleSwitchCore:**
137
+
138
+ | Prop (template form) | Type | Default | Notes |
139
+ |------|------|---------|-------|
140
+ | `:label` | `string` | (required) | Rendered via `InputLabel`. |
141
+ | `:label-weight` | `LabelWeight` (`"normal" \| "semi-bold" \| "bold"`) | `"normal"` | Maps to a `body-normal`/`body-normal-semibold`/`body-normal-bold` class on the label. |
142
+
143
+ This variant has no `required`/`field-has-error`/`error-message` props — it never claimed error-state
144
+ support, unlike `ToggleSwitchWithLabel`.
145
+
146
+ **Slots**: `iconOn`, `iconOff` (forwarded to `ToggleSwitchCore`).
147
+
148
+ ```vue
149
+ <ToggleSwitchWithLabelInline name="darkMode" label="Dark mode" v-model="darkModeEnabled" />
150
+ ```
@@ -107,6 +107,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
107
107
  ├── display-theme-switch.md — DisplayThemeSwitch: system/light/dark picker wrapping TripleToggleSwitchCore, wired to useSettingsStore, labels/icons props, small sizing variant
108
108
  ├── card-core.md — CardCore: generic card container, dynamic named slots as rows, 4 variants, blurred backdrop layer, full CSS token API
109
109
  ├── action-menu.md — ActionMenu + ActionMenuItemCore: ellipsis trigger + anchored popover menu, indexed item-{n} slots, link/button items, full CSS token API
110
+ ├── select-menu.md — SelectMenu: v-model single-select listbox popover (ActionMenu's popover mechanics, InputSelectCore's selected-option semantics), icon/text/chevron trigger toggles, checkmark on selected option, options-array driven, full CSS token API
110
111
  ├── display-dialog.md — DisplayDialog: native <dialog> overlay, 5 variants (dialog/modal/confirm/alert/fullscreen), useDialogControls integration, CSS token API
111
112
  ├── display-chip.md — DisplayChip: status indicator chip overlay, CSS trig positioning, circle/square shapes, status colours, icon/label content
112
113
  ├── display-pill.md — DisplayPill: pill/badge label with icon slot, 6 variants, 3 sizes, reversible order, full CSS token API for border/outline/colour
@@ -127,6 +128,10 @@ Each skill is a single markdown file named `<area>-<task>.md`.
127
128
  ├── pop-over.md — PopOver: generic anchor-positioned disclosure panel, consumer-supplied trigger/content slots, placement prop, CSS token API
128
129
  ├── input-text-core.md — InputTextCore: native text/date/number input primitive, min/max pass-through (date-picker range), CSS token API; Variants section covers InputTextWithLabel/InputPasswordWithLabel/InputTextAsNumberWithLabel
129
130
  ├── input-range-core.md — InputRangeCore: native range-slider primitive, markers/datalist slots, dead --theme-form-range-accent-color token fixed, CSS token API; Variants section covers InputRangeDefault
131
+ ├── input-number-core.md — InputNumberCore: native number-input primitive, left/right step-button slots, missing base class/placeholder/mismatched slot-selector bugs fixed, CSS token API; Variants section covers InputNumberDefault
132
+ ├── input-textarea-core.md — InputTextareaCore: native textarea primitive, left/right decorative slots, undefined-token/dead-code/label-leak bugs fixed, CSS token API; Variants section covers InputTextareaWithLabel
133
+ ├── input-select-core.md — InputSelectCore: native select primitive, data-driven options with icon decorator, dead required/styleClassPassthrough/isDirty-isActive functionality fixed, appearance:base-select browser-support note, CSS token API; Variants section covers InputSelectWithLabel
134
+ ├── toggle-switch-core.md — ToggleSwitchCore: pill/square checkbox-backed toggle primitive, dead round-prop/data-theme-error functionality and story theme-options bugs fixed, CSS token API; Variants section covers ToggleSwitchWithLabel/ToggleSwitchWithLabelInline
130
135
  ├── entry-animation.md — EntryAnimation: scroll-driven entry animation wrapper (slide-in/zoom-reveal/exit-blur utility classes), skipAnimation for above-the-fold loop items, reduced-motion handled at the CSS layer
131
136
  ├── column-flow-grid.md — ColumnFlowGrid (renamed from MasonryGrid): CSS multi-column text-flow layout, named dynamic slots (no count/data prop), itemMinWidth/gap/unit sizing, CSS token API; not a true masonry, see "which one do I want?"
132
137
  ├── masonry-grid.md — MasonryGrid: real measured-height masonry (greedy shortest-column packing, animated resize) absorbed from the now-retired MasonryGridOrdered; named dynamic slots, fixedWidth/justify, CSS token API
@@ -0,0 +1,65 @@
1
+ {
2
+ "SRCDEV InputNumberCore Basic": {
3
+ "description": "Basic InputNumberCore native number input (no label/error wrapper)",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<InputNumberCore",
7
+ " id=\"$1fieldId\"",
8
+ " name=\"$2fieldName\"",
9
+ " v-model=\"$3fieldValue\"",
10
+ " :min=\"$40\"",
11
+ " :max=\"$5100\"",
12
+ " :step=\"$61\"",
13
+ "/>"
14
+ ]
15
+ },
16
+ "SRCDEV InputNumberDefault Basic": {
17
+ "description": "InputNumberDefault: labelled number input with error/description support",
18
+ "scope": "vue,html",
19
+ "body": [
20
+ "<InputNumberDefault",
21
+ " v-model=\"$1fieldValue\"",
22
+ " name=\"$2fieldName\"",
23
+ " label=\"$3Field label\"",
24
+ " :min=\"$40\"",
25
+ " :max=\"$5100\"",
26
+ " :step=\"$61\"",
27
+ " :error-message=\"$7errors.fieldName\"",
28
+ " :field-has-error=\"!!$7errors.fieldName\"",
29
+ "/>"
30
+ ]
31
+ },
32
+ "SRCDEV InputNumberDefault With Step Buttons": {
33
+ "description": "InputNumberDefault with step-down/step-up buttons wired into the left/right slots",
34
+ "scope": "vue,html",
35
+ "body": [
36
+ "<InputNumberDefault",
37
+ " v-model=\"$1fieldValue\"",
38
+ " name=\"$2fieldName\"",
39
+ " label=\"$3Field label\"",
40
+ " :min=\"$40\"",
41
+ " :max=\"$5100\"",
42
+ " :step=\"$61\"",
43
+ " :error-message=\"$7errors.fieldName\"",
44
+ " :field-has-error=\"!!$7errors.fieldName\"",
45
+ ">",
46
+ " <template #left>\\u2212</template>",
47
+ " <template #right>+</template>",
48
+ "</InputNumberDefault>"
49
+ ]
50
+ },
51
+ "SRCDEV InputNumber CSS Override": {
52
+ "description": "CSS override scaffold for InputNumberCore tokens",
53
+ "scope": "css",
54
+ "body": [
55
+ ".$1my-page {",
56
+ " .input-number-wrapper {",
57
+ " --input-number-surface: $2;",
58
+ " --input-number-border: $3;",
59
+ " --input-number-border-hover: $4;",
60
+ " --input-number-border-focus: $5;",
61
+ " }",
62
+ "}"
63
+ ]
64
+ }
65
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "SRCDEV InputSelectCore Basic": {
3
+ "description": "Basic InputSelectCore native select (no label/error wrapper)",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<InputSelectCore",
7
+ " id=\"$1fieldId\"",
8
+ " name=\"$2fieldName\"",
9
+ " v-model=\"$3fieldValue\"",
10
+ " v-model:field-data=\"$4fieldOptions\"",
11
+ " placeholder=\"$5Choose an option\"",
12
+ "/>"
13
+ ]
14
+ },
15
+ "SRCDEV InputSelectWithLabel Basic": {
16
+ "description": "InputSelectWithLabel: labelled select with error/description support",
17
+ "scope": "vue,html",
18
+ "body": [
19
+ "<InputSelectWithLabel",
20
+ " v-model=\"$1fieldValue\"",
21
+ " v-model:field-data=\"$2fieldOptions\"",
22
+ " name=\"$3fieldName\"",
23
+ " label=\"$4Field label\"",
24
+ " placeholder=\"$5Choose an option\"",
25
+ " :error-message=\"$6errors.fieldName\"",
26
+ " :field-has-error=\"!!$6errors.fieldName\"",
27
+ "/>"
28
+ ]
29
+ },
30
+ "SRCDEV InputSelect CSS Override": {
31
+ "description": "CSS override scaffold for InputSelectCore tokens",
32
+ "scope": "css",
33
+ "body": [
34
+ ".$1my-page {",
35
+ " .input-select-wrapper {",
36
+ " --input-select-surface: $2;",
37
+ " --input-select-border: $3;",
38
+ " }",
39
+ "}"
40
+ ]
41
+ }
42
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "SRCDEV InputTextareaCore Basic": {
3
+ "description": "Basic InputTextareaCore native textarea (no label/error wrapper)",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<InputTextareaCore",
7
+ " id=\"$1fieldId\"",
8
+ " name=\"$2fieldName\"",
9
+ " v-model=\"$3fieldValue\"",
10
+ " placeholder=\"$4Placeholder text\"",
11
+ "/>"
12
+ ]
13
+ },
14
+ "SRCDEV InputTextareaWithLabel Basic": {
15
+ "description": "InputTextareaWithLabel: labelled textarea with error/description support",
16
+ "scope": "vue,html",
17
+ "body": [
18
+ "<InputTextareaWithLabel",
19
+ " v-model=\"$1fieldValue\"",
20
+ " name=\"$2fieldName\"",
21
+ " label=\"$3Field label\"",
22
+ " placeholder=\"$4Placeholder text\"",
23
+ " :error-message=\"$5errors.fieldName\"",
24
+ " :field-has-error=\"!!$5errors.fieldName\"",
25
+ "/>"
26
+ ]
27
+ },
28
+ "SRCDEV InputTextarea CSS Override": {
29
+ "description": "CSS override scaffold for InputTextareaCore tokens",
30
+ "scope": "css",
31
+ "body": [
32
+ ".$1my-page {",
33
+ " .input-textarea-wrapper {",
34
+ " --input-textarea-surface: $2;",
35
+ " --input-textarea-border: $3;",
36
+ " }",
37
+ "}"
38
+ ]
39
+ }
40
+ }
@@ -0,0 +1,65 @@
1
+ {
2
+ "SRCDEV SelectMenu Basic": {
3
+ "description": "SelectMenu — v-model single-select popover listbox",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<SelectMenu",
7
+ " v-model=\"${1:selectedValue}\"",
8
+ " :options=\"${2:options}\"",
9
+ " label=\"${3:Choose an option}\"",
10
+ "/>"
11
+ ]
12
+ },
13
+ "SRCDEV SelectMenu Icon Only": {
14
+ "description": "SelectMenu — icon-only compact trigger (e.g. language switcher)",
15
+ "scope": "vue,html",
16
+ "body": [
17
+ "<SelectMenu",
18
+ " v-model=\"${1:language}\"",
19
+ " :options=\"${2:languageOptions}\"",
20
+ " label=\"${3:Language}\"",
21
+ " :show-label=\"false\"",
22
+ " :show-chevron=\"false\"",
23
+ "/>"
24
+ ]
25
+ },
26
+ "SRCDEV SelectMenu Category Filter": {
27
+ "description": "SelectMenu — text + chevron category filter, no per-option icons",
28
+ "scope": "vue,html",
29
+ "body": [
30
+ "<SelectMenu",
31
+ " v-model=\"${1:filterValue}\"",
32
+ " :options=\"${2:filterOptions}\"",
33
+ " label=\"${3:Choose a service}\"",
34
+ " :show-icon=\"false\"",
35
+ "/>"
36
+ ]
37
+ },
38
+ "SRCDEV SelectMenu Multi-Select": {
39
+ "description": "SelectMenu — checkbox-per-option multi-select, v-model is an array",
40
+ "scope": "vue,html",
41
+ "body": [
42
+ "<SelectMenu",
43
+ " v-model=\"${1:selectedValues}\"",
44
+ " :options=\"${2:options}\"",
45
+ " label=\"${3:Services required}\"",
46
+ " :show-icon=\"false\"",
47
+ " multiple",
48
+ "/>"
49
+ ]
50
+ },
51
+ "SRCDEV SelectMenu Multi-Select Showing Selection": {
52
+ "description": "SelectMenu — multi-select where the trigger updates to list checked options",
53
+ "scope": "vue,html",
54
+ "body": [
55
+ "<SelectMenu",
56
+ " v-model=\"${1:selectedValues}\"",
57
+ " :options=\"${2:options}\"",
58
+ " label=\"${3:Services required}\"",
59
+ " :show-icon=\"false\"",
60
+ " multiple",
61
+ " show-selection-in-trigger",
62
+ "/>"
63
+ ]
64
+ }
65
+ }
@@ -0,0 +1,49 @@
1
+ {
2
+ "SRCDEV ToggleSwitchCore": {
3
+ "description": "ToggleSwitchCore — bare toggle switch (no label, use ToggleSwitchWithLabel for that)",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<ToggleSwitchCore",
7
+ " v-model=\"$1modelValue\"",
8
+ " id=\"$2toggle-id\"",
9
+ " name=\"$3toggleName\"",
10
+ "/>"
11
+ ]
12
+ },
13
+ "SRCDEV ToggleSwitchWithLabel": {
14
+ "description": "ToggleSwitchWithLabel: labelled toggle switch with description/error support",
15
+ "scope": "vue,html",
16
+ "body": [
17
+ "<ToggleSwitchWithLabel",
18
+ " v-model=\"$1fieldValue\"",
19
+ " name=\"$2fieldName\"",
20
+ " label=\"$3Field label\"",
21
+ " :error-message=\"$4errors.fieldName\"",
22
+ " :field-has-error=\"!!$4errors.fieldName\"",
23
+ "/>"
24
+ ]
25
+ },
26
+ "SRCDEV ToggleSwitchWithLabelInline": {
27
+ "description": "ToggleSwitchWithLabelInline: compact label + toggle switch side by side, no error support",
28
+ "scope": "vue,html",
29
+ "body": [
30
+ "<ToggleSwitchWithLabelInline",
31
+ " v-model=\"$1fieldValue\"",
32
+ " name=\"$2fieldName\"",
33
+ " label=\"$3Field label\"",
34
+ "/>"
35
+ ]
36
+ },
37
+ "SRCDEV ToggleSwitch CSS Override": {
38
+ "description": "CSS override scaffold for ToggleSwitchCore tokens",
39
+ "scope": "css",
40
+ "body": [
41
+ ".$1my-page {",
42
+ " .toggle-switch-wrapper {",
43
+ " --toggle-switch-surface: $2;",
44
+ " --toggle-switch-symbol-surface-on: $3;",
45
+ " }",
46
+ "}"
47
+ ]
48
+ }
49
+ }
@@ -7,7 +7,7 @@ consuming page's own stylesheet:
7
7
 
8
8
  | Token | Default | Description |
9
9
  |---|---|---|
10
- | `--container-glow-background` | `light-dark(white, hsl(246 44% 7%))` | Card background colour. |
10
+ | `--container-glow-background` | `white` | Card background colour. |
11
11
  | `--container-glow-padding` | `2rem` | Card inner padding. |
12
12
  | `--container-glow-aspect-ratio` | `330 / 400` | Card aspect ratio. |
13
13
  | `--container-glow-border-radius` | `12px` | Card and glow-effect corner radius (shared by the card and every glow layer). |
@@ -191,7 +191,7 @@ onBeforeUnmount(() => {
191
191
  position: relative;
192
192
 
193
193
  height: 100%;
194
- background: var(--container-glow-background, light-dark(white, hsl(246 44% 7%)));
194
+ background: var(--container-glow-background, white);
195
195
  padding: var(--container-glow-padding, 2rem);
196
196
  aspect-ratio: var(--container-glow-aspect-ratio, 330 / 400);
197
197
  border-radius: var(--container-glow-border-radius, 12px);
@@ -11,7 +11,7 @@
11
11
  | `--marquee-scroller-item-hover-filter` | `grayscale(0)` | Filter applied to a single item when it is individually hovered (overrides the group filter for that item) |
12
12
  | `--marquee-scroller-item-transition-duration` | `0.5s` | Transition duration for the item hover filter change |
13
13
  | `--marquee-scroller-item-border-width` | `1px` | Item border width |
14
- | `--marquee-scroller-item-border-colour` | `light-dark(var(--slate-10), var(--slate-00))` | Item border colour |
14
+ | `--marquee-scroller-item-border-colour` | `var(--slate-10)` | Item border colour |
15
15
  | `--marquee-scroller-item-border-radius` | `4px` | Item border radius |
16
16
  | `--marquee-scroller-control-offset` | `8px` | Distance of the pause/play button from the top and end edges |
17
17
  | `--marquee-scroller-control-background-colour` | `rgba(0, 0, 0, 0.7)` | Pause/play button background |