@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
|
@@ -41,6 +41,10 @@
|
|
|
41
41
|
error?: string;
|
|
42
42
|
/** Hint text displayed below the select */
|
|
43
43
|
hint?: string;
|
|
44
|
+
/** Accessible name for the trigger, when there is no visible label */
|
|
45
|
+
'aria-label'?: string;
|
|
46
|
+
/** ID of an element that labels the trigger */
|
|
47
|
+
'aria-labelledby'?: string;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
let {
|
|
@@ -56,6 +60,8 @@
|
|
|
56
60
|
id,
|
|
57
61
|
error,
|
|
58
62
|
hint,
|
|
63
|
+
'aria-label': ariaLabelProp,
|
|
64
|
+
'aria-labelledby': ariaLabelledby,
|
|
59
65
|
}: Props = $props();
|
|
60
66
|
|
|
61
67
|
// Generate unique IDs for accessibility using sequential counter
|
|
@@ -68,11 +74,16 @@
|
|
|
68
74
|
// Compute aria-describedby based on hint and error
|
|
69
75
|
const describedBy = $derived.by(() => {
|
|
70
76
|
const ids: string[] = [];
|
|
71
|
-
|
|
77
|
+
// The hint is not rendered while an error shows
|
|
78
|
+
if (hint && !error) ids.push(hintId);
|
|
72
79
|
if (error) ids.push(errorId);
|
|
73
80
|
return ids.length > 0 ? ids.join(' ') : undefined;
|
|
74
81
|
});
|
|
75
82
|
|
|
83
|
+
const fallbackAriaLabel = $derived(
|
|
84
|
+
ariaLabelProp ?? (!label && !id && !ariaLabelledby ? placeholder : undefined)
|
|
85
|
+
);
|
|
86
|
+
|
|
76
87
|
// Validate props in development
|
|
77
88
|
$effect(() => {
|
|
78
89
|
validateNonEmptyArray(options, 'options', 'Select');
|
|
@@ -81,7 +92,7 @@
|
|
|
81
92
|
// Accessibility: warn if no label is associated in dev mode
|
|
82
93
|
if (DEV) {
|
|
83
94
|
$effect(() => {
|
|
84
|
-
if (!label && !id) {
|
|
95
|
+
if (!label && !id && !ariaLabelProp && !ariaLabelledby) {
|
|
85
96
|
console.warn(
|
|
86
97
|
'[Select] Missing accessible label. Provide a label prop or an id with a matching <label for="...">.'
|
|
87
98
|
);
|
|
@@ -209,7 +220,7 @@
|
|
|
209
220
|
collisionPadding={8}
|
|
210
221
|
avoidCollisions={true}
|
|
211
222
|
>
|
|
212
|
-
<div class="p-1 max-h-
|
|
223
|
+
<div class="p-1 max-h-72 overflow-y-auto">
|
|
213
224
|
{#each options as item}
|
|
214
225
|
{#if isGroup(item)}
|
|
215
226
|
<SelectPrimitive.Group>
|
|
@@ -229,6 +240,8 @@
|
|
|
229
240
|
</SelectPrimitive.Portal>
|
|
230
241
|
{/snippet}
|
|
231
242
|
|
|
243
|
+
<!-- The placeholder is only a last-resort name: an aria-label would override
|
|
244
|
+
an external <label for={id}> or aria-labelledby. -->
|
|
232
245
|
{#snippet selectRoot(ariaLabel: string | undefined)}
|
|
233
246
|
<SelectPrimitive.Root
|
|
234
247
|
type="single"
|
|
@@ -243,6 +256,7 @@
|
|
|
243
256
|
id={componentId}
|
|
244
257
|
class={triggerClasses}
|
|
245
258
|
aria-label={ariaLabel}
|
|
259
|
+
aria-labelledby={ariaLabelledby}
|
|
246
260
|
aria-invalid={error ? 'true' : undefined}
|
|
247
261
|
aria-describedby={describedBy}
|
|
248
262
|
>
|
|
@@ -268,7 +282,7 @@
|
|
|
268
282
|
</Label>
|
|
269
283
|
{/if}
|
|
270
284
|
|
|
271
|
-
{@render selectRoot(
|
|
285
|
+
{@render selectRoot(fallbackAriaLabel)}
|
|
272
286
|
|
|
273
287
|
{#if hint && !error}
|
|
274
288
|
<p id={hintId} class="text-sm text-muted-foreground">
|
|
@@ -283,5 +297,5 @@
|
|
|
283
297
|
{/if}
|
|
284
298
|
</div>
|
|
285
299
|
{:else}
|
|
286
|
-
{@render selectRoot(
|
|
300
|
+
{@render selectRoot(fallbackAriaLabel)}
|
|
287
301
|
{/if}
|
|
@@ -32,6 +32,10 @@ interface Props {
|
|
|
32
32
|
error?: string;
|
|
33
33
|
/** Hint text displayed below the select */
|
|
34
34
|
hint?: string;
|
|
35
|
+
/** Accessible name for the trigger, when there is no visible label */
|
|
36
|
+
'aria-label'?: string;
|
|
37
|
+
/** ID of an element that labels the trigger */
|
|
38
|
+
'aria-labelledby'?: string;
|
|
35
39
|
}
|
|
36
40
|
declare const Select: import("svelte").Component<Props, {}, "value">;
|
|
37
41
|
type Select = ReturnType<typeof Select>;
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
'inline-flex items-center justify-center rounded-md px-3 py-1.5 text-sm font-medium',
|
|
32
32
|
modeButtonActive: 'bg-background text-foreground shadow-sm',
|
|
33
33
|
modeButtonInactive: 'text-muted-foreground hover:text-foreground',
|
|
34
|
-
|
|
34
|
+
// A white pad, like the exported image, so the default black ink stays
|
|
35
|
+
// visible in dark mode.
|
|
36
|
+
canvasContainer: 'relative w-full overflow-hidden rounded-lg border bg-white',
|
|
35
37
|
canvas: 'block w-full cursor-crosshair touch-none',
|
|
36
38
|
toolbar: 'flex flex-wrap items-center gap-2',
|
|
37
39
|
toolButton:
|
|
@@ -88,7 +90,7 @@
|
|
|
88
90
|
/** Default typed name for type mode */
|
|
89
91
|
typedName?: string;
|
|
90
92
|
|
|
91
|
-
/** Stroke color (default:
|
|
93
|
+
/** Stroke color (default: #000000, drawn on a white pad to match the export) */
|
|
92
94
|
strokeColor?: string;
|
|
93
95
|
/** Stroke width in pixels */
|
|
94
96
|
strokeWidth?: number;
|
|
@@ -222,8 +224,11 @@
|
|
|
222
224
|
);
|
|
223
225
|
let fontsLoaded = $state<Set<string>>(new Set());
|
|
224
226
|
|
|
225
|
-
// Consent state
|
|
227
|
+
// Consent state, kept in step with the bindable prop both ways
|
|
226
228
|
let consented = $state(untrack(() => initialConsented));
|
|
229
|
+
$effect(() => {
|
|
230
|
+
consented = initialConsented;
|
|
231
|
+
});
|
|
227
232
|
|
|
228
233
|
// Validation state
|
|
229
234
|
let validationResult = $state<SignatureValidationResult>({ valid: false });
|
|
@@ -661,7 +666,10 @@
|
|
|
661
666
|
function handlePointerUp(e: PointerEvent): void {
|
|
662
667
|
if (!isDrawing || !currentStroke || disabled) return;
|
|
663
668
|
|
|
664
|
-
|
|
669
|
+
// After pointercancel the capture is already gone and release would throw
|
|
670
|
+
if (canvas.hasPointerCapture?.(e.pointerId)) {
|
|
671
|
+
canvas.releasePointerCapture(e.pointerId);
|
|
672
|
+
}
|
|
665
673
|
isDrawing = false;
|
|
666
674
|
currentStroke.endTime = Date.now();
|
|
667
675
|
|
|
@@ -732,6 +740,8 @@
|
|
|
732
740
|
function handleConsentChange(e: Event): void {
|
|
733
741
|
const target = e.target as HTMLInputElement;
|
|
734
742
|
consented = target.checked;
|
|
743
|
+
// Write back so bind:consented reflects the checkbox
|
|
744
|
+
initialConsented = consented;
|
|
735
745
|
onConsentChange?.(consented);
|
|
736
746
|
updateValue();
|
|
737
747
|
}
|
|
@@ -915,6 +925,7 @@
|
|
|
915
925
|
onpointerdown={handlePointerDown}
|
|
916
926
|
onpointermove={handlePointerMove}
|
|
917
927
|
onpointerup={handlePointerUp}
|
|
928
|
+
onpointercancel={handlePointerUp}
|
|
918
929
|
onpointerleave={handlePointerLeave}
|
|
919
930
|
onkeydown={handleKeyDown}
|
|
920
931
|
></canvas>
|
|
@@ -14,7 +14,7 @@ interface Props {
|
|
|
14
14
|
showModeToggle?: boolean;
|
|
15
15
|
/** Default typed name for type mode */
|
|
16
16
|
typedName?: string;
|
|
17
|
-
/** Stroke color (default:
|
|
17
|
+
/** Stroke color (default: #000000, drawn on a white pad to match the export) */
|
|
18
18
|
strokeColor?: string;
|
|
19
19
|
/** Stroke width in pixels */
|
|
20
20
|
strokeWidth?: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
2
|
declare const Skeleton: import("svelte").Component<HTMLAttributes<HTMLDivElement> & {
|
|
3
|
-
variant?: "
|
|
3
|
+
variant?: "text" | "default" | "button" | "title" | "avatar" | "card" | undefined;
|
|
4
4
|
/** Animation style for the skeleton */
|
|
5
5
|
animation?: "pulse" | "shimmer" | "none";
|
|
6
6
|
class?: string;
|
|
@@ -7,23 +7,25 @@
|
|
|
7
7
|
|
|
8
8
|
const sliderVariants = tv({
|
|
9
9
|
slots: {
|
|
10
|
-
root: 'relative flex w-full touch-none select-none items-center',
|
|
11
|
-
|
|
10
|
+
root: 'relative flex w-full touch-none select-none items-center data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-40 data-[orientation=vertical]:flex-col',
|
|
11
|
+
// Vertical: bits-ui positions the range by top/bottom, so drop h-full
|
|
12
|
+
range:
|
|
13
|
+
'absolute h-full bg-primary rounded-full data-[orientation=vertical]:h-auto data-[orientation=vertical]:w-full',
|
|
12
14
|
thumb:
|
|
13
15
|
"block rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 relative after:absolute after:left-1/2 after:top-1/2 after:h-11 after:w-11 after:-translate-x-1/2 after:-translate-y-1/2 after:rounded-full after:content-['']",
|
|
14
16
|
},
|
|
15
17
|
variants: {
|
|
16
18
|
size: {
|
|
17
19
|
sm: {
|
|
18
|
-
root: 'h-1.5',
|
|
20
|
+
root: 'h-1.5 data-[orientation=vertical]:w-1.5',
|
|
19
21
|
thumb: 'h-4 w-4',
|
|
20
22
|
},
|
|
21
23
|
md: {
|
|
22
|
-
root: 'h-2',
|
|
24
|
+
root: 'h-2 data-[orientation=vertical]:w-2',
|
|
23
25
|
thumb: 'h-5 w-5',
|
|
24
26
|
},
|
|
25
27
|
lg: {
|
|
26
|
-
root: 'h-3',
|
|
28
|
+
root: 'h-3 data-[orientation=vertical]:w-3',
|
|
27
29
|
thumb: 'h-6 w-6',
|
|
28
30
|
},
|
|
29
31
|
},
|
|
@@ -140,7 +142,6 @@
|
|
|
140
142
|
onValueChange={handleValueChange}
|
|
141
143
|
onValueCommit={handleValueCommit}
|
|
142
144
|
class={cn(styles.root(), 'rounded-full bg-muted')}
|
|
143
|
-
aria-describedby={describedBy}
|
|
144
145
|
>
|
|
145
146
|
<SliderPrimitive.Range class={styles.range()} />
|
|
146
147
|
{#each value as thumbValue, i}
|
|
@@ -152,6 +153,7 @@
|
|
|
152
153
|
aria-valuemin={min}
|
|
153
154
|
aria-valuemax={max}
|
|
154
155
|
aria-valuetext={formatValue(thumbValue)}
|
|
156
|
+
aria-describedby={describedBy}
|
|
155
157
|
/>
|
|
156
158
|
{/each}
|
|
157
159
|
</SliderPrimitive.Root>
|
|
@@ -79,23 +79,21 @@
|
|
|
79
79
|
updateScrollShadows();
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
// Context for TabPanel children. Getters read the props directly, so the
|
|
83
|
+
// active panel is right on the first render and during SSR (an $effect
|
|
84
|
+
// would only fill it in after hydration).
|
|
85
|
+
setContext('tabs', {
|
|
86
|
+
get activeTab() {
|
|
87
|
+
return value;
|
|
88
|
+
},
|
|
89
|
+
get tabsId() {
|
|
90
|
+
return id;
|
|
91
|
+
},
|
|
90
92
|
});
|
|
91
93
|
|
|
92
|
-
// Set up context for TabPanel children (done once at component init)
|
|
93
|
-
setContext('tabs', tabsState);
|
|
94
|
-
|
|
95
94
|
function selectTab(tabId: string) {
|
|
96
95
|
if (tabs.find((t) => t.id === tabId && t.disabled)) return;
|
|
97
96
|
value = tabId;
|
|
98
|
-
tabsState.activeTab = tabId;
|
|
99
97
|
onValueChange?.(tabId);
|
|
100
98
|
}
|
|
101
99
|
|
|
@@ -6,17 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
type ToastType = 'success' | 'error' | 'warning' | 'info';
|
|
8
8
|
|
|
9
|
+
// Status tokens (light/dark aware) over an opaque card surface: the tint is a
|
|
10
|
+
// ::before layer so the toast never shows the page through it, and text stays
|
|
11
|
+
// foreground-on-card for contrast in both modes.
|
|
9
12
|
const toastVariants = tv({
|
|
10
|
-
base:
|
|
13
|
+
base: "group pointer-events-auto relative isolate flex w-full items-start gap-3 overflow-hidden rounded-lg border bg-card p-4 text-card-foreground shadow-lg transition-all before:absolute before:inset-0 before:-z-10 before:content-['']",
|
|
11
14
|
variants: {
|
|
12
15
|
type: {
|
|
13
|
-
success:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
warning:
|
|
18
|
-
'bg-yellow-50 text-yellow-800 border-yellow-200 dark:bg-yellow-900/20 dark:text-yellow-400 dark:border-yellow-800',
|
|
19
|
-
info: 'bg-blue-50 text-blue-800 border-blue-200 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-800',
|
|
16
|
+
success: 'border-success/40 before:bg-success/10',
|
|
17
|
+
error: 'border-destructive/40 before:bg-destructive/10',
|
|
18
|
+
warning: 'border-warning/40 before:bg-warning/10',
|
|
19
|
+
info: 'border-info/40 before:bg-info/10',
|
|
20
20
|
},
|
|
21
21
|
},
|
|
22
22
|
defaultVariants: {
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
const iconColors: Record<ToastType, string> = {
|
|
39
|
-
success: 'text-
|
|
40
|
-
error: 'text-
|
|
41
|
-
warning: 'text-
|
|
42
|
-
info: 'text-
|
|
39
|
+
success: 'text-success',
|
|
40
|
+
error: 'text-destructive',
|
|
41
|
+
warning: 'text-warning',
|
|
42
|
+
info: 'text-info',
|
|
43
43
|
};
|
|
44
44
|
|
|
45
45
|
interface Props {
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { DataTableInstance } from './createDataTable.svelte.js';
|
|
9
9
|
import DropdownMenu from '../DropdownMenu.svelte';
|
|
10
|
-
import Button from '../Button.svelte';
|
|
11
10
|
import { cn } from '../../utils.js';
|
|
12
11
|
|
|
13
12
|
interface Props {
|
|
@@ -19,13 +18,13 @@
|
|
|
19
18
|
|
|
20
19
|
let { table, class: className }: Props = $props();
|
|
21
20
|
|
|
22
|
-
//
|
|
21
|
+
// One checkbox item per column, checked while the column is visible
|
|
23
22
|
const menuItems = $derived.by(() => {
|
|
24
23
|
return table.allColumns.map((column) => {
|
|
25
|
-
const isVisible = !table.hiddenColumns.has(column.id);
|
|
26
24
|
return {
|
|
27
25
|
type: 'item' as const,
|
|
28
|
-
label:
|
|
26
|
+
label: column.header,
|
|
27
|
+
checked: !table.hiddenColumns.has(column.id),
|
|
29
28
|
onSelect: () => {
|
|
30
29
|
table.toggleColumnVisibility(column.id);
|
|
31
30
|
},
|
|
@@ -35,32 +34,31 @@
|
|
|
35
34
|
</script>
|
|
36
35
|
|
|
37
36
|
<div class={cn('relative', className)}>
|
|
37
|
+
<!-- DropdownMenu renders the trigger <button>; style it via triggerVariant
|
|
38
|
+
rather than nesting a Button inside it. -->
|
|
38
39
|
<DropdownMenu
|
|
39
40
|
items={[{ type: 'label', label: 'Toggle columns' }, { type: 'separator' }, ...menuItems]}
|
|
41
|
+
triggerVariant="outline"
|
|
42
|
+
triggerSize="sm"
|
|
43
|
+
triggerClass="ml-auto"
|
|
40
44
|
>
|
|
41
45
|
{#snippet trigger()}
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
stroke-linejoin="round"
|
|
59
|
-
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
60
|
-
/>
|
|
61
|
-
</svg>
|
|
62
|
-
Columns
|
|
63
|
-
</Button>
|
|
46
|
+
<svg
|
|
47
|
+
class="mr-2 h-4 w-4"
|
|
48
|
+
fill="none"
|
|
49
|
+
viewBox="0 0 24 24"
|
|
50
|
+
stroke="currentColor"
|
|
51
|
+
stroke-width="2"
|
|
52
|
+
aria-hidden="true"
|
|
53
|
+
>
|
|
54
|
+
<path
|
|
55
|
+
stroke-linecap="round"
|
|
56
|
+
stroke-linejoin="round"
|
|
57
|
+
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
|
58
|
+
/>
|
|
59
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
60
|
+
</svg>
|
|
61
|
+
Columns
|
|
64
62
|
{/snippet}
|
|
65
63
|
</DropdownMenu>
|
|
66
64
|
</div>
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Supports text, number, and select inputs.
|
|
7
7
|
* Works with the native Svelte 5 DataTable implementation.
|
|
8
8
|
*/
|
|
9
|
+
import { tick } from 'svelte';
|
|
9
10
|
import type { DataTableColumn } from './types.js';
|
|
10
11
|
import type { DataTableRow } from './createDataTable.svelte.js';
|
|
11
12
|
import { cn } from '../../utils.js';
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
|
|
30
31
|
let isEditing = $state(false);
|
|
31
32
|
let editValue = $state<string>('');
|
|
33
|
+
let buttonEl = $state<HTMLButtonElement | null>(null);
|
|
32
34
|
|
|
33
35
|
// Get the current display value
|
|
34
36
|
const displayValue = $derived.by(() => {
|
|
@@ -70,13 +72,21 @@
|
|
|
70
72
|
editValue = '';
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
// After a keyboard save/cancel the input is gone; return focus to the cell.
|
|
76
|
+
async function refocusCell() {
|
|
77
|
+
await tick();
|
|
78
|
+
buttonEl?.focus();
|
|
79
|
+
}
|
|
80
|
+
|
|
73
81
|
function handleKeydown(e: KeyboardEvent) {
|
|
74
82
|
if (e.key === 'Enter') {
|
|
75
83
|
e.preventDefault();
|
|
76
84
|
saveEdit();
|
|
85
|
+
refocusCell();
|
|
77
86
|
} else if (e.key === 'Escape') {
|
|
78
87
|
e.preventDefault();
|
|
79
88
|
cancelEdit();
|
|
89
|
+
refocusCell();
|
|
80
90
|
} else if (e.key === 'Tab') {
|
|
81
91
|
saveEdit();
|
|
82
92
|
}
|
|
@@ -91,29 +101,32 @@
|
|
|
91
101
|
}
|
|
92
102
|
</script>
|
|
93
103
|
|
|
94
|
-
{#if isEditable}
|
|
104
|
+
{#if isEditable && isEditing}
|
|
105
|
+
<!-- The input sits beside, not inside, the button: an input in a button is
|
|
106
|
+
invalid, and its Enter would bubble to the button and reopen the editor. -->
|
|
107
|
+
<div class={cn('relative w-full', className)}>
|
|
108
|
+
<input
|
|
109
|
+
use:autofocus
|
|
110
|
+
type="text"
|
|
111
|
+
value={editValue}
|
|
112
|
+
oninput={(e: Event) => (editValue = (e.target as HTMLInputElement).value)}
|
|
113
|
+
onkeydown={handleKeydown}
|
|
114
|
+
onblur={handleBlur}
|
|
115
|
+
class="h-7 w-full rounded border border-input bg-background px-2 text-base sm:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
{:else if isEditable}
|
|
95
119
|
<button
|
|
120
|
+
bind:this={buttonEl}
|
|
96
121
|
type="button"
|
|
97
122
|
class={cn('relative w-full text-left hover:bg-muted-hover', className)}
|
|
98
123
|
ondblclick={handleDoubleClick}
|
|
99
|
-
onkeydown={(e: KeyboardEvent) =>
|
|
124
|
+
onkeydown={(e: KeyboardEvent) => e.key === 'Enter' && startEditing()}
|
|
100
125
|
>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
value={editValue}
|
|
106
|
-
oninput={(e: Event) => (editValue = (e.target as HTMLInputElement).value)}
|
|
107
|
-
onkeydown={handleKeydown}
|
|
108
|
-
onblur={handleBlur}
|
|
109
|
-
class="h-7 w-full rounded border border-input bg-background px-2 text-base sm:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
110
|
-
/>
|
|
111
|
-
{:else}
|
|
112
|
-
<span class="border-b border-dashed border-transparent hover:border-primary">
|
|
113
|
-
{displayValue}
|
|
114
|
-
</span>
|
|
115
|
-
<span class="sr-only">(double-click to edit)</span>
|
|
116
|
-
{/if}
|
|
126
|
+
<span class="border-b border-dashed border-transparent hover:border-primary">
|
|
127
|
+
{displayValue}
|
|
128
|
+
</span>
|
|
129
|
+
<span class="sr-only">(double-click to edit)</span>
|
|
117
130
|
</button>
|
|
118
131
|
{:else}
|
|
119
132
|
<span class={cn('relative', className)}>
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DataTableEditableCell - Inline editable cell component
|
|
3
|
-
*
|
|
4
|
-
* Allows users to edit cell values in place.
|
|
5
|
-
* Supports text, number, and select inputs.
|
|
6
|
-
* Works with the native Svelte 5 DataTable implementation.
|
|
7
|
-
*/
|
|
8
1
|
import type { DataTableColumn } from './types.js';
|
|
9
2
|
import type { DataTableRow } from './createDataTable.svelte.js';
|
|
10
3
|
declare function $$render<T extends object>(): {
|
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
class: className,
|
|
29
29
|
}: Props = $props();
|
|
30
30
|
|
|
31
|
+
const uid = $props.id();
|
|
32
|
+
const pageSizeLabelId = `datatable-page-size-${uid}`;
|
|
33
|
+
|
|
31
34
|
function handlePageSizeChange(value: string) {
|
|
32
35
|
table.setPageSize(Number(value));
|
|
33
36
|
}
|
|
@@ -45,8 +48,9 @@
|
|
|
45
48
|
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 sm:gap-x-6 lg:gap-x-8">
|
|
46
49
|
<!-- Page size selector -->
|
|
47
50
|
<div class="flex items-center gap-2">
|
|
48
|
-
<span class="text-sm text-muted-foreground">Rows per page</span>
|
|
51
|
+
<span id={pageSizeLabelId} class="text-sm text-muted-foreground">Rows per page</span>
|
|
49
52
|
<Select
|
|
53
|
+
aria-labelledby={pageSizeLabelId}
|
|
50
54
|
value={String(table.pageSize)}
|
|
51
55
|
options={pageSizeOptions.map((size) => ({
|
|
52
56
|
value: String(size),
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
* ```svelte
|
|
15
15
|
* <DataTableVirtual {table} height="600px" rowHeight={48} />
|
|
16
16
|
* ```
|
|
17
|
+
*
|
|
18
|
+
* Create the table with `enablePagination: false` so `table.rows` holds every
|
|
19
|
+
* row rather than one page.
|
|
17
20
|
*/
|
|
18
21
|
import type { DataTableInstance, DataTableRow } from './createDataTable.svelte.js';
|
|
19
22
|
import type { DataTableColumn } from './types.js';
|
|
@@ -96,12 +99,6 @@
|
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
101
|
|
|
99
|
-
// Handle row selection
|
|
100
|
-
function handleSelectRow(rowId: string, e: MouseEvent) {
|
|
101
|
-
e.stopPropagation();
|
|
102
|
-
table.toggleRowSelection(rowId);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
102
|
// Handle header click for sorting
|
|
106
103
|
function handleHeaderClick(columnId: string, sortable: boolean | undefined) {
|
|
107
104
|
if (!sortable) return;
|
|
@@ -184,7 +181,7 @@
|
|
|
184
181
|
checked={row.isSelected}
|
|
185
182
|
onCheckedChange={() => table.toggleRowSelection(row.id)}
|
|
186
183
|
aria-label={`Select row ${index + 1}`}
|
|
187
|
-
onclick={(e: MouseEvent) =>
|
|
184
|
+
onclick={(e: MouseEvent) => e.stopPropagation()}
|
|
188
185
|
/>
|
|
189
186
|
</div>
|
|
190
187
|
{/if}
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
* ```svelte
|
|
14
14
|
* <DataTableVirtual {table} height="600px" rowHeight={48} />
|
|
15
15
|
* ```
|
|
16
|
+
*
|
|
17
|
+
* Create the table with `enablePagination: false` so `table.rows` holds every
|
|
18
|
+
* row rather than one page.
|
|
16
19
|
*/
|
|
17
20
|
import type { DataTableInstance } from './createDataTable.svelte.js';
|
|
18
21
|
declare function $$render<T extends object>(): {
|
|
@@ -32,7 +32,7 @@ export interface DataTableOptions<T> {
|
|
|
32
32
|
data: T[];
|
|
33
33
|
/** Column definitions */
|
|
34
34
|
columns: DataTableColumn<T>[];
|
|
35
|
-
/** Get unique row ID (defaults to index) */
|
|
35
|
+
/** Get unique row ID (defaults to the row's index in `data`) */
|
|
36
36
|
getRowId?: (row: T, index: number) => string;
|
|
37
37
|
/** Initial sort column ID */
|
|
38
38
|
initialSortColumn?: string;
|
|
@@ -54,6 +54,11 @@ export interface DataTableOptions<T> {
|
|
|
54
54
|
manualFiltering?: boolean;
|
|
55
55
|
/** Use server-side pagination (skip client-side pagination) */
|
|
56
56
|
manualPagination?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Slice rows into pages (default true). Set false to get every row in
|
|
59
|
+
* `rows`, e.g. for a DataTable without a pager or for DataTableVirtual.
|
|
60
|
+
*/
|
|
61
|
+
enablePagination?: boolean;
|
|
57
62
|
/** Total page count (required for manual pagination) */
|
|
58
63
|
pageCount?: number;
|
|
59
64
|
/** Callback when sort changes */
|
|
@@ -162,16 +162,26 @@ export function createDataTable(optionsFn) {
|
|
|
162
162
|
: Math.ceil(totalRows / pageSize) || 1);
|
|
163
163
|
// Clamp pageIndex when data changes
|
|
164
164
|
const clampedPageIndex = $derived(Math.min(Math.max(0, pageIndex), Math.max(0, totalPages - 1)));
|
|
165
|
-
const paginatedData = $derived(options.manualPagination
|
|
165
|
+
const paginatedData = $derived(options.manualPagination || options.enablePagination === false
|
|
166
166
|
? sortedData // In manual mode, data is already paginated from server
|
|
167
167
|
: sortedData.slice(clampedPageIndex * pageSize, (clampedPageIndex + 1) * pageSize));
|
|
168
168
|
// === ROW HELPERS ===
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
// Index of each row in the source data, so row ids (and with them the
|
|
170
|
+
// selection) follow the row rather than its position after sort/filter/page.
|
|
171
|
+
const sourceIndex = $derived.by(() => {
|
|
172
|
+
const map = new Map();
|
|
173
|
+
data.forEach((row, i) => {
|
|
174
|
+
if (!map.has(row))
|
|
175
|
+
map.set(row, i);
|
|
176
|
+
});
|
|
177
|
+
return map;
|
|
178
|
+
});
|
|
179
|
+
const rows = $derived(paginatedData.map((row) => {
|
|
180
|
+
const index = sourceIndex.get(row) ?? -1;
|
|
181
|
+
const id = getRowId(row, index);
|
|
172
182
|
return {
|
|
173
183
|
id,
|
|
174
|
-
index
|
|
184
|
+
index,
|
|
175
185
|
original: row,
|
|
176
186
|
isSelected: selectedRows.has(id),
|
|
177
187
|
};
|
|
@@ -248,14 +258,17 @@ export function createDataTable(optionsFn) {
|
|
|
248
258
|
}
|
|
249
259
|
function nextPage() {
|
|
250
260
|
if (clampedPageIndex < totalPages - 1) {
|
|
251
|
-
|
|
252
|
-
|
|
261
|
+
// Read before assigning: clampedPageIndex recomputes from pageIndex.
|
|
262
|
+
const next = clampedPageIndex + 1;
|
|
263
|
+
pageIndex = next;
|
|
264
|
+
options.onPaginationChange?.({ pageIndex: next, pageSize });
|
|
253
265
|
}
|
|
254
266
|
}
|
|
255
267
|
function previousPage() {
|
|
256
268
|
if (clampedPageIndex > 0) {
|
|
257
|
-
|
|
258
|
-
|
|
269
|
+
const previous = clampedPageIndex - 1;
|
|
270
|
+
pageIndex = previous;
|
|
271
|
+
options.onPaginationChange?.({ pageIndex: previous, pageSize });
|
|
259
272
|
}
|
|
260
273
|
}
|
|
261
274
|
function firstPage() {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
<!-- Skip Link for Accessibility -->
|
|
65
65
|
<a
|
|
66
66
|
href="#{skipToId}"
|
|
67
|
-
class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-
|
|
67
|
+
class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-80 focus:rounded-md focus:bg-primary focus:px-4 focus:py-2 focus:text-primary-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
|
68
68
|
>
|
|
69
69
|
{skipToText}
|
|
70
70
|
</a>
|