@classic-homes/theme-svelte 1.25.1 → 1.27.1
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/AlertDialog.svelte +14 -6
- package/dist/lib/components/AlertDialog.svelte.d.ts +8 -2
- package/dist/lib/components/Badge.svelte +6 -2
- package/dist/lib/components/CodeField.svelte +24 -8
- package/dist/lib/components/CodeField.svelte.d.ts +8 -0
- package/dist/lib/components/Combobox.svelte +25 -111
- package/dist/lib/components/DataPanel.svelte +31 -24
- package/dist/lib/components/DataPanelHeader.svelte +66 -26
- package/dist/lib/components/DataTable.svelte +303 -138
- package/dist/lib/components/DataTable.svelte.d.ts +37 -13
- package/dist/lib/components/DateTimePicker.svelte +159 -34
- package/dist/lib/components/Dialog.svelte +14 -6
- package/dist/lib/components/Dialog.svelte.d.ts +8 -2
- package/dist/lib/components/FileUpload.svelte +23 -6
- package/dist/lib/components/FileUpload.svelte.d.ts +7 -2
- package/dist/lib/components/MultiSelect.svelte +28 -155
- package/dist/lib/components/NumberInput.svelte +28 -110
- package/dist/lib/components/OTPInput.svelte +73 -85
- package/dist/lib/components/Select.svelte +24 -29
- package/dist/lib/components/Signature.svelte +80 -48
- package/dist/lib/components/Signature.svelte.d.ts +6 -1
- package/dist/lib/components/SkipLink.svelte +7 -2
- package/dist/lib/components/SkipLink.svelte.d.ts +2 -0
- package/dist/lib/components/Slider.svelte +4 -4
- package/dist/lib/components/Toast.svelte +11 -1
- package/dist/lib/components/ToastContainer.svelte +38 -24
- package/dist/lib/components/Tooltip.svelte +19 -6
- package/dist/lib/components/Tooltip.svelte.d.ts +8 -2
- package/dist/lib/components/data-table/DataTableColumnFilter.svelte +3 -1
- package/dist/lib/components/data-table/DataTableEditableCell.svelte +25 -3
- package/dist/lib/components/data-table/DataTableFacetedFilter.svelte +50 -40
- package/dist/lib/components/data-table/DataTableFacetedFilter.svelte.d.ts +0 -10
- package/dist/lib/components/data-table/DataTableVirtual.svelte +109 -57
- package/dist/lib/components/data-table/DataTableVirtual.svelte.d.ts +2 -0
- package/dist/lib/components/data-table/createDataTable.svelte.d.ts +26 -4
- package/dist/lib/components/data-table/createDataTable.svelte.js +97 -40
- package/dist/lib/components/data-table/index.d.ts +1 -1
- package/dist/lib/components/data-table/types.d.ts +14 -4
- package/dist/lib/components/data-table/types.js +5 -1
- package/dist/lib/components/internal/TriggerChild.svelte +60 -0
- package/dist/lib/components/internal/TriggerChild.svelte.d.ts +32 -0
- package/dist/lib/components/layout/Sidebar.svelte +33 -3
- package/dist/lib/components/layout/Sidebar.svelte.d.ts +1 -1
- package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte +6 -1
- package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte +6 -1
- package/dist/lib/components/layout/sidebar/SidebarSection.svelte +6 -1
- package/dist/lib/components/media/Audio.svelte +16 -0
- package/dist/lib/components/media/Lightbox.svelte +44 -9
- package/dist/lib/components/media/Lightbox.svelte.d.ts +9 -2
- package/dist/lib/components/media/MediaCarousel.svelte +19 -0
- package/dist/lib/components/media/MediaEmbed.svelte +32 -1
- package/dist/lib/components/media/Video.svelte +16 -0
- package/dist/lib/components/media/internal/MediaControls.svelte +9 -1
- package/dist/lib/components/notifications/NotificationBadge.svelte +1 -1
- package/dist/lib/components/notifications/NotificationItem.svelte +19 -28
- package/dist/lib/composables/useEscapeKey.svelte.d.ts +4 -0
- package/dist/lib/composables/useEscapeKey.svelte.js +15 -0
- package/dist/lib/composables/useHlsSource.svelte.js +31 -4
- package/dist/lib/composables/useMediaPlayer.svelte.js +38 -9
- package/dist/lib/composables/useMediaZoom.svelte.js +32 -0
- package/dist/lib/composables/usePersistedForm.svelte.d.ts +6 -2
- package/dist/lib/composables/usePersistedForm.svelte.js +7 -7
- package/dist/lib/composables/useReducedMotion.svelte.js +6 -1
- package/dist/lib/composables/useRovingTabindex.svelte.js +12 -0
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/stores/toast.svelte.d.ts +13 -0
- package/dist/lib/stores/toast.svelte.js +36 -5
- package/dist/lib/utils/media.d.ts +7 -0
- package/dist/lib/utils/media.js +18 -3
- package/package.json +1 -1
|
@@ -18,15 +18,20 @@
|
|
|
18
18
|
* Content of the skip link. Defaults to "Skip to main content".
|
|
19
19
|
*/
|
|
20
20
|
children?: Snippet;
|
|
21
|
+
/** Runs before the skip; `preventDefault()` cancels the focus move */
|
|
22
|
+
onclick?: (e: MouseEvent) => void;
|
|
21
23
|
/**
|
|
22
24
|
* Additional attributes
|
|
23
25
|
*/
|
|
24
26
|
[key: string]: unknown;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
let { targetId, class: className, children, ...restProps }: Props = $props();
|
|
29
|
+
let { targetId, class: className, children, onclick, ...restProps }: Props = $props();
|
|
28
30
|
|
|
29
31
|
function handleClick(e: MouseEvent) {
|
|
32
|
+
// A consumer onclick is chained, not spread over this handler
|
|
33
|
+
onclick?.(e);
|
|
34
|
+
if (e.defaultPrevented) return;
|
|
30
35
|
e.preventDefault();
|
|
31
36
|
const target = document.getElementById(targetId);
|
|
32
37
|
if (target) {
|
|
@@ -58,7 +63,7 @@
|
|
|
58
63
|
...
|
|
59
64
|
</main>
|
|
60
65
|
-->
|
|
61
|
-
<a href={`#${targetId}`} class={cn('skip-link', className)} onclick={handleClick}
|
|
66
|
+
<a href={`#${targetId}`} class={cn('skip-link', className)} {...restProps} onclick={handleClick}>
|
|
62
67
|
{#if children}
|
|
63
68
|
{@render children()}
|
|
64
69
|
{:else}
|
|
@@ -15,6 +15,8 @@ interface Props {
|
|
|
15
15
|
* Content of the skip link. Defaults to "Skip to main content".
|
|
16
16
|
*/
|
|
17
17
|
children?: Snippet;
|
|
18
|
+
/** Runs before the skip; `preventDefault()` cancels the focus move */
|
|
19
|
+
onclick?: (e: MouseEvent) => void;
|
|
18
20
|
/**
|
|
19
21
|
* Additional attributes
|
|
20
22
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Slider as SliderPrimitive } from 'bits-ui';
|
|
3
3
|
import { cn } from '../utils.js';
|
|
4
|
-
import {
|
|
4
|
+
import { computeDescribedBy } from '../utils/form.js';
|
|
5
5
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
6
6
|
import Label from './Label.svelte';
|
|
7
7
|
|
|
@@ -88,9 +88,9 @@
|
|
|
88
88
|
class: className,
|
|
89
89
|
}: Props = $props();
|
|
90
90
|
|
|
91
|
-
// Generate unique IDs for accessibility
|
|
92
|
-
const
|
|
93
|
-
const componentId = $derived(`slider-${
|
|
91
|
+
// Generate unique IDs for accessibility ($props.id() is SSR-stable)
|
|
92
|
+
const uid = $props.id();
|
|
93
|
+
const componentId = $derived(`slider-${uid}`);
|
|
94
94
|
const hintId = $derived(`${componentId}-hint`);
|
|
95
95
|
const errorId = $derived(`${componentId}-error`);
|
|
96
96
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { fly } from 'svelte/transition';
|
|
4
4
|
import { cn } from '../utils.js';
|
|
5
|
+
import { useReducedMotion } from '../composables/useReducedMotion.svelte.js';
|
|
5
6
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
6
7
|
|
|
7
8
|
type ToastType = 'success' | 'error' | 'warning' | 'info';
|
|
@@ -63,10 +64,19 @@
|
|
|
63
64
|
}: Props = $props();
|
|
64
65
|
|
|
65
66
|
const classes = $derived(cn(toastVariants({ type }), className));
|
|
67
|
+
|
|
68
|
+
// Svelte transitions run as Web Animations, which the CSS reduced-motion
|
|
69
|
+
// override does not reach, so gate the fly here
|
|
70
|
+
const reducedMotion = useReducedMotion();
|
|
66
71
|
const toastType = $derived(type as ToastType);
|
|
67
72
|
</script>
|
|
68
73
|
|
|
69
|
-
<div
|
|
74
|
+
<div
|
|
75
|
+
class={classes}
|
|
76
|
+
role="alert"
|
|
77
|
+
transition:fly={{ y: reducedMotion.value ? 0 : 50, duration: reducedMotion.value ? 0 : 300 }}
|
|
78
|
+
{...restProps}
|
|
79
|
+
>
|
|
70
80
|
<svg
|
|
71
81
|
class="h-5 w-5 flex-shrink-0 {iconColors[toastType]}"
|
|
72
82
|
fill="none"
|
|
@@ -30,27 +30,41 @@
|
|
|
30
30
|
const stackOrder = $derived(position.startsWith('bottom') ? 'flex-col' : 'flex-col-reverse');
|
|
31
31
|
</script>
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
{
|
|
33
|
+
<!-- The live region is always mounted: one that appears together with its
|
|
34
|
+
first toast is not announced. Only additions are read, not the whole stack
|
|
35
|
+
again. Errors are role="alert" (assertive); the rest are read politely. -->
|
|
36
|
+
<div
|
|
37
|
+
class={cn(
|
|
38
|
+
'fixed z-50 flex w-full max-w-sm gap-2 pointer-events-none',
|
|
39
|
+
stackOrder,
|
|
40
|
+
positionClasses[position],
|
|
41
|
+
className
|
|
42
|
+
)}
|
|
43
|
+
role="region"
|
|
44
|
+
aria-live="polite"
|
|
45
|
+
aria-relevant="additions text"
|
|
46
|
+
aria-label="Notifications"
|
|
47
|
+
{...restProps}
|
|
48
|
+
>
|
|
49
|
+
{#each toastStore.toasts as toast (toast.id)}
|
|
50
|
+
<!-- Hover or focus holds the auto-dismiss timer (WCAG 2.2.1) -->
|
|
51
|
+
<div
|
|
52
|
+
class="pointer-events-auto"
|
|
53
|
+
role="presentation"
|
|
54
|
+
onmouseenter={() => toastStore.pause(toast.id)}
|
|
55
|
+
onmouseleave={() => toastStore.resume(toast.id)}
|
|
56
|
+
onfocusin={() => toastStore.pause(toast.id)}
|
|
57
|
+
onfocusout={(e) => {
|
|
58
|
+
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) toastStore.resume(toast.id);
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<Toast
|
|
62
|
+
type={toast.type}
|
|
63
|
+
title={toast.title}
|
|
64
|
+
message={toast.message}
|
|
65
|
+
role={toast.type === 'error' ? 'alert' : undefined}
|
|
66
|
+
onclose={() => toastStore.remove(toast.id)}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
{/each}
|
|
70
|
+
</div>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Tooltip as TooltipPrimitive } from 'bits-ui';
|
|
3
3
|
import { cn } from '../utils.js';
|
|
4
4
|
import type { Snippet } from 'svelte';
|
|
5
|
+
import TriggerChild, { type TriggerSnippetProps } from './internal/TriggerChild.svelte';
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
7
8
|
/** Content to display in the tooltip */
|
|
@@ -14,8 +15,13 @@
|
|
|
14
15
|
delayDuration?: number;
|
|
15
16
|
/** Additional class for tooltip content */
|
|
16
17
|
class?: string;
|
|
17
|
-
/**
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Trigger element. It receives `{ props }`: spread them on your own
|
|
20
|
+
* focusable element (`<Button {...props}>`) so it is the one that opens
|
|
21
|
+
* and is described by the tooltip. A snippet that ignores them still
|
|
22
|
+
* works, wrapped in a focusable span as before.
|
|
23
|
+
*/
|
|
24
|
+
children: Snippet<[TriggerSnippetProps]>;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
let {
|
|
@@ -26,15 +32,22 @@
|
|
|
26
32
|
class: className,
|
|
27
33
|
children,
|
|
28
34
|
}: Props = $props();
|
|
35
|
+
|
|
36
|
+
const uid = $props.id();
|
|
37
|
+
// Changes once if the trigger snippet ignores its props (see TriggerChild)
|
|
38
|
+
let triggerId = $state(`${uid}-trigger`);
|
|
29
39
|
</script>
|
|
30
40
|
|
|
31
41
|
<TooltipPrimitive.Provider>
|
|
32
42
|
<TooltipPrimitive.Root {delayDuration}>
|
|
33
|
-
<TooltipPrimitive.Trigger>
|
|
43
|
+
<TooltipPrimitive.Trigger id={triggerId}>
|
|
34
44
|
{#snippet child({ props })}
|
|
35
|
-
<
|
|
36
|
-
{
|
|
37
|
-
|
|
45
|
+
<TriggerChild
|
|
46
|
+
{props}
|
|
47
|
+
trigger={children}
|
|
48
|
+
fallbackClass="inline-flex"
|
|
49
|
+
onfallback={() => (triggerId = `${uid}-trigger-wrap`)}
|
|
50
|
+
/>
|
|
38
51
|
{/snippet}
|
|
39
52
|
</TooltipPrimitive.Trigger>
|
|
40
53
|
<TooltipPrimitive.Portal>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
import { type TriggerSnippetProps } from './internal/TriggerChild.svelte';
|
|
2
3
|
interface Props {
|
|
3
4
|
/** Content to display in the tooltip */
|
|
4
5
|
content: string;
|
|
@@ -10,8 +11,13 @@ interface Props {
|
|
|
10
11
|
delayDuration?: number;
|
|
11
12
|
/** Additional class for tooltip content */
|
|
12
13
|
class?: string;
|
|
13
|
-
/**
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Trigger element. It receives `{ props }`: spread them on your own
|
|
16
|
+
* focusable element (`<Button {...props}>`) so it is the one that opens
|
|
17
|
+
* and is described by the tooltip. A snippet that ignores them still
|
|
18
|
+
* works, wrapped in a focusable span as before.
|
|
19
|
+
*/
|
|
20
|
+
children: Snippet<[TriggerSnippetProps]>;
|
|
15
21
|
}
|
|
16
22
|
declare const Tooltip: import("svelte").Component<Props, {}, "">;
|
|
17
23
|
type Tooltip = ReturnType<typeof Tooltip>;
|
|
@@ -45,7 +45,9 @@
|
|
|
45
45
|
// Sync with external filter changes (controlled mode)
|
|
46
46
|
// This also handles initial value synchronization
|
|
47
47
|
$effect(() => {
|
|
48
|
-
const
|
|
48
|
+
const raw = table.columnFilters.get(columnId);
|
|
49
|
+
// A faceted (array) filter has no text form; leave the input empty
|
|
50
|
+
const externalFilter = typeof raw === 'string' ? raw : '';
|
|
49
51
|
// Only sync if not currently typing (debounce timer not active)
|
|
50
52
|
// Always sync on first run to get initial value
|
|
51
53
|
if (!isInitialized || (!debounceTimer && externalFilter !== inputValue)) {
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* DataTableEditableCell - Inline editable cell component
|
|
4
4
|
*
|
|
5
5
|
* Allows users to edit cell values in place.
|
|
6
|
-
*
|
|
6
|
+
* Numeric cells (a number value, or `meta.editType: 'number'`) get a number
|
|
7
|
+
* input and save a number (or null when emptied); other cells save a string.
|
|
8
|
+
* `onSave` only fires when the value actually changed.
|
|
7
9
|
* Works with the native Svelte 5 DataTable implementation.
|
|
8
10
|
*/
|
|
9
11
|
import { tick } from 'svelte';
|
|
@@ -28,6 +30,10 @@
|
|
|
28
30
|
const meta = $derived(column.meta as Record<string, unknown> | undefined);
|
|
29
31
|
const isEditable = $derived(column.enableEditing === true || meta?.enableEditing === true);
|
|
30
32
|
|
|
33
|
+
const isNumeric = $derived(
|
|
34
|
+
meta?.editType === 'number' || typeof getColumnValue(row.original, column) === 'number'
|
|
35
|
+
);
|
|
36
|
+
|
|
31
37
|
let isEditing = $state(false);
|
|
32
38
|
let editValue = $state<string>('');
|
|
33
39
|
let buttonEl = $state<HTMLButtonElement | null>(null);
|
|
@@ -61,10 +67,26 @@
|
|
|
61
67
|
isEditing = true;
|
|
62
68
|
}
|
|
63
69
|
|
|
70
|
+
/** The edited text as the column's type; undefined when it is not valid. */
|
|
71
|
+
function parseEditValue(): unknown {
|
|
72
|
+
if (!isNumeric) return editValue;
|
|
73
|
+
if (!editValue.trim()) return null;
|
|
74
|
+
const n = Number(editValue);
|
|
75
|
+
return Number.isFinite(n) ? n : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
64
78
|
function saveEdit() {
|
|
65
79
|
if (!isEditing) return;
|
|
66
|
-
onSave(row.id, column.id, editValue, row.original);
|
|
67
80
|
isEditing = false;
|
|
81
|
+
const original = getColumnValue(row.original, column);
|
|
82
|
+
const next = parseEditValue();
|
|
83
|
+
// Invalid input keeps the old value; an unchanged one is not a save
|
|
84
|
+
if (next === undefined) return;
|
|
85
|
+
const unchanged = isNumeric
|
|
86
|
+
? Object.is(next, original ?? null)
|
|
87
|
+
: next === String(original ?? '');
|
|
88
|
+
if (unchanged) return;
|
|
89
|
+
onSave(row.id, column.id, next, row.original);
|
|
68
90
|
}
|
|
69
91
|
|
|
70
92
|
function cancelEdit() {
|
|
@@ -107,7 +129,7 @@
|
|
|
107
129
|
<div class={cn('relative w-full', className)}>
|
|
108
130
|
<input
|
|
109
131
|
use:autofocus
|
|
110
|
-
type=
|
|
132
|
+
type={isNumeric ? 'number' : 'text'}
|
|
111
133
|
value={editValue}
|
|
112
134
|
oninput={(e: Event) => (editValue = (e.target as HTMLInputElement).value)}
|
|
113
135
|
onkeydown={handleKeydown}
|
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* DataTableFacetedFilter - Multi-select filter for categorical columns
|
|
4
4
|
*
|
|
5
|
-
* Allows filtering by selecting multiple values from a list.
|
|
6
|
-
*
|
|
5
|
+
* Allows filtering by selecting multiple values from a list. The column
|
|
6
|
+
* filter is stored as a string[] and matched exactly, so "active" never
|
|
7
|
+
* matches "inactive". A legacy comma-separated string is still read.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* from the main table's column filter state.
|
|
9
|
+
* The options open in a Bits UI popover; each option is a single
|
|
10
|
+
* `role="checkbox"` button (no nested controls).
|
|
11
11
|
*/
|
|
12
|
+
import { Popover } from 'bits-ui';
|
|
12
13
|
import type { DataTableInstance } from './createDataTable.svelte.js';
|
|
13
14
|
import Button from '../Button.svelte';
|
|
14
15
|
import Badge from '../Badge.svelte';
|
|
15
|
-
import
|
|
16
|
+
import { buttonVariants } from '../button-variants.js';
|
|
16
17
|
import { cn } from '../../utils.js';
|
|
17
18
|
|
|
18
19
|
interface FilterOption {
|
|
@@ -36,11 +37,12 @@
|
|
|
36
37
|
|
|
37
38
|
let { table, columnId, title, options, class: className }: Props = $props();
|
|
38
39
|
|
|
39
|
-
//
|
|
40
|
+
// Current selected values from the column filter
|
|
40
41
|
const selectedValues = $derived.by(() => {
|
|
41
42
|
const filterValue = table.columnFilters.get(columnId);
|
|
42
43
|
if (!filterValue) return new Set<string>();
|
|
43
|
-
|
|
44
|
+
if (Array.isArray(filterValue)) return new Set(filterValue);
|
|
45
|
+
// Legacy: comma-separated string
|
|
44
46
|
return new Set(filterValue.split(',').filter(Boolean));
|
|
45
47
|
});
|
|
46
48
|
|
|
@@ -53,34 +55,19 @@
|
|
|
53
55
|
} else {
|
|
54
56
|
newValues.add(value);
|
|
55
57
|
}
|
|
56
|
-
|
|
57
|
-
table.setColumnFilter(columnId, Array.from(newValues).join(','));
|
|
58
|
+
table.setColumnFilter(columnId, Array.from(newValues));
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
function clearAll() {
|
|
61
|
-
table.setColumnFilter(columnId,
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function handleClickOutside(event: MouseEvent) {
|
|
65
|
-
const target = event.target as HTMLElement;
|
|
66
|
-
if (!target.closest('.faceted-filter-container')) {
|
|
67
|
-
isOpen = false;
|
|
68
|
-
}
|
|
62
|
+
table.setColumnFilter(columnId, []);
|
|
69
63
|
}
|
|
70
64
|
|
|
71
65
|
const selectedCount = $derived(selectedValues.size);
|
|
72
66
|
</script>
|
|
73
67
|
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
<Button
|
|
78
|
-
variant="outline"
|
|
79
|
-
size="sm"
|
|
80
|
-
class="border-dashed"
|
|
81
|
-
onclick={() => (isOpen = !isOpen)}
|
|
82
|
-
aria-expanded={isOpen}
|
|
83
|
-
aria-haspopup="listbox"
|
|
68
|
+
<Popover.Root bind:open={isOpen}>
|
|
69
|
+
<Popover.Trigger
|
|
70
|
+
class={cn(buttonVariants({ variant: 'outline', size: 'sm' }), 'border-dashed', className)}
|
|
84
71
|
aria-label={`Filter by ${title}`}
|
|
85
72
|
>
|
|
86
73
|
<svg
|
|
@@ -103,23 +90,46 @@
|
|
|
103
90
|
{selectedCount}
|
|
104
91
|
</Badge>
|
|
105
92
|
{/if}
|
|
106
|
-
</
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
93
|
+
</Popover.Trigger>
|
|
94
|
+
<Popover.Portal>
|
|
95
|
+
<Popover.Content
|
|
96
|
+
align="start"
|
|
97
|
+
sideOffset={8}
|
|
98
|
+
collisionPadding={8}
|
|
99
|
+
class="z-50 min-w-[200px] rounded-md border bg-popover p-2 text-popover-foreground shadow-md"
|
|
111
100
|
>
|
|
112
|
-
<div class="space-y-1">
|
|
101
|
+
<div class="space-y-1" role="group" aria-label={title}>
|
|
113
102
|
{#each options as option (option.value)}
|
|
103
|
+
{@const checked = selectedValues.has(option.value)}
|
|
114
104
|
<button
|
|
115
105
|
type="button"
|
|
106
|
+
role="checkbox"
|
|
107
|
+
aria-checked={checked}
|
|
116
108
|
class={cn(
|
|
117
|
-
'flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent',
|
|
118
|
-
|
|
109
|
+
'flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
110
|
+
checked && 'bg-accent'
|
|
119
111
|
)}
|
|
120
112
|
onclick={() => toggleOption(option.value)}
|
|
121
113
|
>
|
|
122
|
-
<
|
|
114
|
+
<span
|
|
115
|
+
class={cn(
|
|
116
|
+
'flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border border-primary',
|
|
117
|
+
checked && 'bg-primary text-primary-foreground'
|
|
118
|
+
)}
|
|
119
|
+
aria-hidden="true"
|
|
120
|
+
>
|
|
121
|
+
{#if checked}
|
|
122
|
+
<svg
|
|
123
|
+
class="h-3 w-3"
|
|
124
|
+
fill="none"
|
|
125
|
+
viewBox="0 0 24 24"
|
|
126
|
+
stroke="currentColor"
|
|
127
|
+
stroke-width="3"
|
|
128
|
+
>
|
|
129
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
|
130
|
+
</svg>
|
|
131
|
+
{/if}
|
|
132
|
+
</span>
|
|
123
133
|
<span class="flex-1 text-left">{option.label}</span>
|
|
124
134
|
{#if option.count !== undefined}
|
|
125
135
|
<span class="text-muted-foreground">{option.count}</span>
|
|
@@ -133,6 +143,6 @@
|
|
|
133
143
|
<Button variant="ghost" size="sm" class="w-full" onclick={clearAll}>Clear filters</Button>
|
|
134
144
|
</div>
|
|
135
145
|
{/if}
|
|
136
|
-
</
|
|
137
|
-
|
|
138
|
-
</
|
|
146
|
+
</Popover.Content>
|
|
147
|
+
</Popover.Portal>
|
|
148
|
+
</Popover.Root>
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DataTableFacetedFilter - Multi-select filter for categorical columns
|
|
3
|
-
*
|
|
4
|
-
* Allows filtering by selecting multiple values from a list.
|
|
5
|
-
* Works with the native Svelte 5 DataTable implementation.
|
|
6
|
-
*
|
|
7
|
-
* Note: This component manages its own filter state independently,
|
|
8
|
-
* suitable for use cases where you want faceted filtering separate
|
|
9
|
-
* from the main table's column filter state.
|
|
10
|
-
*/
|
|
11
1
|
import type { DataTableInstance } from './createDataTable.svelte.js';
|
|
12
2
|
declare function $$render<TData extends object>(): {
|
|
13
3
|
props: {
|