@classic-homes/theme-svelte 1.25.0 → 1.26.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/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 +74 -86
- 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 +29 -3
- 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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { AlertDialog as AlertDialogPrimitive } 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
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
6
7
|
|
|
7
8
|
const buttonVariants = tv({
|
|
@@ -41,8 +42,13 @@
|
|
|
41
42
|
onCancel?: () => void;
|
|
42
43
|
/** Additional class for dialog content */
|
|
43
44
|
class?: string;
|
|
44
|
-
/**
|
|
45
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Optional trigger element. It receives `{ props }`: spread them on your
|
|
47
|
+
* own button (`{#snippet trigger({ props })}<Button {...props}>Open</Button>{/snippet}`)
|
|
48
|
+
* so that button is the trigger and gets focus back on close. A snippet
|
|
49
|
+
* that ignores them still works, wrapped in a span trigger as before.
|
|
50
|
+
*/
|
|
51
|
+
trigger?: Snippet<[TriggerSnippetProps]>;
|
|
46
52
|
/** Optional custom content */
|
|
47
53
|
children?: Snippet;
|
|
48
54
|
}
|
|
@@ -62,6 +68,10 @@
|
|
|
62
68
|
children,
|
|
63
69
|
}: Props = $props();
|
|
64
70
|
|
|
71
|
+
const uid = $props.id();
|
|
72
|
+
// Changes once if the trigger snippet ignores its props (see TriggerChild)
|
|
73
|
+
let triggerId = $state(`${uid}-trigger`);
|
|
74
|
+
|
|
65
75
|
// Each time the dialog opens, exactly one of onConfirm / onCancel runs when it
|
|
66
76
|
// closes. Escape and overlay clicks count as cancel, so callers that mount the
|
|
67
77
|
// dialog as `{#if x}<AlertDialog open onCancel={() => (x = null)} />` reset
|
|
@@ -99,11 +109,9 @@
|
|
|
99
109
|
|
|
100
110
|
<AlertDialogPrimitive.Root bind:open onOpenChange={handleOpenChange}>
|
|
101
111
|
{#if trigger}
|
|
102
|
-
<AlertDialogPrimitive.Trigger>
|
|
112
|
+
<AlertDialogPrimitive.Trigger id={triggerId}>
|
|
103
113
|
{#snippet child({ props })}
|
|
104
|
-
<
|
|
105
|
-
{@render trigger()}
|
|
106
|
-
</span>
|
|
114
|
+
<TriggerChild {props} {trigger} onfallback={() => (triggerId = `${uid}-trigger-wrap`)} />
|
|
107
115
|
{/snippet}
|
|
108
116
|
</AlertDialogPrimitive.Trigger>
|
|
109
117
|
{/if}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
import { type TriggerSnippetProps } from './internal/TriggerChild.svelte';
|
|
2
3
|
import { type VariantProps } from 'tailwind-variants';
|
|
3
4
|
declare const AlertDialog: import("svelte").Component<{
|
|
4
5
|
/** Whether the dialog is open */
|
|
@@ -70,8 +71,13 @@ declare const AlertDialog: import("svelte").Component<{
|
|
|
70
71
|
onCancel?: () => void;
|
|
71
72
|
/** Additional class for dialog content */
|
|
72
73
|
class?: string;
|
|
73
|
-
/**
|
|
74
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Optional trigger element. It receives `{ props }`: spread them on your
|
|
76
|
+
* own button (`{#snippet trigger({ props })}<Button {...props}>Open</Button>{/snippet}`)
|
|
77
|
+
* so that button is the trigger and gets focus back on close. A snippet
|
|
78
|
+
* that ignores them still works, wrapped in a span trigger as before.
|
|
79
|
+
*/
|
|
80
|
+
trigger?: Snippet<[TriggerSnippetProps]>;
|
|
75
81
|
/** Optional custom content */
|
|
76
82
|
children?: Snippet;
|
|
77
83
|
}, {}, "open">;
|
|
@@ -77,10 +77,14 @@
|
|
|
77
77
|
...restProps
|
|
78
78
|
}: Props = $props();
|
|
79
79
|
|
|
80
|
-
const classes = $derived(cn(badgeVariants({ variant, size, clickable }), className));
|
|
81
|
-
|
|
82
80
|
// Determine if badge is interactive (has click handler or clickable prop)
|
|
83
81
|
const isInteractive = $derived(clickable || !!onclick);
|
|
82
|
+
|
|
83
|
+
// An onclick-only badge renders a <button>, so it gets the same hover and
|
|
84
|
+
// focus styles as clickable
|
|
85
|
+
const classes = $derived(
|
|
86
|
+
cn(badgeVariants({ variant, size, clickable: isInteractive }), className)
|
|
87
|
+
);
|
|
84
88
|
</script>
|
|
85
89
|
|
|
86
90
|
{#if isInteractive}
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
'aria-invalid'?: boolean | 'true' | 'false';
|
|
29
29
|
class?: string;
|
|
30
30
|
onValueChange?: (value: string) => void;
|
|
31
|
+
/** Chained after the field's own input handling (bind:value keeps working) */
|
|
32
|
+
oninput?: (event: Event & { currentTarget: HTMLTextAreaElement }) => void;
|
|
33
|
+
/** Chained before the field's Tab handling; `preventDefault()` skips it */
|
|
34
|
+
onkeydown?: (event: KeyboardEvent & { currentTarget: HTMLTextAreaElement }) => void;
|
|
31
35
|
[key: string]: unknown;
|
|
32
36
|
}
|
|
33
37
|
|
|
@@ -46,6 +50,8 @@
|
|
|
46
50
|
'aria-invalid': ariaInvalid,
|
|
47
51
|
class: className,
|
|
48
52
|
onValueChange,
|
|
53
|
+
oninput,
|
|
54
|
+
onkeydown,
|
|
49
55
|
...restProps
|
|
50
56
|
}: Props = $props();
|
|
51
57
|
|
|
@@ -63,15 +69,18 @@
|
|
|
63
69
|
* Replace a range through the browser's editing pipeline when it can, so
|
|
64
70
|
* the change lands on the native undo stack.
|
|
65
71
|
*/
|
|
66
|
-
function replace(target: HTMLTextAreaElement, start: number, end: number, text: string) {
|
|
72
|
+
function replace(target: HTMLTextAreaElement, start: number, end: number, text: string): boolean {
|
|
67
73
|
target.setSelectionRange(start, end);
|
|
68
74
|
const viaEditing =
|
|
69
75
|
typeof document.execCommand === 'function' && document.execCommand('insertText', false, text);
|
|
70
76
|
if (!viaEditing) target.setRangeText(text, start, end, 'end');
|
|
77
|
+
return viaEditing;
|
|
71
78
|
}
|
|
72
79
|
|
|
73
|
-
function handleKeydown(event: KeyboardEvent) {
|
|
74
|
-
|
|
80
|
+
function handleKeydown(event: KeyboardEvent & { currentTarget: HTMLTextAreaElement }) {
|
|
81
|
+
onkeydown?.(event);
|
|
82
|
+
if (event.defaultPrevented) return;
|
|
83
|
+
const target = event.currentTarget;
|
|
75
84
|
if (event.key === 'Escape') {
|
|
76
85
|
releaseTab = true;
|
|
77
86
|
return;
|
|
@@ -91,8 +100,10 @@
|
|
|
91
100
|
const lineStart = text.lastIndexOf('\n', start - 1) + 1;
|
|
92
101
|
const multiline = text.slice(start, end).includes('\n');
|
|
93
102
|
|
|
103
|
+
// execCommand fires an input event (which emits); setRangeText does not
|
|
104
|
+
let viaEditing: boolean;
|
|
94
105
|
if (!event.shiftKey && !multiline) {
|
|
95
|
-
replace(target, start, end, pad);
|
|
106
|
+
viaEditing = replace(target, start, end, pad);
|
|
96
107
|
} else {
|
|
97
108
|
// Indent or outdent every line the selection touches.
|
|
98
109
|
const lineEnd = end > start && text[end - 1] === '\n' ? end - 1 : end;
|
|
@@ -103,13 +114,18 @@
|
|
|
103
114
|
const firstLineShift = event.shiftKey
|
|
104
115
|
? -(block.match(new RegExp(`^ {1,${indent}}`))?.[0].length ?? 0)
|
|
105
116
|
: indent;
|
|
106
|
-
replace(target, lineStart, lineEnd, changed);
|
|
117
|
+
viaEditing = replace(target, lineStart, lineEnd, changed);
|
|
107
118
|
target.setSelectionRange(
|
|
108
119
|
Math.max(lineStart, start + firstLineShift),
|
|
109
120
|
lineEnd + (changed.length - block.length)
|
|
110
121
|
);
|
|
111
122
|
}
|
|
112
|
-
emit(target);
|
|
123
|
+
if (!viaEditing) emit(target);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function handleInput(event: Event & { currentTarget: HTMLTextAreaElement }) {
|
|
127
|
+
emit(event.currentTarget);
|
|
128
|
+
oninput?.(event);
|
|
113
129
|
}
|
|
114
130
|
</script>
|
|
115
131
|
|
|
@@ -136,8 +152,8 @@
|
|
|
136
152
|
: '',
|
|
137
153
|
className
|
|
138
154
|
)}
|
|
139
|
-
oninput={(event) => emit(event.currentTarget)}
|
|
140
|
-
onkeydown={handleKeydown}
|
|
141
155
|
{...restProps}
|
|
156
|
+
oninput={handleInput}
|
|
157
|
+
onkeydown={handleKeydown}
|
|
142
158
|
></textarea>
|
|
143
159
|
<span id={hintId} class="sr-only">Tab indents. Press Escape, then Tab, to leave the field.</span>
|
|
@@ -25,6 +25,14 @@ interface Props {
|
|
|
25
25
|
'aria-invalid'?: boolean | 'true' | 'false';
|
|
26
26
|
class?: string;
|
|
27
27
|
onValueChange?: (value: string) => void;
|
|
28
|
+
/** Chained after the field's own input handling (bind:value keeps working) */
|
|
29
|
+
oninput?: (event: Event & {
|
|
30
|
+
currentTarget: HTMLTextAreaElement;
|
|
31
|
+
}) => void;
|
|
32
|
+
/** Chained before the field's Tab handling; `preventDefault()` skips it */
|
|
33
|
+
onkeydown?: (event: KeyboardEvent & {
|
|
34
|
+
currentTarget: HTMLTextAreaElement;
|
|
35
|
+
}) => void;
|
|
28
36
|
[key: string]: unknown;
|
|
29
37
|
}
|
|
30
38
|
declare const CodeField: import("svelte").Component<Props, {}, "value">;
|
|
@@ -123,9 +123,6 @@
|
|
|
123
123
|
};
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
-
// Determine if we need a container wrapper
|
|
127
|
-
const hasWrapper = $derived(!!label || !!hint || !!error);
|
|
128
|
-
|
|
129
126
|
// Generate announcement message for screen readers
|
|
130
127
|
const resultsAnnouncement = $derived.by(() => {
|
|
131
128
|
if (loading) return 'Loading options...';
|
|
@@ -136,114 +133,19 @@
|
|
|
136
133
|
});
|
|
137
134
|
</script>
|
|
138
135
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
<ComboboxPrimitive.Root
|
|
152
|
-
type="single"
|
|
153
|
-
{disabled}
|
|
154
|
-
{required}
|
|
155
|
-
{name}
|
|
156
|
-
bind:open
|
|
157
|
-
onOpenChange={handleOpenChange}
|
|
158
|
-
onValueChange={handleValueChange}
|
|
159
|
-
{value}
|
|
160
|
-
>
|
|
161
|
-
<div class="relative">
|
|
162
|
-
<ComboboxPrimitive.Input
|
|
163
|
-
id={componentId}
|
|
164
|
-
placeholder={selectedLabel || placeholder}
|
|
165
|
-
oninput={handleSearchInput}
|
|
166
|
-
onfocus={() => (open = true)}
|
|
167
|
-
onclick={() => (open = true)}
|
|
168
|
-
class={cn(
|
|
169
|
-
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background text-foreground px-3 py-2 text-base sm:text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
170
|
-
error && 'border-destructive focus:ring-destructive',
|
|
171
|
-
className
|
|
172
|
-
)}
|
|
173
|
-
role="combobox"
|
|
174
|
-
aria-expanded={open}
|
|
175
|
-
aria-invalid={error ? 'true' : undefined}
|
|
176
|
-
aria-describedby={describedBy}
|
|
177
|
-
/>
|
|
178
|
-
</div>
|
|
179
|
-
|
|
180
|
-
<!-- Portal to <body>: escapes transformed/overflow-hidden ancestors
|
|
181
|
-
(e.g. Dialog panels) that would clip the dropdown. -->
|
|
182
|
-
<ComboboxPrimitive.Portal>
|
|
183
|
-
<ComboboxPrimitive.Content
|
|
184
|
-
class="isolate relative z-50 min-w-[var(--bits-floating-anchor-width)] overflow-hidden rounded-md border border-border bg-popover 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"
|
|
185
|
-
sideOffset={4}
|
|
186
|
-
collisionPadding={8}
|
|
187
|
-
avoidCollisions={true}
|
|
188
|
-
>
|
|
189
|
-
<div class="p-1 max-h-72 overflow-y-auto">
|
|
190
|
-
{#if loading}
|
|
191
|
-
<div class="flex items-center justify-center py-6">
|
|
192
|
-
<Spinner size="sm" />
|
|
193
|
-
<span class="ml-2 text-sm text-muted-foreground">Loading...</span>
|
|
194
|
-
</div>
|
|
195
|
-
{:else if filteredOptions.length === 0}
|
|
196
|
-
<div class="py-6 text-center text-sm text-muted-foreground">
|
|
197
|
-
{emptyMessage}
|
|
198
|
-
</div>
|
|
199
|
-
{:else}
|
|
200
|
-
{#each filteredOptions as option}
|
|
201
|
-
<ComboboxPrimitive.Item
|
|
202
|
-
value={option.value}
|
|
203
|
-
label={option.label}
|
|
204
|
-
disabled={option.disabled}
|
|
205
|
-
class="relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default"
|
|
206
|
-
>
|
|
207
|
-
{#if option.value === value}
|
|
208
|
-
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
209
|
-
<svg
|
|
210
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
211
|
-
width="16"
|
|
212
|
-
height="16"
|
|
213
|
-
viewBox="0 0 24 24"
|
|
214
|
-
fill="none"
|
|
215
|
-
stroke="currentColor"
|
|
216
|
-
stroke-width="2"
|
|
217
|
-
stroke-linecap="round"
|
|
218
|
-
stroke-linejoin="round"
|
|
219
|
-
class="h-4 w-4"
|
|
220
|
-
>
|
|
221
|
-
<polyline points="20 6 9 17 4 12" />
|
|
222
|
-
</svg>
|
|
223
|
-
</span>
|
|
224
|
-
{/if}
|
|
225
|
-
{option.label}
|
|
226
|
-
</ComboboxPrimitive.Item>
|
|
227
|
-
{/each}
|
|
228
|
-
{/if}
|
|
229
|
-
</div>
|
|
230
|
-
</ComboboxPrimitive.Content>
|
|
231
|
-
</ComboboxPrimitive.Portal>
|
|
232
|
-
</ComboboxPrimitive.Root>
|
|
136
|
+
<!-- One structure whether or not there is a label, hint or error, so the
|
|
137
|
+
focused control is never remounted when an error appears. -->
|
|
138
|
+
<div class="space-y-2">
|
|
139
|
+
{#if label}
|
|
140
|
+
<Label for={componentId} {disabled}>
|
|
141
|
+
{label}
|
|
142
|
+
{#if required}
|
|
143
|
+
<span class="text-destructive ml-0.5" aria-hidden="true">*</span>
|
|
144
|
+
<span class="sr-only">(required)</span>
|
|
145
|
+
{/if}
|
|
146
|
+
</Label>
|
|
147
|
+
{/if}
|
|
233
148
|
|
|
234
|
-
{#if hint && !error}
|
|
235
|
-
<p id={hintId} class="text-sm text-muted-foreground">
|
|
236
|
-
{hint}
|
|
237
|
-
</p>
|
|
238
|
-
{/if}
|
|
239
|
-
|
|
240
|
-
{#if error}
|
|
241
|
-
<p id={errorId} class="text-sm text-destructive" role="alert" aria-live="polite">
|
|
242
|
-
{error}
|
|
243
|
-
</p>
|
|
244
|
-
{/if}
|
|
245
|
-
</div>
|
|
246
|
-
{:else}
|
|
247
149
|
<ComboboxPrimitive.Root
|
|
248
150
|
type="single"
|
|
249
151
|
{disabled}
|
|
@@ -326,7 +228,19 @@
|
|
|
326
228
|
</ComboboxPrimitive.Content>
|
|
327
229
|
</ComboboxPrimitive.Portal>
|
|
328
230
|
</ComboboxPrimitive.Root>
|
|
329
|
-
|
|
231
|
+
|
|
232
|
+
{#if hint && !error}
|
|
233
|
+
<p id={hintId} class="text-sm text-muted-foreground">
|
|
234
|
+
{hint}
|
|
235
|
+
</p>
|
|
236
|
+
{/if}
|
|
237
|
+
|
|
238
|
+
{#if error}
|
|
239
|
+
<p id={errorId} class="text-sm text-destructive" role="alert" aria-live="polite">
|
|
240
|
+
{error}
|
|
241
|
+
</p>
|
|
242
|
+
{/if}
|
|
243
|
+
</div>
|
|
330
244
|
|
|
331
245
|
<!-- Live region for screen reader announcements -->
|
|
332
246
|
<LiveRegion message={resultsAnnouncement} politeness="polite" />
|
|
@@ -309,6 +309,23 @@
|
|
|
309
309
|
// Save timeout for debounced persistence
|
|
310
310
|
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
311
311
|
|
|
312
|
+
function saveNow() {
|
|
313
|
+
if (!persistKey) return;
|
|
314
|
+
// Persist the resolved values so controlled mode saves what's shown,
|
|
315
|
+
// not the untouched internal defaults
|
|
316
|
+
savePanelState(persistKey, {
|
|
317
|
+
mode,
|
|
318
|
+
variant,
|
|
319
|
+
edge,
|
|
320
|
+
isExpanded,
|
|
321
|
+
detachedPosition,
|
|
322
|
+
detachedSize,
|
|
323
|
+
pinnedSize,
|
|
324
|
+
cardSnapIndex: 0, // Legacy field
|
|
325
|
+
tabOffset: internalTabOffset,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
312
329
|
function debouncedSave() {
|
|
313
330
|
if (!persistKey) return;
|
|
314
331
|
|
|
@@ -317,25 +334,19 @@
|
|
|
317
334
|
}
|
|
318
335
|
|
|
319
336
|
saveTimeout = setTimeout(() => {
|
|
320
|
-
// Persist the resolved values so controlled mode saves what's shown,
|
|
321
|
-
// not the untouched internal defaults
|
|
322
|
-
savePanelState(persistKey, {
|
|
323
|
-
mode,
|
|
324
|
-
variant,
|
|
325
|
-
edge,
|
|
326
|
-
isExpanded,
|
|
327
|
-
detachedPosition,
|
|
328
|
-
detachedSize,
|
|
329
|
-
pinnedSize,
|
|
330
|
-
cardSnapIndex: 0, // Legacy field
|
|
331
|
-
tabOffset: internalTabOffset,
|
|
332
|
-
});
|
|
333
337
|
saveTimeout = null;
|
|
338
|
+
saveNow();
|
|
334
339
|
}, 300);
|
|
335
340
|
}
|
|
336
341
|
|
|
337
342
|
onDestroy(() => {
|
|
338
|
-
|
|
343
|
+
// Flush a pending save, or a change made just before unmount (e.g. closing
|
|
344
|
+
// the panel right after resizing it) is lost
|
|
345
|
+
if (saveTimeout) {
|
|
346
|
+
clearTimeout(saveTimeout);
|
|
347
|
+
saveTimeout = null;
|
|
348
|
+
saveNow();
|
|
349
|
+
}
|
|
339
350
|
// If we unmount mid-drag/resize (e.g. Escape closes the panel), the *End
|
|
340
351
|
// handlers never run — reset the body styles they own so the page isn't
|
|
341
352
|
// left stuck at grabbing cursor / no user-select.
|
|
@@ -1560,7 +1571,9 @@
|
|
|
1560
1571
|
<!-- Resize handles (full variant only) -->
|
|
1561
1572
|
{#if variant === 'full' && !disableResize}
|
|
1562
1573
|
{#if mode === 'detached'}
|
|
1563
|
-
<!-- Edge + corner handles: keyboard-resizable sliders (arrow keys, shift for large steps)
|
|
1574
|
+
<!-- Edge + corner handles: keyboard-resizable sliders (arrow keys, shift for large steps).
|
|
1575
|
+
Top/bottom edges change height (Up/Down, so a vertical slider); left/right
|
|
1576
|
+
edges and corners report width (Left/Right, horizontal). -->
|
|
1564
1577
|
{#each detachedResizeHandles as { handle, orientation } (handle)}
|
|
1565
1578
|
<div
|
|
1566
1579
|
class={cn(
|
|
@@ -1572,7 +1585,7 @@
|
|
|
1572
1585
|
)}
|
|
1573
1586
|
role="slider"
|
|
1574
1587
|
tabindex="0"
|
|
1575
|
-
aria-orientation={orientation === '
|
|
1588
|
+
aria-orientation={orientation === 'horizontal' ? 'vertical' : 'horizontal'}
|
|
1576
1589
|
aria-label={`Resize panel from ${handle.replace('-', ' ')} ${
|
|
1577
1590
|
handle.includes('-') ? 'corner' : 'edge'
|
|
1578
1591
|
}`}
|
|
@@ -1683,7 +1696,6 @@
|
|
|
1683
1696
|
right: 0;
|
|
1684
1697
|
height: 2px;
|
|
1685
1698
|
background: transparent;
|
|
1686
|
-
transition: background-color 0.15s ease;
|
|
1687
1699
|
}
|
|
1688
1700
|
|
|
1689
1701
|
.resize-handle--top {
|
|
@@ -1717,7 +1729,6 @@
|
|
|
1717
1729
|
bottom: 0;
|
|
1718
1730
|
width: 2px;
|
|
1719
1731
|
background: transparent;
|
|
1720
|
-
transition: background-color 0.15s ease;
|
|
1721
1732
|
}
|
|
1722
1733
|
|
|
1723
1734
|
.resize-handle--left {
|
|
@@ -1736,7 +1747,8 @@
|
|
|
1736
1747
|
right: 0;
|
|
1737
1748
|
}
|
|
1738
1749
|
|
|
1739
|
-
/* Hover / keyboard-focus state for edge handles
|
|
1750
|
+
/* Hover / keyboard-focus state for edge handles. Hover colours snap (no
|
|
1751
|
+
transition), as everywhere in the theme: a fade reads as flicker. */
|
|
1740
1752
|
.resize-handle--horizontal:hover::after,
|
|
1741
1753
|
.resize-handle--vertical:hover::after,
|
|
1742
1754
|
.resize-handle--horizontal:focus-visible::after,
|
|
@@ -1769,11 +1781,6 @@
|
|
|
1769
1781
|
height: 10px;
|
|
1770
1782
|
/* !important needed to override Tailwind preflight reset (border-width: 0) */
|
|
1771
1783
|
border: 2px solid transparent !important;
|
|
1772
|
-
transition:
|
|
1773
|
-
border-top-color 0.15s ease,
|
|
1774
|
-
border-right-color 0.15s ease,
|
|
1775
|
-
border-bottom-color 0.15s ease,
|
|
1776
|
-
border-left-color 0.15s ease;
|
|
1777
1784
|
}
|
|
1778
1785
|
|
|
1779
1786
|
/* Corner positions */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { cn } from '../utils.js';
|
|
3
|
-
import DropdownMenu from '
|
|
4
|
-
import
|
|
3
|
+
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
|
4
|
+
import Icon from './Icon.svelte';
|
|
5
5
|
import type { Snippet } from 'svelte';
|
|
6
6
|
import type { PanelEdge, PanelMode } from '@classic-homes/data-panel-core';
|
|
7
7
|
|
|
@@ -64,33 +64,37 @@
|
|
|
64
64
|
bottom: 'Pin to bottom',
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
// Panel Position is one choice among five (four edges or detached), so it
|
|
68
|
+
// is a radio group: menuitemradio items with aria-checked, not a ✓ glyph.
|
|
69
|
+
const positionValue = $derived(mode === 'detached' ? 'detached' : edge);
|
|
70
|
+
const positionOptions: { value: PanelEdge | 'detached'; label: string }[] = [
|
|
70
71
|
...(['left', 'right', 'top', 'bottom'] as PanelEdge[]).map((e) => ({
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
onModeChange?.('pinned');
|
|
74
|
-
onEdgeChange?.(e);
|
|
75
|
-
},
|
|
72
|
+
value: e,
|
|
73
|
+
label: edgeLabels[e],
|
|
76
74
|
})),
|
|
77
|
-
{
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
{ value: 'detached', label: 'Detach' },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
function handlePositionChange(next: string) {
|
|
79
|
+
if (next === positionValue) return;
|
|
80
|
+
if (next === 'detached') {
|
|
81
|
+
onModeChange?.('detached');
|
|
82
|
+
} else {
|
|
83
|
+
onModeChange?.('pinned');
|
|
84
|
+
onEdgeChange?.(next as PanelEdge);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const itemClass =
|
|
89
|
+
'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';
|
|
83
90
|
</script>
|
|
84
91
|
|
|
85
92
|
{#snippet modeMenu()}
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
triggerClass="relative flex h-8 w-8 items-center justify-center rounded-md before:absolute before:-inset-1.5 before:content-[''] bg-transparent text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
92
|
-
>
|
|
93
|
-
{#snippet trigger()}
|
|
93
|
+
<DropdownMenuPrimitive.Root>
|
|
94
|
+
<DropdownMenuPrimitive.Trigger
|
|
95
|
+
aria-label="Panel options"
|
|
96
|
+
class="relative flex h-8 w-8 items-center justify-center rounded-md before:absolute before:-inset-1.5 before:content-[''] bg-transparent text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
97
|
+
>
|
|
94
98
|
<svg
|
|
95
99
|
xmlns="http://www.w3.org/2000/svg"
|
|
96
100
|
width="16"
|
|
@@ -107,8 +111,44 @@
|
|
|
107
111
|
<circle cx="12" cy="5" r="1" />
|
|
108
112
|
<circle cx="12" cy="19" r="1" />
|
|
109
113
|
</svg>
|
|
110
|
-
|
|
111
|
-
|
|
114
|
+
</DropdownMenuPrimitive.Trigger>
|
|
115
|
+
<DropdownMenuPrimitive.Portal>
|
|
116
|
+
<DropdownMenuPrimitive.Content
|
|
117
|
+
side="bottom"
|
|
118
|
+
align="end"
|
|
119
|
+
sideOffset={4}
|
|
120
|
+
collisionPadding={8}
|
|
121
|
+
class="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"
|
|
122
|
+
>
|
|
123
|
+
<DropdownMenuPrimitive.RadioGroup
|
|
124
|
+
value={positionValue}
|
|
125
|
+
onValueChange={handlePositionChange}
|
|
126
|
+
>
|
|
127
|
+
<DropdownMenuPrimitive.GroupHeading class="px-2 py-1.5 text-sm font-semibold">
|
|
128
|
+
Panel Position
|
|
129
|
+
</DropdownMenuPrimitive.GroupHeading>
|
|
130
|
+
{#each positionOptions as option (option.value)}
|
|
131
|
+
{#if option.value === 'detached'}
|
|
132
|
+
<DropdownMenuPrimitive.Separator class="-mx-1 my-1 h-px bg-muted" />
|
|
133
|
+
{/if}
|
|
134
|
+
<DropdownMenuPrimitive.RadioItem value={option.value} class={itemClass}>
|
|
135
|
+
{#snippet children({ checked })}
|
|
136
|
+
{#if checked}
|
|
137
|
+
<Icon
|
|
138
|
+
name="check"
|
|
139
|
+
size={16}
|
|
140
|
+
class="absolute left-2 shrink-0"
|
|
141
|
+
aria-hidden="true"
|
|
142
|
+
/>
|
|
143
|
+
{/if}
|
|
144
|
+
{option.label}
|
|
145
|
+
{/snippet}
|
|
146
|
+
</DropdownMenuPrimitive.RadioItem>
|
|
147
|
+
{/each}
|
|
148
|
+
</DropdownMenuPrimitive.RadioGroup>
|
|
149
|
+
</DropdownMenuPrimitive.Content>
|
|
150
|
+
</DropdownMenuPrimitive.Portal>
|
|
151
|
+
</DropdownMenuPrimitive.Root>
|
|
112
152
|
{/snippet}
|
|
113
153
|
|
|
114
154
|
<!-- When showModeMenuOnly is true, only render the dropdown menu -->
|