@synthaxai/ui 1.0.0 → 1.2.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.
- package/LICENSE +9 -0
- package/README.md +40 -220
- package/dist/data-display/DataTable/DataTable.svelte +14 -8
- package/dist/data-display/DataTable/DataTable.svelte.d.ts +64 -61
- package/dist/data-display/DataTable/DataTable.svelte.d.ts.map +1 -1
- package/dist/forms/DatePicker/DatePicker.svelte +1564 -0
- package/dist/forms/DatePicker/DatePicker.svelte.d.ts +129 -0
- package/dist/forms/DatePicker/DatePicker.svelte.d.ts.map +1 -0
- package/dist/forms/DatePicker/index.d.ts +8 -0
- package/dist/forms/DatePicker/index.d.ts.map +1 -0
- package/dist/forms/DatePicker/index.js +7 -0
- package/dist/forms/MultiSelect/MultiSelect.svelte +1651 -0
- package/dist/forms/MultiSelect/MultiSelect.svelte.d.ts +127 -0
- package/dist/forms/MultiSelect/MultiSelect.svelte.d.ts.map +1 -0
- package/dist/forms/MultiSelect/index.d.ts +9 -0
- package/dist/forms/MultiSelect/index.d.ts.map +1 -0
- package/dist/forms/MultiSelect/index.js +7 -0
- package/dist/forms/SearchInput/SearchInput.svelte +1319 -0
- package/dist/forms/SearchInput/SearchInput.svelte.d.ts +148 -0
- package/dist/forms/SearchInput/SearchInput.svelte.d.ts.map +1 -0
- package/dist/forms/SearchInput/index.d.ts +9 -0
- package/dist/forms/SearchInput/index.d.ts.map +1 -0
- package/dist/forms/SearchInput/index.js +7 -0
- package/dist/forms/TextInput/TextInput.svelte +763 -122
- package/dist/forms/TextInput/TextInput.svelte.d.ts +98 -13
- package/dist/forms/TextInput/TextInput.svelte.d.ts.map +1 -1
- package/dist/forms/TextInput/index.d.ts +7 -1
- package/dist/forms/TextInput/index.d.ts.map +1 -1
- package/dist/forms/TextInput/index.js +7 -1
- package/dist/forms/index.d.ts +3 -0
- package/dist/forms/index.d.ts.map +1 -1
- package/dist/forms/index.js +3 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/package.json +7 -7
|
@@ -0,0 +1,1651 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component MultiSelect
|
|
3
|
+
|
|
4
|
+
A fully accessible multi-select component with chips/tags display,
|
|
5
|
+
following WAI-ARIA 1.2 listbox pattern. Supports searchable options,
|
|
6
|
+
grouped options, and comprehensive keyboard navigation.
|
|
7
|
+
|
|
8
|
+
Designed for healthcare applications requiring selection of multiple items
|
|
9
|
+
such as service types, diagnoses, medications, or procedures.
|
|
10
|
+
|
|
11
|
+
@example Basic usage
|
|
12
|
+
<MultiSelect
|
|
13
|
+
label="Service Types"
|
|
14
|
+
options={serviceTypes}
|
|
15
|
+
bind:selected={selectedServices}
|
|
16
|
+
placeholder="Select services..."
|
|
17
|
+
/>
|
|
18
|
+
|
|
19
|
+
@example With search and max selections
|
|
20
|
+
<MultiSelect
|
|
21
|
+
label="Diagnoses"
|
|
22
|
+
options={diagnoses}
|
|
23
|
+
bind:selected={selectedDiagnoses}
|
|
24
|
+
searchable
|
|
25
|
+
maxSelections={5}
|
|
26
|
+
placeholder="Search diagnoses..."
|
|
27
|
+
/>
|
|
28
|
+
-->
|
|
29
|
+
<script lang="ts">
|
|
30
|
+
import type { Snippet } from 'svelte';
|
|
31
|
+
import { X, ChevronDown, Check, AlertCircle, Search, Info, Loader2 } from 'lucide-svelte';
|
|
32
|
+
import { cn } from '../../utils/cn.js';
|
|
33
|
+
import { generateId, Keys } from '../../utils/keyboard.js';
|
|
34
|
+
import { announce, prefersReducedMotion } from '../../utils/accessibility.js';
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Types
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
type InputSize = 'sm' | 'md' | 'lg';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Multi-select option item.
|
|
44
|
+
*/
|
|
45
|
+
export interface MultiSelectOption {
|
|
46
|
+
/** Unique value for the option */
|
|
47
|
+
value: string;
|
|
48
|
+
/** Display label for the option */
|
|
49
|
+
label: string;
|
|
50
|
+
/** Whether the option is disabled */
|
|
51
|
+
disabled?: boolean;
|
|
52
|
+
/** Optional description shown below the label */
|
|
53
|
+
description?: string;
|
|
54
|
+
/** Optional metadata for custom rendering */
|
|
55
|
+
metadata?: Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Grouped multi-select options.
|
|
60
|
+
*/
|
|
61
|
+
export interface MultiSelectOptionGroup {
|
|
62
|
+
/** Group label */
|
|
63
|
+
label: string;
|
|
64
|
+
/** Options in this group */
|
|
65
|
+
options: MultiSelectOption[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type MultiSelectOptions = MultiSelectOption[] | MultiSelectOptionGroup[];
|
|
69
|
+
|
|
70
|
+
interface Props {
|
|
71
|
+
/** Unique identifier for the component */
|
|
72
|
+
id?: string;
|
|
73
|
+
/** Form field name */
|
|
74
|
+
name?: string;
|
|
75
|
+
/** Label text for the field */
|
|
76
|
+
label: string;
|
|
77
|
+
/** Currently selected options (bindable) */
|
|
78
|
+
selected?: MultiSelectOption[];
|
|
79
|
+
/** Placeholder text when no selections */
|
|
80
|
+
placeholder?: string;
|
|
81
|
+
/** Options to display (flat or grouped) */
|
|
82
|
+
options?: MultiSelectOptions;
|
|
83
|
+
/** Whether the field is required */
|
|
84
|
+
required?: boolean;
|
|
85
|
+
/** Whether the field is disabled */
|
|
86
|
+
disabled?: boolean;
|
|
87
|
+
/** Whether the field is read-only */
|
|
88
|
+
readonly?: boolean;
|
|
89
|
+
/** Error message to display */
|
|
90
|
+
error?: string;
|
|
91
|
+
/** Hint text to display below the component */
|
|
92
|
+
hint?: string;
|
|
93
|
+
/** Success message when valid */
|
|
94
|
+
successMessage?: string;
|
|
95
|
+
/** Size of the component */
|
|
96
|
+
size?: InputSize;
|
|
97
|
+
/** Whether to hide the label visually (still accessible) */
|
|
98
|
+
hideLabel?: boolean;
|
|
99
|
+
/** Additional CSS classes for the wrapper */
|
|
100
|
+
class?: string;
|
|
101
|
+
/** Enable search/filter functionality */
|
|
102
|
+
searchable?: boolean;
|
|
103
|
+
/** Search placeholder text */
|
|
104
|
+
searchPlaceholder?: string;
|
|
105
|
+
/** Maximum number of selections allowed */
|
|
106
|
+
maxSelections?: number;
|
|
107
|
+
/** Whether to close dropdown after each selection */
|
|
108
|
+
closeOnSelect?: boolean;
|
|
109
|
+
/** Show loading spinner during async operations */
|
|
110
|
+
loading?: boolean;
|
|
111
|
+
/** Message to show when no options match search */
|
|
112
|
+
emptyMessage?: string;
|
|
113
|
+
/** Whether to show "Select All" option */
|
|
114
|
+
showSelectAll?: boolean;
|
|
115
|
+
/** Label for "Select All" */
|
|
116
|
+
selectAllLabel?: string;
|
|
117
|
+
/** Whether to show selected count badge */
|
|
118
|
+
showCount?: boolean;
|
|
119
|
+
/** Custom chip rendering */
|
|
120
|
+
chipContent?: Snippet<[MultiSelectOption]>;
|
|
121
|
+
/** Custom option rendering */
|
|
122
|
+
optionContent?: Snippet<[MultiSelectOption]>;
|
|
123
|
+
/** Custom empty state content */
|
|
124
|
+
emptyContent?: Snippet;
|
|
125
|
+
/** Called when selection changes */
|
|
126
|
+
onchange?: (selected: MultiSelectOption[]) => void;
|
|
127
|
+
/** Called when an option is selected */
|
|
128
|
+
onselect?: (option: MultiSelectOption) => void;
|
|
129
|
+
/** Called when an option is deselected */
|
|
130
|
+
ondeselect?: (option: MultiSelectOption) => void;
|
|
131
|
+
/** Called when search query changes */
|
|
132
|
+
onsearch?: (query: string) => void;
|
|
133
|
+
/** Test ID for e2e testing */
|
|
134
|
+
testId?: string;
|
|
135
|
+
/** Show validation state immediately */
|
|
136
|
+
validateOnMount?: boolean;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let {
|
|
140
|
+
id = generateId('multiselect'),
|
|
141
|
+
name,
|
|
142
|
+
label,
|
|
143
|
+
selected = $bindable([]),
|
|
144
|
+
placeholder = 'Select options...',
|
|
145
|
+
options = [],
|
|
146
|
+
required = false,
|
|
147
|
+
disabled = false,
|
|
148
|
+
readonly = false,
|
|
149
|
+
error = '',
|
|
150
|
+
hint = '',
|
|
151
|
+
successMessage = '',
|
|
152
|
+
size = 'md',
|
|
153
|
+
hideLabel = false,
|
|
154
|
+
class: className = '',
|
|
155
|
+
searchable = false,
|
|
156
|
+
searchPlaceholder = 'Search...',
|
|
157
|
+
maxSelections,
|
|
158
|
+
closeOnSelect = false,
|
|
159
|
+
loading = false,
|
|
160
|
+
emptyMessage = 'No options found',
|
|
161
|
+
showSelectAll = false,
|
|
162
|
+
selectAllLabel = 'Select All',
|
|
163
|
+
showCount = true,
|
|
164
|
+
chipContent,
|
|
165
|
+
optionContent,
|
|
166
|
+
emptyContent,
|
|
167
|
+
onchange,
|
|
168
|
+
onselect,
|
|
169
|
+
ondeselect,
|
|
170
|
+
onsearch,
|
|
171
|
+
testId,
|
|
172
|
+
validateOnMount = false
|
|
173
|
+
}: Props = $props();
|
|
174
|
+
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// State
|
|
177
|
+
// ============================================================================
|
|
178
|
+
|
|
179
|
+
let containerRef: HTMLDivElement | undefined = $state();
|
|
180
|
+
let inputRef: HTMLInputElement | HTMLButtonElement | undefined = $state();
|
|
181
|
+
let listboxRef: HTMLDivElement | undefined = $state();
|
|
182
|
+
let isOpen = $state(false);
|
|
183
|
+
let isFocused = $state(false);
|
|
184
|
+
let hasInteracted = $state(false);
|
|
185
|
+
let searchQuery = $state('');
|
|
186
|
+
let activeIndex = $state(-1);
|
|
187
|
+
let focusedChipIndex = $state(-1);
|
|
188
|
+
|
|
189
|
+
// ============================================================================
|
|
190
|
+
// Derived State
|
|
191
|
+
// ============================================================================
|
|
192
|
+
|
|
193
|
+
// Flatten grouped options
|
|
194
|
+
const flatOptions = $derived.by(() => {
|
|
195
|
+
if (options.length === 0) return [];
|
|
196
|
+
if ('options' in options[0]) {
|
|
197
|
+
return (options as MultiSelectOptionGroup[]).flatMap((g) => g.options);
|
|
198
|
+
}
|
|
199
|
+
return options as MultiSelectOption[];
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Check if options are grouped
|
|
203
|
+
const isGrouped = $derived(options.length > 0 && 'options' in options[0]);
|
|
204
|
+
|
|
205
|
+
// Filter options based on search
|
|
206
|
+
const filteredOptions = $derived.by(() => {
|
|
207
|
+
if (!searchable || !searchQuery.trim()) {
|
|
208
|
+
return options;
|
|
209
|
+
}
|
|
210
|
+
const query = searchQuery.toLowerCase().trim();
|
|
211
|
+
|
|
212
|
+
if (isGrouped) {
|
|
213
|
+
const groups = options as MultiSelectOptionGroup[];
|
|
214
|
+
return groups
|
|
215
|
+
.map((group) => ({
|
|
216
|
+
...group,
|
|
217
|
+
options: group.options.filter(
|
|
218
|
+
(opt) =>
|
|
219
|
+
opt.label.toLowerCase().includes(query) ||
|
|
220
|
+
opt.description?.toLowerCase().includes(query)
|
|
221
|
+
)
|
|
222
|
+
}))
|
|
223
|
+
.filter((group) => group.options.length > 0);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return (options as MultiSelectOption[]).filter(
|
|
227
|
+
(opt) =>
|
|
228
|
+
opt.label.toLowerCase().includes(query) ||
|
|
229
|
+
opt.description?.toLowerCase().includes(query)
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Flatten filtered options for navigation
|
|
234
|
+
const filteredFlatOptions = $derived.by(() => {
|
|
235
|
+
if (!Array.isArray(filteredOptions) || filteredOptions.length === 0) return [];
|
|
236
|
+
if ('options' in filteredOptions[0]) {
|
|
237
|
+
return (filteredOptions as MultiSelectOptionGroup[]).flatMap((g) => g.options);
|
|
238
|
+
}
|
|
239
|
+
return filteredOptions as MultiSelectOption[];
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Filter to only enabled options for navigation
|
|
243
|
+
const enabledOptions = $derived(
|
|
244
|
+
filteredFlatOptions.filter((o) => !o.disabled && !isSelected(o))
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// All enabled options (including selected, for keyboard nav)
|
|
248
|
+
const allEnabledOptions = $derived(filteredFlatOptions.filter((o) => !o.disabled));
|
|
249
|
+
|
|
250
|
+
// Selected values set for quick lookup
|
|
251
|
+
const selectedValues = $derived(new Set(selected.map((o) => o.value)));
|
|
252
|
+
|
|
253
|
+
// Check if at max selections
|
|
254
|
+
const atMaxSelections = $derived(
|
|
255
|
+
maxSelections !== undefined && selected.length >= maxSelections
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// Check if all options are selected
|
|
259
|
+
const allSelected = $derived(
|
|
260
|
+
flatOptions.length > 0 &&
|
|
261
|
+
flatOptions.filter((o) => !o.disabled).every((o) => selectedValues.has(o.value))
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// Validation display logic
|
|
265
|
+
const shouldValidate = $derived(hasInteracted || validateOnMount);
|
|
266
|
+
const showError = $derived(shouldValidate && !!error);
|
|
267
|
+
const showSuccess = $derived(shouldValidate && !error && selected.length > 0 && !!successMessage);
|
|
268
|
+
const isValid = $derived(shouldValidate && !error && selected.length > 0 && !!successMessage);
|
|
269
|
+
|
|
270
|
+
// Show empty state
|
|
271
|
+
const showEmpty = $derived(!loading && filteredFlatOptions.length === 0);
|
|
272
|
+
|
|
273
|
+
// IDs for accessibility
|
|
274
|
+
const listboxId = $derived(`${id}-listbox`);
|
|
275
|
+
const errorId = $derived(`${id}-error`);
|
|
276
|
+
const hintId = $derived(`${id}-hint`);
|
|
277
|
+
const labelId = $derived(`${id}-label`);
|
|
278
|
+
const activeDescendantId = $derived(
|
|
279
|
+
activeIndex >= 0 ? `${id}-option-${activeIndex}` : undefined
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// Build aria-describedby
|
|
283
|
+
const ariaDescribedBy = $derived(
|
|
284
|
+
[showError && errorId, hint && hintId].filter(Boolean).join(' ') || undefined
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// ============================================================================
|
|
288
|
+
// Helpers
|
|
289
|
+
// ============================================================================
|
|
290
|
+
|
|
291
|
+
function isSelected(option: MultiSelectOption): boolean {
|
|
292
|
+
return selectedValues.has(option.value);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function getOptionIndex(option: MultiSelectOption): number {
|
|
296
|
+
return filteredFlatOptions.findIndex((o) => o.value === option.value);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function scrollToOption(index: number) {
|
|
300
|
+
if (!listboxRef || index < 0) return;
|
|
301
|
+
const optionEl = listboxRef.querySelector(`[data-index="${index}"]`);
|
|
302
|
+
if (optionEl && typeof optionEl.scrollIntoView === 'function') {
|
|
303
|
+
optionEl.scrollIntoView({ block: 'nearest', behavior: prefersReducedMotion() ? 'auto' : 'smooth' });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ============================================================================
|
|
308
|
+
// Selection Logic
|
|
309
|
+
// ============================================================================
|
|
310
|
+
|
|
311
|
+
function toggleOption(option: MultiSelectOption) {
|
|
312
|
+
if (option.disabled) return;
|
|
313
|
+
|
|
314
|
+
if (isSelected(option)) {
|
|
315
|
+
// Deselect
|
|
316
|
+
selected = selected.filter((o) => o.value !== option.value);
|
|
317
|
+
announce(`Removed ${option.label}`, 'polite');
|
|
318
|
+
ondeselect?.(option);
|
|
319
|
+
} else {
|
|
320
|
+
// Check max selections
|
|
321
|
+
if (atMaxSelections) {
|
|
322
|
+
announce(`Maximum ${maxSelections} selections reached`, 'assertive');
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// Select
|
|
326
|
+
selected = [...selected, option];
|
|
327
|
+
announce(`Added ${option.label}`, 'polite');
|
|
328
|
+
onselect?.(option);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
onchange?.(selected);
|
|
332
|
+
|
|
333
|
+
if (closeOnSelect) {
|
|
334
|
+
isOpen = false;
|
|
335
|
+
activeIndex = -1;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function removeChip(option: MultiSelectOption, e?: MouseEvent) {
|
|
340
|
+
e?.preventDefault();
|
|
341
|
+
e?.stopPropagation();
|
|
342
|
+
|
|
343
|
+
if (disabled || readonly) return;
|
|
344
|
+
|
|
345
|
+
selected = selected.filter((o) => o.value !== option.value);
|
|
346
|
+
announce(`Removed ${option.label}`, 'polite');
|
|
347
|
+
ondeselect?.(option);
|
|
348
|
+
onchange?.(selected);
|
|
349
|
+
|
|
350
|
+
// Return focus to input
|
|
351
|
+
inputRef?.focus();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function selectAll() {
|
|
355
|
+
if (disabled || readonly) return;
|
|
356
|
+
|
|
357
|
+
if (allSelected) {
|
|
358
|
+
// Deselect all
|
|
359
|
+
selected = [];
|
|
360
|
+
announce('All options deselected', 'polite');
|
|
361
|
+
} else {
|
|
362
|
+
// Select all non-disabled options (respecting maxSelections)
|
|
363
|
+
const selectableOptions = flatOptions.filter((o) => !o.disabled);
|
|
364
|
+
if (maxSelections !== undefined) {
|
|
365
|
+
selected = selectableOptions.slice(0, maxSelections);
|
|
366
|
+
if (selectableOptions.length > maxSelections) {
|
|
367
|
+
announce(`Selected first ${maxSelections} options (maximum reached)`, 'polite');
|
|
368
|
+
} else {
|
|
369
|
+
announce('All options selected', 'polite');
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
selected = selectableOptions;
|
|
373
|
+
announce('All options selected', 'polite');
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
onchange?.(selected);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function clearAll() {
|
|
380
|
+
if (disabled || readonly) return;
|
|
381
|
+
selected = [];
|
|
382
|
+
announce('All selections cleared', 'polite');
|
|
383
|
+
onchange?.(selected);
|
|
384
|
+
inputRef?.focus();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ============================================================================
|
|
388
|
+
// Event Handlers
|
|
389
|
+
// ============================================================================
|
|
390
|
+
|
|
391
|
+
function handleContainerClick(e: MouseEvent) {
|
|
392
|
+
if (disabled || readonly) return;
|
|
393
|
+
|
|
394
|
+
// Don't open if clicking on chip remove button
|
|
395
|
+
const target = e.target as HTMLElement;
|
|
396
|
+
if (target.closest('.chip-remove')) return;
|
|
397
|
+
|
|
398
|
+
isOpen = true;
|
|
399
|
+
inputRef?.focus();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function handleInputFocus() {
|
|
403
|
+
isFocused = true;
|
|
404
|
+
focusedChipIndex = -1;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function handleInputBlur(e: FocusEvent) {
|
|
408
|
+
// Don't close if focus moves within the component
|
|
409
|
+
const relatedTarget = e.relatedTarget as HTMLElement | null;
|
|
410
|
+
if (relatedTarget && containerRef?.contains(relatedTarget)) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
isFocused = false;
|
|
415
|
+
hasInteracted = true;
|
|
416
|
+
focusedChipIndex = -1;
|
|
417
|
+
|
|
418
|
+
// Delay closing to allow click events
|
|
419
|
+
setTimeout(() => {
|
|
420
|
+
if (!isFocused) {
|
|
421
|
+
isOpen = false;
|
|
422
|
+
activeIndex = -1;
|
|
423
|
+
searchQuery = '';
|
|
424
|
+
}
|
|
425
|
+
}, 150);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function handleSearchInput(e: Event) {
|
|
429
|
+
const target = e.target as HTMLInputElement;
|
|
430
|
+
searchQuery = target.value;
|
|
431
|
+
activeIndex = 0;
|
|
432
|
+
onsearch?.(searchQuery);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
436
|
+
if (disabled || readonly) return;
|
|
437
|
+
|
|
438
|
+
// Handle chip navigation when not in dropdown
|
|
439
|
+
if (focusedChipIndex >= 0) {
|
|
440
|
+
handleChipKeyDown(e);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
switch (e.key) {
|
|
445
|
+
case Keys.ArrowDown:
|
|
446
|
+
e.preventDefault();
|
|
447
|
+
if (!isOpen) {
|
|
448
|
+
isOpen = true;
|
|
449
|
+
activeIndex = 0;
|
|
450
|
+
} else if (allEnabledOptions.length > 0) {
|
|
451
|
+
activeIndex = Math.min(activeIndex + 1, allEnabledOptions.length - 1);
|
|
452
|
+
const targetOption = allEnabledOptions[activeIndex];
|
|
453
|
+
const actualIndex = getOptionIndex(targetOption);
|
|
454
|
+
scrollToOption(actualIndex);
|
|
455
|
+
}
|
|
456
|
+
break;
|
|
457
|
+
|
|
458
|
+
case Keys.ArrowUp:
|
|
459
|
+
e.preventDefault();
|
|
460
|
+
if (isOpen && allEnabledOptions.length > 0) {
|
|
461
|
+
activeIndex = Math.max(activeIndex - 1, 0);
|
|
462
|
+
const targetOption = allEnabledOptions[activeIndex];
|
|
463
|
+
const actualIndex = getOptionIndex(targetOption);
|
|
464
|
+
scrollToOption(actualIndex);
|
|
465
|
+
}
|
|
466
|
+
break;
|
|
467
|
+
|
|
468
|
+
case Keys.ArrowLeft:
|
|
469
|
+
// Navigate to chips if at start of input
|
|
470
|
+
if (searchable && inputRef && inputRef instanceof HTMLInputElement && inputRef.selectionStart === 0 && selected.length > 0) {
|
|
471
|
+
e.preventDefault();
|
|
472
|
+
focusedChipIndex = selected.length - 1;
|
|
473
|
+
}
|
|
474
|
+
break;
|
|
475
|
+
|
|
476
|
+
case Keys.Home:
|
|
477
|
+
if (isOpen && allEnabledOptions.length > 0) {
|
|
478
|
+
e.preventDefault();
|
|
479
|
+
activeIndex = 0;
|
|
480
|
+
scrollToOption(getOptionIndex(allEnabledOptions[0]));
|
|
481
|
+
}
|
|
482
|
+
break;
|
|
483
|
+
|
|
484
|
+
case Keys.End:
|
|
485
|
+
if (isOpen && allEnabledOptions.length > 0) {
|
|
486
|
+
e.preventDefault();
|
|
487
|
+
activeIndex = allEnabledOptions.length - 1;
|
|
488
|
+
scrollToOption(getOptionIndex(allEnabledOptions[activeIndex]));
|
|
489
|
+
}
|
|
490
|
+
break;
|
|
491
|
+
|
|
492
|
+
case Keys.Enter:
|
|
493
|
+
if (isOpen && activeIndex >= 0 && activeIndex < allEnabledOptions.length) {
|
|
494
|
+
e.preventDefault();
|
|
495
|
+
toggleOption(allEnabledOptions[activeIndex]);
|
|
496
|
+
}
|
|
497
|
+
break;
|
|
498
|
+
|
|
499
|
+
case Keys.Space:
|
|
500
|
+
if (!searchable) {
|
|
501
|
+
e.preventDefault();
|
|
502
|
+
if (isOpen && activeIndex >= 0 && activeIndex < allEnabledOptions.length) {
|
|
503
|
+
toggleOption(allEnabledOptions[activeIndex]);
|
|
504
|
+
} else if (!isOpen) {
|
|
505
|
+
isOpen = true;
|
|
506
|
+
activeIndex = 0;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
break;
|
|
510
|
+
|
|
511
|
+
case Keys.Escape:
|
|
512
|
+
if (isOpen) {
|
|
513
|
+
e.preventDefault();
|
|
514
|
+
isOpen = false;
|
|
515
|
+
activeIndex = -1;
|
|
516
|
+
searchQuery = '';
|
|
517
|
+
}
|
|
518
|
+
break;
|
|
519
|
+
|
|
520
|
+
case Keys.Backspace:
|
|
521
|
+
// Remove last chip if input is empty
|
|
522
|
+
if (searchable && searchQuery === '' && selected.length > 0) {
|
|
523
|
+
removeChip(selected[selected.length - 1]);
|
|
524
|
+
}
|
|
525
|
+
break;
|
|
526
|
+
|
|
527
|
+
case Keys.Tab:
|
|
528
|
+
// Close dropdown on tab
|
|
529
|
+
isOpen = false;
|
|
530
|
+
activeIndex = -1;
|
|
531
|
+
searchQuery = '';
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function handleChipKeyDown(e: KeyboardEvent) {
|
|
537
|
+
switch (e.key) {
|
|
538
|
+
case Keys.ArrowLeft:
|
|
539
|
+
e.preventDefault();
|
|
540
|
+
if (focusedChipIndex > 0) {
|
|
541
|
+
focusedChipIndex--;
|
|
542
|
+
}
|
|
543
|
+
break;
|
|
544
|
+
|
|
545
|
+
case Keys.ArrowRight:
|
|
546
|
+
e.preventDefault();
|
|
547
|
+
if (focusedChipIndex < selected.length - 1) {
|
|
548
|
+
focusedChipIndex++;
|
|
549
|
+
} else {
|
|
550
|
+
// Move focus back to input
|
|
551
|
+
focusedChipIndex = -1;
|
|
552
|
+
inputRef?.focus();
|
|
553
|
+
}
|
|
554
|
+
break;
|
|
555
|
+
|
|
556
|
+
case Keys.Backspace:
|
|
557
|
+
case Keys.Delete:
|
|
558
|
+
e.preventDefault();
|
|
559
|
+
const removedOption = selected[focusedChipIndex];
|
|
560
|
+
removeChip(removedOption);
|
|
561
|
+
// Adjust focus
|
|
562
|
+
if (selected.length === 0) {
|
|
563
|
+
focusedChipIndex = -1;
|
|
564
|
+
inputRef?.focus();
|
|
565
|
+
} else if (focusedChipIndex >= selected.length) {
|
|
566
|
+
focusedChipIndex = selected.length - 1;
|
|
567
|
+
}
|
|
568
|
+
break;
|
|
569
|
+
|
|
570
|
+
case Keys.Escape:
|
|
571
|
+
e.preventDefault();
|
|
572
|
+
focusedChipIndex = -1;
|
|
573
|
+
inputRef?.focus();
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function handleOptionClick(option: MultiSelectOption, e: MouseEvent) {
|
|
579
|
+
e.preventDefault();
|
|
580
|
+
e.stopPropagation();
|
|
581
|
+
toggleOption(option);
|
|
582
|
+
inputRef?.focus();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function handleOptionMouseEnter(index: number) {
|
|
586
|
+
const option = filteredFlatOptions[index];
|
|
587
|
+
if (!option?.disabled) {
|
|
588
|
+
const enabledIndex = allEnabledOptions.findIndex((o) => o.value === option.value);
|
|
589
|
+
if (enabledIndex >= 0) {
|
|
590
|
+
activeIndex = enabledIndex;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function handleToggleClick(e: MouseEvent) {
|
|
596
|
+
e.preventDefault();
|
|
597
|
+
e.stopPropagation();
|
|
598
|
+
if (!disabled && !readonly) {
|
|
599
|
+
isOpen = !isOpen;
|
|
600
|
+
if (isOpen) {
|
|
601
|
+
inputRef?.focus();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ============================================================================
|
|
607
|
+
// Effects
|
|
608
|
+
// ============================================================================
|
|
609
|
+
|
|
610
|
+
// Focus chip when focusedChipIndex changes
|
|
611
|
+
$effect(() => {
|
|
612
|
+
if (focusedChipIndex >= 0 && containerRef) {
|
|
613
|
+
const chip = containerRef.querySelector(`[data-chip-index="${focusedChipIndex}"]`) as HTMLElement;
|
|
614
|
+
chip?.focus();
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
</script>
|
|
618
|
+
|
|
619
|
+
<div
|
|
620
|
+
class={cn(
|
|
621
|
+
'multiselect-wrapper',
|
|
622
|
+
`multiselect-${size}`,
|
|
623
|
+
disabled && 'multiselect-disabled',
|
|
624
|
+
className
|
|
625
|
+
)}
|
|
626
|
+
>
|
|
627
|
+
<!-- Label -->
|
|
628
|
+
<label id={labelId} for={id} class={cn('multiselect-label', hideLabel && 'sr-only')}>
|
|
629
|
+
{label}
|
|
630
|
+
{#if required}
|
|
631
|
+
<span class="multiselect-required" aria-hidden="true">*</span>
|
|
632
|
+
{/if}
|
|
633
|
+
{#if hint && !hideLabel}
|
|
634
|
+
<span class="multiselect-hint-icon" title={hint}>
|
|
635
|
+
<Info size={14} />
|
|
636
|
+
</span>
|
|
637
|
+
{/if}
|
|
638
|
+
</label>
|
|
639
|
+
|
|
640
|
+
<!-- Main container -->
|
|
641
|
+
<div class="multiselect-container" bind:this={containerRef}>
|
|
642
|
+
<!-- Input area with chips -->
|
|
643
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
644
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
645
|
+
<div
|
|
646
|
+
class={cn(
|
|
647
|
+
'multiselect-input-area',
|
|
648
|
+
isFocused && 'multiselect-input-area-focused',
|
|
649
|
+
showError && 'multiselect-input-area-error',
|
|
650
|
+
isValid && 'multiselect-input-area-valid',
|
|
651
|
+
disabled && 'multiselect-input-area-disabled'
|
|
652
|
+
)}
|
|
653
|
+
onclick={handleContainerClick}
|
|
654
|
+
>
|
|
655
|
+
<!-- Selected chips -->
|
|
656
|
+
<div class="multiselect-chips">
|
|
657
|
+
{#each selected as option, index (option.value)}
|
|
658
|
+
<span
|
|
659
|
+
class={cn('multiselect-chip', focusedChipIndex === index && 'multiselect-chip-focused')}
|
|
660
|
+
data-chip-index={index}
|
|
661
|
+
tabindex={focusedChipIndex === index ? 0 : -1}
|
|
662
|
+
role="button"
|
|
663
|
+
aria-label={`${option.label}, press Delete to remove`}
|
|
664
|
+
>
|
|
665
|
+
{#if chipContent}
|
|
666
|
+
{@render chipContent(option)}
|
|
667
|
+
{:else}
|
|
668
|
+
<span class="multiselect-chip-label">{option.label}</span>
|
|
669
|
+
{/if}
|
|
670
|
+
{#if !disabled && !readonly}
|
|
671
|
+
<button
|
|
672
|
+
type="button"
|
|
673
|
+
class="chip-remove"
|
|
674
|
+
onclick={(e) => removeChip(option, e)}
|
|
675
|
+
aria-label={`Remove ${option.label}`}
|
|
676
|
+
tabindex={-1}
|
|
677
|
+
>
|
|
678
|
+
<X size={size === 'sm' ? 12 : 14} />
|
|
679
|
+
</button>
|
|
680
|
+
{/if}
|
|
681
|
+
</span>
|
|
682
|
+
{/each}
|
|
683
|
+
|
|
684
|
+
<!-- Input / Placeholder -->
|
|
685
|
+
{#if searchable}
|
|
686
|
+
<input
|
|
687
|
+
bind:this={inputRef}
|
|
688
|
+
{id}
|
|
689
|
+
{name}
|
|
690
|
+
type="text"
|
|
691
|
+
role="combobox"
|
|
692
|
+
value={searchQuery}
|
|
693
|
+
placeholder={selected.length === 0 ? placeholder : searchPlaceholder}
|
|
694
|
+
disabled={disabled}
|
|
695
|
+
readonly={readonly}
|
|
696
|
+
aria-expanded={isOpen}
|
|
697
|
+
aria-controls={listboxId}
|
|
698
|
+
aria-haspopup="listbox"
|
|
699
|
+
aria-activedescendant={activeDescendantId}
|
|
700
|
+
aria-describedby={ariaDescribedBy}
|
|
701
|
+
aria-labelledby={labelId}
|
|
702
|
+
aria-required={required}
|
|
703
|
+
aria-invalid={showError}
|
|
704
|
+
data-testid={testId}
|
|
705
|
+
class="multiselect-search-input"
|
|
706
|
+
oninput={handleSearchInput}
|
|
707
|
+
onfocus={handleInputFocus}
|
|
708
|
+
onblur={handleInputBlur}
|
|
709
|
+
onkeydown={handleKeyDown}
|
|
710
|
+
/>
|
|
711
|
+
{:else}
|
|
712
|
+
<button
|
|
713
|
+
bind:this={inputRef}
|
|
714
|
+
{id}
|
|
715
|
+
type="button"
|
|
716
|
+
role="combobox"
|
|
717
|
+
disabled={disabled}
|
|
718
|
+
aria-expanded={isOpen}
|
|
719
|
+
aria-controls={listboxId}
|
|
720
|
+
aria-haspopup="listbox"
|
|
721
|
+
aria-activedescendant={activeDescendantId}
|
|
722
|
+
aria-describedby={ariaDescribedBy}
|
|
723
|
+
aria-labelledby={labelId}
|
|
724
|
+
aria-required={required}
|
|
725
|
+
aria-invalid={showError}
|
|
726
|
+
data-testid={testId}
|
|
727
|
+
class="multiselect-trigger-button"
|
|
728
|
+
onfocus={handleInputFocus}
|
|
729
|
+
onblur={handleInputBlur}
|
|
730
|
+
onkeydown={handleKeyDown}
|
|
731
|
+
>
|
|
732
|
+
{#if selected.length === 0}
|
|
733
|
+
<span class="multiselect-placeholder">{placeholder}</span>
|
|
734
|
+
{/if}
|
|
735
|
+
</button>
|
|
736
|
+
{/if}
|
|
737
|
+
</div>
|
|
738
|
+
|
|
739
|
+
<!-- Right side controls -->
|
|
740
|
+
<div class="multiselect-controls">
|
|
741
|
+
{#if loading}
|
|
742
|
+
<span class="multiselect-loading" aria-hidden="true">
|
|
743
|
+
<Loader2 size={size === 'sm' ? 14 : 16} class="multiselect-spinner" />
|
|
744
|
+
</span>
|
|
745
|
+
{:else if showError}
|
|
746
|
+
<span class="multiselect-icon-error" aria-hidden="true">
|
|
747
|
+
<AlertCircle size={size === 'sm' ? 14 : 16} />
|
|
748
|
+
</span>
|
|
749
|
+
{:else if isValid}
|
|
750
|
+
<span class="multiselect-icon-success" aria-hidden="true">
|
|
751
|
+
<Check size={size === 'sm' ? 14 : 16} />
|
|
752
|
+
</span>
|
|
753
|
+
{/if}
|
|
754
|
+
|
|
755
|
+
{#if showCount && selected.length > 0}
|
|
756
|
+
<span class="multiselect-count" aria-label={`${selected.length} selected`}>
|
|
757
|
+
{selected.length}
|
|
758
|
+
</span>
|
|
759
|
+
{/if}
|
|
760
|
+
|
|
761
|
+
{#if selected.length > 0 && !disabled && !readonly}
|
|
762
|
+
<button
|
|
763
|
+
type="button"
|
|
764
|
+
class="multiselect-clear-button"
|
|
765
|
+
onclick={(e) => { e.stopPropagation(); clearAll(); }}
|
|
766
|
+
aria-label="Clear all selections"
|
|
767
|
+
tabindex={-1}
|
|
768
|
+
>
|
|
769
|
+
<X size={size === 'sm' ? 14 : 16} />
|
|
770
|
+
</button>
|
|
771
|
+
{/if}
|
|
772
|
+
|
|
773
|
+
<button
|
|
774
|
+
type="button"
|
|
775
|
+
class="multiselect-toggle-button"
|
|
776
|
+
onclick={handleToggleClick}
|
|
777
|
+
aria-label={isOpen ? 'Close options' : 'Open options'}
|
|
778
|
+
aria-expanded={isOpen}
|
|
779
|
+
tabindex={-1}
|
|
780
|
+
{disabled}
|
|
781
|
+
>
|
|
782
|
+
<ChevronDown
|
|
783
|
+
size={size === 'sm' ? 16 : 18}
|
|
784
|
+
class={cn('multiselect-chevron', isOpen && 'multiselect-chevron-open')}
|
|
785
|
+
/>
|
|
786
|
+
</button>
|
|
787
|
+
</div>
|
|
788
|
+
</div>
|
|
789
|
+
|
|
790
|
+
<!-- Dropdown listbox -->
|
|
791
|
+
{#if isOpen}
|
|
792
|
+
<div
|
|
793
|
+
bind:this={listboxRef}
|
|
794
|
+
id={listboxId}
|
|
795
|
+
role="listbox"
|
|
796
|
+
aria-label={`${label} options`}
|
|
797
|
+
aria-multiselectable="true"
|
|
798
|
+
class="multiselect-listbox"
|
|
799
|
+
>
|
|
800
|
+
<!-- Search indicator if searchable -->
|
|
801
|
+
{#if searchable && searchQuery}
|
|
802
|
+
<div class="multiselect-search-indicator">
|
|
803
|
+
<Search size={14} />
|
|
804
|
+
<span>Showing results for "{searchQuery}"</span>
|
|
805
|
+
</div>
|
|
806
|
+
{/if}
|
|
807
|
+
|
|
808
|
+
<!-- Select All option -->
|
|
809
|
+
{#if showSelectAll && !searchQuery && flatOptions.length > 0}
|
|
810
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
811
|
+
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
|
812
|
+
<div
|
|
813
|
+
role="option"
|
|
814
|
+
aria-selected={allSelected}
|
|
815
|
+
class={cn('multiselect-option', 'multiselect-option-select-all')}
|
|
816
|
+
onclick={() => selectAll()}
|
|
817
|
+
>
|
|
818
|
+
<span class="multiselect-option-checkbox">
|
|
819
|
+
{#if allSelected}
|
|
820
|
+
<Check size={14} />
|
|
821
|
+
{/if}
|
|
822
|
+
</span>
|
|
823
|
+
<span class="multiselect-option-label">{selectAllLabel}</span>
|
|
824
|
+
<span class="multiselect-option-count">({flatOptions.filter(o => !o.disabled).length})</span>
|
|
825
|
+
</div>
|
|
826
|
+
<div class="multiselect-divider"></div>
|
|
827
|
+
{/if}
|
|
828
|
+
|
|
829
|
+
{#if loading}
|
|
830
|
+
<div class="multiselect-loading-state" role="status" aria-live="polite">
|
|
831
|
+
<Loader2 size={20} class="multiselect-spinner" />
|
|
832
|
+
<span>Loading options...</span>
|
|
833
|
+
</div>
|
|
834
|
+
{:else if showEmpty}
|
|
835
|
+
<div class="multiselect-empty" role="status" aria-live="polite">
|
|
836
|
+
{#if emptyContent}
|
|
837
|
+
{@render emptyContent()}
|
|
838
|
+
{:else}
|
|
839
|
+
<span>{emptyMessage}</span>
|
|
840
|
+
{/if}
|
|
841
|
+
</div>
|
|
842
|
+
{:else if isGrouped}
|
|
843
|
+
{#each filteredOptions as group, groupIndex}
|
|
844
|
+
{@const typedGroup = group as MultiSelectOptionGroup}
|
|
845
|
+
<div class="multiselect-group" role="group" aria-labelledby={`${id}-group-${groupIndex}`}>
|
|
846
|
+
<div id={`${id}-group-${groupIndex}`} class="multiselect-group-label">
|
|
847
|
+
{typedGroup.label}
|
|
848
|
+
</div>
|
|
849
|
+
{#each typedGroup.options as option}
|
|
850
|
+
{@const optionIndex = getOptionIndex(option)}
|
|
851
|
+
{@const enabledIndex = allEnabledOptions.findIndex((o) => o.value === option.value)}
|
|
852
|
+
{@const isActive = enabledIndex === activeIndex}
|
|
853
|
+
{@const selected = isSelected(option)}
|
|
854
|
+
{@const isDisabled = option.disabled || (!selected && atMaxSelections)}
|
|
855
|
+
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
|
856
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
857
|
+
<div
|
|
858
|
+
id={`${id}-option-${optionIndex}`}
|
|
859
|
+
role="option"
|
|
860
|
+
aria-selected={selected}
|
|
861
|
+
aria-disabled={isDisabled}
|
|
862
|
+
data-index={optionIndex}
|
|
863
|
+
class={cn(
|
|
864
|
+
'multiselect-option',
|
|
865
|
+
isActive && 'multiselect-option-active',
|
|
866
|
+
selected && 'multiselect-option-selected',
|
|
867
|
+
isDisabled && 'multiselect-option-disabled'
|
|
868
|
+
)}
|
|
869
|
+
onclick={(e) => !isDisabled && handleOptionClick(option, e)}
|
|
870
|
+
onmouseenter={() => handleOptionMouseEnter(optionIndex)}
|
|
871
|
+
>
|
|
872
|
+
<span class="multiselect-option-checkbox">
|
|
873
|
+
{#if selected}
|
|
874
|
+
<Check size={14} />
|
|
875
|
+
{/if}
|
|
876
|
+
</span>
|
|
877
|
+
{#if optionContent}
|
|
878
|
+
{@render optionContent(option)}
|
|
879
|
+
{:else}
|
|
880
|
+
<div class="multiselect-option-content">
|
|
881
|
+
<span class="multiselect-option-label">{option.label}</span>
|
|
882
|
+
{#if option.description}
|
|
883
|
+
<span class="multiselect-option-description">{option.description}</span>
|
|
884
|
+
{/if}
|
|
885
|
+
</div>
|
|
886
|
+
{/if}
|
|
887
|
+
</div>
|
|
888
|
+
{/each}
|
|
889
|
+
</div>
|
|
890
|
+
{/each}
|
|
891
|
+
{:else}
|
|
892
|
+
{#each filteredFlatOptions as option, index}
|
|
893
|
+
{@const enabledIndex = allEnabledOptions.findIndex((o) => o.value === option.value)}
|
|
894
|
+
{@const isActive = enabledIndex === activeIndex}
|
|
895
|
+
{@const selected = isSelected(option)}
|
|
896
|
+
{@const isDisabled = option.disabled || (!selected && atMaxSelections)}
|
|
897
|
+
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
|
898
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
899
|
+
<div
|
|
900
|
+
id={`${id}-option-${index}`}
|
|
901
|
+
role="option"
|
|
902
|
+
aria-selected={selected}
|
|
903
|
+
aria-disabled={isDisabled}
|
|
904
|
+
data-index={index}
|
|
905
|
+
class={cn(
|
|
906
|
+
'multiselect-option',
|
|
907
|
+
isActive && 'multiselect-option-active',
|
|
908
|
+
selected && 'multiselect-option-selected',
|
|
909
|
+
isDisabled && 'multiselect-option-disabled'
|
|
910
|
+
)}
|
|
911
|
+
onclick={(e) => !isDisabled && handleOptionClick(option, e)}
|
|
912
|
+
onmouseenter={() => handleOptionMouseEnter(index)}
|
|
913
|
+
>
|
|
914
|
+
<span class="multiselect-option-checkbox">
|
|
915
|
+
{#if selected}
|
|
916
|
+
<Check size={14} />
|
|
917
|
+
{/if}
|
|
918
|
+
</span>
|
|
919
|
+
{#if optionContent}
|
|
920
|
+
{@render optionContent(option)}
|
|
921
|
+
{:else}
|
|
922
|
+
<div class="multiselect-option-content">
|
|
923
|
+
<span class="multiselect-option-label">{option.label}</span>
|
|
924
|
+
{#if option.description}
|
|
925
|
+
<span class="multiselect-option-description">{option.description}</span>
|
|
926
|
+
{/if}
|
|
927
|
+
</div>
|
|
928
|
+
{/if}
|
|
929
|
+
</div>
|
|
930
|
+
{/each}
|
|
931
|
+
{/if}
|
|
932
|
+
|
|
933
|
+
{#if maxSelections !== undefined && !showEmpty}
|
|
934
|
+
<div class="multiselect-max-info">
|
|
935
|
+
{selected.length} / {maxSelections} selected
|
|
936
|
+
</div>
|
|
937
|
+
{/if}
|
|
938
|
+
</div>
|
|
939
|
+
{/if}
|
|
940
|
+
</div>
|
|
941
|
+
|
|
942
|
+
<!-- Screen reader announcements -->
|
|
943
|
+
{#if isOpen && !loading && filteredFlatOptions.length > 0}
|
|
944
|
+
<div class="sr-only" role="status" aria-live="polite" aria-atomic="true">
|
|
945
|
+
{filteredFlatOptions.length} option{filteredFlatOptions.length === 1 ? '' : 's'} available,
|
|
946
|
+
{selected.length} selected
|
|
947
|
+
</div>
|
|
948
|
+
{/if}
|
|
949
|
+
|
|
950
|
+
<!-- Footer messages -->
|
|
951
|
+
<div class="multiselect-footer">
|
|
952
|
+
{#if showError}
|
|
953
|
+
<p id={errorId} class="multiselect-message multiselect-message-error" role="alert" aria-live="assertive">
|
|
954
|
+
{error}
|
|
955
|
+
</p>
|
|
956
|
+
{:else if showSuccess}
|
|
957
|
+
<p class="multiselect-message multiselect-message-success">
|
|
958
|
+
{successMessage}
|
|
959
|
+
</p>
|
|
960
|
+
{:else if hint && isFocused}
|
|
961
|
+
<p id={hintId} class="multiselect-message multiselect-message-hint">
|
|
962
|
+
{hint}
|
|
963
|
+
</p>
|
|
964
|
+
{/if}
|
|
965
|
+
</div>
|
|
966
|
+
</div>
|
|
967
|
+
|
|
968
|
+
<style>
|
|
969
|
+
/* ========================================
|
|
970
|
+
BASE WRAPPER
|
|
971
|
+
======================================== */
|
|
972
|
+
.multiselect-wrapper {
|
|
973
|
+
display: flex;
|
|
974
|
+
flex-direction: column;
|
|
975
|
+
gap: 0.375rem;
|
|
976
|
+
position: relative;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
.multiselect-disabled {
|
|
980
|
+
opacity: 0.6;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/* ========================================
|
|
984
|
+
LABEL
|
|
985
|
+
======================================== */
|
|
986
|
+
.multiselect-label {
|
|
987
|
+
display: flex;
|
|
988
|
+
align-items: center;
|
|
989
|
+
gap: 0.375rem;
|
|
990
|
+
font-weight: 500;
|
|
991
|
+
color: var(--ui-text-primary);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
.multiselect-sm .multiselect-label {
|
|
995
|
+
font-size: 0.75rem;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
.multiselect-md .multiselect-label,
|
|
999
|
+
.multiselect-lg .multiselect-label {
|
|
1000
|
+
font-size: 0.875rem;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
.multiselect-required {
|
|
1004
|
+
color: rgb(var(--ui-color-error));
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
:global([data-theme='dark']) .multiselect-required {
|
|
1008
|
+
color: rgb(var(--ui-color-error-light));
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
@media (prefers-color-scheme: dark) {
|
|
1012
|
+
:global(:root:not([data-theme='light'])) .multiselect-required {
|
|
1013
|
+
color: rgb(var(--ui-color-error-light));
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
.multiselect-hint-icon {
|
|
1018
|
+
color: var(--ui-text-tertiary);
|
|
1019
|
+
cursor: help;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/* ========================================
|
|
1023
|
+
CONTAINER
|
|
1024
|
+
======================================== */
|
|
1025
|
+
.multiselect-container {
|
|
1026
|
+
position: relative;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/* ========================================
|
|
1030
|
+
INPUT AREA
|
|
1031
|
+
======================================== */
|
|
1032
|
+
.multiselect-input-area {
|
|
1033
|
+
display: flex;
|
|
1034
|
+
align-items: center;
|
|
1035
|
+
gap: 0.5rem;
|
|
1036
|
+
min-height: 2.5rem;
|
|
1037
|
+
padding: 0.375rem 0.5rem;
|
|
1038
|
+
border: 2px solid var(--ui-border-default);
|
|
1039
|
+
border-radius: 0.5rem;
|
|
1040
|
+
background: var(--ui-bg-secondary);
|
|
1041
|
+
cursor: pointer;
|
|
1042
|
+
transition: all 0.2s ease;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
.multiselect-sm .multiselect-input-area {
|
|
1046
|
+
min-height: 2rem;
|
|
1047
|
+
padding: 0.25rem 0.375rem;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
.multiselect-lg .multiselect-input-area {
|
|
1051
|
+
min-height: 3rem;
|
|
1052
|
+
padding: 0.5rem 0.625rem;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
.multiselect-input-area-focused {
|
|
1056
|
+
outline: none;
|
|
1057
|
+
background: var(--ui-bg-primary);
|
|
1058
|
+
border-color: rgb(var(--ui-color-primary));
|
|
1059
|
+
box-shadow: 0 0 0 3px rgb(var(--ui-color-primary) / 0.4);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
:global([data-theme='dark']) .multiselect-input-area-focused {
|
|
1063
|
+
border-color: rgb(var(--ui-color-primary-light));
|
|
1064
|
+
box-shadow: 0 0 0 3px rgb(var(--ui-color-primary-light) / 0.5);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
@media (prefers-color-scheme: dark) {
|
|
1068
|
+
:global(:root:not([data-theme='light'])) .multiselect-input-area-focused {
|
|
1069
|
+
border-color: rgb(var(--ui-color-primary-light));
|
|
1070
|
+
box-shadow: 0 0 0 3px rgb(var(--ui-color-primary-light) / 0.5);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
.multiselect-input-area-disabled {
|
|
1075
|
+
cursor: not-allowed;
|
|
1076
|
+
background: var(--ui-bg-tertiary);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
.multiselect-input-area-error {
|
|
1080
|
+
border-color: rgb(var(--ui-color-error));
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
:global([data-theme='dark']) .multiselect-input-area-error {
|
|
1084
|
+
border-color: rgb(var(--ui-color-error-light));
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
@media (prefers-color-scheme: dark) {
|
|
1088
|
+
:global(:root:not([data-theme='light'])) .multiselect-input-area-error {
|
|
1089
|
+
border-color: rgb(var(--ui-color-error-light));
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
.multiselect-input-area-valid {
|
|
1094
|
+
border-color: rgb(var(--ui-color-success));
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
:global([data-theme='dark']) .multiselect-input-area-valid {
|
|
1098
|
+
border-color: rgb(var(--ui-color-success-light));
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
@media (prefers-color-scheme: dark) {
|
|
1102
|
+
:global(:root:not([data-theme='light'])) .multiselect-input-area-valid {
|
|
1103
|
+
border-color: rgb(var(--ui-color-success-light));
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/* ========================================
|
|
1108
|
+
CHIPS
|
|
1109
|
+
======================================== */
|
|
1110
|
+
.multiselect-chips {
|
|
1111
|
+
display: flex;
|
|
1112
|
+
flex-wrap: wrap;
|
|
1113
|
+
gap: 0.375rem;
|
|
1114
|
+
flex: 1;
|
|
1115
|
+
min-width: 0;
|
|
1116
|
+
align-items: center;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
.multiselect-chip {
|
|
1120
|
+
display: inline-flex;
|
|
1121
|
+
align-items: center;
|
|
1122
|
+
gap: 0.25rem;
|
|
1123
|
+
padding: 0.125rem 0.375rem 0.125rem 0.5rem;
|
|
1124
|
+
background: rgb(var(--ui-color-primary) / 0.1);
|
|
1125
|
+
color: rgb(var(--ui-color-primary));
|
|
1126
|
+
border-radius: 9999px;
|
|
1127
|
+
font-size: 0.8125rem;
|
|
1128
|
+
font-weight: 500;
|
|
1129
|
+
max-width: 200px;
|
|
1130
|
+
transition: all 0.15s ease;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
:global([data-theme='dark']) .multiselect-chip {
|
|
1134
|
+
background: rgb(var(--ui-color-primary-light) / 0.15);
|
|
1135
|
+
color: rgb(var(--ui-color-primary-light));
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
@media (prefers-color-scheme: dark) {
|
|
1139
|
+
:global(:root:not([data-theme='light'])) .multiselect-chip {
|
|
1140
|
+
background: rgb(var(--ui-color-primary-light) / 0.15);
|
|
1141
|
+
color: rgb(var(--ui-color-primary-light));
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
.multiselect-sm .multiselect-chip {
|
|
1146
|
+
font-size: 0.75rem;
|
|
1147
|
+
padding: 0.0625rem 0.25rem 0.0625rem 0.375rem;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
.multiselect-chip-focused {
|
|
1151
|
+
outline: 2px solid rgb(var(--ui-color-primary));
|
|
1152
|
+
outline-offset: 1px;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
:global([data-theme='dark']) .multiselect-chip-focused {
|
|
1156
|
+
outline-color: rgb(var(--ui-color-primary-light));
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
@media (prefers-color-scheme: dark) {
|
|
1160
|
+
:global(:root:not([data-theme='light'])) .multiselect-chip-focused {
|
|
1161
|
+
outline-color: rgb(var(--ui-color-primary-light));
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
.multiselect-chip-label {
|
|
1166
|
+
overflow: hidden;
|
|
1167
|
+
text-overflow: ellipsis;
|
|
1168
|
+
white-space: nowrap;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
.chip-remove {
|
|
1172
|
+
display: flex;
|
|
1173
|
+
align-items: center;
|
|
1174
|
+
justify-content: center;
|
|
1175
|
+
padding: 0.125rem;
|
|
1176
|
+
background: transparent;
|
|
1177
|
+
border: none;
|
|
1178
|
+
color: inherit;
|
|
1179
|
+
cursor: pointer;
|
|
1180
|
+
border-radius: 50%;
|
|
1181
|
+
opacity: 0.7;
|
|
1182
|
+
transition: all 0.15s ease;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
.chip-remove:hover {
|
|
1186
|
+
opacity: 1;
|
|
1187
|
+
background: rgb(var(--ui-color-primary) / 0.2);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
:global([data-theme='dark']) .chip-remove:hover {
|
|
1191
|
+
background: rgb(var(--ui-color-primary-light) / 0.25);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
@media (prefers-color-scheme: dark) {
|
|
1195
|
+
:global(:root:not([data-theme='light'])) .chip-remove:hover {
|
|
1196
|
+
background: rgb(var(--ui-color-primary-light) / 0.25);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/* ========================================
|
|
1201
|
+
SEARCH INPUT
|
|
1202
|
+
======================================== */
|
|
1203
|
+
.multiselect-search-input {
|
|
1204
|
+
flex: 1;
|
|
1205
|
+
min-width: 60px;
|
|
1206
|
+
border: none;
|
|
1207
|
+
background: transparent;
|
|
1208
|
+
color: var(--ui-text-primary);
|
|
1209
|
+
font-size: 0.875rem;
|
|
1210
|
+
outline: none;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
.multiselect-search-input::placeholder {
|
|
1214
|
+
color: var(--ui-text-tertiary);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
.multiselect-sm .multiselect-search-input {
|
|
1218
|
+
font-size: 0.8125rem;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
.multiselect-lg .multiselect-search-input {
|
|
1222
|
+
font-size: 1rem;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
.multiselect-trigger-button {
|
|
1226
|
+
flex: 1;
|
|
1227
|
+
min-width: 0;
|
|
1228
|
+
border: none;
|
|
1229
|
+
background: transparent;
|
|
1230
|
+
color: var(--ui-text-primary);
|
|
1231
|
+
font-size: 0.875rem;
|
|
1232
|
+
text-align: left;
|
|
1233
|
+
cursor: pointer;
|
|
1234
|
+
outline: none;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
.multiselect-placeholder {
|
|
1238
|
+
color: var(--ui-text-tertiary);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/* ========================================
|
|
1242
|
+
CONTROLS
|
|
1243
|
+
======================================== */
|
|
1244
|
+
.multiselect-controls {
|
|
1245
|
+
display: flex;
|
|
1246
|
+
align-items: center;
|
|
1247
|
+
gap: 0.25rem;
|
|
1248
|
+
flex-shrink: 0;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
.multiselect-loading,
|
|
1252
|
+
.multiselect-icon-error,
|
|
1253
|
+
.multiselect-icon-success {
|
|
1254
|
+
display: flex;
|
|
1255
|
+
align-items: center;
|
|
1256
|
+
justify-content: center;
|
|
1257
|
+
color: var(--ui-text-tertiary);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
.multiselect-icon-error {
|
|
1261
|
+
color: rgb(var(--ui-color-error));
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
:global([data-theme='dark']) .multiselect-icon-error {
|
|
1265
|
+
color: rgb(var(--ui-color-error-light));
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
@media (prefers-color-scheme: dark) {
|
|
1269
|
+
:global(:root:not([data-theme='light'])) .multiselect-icon-error {
|
|
1270
|
+
color: rgb(var(--ui-color-error-light));
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
.multiselect-icon-success {
|
|
1275
|
+
color: rgb(var(--ui-color-success));
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
:global([data-theme='dark']) .multiselect-icon-success {
|
|
1279
|
+
color: rgb(var(--ui-color-success-light));
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
@media (prefers-color-scheme: dark) {
|
|
1283
|
+
:global(:root:not([data-theme='light'])) .multiselect-icon-success {
|
|
1284
|
+
color: rgb(var(--ui-color-success-light));
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
.multiselect-spinner {
|
|
1289
|
+
animation: spin 1s linear infinite;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
@keyframes spin {
|
|
1293
|
+
from { transform: rotate(0deg); }
|
|
1294
|
+
to { transform: rotate(360deg); }
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1298
|
+
.multiselect-spinner {
|
|
1299
|
+
animation: none;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
.multiselect-count {
|
|
1304
|
+
display: flex;
|
|
1305
|
+
align-items: center;
|
|
1306
|
+
justify-content: center;
|
|
1307
|
+
min-width: 1.25rem;
|
|
1308
|
+
height: 1.25rem;
|
|
1309
|
+
padding: 0 0.375rem;
|
|
1310
|
+
background: rgb(var(--ui-color-primary));
|
|
1311
|
+
color: white;
|
|
1312
|
+
font-size: 0.6875rem;
|
|
1313
|
+
font-weight: 600;
|
|
1314
|
+
border-radius: 9999px;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
:global([data-theme='dark']) .multiselect-count {
|
|
1318
|
+
background: rgb(var(--ui-color-primary-light));
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
@media (prefers-color-scheme: dark) {
|
|
1322
|
+
:global(:root:not([data-theme='light'])) .multiselect-count {
|
|
1323
|
+
background: rgb(var(--ui-color-primary-light));
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
.multiselect-clear-button,
|
|
1328
|
+
.multiselect-toggle-button {
|
|
1329
|
+
display: flex;
|
|
1330
|
+
align-items: center;
|
|
1331
|
+
justify-content: center;
|
|
1332
|
+
padding: 0.25rem;
|
|
1333
|
+
border: none;
|
|
1334
|
+
background: transparent;
|
|
1335
|
+
color: var(--ui-text-tertiary);
|
|
1336
|
+
cursor: pointer;
|
|
1337
|
+
border-radius: 0.25rem;
|
|
1338
|
+
transition: all 0.15s ease;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
.multiselect-clear-button:hover,
|
|
1342
|
+
.multiselect-toggle-button:hover {
|
|
1343
|
+
color: var(--ui-text-primary);
|
|
1344
|
+
background: var(--ui-bg-tertiary);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
.multiselect-toggle-button:disabled {
|
|
1348
|
+
cursor: not-allowed;
|
|
1349
|
+
opacity: 0.5;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
.multiselect-chevron {
|
|
1353
|
+
transition: transform 0.2s ease;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
.multiselect-chevron-open {
|
|
1357
|
+
transform: rotate(180deg);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/* ========================================
|
|
1361
|
+
LISTBOX DROPDOWN
|
|
1362
|
+
======================================== */
|
|
1363
|
+
.multiselect-listbox {
|
|
1364
|
+
position: absolute;
|
|
1365
|
+
top: 100%;
|
|
1366
|
+
left: 0;
|
|
1367
|
+
right: 0;
|
|
1368
|
+
z-index: 50;
|
|
1369
|
+
margin-top: 0.25rem;
|
|
1370
|
+
max-height: 18rem;
|
|
1371
|
+
overflow-y: auto;
|
|
1372
|
+
background: var(--ui-bg-primary);
|
|
1373
|
+
border: 2px solid var(--ui-border-default);
|
|
1374
|
+
border-radius: 0.5rem;
|
|
1375
|
+
box-shadow: var(--ui-shadow-lg);
|
|
1376
|
+
animation: slideDown 0.15s ease-out;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
@keyframes slideDown {
|
|
1380
|
+
from {
|
|
1381
|
+
opacity: 0;
|
|
1382
|
+
transform: translateY(-4px);
|
|
1383
|
+
}
|
|
1384
|
+
to {
|
|
1385
|
+
opacity: 1;
|
|
1386
|
+
transform: translateY(0);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1391
|
+
.multiselect-listbox {
|
|
1392
|
+
animation: none;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/* ========================================
|
|
1397
|
+
SEARCH INDICATOR
|
|
1398
|
+
======================================== */
|
|
1399
|
+
.multiselect-search-indicator {
|
|
1400
|
+
display: flex;
|
|
1401
|
+
align-items: center;
|
|
1402
|
+
gap: 0.5rem;
|
|
1403
|
+
padding: 0.5rem 0.75rem;
|
|
1404
|
+
font-size: 0.75rem;
|
|
1405
|
+
color: var(--ui-text-tertiary);
|
|
1406
|
+
background: var(--ui-bg-secondary);
|
|
1407
|
+
border-bottom: 1px solid var(--ui-border-default);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/* ========================================
|
|
1411
|
+
OPTIONS
|
|
1412
|
+
======================================== */
|
|
1413
|
+
.multiselect-option {
|
|
1414
|
+
display: flex;
|
|
1415
|
+
align-items: flex-start;
|
|
1416
|
+
gap: 0.5rem;
|
|
1417
|
+
padding: 0.625rem 0.75rem;
|
|
1418
|
+
cursor: pointer;
|
|
1419
|
+
transition: background-color 0.1s ease;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
.multiselect-option:hover,
|
|
1423
|
+
.multiselect-option-active {
|
|
1424
|
+
background: var(--ui-bg-secondary);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
.multiselect-option-selected {
|
|
1428
|
+
background: rgb(var(--ui-color-primary) / 0.08);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
:global([data-theme='dark']) .multiselect-option-selected {
|
|
1432
|
+
background: rgb(var(--ui-color-primary-light) / 0.12);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
@media (prefers-color-scheme: dark) {
|
|
1436
|
+
:global(:root:not([data-theme='light'])) .multiselect-option-selected {
|
|
1437
|
+
background: rgb(var(--ui-color-primary-light) / 0.12);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
.multiselect-option-active.multiselect-option-selected {
|
|
1442
|
+
background: rgb(var(--ui-color-primary) / 0.12);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
:global([data-theme='dark']) .multiselect-option-active.multiselect-option-selected {
|
|
1446
|
+
background: rgb(var(--ui-color-primary-light) / 0.18);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
@media (prefers-color-scheme: dark) {
|
|
1450
|
+
:global(:root:not([data-theme='light'])) .multiselect-option-active.multiselect-option-selected {
|
|
1451
|
+
background: rgb(var(--ui-color-primary-light) / 0.18);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
.multiselect-option-disabled {
|
|
1456
|
+
opacity: 0.5;
|
|
1457
|
+
cursor: not-allowed;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
.multiselect-option-disabled:hover {
|
|
1461
|
+
background: transparent;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
.multiselect-option-select-all {
|
|
1465
|
+
font-weight: 500;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
.multiselect-option-checkbox {
|
|
1469
|
+
display: flex;
|
|
1470
|
+
align-items: center;
|
|
1471
|
+
justify-content: center;
|
|
1472
|
+
width: 1.125rem;
|
|
1473
|
+
height: 1.125rem;
|
|
1474
|
+
border: 2px solid var(--ui-border-default);
|
|
1475
|
+
border-radius: 0.25rem;
|
|
1476
|
+
flex-shrink: 0;
|
|
1477
|
+
background: var(--ui-bg-primary);
|
|
1478
|
+
color: white;
|
|
1479
|
+
transition: all 0.15s ease;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
.multiselect-option-selected .multiselect-option-checkbox {
|
|
1483
|
+
background: rgb(var(--ui-color-primary));
|
|
1484
|
+
border-color: rgb(var(--ui-color-primary));
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
:global([data-theme='dark']) .multiselect-option-selected .multiselect-option-checkbox {
|
|
1488
|
+
background: rgb(var(--ui-color-primary-light));
|
|
1489
|
+
border-color: rgb(var(--ui-color-primary-light));
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
@media (prefers-color-scheme: dark) {
|
|
1493
|
+
:global(:root:not([data-theme='light'])) .multiselect-option-selected .multiselect-option-checkbox {
|
|
1494
|
+
background: rgb(var(--ui-color-primary-light));
|
|
1495
|
+
border-color: rgb(var(--ui-color-primary-light));
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
.multiselect-option-content {
|
|
1500
|
+
display: flex;
|
|
1501
|
+
flex-direction: column;
|
|
1502
|
+
gap: 0.125rem;
|
|
1503
|
+
min-width: 0;
|
|
1504
|
+
flex: 1;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
.multiselect-option-label {
|
|
1508
|
+
font-weight: 500;
|
|
1509
|
+
color: var(--ui-text-primary);
|
|
1510
|
+
white-space: nowrap;
|
|
1511
|
+
overflow: hidden;
|
|
1512
|
+
text-overflow: ellipsis;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
.multiselect-option-description {
|
|
1516
|
+
font-size: 0.75rem;
|
|
1517
|
+
color: var(--ui-text-tertiary);
|
|
1518
|
+
white-space: nowrap;
|
|
1519
|
+
overflow: hidden;
|
|
1520
|
+
text-overflow: ellipsis;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
.multiselect-option-count {
|
|
1524
|
+
font-size: 0.75rem;
|
|
1525
|
+
color: var(--ui-text-tertiary);
|
|
1526
|
+
margin-left: auto;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/* ========================================
|
|
1530
|
+
GROUPS
|
|
1531
|
+
======================================== */
|
|
1532
|
+
.multiselect-group {
|
|
1533
|
+
padding-top: 0.25rem;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
.multiselect-group:not(:first-child) {
|
|
1537
|
+
border-top: 1px solid var(--ui-border-default);
|
|
1538
|
+
margin-top: 0.25rem;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
.multiselect-group-label {
|
|
1542
|
+
padding: 0.5rem 0.75rem 0.25rem;
|
|
1543
|
+
font-size: 0.6875rem;
|
|
1544
|
+
font-weight: 600;
|
|
1545
|
+
text-transform: uppercase;
|
|
1546
|
+
letter-spacing: 0.05em;
|
|
1547
|
+
color: var(--ui-text-tertiary);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/* ========================================
|
|
1551
|
+
DIVIDER
|
|
1552
|
+
======================================== */
|
|
1553
|
+
.multiselect-divider {
|
|
1554
|
+
height: 1px;
|
|
1555
|
+
background: var(--ui-border-default);
|
|
1556
|
+
margin: 0.25rem 0;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
/* ========================================
|
|
1560
|
+
STATES
|
|
1561
|
+
======================================== */
|
|
1562
|
+
.multiselect-loading-state,
|
|
1563
|
+
.multiselect-empty {
|
|
1564
|
+
display: flex;
|
|
1565
|
+
align-items: center;
|
|
1566
|
+
justify-content: center;
|
|
1567
|
+
gap: 0.5rem;
|
|
1568
|
+
padding: 1.5rem;
|
|
1569
|
+
color: var(--ui-text-tertiary);
|
|
1570
|
+
font-size: 0.875rem;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
.multiselect-max-info {
|
|
1574
|
+
padding: 0.5rem 0.75rem;
|
|
1575
|
+
font-size: 0.75rem;
|
|
1576
|
+
color: var(--ui-text-tertiary);
|
|
1577
|
+
text-align: center;
|
|
1578
|
+
background: var(--ui-bg-secondary);
|
|
1579
|
+
border-top: 1px solid var(--ui-border-default);
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
/* ========================================
|
|
1583
|
+
FOOTER & MESSAGES
|
|
1584
|
+
======================================== */
|
|
1585
|
+
.multiselect-footer {
|
|
1586
|
+
min-height: 1.25rem;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
.multiselect-message {
|
|
1590
|
+
font-size: 0.75rem;
|
|
1591
|
+
animation: slideIn 0.2s ease-out;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
@keyframes slideIn {
|
|
1595
|
+
from {
|
|
1596
|
+
opacity: 0;
|
|
1597
|
+
transform: translateY(-4px);
|
|
1598
|
+
}
|
|
1599
|
+
to {
|
|
1600
|
+
opacity: 1;
|
|
1601
|
+
transform: translateY(0);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
.multiselect-message-error {
|
|
1606
|
+
color: rgb(var(--ui-color-error));
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
:global([data-theme='dark']) .multiselect-message-error {
|
|
1610
|
+
color: rgb(var(--ui-color-error-light));
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
@media (prefers-color-scheme: dark) {
|
|
1614
|
+
:global(:root:not([data-theme='light'])) .multiselect-message-error {
|
|
1615
|
+
color: rgb(var(--ui-color-error-light));
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
.multiselect-message-success {
|
|
1620
|
+
color: rgb(var(--ui-color-success));
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
:global([data-theme='dark']) .multiselect-message-success {
|
|
1624
|
+
color: rgb(var(--ui-color-success-light));
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
@media (prefers-color-scheme: dark) {
|
|
1628
|
+
:global(:root:not([data-theme='light'])) .multiselect-message-success {
|
|
1629
|
+
color: rgb(var(--ui-color-success-light));
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
.multiselect-message-hint {
|
|
1634
|
+
color: var(--ui-text-tertiary);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
/* ========================================
|
|
1638
|
+
UTILITIES
|
|
1639
|
+
======================================== */
|
|
1640
|
+
.sr-only {
|
|
1641
|
+
position: absolute;
|
|
1642
|
+
width: 1px;
|
|
1643
|
+
height: 1px;
|
|
1644
|
+
padding: 0;
|
|
1645
|
+
margin: -1px;
|
|
1646
|
+
overflow: hidden;
|
|
1647
|
+
clip: rect(0, 0, 0, 0);
|
|
1648
|
+
white-space: nowrap;
|
|
1649
|
+
border: 0;
|
|
1650
|
+
}
|
|
1651
|
+
</style>
|