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,384 @@
1
+ <template>
2
+ <div class="select-menu" :class="[elementClasses]" :style="`--_anchor-name: ${anchorName}`">
3
+ <button
4
+ ref="triggerRef"
5
+ :popovertarget="menuId"
6
+ popovertargetaction="toggle"
7
+ type="button"
8
+ class="select-menu-trigger"
9
+ :aria-label="label"
10
+ aria-haspopup="listbox"
11
+ :aria-expanded="isOpen"
12
+ >
13
+ <Icon
14
+ v-if="showIcon && !multiple && selectedOption?.icon"
15
+ :name="selectedOption.icon"
16
+ class="select-menu-trigger-icon"
17
+ aria-hidden="true"
18
+ />
19
+ <span v-if="showLabel" class="select-menu-trigger-label">{{ triggerLabelText }}</span>
20
+ <Icon
21
+ v-if="showChevron"
22
+ name="lucide:chevron-down"
23
+ class="select-menu-trigger-chevron"
24
+ aria-hidden="true"
25
+ />
26
+ </button>
27
+
28
+ <div
29
+ :id="menuId"
30
+ ref="popoverRef"
31
+ popover
32
+ class="select-menu-popover"
33
+ @toggle="handleToggle"
34
+ @keydown="handleKeydown"
35
+ >
36
+ <ul class="select-menu-list" role="listbox" :aria-label="label" :aria-multiselectable="multiple || undefined">
37
+ <li
38
+ v-for="option in options"
39
+ :key="option.value"
40
+ class="select-menu-list-item"
41
+ role="option"
42
+ tabindex="-1"
43
+ :aria-selected="isSelected(option)"
44
+ @click="selectOption(option)"
45
+ >
46
+ <span class="select-menu-item-check" aria-hidden="true">
47
+ <Icon
48
+ v-if="multiple"
49
+ :name="isSelected(option) ? 'lucide:square-check' : 'lucide:square'"
50
+ class="select-menu-item-check-icon"
51
+ />
52
+ <Icon v-else-if="isSelected(option)" name="lucide:check" class="select-menu-item-check-icon" />
53
+ </span>
54
+ <Icon v-if="option.icon" :name="option.icon" class="select-menu-item-icon" aria-hidden="true" />
55
+ <span class="select-menu-item-label">{{ option.label }}</span>
56
+ </li>
57
+ </ul>
58
+ </div>
59
+ </div>
60
+ </template>
61
+
62
+ <script setup lang="ts">
63
+ import type { SelectMenuOption } from "~/types/components/select-menu";
64
+
65
+ interface Props {
66
+ /** The full list of selectable options. */
67
+ options: SelectMenuOption[];
68
+ /** Accessible name for the trigger button and the listbox. Also used as the trigger's fallback label when nothing is selected and no placeholder is given. */
69
+ label: string;
70
+ /** Text shown in the trigger when no option is selected. Falls back to `label`. */
71
+ placeholder?: string;
72
+ /** Show the selected option's icon in the trigger. */
73
+ showIcon?: boolean;
74
+ /** Show the selected option's label (or placeholder) text in the trigger. Set to false for an icon-only compact trigger. */
75
+ showLabel?: boolean;
76
+ /** Show the trailing chevron in the trigger. */
77
+ showChevron?: boolean;
78
+ /** Allow selecting more than one option. Each option gets a checkbox indicator, and v-model becomes an array. Selecting an option leaves the popover open so more can be toggled. */
79
+ multiple?: boolean;
80
+ /** In multiple mode, update the trigger text to a comma-separated list of the currently checked options instead of leaving it fixed on placeholder/label. No effect outside multiple mode. */
81
+ showSelectionInTrigger?: boolean;
82
+ styleClassPassthrough?: string | string[];
83
+ }
84
+
85
+ const props = withDefaults(defineProps<Props>(), {
86
+ placeholder: undefined,
87
+ showIcon: true,
88
+ showLabel: true,
89
+ showChevron: true,
90
+ multiple: false,
91
+ showSelectionInTrigger: false,
92
+ styleClassPassthrough: () => [],
93
+ });
94
+
95
+ const modelValue = defineModel<string | number | (string | number)[] | undefined>({ default: undefined });
96
+
97
+ const id = useId();
98
+ const menuId = `select-menu-${id}`;
99
+ const anchorName = `--select-menu-anchor-${id}`;
100
+
101
+ const triggerRef = ref<HTMLButtonElement | null>(null);
102
+ const popoverRef = ref<HTMLDivElement | null>(null);
103
+ const isOpen = ref(false);
104
+
105
+ const selectedValues = computed(() => (props.multiple && Array.isArray(modelValue.value) ? modelValue.value : []));
106
+ const selectedOption = computed(() =>
107
+ props.multiple ? undefined : props.options.find((option) => option.value === modelValue.value)
108
+ );
109
+ const selectedOptions = computed(() => props.options.filter((option) => selectedValues.value.includes(option.value)));
110
+
111
+ /**
112
+ * In multi-select mode the trigger shows `placeholder`/`label` as a static
113
+ * category tag (e.g. "Services required") by default — it does not update
114
+ * to reflect the current selection, since a comma-joined list of checked
115
+ * options would grow unpredictably long and push on adjacent triggers. Set
116
+ * `showSelectionInTrigger` to opt into the comma-joined list instead.
117
+ */
118
+ const triggerLabelText = computed(() => {
119
+ if (props.multiple) {
120
+ if (props.showSelectionInTrigger && selectedOptions.value.length) {
121
+ return selectedOptions.value.map((option) => option.label).join(", ");
122
+ }
123
+ return props.placeholder ?? props.label;
124
+ }
125
+ return selectedOption.value?.label ?? props.placeholder ?? props.label;
126
+ });
127
+
128
+ const isSelected = (option: SelectMenuOption): boolean =>
129
+ props.multiple ? selectedValues.value.includes(option.value) : option.value === modelValue.value;
130
+
131
+ /** Returns all options in DOM order. */
132
+ const getMenuItems = (): HTMLElement[] =>
133
+ Array.from(popoverRef.value?.querySelectorAll<HTMLElement>('[role="option"]') ?? []);
134
+
135
+ const selectOption = (option: SelectMenuOption) => {
136
+ if (props.multiple) {
137
+ const next = selectedValues.value.includes(option.value)
138
+ ? selectedValues.value.filter((value) => value !== option.value)
139
+ : [...selectedValues.value, option.value];
140
+ modelValue.value = next;
141
+ return;
142
+ }
143
+ modelValue.value = option.value;
144
+ popoverRef.value?.hidePopover();
145
+ triggerRef.value?.focus();
146
+ };
147
+
148
+ const handleToggle = (event: Event) => {
149
+ const toggleEvent = event as ToggleEvent;
150
+ isOpen.value = toggleEvent.newState === "open";
151
+ if (isOpen.value) {
152
+ const items = getMenuItems();
153
+ const selectedIndex = items.findIndex((el) => el.getAttribute("aria-selected") === "true");
154
+ items[selectedIndex === -1 ? 0 : selectedIndex]?.focus();
155
+ }
156
+ };
157
+
158
+ /**
159
+ * Keyboard navigation following the WAI-ARIA listbox pattern.
160
+ *
161
+ * ArrowDown / ArrowUp — move between options (wraps around).
162
+ * Home / End — jump to first / last option.
163
+ * Enter / Space — select the focused option. Closes the menu in single-select mode;
164
+ * toggles the checkbox and keeps the menu open in multi-select mode.
165
+ * Tab — close the menu; let the browser Tab naturally.
166
+ * Escape — handled natively by the Popover API.
167
+ */
168
+ const handleKeydown = (event: KeyboardEvent) => {
169
+ const items = getMenuItems();
170
+ if (!items.length) return;
171
+
172
+ const currentIndex = items.indexOf(document.activeElement as HTMLElement);
173
+
174
+ switch (event.key) {
175
+ case "ArrowDown":
176
+ event.preventDefault();
177
+ items[currentIndex === -1 ? 0 : (currentIndex + 1) % items.length]?.focus();
178
+ break;
179
+ case "ArrowUp":
180
+ event.preventDefault();
181
+ items[currentIndex === -1 ? items.length - 1 : (currentIndex - 1 + items.length) % items.length]?.focus();
182
+ break;
183
+ case "Home":
184
+ event.preventDefault();
185
+ items[0]?.focus();
186
+ break;
187
+ case "End":
188
+ event.preventDefault();
189
+ items[items.length - 1]?.focus();
190
+ break;
191
+ case "Enter":
192
+ case " ": {
193
+ event.preventDefault();
194
+ const option = props.options[currentIndex === -1 ? 0 : currentIndex];
195
+ if (option) selectOption(option);
196
+ break;
197
+ }
198
+ case "Tab":
199
+ popoverRef.value?.hidePopover();
200
+ break;
201
+ }
202
+ };
203
+
204
+ const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
205
+
206
+ watch(
207
+ () => props.styleClassPassthrough,
208
+ () => {
209
+ resetElementClasses(props.styleClassPassthrough);
210
+ }
211
+ );
212
+ </script>
213
+
214
+ <style lang="css">
215
+ @layer components {
216
+ .select-menu {
217
+ position: relative;
218
+ display: inline-block;
219
+
220
+ .select-menu-trigger {
221
+ all: unset;
222
+ box-sizing: border-box;
223
+ cursor: pointer;
224
+ display: flex;
225
+ align-items: center;
226
+ gap: var(--select-menu-trigger-gap, 0.8rem);
227
+ min-height: var(--select-menu-trigger-min-height, 4.4rem);
228
+ padding-block: var(--select-menu-trigger-padding-block, 0.8rem);
229
+ padding-inline: var(--select-menu-trigger-padding-inline, 1.2rem);
230
+ border: var(--select-menu-trigger-border-width, 0.1rem) solid var(--select-menu-trigger-border, var(--theme-border));
231
+ border-radius: var(--select-menu-trigger-border-radius, 0.5rem);
232
+ background-color: var(--select-menu-trigger-surface, var(--theme-input-surface));
233
+ color: var(--select-menu-trigger-text-color, var(--theme-text));
234
+ font-family: var(--font-family);
235
+ font-size: var(--select-menu-trigger-font-size, 1.6rem);
236
+ line-height: 1.2;
237
+ anchor-name: var(--_anchor-name);
238
+ transition: border-color var(--select-menu-transition-duration, 200ms) ease;
239
+
240
+ &:hover,
241
+ &:focus-visible {
242
+ border-color: var(--select-menu-trigger-border-focus, var(--theme-border-focus));
243
+ }
244
+
245
+ &:focus-visible {
246
+ outline: var(--select-menu-trigger-outline-width, 0.2rem) solid var(--theme-ring, currentcolor);
247
+ outline-offset: 0.2rem;
248
+ }
249
+
250
+ .select-menu-trigger-icon {
251
+ display: block;
252
+ flex-shrink: 0;
253
+ width: var(--select-menu-trigger-icon-size, 2rem);
254
+ height: var(--select-menu-trigger-icon-size, 2rem);
255
+ }
256
+
257
+ .select-menu-trigger-label {
258
+ flex: 1;
259
+ min-width: 0;
260
+ white-space: nowrap;
261
+ overflow: hidden;
262
+ text-overflow: ellipsis;
263
+ text-align: start;
264
+ }
265
+
266
+ .select-menu-trigger-chevron {
267
+ display: block;
268
+ flex-shrink: 0;
269
+ width: var(--select-menu-trigger-chevron-size, 1.6rem);
270
+ height: var(--select-menu-trigger-chevron-size, 1.6rem);
271
+ opacity: 0.6;
272
+ transition: transform var(--select-menu-transition-duration, 200ms) ease;
273
+ }
274
+ }
275
+
276
+ &:has(.select-menu-popover:popover-open) .select-menu-trigger-chevron {
277
+ transform: rotate(180deg);
278
+ }
279
+
280
+ .select-menu-popover {
281
+ border: var(--select-menu-popover-border-width, 0.1rem) solid
282
+ var(--select-menu-popover-border, var(--theme-border));
283
+ margin: 0;
284
+ padding: 0;
285
+ inset: auto;
286
+ background-color: var(--select-menu-popover-surface, var(--theme-input-surface));
287
+ border-radius: var(--select-menu-popover-border-radius, 0.5rem);
288
+ min-width: var(--select-menu-popover-min-width, 18rem);
289
+ max-height: var(--select-menu-popover-max-height, 32rem);
290
+ overflow: auto;
291
+ box-shadow: var(--select-menu-popover-shadow, 0 0.4rem 1.6rem rgb(0 0 0 / 12%));
292
+
293
+ position-anchor: var(--_anchor-name);
294
+ top: calc(anchor(bottom) + var(--select-menu-block-distance, 0.4rem));
295
+ left: anchor(left);
296
+ right: auto;
297
+ position-try-fallbacks: flip-block;
298
+
299
+ opacity: 0;
300
+ display: none;
301
+ transition:
302
+ opacity var(--select-menu-transition-duration, 200ms),
303
+ display var(--select-menu-transition-duration, 200ms),
304
+ overlay var(--select-menu-transition-duration, 200ms);
305
+ transition-behavior: allow-discrete;
306
+
307
+ &:popover-open {
308
+ display: block;
309
+ opacity: 1;
310
+
311
+ @starting-style {
312
+ display: block;
313
+ opacity: 0;
314
+ }
315
+ }
316
+
317
+ .select-menu-list {
318
+ display: grid;
319
+ grid-template-columns: auto auto 1fr;
320
+ list-style: none;
321
+ padding: 0;
322
+ margin: 0;
323
+
324
+ .select-menu-list-item {
325
+ grid-column: 1 / -1;
326
+ display: grid;
327
+ grid-template-columns: subgrid;
328
+ align-items: center;
329
+ gap: var(--select-menu-item-gap, 0.8rem);
330
+ padding-block: var(--select-menu-item-padding-block, 1rem);
331
+ padding-inline: var(--select-menu-item-padding-inline, 1.2rem);
332
+ cursor: pointer;
333
+ color: var(--select-menu-item-text-color, var(--theme-text));
334
+ font-family: var(--font-family);
335
+ font-size: var(--select-menu-item-font-size, 1.5rem);
336
+ transition: background-color var(--select-menu-transition-duration, 200ms) ease;
337
+
338
+ &:hover,
339
+ &:focus-visible {
340
+ background-color: var(--select-menu-item-surface-hover, var(--theme-input-surface-hover));
341
+ }
342
+
343
+ &:focus-visible {
344
+ outline: var(--select-menu-trigger-outline-width, 0.2rem) solid var(--theme-ring, currentcolor);
345
+ outline-offset: -0.2rem;
346
+ }
347
+
348
+ .select-menu-item-check {
349
+ grid-column: 1;
350
+ display: flex;
351
+ align-items: center;
352
+ justify-content: center;
353
+ width: var(--select-menu-item-check-size, 1.6rem);
354
+ height: var(--select-menu-item-check-size, 1.6rem);
355
+ flex-shrink: 0;
356
+ color: var(--select-menu-item-check-color, currentcolor);
357
+
358
+ .select-menu-item-check-icon {
359
+ display: block;
360
+ width: 100%;
361
+ height: 100%;
362
+ }
363
+ }
364
+
365
+ .select-menu-item-icon {
366
+ grid-column: 2;
367
+ display: block;
368
+ flex-shrink: 0;
369
+ width: var(--select-menu-item-icon-size, 1.8rem);
370
+ height: var(--select-menu-item-icon-size, 1.8rem);
371
+ }
372
+
373
+ .select-menu-item-label {
374
+ grid-column: 3;
375
+ white-space: nowrap;
376
+ overflow: hidden;
377
+ text-overflow: ellipsis;
378
+ }
379
+ }
380
+ }
381
+ }
382
+ }
383
+ }
384
+ </style>