svelte-5-select 1.0.2 → 2.0.0

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 (36) hide show
  1. package/README.md +239 -109
  2. package/dist/ChevronIcon.svelte +8 -13
  3. package/dist/ClearIcon.svelte +3 -11
  4. package/dist/LoadingIcon.svelte +9 -2
  5. package/dist/Select.svelte +954 -436
  6. package/dist/Select.svelte.d.ts +37 -5
  7. package/dist/aria-handlers.svelte.d.ts +4 -1
  8. package/dist/aria-handlers.svelte.js +19 -8
  9. package/dist/filter.d.ts +10 -2
  10. package/dist/filter.js +14 -7
  11. package/dist/index.d.ts +2 -3
  12. package/dist/index.js +1 -2
  13. package/dist/keyboard-navigation.svelte.d.ts +7 -2
  14. package/dist/keyboard-navigation.svelte.js +285 -47
  15. package/dist/no-styles/ChevronIcon.svelte +11 -0
  16. package/dist/no-styles/ChevronIcon.svelte.d.ts +26 -0
  17. package/dist/no-styles/ClearIcon.svelte +8 -0
  18. package/dist/no-styles/ClearIcon.svelte.d.ts +26 -0
  19. package/dist/no-styles/LoadingIcon.svelte +13 -0
  20. package/dist/no-styles/LoadingIcon.svelte.d.ts +26 -0
  21. package/dist/no-styles/Select.svelte +1383 -0
  22. package/dist/no-styles/Select.svelte.d.ts +38 -0
  23. package/dist/select-state.svelte.d.ts +15 -0
  24. package/dist/select-state.svelte.js +161 -0
  25. package/dist/styles/default.css +112 -71
  26. package/dist/tailwind.css +120 -17
  27. package/dist/types.d.ts +459 -129
  28. package/dist/use-hover.svelte.d.ts +3 -7
  29. package/dist/use-hover.svelte.js +91 -36
  30. package/dist/use-load-options.svelte.d.ts +18 -3
  31. package/dist/use-load-options.svelte.js +333 -42
  32. package/dist/use-value.svelte.d.ts +2 -11
  33. package/dist/use-value.svelte.js +322 -111
  34. package/dist/utils.d.ts +19 -8
  35. package/dist/utils.js +52 -8
  36. package/package.json +117 -94
@@ -0,0 +1,38 @@
1
+ import type { ItemLike, SelectProps, SelectRow } from '../types';
2
+ import type { SelectItem } from '../types';
3
+ declare function $$render<Item extends ItemLike = SelectItem, Multiple extends boolean = false>(): {
4
+ props: SelectProps<Item, Multiple>;
5
+ exports: {
6
+ getFilteredItems: () => SelectRow<Item>[];
7
+ reset: () => void;
8
+ };
9
+ bindings: "loading" | "value" | "hoverItemIndex" | "listOpen" | "filterText" | "items" | "justValue" | "focused" | "container" | "input";
10
+ slots: {};
11
+ events: {};
12
+ };
13
+ declare class __sveltets_Render<Item extends ItemLike = SelectItem, Multiple extends boolean = false> {
14
+ props(): ReturnType<typeof $$render<Item, Multiple>>['props'];
15
+ events(): ReturnType<typeof $$render<Item, Multiple>>['events'];
16
+ slots(): ReturnType<typeof $$render<Item, Multiple>>['slots'];
17
+ bindings(): "loading" | "value" | "hoverItemIndex" | "listOpen" | "filterText" | "items" | "justValue" | "focused" | "container" | "input";
18
+ exports(): {
19
+ getFilteredItems: () => SelectRow<Item>[];
20
+ reset: () => void;
21
+ };
22
+ }
23
+ interface $$IsomorphicComponent {
24
+ new <Item extends ItemLike = SelectItem, Multiple extends boolean = false>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Item, Multiple>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Item, Multiple>['props']>, ReturnType<__sveltets_Render<Item, Multiple>['events']>, ReturnType<__sveltets_Render<Item, Multiple>['slots']>> & {
25
+ $$bindings?: ReturnType<__sveltets_Render<Item, Multiple>['bindings']>;
26
+ } & ReturnType<__sveltets_Render<Item, Multiple>['exports']>;
27
+ <Item extends ItemLike = SelectItem, Multiple extends boolean = false>(internal: unknown, props: ReturnType<__sveltets_Render<Item, Multiple>['props']> & {}): ReturnType<__sveltets_Render<Item, Multiple>['exports']>;
28
+ z_$$bindings?: ReturnType<__sveltets_Render<any, any>['bindings']>;
29
+ }
30
+ /**
31
+ * A select/autocomplete/typeahead control: a WAI-ARIA combobox with a floating
32
+ * listbox popup. Generic over your item type; supports multi-select, async
33
+ * loading (`loadOptions`), grouping (`groupBy`), and snippet-based customization.
34
+ * Bind `value` (and optionally `justValue`, `filterText`, `listOpen`, `focused`).
35
+ */
36
+ declare const Select: $$IsomorphicComponent;
37
+ type Select<Item extends ItemLike = SelectItem, Multiple extends boolean = false> = InstanceType<typeof Select<Item, Multiple>>;
38
+ export default Select;
@@ -0,0 +1,15 @@
1
+ import type { ItemLike, SelectItem, SelectState } from './types';
2
+ /**
3
+ * The prop-backed part of the store: Select.svelte supplies live getter/setter
4
+ * accessors over its `$props()` bindings and derived values, so reads stay
5
+ * reactive and writes flow back into the bound props.
6
+ */
7
+ export type SelectStateProps<Item extends ItemLike = SelectItem> = Omit<SelectState<Item>, 'activeValue' | 'isScrolling' | 'clearState' | 'prevValue' | 'prevFilterText' | 'prevMultiple' | 'suppressValueHoverSnap' | 'userNavigatedSinceOpen'>;
8
+ /**
9
+ * Builds the single shared state object passed to every composable (in place of
10
+ * the former per-composable getState()/setter bundles). Internal shared state
11
+ * lives here; prop-backed fields are forwarded to the accessors from Select.svelte.
12
+ * Per-field access means a read tracks only that one signal, so composables no
13
+ * longer create blanket reactive dependencies on all state at once.
14
+ */
15
+ export declare function createSelectState<Item extends ItemLike = SelectItem>(props: SelectStateProps<Item>): SelectState<Item>;
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Builds the single shared state object passed to every composable (in place of
3
+ * the former per-composable getState()/setter bundles). Internal shared state
4
+ * lives here; prop-backed fields are forwarded to the accessors from Select.svelte.
5
+ * Per-field access means a read tracks only that one signal, so composables no
6
+ * longer create blanket reactive dependencies on all state at once.
7
+ */
8
+ export function createSelectState(props) {
9
+ let activeValue = $state(undefined);
10
+ let isScrolling = $state(false);
11
+ let clearState = $state(false);
12
+ // Comparison scratch fields, only read inside untrack()ed code — deliberately non-reactive.
13
+ // prevValue is seeded with the initial value so mount does not dispatch onValueChange;
14
+ // prevFilterText is seeded so an initial filterText does not open the list (and
15
+ // steal focus through the list-open effect) on mount.
16
+ let prevValue = props.value;
17
+ let prevFilterText = props.filterText;
18
+ let prevMultiple = undefined;
19
+ let suppressValueHoverSnap = false;
20
+ let userNavigatedSinceOpen = false;
21
+ return {
22
+ get value() {
23
+ return props.value;
24
+ },
25
+ set value(v) {
26
+ props.value = v;
27
+ },
28
+ get items() {
29
+ return props.items;
30
+ },
31
+ set items(v) {
32
+ props.items = v;
33
+ },
34
+ get filterText() {
35
+ return props.filterText;
36
+ },
37
+ set filterText(v) {
38
+ props.filterText = v;
39
+ },
40
+ get justValue() {
41
+ return props.justValue;
42
+ },
43
+ set justValue(v) {
44
+ props.justValue = v;
45
+ },
46
+ get listOpen() {
47
+ return props.listOpen;
48
+ },
49
+ set listOpen(v) {
50
+ props.listOpen = v;
51
+ },
52
+ get loading() {
53
+ return props.loading;
54
+ },
55
+ set loading(v) {
56
+ props.loading = v;
57
+ },
58
+ get focused() {
59
+ return props.focused;
60
+ },
61
+ set focused(v) {
62
+ props.focused = v;
63
+ },
64
+ get hoverItemIndex() {
65
+ return props.hoverItemIndex;
66
+ },
67
+ set hoverItemIndex(v) {
68
+ props.hoverItemIndex = v;
69
+ },
70
+ get multiple() {
71
+ return props.multiple;
72
+ },
73
+ get itemId() {
74
+ return props.itemId;
75
+ },
76
+ get label() {
77
+ return props.label;
78
+ },
79
+ get searchable() {
80
+ return props.searchable;
81
+ },
82
+ get disabled() {
83
+ return props.disabled;
84
+ },
85
+ get useJustValue() {
86
+ return props.useJustValue;
87
+ },
88
+ get closeListOnChange() {
89
+ return props.closeListOnChange;
90
+ },
91
+ get debounceWait() {
92
+ return props.debounceWait;
93
+ },
94
+ get groupBy() {
95
+ return props.groupBy;
96
+ },
97
+ get loadOptions() {
98
+ return props.loadOptions;
99
+ },
100
+ get loadOptionsDeps() {
101
+ return props.loadOptionsDeps;
102
+ },
103
+ get filteredItems() {
104
+ return props.filteredItems;
105
+ },
106
+ get normalizedValue() {
107
+ return props.normalizedValue;
108
+ },
109
+ get hasValue() {
110
+ return props.hasValue;
111
+ },
112
+ get activeValue() {
113
+ return activeValue;
114
+ },
115
+ set activeValue(v) {
116
+ activeValue = v;
117
+ },
118
+ get isScrolling() {
119
+ return isScrolling;
120
+ },
121
+ set isScrolling(v) {
122
+ isScrolling = v;
123
+ },
124
+ get clearState() {
125
+ return clearState;
126
+ },
127
+ set clearState(v) {
128
+ clearState = v;
129
+ },
130
+ get prevValue() {
131
+ return prevValue;
132
+ },
133
+ set prevValue(v) {
134
+ prevValue = v;
135
+ },
136
+ get prevFilterText() {
137
+ return prevFilterText;
138
+ },
139
+ set prevFilterText(v) {
140
+ prevFilterText = v;
141
+ },
142
+ get prevMultiple() {
143
+ return prevMultiple;
144
+ },
145
+ set prevMultiple(v) {
146
+ prevMultiple = v;
147
+ },
148
+ get suppressValueHoverSnap() {
149
+ return suppressValueHoverSnap;
150
+ },
151
+ set suppressValueHoverSnap(v) {
152
+ suppressValueHoverSnap = v;
153
+ },
154
+ get userNavigatedSinceOpen() {
155
+ return userNavigatedSinceOpen;
156
+ },
157
+ set userNavigatedSinceOpen(v) {
158
+ userNavigatedSinceOpen = v;
159
+ },
160
+ };
161
+ }
@@ -1,71 +1,10 @@
1
1
  .svelte-select {
2
- /* deprecating camelCase custom props in favour of kebab-case for v5 */
3
- --borderRadius: var(--border-radius);
4
- --clearSelectColor: var(--clear-select-color);
5
- --clearSelectWidth: var(--clear-select-width);
6
- --disabledBackground: var(--disabled-background);
7
- --disabledBorderColor: var(--disabled-border-color);
8
- --disabledColor: var(--disabled-color);
9
- --disabledPlaceholderColor: var(--disabled-placeholder-color);
10
- --disabledPlaceholderOpacity: var(--disabled-placeholder-opacity);
11
- --errorBackground: var(--error-background);
12
- --errorBorder: var(--error-border);
13
- --groupItemPaddingLeft: var(--group-item-padding-left);
14
- --groupTitleColor: var(--group-title-color);
15
- --groupTitleFontSize: var(--group-title-font-size);
16
- --groupTitleFontWeight: var(--group-title-font-weight);
17
- --groupTitlePadding: var(--group-title-padding);
18
- --groupTitleTextTransform: var(--group-title-text-transform);
19
- --groupTitleBorderColor: var(--group-title-border-color);
20
- --groupTitleBorderWidth: var(--group-title-border-width);
21
- --groupTitleBorderStyle: var(--group-title-border-style);
22
- --indicatorColor: var(--chevron-color);
23
- --indicatorHeight: var(--chevron-height);
24
- --indicatorWidth: var(--chevron-width);
25
- --inputColor: var(--input-color);
26
- --inputLeft: var(--input-left);
27
- --inputLetterSpacing: var(--input-letter-spacing);
28
- --inputMargin: var(--input-margin);
29
- --inputPadding: var(--input-padding);
30
- --itemActiveBackground: var(--item-active-background);
31
- --itemColor: var(--item-color);
32
- --itemFirstBorderRadius: var(--item-first-border-radius);
33
- --itemHoverBG: var(--item-hover-bg);
34
- --itemHoverColor: var(--item-hover-color);
35
- --itemIsActiveBG: var(--item-is-active-bg);
36
- --itemIsActiveColor: var(--item-is-active-color);
37
- --itemIsNotSelectableColor: var(--item-is-not-selectable-color);
38
- --itemPadding: var(--item-padding);
39
- --listBackground: var(--list-background);
40
- --listBorder: var(--list-border);
41
- --listBorderRadius: var(--list-border-radius);
42
- --listEmptyColor: var(--list-empty-color);
43
- --listEmptyPadding: var(--list-empty-padding);
44
- --listEmptyTextAlign: var(--list-empty-text-align);
45
- --listMaxHeight: var(--list-max-height);
46
- --listPosition: var(--list-position);
47
- --listShadow: var(--list-shadow);
48
- --listZIndex: var(--list-z-index);
49
- --multiItemBG: var(--multi-item-bg);
50
- --multiItemBorderRadius: var(--multi-item-border-radius);
51
- --multiItemDisabledHoverBg: var(--multi-item-disabled-hover-bg);
52
- --multiItemDisabledHoverColor: var(--multi-item-disabled-hover-color);
53
- --multiItemHeight: var(--multi-item-height);
54
- --multiItemMargin: var(--multi-item-margin);
55
- --multiItemPadding: var(--multi-item-padding);
56
- --multiSelectInputMargin: var(--multi-select-input-margin);
57
- --multiSelectInputPadding: var(--multi-select-input-padding);
58
- --multiSelectPadding: var(--multi-select-padding);
59
- --placeholderColor: var(--placeholder-color);
60
- --placeholderOpacity: var(--placeholder-opacity);
61
- --selectedItemPadding: var(--selected-item-padding);
62
- --spinnerColor: var(--spinner-color);
63
- --spinnerHeight: var(--spinner-height);
64
- --spinnerWidth: var(--spinner-width);
65
-
66
2
  --internal-padding: 0 0 0 16px;
67
3
 
68
- border: var(--border, 1px solid #d8dbdf);
4
+ /* #858a93 is 3.47:1 on the default #fff background. The old #d8dbdf was 1.39:1 —
5
+ the border is what identifies the control's boundary, so it needs 3:1
6
+ (WCAG 1.4.11 Non-text Contrast). Override --border to restyle. */
7
+ border: var(--border, 1px solid #858a93);
69
8
  border-radius: var(--border-radius, 6px);
70
9
  min-height: var(--height, 42px);
71
10
  position: relative;
@@ -84,7 +23,9 @@
84
23
  }
85
24
 
86
25
  .svelte-select:hover {
87
- border: var(--border-hover, 1px solid #b2b8bf);
26
+ /* Darker than --border so hover stays a visible change, and 4.91:1 on #fff
27
+ (the old #b2b8bf was 2.00:1) */
28
+ border: var(--border-hover, 1px solid #67727d);
88
29
  }
89
30
 
90
31
  .value-container {
@@ -136,7 +77,7 @@ input {
136
77
  }
137
78
 
138
79
  input::placeholder {
139
- color: var(--placeholder-color, #78848f);
80
+ color: var(--placeholder-color, #67727d);
140
81
  opacity: var(--placeholder-opacity, 1);
141
82
  }
142
83
 
@@ -147,6 +88,10 @@ input:focus {
147
88
  .svelte-select.focused {
148
89
  border: var(--border-focused, 1px solid #006fe8);
149
90
  border-radius: var(--border-radius-focused, var(--border-radius, 6px));
91
+ /* A focus ring in addition to the border colour change: the input's own
92
+ outline is suppressed, so relying on the 1px border alone was a
93
+ colour-only cue (WCAG 2.4.7/2.4.11) that vanished if --border was overridden. */
94
+ box-shadow: var(--focused-box-shadow, 0 0 0 2px rgba(0, 111, 232, 0.4));
150
95
  }
151
96
 
152
97
  .disabled {
@@ -203,7 +148,7 @@ input:focus {
203
148
  flex-shrink: 0;
204
149
  }
205
150
 
206
- .clear-select:focus {
151
+ .clear-select:focus-visible {
207
152
  outline: var(--clear-select-focus-outline, 1px solid #006fe8);
208
153
  }
209
154
 
@@ -283,12 +228,33 @@ input:focus {
283
228
  }
284
229
 
285
230
  .multi-item-clear {
231
+ all: unset;
286
232
  display: flex;
287
233
  align-items: center;
288
234
  justify-content: center;
235
+ cursor: pointer;
236
+ /* The button stretches to the chip's height (25px); without a width floor
237
+ it shrinks to the 20px icon, under the 24px activation-target minimum
238
+ (WCAG 2.2 2.5.8) */
239
+ min-width: var(--multi-item-clear-min-width, 24px);
289
240
  --clear-icon-color: var(--multi-item-clear-icon-color, #000);
290
241
  }
291
242
 
243
+ .multi-item-clear:focus-visible {
244
+ outline: var(--multi-item-clear-focus-outline, 1px solid #006fe8);
245
+ }
246
+
247
+ /* With multiFullItemClearable the whole chip is a real tab stop (role="button",
248
+ tabindex="0"). .multi-item sets `outline` unconditionally, and an author outline
249
+ beats the UA's :focus-visible ring by cascade origin, so without this rule a
250
+ keyboard-focused chip looked identical to an unfocused one (WCAG 2.4.7 Focus
251
+ Visible). Distinct from .multi-item.active below, which is the arrow-key tag
252
+ cursor, not DOM focus. */
253
+ .multi-item:focus-visible {
254
+ outline: var(--multi-item-focus-outline, 2px solid #006fe8);
255
+ outline-offset: -1px;
256
+ }
257
+
292
258
  .multi-item.active {
293
259
  outline: var(--multi-item-active-outline, 1px solid #006fe8);
294
260
  }
@@ -329,7 +295,7 @@ input:focus {
329
295
  .empty {
330
296
  text-align: var(--list-empty-text-align, center);
331
297
  padding: var(--list-empty-padding, 20px 0);
332
- color: var(--list-empty-color, #78848f);
298
+ color: var(--list-empty-color, #67727d);
333
299
  }
334
300
 
335
301
  .item {
@@ -355,7 +321,7 @@ input:focus {
355
321
  }
356
322
 
357
323
  .item.active {
358
- background: var(--item-is-active-bg, #007aff);
324
+ background: var(--item-is-active-bg, #006fe8);
359
325
  color: var(--item-is-active-color, #fff);
360
326
  }
361
327
 
@@ -363,9 +329,28 @@ input:focus {
363
329
  border-radius: var(--item-first-border-radius, 4px 4px 0 0);
364
330
  }
365
331
 
332
+ /* Mirror of .item.first: without it the last item's hover/selection background
333
+ (and the keyboard-cursor outline) square off against the list's rounded
334
+ bottom corners */
335
+ .item.last {
336
+ border-radius: var(--item-last-border-radius, 0 0 4px 4px);
337
+ }
338
+
339
+ /* A single-option list is both first and last: round all corners (the two
340
+ shorthand variables above cannot compose, so this matches their defaults) */
341
+ .item.first.last {
342
+ border-radius: 4px;
343
+ }
344
+
366
345
  .item.hover:not(.active) {
367
346
  background: var(--item-hover-bg, #e7f2ff);
368
347
  color: var(--item-hover-color, inherit);
348
+ /* The background alone is ~1.13:1 against the default #fff list — a
349
+ colour-only cue for the keyboard cursor (WCAG 1.4.11 Non-text Contrast).
350
+ #006fe8 is 4.5:1 on #fff and ~4.1:1 on the hover background; set
351
+ --item-hover-outline to `none` to opt out. */
352
+ outline: var(--item-hover-outline, 2px solid #006fe8);
353
+ outline-offset: -2px;
369
354
  }
370
355
 
371
356
  .item.not-selectable,
@@ -374,6 +359,22 @@ input:focus {
374
359
  .item.not-selectable:active {
375
360
  color: var(--item-is-not-selectable-color, #999);
376
361
  background: transparent;
362
+ /* Not actionable, so it must not show the keyboard-cursor outline either */
363
+ outline: none;
364
+ }
365
+
366
+ /* Group titles are informative text, not disabled controls, so WCAG 1.4.3's
367
+ inactive-component exemption does not apply to them: the not-selectable
368
+ dimming above (#999 = 2.84:1) must not reach a header. Keep them at the
369
+ group-title color (default #000 = 21:1 on white). The hover variant must
370
+ win over `.item.hover.item.not-selectable` too: the first frame parks the
371
+ keyboard cursor on index 0 before the keep-hover effect moves it off a
372
+ header, and without it that frame paints #999 which then *animates* back
373
+ through `transition: all` — mid-transition greys fail contrast when
374
+ sampled (the CI-only axe flake). */
375
+ .item.list-group-title.not-selectable,
376
+ .item.hover.item.list-group-title.not-selectable {
377
+ color: var(--group-title-color, #000000);
377
378
  }
378
379
 
379
380
  .required {
@@ -384,4 +385,44 @@ input:focus {
384
385
  left: 0;
385
386
  bottom: 0;
386
387
  right: 0;
387
- }
388
+ }
389
+
390
+ @media (prefers-reduced-motion: reduce) {
391
+ .item {
392
+ transition: none;
393
+ }
394
+ }
395
+
396
+ @media (forced-colors: active) {
397
+ /* box-shadow is dropped in forced-colors mode, so the focus ring above is
398
+ invisible there — restore a real outline using a system colour. */
399
+ .svelte-select.focused {
400
+ outline: 2px solid Highlight;
401
+ outline-offset: 1px;
402
+ }
403
+
404
+ /* Custom background colours are also flattened, so distinguish the selected
405
+ option (filled) from the option under the keyboard cursor (outlined). */
406
+ .item.active {
407
+ background: Highlight;
408
+ color: HighlightText;
409
+ }
410
+
411
+ .item.hover:not(.active) {
412
+ outline: 2px solid Highlight;
413
+ outline-offset: -2px;
414
+ }
415
+
416
+ /* The chip's focus ring is an author colour, which is flattened here */
417
+ .multi-item:focus-visible {
418
+ outline: 2px solid Highlight;
419
+ }
420
+
421
+ /* Every chip carries a 1px outline and the arrow-key tag cursor differs
422
+ only by outline colour — flattened to the same system colour here, the
423
+ Backspace target vanished. Width + style survive the forced palette. */
424
+ .multi-item.active {
425
+ outline: 2px dashed Highlight;
426
+ outline-offset: -2px;
427
+ }
428
+ }
package/dist/tailwind.css CHANGED
@@ -2,16 +2,25 @@
2
2
 
3
3
  .svelte-select {
4
4
  @apply border rounded box-border h-10 relative flex items-center px-4 py-0 bg-white m-0 w-full
5
- hover:border-gray-400;
5
+ hover:border-gray-500;
6
+ /* gray-500 (~4.8:1 on white), not gray-400 (~2.5:1): preflight defaults the
7
+ border to currentColor, so a gray-400 hover *lightened* the control's
8
+ boundary below the 3:1 non-text bar (WCAG 1.4.11) at the moment of
9
+ interaction — mirrors --border-hover (#67727d) in styles/default.css. */
6
10
  }
7
11
 
8
12
  .svelte-select input {
9
- @apply cursor-default border-none text-gray-600 h-10 leading-10 px-4 py-0 bg-transparent text-sm absolute left-0 m-0 w-full
10
- focus:outline-none hover:border-gray-400;
13
+ @apply cursor-default border-none text-gray-600 h-10 leading-10 px-4 py-0 bg-transparent text-sm absolute left-0 m-0 w-full
14
+ focus:outline-none
15
+ placeholder:text-gray-500 placeholder:opacity-100;
16
+ /* Without an explicit placeholder color, Tailwind v4 preflight's 50%-alpha
17
+ currentColor ::placeholder applies — ~2.3:1 on white, a WCAG 1.4.3
18
+ failure on the control's primary prompt. gray-500 mirrors the default
19
+ theme's #67727d (~4.9:1); the disabled rule below still overrides. */
11
20
  }
12
21
 
13
22
  .svelte-select.focused {
14
- @apply border-blue-600;
23
+ @apply border-blue-600 ring-2 ring-blue-600/40;
15
24
  }
16
25
 
17
26
  .svelte-select.disabled {
@@ -27,11 +36,11 @@
27
36
  focus:outline-none;
28
37
  }
29
38
 
30
- .svelte-select .icons {
31
- @apply absolute flex items-center right-0 translate-y-0 text-gray-200 pointer-events-none top-0 bottom-0;
39
+ .svelte-select .indicators {
40
+ @apply absolute flex items-center right-0 translate-y-0 text-gray-500 pointer-events-none top-0 bottom-0;
32
41
  }
33
42
 
34
- .svelte-select .icons > * {
43
+ .svelte-select .indicators > * {
35
44
  @apply transition-colors ease-in-out duration-200;
36
45
  }
37
46
 
@@ -39,18 +48,20 @@
39
48
  @apply pointer-events-auto;
40
49
  }
41
50
 
42
- .svelte-select.focused .icons,
51
+ .svelte-select.focused .indicators,
43
52
  .svelte-select .chevron:hover,
44
53
  .svelte-select .clear-select:hover {
45
54
  @apply text-gray-600;
46
55
  }
47
56
 
48
57
  .svelte-select .clear-select {
49
- @apply px-2 h-5 text-gray-300 flex-none w-9;
58
+ /* h-6: 24px minimum in both dimensions (WCAG 2.2 2.5.8 target size)
59
+ h-5 left the activation target 20px tall */
60
+ @apply px-2 h-6 text-gray-500 flex-none w-9;
50
61
  }
51
62
 
52
63
  .svelte-select .chevron {
53
- @apply flex pt-0 pr-2 pl-2 border-l-2 w-9 h-5 text-gray-300;
64
+ @apply flex pt-0 pr-2 pl-2 border-l-2 w-9 h-5 text-gray-500;
54
65
  }
55
66
 
56
67
  .svelte-select.multi {
@@ -69,15 +80,19 @@
69
80
  @apply sr-only;
70
81
  }
71
82
 
72
- .list {
83
+ .svelte-select-list {
73
84
  @apply shadow-md rounded-sm max-h-64 overflow-y-auto bg-white border-none absolute z-10 w-full left-0 right-0;
74
85
  }
75
86
 
76
- .list .list-group-title {
87
+ .svelte-select-list.prefloat {
88
+ @apply opacity-0 pointer-events-none;
89
+ }
90
+
91
+ .svelte-select-list .list-group-title {
77
92
  @apply text-slate-800 cursor-default text-sm font-medium h-10 leading-10 px-5 text-ellipsis whitespace-nowrap uppercase;
78
93
  }
79
94
 
80
- .list .empty {
95
+ .svelte-select-list .empty {
81
96
  @apply text-center py-5 text-gray-500;
82
97
  }
83
98
 
@@ -97,16 +112,47 @@
97
112
  @apply bg-blue-600 text-white;
98
113
  }
99
114
 
100
- .item.not-selectable {
101
- @apply text-gray-300;
115
+ /* Disabled options are not actionable, so they must not show the hover/active
116
+ affordances (background or the keyboard-cursor outline). gray-400 tracks the
117
+ default theme's #999 (2.84:1 — allowed by WCAG 1.4.3's inactive-component
118
+ exemption). */
119
+ .item.not-selectable,
120
+ .item.hover.item.not-selectable,
121
+ .item.active.item.not-selectable,
122
+ .item.not-selectable:active {
123
+ @apply text-gray-400 bg-transparent;
124
+ outline: none;
125
+ }
126
+
127
+ /* Group titles are informative text, not disabled controls, so WCAG 1.4.3's
128
+ inactive-component exemption does not apply to them: the not-selectable
129
+ dimming above must not reach a header. Keep them at the group-title color,
130
+ including during the transient first frame where the keyboard cursor parks
131
+ on index 0 before the keep-hover effect moves it off a header. (Mirrors
132
+ styles/default.css — keep the two themes in sync.) */
133
+ .item.list-group-title.not-selectable,
134
+ .item.hover.item.list-group-title.not-selectable {
135
+ @apply text-slate-800;
102
136
  }
103
137
 
104
138
  .item.first {
105
139
  @apply rounded-t-sm;
106
140
  }
107
141
 
142
+ .item.last {
143
+ @apply rounded-b-sm;
144
+ }
145
+
146
+ .item.first.last {
147
+ @apply rounded-sm;
148
+ }
149
+
108
150
  .item.hover:not(.active) {
109
151
  @apply bg-blue-100;
152
+ /* The background alone is a colour-only cue for the keyboard cursor
153
+ (WCAG 1.4.11) — blue-600 is >=3:1 on white and on blue-100 */
154
+ outline: 2px solid #2563eb;
155
+ outline-offset: -2px;
110
156
  }
111
157
 
112
158
  .multi input {
@@ -114,15 +160,72 @@
114
160
  }
115
161
 
116
162
  .multi-item {
117
- @apply bg-gray-100 mt-1 mr-1 border border-gray-200 rounded-sm h-8 leading-8 flex cursor-default pr-1 pl-1 max-w-full items-center mr-1 overflow-hidden text-ellipsis whitespace-nowrap;
163
+ @apply bg-gray-100 mt-1 mr-1 border border-gray-200 rounded-sm h-8 leading-8 flex cursor-default pr-1 pl-1 max-w-full items-center overflow-hidden text-ellipsis whitespace-nowrap;
118
164
  }
119
165
 
120
166
  .multi-item.disabled {
121
167
  @apply hover:bg-gray-300 hover:text-gray-500;
122
168
  }
123
169
 
170
+ /* Keyboard-focused chip (multiFullItemClearable): explicit ring in the theme
171
+ accent (mirrors styles/default.css). Distinct from .multi-item.active below,
172
+ which is the arrow-key tag cursor, not DOM focus. */
173
+ .multi-item:focus-visible {
174
+ outline: 2px solid #2563eb;
175
+ outline-offset: -1px;
176
+ }
177
+
178
+ /* ArrowLeft/ArrowRight park a virtual cursor on a tag before Backspace removes
179
+ it; without a visible indicator a sighted keyboard user deletes a chip they
180
+ cannot see targeted (WCAG 1.4.11 non-text contrast: blue-600 is >=3:1 on
181
+ white and on the gray-100 chip). */
182
+ .multi-item.active {
183
+ outline: 1px solid #2563eb;
184
+ }
185
+
124
186
  .multi-item-clear {
125
- @apply flex items-center justify-center w-5;
187
+ /* w-6 + self-stretch: a 24x32px activation target (WCAG 2.2 2.5.8) —
188
+ w-5 alone left the remove button 20x20px */
189
+ @apply flex items-center justify-center w-6 self-stretch;
190
+ }
191
+
192
+ @media (prefers-reduced-motion: reduce) {
193
+ .svelte-select .indicators > * {
194
+ @apply transition-none;
195
+ }
196
+ }
197
+
198
+ @media (forced-colors: active) {
199
+ /* The ring above uses box-shadow, which forced-colors mode drops; restore a
200
+ real outline, and distinguish selected (filled) from cursor (outlined). */
201
+ .svelte-select.focused {
202
+ outline: 2px solid Highlight;
203
+ outline-offset: 1px;
204
+ }
205
+
206
+ .item.active {
207
+ background: Highlight;
208
+ color: HighlightText;
209
+ }
210
+
211
+ .item.hover:not(.active) {
212
+ outline: 2px solid Highlight;
213
+ outline-offset: -2px;
214
+ }
215
+
216
+ /* The chip's focus ring is an author colour, which is flattened here */
217
+ .multi-item:focus-visible {
218
+ outline: 2px solid Highlight;
219
+ }
220
+
221
+ /* The arrow-key tag cursor (the normal-mode .multi-item.active rule above)
222
+ differs from an ordinary chip only by an author outline colour, which is
223
+ flattened here — width + style survive the forced palette, so the
224
+ Backspace target stays visible. */
225
+ .multi-item.active {
226
+ outline: 2px dashed Highlight;
227
+ outline-offset: -2px;
228
+ }
126
229
  }
127
230
 
128
231
  .list-item {