@classic-homes/theme-svelte 1.37.0 → 1.39.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 +8 -0
- package/dist/lib/components/AlertDialog.svelte.d.ts +6 -0
- package/dist/lib/components/Collapsible.svelte +68 -0
- package/dist/lib/components/Collapsible.svelte.d.ts +27 -0
- package/dist/lib/components/Combobox.svelte +37 -2
- package/dist/lib/components/Combobox.svelte.d.ts +6 -0
- package/dist/lib/components/ContextMenu.svelte +135 -0
- package/dist/lib/components/ContextMenu.svelte.d.ts +26 -0
- package/dist/lib/components/CopyButton.svelte +101 -0
- package/dist/lib/components/CopyButton.svelte.d.ts +32 -0
- package/dist/lib/components/EmptyState.svelte +72 -0
- package/dist/lib/components/EmptyState.svelte.d.ts +24 -0
- package/dist/lib/components/Input.svelte +4 -0
- package/dist/lib/components/Input.svelte.d.ts +4 -0
- package/dist/lib/components/Kbd.svelte +80 -0
- package/dist/lib/components/Kbd.svelte.d.ts +22 -0
- package/dist/lib/components/Popover.svelte +90 -0
- package/dist/lib/components/Popover.svelte.d.ts +34 -0
- package/dist/lib/components/ResizablePanes.svelte +185 -0
- package/dist/lib/components/ResizablePanes.svelte.d.ts +33 -0
- package/dist/lib/components/Select.svelte +4 -0
- package/dist/lib/components/Select.svelte.d.ts +2 -0
- package/dist/lib/components/Textarea.svelte +45 -0
- package/dist/lib/components/Textarea.svelte.d.ts +14 -1
- package/dist/lib/index.d.ts +7 -0
- package/dist/lib/index.js +8 -0
- package/package.json +1 -1
|
@@ -51,6 +51,12 @@
|
|
|
51
51
|
trigger?: Snippet<[TriggerSnippetProps]>;
|
|
52
52
|
/** Optional custom content */
|
|
53
53
|
children?: Snippet;
|
|
54
|
+
/**
|
|
55
|
+
* Called before focus returns to the trigger (or the previously focused
|
|
56
|
+
* element) on close; `preventDefault()` keeps focus where you put it, e.g.
|
|
57
|
+
* when the confirmed action removed that element.
|
|
58
|
+
*/
|
|
59
|
+
onCloseAutoFocus?: (event: Event) => void;
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
let {
|
|
@@ -66,6 +72,7 @@
|
|
|
66
72
|
class: className,
|
|
67
73
|
trigger,
|
|
68
74
|
children,
|
|
75
|
+
onCloseAutoFocus,
|
|
69
76
|
}: Props = $props();
|
|
70
77
|
|
|
71
78
|
const uid = $props.id();
|
|
@@ -121,6 +128,7 @@
|
|
|
121
128
|
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
|
122
129
|
/>
|
|
123
130
|
<AlertDialogPrimitive.Content
|
|
131
|
+
{onCloseAutoFocus}
|
|
124
132
|
class={cn(
|
|
125
133
|
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100vw-2rem)] sm:max-w-lg max-h-[calc(100dvh-2rem)] overflow-y-auto translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border border-black bg-background p-4 sm:p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]',
|
|
126
134
|
className
|
|
@@ -80,6 +80,12 @@ declare const AlertDialog: import("svelte").Component<{
|
|
|
80
80
|
trigger?: Snippet<[TriggerSnippetProps]>;
|
|
81
81
|
/** Optional custom content */
|
|
82
82
|
children?: Snippet;
|
|
83
|
+
/**
|
|
84
|
+
* Called before focus returns to the trigger (or the previously focused
|
|
85
|
+
* element) on close; `preventDefault()` keeps focus where you put it, e.g.
|
|
86
|
+
* when the confirmed action removed that element.
|
|
87
|
+
*/
|
|
88
|
+
onCloseAutoFocus?: (event: Event) => void;
|
|
83
89
|
}, {}, "open">;
|
|
84
90
|
type AlertDialog = ReturnType<typeof AlertDialog>;
|
|
85
91
|
export default AlertDialog;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
A region that opens and closes from a trigger button (Bits UI Collapsible).
|
|
4
|
+
Pass `label` for a plain trigger with a chevron, or a `trigger` snippet for
|
|
5
|
+
custom trigger content (it receives `{ open }`; the button wraps it).
|
|
6
|
+
-->
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import { Collapsible as CollapsiblePrimitive } from 'bits-ui';
|
|
9
|
+
import type { Snippet } from 'svelte';
|
|
10
|
+
import { cn } from '../utils.js';
|
|
11
|
+
import Icon from './Icon.svelte';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
/** Whether the content is shown (bindable) */
|
|
15
|
+
open?: boolean;
|
|
16
|
+
onOpenChange?: (open: boolean) => void;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
/** Trigger text; ignored when `trigger` is given */
|
|
19
|
+
label?: string;
|
|
20
|
+
/** Custom trigger content, rendered inside the trigger button */
|
|
21
|
+
trigger?: Snippet<[{ open: boolean }]>;
|
|
22
|
+
/** Show the chevron before the trigger content (default true) */
|
|
23
|
+
chevron?: boolean;
|
|
24
|
+
class?: string;
|
|
25
|
+
triggerClass?: string;
|
|
26
|
+
contentClass?: string;
|
|
27
|
+
children: Snippet;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
open = $bindable(false),
|
|
32
|
+
onOpenChange,
|
|
33
|
+
disabled = false,
|
|
34
|
+
label,
|
|
35
|
+
trigger,
|
|
36
|
+
chevron = true,
|
|
37
|
+
class: className,
|
|
38
|
+
triggerClass,
|
|
39
|
+
contentClass,
|
|
40
|
+
children,
|
|
41
|
+
}: Props = $props();
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<CollapsiblePrimitive.Root bind:open {onOpenChange} {disabled} class={className}>
|
|
45
|
+
<CollapsiblePrimitive.Trigger
|
|
46
|
+
class={cn(
|
|
47
|
+
'group inline-flex items-center gap-1.5 rounded-md text-sm font-medium text-foreground hover:text-foreground/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
48
|
+
triggerClass
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
51
|
+
{#if chevron}
|
|
52
|
+
<Icon
|
|
53
|
+
name="chevron-right"
|
|
54
|
+
size={14}
|
|
55
|
+
class="shrink-0 transition-transform duration-200 group-data-[state=open]:rotate-90 motion-reduce:transition-none"
|
|
56
|
+
/>
|
|
57
|
+
{/if}
|
|
58
|
+
{#if trigger}{@render trigger({ open })}{:else}{label}{/if}
|
|
59
|
+
</CollapsiblePrimitive.Trigger>
|
|
60
|
+
<CollapsiblePrimitive.Content
|
|
61
|
+
class={cn(
|
|
62
|
+
'overflow-hidden data-[state=open]:animate-collapsible-down data-[state=closed]:animate-collapsible-up motion-reduce:animate-none',
|
|
63
|
+
contentClass
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
{@render children()}
|
|
67
|
+
</CollapsiblePrimitive.Content>
|
|
68
|
+
</CollapsiblePrimitive.Root>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Whether the content is shown (bindable) */
|
|
4
|
+
open?: boolean;
|
|
5
|
+
onOpenChange?: (open: boolean) => void;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
/** Trigger text; ignored when `trigger` is given */
|
|
8
|
+
label?: string;
|
|
9
|
+
/** Custom trigger content, rendered inside the trigger button */
|
|
10
|
+
trigger?: Snippet<[{
|
|
11
|
+
open: boolean;
|
|
12
|
+
}]>;
|
|
13
|
+
/** Show the chevron before the trigger content (default true) */
|
|
14
|
+
chevron?: boolean;
|
|
15
|
+
class?: string;
|
|
16
|
+
triggerClass?: string;
|
|
17
|
+
contentClass?: string;
|
|
18
|
+
children: Snippet;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A region that opens and closes from a trigger button (Bits UI Collapsible).
|
|
22
|
+
* Pass `label` for a plain trigger with a chevron, or a `trigger` snippet for
|
|
23
|
+
* custom trigger content (it receives `{ open }`; the button wraps it).
|
|
24
|
+
*/
|
|
25
|
+
declare const Collapsible: import("svelte").Component<Props, {}, "open">;
|
|
26
|
+
type Collapsible = ReturnType<typeof Collapsible>;
|
|
27
|
+
export default Collapsible;
|
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
placeholder?: string;
|
|
26
26
|
/** Message to display when no results found */
|
|
27
27
|
emptyMessage?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Render at most this many matching options, with a note to type to
|
|
30
|
+
* narrow the rest. Filtering still searches every option. Keeps long
|
|
31
|
+
* lists (thousands of options) quick to open.
|
|
32
|
+
*/
|
|
33
|
+
maxResults?: number;
|
|
28
34
|
/** Whether the combobox is disabled */
|
|
29
35
|
disabled?: boolean;
|
|
30
36
|
/** Whether selection is required */
|
|
@@ -69,6 +75,7 @@
|
|
|
69
75
|
loading = false,
|
|
70
76
|
onSearch,
|
|
71
77
|
debounceMs = 300,
|
|
78
|
+
maxResults,
|
|
72
79
|
class: className,
|
|
73
80
|
'aria-label': ariaLabelProp,
|
|
74
81
|
'aria-labelledby': ariaLabelledby,
|
|
@@ -129,6 +136,28 @@
|
|
|
129
136
|
filteredOptions.reduce((n: number, item) => n + (isGroup(item) ? item.options.length : 1), 0)
|
|
130
137
|
);
|
|
131
138
|
|
|
139
|
+
// The first `maxResults` matches, keeping their groups.
|
|
140
|
+
const shownOptions = $derived.by((): (ComboboxOption | SelectGroup)[] => {
|
|
141
|
+
if (maxResults === undefined || filteredCount <= maxResults) return filteredOptions;
|
|
142
|
+
let left = maxResults;
|
|
143
|
+
const out: (ComboboxOption | SelectGroup)[] = [];
|
|
144
|
+
for (const item of filteredOptions) {
|
|
145
|
+
if (left <= 0) break;
|
|
146
|
+
if (isGroup(item)) {
|
|
147
|
+
const opts = item.options.slice(0, left);
|
|
148
|
+
out.push({ ...item, options: opts });
|
|
149
|
+
left -= opts.length;
|
|
150
|
+
} else {
|
|
151
|
+
out.push(item);
|
|
152
|
+
left--;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
});
|
|
157
|
+
const hiddenCount = $derived(
|
|
158
|
+
maxResults === undefined ? 0 : Math.max(0, filteredCount - maxResults)
|
|
159
|
+
);
|
|
160
|
+
|
|
132
161
|
// Closed, the input shows the selected label; open, it holds what the user
|
|
133
162
|
// types, with the selected label as the placeholder so the full list shows.
|
|
134
163
|
const inputValue = $derived(open ? searchQuery : (selectedLabel ?? ''));
|
|
@@ -174,7 +203,8 @@
|
|
|
174
203
|
if (!open) return '';
|
|
175
204
|
if (filteredCount === 0) return emptyMessage;
|
|
176
205
|
const count = filteredCount;
|
|
177
|
-
|
|
206
|
+
const shown = hiddenCount ? `, showing the first ${count - hiddenCount}` : '';
|
|
207
|
+
return `${count} ${count === 1 ? 'option' : 'options'} available${shown}`;
|
|
178
208
|
});
|
|
179
209
|
</script>
|
|
180
210
|
|
|
@@ -276,7 +306,7 @@
|
|
|
276
306
|
{emptyMessage}
|
|
277
307
|
</div>
|
|
278
308
|
{:else}
|
|
279
|
-
{#each
|
|
309
|
+
{#each shownOptions as item}
|
|
280
310
|
{#if isGroup(item)}
|
|
281
311
|
<ComboboxPrimitive.Group>
|
|
282
312
|
<ComboboxPrimitive.GroupHeading class="py-1.5 pl-8 pr-2 text-sm font-semibold">
|
|
@@ -290,6 +320,11 @@
|
|
|
290
320
|
{@render comboboxItem(item)}
|
|
291
321
|
{/if}
|
|
292
322
|
{/each}
|
|
323
|
+
{#if hiddenCount}
|
|
324
|
+
<div class="px-2 py-1.5 text-xs text-muted-foreground" data-combobox-more>
|
|
325
|
+
{hiddenCount} more. Type to narrow the list.
|
|
326
|
+
</div>
|
|
327
|
+
{/if}
|
|
293
328
|
{/if}
|
|
294
329
|
</div>
|
|
295
330
|
</ComboboxPrimitive.Content>
|
|
@@ -15,6 +15,12 @@ interface Props {
|
|
|
15
15
|
placeholder?: string;
|
|
16
16
|
/** Message to display when no results found */
|
|
17
17
|
emptyMessage?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Render at most this many matching options, with a note to type to
|
|
20
|
+
* narrow the rest. Filtering still searches every option. Keeps long
|
|
21
|
+
* lists (thousands of options) quick to open.
|
|
22
|
+
*/
|
|
23
|
+
maxResults?: number;
|
|
18
24
|
/** Whether the combobox is disabled */
|
|
19
25
|
disabled?: boolean;
|
|
20
26
|
/** Whether selection is required */
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
import { cn } from '../utils.js';
|
|
5
|
+
import Icon from './Icon.svelte';
|
|
6
|
+
import type { DropdownMenuGroup, DropdownMenuItem } from './DropdownMenu.svelte';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
/** Menu items, the same shape as DropdownMenu's */
|
|
10
|
+
items: (DropdownMenuItem | DropdownMenuGroup)[];
|
|
11
|
+
/** The area that opens the menu on right-click (or long-press) */
|
|
12
|
+
children: Snippet;
|
|
13
|
+
/** Whether the menu is open (bindable) */
|
|
14
|
+
open?: boolean;
|
|
15
|
+
/** Called when the menu opens or closes */
|
|
16
|
+
onOpenChange?: (open: boolean) => void;
|
|
17
|
+
/** Leave the browser's own menu in place */
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
/** Classes for the trigger area */
|
|
20
|
+
triggerClass?: string;
|
|
21
|
+
/** Classes for the menu */
|
|
22
|
+
class?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Called as the menu closes, before focus returns to where it was;
|
|
25
|
+
* `preventDefault()` keeps focus where a chosen item put it.
|
|
26
|
+
*/
|
|
27
|
+
onCloseAutoFocus?: (event: Event) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
items,
|
|
32
|
+
children,
|
|
33
|
+
open = $bindable(false),
|
|
34
|
+
onOpenChange,
|
|
35
|
+
disabled = false,
|
|
36
|
+
triggerClass,
|
|
37
|
+
class: className,
|
|
38
|
+
onCloseAutoFocus,
|
|
39
|
+
}: Props = $props();
|
|
40
|
+
|
|
41
|
+
function isGroup(item: DropdownMenuItem | DropdownMenuGroup): item is DropdownMenuGroup {
|
|
42
|
+
return 'items' in item && Array.isArray(item.items);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function handleOpenChange(next: boolean) {
|
|
46
|
+
open = next;
|
|
47
|
+
onOpenChange?.(next);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const itemClass =
|
|
51
|
+
'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';
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
{#snippet itemContent(item: DropdownMenuItem)}
|
|
55
|
+
{#if item.icon}
|
|
56
|
+
<Icon name={item.icon} size={16} class="mr-2 shrink-0" aria-hidden="true" />
|
|
57
|
+
{/if}
|
|
58
|
+
{item.label}
|
|
59
|
+
{#if item.shortcut}
|
|
60
|
+
<span class="ml-auto pl-4 text-xs tracking-widest opacity-60" aria-hidden="true">
|
|
61
|
+
{item.shortcut}
|
|
62
|
+
</span>
|
|
63
|
+
<span class="sr-only">, keyboard shortcut {item.shortcut}</span>
|
|
64
|
+
{/if}
|
|
65
|
+
{/snippet}
|
|
66
|
+
|
|
67
|
+
{#snippet entry(item: DropdownMenuItem)}
|
|
68
|
+
{#if item.type === 'separator'}
|
|
69
|
+
<ContextMenuPrimitive.Separator class="-mx-1 my-1 h-px bg-muted" />
|
|
70
|
+
{:else if item.type === 'label'}
|
|
71
|
+
<div role="presentation" class="px-2 py-1.5 text-sm font-semibold">{item.label}</div>
|
|
72
|
+
{:else if item.checked !== undefined}
|
|
73
|
+
<ContextMenuPrimitive.CheckboxItem
|
|
74
|
+
checked={item.checked}
|
|
75
|
+
disabled={item.disabled}
|
|
76
|
+
closeOnSelect={false}
|
|
77
|
+
onSelect={() => item.onSelect?.()}
|
|
78
|
+
class={cn(itemClass, 'pl-8')}
|
|
79
|
+
>
|
|
80
|
+
{#if item.checked}
|
|
81
|
+
<Icon name="check" size={16} class="absolute left-2 shrink-0" aria-hidden="true" />
|
|
82
|
+
{/if}
|
|
83
|
+
{@render itemContent(item)}
|
|
84
|
+
</ContextMenuPrimitive.CheckboxItem>
|
|
85
|
+
{:else}
|
|
86
|
+
<ContextMenuPrimitive.Item
|
|
87
|
+
disabled={item.disabled}
|
|
88
|
+
onSelect={() => item.onSelect?.()}
|
|
89
|
+
class={cn(
|
|
90
|
+
itemClass,
|
|
91
|
+
item.destructive && 'text-destructive data-[highlighted]:text-destructive'
|
|
92
|
+
)}
|
|
93
|
+
aria-keyshortcuts={item.shortcut || undefined}
|
|
94
|
+
>
|
|
95
|
+
{@render itemContent(item)}
|
|
96
|
+
</ContextMenuPrimitive.Item>
|
|
97
|
+
{/if}
|
|
98
|
+
{/snippet}
|
|
99
|
+
|
|
100
|
+
<ContextMenuPrimitive.Root bind:open onOpenChange={handleOpenChange}>
|
|
101
|
+
<ContextMenuPrimitive.Trigger {disabled} class={triggerClass}>
|
|
102
|
+
{@render children()}
|
|
103
|
+
</ContextMenuPrimitive.Trigger>
|
|
104
|
+
|
|
105
|
+
<!-- Portal so the menu is not clipped by an overflow/transformed ancestor. -->
|
|
106
|
+
<ContextMenuPrimitive.Portal>
|
|
107
|
+
<ContextMenuPrimitive.Content
|
|
108
|
+
collisionPadding={8}
|
|
109
|
+
{onCloseAutoFocus}
|
|
110
|
+
class={cn(
|
|
111
|
+
'isolate z-50 min-w-[10rem] 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',
|
|
112
|
+
className
|
|
113
|
+
)}
|
|
114
|
+
>
|
|
115
|
+
<div class="max-h-80 overflow-y-auto overflow-x-hidden">
|
|
116
|
+
{#each items as item, i (i)}
|
|
117
|
+
{#if isGroup(item)}
|
|
118
|
+
<ContextMenuPrimitive.Group>
|
|
119
|
+
{#if item.label}
|
|
120
|
+
<ContextMenuPrimitive.GroupHeading class="px-2 py-1.5 text-sm font-semibold">
|
|
121
|
+
{item.label}
|
|
122
|
+
</ContextMenuPrimitive.GroupHeading>
|
|
123
|
+
{/if}
|
|
124
|
+
{#each item.items as sub, j (j)}
|
|
125
|
+
{@render entry(sub)}
|
|
126
|
+
{/each}
|
|
127
|
+
</ContextMenuPrimitive.Group>
|
|
128
|
+
{:else}
|
|
129
|
+
{@render entry(item)}
|
|
130
|
+
{/if}
|
|
131
|
+
{/each}
|
|
132
|
+
</div>
|
|
133
|
+
</ContextMenuPrimitive.Content>
|
|
134
|
+
</ContextMenuPrimitive.Portal>
|
|
135
|
+
</ContextMenuPrimitive.Root>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { DropdownMenuGroup, DropdownMenuItem } from './DropdownMenu.svelte';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** Menu items, the same shape as DropdownMenu's */
|
|
5
|
+
items: (DropdownMenuItem | DropdownMenuGroup)[];
|
|
6
|
+
/** The area that opens the menu on right-click (or long-press) */
|
|
7
|
+
children: Snippet;
|
|
8
|
+
/** Whether the menu is open (bindable) */
|
|
9
|
+
open?: boolean;
|
|
10
|
+
/** Called when the menu opens or closes */
|
|
11
|
+
onOpenChange?: (open: boolean) => void;
|
|
12
|
+
/** Leave the browser's own menu in place */
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
/** Classes for the trigger area */
|
|
15
|
+
triggerClass?: string;
|
|
16
|
+
/** Classes for the menu */
|
|
17
|
+
class?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Called as the menu closes, before focus returns to where it was;
|
|
20
|
+
* `preventDefault()` keeps focus where a chosen item put it.
|
|
21
|
+
*/
|
|
22
|
+
onCloseAutoFocus?: (event: Event) => void;
|
|
23
|
+
}
|
|
24
|
+
declare const ContextMenu: import("svelte").Component<Props, {}, "open">;
|
|
25
|
+
type ContextMenu = ReturnType<typeof ContextMenu>;
|
|
26
|
+
export default ContextMenu;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Copies text to the clipboard and confirms it: the icon turns into a check
|
|
4
|
+
and screen readers hear "Copied" for `timeout` ms. Icon-only by default
|
|
5
|
+
(with a tooltip); pass `children` for a labelled button.
|
|
6
|
+
-->
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import type { Snippet } from 'svelte';
|
|
9
|
+
import Button from './Button.svelte';
|
|
10
|
+
import Icon from './Icon.svelte';
|
|
11
|
+
import Tooltip from './Tooltip.svelte';
|
|
12
|
+
import type { ButtonVariants } from './button-variants.js';
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
/** Text to copy, or a function returning it (read when clicked) */
|
|
16
|
+
value: string | (() => string | Promise<string>);
|
|
17
|
+
/** Accessible name and tooltip before copying (default "Copy") */
|
|
18
|
+
label?: string;
|
|
19
|
+
/** Accessible name, tooltip and announcement after copying (default "Copied") */
|
|
20
|
+
copiedLabel?: string;
|
|
21
|
+
/** How long the copied state lasts, in ms (default 2000) */
|
|
22
|
+
timeout?: number;
|
|
23
|
+
variant?: ButtonVariants['variant'];
|
|
24
|
+
size?: ButtonVariants['size'];
|
|
25
|
+
/** Show a tooltip on the icon-only button (default true) */
|
|
26
|
+
tooltip?: boolean;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
class?: string;
|
|
29
|
+
/** Called after the text was written to the clipboard */
|
|
30
|
+
oncopy?: (text: string) => void;
|
|
31
|
+
/** Called when the clipboard is unavailable or the write was refused */
|
|
32
|
+
onerror?: (error: unknown) => void;
|
|
33
|
+
/** Visible label; omit for an icon-only button */
|
|
34
|
+
children?: Snippet;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let {
|
|
38
|
+
value,
|
|
39
|
+
label = 'Copy',
|
|
40
|
+
copiedLabel = 'Copied',
|
|
41
|
+
timeout = 2000,
|
|
42
|
+
variant = 'ghost',
|
|
43
|
+
size,
|
|
44
|
+
tooltip = true,
|
|
45
|
+
disabled = false,
|
|
46
|
+
class: className,
|
|
47
|
+
oncopy,
|
|
48
|
+
onerror,
|
|
49
|
+
children,
|
|
50
|
+
}: Props = $props();
|
|
51
|
+
|
|
52
|
+
let copied = $state(false);
|
|
53
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
54
|
+
$effect(() => () => clearTimeout(timer));
|
|
55
|
+
|
|
56
|
+
async function copy() {
|
|
57
|
+
try {
|
|
58
|
+
const text = typeof value === 'function' ? await value() : value;
|
|
59
|
+
if (!navigator.clipboard?.writeText) throw new Error('Clipboard is not available');
|
|
60
|
+
await navigator.clipboard.writeText(text);
|
|
61
|
+
copied = true;
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
timer = setTimeout(() => (copied = false), timeout);
|
|
64
|
+
oncopy?.(text);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
onerror?.(error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const name = $derived(copied ? copiedLabel : label);
|
|
71
|
+
const iconSize = $derived(size === 'sm' ? 14 : 16);
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
{#snippet button(props: Record<string, unknown> = {})}
|
|
75
|
+
<Button
|
|
76
|
+
{...props}
|
|
77
|
+
type="button"
|
|
78
|
+
{variant}
|
|
79
|
+
size={size ?? (children ? 'sm' : 'icon')}
|
|
80
|
+
{disabled}
|
|
81
|
+
class={className}
|
|
82
|
+
aria-label={children ? undefined : name}
|
|
83
|
+
data-copied={copied || undefined}
|
|
84
|
+
onclick={(e: MouseEvent) => {
|
|
85
|
+
(props.onclick as ((e: MouseEvent) => void) | undefined)?.(e);
|
|
86
|
+
void copy();
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
<Icon name={copied ? 'check' : 'copy'} size={iconSize} />
|
|
90
|
+
{#if children}{@render children()}{/if}
|
|
91
|
+
</Button>
|
|
92
|
+
{/snippet}
|
|
93
|
+
|
|
94
|
+
{#if tooltip && !children}
|
|
95
|
+
<Tooltip content={name}>
|
|
96
|
+
{#snippet children({ props })}{@render button(props)}{/snippet}
|
|
97
|
+
</Tooltip>
|
|
98
|
+
{:else}
|
|
99
|
+
{@render button()}
|
|
100
|
+
{/if}
|
|
101
|
+
<span class="sr-only" role="status">{copied ? copiedLabel : ''}</span>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { ButtonVariants } from './button-variants.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** Text to copy, or a function returning it (read when clicked) */
|
|
5
|
+
value: string | (() => string | Promise<string>);
|
|
6
|
+
/** Accessible name and tooltip before copying (default "Copy") */
|
|
7
|
+
label?: string;
|
|
8
|
+
/** Accessible name, tooltip and announcement after copying (default "Copied") */
|
|
9
|
+
copiedLabel?: string;
|
|
10
|
+
/** How long the copied state lasts, in ms (default 2000) */
|
|
11
|
+
timeout?: number;
|
|
12
|
+
variant?: ButtonVariants['variant'];
|
|
13
|
+
size?: ButtonVariants['size'];
|
|
14
|
+
/** Show a tooltip on the icon-only button (default true) */
|
|
15
|
+
tooltip?: boolean;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
class?: string;
|
|
18
|
+
/** Called after the text was written to the clipboard */
|
|
19
|
+
oncopy?: (text: string) => void;
|
|
20
|
+
/** Called when the clipboard is unavailable or the write was refused */
|
|
21
|
+
onerror?: (error: unknown) => void;
|
|
22
|
+
/** Visible label; omit for an icon-only button */
|
|
23
|
+
children?: Snippet;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Copies text to the clipboard and confirms it: the icon turns into a check
|
|
27
|
+
* and screen readers hear "Copied" for `timeout` ms. Icon-only by default
|
|
28
|
+
* (with a tooltip); pass `children` for a labelled button.
|
|
29
|
+
*/
|
|
30
|
+
declare const CopyButton: import("svelte").Component<Props, {}, "">;
|
|
31
|
+
type CopyButton = ReturnType<typeof CopyButton>;
|
|
32
|
+
export default CopyButton;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
A centred placeholder for a view with nothing to show yet: an icon, a
|
|
4
|
+
title, an optional description and actions (e.g. a "Create" button).
|
|
5
|
+
-->
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import type { Snippet } from 'svelte';
|
|
8
|
+
import { cn } from '../utils.js';
|
|
9
|
+
import Icon, { type IconName } from './Icon.svelte';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
title: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
/** Icon shown above the title in a muted circle */
|
|
15
|
+
icon?: IconName;
|
|
16
|
+
/** `sm` for panels and lists, `md` for pages (default) */
|
|
17
|
+
size?: 'sm' | 'md';
|
|
18
|
+
/** Heading element for the title (default h3) */
|
|
19
|
+
headingLevel?: 'h2' | 'h3' | 'h4' | 'p';
|
|
20
|
+
class?: string;
|
|
21
|
+
/** Buttons or links under the description */
|
|
22
|
+
actions?: Snippet;
|
|
23
|
+
/** Extra content between the description and the actions */
|
|
24
|
+
children?: Snippet;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let {
|
|
28
|
+
title,
|
|
29
|
+
description,
|
|
30
|
+
icon,
|
|
31
|
+
size = 'md',
|
|
32
|
+
headingLevel = 'h3',
|
|
33
|
+
class: className,
|
|
34
|
+
actions,
|
|
35
|
+
children,
|
|
36
|
+
}: Props = $props();
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<div
|
|
40
|
+
class={cn(
|
|
41
|
+
'mx-auto flex w-full flex-col items-center text-center',
|
|
42
|
+
size === 'sm' ? 'max-w-xs gap-2 px-4 py-6' : 'max-w-md gap-3 px-6 py-12',
|
|
43
|
+
className
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
{#if icon}
|
|
47
|
+
<div
|
|
48
|
+
class={cn(
|
|
49
|
+
'flex items-center justify-center rounded-full bg-muted text-muted-foreground',
|
|
50
|
+
size === 'sm' ? 'mb-1 h-10 w-10' : 'mb-2 h-12 w-12'
|
|
51
|
+
)}
|
|
52
|
+
aria-hidden="true"
|
|
53
|
+
>
|
|
54
|
+
<Icon name={icon} size={size === 'sm' ? 20 : 24} />
|
|
55
|
+
</div>
|
|
56
|
+
{/if}
|
|
57
|
+
<svelte:element
|
|
58
|
+
this={headingLevel}
|
|
59
|
+
class={cn('font-semibold text-foreground', size === 'sm' ? 'text-sm' : 'text-base')}
|
|
60
|
+
>
|
|
61
|
+
{title}
|
|
62
|
+
</svelte:element>
|
|
63
|
+
{#if description}
|
|
64
|
+
<p class={cn('text-muted-foreground', size === 'sm' ? 'text-xs' : 'text-sm')}>
|
|
65
|
+
{description}
|
|
66
|
+
</p>
|
|
67
|
+
{/if}
|
|
68
|
+
{#if children}{@render children()}{/if}
|
|
69
|
+
{#if actions}
|
|
70
|
+
<div class="mt-2 flex flex-wrap items-center justify-center gap-2">{@render actions()}</div>
|
|
71
|
+
{/if}
|
|
72
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import { type IconName } from './Icon.svelte';
|
|
3
|
+
interface Props {
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
/** Icon shown above the title in a muted circle */
|
|
7
|
+
icon?: IconName;
|
|
8
|
+
/** `sm` for panels and lists, `md` for pages (default) */
|
|
9
|
+
size?: 'sm' | 'md';
|
|
10
|
+
/** Heading element for the title (default h3) */
|
|
11
|
+
headingLevel?: 'h2' | 'h3' | 'h4' | 'p';
|
|
12
|
+
class?: string;
|
|
13
|
+
/** Buttons or links under the description */
|
|
14
|
+
actions?: Snippet;
|
|
15
|
+
/** Extra content between the description and the actions */
|
|
16
|
+
children?: Snippet;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A centred placeholder for a view with nothing to show yet: an icon, a
|
|
20
|
+
* title, an optional description and actions (e.g. a "Create" button).
|
|
21
|
+
*/
|
|
22
|
+
declare const EmptyState: import("svelte").Component<Props, {}, "">;
|
|
23
|
+
type EmptyState = ReturnType<typeof EmptyState>;
|
|
24
|
+
export default EmptyState;
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
/** Whether the input has an error */
|
|
25
25
|
'aria-invalid'?: boolean | 'true' | 'false';
|
|
26
26
|
onValueChange?: (value: string) => void;
|
|
27
|
+
/** Native input listener; runs after the value is updated */
|
|
28
|
+
oninput?: (e: Event & { currentTarget: HTMLInputElement }) => void;
|
|
27
29
|
[key: string]: unknown;
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -44,6 +46,7 @@
|
|
|
44
46
|
'aria-describedby': ariaDescribedby,
|
|
45
47
|
'aria-invalid': ariaInvalid,
|
|
46
48
|
onValueChange,
|
|
49
|
+
oninput,
|
|
47
50
|
...restProps
|
|
48
51
|
}: Props = $props();
|
|
49
52
|
|
|
@@ -62,6 +65,7 @@
|
|
|
62
65
|
const target = e.target as HTMLInputElement;
|
|
63
66
|
value = target.value;
|
|
64
67
|
onValueChange?.(target.value);
|
|
68
|
+
oninput?.(e as Event & { currentTarget: HTMLInputElement });
|
|
65
69
|
}
|
|
66
70
|
</script>
|
|
67
71
|
|
|
@@ -20,6 +20,10 @@ interface Props {
|
|
|
20
20
|
/** Whether the input has an error */
|
|
21
21
|
'aria-invalid'?: boolean | 'true' | 'false';
|
|
22
22
|
onValueChange?: (value: string) => void;
|
|
23
|
+
/** Native input listener; runs after the value is updated */
|
|
24
|
+
oninput?: (e: Event & {
|
|
25
|
+
currentTarget: HTMLInputElement;
|
|
26
|
+
}) => void;
|
|
23
27
|
[key: string]: unknown;
|
|
24
28
|
}
|
|
25
29
|
declare const Input: import("svelte").Component<Props, {}, "value">;
|