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,102 @@
1
+ <template>
2
+ <ClientOnly>
3
+ <TripleToggleSwitchCore
4
+ v-model="colorModeVal"
5
+ v-model:field-data="sampleFieldData"
6
+ :style-class-passthrough="[`colour-scheme-select`, elementClasses]"
7
+ />
8
+ </ClientOnly>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ import type { IFormMultipleOptions } from "~/types/forms/types.forms";
13
+ interface Props {
14
+ styleClassPassthrough?: string | string[];
15
+ systemLabel?: string;
16
+ lightLabel?: string;
17
+ darkLabel?: string;
18
+ systemIcon?: string;
19
+ lightIcon?: string;
20
+ darkIcon?: string;
21
+ }
22
+
23
+ const props = withDefaults(defineProps<Props>(), {
24
+ styleClassPassthrough: () => [],
25
+ systemLabel: "System",
26
+ lightLabel: "Light",
27
+ darkLabel: "Dark",
28
+ systemIcon: "material-symbols:night-sight-auto-sharp",
29
+ lightIcon: "radix-icons:sun",
30
+ darkIcon: "radix-icons:moon",
31
+ });
32
+
33
+ const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
34
+
35
+ const sampleFieldData = computed<IFormMultipleOptions>(() => ({
36
+ data: [
37
+ {
38
+ id: "system",
39
+ name: "colorModeVal",
40
+ value: "system",
41
+ label: props.systemLabel,
42
+ icon: props.systemIcon,
43
+ },
44
+ {
45
+ id: "light",
46
+ name: "colorModeVal",
47
+ value: "light",
48
+ label: props.lightLabel,
49
+ icon: props.lightIcon,
50
+ },
51
+ {
52
+ id: "dark",
53
+ name: "colorModeVal",
54
+ value: "dark",
55
+ label: props.darkLabel,
56
+ icon: props.darkIcon,
57
+ },
58
+ ],
59
+ total: 3,
60
+ skip: 0,
61
+ limit: 3,
62
+ }));
63
+
64
+ const { colourScheme, setColourScheme } = useSettingsStore();
65
+
66
+ const colorModeVal = ref(colourScheme);
67
+ watch(colorModeVal, (val) => {
68
+ setColourScheme(val);
69
+ });
70
+ </script>
71
+
72
+ <style lang="css">
73
+ @layer components {
74
+ .colour-scheme-select {
75
+ /* Everything DisplayThemeSwitch used to redeclare here beyond sizing (wrapper background/border/
76
+ focus ring, marker border, option border/outline/focus/hover, option-icon colours, and the
77
+ system/light/dark marker gradients) was a byte-for-byte duplicate of TripleToggleSwitchCore's
78
+ own defaults for the exact same selectors — a no-op at best, and for the ones that bypassed
79
+ TripleToggleSwitchCore's public --triple-toggle-switch-* tokens with a hardcoded literal
80
+ (background-color, border, focus outline, marker border, option border/hover/focus, icon
81
+ colour) an active bug: a consumer setting e.g. --triple-toggle-switch-surface globally would
82
+ have been silently overridden back to the hardcoded default by this block's higher
83
+ specificity. Removed 2026-09-20 — TripleToggleSwitchCore already owns all of that. Only the
84
+ genuine "small" sizing variant remains, now driving TripleToggleSwitchCore's own public
85
+ sizing tokens instead of reaching into its private --_form-* and --_scheme-icon-font-size
86
+ locals directly (see TripleToggleSwitchCore's CONSUMER-STYLING.md). */
87
+ &.triple-toggle-switch {
88
+ --triple-toggle-switch-gap: 0.4rem;
89
+ --triple-toggle-switch-padding: 0.4rem;
90
+ --triple-toggle-switch-option-padding: 0.4rem;
91
+ --triple-toggle-switch-icon-size: 2rem;
92
+
93
+ &.small {
94
+ --triple-toggle-switch-gap: 0.2rem;
95
+ --triple-toggle-switch-padding: 0.2rem;
96
+ --triple-toggle-switch-option-padding: 0.2rem;
97
+ --triple-toggle-switch-icon-size: 1.6rem;
98
+ }
99
+ }
100
+ }
101
+ }
102
+ </style>
@@ -0,0 +1,115 @@
1
+ import DisplayThemeSwitch from "../DisplayThemeSwitch.vue";
2
+ import type { Meta, StoryObj } from "@nuxtjs/storybook";
3
+
4
+ const meta: Meta<typeof DisplayThemeSwitch> = {
5
+ title: "Molecules/DisplayThemeSwitch",
6
+ component: DisplayThemeSwitch,
7
+ argTypes: {
8
+ styleClassPassthrough: {
9
+ control: "object",
10
+ description: "Extra classes — pass \"small\" for a compact size",
11
+ table: { category: "Styling" },
12
+ },
13
+ systemLabel: {
14
+ control: "text",
15
+ description: "Accessible label for the system option",
16
+ table: { category: "Labels" },
17
+ },
18
+ lightLabel: {
19
+ control: "text",
20
+ description: "Accessible label for the light option",
21
+ table: { category: "Labels" },
22
+ },
23
+ darkLabel: {
24
+ control: "text",
25
+ description: "Accessible label for the dark option",
26
+ table: { category: "Labels" },
27
+ },
28
+ systemIcon: {
29
+ control: "text",
30
+ description: "Icon name for the system option",
31
+ table: { category: "Icons" },
32
+ },
33
+ lightIcon: {
34
+ control: "text",
35
+ description: "Icon name for the light option",
36
+ table: { category: "Icons" },
37
+ },
38
+ darkIcon: {
39
+ control: "text",
40
+ description: "Icon name for the dark option",
41
+ table: { category: "Icons" },
42
+ },
43
+ },
44
+ parameters: {
45
+ docs: {
46
+ description: {
47
+ component:
48
+ "A system/light/dark theme switch wrapping TripleToggleSwitchCore, wired to useSettingsStore. Colour and sizing are entirely delegated to TripleToggleSwitchCore's public --triple-toggle-switch-* tokens — see CONSUMER-STYLING.md.",
49
+ },
50
+ },
51
+ },
52
+ };
53
+
54
+ export default meta;
55
+ type Story = StoryObj<typeof DisplayThemeSwitch>;
56
+
57
+ export const Default: Story = {
58
+ args: {},
59
+ render: (args) => ({
60
+ components: { DisplayThemeSwitch },
61
+ setup() {
62
+ return { args };
63
+ },
64
+ template: `<DisplayThemeSwitch v-bind="args" />`,
65
+ }),
66
+ };
67
+
68
+ export const Small: Story = {
69
+ args: {
70
+ styleClassPassthrough: "small",
71
+ },
72
+ render: (args) => ({
73
+ components: { DisplayThemeSwitch },
74
+ setup() {
75
+ return { args };
76
+ },
77
+ template: `<DisplayThemeSwitch v-bind="args" />`,
78
+ }),
79
+ parameters: {
80
+ docs: {
81
+ description: {
82
+ story: "Compact sizing for use in a tight header/nav slot.",
83
+ },
84
+ },
85
+ },
86
+ };
87
+
88
+ export const InNavigation: Story = {
89
+ args: {
90
+ styleClassPassthrough: "small",
91
+ },
92
+ render: (args) => ({
93
+ components: { DisplayThemeSwitch },
94
+ setup() {
95
+ return { args };
96
+ },
97
+ template: `
98
+ <nav style="display: flex; align-items: center; justify-content: space-between; padding: 1.6rem 2.4rem; background: var(--theme-surface); border-radius: 0.8rem; border: 1px solid var(--theme-border);">
99
+ <div style="display: flex; align-items: center; gap: 1.6rem;">
100
+ <span style="font-weight: 600; color: var(--theme-text);">My App</span>
101
+ <a href="#" style="color: var(--theme-text); text-decoration: none;">Home</a>
102
+ <a href="#" style="color: var(--theme-text); text-decoration: none;">About</a>
103
+ </div>
104
+ <DisplayThemeSwitch v-bind="args" />
105
+ </nav>
106
+ `,
107
+ }),
108
+ parameters: {
109
+ docs: {
110
+ description: {
111
+ story: "Example of the theme switch integrated into a navigation bar context.",
112
+ },
113
+ },
114
+ },
115
+ };
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { mountSuspended, mockNuxtImport } from "@nuxt/test-utils/runtime";
3
+ import { nextTick, ref } from "vue";
4
+ import DisplayThemeSwitch from "../DisplayThemeSwitch.vue";
5
+
6
+ const { useSettingsStoreMock, setColourSchemeMock } = vi.hoisted(() => ({
7
+ useSettingsStoreMock: vi.fn(),
8
+ setColourSchemeMock: vi.fn(),
9
+ }));
10
+
11
+ mockNuxtImport("useSettingsStore", () => useSettingsStoreMock);
12
+
13
+ describe("DisplayThemeSwitch", () => {
14
+ beforeEach(() => {
15
+ setColourSchemeMock.mockClear();
16
+ useSettingsStoreMock.mockReturnValue({
17
+ colourScheme: ref("system"),
18
+ setColourScheme: setColourSchemeMock,
19
+ });
20
+ });
21
+
22
+ it("mounts without error", async () => {
23
+ const wrapper = await mountSuspended(DisplayThemeSwitch);
24
+ await nextTick();
25
+ expect(wrapper.vm).toBeTruthy();
26
+ });
27
+
28
+ it("renders TripleToggleSwitchCore with the colour-scheme-select class", async () => {
29
+ const wrapper = await mountSuspended(DisplayThemeSwitch);
30
+ await nextTick();
31
+ expect(wrapper.find(".colour-scheme-select").exists()).toBe(true);
32
+ expect(wrapper.find(".triple-toggle-switch").exists()).toBe(true);
33
+ });
34
+
35
+ it("renders default option labels", async () => {
36
+ const wrapper = await mountSuspended(DisplayThemeSwitch);
37
+ await nextTick();
38
+ const labels = wrapper.findAll(".option-group .sr-only").map((el) => el.text());
39
+ expect(labels).toEqual(["System", "Light", "Dark"]);
40
+ });
41
+
42
+ it("renders custom option labels via props", async () => {
43
+ const wrapper = await mountSuspended(DisplayThemeSwitch, {
44
+ props: {
45
+ systemLabel: "Auto",
46
+ lightLabel: "Day",
47
+ darkLabel: "Night",
48
+ },
49
+ });
50
+ await nextTick();
51
+ const labels = wrapper.findAll(".option-group .sr-only").map((el) => el.text());
52
+ expect(labels).toEqual(["Auto", "Day", "Night"]);
53
+ });
54
+
55
+ it("applies custom option icons via props", async () => {
56
+ const wrapper = await mountSuspended(DisplayThemeSwitch, {
57
+ props: {
58
+ systemIcon: "custom:system",
59
+ lightIcon: "custom:light",
60
+ darkIcon: "custom:dark",
61
+ },
62
+ });
63
+ await nextTick();
64
+ const icons = wrapper.findAll(".option-icon").map((el) => el.attributes("class"));
65
+ expect(icons.some((c) => c?.includes("system"))).toBe(true);
66
+ });
67
+
68
+ it("applies the small class via styleClassPassthrough", async () => {
69
+ const wrapper = await mountSuspended(DisplayThemeSwitch, {
70
+ props: { styleClassPassthrough: "small" },
71
+ });
72
+ await nextTick();
73
+ expect(wrapper.find(".triple-toggle-switch").classes()).toContain("small");
74
+ });
75
+
76
+ it("calls setColourScheme when the selected option changes", async () => {
77
+ const wrapper = await mountSuspended(DisplayThemeSwitch);
78
+ await nextTick();
79
+
80
+ const lightInput = wrapper.findAll("input.option-input")[1];
81
+ await lightInput?.setValue(true);
82
+ await nextTick();
83
+
84
+ expect(setColourSchemeMock).toHaveBeenCalledWith("light");
85
+ });
86
+ });
@@ -7,7 +7,7 @@
7
7
  | `--deep-expanding-menu-gap` | `2.4rem` | Gap between top-level items |
8
8
  | `--deep-expanding-menu-link-border-width` | `0.2rem` | Bottom border width on links and group toggles |
9
9
  | `--deep-expanding-menu-link-padding-block` | `0.8rem` | Vertical padding on links and group toggles |
10
- | `--deep-expanding-menu-link-border-colour-hover` | `light-dark(var(--blue-10), var(--slate-00))` | Bottom border colour on hover/focus |
10
+ | `--deep-expanding-menu-link-border-colour-hover` | `var(--blue-10)` | Bottom border colour on hover/focus |
11
11
  | `--deep-expanding-menu-icon-size` | `1.2rem` | Caret icon size |
12
12
  | `--deep-expanding-menu-panel-width` | `min(100%, 50vw)` | Popover panel width |
13
13
  | `--deep-expanding-menu-panel-background-colour` | `white` | Popover panel background |
@@ -92,7 +92,7 @@ watch(
92
92
  cursor: pointer;
93
93
  border-color: var(
94
94
  --deep-expanding-menu-link-border-colour-hover,
95
- light-dark(var(--blue-10), var(--slate-00))
95
+ var(--blue-10)
96
96
  );
97
97
  }
98
98
  }
@@ -8,7 +8,7 @@
8
8
  | `--deep-expanding-menu-classic-item-gap` | `2.4rem` | Gap between top-level items |
9
9
  | `--deep-expanding-menu-classic-link-border-width` | `0.2rem` | Bottom border width on links and group toggles |
10
10
  | `--deep-expanding-menu-classic-link-padding-block` | `0.8rem` | Vertical padding on links and group toggles |
11
- | `--deep-expanding-menu-classic-link-border-colour-hover` | `light-dark(var(--blue-10), var(--slate-00))` | Bottom border colour on hover/focus |
11
+ | `--deep-expanding-menu-classic-link-border-colour-hover` | `var(--blue-10)` | Bottom border colour on hover/focus |
12
12
  | `--deep-expanding-menu-classic-icon-size` | `1.2rem` | Caret icon size |
13
13
  | `--deep-expanding-menu-classic-panel-offset-top` | `4rem` | Panel offset from the toggle |
14
14
  | `--deep-expanding-menu-classic-panel-width` | `20rem` | Panel width below the `768px` breakpoint |
@@ -89,7 +89,7 @@ onMounted(() => {
89
89
  cursor: pointer;
90
90
  border-color: var(
91
91
  --deep-expanding-menu-classic-link-border-colour-hover,
92
- light-dark(var(--blue-10), var(--slate-00))
92
+ var(--blue-10)
93
93
  );
94
94
  }
95
95
  }
@@ -0,0 +1,168 @@
1
+ # SelectMenu — Consumer Styling Guide
2
+
3
+ ## Public token API
4
+
5
+ All `--select-menu-*` tokens are the stable override surface, each falling back to a `--theme-*`
6
+ global token so it matches the rest of the design system out of the box.
7
+
8
+ ### Trigger button
9
+
10
+ | Token | Default | Controls |
11
+ |---|---|---|
12
+ | `--select-menu-trigger-gap` | `0.8rem` | Gap between icon / label / chevron |
13
+ | `--select-menu-trigger-min-height` | `4.4rem` | Trigger minimum height |
14
+ | `--select-menu-trigger-padding-block` | `0.8rem` | Trigger vertical padding |
15
+ | `--select-menu-trigger-padding-inline` | `1.2rem` | Trigger horizontal padding |
16
+ | `--select-menu-trigger-border-width` | `0.1rem` | Trigger border thickness |
17
+ | `--select-menu-trigger-border` | `var(--theme-border)` | Trigger border colour (rest state) |
18
+ | `--select-menu-trigger-border-focus` | `var(--theme-border-focus)` | Trigger border colour on hover/focus |
19
+ | `--select-menu-trigger-border-radius` | `0.5rem` | Trigger corner rounding |
20
+ | `--select-menu-trigger-surface` | `var(--theme-input-surface)` | Trigger background |
21
+ | `--select-menu-trigger-text-color` | `var(--theme-text)` | Trigger label text colour |
22
+ | `--select-menu-trigger-font-size` | `1.6rem` | Trigger label font size |
23
+ | `--select-menu-trigger-outline-width` | `0.2rem` | Focus-visible outline width (trigger and items) |
24
+ | `--select-menu-trigger-icon-size` | `2rem` | Selected option's icon size in the trigger |
25
+ | `--select-menu-trigger-chevron-size` | `1.6rem` | Chevron icon size |
26
+
27
+ ### Menu popover
28
+
29
+ | Token | Default | Controls |
30
+ |---|---|---|
31
+ | `--select-menu-block-distance` | `0.4rem` | Gap between trigger bottom and menu top |
32
+ | `--select-menu-popover-border-width` | `0.1rem` | Popover border thickness |
33
+ | `--select-menu-popover-border` | `var(--theme-border)` | Popover border colour |
34
+ | `--select-menu-popover-surface` | `var(--theme-input-surface)` | Popover background |
35
+ | `--select-menu-popover-border-radius` | `0.5rem` | Popover corner rounding |
36
+ | `--select-menu-popover-min-width` | `18rem` | Minimum popover width |
37
+ | `--select-menu-popover-max-height` | `32rem` | Maximum popover height before scrolling |
38
+ | `--select-menu-popover-shadow` | `0 0.4rem 1.6rem rgb(0 0 0 / 12%)` | Popover drop shadow |
39
+ | `--select-menu-transition-duration` | `200ms` | Open/close fade and chevron-rotate duration |
40
+
41
+ ### Options
42
+
43
+ | Token | Default | Controls |
44
+ |---|---|---|
45
+ | `--select-menu-item-gap` | `0.8rem` | Gap between checkmark / icon / label in a row |
46
+ | `--select-menu-item-padding-block` | `1rem` | Option row vertical padding |
47
+ | `--select-menu-item-padding-inline` | `1.2rem` | Option row horizontal padding |
48
+ | `--select-menu-item-text-color` | `var(--theme-text)` | Option label colour |
49
+ | `--select-menu-item-font-size` | `1.5rem` | Option label font size |
50
+ | `--select-menu-item-surface-hover` | `var(--theme-input-surface-hover)` | Option row background on hover/focus |
51
+ | `--select-menu-item-check-size` | `1.6rem` | Checkmark icon box size |
52
+ | `--select-menu-item-check-color` | `currentcolor` | Checkmark icon colour |
53
+ | `--select-menu-item-icon-size` | `1.8rem` | Per-option icon size |
54
+
55
+ ---
56
+
57
+ ## Global theming — recommended approach
58
+
59
+ Create `assets/styles/setup/07.components/select-menu.css` in the consuming app and set tokens
60
+ on `:root`. This applies to every `SelectMenu` across the site.
61
+
62
+ ```css
63
+ /* assets/styles/setup/07.components/select-menu.css */
64
+ :root {
65
+ --select-menu-trigger-border-radius: 999rem;
66
+ --select-menu-trigger-surface: var(--brand-surface);
67
+ --select-menu-trigger-border: var(--brand-border);
68
+
69
+ --select-menu-popover-surface: var(--brand-surface);
70
+ --select-menu-popover-border: var(--brand-border);
71
+ --select-menu-popover-shadow: 0 0.8rem 2.4rem rgba(0, 0, 0, 0.15);
72
+
73
+ --select-menu-item-surface-hover: var(--brand-surface-subtle);
74
+ --select-menu-item-check-color: var(--brand-accent);
75
+ }
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Per-instance overrides via styleClassPassthrough
81
+
82
+ ```vue
83
+ <SelectMenu
84
+ v-model="language"
85
+ :options="languageOptions"
86
+ label="Language"
87
+ :style-class-passthrough="['compact-switcher']"
88
+ />
89
+ ```
90
+
91
+ ```css
92
+ .select-menu.compact-switcher {
93
+ --select-menu-trigger-min-height: 3.2rem;
94
+ --select-menu-trigger-padding-inline: 0.8rem;
95
+ --select-menu-trigger-border-radius: 50%;
96
+ }
97
+ ```
98
+
99
+ ---
100
+
101
+ ## Trigger content variants
102
+
103
+ `showIcon` / `showLabel` / `showChevron` control what renders in the trigger — combine them for
104
+ the three common shapes:
105
+
106
+ | Variant | Props |
107
+ |---|---|
108
+ | Icon-only (e.g. flag switcher in a tight header) | `:show-label="false" :show-chevron="false"` |
109
+ | Text + chevron (e.g. a category filter with no per-option icons) | `:show-icon="false"` (default `showLabel`/`showChevron`) |
110
+ | Icon + text + chevron (full select) | Defaults — no overrides needed |
111
+
112
+ ---
113
+
114
+ ## Single-select vs multi-select
115
+
116
+ By default `SelectMenu` is single-select: `v-model` is `string | number | undefined`, the selected
117
+ option shows a checkmark, and picking one closes the popover.
118
+
119
+ Set `multiple` for a checkbox-per-option multi-select instead: `v-model` becomes
120
+ `(string | number)[]`, every option shows a checkbox (checked/unchecked), and picking an option
121
+ toggles it in the array without closing the popover — so several can be picked in one open/close
122
+ cycle. By default the trigger text stays fixed on `placeholder`/`label` as a static category tag
123
+ (e.g. `"Services required"`) regardless of how many options are checked, since a comma-joined list
124
+ of checked options would grow unpredictably long. Set `showSelectionInTrigger` to opt into that
125
+ comma-joined list instead (falling back to `placeholder`/`label` when nothing is checked). The
126
+ trigger icon is not shown in multi-select mode (`showIcon` has no effect), since there's no single
127
+ selected option to represent.
128
+
129
+ ```vue
130
+ <script setup lang="ts">
131
+ const treatments = ref<string[]>([]);
132
+ const treatmentOptions = [
133
+ { value: "trim", label: "Trim" },
134
+ { value: "layers", label: "Layers" },
135
+ { value: "restyle", label: "Restyle" },
136
+ { value: "straightening", label: "Straightening" },
137
+ ];
138
+ </script>
139
+
140
+ <template>
141
+ <SelectMenu v-model="treatments" :options="treatmentOptions" label="Services required" multiple />
142
+
143
+ <!-- Trigger updates to "Layers, Restyle" etc. as options are checked -->
144
+ <SelectMenu
145
+ v-model="treatments"
146
+ :options="treatmentOptions"
147
+ label="Services required"
148
+ multiple
149
+ show-selection-in-trigger
150
+ />
151
+ </template>
152
+ ```
153
+
154
+ For several independent single-select filter categories (rather than one multi-select category),
155
+ place multiple single-select `SelectMenu` instances side by side instead — see the "Filter Bar"
156
+ story.
157
+
158
+ ---
159
+
160
+ ## Notes
161
+
162
+ - **Popover API + CSS anchor positioning** — same mechanism as `ActionMenu`. Broadly supported
163
+ (Chrome 114+, Firefox 125+, Safari 17+). No polyfill is included.
164
+ - **Popover left-aligns with the trigger** by default (`left: anchor(left)`), unlike `ActionMenu`
165
+ which right-aligns — matches native `<select>` dropdown behaviour. Flips above the trigger near
166
+ the bottom of the viewport (`position-try-fallbacks: flip-block`).
167
+ - **Chevron rotation** is pure CSS via `:has(.select-menu-popover:popover-open)` — no JS state
168
+ needed for the visual, though `isOpen` is still tracked internally to drive `aria-expanded`.