srcdev-nuxt-components 9.5.4 → 9.5.5
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/components/select-menu.md +10 -0
- package/.claude/skills/components/triple-toggle-switch.md +7 -1
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +9 -1
- package/app/components/02.molecules/select-menu/SelectMenu.vue +39 -2
- package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +9 -9
- package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitch.vue +11 -3
- package/package.json +1 -1
|
@@ -188,6 +188,16 @@ Quick reference:
|
|
|
188
188
|
`[role="option"]` if one exists (in `multiple` mode, the first checked item), otherwise the first
|
|
189
189
|
option (unlike `ActionMenu`, which always focuses the first item since it has no selection state).
|
|
190
190
|
- **`aria-multiselectable="true"`** is set on the listbox only when `multiple` is `true`.
|
|
191
|
+
- **Selected rows are tinted** via `[aria-selected="true"]` (`--select-menu-item-surface-selected`,
|
|
192
|
+
default `--theme-surface-subtle`), in both single and multiple mode. Hover/focus is declared after
|
|
193
|
+
it at equal specificity, so the hover surface still shows on a selected row. Selected text colour,
|
|
194
|
+
font weight and border colour have their own `-selected` tokens falling back to the resting ones.
|
|
195
|
+
The row border width (`--select-menu-item-border-width`, default `0`) applies to every row with a
|
|
196
|
+
transparent resting colour, so a selected border never shifts layout. A hidden `::after` carrying
|
|
197
|
+
`data-label` in the selected weight reserves the label column's width, so a bolder selected
|
|
198
|
+
weight doesn't resize the popover. Hover/focus has `-text-color-hover` and `-border-hover`, which
|
|
199
|
+
fall back to the row's *current* state (selected or resting), not always to resting. There's no
|
|
200
|
+
hover font-weight token on purpose: the width reserve only covers one alternate weight.
|
|
191
201
|
- **Chevron rotation is pure CSS** via `:has(.select-menu-popover:popover-open)` on the root — no
|
|
192
202
|
JS state drives the visual. `isOpen` is still tracked internally, but only to set `aria-expanded`
|
|
193
203
|
on the trigger.
|
|
@@ -28,7 +28,7 @@ for that for a theme switch; use `TripleToggleSwitch` directly for any other thr
|
|
|
28
28
|
| `name` | `string` | `"triple-toggle-switch"` | Shared radio `name`. Give each instance on a page its own. |
|
|
29
29
|
| `aria-label` | `string` | `""` | Accessible name for the `role="radiogroup"`. Omitted from the DOM when empty. Always pass one. |
|
|
30
30
|
| `theme` | `FormUiTheme` | `"default"` | Rendered as `data-theme`. |
|
|
31
|
-
| `step-animation-duration` | `string` | `"250ms"` | Marker slide duration. Ignored under `prefers-reduced-motion`. |
|
|
31
|
+
| `step-animation-duration` | `string` | `"250ms"` | Marker slide duration; also times the icon colour swap (see Behaviour). Ignored under `prefers-reduced-motion`. |
|
|
32
32
|
| `style-class-passthrough` | `string \| string[]` | `[]` | Classes on the root. |
|
|
33
33
|
|
|
34
34
|
Option `id`s are used as element ids (and as a class on each icon), so they must be unique on the
|
|
@@ -66,6 +66,12 @@ const options = ref<IFormMultipleOptions>({
|
|
|
66
66
|
- The marker is measured from the first option's width on mount and fades in after ~250ms. It
|
|
67
67
|
isn't re-measured on resize, so changing the icon-size/padding tokens after mount (e.g. toggling
|
|
68
68
|
a class) leaves the marker at its old size.
|
|
69
|
+
- Icon colour swaps are timed to the marker: the newly active icon waits half of
|
|
70
|
+
`stepAnimationDuration`, then fades to the active colour over the other half, so it lands as the
|
|
71
|
+
marker arrives rather than turning white on the bare track. The previous icon fades back straight
|
|
72
|
+
away. The marker takes the same time whatever the distance, so no per-index delay is needed.
|
|
73
|
+
- Option circles are sized by their content (1em icon + equal padding) with `place-items: center`,
|
|
74
|
+
not `aspect-ratio`; older iPad Safari mis-centred the icons under the `aspect-ratio` version.
|
|
69
75
|
- Marker gradients only exist for the values `system`, `light` and `dark`; other values get the
|
|
70
76
|
plain `--triple-toggle-switch-marker-surface` background.
|
|
71
77
|
|
|
@@ -49,6 +49,15 @@ global token so it matches the rest of the design system out of the box.
|
|
|
49
49
|
| `--select-menu-item-text-color` | `var(--theme-text)` | Option label colour |
|
|
50
50
|
| `--select-menu-item-font-size` | `1.5rem` | Option label font size |
|
|
51
51
|
| `--select-menu-item-surface-hover` | `var(--theme-input-surface-hover)` | Option row background on hover/focus |
|
|
52
|
+
| `--select-menu-item-surface-selected` | `var(--theme-surface-subtle)` | Background of selected/checked option rows (hover/focus still wins) |
|
|
53
|
+
| `--select-menu-item-text-color-selected` | `--select-menu-item-text-color` | Selected row label colour (the checkmark follows via `currentcolor`) |
|
|
54
|
+
| `--select-menu-item-font-weight` | inherited | Option row font weight |
|
|
55
|
+
| `--select-menu-item-font-weight-selected` | `--select-menu-item-font-weight` | Selected row font weight; the popover reserves the bolder width, so selecting doesn't resize it |
|
|
56
|
+
| `--select-menu-item-border-width` | `0` | Block (top/bottom) border width on **every** row, so a selected border never shifts layout |
|
|
57
|
+
| `--select-menu-item-border` | `transparent` | Resting row border colour |
|
|
58
|
+
| `--select-menu-item-border-selected` | `--select-menu-item-border` | Selected row border colour. Two adjacent selected rows show both borders where they meet |
|
|
59
|
+
| `--select-menu-item-text-color-hover` | the row's current colour | Label colour on hover/focus. Unset, a selected row keeps its selected colour |
|
|
60
|
+
| `--select-menu-item-border-hover` | the row's current border | Border colour on hover/focus. Unset, a selected row keeps its selected border |
|
|
52
61
|
| `--select-menu-item-check-size` | `1.6rem` | Checkmark icon box size |
|
|
53
62
|
| `--select-menu-item-check-color` | `currentcolor` | Checkmark icon colour |
|
|
54
63
|
| `--select-menu-item-icon-size` | `1.8rem` | Per-option icon size |
|
|
@@ -184,4 +193,3 @@ story.
|
|
|
184
193
|
the bottom of the viewport (`position-try-fallbacks: flip-block`).
|
|
185
194
|
- **Chevron rotation** is pure CSS via `:has(.select-menu-popover:popover-open)` — no JS state
|
|
186
195
|
needed for the visual, though `isOpen` is still tracked internally to drive `aria-expanded`.
|
|
187
|
-
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
role="option"
|
|
42
42
|
tabindex="-1"
|
|
43
43
|
:aria-selected="isSelected(option)"
|
|
44
|
+
:data-label="option.label"
|
|
44
45
|
@click="selectOption(option)"
|
|
45
46
|
>
|
|
46
47
|
<span class="select-menu-item-check" aria-hidden="true">
|
|
@@ -343,15 +344,50 @@ watch(
|
|
|
343
344
|
gap: var(--select-menu-item-gap, 0.8rem);
|
|
344
345
|
padding-block: var(--select-menu-item-padding-block, 1rem);
|
|
345
346
|
padding-inline: var(--select-menu-item-padding-inline, 1.2rem);
|
|
347
|
+
--_text-color: var(--select-menu-item-text-color, var(--theme-text));
|
|
348
|
+
--_border: var(--select-menu-item-border, transparent);
|
|
349
|
+
|
|
346
350
|
cursor: pointer;
|
|
347
|
-
color: var(--
|
|
351
|
+
color: var(--_text-color);
|
|
348
352
|
font-family: var(--font-family);
|
|
349
353
|
font-size: var(--select-menu-item-font-size, 1.5rem);
|
|
350
|
-
|
|
354
|
+
/* No fallback: unset means invalid at computed time, so the weight inherits. */
|
|
355
|
+
font-weight: var(--select-menu-item-font-weight);
|
|
356
|
+
border-block: var(--select-menu-item-border-width, 0) solid var(--_border);
|
|
357
|
+
transition:
|
|
358
|
+
background-color var(--select-menu-transition-duration, 200ms) ease,
|
|
359
|
+
border-color var(--select-menu-transition-duration, 200ms) ease,
|
|
360
|
+
color var(--select-menu-transition-duration, 200ms) ease;
|
|
361
|
+
|
|
362
|
+
/* Reserves the selected label width so a bolder selected weight doesn't resize the popover. */
|
|
363
|
+
&::after {
|
|
364
|
+
content: attr(data-label);
|
|
365
|
+
grid-column: 3;
|
|
366
|
+
grid-row: 1;
|
|
367
|
+
block-size: 0;
|
|
368
|
+
overflow: hidden;
|
|
369
|
+
visibility: hidden;
|
|
370
|
+
white-space: nowrap;
|
|
371
|
+
font-weight: var(--select-menu-item-font-weight-selected, var(--select-menu-item-font-weight));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
&[aria-selected="true"] {
|
|
375
|
+
--_text-color: var(
|
|
376
|
+
--select-menu-item-text-color-selected,
|
|
377
|
+
var(--select-menu-item-text-color, var(--theme-text))
|
|
378
|
+
);
|
|
379
|
+
--_border: var(--select-menu-item-border-selected, var(--select-menu-item-border, transparent));
|
|
380
|
+
|
|
381
|
+
background-color: var(--select-menu-item-surface-selected, var(--theme-surface-subtle));
|
|
382
|
+
font-weight: var(--select-menu-item-font-weight-selected, var(--select-menu-item-font-weight));
|
|
383
|
+
}
|
|
351
384
|
|
|
385
|
+
/* Hover falls back to the row's current state, so an unset hover token keeps a selected row's colours. */
|
|
352
386
|
&:hover,
|
|
353
387
|
&:focus-visible {
|
|
354
388
|
background-color: var(--select-menu-item-surface-hover, var(--theme-input-surface-hover));
|
|
389
|
+
color: var(--select-menu-item-text-color-hover, var(--_text-color));
|
|
390
|
+
border-color: var(--select-menu-item-border-hover, var(--_border));
|
|
355
391
|
}
|
|
356
392
|
|
|
357
393
|
&:focus-visible {
|
|
@@ -386,6 +422,7 @@ watch(
|
|
|
386
422
|
|
|
387
423
|
.select-menu-item-label {
|
|
388
424
|
grid-column: 3;
|
|
425
|
+
grid-row: 1;
|
|
389
426
|
white-space: nowrap;
|
|
390
427
|
overflow: hidden;
|
|
391
428
|
text-overflow: ellipsis;
|
|
@@ -6,9 +6,9 @@ exports[`SelectMenu > Snapshots > default 1`] = `
|
|
|
6
6
|
</button>
|
|
7
7
|
<div id="select-menu-v-0-0" popover="" class="select-menu-popover">
|
|
8
8
|
<ul class="select-menu-list" role="listbox" aria-label="Language">
|
|
9
|
-
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:gb-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">English</span></li>
|
|
10
|
-
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:fr-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Français</span></li>
|
|
11
|
-
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:de-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Deutsch</span></li>
|
|
9
|
+
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false" data-label="English"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:gb-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">English</span></li>
|
|
10
|
+
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false" data-label="Français"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:fr-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Français</span></li>
|
|
11
|
+
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false" data-label="Deutsch"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:de-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Deutsch</span></li>
|
|
12
12
|
</ul>
|
|
13
13
|
</div>
|
|
14
14
|
</div>"
|
|
@@ -21,9 +21,9 @@ exports[`SelectMenu > Snapshots > icon-only trigger 1`] = `
|
|
|
21
21
|
</button>
|
|
22
22
|
<div id="select-menu-v-0-0" popover="" class="select-menu-popover">
|
|
23
23
|
<ul class="select-menu-list" role="listbox" aria-label="Language">
|
|
24
|
-
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="true"><span class="select-menu-item-check" aria-hidden="true"><span class="iconify i-lucide:check select-menu-item-check-icon" aria-hidden="true"></span></span><span class="iconify i-flag:gb-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">English</span></li>
|
|
25
|
-
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:fr-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Français</span></li>
|
|
26
|
-
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:de-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Deutsch</span></li>
|
|
24
|
+
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="true" data-label="English"><span class="select-menu-item-check" aria-hidden="true"><span class="iconify i-lucide:check select-menu-item-check-icon" aria-hidden="true"></span></span><span class="iconify i-flag:gb-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">English</span></li>
|
|
25
|
+
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false" data-label="Français"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:fr-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Français</span></li>
|
|
26
|
+
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false" data-label="Deutsch"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:de-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Deutsch</span></li>
|
|
27
27
|
</ul>
|
|
28
28
|
</div>
|
|
29
29
|
</div>"
|
|
@@ -33,9 +33,9 @@ exports[`SelectMenu > Snapshots > with a selected value 1`] = `
|
|
|
33
33
|
"<div class="select-menu normal" style="--_anchor-name: --select-menu-anchor-v-0-0;"><button popovertarget="select-menu-v-0-0" popovertargetaction="toggle" type="button" class="select-menu-trigger" aria-label="Language" aria-haspopup="listbox" aria-expanded="false"><span class="iconify i-flag:fr-4x3 select-menu-trigger-icon" aria-hidden="true"></span><span class="select-menu-trigger-label">Français</span><span class="iconify i-lucide:chevron-down select-menu-trigger-chevron" aria-hidden="true"></span></button>
|
|
34
34
|
<div id="select-menu-v-0-0" popover="" class="select-menu-popover">
|
|
35
35
|
<ul class="select-menu-list" role="listbox" aria-label="Language">
|
|
36
|
-
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:gb-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">English</span></li>
|
|
37
|
-
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="true"><span class="select-menu-item-check" aria-hidden="true"><span class="iconify i-lucide:check select-menu-item-check-icon" aria-hidden="true"></span></span><span class="iconify i-flag:fr-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Français</span></li>
|
|
38
|
-
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:de-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Deutsch</span></li>
|
|
36
|
+
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false" data-label="English"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:gb-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">English</span></li>
|
|
37
|
+
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="true" data-label="Français"><span class="select-menu-item-check" aria-hidden="true"><span class="iconify i-lucide:check select-menu-item-check-icon" aria-hidden="true"></span></span><span class="iconify i-flag:fr-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Français</span></li>
|
|
38
|
+
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false" data-label="Deutsch"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:de-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Deutsch</span></li>
|
|
39
39
|
</ul>
|
|
40
40
|
</div>
|
|
41
41
|
</div>"
|
|
@@ -186,10 +186,9 @@ onMounted(() => {
|
|
|
186
186
|
position: relative;
|
|
187
187
|
|
|
188
188
|
.option-group {
|
|
189
|
-
aspect-ratio: 1;
|
|
190
189
|
display: grid;
|
|
191
190
|
grid-template-areas: "icon-stack";
|
|
192
|
-
place-
|
|
191
|
+
place-items: center;
|
|
193
192
|
background: transparent;
|
|
194
193
|
border: var(--form-element-border-width) solid var(--triple-toggle-switch-option-border, #00000025);
|
|
195
194
|
outline: var(--form-element-outline-width) solid transparent;
|
|
@@ -214,10 +213,18 @@ onMounted(() => {
|
|
|
214
213
|
grid-area: icon-stack;
|
|
215
214
|
display: block;
|
|
216
215
|
font-size: var(--_scheme-icon-font-size);
|
|
216
|
+
width: 1em;
|
|
217
|
+
height: 1em;
|
|
217
218
|
color: var(--triple-toggle-switch-option-icon-color, var(--slate-10));
|
|
219
|
+
transition: color calc(v-bind(stepAnimationDuration) / 2) ease-in-out;
|
|
218
220
|
|
|
219
221
|
&.active {
|
|
220
222
|
color: var(--triple-toggle-switch-option-icon-color-active, var(--slate-00));
|
|
223
|
+
transition-delay: calc(v-bind(stepAnimationDuration) / 2);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
@media (prefers-reduced-motion: reduce) {
|
|
227
|
+
transition: none;
|
|
221
228
|
}
|
|
222
229
|
|
|
223
230
|
&:hover {
|
|
@@ -227,9 +234,10 @@ onMounted(() => {
|
|
|
227
234
|
|
|
228
235
|
.option-input {
|
|
229
236
|
grid-area: icon-stack;
|
|
237
|
+
appearance: none;
|
|
230
238
|
opacity: 0;
|
|
231
|
-
aspect-ratio: 1;
|
|
232
239
|
width: var(--_scheme-icon-font-size);
|
|
240
|
+
height: var(--_scheme-icon-font-size);
|
|
233
241
|
|
|
234
242
|
&:hover {
|
|
235
243
|
cursor: pointer;
|