@classic-homes/theme-svelte 1.22.0 → 1.22.2
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/dist/lib/components/Alert.svelte +8 -10
- package/dist/lib/components/Combobox.svelte +2 -2
- package/dist/lib/components/DataPanel.svelte +19 -5
- package/dist/lib/components/DataTable.svelte +15 -6
- package/dist/lib/components/DataTable.svelte.d.ts +1 -1
- package/dist/lib/components/DateTimePicker.svelte +31 -34
- package/dist/lib/components/DropdownMenu.svelte +52 -30
- package/dist/lib/components/DropdownMenu.svelte.d.ts +5 -0
- package/dist/lib/components/FileUpload.svelte +5 -19
- package/dist/lib/components/HeaderSearch.svelte +18 -10
- package/dist/lib/components/MultiSelect.svelte +2 -2
- package/dist/lib/components/NumberInput.svelte +15 -2
- package/dist/lib/components/Select.svelte +19 -5
- package/dist/lib/components/Select.svelte.d.ts +4 -0
- package/dist/lib/components/Signature.svelte +15 -4
- package/dist/lib/components/Signature.svelte.d.ts +1 -1
- package/dist/lib/components/Skeleton.svelte.d.ts +1 -1
- package/dist/lib/components/Slider.svelte +8 -6
- package/dist/lib/components/Tabs.svelte +10 -12
- package/dist/lib/components/Toast.svelte +12 -12
- package/dist/lib/components/data-table/DataTableColumnVisibility.svelte +24 -26
- package/dist/lib/components/data-table/DataTableEditableCell.svelte +31 -18
- package/dist/lib/components/data-table/DataTableEditableCell.svelte.d.ts +0 -7
- package/dist/lib/components/data-table/DataTablePagination.svelte +5 -1
- package/dist/lib/components/data-table/DataTableVirtual.svelte +4 -7
- package/dist/lib/components/data-table/DataTableVirtual.svelte.d.ts +3 -0
- package/dist/lib/components/data-table/createDataTable.svelte.d.ts +6 -1
- package/dist/lib/components/data-table/createDataTable.svelte.js +22 -9
- package/dist/lib/components/layout/AppShell.svelte +1 -1
- package/dist/lib/components/layout/DashboardLayout.svelte +15 -6
- package/dist/lib/components/layout/Footer.svelte +6 -6
- package/dist/lib/components/layout/FormPageLayout.svelte +4 -3
- package/dist/lib/components/layout/Sidebar.svelte +53 -21
- package/dist/lib/components/media/Image.svelte.d.ts +1 -1
- package/dist/lib/components/media/Lightbox.svelte +13 -9
- package/dist/lib/components/media/MediaCarousel.svelte +5 -0
- package/dist/lib/components/media/MediaGrid.svelte +4 -1
- package/dist/lib/components/notifications/NotificationDialog.svelte +8 -5
- package/dist/lib/components/notifications/NotificationItem.svelte +16 -6
- package/dist/lib/components/studio/InsertPalette.svelte +4 -1
- package/dist/lib/components/studio/StudioCanvas.svelte +23 -2
- package/dist/lib/components/studio/StudioCanvas.svelte.d.ts +5 -1
- package/dist/lib/composables/useAsync.svelte.d.ts +1 -1
- package/dist/lib/composables/useAsync.svelte.js +33 -14
- package/dist/lib/composables/useFocusTrap.svelte.js +9 -2
- package/dist/lib/composables/useForm.svelte.js +19 -7
- package/dist/lib/composables/usePersistedForm.svelte.js +37 -10
- package/dist/lib/notices/sanitize.js +10 -3
- package/dist/lib/notices/store.d.ts +3 -3
- package/dist/lib/notices/store.js +14 -2
- package/dist/lib/notices/types.d.ts +8 -8
- package/dist/lib/notices/types.js +5 -1
- package/dist/lib/stores/sidebar.svelte.d.ts +12 -0
- package/dist/lib/stores/sidebar.svelte.js +76 -26
- package/dist/lib/stores/toast.svelte.js +12 -1
- package/dist/lib/utils/date.d.ts +3 -1
- package/dist/lib/utils/date.js +54 -34
- package/dist/lib/utils/form.d.ts +2 -1
- package/dist/lib/utils/form.js +3 -2
- package/dist/lib/utils/media.js +4 -3
- package/dist/lib/utils/url.d.ts +10 -0
- package/dist/lib/utils/url.js +21 -0
- package/dist/lib/utils.js +14 -1
- package/package.json +5 -4
- package/dist/__tests__/FileUpload.test.js +0 -45
- package/dist/__tests__/a11y/Alert.a11y.test.js +0 -84
- package/dist/__tests__/a11y/AlertDialog.a11y.test.js +0 -126
- package/dist/__tests__/a11y/Button.a11y.test.js +0 -66
- package/dist/__tests__/a11y/Card.a11y.test.js +0 -77
- package/dist/__tests__/a11y/Combobox.a11y.test.js +0 -209
- package/dist/__tests__/a11y/Dialog.a11y.test.js +0 -103
- package/dist/__tests__/a11y/DropdownMenu.a11y.test.js +0 -132
- package/dist/__tests__/a11y/FileUpload.a11y.test.js +0 -158
- package/dist/__tests__/a11y/FilterToolbar.a11y.test.js +0 -62
- package/dist/__tests__/a11y/Icon.a11y.test.js +0 -119
- package/dist/__tests__/a11y/Input.a11y.test.js +0 -96
- package/dist/__tests__/a11y/LiveRegion.a11y.test.js +0 -102
- package/dist/__tests__/a11y/Media.a11y.test.js +0 -145
- package/dist/__tests__/a11y/PublicLayout.a11y.test.js +0 -139
- package/dist/__tests__/a11y/SegmentedControl.a11y.test.js +0 -50
- package/dist/__tests__/a11y/Switch.a11y.test.js +0 -95
- package/dist/__tests__/a11y/Tabs.a11y.test.js +0 -116
- package/dist/__tests__/a11y/Textarea.a11y.test.js +0 -111
- package/dist/__tests__/a11y/Toolbar.a11y.test.js +0 -31
- package/dist/__tests__/a11y/ViewToggle.a11y.test.js +0 -40
- package/dist/__tests__/action-header.test.js +0 -150
- package/dist/__tests__/alert-dialog-dismiss.test.js +0 -54
- package/dist/__tests__/code-field.test.js +0 -65
- package/dist/__tests__/control-text-color.test.js +0 -29
- package/dist/__tests__/data-panel-card-gestures.test.js +0 -138
- package/dist/__tests__/data-panel-viewport.test.js +0 -45
- package/dist/__tests__/dropdown-trigger.test.js +0 -39
- package/dist/__tests__/header-search.test.js +0 -90
- package/dist/__tests__/media-components.test.js +0 -206
- package/dist/__tests__/media-hls.test.js +0 -273
- package/dist/__tests__/media-matterport.test.js +0 -296
- package/dist/__tests__/media-players.test.js +0 -244
- package/dist/__tests__/media-scrubber.test.js +0 -44
- package/dist/__tests__/media-utils.test.js +0 -427
- package/dist/__tests__/nav-access.test.js +0 -145
- package/dist/__tests__/page-header.test.js +0 -36
- package/dist/__tests__/public-layout-drawer.test.js +0 -116
- package/dist/__tests__/setup.js +0 -56
- package/dist/__tests__/sidebar-collapse.test.js +0 -58
- package/dist/__tests__/sidebar-docs-parity.test.js +0 -134
- package/dist/__tests__/sidebar-expansion.test.js +0 -59
- package/dist/__tests__/sidebar-items.test.js +0 -59
- package/dist/__tests__/sidebar-mobile.test.js +0 -86
- package/dist/__tests__/studio-canvas-item.svelte.test.js +0 -98
- package/dist/__tests__/studio.test.js +0 -240
|
@@ -7,17 +7,15 @@
|
|
|
7
7
|
base: 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
|
|
8
8
|
variants: {
|
|
9
9
|
variant: {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// Status tokens follow the theme into dark mode. Text stays foreground:
|
|
11
|
+
// the status hues are tuned for fills and icons, not body-text contrast.
|
|
12
|
+
default: 'bg-muted/50 border-border text-foreground',
|
|
12
13
|
destructive:
|
|
13
|
-
'bg-
|
|
14
|
-
error:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
warning:
|
|
19
|
-
'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800 text-yellow-700 dark:text-yellow-400 [&>svg]:text-yellow-700 dark:[&>svg]:text-yellow-400',
|
|
20
|
-
info: 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-400 [&>svg]:text-blue-700 dark:[&>svg]:text-blue-400',
|
|
14
|
+
'bg-destructive/10 border-destructive/40 text-foreground [&>svg]:text-destructive',
|
|
15
|
+
error: 'bg-destructive/10 border-destructive/40 text-foreground [&>svg]:text-destructive',
|
|
16
|
+
success: 'bg-success/10 border-success/40 text-foreground [&>svg]:text-success',
|
|
17
|
+
warning: 'bg-warning/10 border-warning/40 text-foreground [&>svg]:text-warning',
|
|
18
|
+
info: 'bg-info/10 border-info/40 text-foreground [&>svg]:text-info',
|
|
21
19
|
},
|
|
22
20
|
},
|
|
23
21
|
defaultVariants: {
|
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
collisionPadding={8}
|
|
187
187
|
avoidCollisions={true}
|
|
188
188
|
>
|
|
189
|
-
<div class="p-1 max-h-
|
|
189
|
+
<div class="p-1 max-h-72 overflow-y-auto">
|
|
190
190
|
{#if loading}
|
|
191
191
|
<div class="flex items-center justify-center py-6">
|
|
192
192
|
<Spinner size="sm" />
|
|
@@ -282,7 +282,7 @@
|
|
|
282
282
|
collisionPadding={8}
|
|
283
283
|
avoidCollisions={true}
|
|
284
284
|
>
|
|
285
|
-
<div class="p-1 max-h-
|
|
285
|
+
<div class="p-1 max-h-72 overflow-y-auto">
|
|
286
286
|
{#if loading}
|
|
287
287
|
<div class="flex items-center justify-center py-6">
|
|
288
288
|
<Spinner size="sm" />
|
|
@@ -169,6 +169,10 @@
|
|
|
169
169
|
class: className,
|
|
170
170
|
}: Props = $props();
|
|
171
171
|
|
|
172
|
+
// Unique per instance: two panels on a page must not share a title id.
|
|
173
|
+
const uid = $props.id();
|
|
174
|
+
const titleId = `data-panel-${uid}-title`;
|
|
175
|
+
|
|
172
176
|
// Resolve constraints and card config (use $derived to react to prop changes)
|
|
173
177
|
const resolvedConstraints = $derived({ ...DEFAULT_CONSTRAINTS, ...constraints });
|
|
174
178
|
const resolvedCardConfig = $derived({ ...DEFAULT_CARD_CONFIG, ...cardConfig });
|
|
@@ -440,6 +444,15 @@
|
|
|
440
444
|
handleExpandedChange(!isExpanded);
|
|
441
445
|
}
|
|
442
446
|
|
|
447
|
+
// Keyboard activation for the card's role="button" header areas. Space must
|
|
448
|
+
// not also scroll the page, and a held key must not toggle repeatedly.
|
|
449
|
+
function handleCardToggleKeyDown(e: KeyboardEvent) {
|
|
450
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
451
|
+
e.preventDefault();
|
|
452
|
+
if (e.repeat) return;
|
|
453
|
+
handleToggleExpand();
|
|
454
|
+
}
|
|
455
|
+
|
|
443
456
|
// Header collapse button. A detached full panel collapses by re-pinning to its
|
|
444
457
|
// last edge (edge always holds the previous pin, or the default) and showing the
|
|
445
458
|
// collapsed tab. Pinned/card just toggle expanded.
|
|
@@ -1357,7 +1370,8 @@
|
|
|
1357
1370
|
.join(';')}
|
|
1358
1371
|
role={mode === 'detached' || variant === 'card' ? 'dialog' : 'complementary'}
|
|
1359
1372
|
aria-modal={isDialog ? 'true' : undefined}
|
|
1360
|
-
aria-labelledby={title ?
|
|
1373
|
+
aria-labelledby={title && !header ? titleId : undefined}
|
|
1374
|
+
aria-label={title && header ? title : undefined}
|
|
1361
1375
|
aria-expanded={variant === 'card' ? isExpanded : undefined}
|
|
1362
1376
|
tabindex="-1"
|
|
1363
1377
|
onpointerdown={variant === 'full' &&
|
|
@@ -1388,7 +1402,7 @@
|
|
|
1388
1402
|
{#if variant === 'card' && resolvedCardConfig.showDragHandle}
|
|
1389
1403
|
<div
|
|
1390
1404
|
class="absolute top-1.5 left-1/2 -translate-x-1/2 w-10 h-1.5 rounded-full bg-muted-foreground/30 cursor-pointer touch-none"
|
|
1391
|
-
onkeydown={
|
|
1405
|
+
onkeydown={handleCardToggleKeyDown}
|
|
1392
1406
|
onpointerdown={handleCardHeaderPointerDown}
|
|
1393
1407
|
role="button"
|
|
1394
1408
|
tabindex="0"
|
|
@@ -1399,7 +1413,7 @@
|
|
|
1399
1413
|
{#if variant === 'card'}
|
|
1400
1414
|
<div
|
|
1401
1415
|
class="flex-1 min-w-0"
|
|
1402
|
-
onkeydown={
|
|
1416
|
+
onkeydown={handleCardToggleKeyDown}
|
|
1403
1417
|
role="button"
|
|
1404
1418
|
tabindex="0"
|
|
1405
1419
|
>
|
|
@@ -1410,7 +1424,7 @@
|
|
|
1410
1424
|
<p class="text-sm text-muted-foreground truncate">{subtitle}</p>
|
|
1411
1425
|
{/if}
|
|
1412
1426
|
{#if title}
|
|
1413
|
-
<h3 id=
|
|
1427
|
+
<h3 id={titleId} class="text-lg font-semibold truncate">{title}</h3>
|
|
1414
1428
|
{/if}
|
|
1415
1429
|
{/if}
|
|
1416
1430
|
</div>
|
|
@@ -1423,7 +1437,7 @@
|
|
|
1423
1437
|
<p class="text-sm text-muted-foreground truncate">{subtitle}</p>
|
|
1424
1438
|
{/if}
|
|
1425
1439
|
{#if title}
|
|
1426
|
-
<h3 id=
|
|
1440
|
+
<h3 id={titleId} class="text-lg font-semibold truncate">{title}</h3>
|
|
1427
1441
|
{/if}
|
|
1428
1442
|
{/if}
|
|
1429
1443
|
</div>
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
rowSelection?: Set<string>;
|
|
86
86
|
/** Callback when selection changes */
|
|
87
87
|
onRowSelectionChange?: (selection: Set<string>) => void;
|
|
88
|
-
/** Function to get unique row ID (defaults to index) */
|
|
88
|
+
/** Function to get unique row ID (defaults to the row's index in `data`) */
|
|
89
89
|
getRowId?: (row: T, index: number) => string;
|
|
90
90
|
|
|
91
91
|
// === Visibility Props ===
|
|
@@ -239,7 +239,7 @@
|
|
|
239
239
|
const table = createDataTable<T>(() => ({
|
|
240
240
|
data,
|
|
241
241
|
columns,
|
|
242
|
-
getRowId
|
|
242
|
+
getRowId,
|
|
243
243
|
initialSortColumn: sortColumn,
|
|
244
244
|
initialSortDirection: sortDirection,
|
|
245
245
|
initialPageSize: pageSize,
|
|
@@ -248,6 +248,8 @@
|
|
|
248
248
|
manualSorting: !!onSort,
|
|
249
249
|
manualFiltering,
|
|
250
250
|
manualPagination,
|
|
251
|
+
// Without the pager every row must render, not just the first page.
|
|
252
|
+
enablePagination,
|
|
251
253
|
pageCount,
|
|
252
254
|
onSortChange: (col, dir) => {
|
|
253
255
|
sortColumn = col ?? undefined;
|
|
@@ -324,9 +326,14 @@
|
|
|
324
326
|
table.toggleAllRows();
|
|
325
327
|
}
|
|
326
328
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
329
|
+
// Keyboard activation for clickable rows; ignore keys aimed at the
|
|
330
|
+
// row's own controls (the selection checkbox handles its own Space).
|
|
331
|
+
function handleRowKeydown(e: KeyboardEvent, row: T) {
|
|
332
|
+
if (e.target !== e.currentTarget) return;
|
|
333
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
334
|
+
e.preventDefault();
|
|
335
|
+
handleRowClick(row);
|
|
336
|
+
}
|
|
330
337
|
}
|
|
331
338
|
|
|
332
339
|
// Literal class strings so Tailwind's content scan generates them.
|
|
@@ -442,7 +449,9 @@
|
|
|
442
449
|
onRowClick && 'cursor-pointer hover:bg-muted-hover',
|
|
443
450
|
row.isSelected && 'bg-primary/5'
|
|
444
451
|
)}
|
|
452
|
+
tabindex={onRowClick ? 0 : undefined}
|
|
445
453
|
onclick={() => handleRowClick(row.original)}
|
|
454
|
+
onkeydown={onRowClick ? (e) => handleRowKeydown(e, row.original) : undefined}
|
|
446
455
|
>
|
|
447
456
|
{#if enableRowSelection}
|
|
448
457
|
<td class="px-4 py-3">
|
|
@@ -451,7 +460,7 @@
|
|
|
451
460
|
checked={row.isSelected}
|
|
452
461
|
onCheckedChange={() => table.toggleRowSelection(row.id)}
|
|
453
462
|
aria-label={`Select row ${index + 1}`}
|
|
454
|
-
onclick={(e: MouseEvent) =>
|
|
463
|
+
onclick={(e: MouseEvent) => e.stopPropagation()}
|
|
455
464
|
/>
|
|
456
465
|
{/if}
|
|
457
466
|
</td>
|
|
@@ -66,7 +66,7 @@ declare function $$render<T extends object>(): {
|
|
|
66
66
|
rowSelection?: Set<string>;
|
|
67
67
|
/** Callback when selection changes */
|
|
68
68
|
onRowSelectionChange?: (selection: Set<string>) => void;
|
|
69
|
-
/** Function to get unique row ID (defaults to index) */
|
|
69
|
+
/** Function to get unique row ID (defaults to the row's index in `data`) */
|
|
70
70
|
getRowId?: (row: T, index: number) => string;
|
|
71
71
|
/** Enable column visibility toggling */
|
|
72
72
|
enableColumnVisibility?: boolean;
|
|
@@ -75,8 +75,9 @@
|
|
|
75
75
|
const hintId = $derived(`${componentId}-hint`);
|
|
76
76
|
const errorId = $derived(`${componentId}-error`);
|
|
77
77
|
|
|
78
|
-
// Sync stringValue -> value when stringValue changes externally
|
|
79
|
-
|
|
78
|
+
// Sync stringValue -> value when stringValue changes externally. Starts at
|
|
79
|
+
// null so an initial stringValue is adopted before value -> stringValue runs.
|
|
80
|
+
let lastStringValue = $state<string | null>(null);
|
|
80
81
|
$effect(() => {
|
|
81
82
|
if (stringValue !== lastStringValue) {
|
|
82
83
|
lastStringValue = stringValue;
|
|
@@ -117,7 +118,8 @@
|
|
|
117
118
|
// Compute aria-describedby based on hint and error
|
|
118
119
|
const describedBy = $derived.by(() => {
|
|
119
120
|
const ids: string[] = [];
|
|
120
|
-
|
|
121
|
+
// The hint is not rendered while an error shows
|
|
122
|
+
if (hint && !displayError) ids.push(hintId);
|
|
121
123
|
if (displayError) ids.push(errorId);
|
|
122
124
|
return ids.length > 0 ? ids.join(' ') : undefined;
|
|
123
125
|
});
|
|
@@ -412,9 +414,6 @@
|
|
|
412
414
|
];
|
|
413
415
|
|
|
414
416
|
const dayNames = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
|
415
|
-
|
|
416
|
-
// Determine if we need a container wrapper
|
|
417
|
-
const hasWrapper = $derived(!!label || !!hint || !!error || !!parseError);
|
|
418
417
|
</script>
|
|
419
418
|
|
|
420
419
|
{#snippet pickerContent()}
|
|
@@ -633,32 +632,30 @@
|
|
|
633
632
|
{/if}
|
|
634
633
|
{/snippet}
|
|
635
634
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
{#if hint && !displayError}
|
|
651
|
-
<p id={hintId} class="text-sm text-muted-foreground">
|
|
652
|
-
{hint}
|
|
653
|
-
</p>
|
|
654
|
-
{/if}
|
|
655
|
-
|
|
656
|
-
{#if displayError}
|
|
657
|
-
<p id={errorId} class="text-sm text-destructive" role="alert" aria-live="polite">
|
|
658
|
-
{displayError}
|
|
659
|
-
</p>
|
|
660
|
-
{/if}
|
|
661
|
-
</div>
|
|
662
|
-
{:else}
|
|
635
|
+
<!-- Always wrapped: toggling the wrapper on a parse error would re-mount the
|
|
636
|
+
input and drop focus. -->
|
|
637
|
+
<div class="space-y-2">
|
|
638
|
+
{#if label}
|
|
639
|
+
<Label for={componentId} {disabled}>
|
|
640
|
+
{label}
|
|
641
|
+
{#if required}
|
|
642
|
+
<span class="text-destructive ml-0.5" aria-hidden="true">*</span>
|
|
643
|
+
<span class="sr-only">(required)</span>
|
|
644
|
+
{/if}
|
|
645
|
+
</Label>
|
|
646
|
+
{/if}
|
|
647
|
+
|
|
663
648
|
{@render pickerContent()}
|
|
664
|
-
|
|
649
|
+
|
|
650
|
+
{#if hint && !displayError}
|
|
651
|
+
<p id={hintId} class="text-sm text-muted-foreground">
|
|
652
|
+
{hint}
|
|
653
|
+
</p>
|
|
654
|
+
{/if}
|
|
655
|
+
|
|
656
|
+
{#if displayError}
|
|
657
|
+
<p id={errorId} class="text-sm text-destructive" role="alert" aria-live="polite">
|
|
658
|
+
{displayError}
|
|
659
|
+
</p>
|
|
660
|
+
{/if}
|
|
661
|
+
</div>
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
icon?: IconName;
|
|
18
18
|
/** Render the item as a link to this URL (selection still fires `onSelect`) */
|
|
19
19
|
href?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Make this a checkbox item (`menuitemcheckbox`) showing this state.
|
|
22
|
+
* Selecting it fires `onSelect` and keeps the menu open.
|
|
23
|
+
*/
|
|
24
|
+
checked?: boolean;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
export interface DropdownMenuGroup {
|
|
@@ -102,6 +107,19 @@
|
|
|
102
107
|
<div role="presentation" class="px-2 py-1.5 text-sm font-semibold">
|
|
103
108
|
{item.label}
|
|
104
109
|
</div>
|
|
110
|
+
{:else if item.checked !== undefined}
|
|
111
|
+
<DropdownMenuPrimitive.CheckboxItem
|
|
112
|
+
checked={item.checked}
|
|
113
|
+
disabled={item.disabled}
|
|
114
|
+
closeOnSelect={false}
|
|
115
|
+
onSelect={() => item.onSelect?.()}
|
|
116
|
+
class="relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none bg-popover data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default"
|
|
117
|
+
>
|
|
118
|
+
{#if item.checked}
|
|
119
|
+
<Icon name="check" size={16} class="absolute left-2 shrink-0" aria-hidden="true" />
|
|
120
|
+
{/if}
|
|
121
|
+
{@render itemContent(item)}
|
|
122
|
+
</DropdownMenuPrimitive.CheckboxItem>
|
|
105
123
|
{:else}
|
|
106
124
|
{@const itemClass = cn(
|
|
107
125
|
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none bg-popover data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default',
|
|
@@ -151,34 +169,38 @@
|
|
|
151
169
|
{/snippet}
|
|
152
170
|
</DropdownMenuPrimitive.Trigger>
|
|
153
171
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
172
|
+
<!-- Portal so the menu is not clipped by an overflow/transformed ancestor
|
|
173
|
+
(a Dialog, or the DataTable's scroll wrapper). -->
|
|
174
|
+
<DropdownMenuPrimitive.Portal>
|
|
175
|
+
<DropdownMenuPrimitive.Content
|
|
176
|
+
{side}
|
|
177
|
+
{align}
|
|
178
|
+
sideOffset={4}
|
|
179
|
+
collisionPadding={8}
|
|
180
|
+
avoidCollisions={true}
|
|
181
|
+
class={cn(
|
|
182
|
+
'isolate z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
183
|
+
className
|
|
184
|
+
)}
|
|
185
|
+
>
|
|
186
|
+
<div class="max-h-72 overflow-y-auto overflow-x-hidden">
|
|
187
|
+
{#each items as item}
|
|
188
|
+
{#if isGroup(item)}
|
|
189
|
+
<DropdownMenuPrimitive.Group>
|
|
190
|
+
{#if item.label}
|
|
191
|
+
<DropdownMenuPrimitive.GroupHeading class="px-2 py-1.5 text-sm font-semibold">
|
|
192
|
+
{item.label}
|
|
193
|
+
</DropdownMenuPrimitive.GroupHeading>
|
|
194
|
+
{/if}
|
|
195
|
+
{#each item.items as subItem}
|
|
196
|
+
{@render entry(subItem)}
|
|
197
|
+
{/each}
|
|
198
|
+
</DropdownMenuPrimitive.Group>
|
|
199
|
+
{:else}
|
|
200
|
+
{@render entry(item)}
|
|
201
|
+
{/if}
|
|
202
|
+
{/each}
|
|
203
|
+
</div>
|
|
204
|
+
</DropdownMenuPrimitive.Content>
|
|
205
|
+
</DropdownMenuPrimitive.Portal>
|
|
184
206
|
</DropdownMenuPrimitive.Root>
|
|
@@ -13,6 +13,11 @@ export interface DropdownMenuItem {
|
|
|
13
13
|
icon?: IconName;
|
|
14
14
|
/** Render the item as a link to this URL (selection still fires `onSelect`) */
|
|
15
15
|
href?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Make this a checkbox item (`menuitemcheckbox`) showing this state.
|
|
18
|
+
* Selecting it fires `onSelect` and keeps the menu open.
|
|
19
|
+
*/
|
|
20
|
+
checked?: boolean;
|
|
16
21
|
}
|
|
17
22
|
export interface DropdownMenuGroup {
|
|
18
23
|
label?: string;
|
|
@@ -86,7 +86,6 @@
|
|
|
86
86
|
|
|
87
87
|
let isDragging = $state(false);
|
|
88
88
|
let internalError = $state<string | undefined>(undefined);
|
|
89
|
-
let fileInput: HTMLInputElement;
|
|
90
89
|
let lightboxOpen = $state(false);
|
|
91
90
|
let lightboxIndex = $state(0);
|
|
92
91
|
|
|
@@ -420,13 +419,6 @@
|
|
|
420
419
|
}
|
|
421
420
|
}
|
|
422
421
|
|
|
423
|
-
// Handle click to browse
|
|
424
|
-
function handleClick() {
|
|
425
|
-
if (!disabled) {
|
|
426
|
-
fileInput?.click();
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
422
|
// Handle file input change
|
|
431
423
|
function handleInputChange(e: Event) {
|
|
432
424
|
const input = e.target as HTMLInputElement;
|
|
@@ -492,20 +484,15 @@
|
|
|
492
484
|
|
|
493
485
|
const usePreviewGrid = $derived(previewLayout === 'grid' && previewItems.length > 0);
|
|
494
486
|
|
|
487
|
+
// The focusable file input carries the description, not the drop-zone label
|
|
488
|
+
const describedBy = $derived(displayError ? `${id}-error` : hint ? `${id}-hint` : undefined);
|
|
489
|
+
|
|
495
490
|
function openPreview(fileId: string) {
|
|
496
491
|
const previewIndex = previewItems.findIndex((item) => item.id === fileId);
|
|
497
492
|
if (previewIndex === -1) return;
|
|
498
493
|
lightboxIndex = previewIndex;
|
|
499
494
|
lightboxOpen = true;
|
|
500
495
|
}
|
|
501
|
-
|
|
502
|
-
// Handle keyboard activation
|
|
503
|
-
function handleKeyDown(e: KeyboardEvent) {
|
|
504
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
505
|
-
e.preventDefault();
|
|
506
|
-
handleClick();
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
496
|
</script>
|
|
510
497
|
|
|
511
498
|
<div class={cn('space-y-2', className)}>
|
|
@@ -518,7 +505,6 @@
|
|
|
518
505
|
<!-- Drop Zone - using label to wrap the hidden input for better accessibility -->
|
|
519
506
|
<label
|
|
520
507
|
for={id}
|
|
521
|
-
aria-describedby={displayError ? `${id}-error` : undefined}
|
|
522
508
|
class={cn(
|
|
523
509
|
'relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed p-6',
|
|
524
510
|
'cursor-pointer focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2',
|
|
@@ -560,12 +546,12 @@
|
|
|
560
546
|
|
|
561
547
|
<!-- Hidden file input -->
|
|
562
548
|
<input
|
|
563
|
-
bind:this={fileInput}
|
|
564
549
|
type="file"
|
|
565
550
|
{id}
|
|
566
551
|
multiple={maxFiles > 1}
|
|
567
552
|
accept={acceptedTypes.join(',')}
|
|
568
553
|
{disabled}
|
|
554
|
+
aria-describedby={describedBy}
|
|
569
555
|
class="sr-only"
|
|
570
556
|
onchange={handleInputChange}
|
|
571
557
|
/>
|
|
@@ -573,7 +559,7 @@
|
|
|
573
559
|
|
|
574
560
|
<!-- Hint text -->
|
|
575
561
|
{#if hint && !displayError}
|
|
576
|
-
<p class="text-sm text-muted-foreground">{hint}</p>
|
|
562
|
+
<p id={`${id}-hint`} class="text-sm text-muted-foreground">{hint}</p>
|
|
577
563
|
{/if}
|
|
578
564
|
|
|
579
565
|
<!-- Error message -->
|
|
@@ -134,14 +134,15 @@
|
|
|
134
134
|
document.getElementById(optionId(selectedIndex))?.scrollIntoView?.({ block: 'nearest' });
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
-
// Keyboard shortcut handler
|
|
137
|
+
// Keyboard shortcut handler. Not registered while disabled, so the browser's
|
|
138
|
+
// own Ctrl+K stays usable and no hidden open is queued for later.
|
|
138
139
|
$effect(() => {
|
|
139
|
-
if (!enableShortcut || typeof window === 'undefined') return;
|
|
140
|
+
if (!enabled || !enableShortcut || typeof window === 'undefined') return;
|
|
140
141
|
|
|
141
142
|
const handleKeydown = (e: KeyboardEvent) => {
|
|
142
143
|
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === shortcutKey) {
|
|
143
144
|
e.preventDefault();
|
|
144
|
-
|
|
145
|
+
setOpen(true);
|
|
145
146
|
}
|
|
146
147
|
};
|
|
147
148
|
|
|
@@ -193,17 +194,24 @@
|
|
|
193
194
|
|
|
194
195
|
function handleSelect(item: SearchResultItem) {
|
|
195
196
|
onSelect?.(item);
|
|
196
|
-
|
|
197
|
-
query = '';
|
|
197
|
+
setOpen(false);
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
|
|
200
|
+
/**
|
|
201
|
+
* Every open/close goes through here. bits-ui only reports its own
|
|
202
|
+
* (Escape, outside click) changes, so the triggers, shortcut and selection
|
|
203
|
+
* must notify `onOpenChange` themselves.
|
|
204
|
+
*/
|
|
205
|
+
function setOpen(isOpen: boolean) {
|
|
206
|
+
if (open === isOpen) return;
|
|
201
207
|
open = isOpen;
|
|
202
|
-
onOpenChange?.(isOpen);
|
|
203
208
|
if (!isOpen) {
|
|
204
209
|
query = '';
|
|
205
210
|
selectedIndex = 0;
|
|
211
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
212
|
+
debounceTimer = null;
|
|
206
213
|
}
|
|
214
|
+
onOpenChange?.(isOpen);
|
|
207
215
|
}
|
|
208
216
|
</script>
|
|
209
217
|
|
|
@@ -213,7 +221,7 @@
|
|
|
213
221
|
<!-- Icon-only variant -->
|
|
214
222
|
<button
|
|
215
223
|
type="button"
|
|
216
|
-
onclick={() => (
|
|
224
|
+
onclick={() => setOpen(true)}
|
|
217
225
|
class={cn(
|
|
218
226
|
'inline-flex items-center justify-center rounded-md border border-input bg-background text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
219
227
|
size === 'sm' ? 'h-8 w-8' : 'h-9 w-9',
|
|
@@ -235,7 +243,7 @@
|
|
|
235
243
|
<!-- Default full search box variant -->
|
|
236
244
|
<button
|
|
237
245
|
type="button"
|
|
238
|
-
onclick={() => (
|
|
246
|
+
onclick={() => setOpen(true)}
|
|
239
247
|
class={cn(
|
|
240
248
|
'flex items-center gap-2 rounded-md border border-input bg-background px-3 text-sm text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
241
249
|
size === 'sm' ? 'h-8' : 'h-9',
|
|
@@ -263,7 +271,7 @@
|
|
|
263
271
|
{/if}
|
|
264
272
|
|
|
265
273
|
<!-- Search Dialog -->
|
|
266
|
-
<DialogPrimitive.Root
|
|
274
|
+
<DialogPrimitive.Root {open} onOpenChange={setOpen}>
|
|
267
275
|
<DialogPrimitive.Portal>
|
|
268
276
|
<DialogPrimitive.Overlay
|
|
269
277
|
class="fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
|
@@ -244,7 +244,7 @@
|
|
|
244
244
|
collisionPadding={8}
|
|
245
245
|
avoidCollisions={true}
|
|
246
246
|
>
|
|
247
|
-
<div class="p-1 max-h-
|
|
247
|
+
<div class="p-1 max-h-72 overflow-y-auto">
|
|
248
248
|
{#if loading}
|
|
249
249
|
<div class="flex items-center justify-center py-6">
|
|
250
250
|
<Spinner size="sm" />
|
|
@@ -381,7 +381,7 @@
|
|
|
381
381
|
collisionPadding={8}
|
|
382
382
|
avoidCollisions={true}
|
|
383
383
|
>
|
|
384
|
-
<div class="p-1 max-h-
|
|
384
|
+
<div class="p-1 max-h-72 overflow-y-auto">
|
|
385
385
|
{#if loading}
|
|
386
386
|
<div class="flex items-center justify-center py-6">
|
|
387
387
|
<Spinner size="sm" />
|
|
@@ -101,10 +101,23 @@
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
// Decimal places of a number, e.g. 0.25 -> 2, 1e-7 -> 7
|
|
105
|
+
function decimalPlaces(n: number): number {
|
|
106
|
+
const [mantissa, exponent] = String(n).split('e');
|
|
107
|
+
const fraction = mantissa.split('.')[1]?.length ?? 0;
|
|
108
|
+
return Math.max(0, fraction - (exponent ? parseInt(exponent) : 0));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Add in floating point, then round away artifacts (0.2 + 0.1 -> 0.3)
|
|
112
|
+
function addStep(current: number, delta: number): number {
|
|
113
|
+
const places = Math.min(20, Math.max(decimalPlaces(current), decimalPlaces(delta)));
|
|
114
|
+
return Number((current + delta).toFixed(places));
|
|
115
|
+
}
|
|
116
|
+
|
|
104
117
|
function increment() {
|
|
105
118
|
if (disabled || readonly) return;
|
|
106
119
|
const currentValue = value ?? min ?? 0;
|
|
107
|
-
const newValue = clampValue(currentValue
|
|
120
|
+
const newValue = clampValue(addStep(currentValue, step));
|
|
108
121
|
value = newValue;
|
|
109
122
|
onValueChange?.(newValue);
|
|
110
123
|
}
|
|
@@ -112,7 +125,7 @@
|
|
|
112
125
|
function decrement() {
|
|
113
126
|
if (disabled || readonly) return;
|
|
114
127
|
const currentValue = value ?? max ?? 0;
|
|
115
|
-
const newValue = clampValue(currentValue -
|
|
128
|
+
const newValue = clampValue(addStep(currentValue, -step));
|
|
116
129
|
value = newValue;
|
|
117
130
|
onValueChange?.(newValue);
|
|
118
131
|
}
|