srcdev-nuxt-components 9.6.0 → 9.6.1
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.
- package/.claude/skills/component-compliance-checklist.md +8 -2
- package/.claude/skills/component-dynamic-slots.md +3 -3
- package/.claude/skills/component-export-types.md +10 -16
- package/.claude/skills/component-naming.md +1 -1
- package/.claude/skills/components/alert-content.md +9 -1
- package/.claude/skills/components/container-glow.md +21 -8
- package/.claude/skills/components/display-avatar.md +13 -7
- package/.claude/skills/components/display-chip.md +37 -24
- package/.claude/skills/components/display-pill.md +58 -10
- package/.claude/skills/components/display-prompt.md +8 -5
- package/.claude/skills/components/display-toast.md +21 -4
- package/.claude/skills/components/glowing-border.md +20 -2
- package/.claude/skills/components/responsive-header.md +11 -1
- package/.claude/skills/components/select-menu.md +54 -5
- package/.claude/skills/components/site-header.md +2 -2
- package/.claude/skills/components/tabbed-content.md +300 -0
- package/.claude/skills/composable-anchor-scroll.md +16 -15
- package/.claude/skills/index.md +4 -4
- package/.claude/skills/storybook-add-story.md +53 -0
- package/.claude/skills/theming-status-tokens.md +60 -0
- package/.vscode/srcdev-component-display-avatar.code-snippets +2 -1
- package/.vscode/srcdev-component-display-chip.code-snippets +3 -2
- package/.vscode/srcdev-component-display-pill.code-snippets +10 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +6 -0
- package/.vscode/srcdev-component-select-menu.code-snippets +28 -0
- package/.vscode/{srcdev-component-tabs-core.code-snippets → srcdev-component-tabbed-content.code-snippets} +35 -16
- package/app/assets/styles/setup/03.theming/_status.css +24 -0
- package/app/assets/styles/setup/03.theming/index.css +1 -0
- package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +39 -35
- package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +25 -15
- package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +130 -24
- package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +45 -0
- package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +2 -2
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +76 -19
- package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +12 -10
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +201 -42
- package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +15 -4
- package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +1 -1
- package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +75 -9
- package/app/components/01.atoms/display-pill/DisplayPill.vue +57 -24
- package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +240 -16
- package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +43 -14
- package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +3 -3
- package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +201 -0
- package/app/components/01.atoms/tabbed-content/TabbedContent.vue +750 -0
- package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +279 -0
- package/app/components/01.atoms/tabbed-content/tests/TabbedContent.spec.ts +816 -0
- package/app/components/02.molecules/alert-content/AlertContent.vue +11 -2
- package/app/components/02.molecules/alert-content/AlertContentInner.vue +19 -0
- package/app/components/02.molecules/alert-content/CONSUMER-STYLING.md +17 -4
- package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +200 -17
- package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +33 -0
- package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +7 -5
- package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +6 -2
- package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +24 -3
- package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +19 -0
- package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +2 -1
- package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +52 -19
- package/app/components/02.molecules/display-chip/DisplayChip.vue +72 -54
- package/app/components/02.molecules/display-chip/stories/DisplayChip.stories.ts +103 -6
- package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +98 -29
- package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +8 -4
- package/app/components/02.molecules/prompt/CONSUMER-STYLING.md +9 -1
- package/app/components/02.molecules/prompt/DisplayPrompt.vue +2 -2
- package/app/components/02.molecules/prompt/stories/DisplayPrompt.stories.ts +208 -71
- package/app/components/02.molecules/prompt/tests/DisplayPrompt.spec.ts +19 -6
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +66 -8
- package/app/components/02.molecules/select-menu/SelectMenu.vue +141 -18
- package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +232 -2
- package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +151 -0
- package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +6 -2
- package/app/components/02.molecules/toast/CONSUMER-STYLING.md +91 -27
- package/app/components/02.molecules/toast/DisplayToast.vue +109 -50
- package/app/components/02.molecules/toast/DisplayToastProvider.vue +109 -45
- package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +23 -0
- package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +73 -0
- package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +132 -18
- package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +105 -39
- package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +38 -11
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +27 -12
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +35 -20
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +1 -1
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +1 -1
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +1 -1
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +3 -3
- package/app/composables/useTabs.ts +148 -206
- package/app/types/components/display-chip.d.ts +8 -4
- package/app/types/components/display-toast.d.ts +2 -0
- package/app/types/components/index.ts +0 -1
- package/app/types/components/select-menu.d.ts +2 -0
- package/app/utils/pausableTimeout.ts +41 -0
- package/nuxt.config.ts +1 -1
- package/package.json +1 -1
- package/.claude/skills/components/tab-navigation.md +0 -288
- package/.claude/skills/components/tabs-core.md +0 -187
- package/.vscode/srcdev-component-tab-navigation.code-snippets +0 -30
- package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +0 -120
- package/app/components/01.atoms/navigation/tabs/TabsCore.vue +0 -272
- package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +0 -199
- package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +0 -274
- package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +0 -191
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -550
- package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -259
- package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -379
- package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +0 -33
- package/app/composables/useNavCollapse.ts +0 -86
- package/app/types/components/nav-item.d.ts +0 -11
|
@@ -22,7 +22,20 @@ global token so it matches the rest of the design system out of the box.
|
|
|
22
22
|
| `--select-menu-trigger-text-color` | `var(--theme-text)` | Trigger label text colour |
|
|
23
23
|
| `--select-menu-trigger-font-size` | `1.6rem` | Trigger label font size |
|
|
24
24
|
| `--select-menu-trigger-outline-width` | `0.2rem` | Focus-visible outline width (trigger and items) |
|
|
25
|
-
| `--select-menu-trigger-icon-size` | `2rem` |
|
|
25
|
+
| `--select-menu-trigger-icon-size` | `2rem` | Leading icon size in the trigger (selected option's icon or `triggerIcon`) |
|
|
26
|
+
| `--select-menu-trigger-icon-color` | `currentcolor` | Leading icon colour (monochrome icon sets only) |
|
|
27
|
+
| `--select-menu-trigger-count-surface` | `var(--theme-surface-subtle)` | Count badge background (`indicator="count"`) |
|
|
28
|
+
| `--select-menu-trigger-count-text-color` | `var(--theme-text)` | Count badge text colour |
|
|
29
|
+
| `--select-menu-trigger-count-font-size` | `1.4rem` | Count badge font size |
|
|
30
|
+
| `--select-menu-trigger-count-font-weight` | `600` | Count badge font weight |
|
|
31
|
+
| `--select-menu-trigger-count-min-width` | `2.4rem` | Count badge minimum width, so 1 and 12 read as the same shape |
|
|
32
|
+
| `--select-menu-trigger-count-padding-block` | `0.2rem` | Count badge vertical padding |
|
|
33
|
+
| `--select-menu-trigger-count-padding-inline` | `0.6rem` | Count badge horizontal padding |
|
|
34
|
+
| `--select-menu-trigger-count-border-radius` | `0.4rem` | Count badge corner rounding (`999rem` for a pill) |
|
|
35
|
+
| `--select-menu-trigger-count-margin-inline` | `1.2rem 0` | Extra space around the count badge, on top of `--select-menu-trigger-gap` (start, end) |
|
|
36
|
+
| `--select-menu-trigger-dot-size` | `0.8rem` | Status dot diameter (`indicator="dot"`) |
|
|
37
|
+
| `--select-menu-trigger-dot-margin-inline` | `1.2rem 0` | Extra space around the status dot, on top of `--select-menu-trigger-gap` (start, end) |
|
|
38
|
+
| `--select-menu-trigger-dot-color` | `var(--theme-accent)` | Status dot colour when the selected option has no `dotColor` |
|
|
26
39
|
| `--select-menu-trigger-chevron-size` | `1.6rem` | Chevron icon size |
|
|
27
40
|
|
|
28
41
|
### Menu popover
|
|
@@ -35,10 +48,11 @@ global token so it matches the rest of the design system out of the box.
|
|
|
35
48
|
| `--select-menu-popover-surface` | `var(--theme-input-surface)` | Popover background |
|
|
36
49
|
| `--select-menu-popover-border-radius` | `0.5rem` | Popover corner rounding |
|
|
37
50
|
| `--select-menu-popover-min-width` | `18rem` | Minimum popover width |
|
|
51
|
+
| `--select-menu-popover-max-width` | `calc(100vw - 3.2rem)` | Maximum popover width; longer option labels wrap (see `--select-menu-item-label-line-clamp`) |
|
|
38
52
|
| `--select-menu-popover-max-height` | `32rem` | Maximum popover height before scrolling |
|
|
39
53
|
| `--select-menu-popover-shadow` | `0 0.4rem 1.6rem rgb(0 0 0 / 12%)` | Popover drop shadow |
|
|
40
54
|
| `--select-menu-popover-z-index` | `999999` | Stacking order in browsers without CSS anchor positioning (see Notes). Ignored where the popover renders in the top layer |
|
|
41
|
-
| `--select-menu-transition-duration` | `200ms` | Open/close fade and chevron-
|
|
55
|
+
| `--select-menu-transition-duration` | `200ms` | Open/close fade and chevron-flip duration |
|
|
42
56
|
|
|
43
57
|
### Options
|
|
44
58
|
|
|
@@ -62,6 +76,14 @@ global token so it matches the rest of the design system out of the box.
|
|
|
62
76
|
| `--select-menu-item-check-size` | `1.6rem` | Checkmark icon box size |
|
|
63
77
|
| `--select-menu-item-check-color` | `currentcolor` | Checkmark icon colour |
|
|
64
78
|
| `--select-menu-item-icon-size` | `1.8rem` | Per-option icon size |
|
|
79
|
+
| `--select-menu-item-dot-size` | `0.8rem` | Per-option status dot size (options with `dotColor` and no `icon`) |
|
|
80
|
+
| `--select-menu-item-label-line-clamp` | `none` | Lines an option label may take before it's cut with an ellipsis. `none` wraps the full label, `1` is single-line ellipsis |
|
|
81
|
+
|
|
82
|
+
> Changed 2026-10-08: long values used to push things off screen. The root now caps at its
|
|
83
|
+
> container's width (`max-inline-size: 100%`), so a long trigger label truncates with its ellipsis
|
|
84
|
+
> instead of overflowing. The popover caps at `--select-menu-popover-max-width`, and option labels
|
|
85
|
+
> wrap (they were `nowrap`, so a long one made the menu wider than the viewport). Set
|
|
86
|
+
> `--select-menu-item-label-line-clamp: 1` for the old single-line look.
|
|
65
87
|
|
|
66
88
|
---
|
|
67
89
|
|
|
@@ -70,7 +92,7 @@ global token so it matches the rest of the design system out of the box.
|
|
|
70
92
|
| Hook | Where | Meaning |
|
|
71
93
|
|---|---|---|
|
|
72
94
|
| `.normal` / `.underlined` | `.select-menu` root | The `inputVariant` prop. `underlined` removes the trigger's top and side borders and its corner radius, matching `InputSelect`'s underlined variant. |
|
|
73
|
-
| `.select-menu-trigger[aria-expanded="true"]` | trigger | Menu open (
|
|
95
|
+
| `.select-menu-trigger[aria-expanded="true"]` | trigger | Menu open (flips the chevron vertically). Works with and without the Popover API. |
|
|
74
96
|
| `.select-menu-popover:popover-open` | popover | Menu open, Popover API browsers. |
|
|
75
97
|
| `.select-menu-popover-open` | popover | Menu open, browsers without the Popover API (Safari 16 and older). Style both open hooks in **separate rules**: a selector list containing `:popover-open` is dropped whole where it's unsupported. |
|
|
76
98
|
| `[data-placement="top"]` | popover | Flipped above the trigger, browsers without CSS anchor positioning only. |
|
|
@@ -142,6 +164,40 @@ the three common shapes:
|
|
|
142
164
|
| Icon-only (e.g. flag switcher in a tight header) | `:show-label="false" :show-chevron="false"` |
|
|
143
165
|
| Text + chevron (e.g. a category filter with no per-option icons) | `:show-icon="false"` (default `showLabel`/`showChevron`) |
|
|
144
166
|
| Icon + text + chevron (full select) | Defaults — no overrides needed |
|
|
167
|
+
| Filter: category icon + text + count/dot + chevron | `trigger-icon="lucide:list" indicator="count"` (or `"dot"`) |
|
|
168
|
+
|
|
169
|
+
### Stable trigger width
|
|
170
|
+
|
|
171
|
+
In single-select the trigger text changes with the selection, so by default the trigger resizes
|
|
172
|
+
("Active" to "Archived"). Set `reserve-label-width` to size it to the longest option label (or
|
|
173
|
+
placeholder) instead: hidden copies of each label (`.select-menu-trigger-label-sizer`) share a grid
|
|
174
|
+
cell with the visible text (`.select-menu-trigger-label-text`), and the root label span gets
|
|
175
|
+
`.select-menu-trigger-label-reserved`. No effect with `multiple`. If only some options have an
|
|
176
|
+
`icon`, the trigger can still change width when the icon appears; give them all an icon, or use
|
|
177
|
+
`triggerIcon`.
|
|
178
|
+
|
|
179
|
+
### Filter icon and indicator
|
|
180
|
+
|
|
181
|
+
`triggerIcon` sets a fixed leading icon (a category icon for a filter). It shows in both modes;
|
|
182
|
+
in single-select a selected option's own icon replaces it. `showIcon` hides either.
|
|
183
|
+
|
|
184
|
+
`indicator` adds a marker after the label, visible only while something is selected. With nothing
|
|
185
|
+
selected it stays in the layout but hidden (`.select-menu-trigger-indicator-empty`, `visibility:
|
|
186
|
+
hidden`), so the trigger doesn't change width as the filter is set and cleared. The count badge's
|
|
187
|
+
`--select-menu-trigger-count-min-width` fits two digits, so it only grows at 100+:
|
|
188
|
+
|
|
189
|
+
- `count`: a badge with the number selected. Meant for `multiple`.
|
|
190
|
+
- `dot`: a status dot. Meant for single-select, to flag that the filter is active.
|
|
191
|
+
|
|
192
|
+
Give options a `dotColor` (any CSS colour) to colour the dot per status: the trigger dot takes the
|
|
193
|
+
selected option's colour, and options with a `dotColor` and no `icon` show the same dot in the
|
|
194
|
+
list. The per-option colour beats `--select-menu-trigger-dot-color`, which stays the fallback
|
|
195
|
+
(and the only colour in `multiple` mode, where there's no single selected option). Pass a global
|
|
196
|
+
status token, e.g. `dotColor: "var(--status-success)"`, so the colours stay in step with every other
|
|
197
|
+
status indicator (see `.claude/skills/theming-status-tokens.md`).
|
|
198
|
+
|
|
199
|
+
The indicator is `aria-hidden`; instead the trigger's accessible name becomes
|
|
200
|
+
`"<label>, <selectedCountLabel>"` (default `"{count} selected"`, override it for translation).
|
|
145
201
|
|
|
146
202
|
---
|
|
147
203
|
|
|
@@ -156,9 +212,9 @@ toggles it in the array without closing the popover — so several can be picked
|
|
|
156
212
|
cycle. By default the trigger text stays fixed on `placeholder`/`label` as a static category tag
|
|
157
213
|
(e.g. `"Services required"`) regardless of how many options are checked, since a comma-joined list
|
|
158
214
|
of checked options would grow unpredictably long. Set `showSelectionInTrigger` to opt into that
|
|
159
|
-
comma-joined list instead (falling back to `placeholder`/`label` when nothing is checked).
|
|
160
|
-
|
|
161
|
-
|
|
215
|
+
comma-joined list instead (falling back to `placeholder`/`label` when nothing is checked). No
|
|
216
|
+
option icon is shown in the trigger in multi-select mode, since there's no single selected option
|
|
217
|
+
to represent; a fixed `triggerIcon` still shows.
|
|
162
218
|
|
|
163
219
|
```vue
|
|
164
220
|
<script setup lang="ts">
|
|
@@ -202,6 +258,8 @@ story.
|
|
|
202
258
|
`contain` becomes its containing block and can misplace it.
|
|
203
259
|
- **Popover left-aligns with the trigger** by default (`left: anchor(left)`), unlike `ActionMenu`
|
|
204
260
|
which right-aligns — matches native `<select>` dropdown behaviour. Flips above the trigger near
|
|
205
|
-
the bottom of the viewport
|
|
206
|
-
-
|
|
261
|
+
the bottom of the viewport, and right-aligns when there's no room to the right
|
|
262
|
+
(`position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline`). The inline flip needs
|
|
263
|
+
CSS anchor positioning; the JS fallback only flips above/below.
|
|
264
|
+
- **Chevron flip** (`scaleY(-1)`, was `rotate(180deg)` until 2026-10-07) is driven by the trigger's `aria-expanded`, which tracks the open state in
|
|
207
265
|
every browser.
|
|
@@ -11,24 +11,45 @@
|
|
|
11
11
|
popovertargetaction="toggle"
|
|
12
12
|
type="button"
|
|
13
13
|
class="select-menu-trigger"
|
|
14
|
-
:aria-label="
|
|
14
|
+
:aria-label="triggerAriaLabel"
|
|
15
15
|
aria-haspopup="listbox"
|
|
16
16
|
:aria-expanded="isOpen"
|
|
17
17
|
@click="handleTriggerClick"
|
|
18
18
|
>
|
|
19
|
-
<Icon
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class="select-menu-trigger-
|
|
19
|
+
<Icon v-if="leadingIcon" :name="leadingIcon" class="select-menu-trigger-icon" aria-hidden="true" />
|
|
20
|
+
<span
|
|
21
|
+
v-if="showLabel"
|
|
22
|
+
class="select-menu-trigger-label"
|
|
23
|
+
:class="{ 'select-menu-trigger-label-reserved': reservedLabels.length }"
|
|
24
|
+
>
|
|
25
|
+
<template v-if="reservedLabels.length">
|
|
26
|
+
<span class="select-menu-trigger-label-text">{{ triggerLabelText }}</span>
|
|
27
|
+
<span
|
|
28
|
+
v-for="(text, index) in reservedLabels"
|
|
29
|
+
:key="index"
|
|
30
|
+
class="select-menu-trigger-label-sizer"
|
|
31
|
+
aria-hidden="true"
|
|
32
|
+
>{{ text }}</span
|
|
33
|
+
>
|
|
34
|
+
</template>
|
|
35
|
+
<template v-else>{{ triggerLabelText }}</template>
|
|
36
|
+
</span>
|
|
37
|
+
<span
|
|
38
|
+
v-if="indicator === 'count'"
|
|
39
|
+
class="select-menu-trigger-count"
|
|
40
|
+
:class="{ 'select-menu-trigger-indicator-empty': !selectedCount }"
|
|
23
41
|
aria-hidden="true"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class="select-menu-trigger-
|
|
42
|
+
>
|
|
43
|
+
{{ selectedCount || "" }}
|
|
44
|
+
</span>
|
|
45
|
+
<span
|
|
46
|
+
v-else-if="indicator === 'dot'"
|
|
47
|
+
class="select-menu-trigger-dot"
|
|
48
|
+
:class="{ 'select-menu-trigger-indicator-empty': !selectedCount }"
|
|
49
|
+
:style="selectedOption?.dotColor ? { '--_dot-color': selectedOption.dotColor } : undefined"
|
|
30
50
|
aria-hidden="true"
|
|
31
|
-
|
|
51
|
+
></span>
|
|
52
|
+
<Icon v-if="showChevron" name="lucide:chevron-down" class="select-menu-trigger-chevron" aria-hidden="true" />
|
|
32
53
|
</button>
|
|
33
54
|
|
|
34
55
|
<div
|
|
@@ -63,6 +84,12 @@
|
|
|
63
84
|
<Icon v-else-if="isSelected(option)" name="lucide:check" class="select-menu-item-check-icon" />
|
|
64
85
|
</span>
|
|
65
86
|
<Icon v-if="option.icon" :name="option.icon" class="select-menu-item-icon" aria-hidden="true" />
|
|
87
|
+
<span
|
|
88
|
+
v-else-if="option.dotColor"
|
|
89
|
+
class="select-menu-item-dot"
|
|
90
|
+
:style="{ '--_dot-color': option.dotColor }"
|
|
91
|
+
aria-hidden="true"
|
|
92
|
+
></span>
|
|
66
93
|
<span class="select-menu-item-label">{{ option.label }}</span>
|
|
67
94
|
</li>
|
|
68
95
|
</ul>
|
|
@@ -81,8 +108,14 @@ interface Props {
|
|
|
81
108
|
label: string;
|
|
82
109
|
/** Text shown in the trigger when no option is selected. Falls back to `label`. */
|
|
83
110
|
placeholder?: string;
|
|
84
|
-
/** Show the selected option's icon
|
|
111
|
+
/** Show the leading icon in the trigger: the selected option's icon (single-select), otherwise `triggerIcon`. */
|
|
85
112
|
showIcon?: boolean;
|
|
113
|
+
/** Fixed leading icon for the trigger, e.g. a filter category icon. Shown in both modes; in single-select a selected option's own icon replaces it. */
|
|
114
|
+
triggerIcon?: string;
|
|
115
|
+
/** Selection indicator after the trigger text, shown only while something is selected: `count` (badge with the number selected) or `dot` (status dot). */
|
|
116
|
+
indicator?: "none" | "count" | "dot";
|
|
117
|
+
/** Appended to the trigger's accessible name while the indicator is showing. `{count}` is replaced with the number selected. */
|
|
118
|
+
selectedCountLabel?: string;
|
|
86
119
|
/** Show the selected option's label (or placeholder) text in the trigger. Set to false for an icon-only compact trigger. */
|
|
87
120
|
showLabel?: boolean;
|
|
88
121
|
/** Show the trailing chevron in the trigger. */
|
|
@@ -91,6 +124,8 @@ interface Props {
|
|
|
91
124
|
multiple?: boolean;
|
|
92
125
|
/** 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. */
|
|
93
126
|
showSelectionInTrigger?: boolean;
|
|
127
|
+
/** Single-select only. Size the trigger text to the longest option label (or placeholder), so the trigger doesn't change width as the selection changes. */
|
|
128
|
+
reserveLabelWidth?: boolean;
|
|
94
129
|
/** Trigger border style, matching InputSelect: `normal` (bordered box) or `underlined` (bottom border only). */
|
|
95
130
|
inputVariant?: InputUiVariant;
|
|
96
131
|
styleClassPassthrough?: string | string[];
|
|
@@ -99,10 +134,14 @@ interface Props {
|
|
|
99
134
|
const props = withDefaults(defineProps<Props>(), {
|
|
100
135
|
placeholder: undefined,
|
|
101
136
|
showIcon: true,
|
|
137
|
+
triggerIcon: undefined,
|
|
138
|
+
indicator: "none",
|
|
139
|
+
selectedCountLabel: "{count} selected",
|
|
102
140
|
showLabel: true,
|
|
103
141
|
showChevron: true,
|
|
104
142
|
multiple: false,
|
|
105
143
|
showSelectionInTrigger: false,
|
|
144
|
+
reserveLabelWidth: false,
|
|
106
145
|
inputVariant: "normal",
|
|
107
146
|
styleClassPassthrough: () => [],
|
|
108
147
|
});
|
|
@@ -130,6 +169,11 @@ const selectedOptions = computed(() => props.options.filter((option) => selected
|
|
|
130
169
|
* options would grow unpredictably long and push on adjacent triggers. Set
|
|
131
170
|
* `showSelectionInTrigger` to opt into the comma-joined list instead.
|
|
132
171
|
*/
|
|
172
|
+
const reservedLabels = computed(() => {
|
|
173
|
+
if (!props.reserveLabelWidth || props.multiple) return [];
|
|
174
|
+
return [props.placeholder ?? props.label, ...props.options.map((option) => option.label)];
|
|
175
|
+
});
|
|
176
|
+
|
|
133
177
|
const triggerLabelText = computed(() => {
|
|
134
178
|
if (props.multiple) {
|
|
135
179
|
if (props.showSelectionInTrigger && selectedOptions.value.length) {
|
|
@@ -140,6 +184,21 @@ const triggerLabelText = computed(() => {
|
|
|
140
184
|
return selectedOption.value?.label ?? props.placeholder ?? props.label;
|
|
141
185
|
});
|
|
142
186
|
|
|
187
|
+
const selectedCount = computed(() => {
|
|
188
|
+
if (props.multiple) return selectedOptions.value.length;
|
|
189
|
+
return selectedOption.value ? 1 : 0;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const leadingIcon = computed(() => {
|
|
193
|
+
if (!props.showIcon) return undefined;
|
|
194
|
+
return (!props.multiple && selectedOption.value?.icon) || props.triggerIcon;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const triggerAriaLabel = computed(() => {
|
|
198
|
+
if (props.indicator === "none" || !selectedCount.value) return props.label;
|
|
199
|
+
return `${props.label}, ${props.selectedCountLabel.replace("{count}", String(selectedCount.value))}`;
|
|
200
|
+
});
|
|
201
|
+
|
|
143
202
|
const isSelected = (option: SelectMenuOption): boolean =>
|
|
144
203
|
props.multiple ? selectedValues.value.includes(option.value) : option.value === modelValue.value;
|
|
145
204
|
|
|
@@ -238,6 +297,8 @@ watch(
|
|
|
238
297
|
.select-menu {
|
|
239
298
|
position: relative;
|
|
240
299
|
display: inline-block;
|
|
300
|
+
max-inline-size: 100%;
|
|
301
|
+
min-inline-size: 0;
|
|
241
302
|
|
|
242
303
|
.select-menu-trigger {
|
|
243
304
|
all: unset;
|
|
@@ -249,7 +310,8 @@ watch(
|
|
|
249
310
|
min-height: var(--select-menu-trigger-min-height, 4.4rem);
|
|
250
311
|
padding-block: var(--select-menu-trigger-padding-block, 0.8rem);
|
|
251
312
|
padding-inline: var(--select-menu-trigger-padding-inline, 1.2rem);
|
|
252
|
-
border: var(--select-menu-trigger-border-width, 0.1rem) solid
|
|
313
|
+
border: var(--select-menu-trigger-border-width, 0.1rem) solid
|
|
314
|
+
var(--select-menu-trigger-border, var(--theme-border));
|
|
253
315
|
border-radius: var(--select-menu-trigger-border-radius, 0.5rem);
|
|
254
316
|
background-color: var(--select-menu-trigger-surface, var(--theme-input-surface));
|
|
255
317
|
color: var(--select-menu-trigger-text-color, var(--theme-text));
|
|
@@ -274,6 +336,7 @@ watch(
|
|
|
274
336
|
flex-shrink: 0;
|
|
275
337
|
width: var(--select-menu-trigger-icon-size, 2rem);
|
|
276
338
|
height: var(--select-menu-trigger-icon-size, 2rem);
|
|
339
|
+
color: var(--select-menu-trigger-icon-color, currentcolor);
|
|
277
340
|
}
|
|
278
341
|
|
|
279
342
|
.select-menu-trigger-label {
|
|
@@ -285,6 +348,53 @@ watch(
|
|
|
285
348
|
text-align: start;
|
|
286
349
|
}
|
|
287
350
|
|
|
351
|
+
/* Hidden copies of every possible label share one grid cell, so the widest sets the width. */
|
|
352
|
+
.select-menu-trigger-label-reserved {
|
|
353
|
+
display: grid;
|
|
354
|
+
|
|
355
|
+
.select-menu-trigger-label-text,
|
|
356
|
+
.select-menu-trigger-label-sizer {
|
|
357
|
+
grid-area: 1 / 1;
|
|
358
|
+
min-width: 0;
|
|
359
|
+
overflow: hidden;
|
|
360
|
+
text-overflow: ellipsis;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.select-menu-trigger-label-sizer {
|
|
364
|
+
visibility: hidden;
|
|
365
|
+
block-size: 0;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.select-menu-trigger-count {
|
|
370
|
+
flex-shrink: 0;
|
|
371
|
+
min-width: var(--select-menu-trigger-count-min-width, 2.4rem);
|
|
372
|
+
padding-block: var(--select-menu-trigger-count-padding-block, 0.2rem);
|
|
373
|
+
padding-inline: var(--select-menu-trigger-count-padding-inline, 0.6rem);
|
|
374
|
+
margin-inline: var(--select-menu-trigger-count-margin-inline, 1.2rem 0);
|
|
375
|
+
border-radius: var(--select-menu-trigger-count-border-radius, 0.4rem);
|
|
376
|
+
background-color: var(--select-menu-trigger-count-surface, var(--theme-surface-subtle));
|
|
377
|
+
color: var(--select-menu-trigger-count-text-color, var(--theme-text));
|
|
378
|
+
font-size: var(--select-menu-trigger-count-font-size, 1.4rem);
|
|
379
|
+
font-weight: var(--select-menu-trigger-count-font-weight, 600);
|
|
380
|
+
font-variant-numeric: tabular-nums;
|
|
381
|
+
text-align: center;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.select-menu-trigger-dot {
|
|
385
|
+
flex-shrink: 0;
|
|
386
|
+
width: var(--select-menu-trigger-dot-size, 0.8rem);
|
|
387
|
+
height: var(--select-menu-trigger-dot-size, 0.8rem);
|
|
388
|
+
margin-inline: var(--select-menu-trigger-dot-margin-inline, 1.2rem 0);
|
|
389
|
+
border-radius: 50%;
|
|
390
|
+
background-color: var(--_dot-color, var(--select-menu-trigger-dot-color, var(--theme-accent)));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/* Kept in the layout while nothing is selected, so the trigger width doesn't jump. */
|
|
394
|
+
.select-menu-trigger-indicator-empty {
|
|
395
|
+
visibility: hidden;
|
|
396
|
+
}
|
|
397
|
+
|
|
288
398
|
.select-menu-trigger-chevron {
|
|
289
399
|
display: block;
|
|
290
400
|
flex-shrink: 0;
|
|
@@ -306,7 +416,7 @@ watch(
|
|
|
306
416
|
}
|
|
307
417
|
|
|
308
418
|
.select-menu-trigger[aria-expanded="true"] .select-menu-trigger-chevron {
|
|
309
|
-
transform:
|
|
419
|
+
transform: scaleY(-1);
|
|
310
420
|
}
|
|
311
421
|
|
|
312
422
|
.select-menu-popover {
|
|
@@ -318,6 +428,7 @@ watch(
|
|
|
318
428
|
background-color: var(--select-menu-popover-surface, var(--theme-input-surface));
|
|
319
429
|
border-radius: var(--select-menu-popover-border-radius, 0.5rem);
|
|
320
430
|
min-width: var(--select-menu-popover-min-width, 18rem);
|
|
431
|
+
max-inline-size: var(--select-menu-popover-max-width, calc(100vw - 3.2rem));
|
|
321
432
|
max-height: var(--select-menu-popover-max-height, 32rem);
|
|
322
433
|
overflow: auto;
|
|
323
434
|
box-shadow: var(--select-menu-popover-shadow, 0 0.4rem 1.6rem rgb(0 0 0 / 12%));
|
|
@@ -326,7 +437,7 @@ watch(
|
|
|
326
437
|
top: calc(anchor(bottom) + var(--select-menu-block-distance, 0.4rem));
|
|
327
438
|
left: anchor(left);
|
|
328
439
|
right: auto;
|
|
329
|
-
position-try-fallbacks: flip-block;
|
|
440
|
+
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
|
|
330
441
|
|
|
331
442
|
opacity: 0;
|
|
332
443
|
display: none;
|
|
@@ -455,12 +566,24 @@ watch(
|
|
|
455
566
|
height: var(--select-menu-item-icon-size, 1.8rem);
|
|
456
567
|
}
|
|
457
568
|
|
|
569
|
+
.select-menu-item-dot {
|
|
570
|
+
grid-column: 2;
|
|
571
|
+
justify-self: center;
|
|
572
|
+
width: var(--select-menu-item-dot-size, 0.8rem);
|
|
573
|
+
height: var(--select-menu-item-dot-size, 0.8rem);
|
|
574
|
+
border-radius: 50%;
|
|
575
|
+
background-color: var(--_dot-color);
|
|
576
|
+
}
|
|
577
|
+
|
|
458
578
|
.select-menu-item-label {
|
|
459
579
|
grid-column: 3;
|
|
460
580
|
grid-row: 1;
|
|
461
|
-
|
|
581
|
+
display: -webkit-box;
|
|
582
|
+
-webkit-box-orient: vertical;
|
|
462
583
|
overflow: hidden;
|
|
463
|
-
|
|
584
|
+
overflow-wrap: anywhere;
|
|
585
|
+
-webkit-line-clamp: var(--select-menu-item-label-line-clamp, none);
|
|
586
|
+
line-clamp: var(--select-menu-item-label-line-clamp, none);
|
|
464
587
|
}
|
|
465
588
|
}
|
|
466
589
|
}
|