@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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
A keyboard key or shortcut. Pass `keys` for a combination ("Mod" shows as
|
|
4
|
+
⌘ on Apple devices and Ctrl elsewhere), or `children` for a single key.
|
|
5
|
+
-->
|
|
6
|
+
<script lang="ts" module>
|
|
7
|
+
const SYMBOLS: Record<string, { apple: string; other: string; name: string }> = {
|
|
8
|
+
mod: { apple: '⌘', other: 'Ctrl', name: 'Command' },
|
|
9
|
+
ctrl: { apple: '⌃', other: 'Ctrl', name: 'Control' },
|
|
10
|
+
alt: { apple: '⌥', other: 'Alt', name: 'Option' },
|
|
11
|
+
shift: { apple: '⇧', other: 'Shift', name: 'Shift' },
|
|
12
|
+
enter: { apple: '↵', other: 'Enter', name: 'Enter' },
|
|
13
|
+
escape: { apple: 'Esc', other: 'Esc', name: 'Escape' },
|
|
14
|
+
esc: { apple: 'Esc', other: 'Esc', name: 'Escape' },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Whether shortcuts should use Apple modifier symbols (⌘, ⌥, ⇧). */
|
|
18
|
+
export function isApplePlatform(): boolean {
|
|
19
|
+
if (typeof navigator === 'undefined') return false;
|
|
20
|
+
const nav = navigator as Navigator & { userAgentData?: { platform?: string } };
|
|
21
|
+
const platform = nav.userAgentData?.platform ?? navigator.platform ?? '';
|
|
22
|
+
return /mac|iphone|ipad|ipod/i.test(platform);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The text shown for one key, and the name read out for it. */
|
|
26
|
+
export function formatKey(
|
|
27
|
+
key: string,
|
|
28
|
+
apple = isApplePlatform()
|
|
29
|
+
): { label: string; name: string } {
|
|
30
|
+
const known = SYMBOLS[key.toLowerCase()];
|
|
31
|
+
if (!known) return { label: key.length === 1 ? key.toUpperCase() : key, name: key };
|
|
32
|
+
const name = key.toLowerCase() === 'mod' && !apple ? 'Control' : known.name;
|
|
33
|
+
return { label: apple ? known.apple : known.other, name };
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<script lang="ts">
|
|
38
|
+
import type { Snippet } from 'svelte';
|
|
39
|
+
import { cn } from '../utils.js';
|
|
40
|
+
|
|
41
|
+
interface Props {
|
|
42
|
+
/** Keys of a combination, e.g. ['Mod', 'K'] */
|
|
43
|
+
keys?: string[];
|
|
44
|
+
size?: 'sm' | 'md';
|
|
45
|
+
class?: string;
|
|
46
|
+
children?: Snippet;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let { keys, size = 'sm', class: className, children }: Props = $props();
|
|
50
|
+
|
|
51
|
+
// Resolved after mount so server and client render the same markup
|
|
52
|
+
let apple = $state(false);
|
|
53
|
+
$effect(() => {
|
|
54
|
+
apple = isApplePlatform();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const keyClass = $derived(
|
|
58
|
+
cn(
|
|
59
|
+
'inline-flex items-center justify-center rounded border border-border bg-muted font-sans font-medium text-muted-foreground shadow-[inset_0_-1px_0_hsl(var(--border))]',
|
|
60
|
+
size === 'sm' ? 'h-5 min-w-5 px-1 text-[11px]' : 'h-6 min-w-6 px-1.5 text-xs'
|
|
61
|
+
)
|
|
62
|
+
);
|
|
63
|
+
const formatted = $derived((keys ?? []).map((k) => formatKey(k, apple)));
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
{#if keys?.length}
|
|
67
|
+
<span
|
|
68
|
+
class={cn('inline-flex items-center gap-0.5', className)}
|
|
69
|
+
aria-label={formatted.map((k) => k.name).join(' + ')}
|
|
70
|
+
role="img"
|
|
71
|
+
>
|
|
72
|
+
{#each formatted as key, i (i)}
|
|
73
|
+
<kbd class={keyClass} aria-hidden="true">{key.label}</kbd>
|
|
74
|
+
{/each}
|
|
75
|
+
</span>
|
|
76
|
+
{:else}
|
|
77
|
+
<kbd class={cn(keyClass, className)}
|
|
78
|
+
>{#if children}{@render children()}{/if}</kbd
|
|
79
|
+
>
|
|
80
|
+
{/if}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Whether shortcuts should use Apple modifier symbols (⌘, ⌥, ⇧). */
|
|
2
|
+
export declare function isApplePlatform(): boolean;
|
|
3
|
+
/** The text shown for one key, and the name read out for it. */
|
|
4
|
+
export declare function formatKey(key: string, apple?: boolean): {
|
|
5
|
+
label: string;
|
|
6
|
+
name: string;
|
|
7
|
+
};
|
|
8
|
+
import type { Snippet } from 'svelte';
|
|
9
|
+
interface Props {
|
|
10
|
+
/** Keys of a combination, e.g. ['Mod', 'K'] */
|
|
11
|
+
keys?: string[];
|
|
12
|
+
size?: 'sm' | 'md';
|
|
13
|
+
class?: string;
|
|
14
|
+
children?: Snippet;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A keyboard key or shortcut. Pass `keys` for a combination ("Mod" shows as
|
|
18
|
+
* ⌘ on Apple devices and Ctrl elsewhere), or `children` for a single key.
|
|
19
|
+
*/
|
|
20
|
+
declare const Kbd: import("svelte").Component<Props, {}, "">;
|
|
21
|
+
type Kbd = ReturnType<typeof Kbd>;
|
|
22
|
+
export default Kbd;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Floating content anchored to a trigger (Bits UI Popover). The `trigger`
|
|
4
|
+
snippet receives `{ props }`: spread them on your own focusable element
|
|
5
|
+
(`<Button {...props}>`). Without a trigger, anchor it with `anchor` (an
|
|
6
|
+
element or selector) and control it through `open`, e.g. a suggestion list
|
|
7
|
+
under a text field that must keep focus (`autoFocus={false}`).
|
|
8
|
+
-->
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
import { Popover as PopoverPrimitive } from 'bits-ui';
|
|
11
|
+
import type { Snippet } from 'svelte';
|
|
12
|
+
import { cn } from '../utils.js';
|
|
13
|
+
import TriggerChild, { type TriggerSnippetProps } from './internal/TriggerChild.svelte';
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
/** Whether the popover is shown (bindable) */
|
|
17
|
+
open?: boolean;
|
|
18
|
+
onOpenChange?: (open: boolean) => void;
|
|
19
|
+
side?: 'top' | 'right' | 'bottom' | 'left';
|
|
20
|
+
align?: 'start' | 'center' | 'end';
|
|
21
|
+
sideOffset?: number;
|
|
22
|
+
/** Element (or selector) to position against instead of the trigger */
|
|
23
|
+
anchor?: HTMLElement | string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Move focus into the content when it opens (default true). Set false to
|
|
26
|
+
* keep focus where it is, e.g. in a text field driving the popover.
|
|
27
|
+
*/
|
|
28
|
+
autoFocus?: boolean;
|
|
29
|
+
/** Keep Tab inside the content while open (default false) */
|
|
30
|
+
trapFocus?: boolean;
|
|
31
|
+
/** Accessible name for the content when it has no visible heading */
|
|
32
|
+
'aria-label'?: string;
|
|
33
|
+
class?: string;
|
|
34
|
+
trigger?: Snippet<[TriggerSnippetProps]>;
|
|
35
|
+
children: Snippet;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let {
|
|
39
|
+
open = $bindable(false),
|
|
40
|
+
onOpenChange,
|
|
41
|
+
side = 'bottom',
|
|
42
|
+
align = 'center',
|
|
43
|
+
sideOffset = 6,
|
|
44
|
+
anchor = null,
|
|
45
|
+
autoFocus = true,
|
|
46
|
+
trapFocus = false,
|
|
47
|
+
'aria-label': ariaLabel,
|
|
48
|
+
class: className,
|
|
49
|
+
trigger,
|
|
50
|
+
children,
|
|
51
|
+
}: Props = $props();
|
|
52
|
+
|
|
53
|
+
const uid = $props.id();
|
|
54
|
+
let triggerId = $state(`${uid}-trigger`);
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<PopoverPrimitive.Root bind:open {onOpenChange}>
|
|
58
|
+
{#if trigger}
|
|
59
|
+
<PopoverPrimitive.Trigger id={triggerId}>
|
|
60
|
+
{#snippet child({ props })}
|
|
61
|
+
<TriggerChild
|
|
62
|
+
{props}
|
|
63
|
+
{trigger}
|
|
64
|
+
fallbackClass="inline-flex"
|
|
65
|
+
onfallback={() => (triggerId = `${uid}-trigger-wrap`)}
|
|
66
|
+
/>
|
|
67
|
+
{/snippet}
|
|
68
|
+
</PopoverPrimitive.Trigger>
|
|
69
|
+
{/if}
|
|
70
|
+
<PopoverPrimitive.Portal>
|
|
71
|
+
<PopoverPrimitive.Content
|
|
72
|
+
{side}
|
|
73
|
+
{align}
|
|
74
|
+
{sideOffset}
|
|
75
|
+
{trapFocus}
|
|
76
|
+
customAnchor={anchor}
|
|
77
|
+
collisionPadding={8}
|
|
78
|
+
aria-label={ariaLabel}
|
|
79
|
+
onOpenAutoFocus={(e) => {
|
|
80
|
+
if (!autoFocus) e.preventDefault();
|
|
81
|
+
}}
|
|
82
|
+
class={cn(
|
|
83
|
+
'z-50 w-72 rounded-lg border border-border bg-popover p-3 text-sm text-popover-foreground shadow-md outline-none animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 motion-reduce:animate-none',
|
|
84
|
+
className
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
{@render children()}
|
|
88
|
+
</PopoverPrimitive.Content>
|
|
89
|
+
</PopoverPrimitive.Portal>
|
|
90
|
+
</PopoverPrimitive.Root>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import { type TriggerSnippetProps } from './internal/TriggerChild.svelte';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** Whether the popover is shown (bindable) */
|
|
5
|
+
open?: boolean;
|
|
6
|
+
onOpenChange?: (open: boolean) => void;
|
|
7
|
+
side?: 'top' | 'right' | 'bottom' | 'left';
|
|
8
|
+
align?: 'start' | 'center' | 'end';
|
|
9
|
+
sideOffset?: number;
|
|
10
|
+
/** Element (or selector) to position against instead of the trigger */
|
|
11
|
+
anchor?: HTMLElement | string | null;
|
|
12
|
+
/**
|
|
13
|
+
* Move focus into the content when it opens (default true). Set false to
|
|
14
|
+
* keep focus where it is, e.g. in a text field driving the popover.
|
|
15
|
+
*/
|
|
16
|
+
autoFocus?: boolean;
|
|
17
|
+
/** Keep Tab inside the content while open (default false) */
|
|
18
|
+
trapFocus?: boolean;
|
|
19
|
+
/** Accessible name for the content when it has no visible heading */
|
|
20
|
+
'aria-label'?: string;
|
|
21
|
+
class?: string;
|
|
22
|
+
trigger?: Snippet<[TriggerSnippetProps]>;
|
|
23
|
+
children: Snippet;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Floating content anchored to a trigger (Bits UI Popover). The `trigger`
|
|
27
|
+
* snippet receives `{ props }`: spread them on your own focusable element
|
|
28
|
+
* (`<Button {...props}>`). Without a trigger, anchor it with `anchor` (an
|
|
29
|
+
* element or selector) and control it through `open`, e.g. a suggestion list
|
|
30
|
+
* under a text field that must keep focus (`autoFocus={false}`).
|
|
31
|
+
*/
|
|
32
|
+
declare const Popover: import("svelte").Component<Props, {}, "open">;
|
|
33
|
+
type Popover = ReturnType<typeof Popover>;
|
|
34
|
+
export default Popover;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Two panes split by a draggable handle. `size` is the start pane's share in
|
|
4
|
+
percent; the end pane takes the rest. The handle is a focusable
|
|
5
|
+
`separator`: arrow keys move it by `step`, Home/End jump to `min`/`max`.
|
|
6
|
+
With `persistKey` the size is remembered in localStorage.
|
|
7
|
+
-->
|
|
8
|
+
<script lang="ts" module>
|
|
9
|
+
const STORAGE_PREFIX = 'classic-theme:resizable:';
|
|
10
|
+
|
|
11
|
+
export function clampSize(value: number, min: number, max: number): number {
|
|
12
|
+
return Math.min(Math.max(value, min), max);
|
|
13
|
+
}
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<script lang="ts">
|
|
17
|
+
import type { Snippet } from 'svelte';
|
|
18
|
+
import { cn } from '../utils.js';
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
/** Start pane share in percent (bindable) */
|
|
22
|
+
size?: number;
|
|
23
|
+
/** Smallest start share in percent (default 20) */
|
|
24
|
+
min?: number;
|
|
25
|
+
/** Largest start share in percent (default 80) */
|
|
26
|
+
max?: number;
|
|
27
|
+
/** Keyboard step in percent (default 5) */
|
|
28
|
+
step?: number;
|
|
29
|
+
/** `horizontal`: side by side (default); `vertical`: stacked */
|
|
30
|
+
orientation?: 'horizontal' | 'vertical';
|
|
31
|
+
/** Remember the size under this key */
|
|
32
|
+
persistKey?: string;
|
|
33
|
+
/** Accessible name for the handle (default "Resize panes") */
|
|
34
|
+
handleLabel?: string;
|
|
35
|
+
onResize?: (size: number) => void;
|
|
36
|
+
class?: string;
|
|
37
|
+
startClass?: string;
|
|
38
|
+
endClass?: string;
|
|
39
|
+
start: Snippet;
|
|
40
|
+
end: Snippet;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let {
|
|
44
|
+
size = $bindable(50),
|
|
45
|
+
min = 20,
|
|
46
|
+
max = 80,
|
|
47
|
+
step = 5,
|
|
48
|
+
orientation = 'horizontal',
|
|
49
|
+
persistKey,
|
|
50
|
+
handleLabel = 'Resize panes',
|
|
51
|
+
onResize,
|
|
52
|
+
class: className,
|
|
53
|
+
startClass,
|
|
54
|
+
endClass,
|
|
55
|
+
start,
|
|
56
|
+
end,
|
|
57
|
+
}: Props = $props();
|
|
58
|
+
|
|
59
|
+
const uid = $props.id();
|
|
60
|
+
let container = $state<HTMLDivElement | null>(null);
|
|
61
|
+
let dragging = $state(false);
|
|
62
|
+
const horizontal = $derived(orientation === 'horizontal');
|
|
63
|
+
|
|
64
|
+
// Read the remembered size once, after mount (no storage during SSR)
|
|
65
|
+
$effect(() => {
|
|
66
|
+
if (!persistKey) return;
|
|
67
|
+
try {
|
|
68
|
+
const stored = Number(localStorage.getItem(STORAGE_PREFIX + persistKey));
|
|
69
|
+
if (Number.isFinite(stored) && stored > 0) size = clampSize(stored, min, max);
|
|
70
|
+
} catch {
|
|
71
|
+
// Storage blocked: keep the default size
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
function set(next: number, save = true) {
|
|
76
|
+
const value = Math.round(clampSize(next, min, max) * 10) / 10;
|
|
77
|
+
if (value === size) return;
|
|
78
|
+
size = value;
|
|
79
|
+
onResize?.(value);
|
|
80
|
+
if (save) persist();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function persist() {
|
|
84
|
+
if (!persistKey) return;
|
|
85
|
+
try {
|
|
86
|
+
localStorage.setItem(STORAGE_PREFIX + persistKey, String(size));
|
|
87
|
+
} catch {
|
|
88
|
+
// Storage blocked: the size just isn't remembered
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function onkeydown(e: KeyboardEvent) {
|
|
93
|
+
const back = horizontal ? 'ArrowLeft' : 'ArrowUp';
|
|
94
|
+
const forward = horizontal ? 'ArrowRight' : 'ArrowDown';
|
|
95
|
+
if (e.key === back) set(size - step);
|
|
96
|
+
else if (e.key === forward) set(size + step);
|
|
97
|
+
else if (e.key === 'Home') set(min);
|
|
98
|
+
else if (e.key === 'End') set(max);
|
|
99
|
+
else return;
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function fromPointer(e: PointerEvent) {
|
|
104
|
+
if (!container) return;
|
|
105
|
+
const rect = container.getBoundingClientRect();
|
|
106
|
+
const total = horizontal ? rect.width : rect.height;
|
|
107
|
+
if (total <= 0) return;
|
|
108
|
+
const offset = horizontal ? e.clientX - rect.left : e.clientY - rect.top;
|
|
109
|
+
set((offset / total) * 100, false);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function onpointerdown(e: PointerEvent) {
|
|
113
|
+
if (e.button !== 0) return;
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
dragging = true;
|
|
116
|
+
(e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function onpointermove(e: PointerEvent) {
|
|
120
|
+
if (dragging) fromPointer(e);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function onpointerup(e: PointerEvent) {
|
|
124
|
+
if (!dragging) return;
|
|
125
|
+
dragging = false;
|
|
126
|
+
(e.currentTarget as HTMLElement).releasePointerCapture?.(e.pointerId);
|
|
127
|
+
persist();
|
|
128
|
+
}
|
|
129
|
+
</script>
|
|
130
|
+
|
|
131
|
+
<div
|
|
132
|
+
bind:this={container}
|
|
133
|
+
class={cn('flex h-full min-h-0 w-full min-w-0', horizontal ? 'flex-row' : 'flex-col', className)}
|
|
134
|
+
data-dragging={dragging || undefined}
|
|
135
|
+
>
|
|
136
|
+
<div
|
|
137
|
+
id={`${uid}-start`}
|
|
138
|
+
class={cn('min-h-0 min-w-0 shrink-0 overflow-hidden', startClass)}
|
|
139
|
+
style={`flex-basis:${size}%`}
|
|
140
|
+
>
|
|
141
|
+
{@render start()}
|
|
142
|
+
</div>
|
|
143
|
+
<!-- A focusable separator is an interactive widget (WAI-ARIA window splitter) -->
|
|
144
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
|
|
145
|
+
<div
|
|
146
|
+
role="separator"
|
|
147
|
+
tabindex="0"
|
|
148
|
+
aria-label={handleLabel}
|
|
149
|
+
aria-orientation={horizontal ? 'vertical' : 'horizontal'}
|
|
150
|
+
aria-controls={`${uid}-start`}
|
|
151
|
+
aria-valuenow={size}
|
|
152
|
+
aria-valuemin={min}
|
|
153
|
+
aria-valuemax={max}
|
|
154
|
+
class={cn(
|
|
155
|
+
'group relative z-10 flex shrink-0 touch-none select-none items-center justify-center bg-border outline-none transition-colors hover:bg-primary/40 focus-visible:bg-primary data-[dragging]:bg-primary',
|
|
156
|
+
horizontal ? 'w-px cursor-col-resize' : 'h-px cursor-row-resize'
|
|
157
|
+
)}
|
|
158
|
+
data-dragging={dragging || undefined}
|
|
159
|
+
{onkeydown}
|
|
160
|
+
{onpointerdown}
|
|
161
|
+
{onpointermove}
|
|
162
|
+
{onpointerup}
|
|
163
|
+
onpointercancel={onpointerup}
|
|
164
|
+
>
|
|
165
|
+
<!-- Wider invisible hit area around the 1px line -->
|
|
166
|
+
<span
|
|
167
|
+
class={cn(
|
|
168
|
+
'absolute',
|
|
169
|
+
horizontal ? 'inset-y-0 -left-1.5 -right-1.5' : 'inset-x-0 -top-1.5 -bottom-1.5'
|
|
170
|
+
)}
|
|
171
|
+
aria-hidden="true"
|
|
172
|
+
></span>
|
|
173
|
+
</div>
|
|
174
|
+
<div class={cn('min-h-0 min-w-0 flex-1 overflow-hidden', endClass)}>
|
|
175
|
+
{@render end()}
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{#if dragging}
|
|
180
|
+
<!-- Keeps the resize cursor and stops iframes in the panes from swallowing pointer moves -->
|
|
181
|
+
<div
|
|
182
|
+
class={cn('fixed inset-0 z-50', horizontal ? 'cursor-col-resize' : 'cursor-row-resize')}
|
|
183
|
+
aria-hidden="true"
|
|
184
|
+
></div>
|
|
185
|
+
{/if}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare function clampSize(value: number, min: number, max: number): number;
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** Start pane share in percent (bindable) */
|
|
5
|
+
size?: number;
|
|
6
|
+
/** Smallest start share in percent (default 20) */
|
|
7
|
+
min?: number;
|
|
8
|
+
/** Largest start share in percent (default 80) */
|
|
9
|
+
max?: number;
|
|
10
|
+
/** Keyboard step in percent (default 5) */
|
|
11
|
+
step?: number;
|
|
12
|
+
/** `horizontal`: side by side (default); `vertical`: stacked */
|
|
13
|
+
orientation?: 'horizontal' | 'vertical';
|
|
14
|
+
/** Remember the size under this key */
|
|
15
|
+
persistKey?: string;
|
|
16
|
+
/** Accessible name for the handle (default "Resize panes") */
|
|
17
|
+
handleLabel?: string;
|
|
18
|
+
onResize?: (size: number) => void;
|
|
19
|
+
class?: string;
|
|
20
|
+
startClass?: string;
|
|
21
|
+
endClass?: string;
|
|
22
|
+
start: Snippet;
|
|
23
|
+
end: Snippet;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Two panes split by a draggable handle. `size` is the start pane's share in
|
|
27
|
+
* percent; the end pane takes the rest. The handle is a focusable
|
|
28
|
+
* `separator`: arrow keys move it by `step`, Home/End jump to `min`/`max`.
|
|
29
|
+
* With `persistKey` the size is remembered in localStorage.
|
|
30
|
+
*/
|
|
31
|
+
declare const ResizablePanes: import("svelte").Component<Props, {}, "size">;
|
|
32
|
+
type ResizablePanes = ReturnType<typeof ResizablePanes>;
|
|
33
|
+
export default ResizablePanes;
|
|
@@ -51,6 +51,8 @@
|
|
|
51
51
|
* renders one once there are at least that many options.
|
|
52
52
|
*/
|
|
53
53
|
searchable?: boolean | number;
|
|
54
|
+
/** With `searchable`: render at most this many matches (see Combobox) */
|
|
55
|
+
maxResults?: number;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
let {
|
|
@@ -69,6 +71,7 @@
|
|
|
69
71
|
'aria-label': ariaLabelProp,
|
|
70
72
|
'aria-labelledby': ariaLabelledby,
|
|
71
73
|
searchable = false,
|
|
74
|
+
maxResults,
|
|
72
75
|
}: Props = $props();
|
|
73
76
|
|
|
74
77
|
// Generate unique IDs for accessibility using sequential counter
|
|
@@ -291,6 +294,7 @@
|
|
|
291
294
|
{id}
|
|
292
295
|
{error}
|
|
293
296
|
{hint}
|
|
297
|
+
{maxResults}
|
|
294
298
|
aria-label={ariaLabelProp}
|
|
295
299
|
aria-labelledby={ariaLabelledby}
|
|
296
300
|
class={className}
|
|
@@ -41,6 +41,8 @@ interface Props {
|
|
|
41
41
|
* renders one once there are at least that many options.
|
|
42
42
|
*/
|
|
43
43
|
searchable?: boolean | number;
|
|
44
|
+
/** With `searchable`: render at most this many matches (see Combobox) */
|
|
45
|
+
maxResults?: number;
|
|
44
46
|
}
|
|
45
47
|
declare const Select: import("svelte").Component<Props, {}, "value">;
|
|
46
48
|
type Select = ReturnType<typeof Select>;
|
|
@@ -20,6 +20,17 @@
|
|
|
20
20
|
/** Whether the textarea 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 & { currentTarget: HTMLTextAreaElement }) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Grow with the content instead of showing a resize handle. `rows` is the
|
|
27
|
+
* minimum height; past `maxRows` the textarea scrolls.
|
|
28
|
+
*/
|
|
29
|
+
autosize?: boolean;
|
|
30
|
+
/** Upper bound for `autosize`, in rows (default 8) */
|
|
31
|
+
maxRows?: number;
|
|
32
|
+
/** The textarea element (bindable), e.g. to focus it */
|
|
33
|
+
ref?: HTMLTextAreaElement | null;
|
|
23
34
|
[key: string]: unknown;
|
|
24
35
|
}
|
|
25
36
|
|
|
@@ -38,17 +49,50 @@
|
|
|
38
49
|
'aria-describedby': ariaDescribedby,
|
|
39
50
|
'aria-invalid': ariaInvalid,
|
|
40
51
|
onValueChange,
|
|
52
|
+
oninput,
|
|
53
|
+
autosize = false,
|
|
54
|
+
maxRows = 8,
|
|
55
|
+
ref = $bindable(null),
|
|
41
56
|
...restProps
|
|
42
57
|
}: Props = $props();
|
|
43
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Fit the height to the content: measure at `auto`, then clamp between
|
|
61
|
+
* `rows` and `maxRows` lines (plus padding and border, read from the
|
|
62
|
+
* computed style so consumer classes are respected).
|
|
63
|
+
*/
|
|
64
|
+
function fit(el: HTMLTextAreaElement) {
|
|
65
|
+
const style = getComputedStyle(el);
|
|
66
|
+
const lineHeight = parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.5 || 20;
|
|
67
|
+
const chrome =
|
|
68
|
+
parseFloat(style.paddingTop) +
|
|
69
|
+
parseFloat(style.paddingBottom) +
|
|
70
|
+
parseFloat(style.borderTopWidth) +
|
|
71
|
+
parseFloat(style.borderBottomWidth);
|
|
72
|
+
const min = rows * lineHeight + chrome;
|
|
73
|
+
const max = Math.max(rows, maxRows) * lineHeight + chrome;
|
|
74
|
+
el.style.height = 'auto';
|
|
75
|
+
const next = Math.min(Math.max(el.scrollHeight, min), max);
|
|
76
|
+
el.style.height = `${next}px`;
|
|
77
|
+
el.style.overflowY = el.scrollHeight > max ? 'auto' : 'hidden';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
$effect(() => {
|
|
81
|
+
// Re-fit when the value changes from outside too (cleared after send, prefilled)
|
|
82
|
+
void value;
|
|
83
|
+
if (autosize && ref) fit(ref);
|
|
84
|
+
});
|
|
85
|
+
|
|
44
86
|
function handleInput(e: Event) {
|
|
45
87
|
const target = e.target as HTMLTextAreaElement;
|
|
46
88
|
value = target.value;
|
|
47
89
|
onValueChange?.(target.value);
|
|
90
|
+
oninput?.(e as Event & { currentTarget: HTMLTextAreaElement });
|
|
48
91
|
}
|
|
49
92
|
</script>
|
|
50
93
|
|
|
51
94
|
<textarea
|
|
95
|
+
bind:this={ref}
|
|
52
96
|
{value}
|
|
53
97
|
{placeholder}
|
|
54
98
|
{disabled}
|
|
@@ -63,6 +107,7 @@
|
|
|
63
107
|
aria-invalid={ariaInvalid}
|
|
64
108
|
class={cn(
|
|
65
109
|
'flex min-h-[80px] w-full rounded-md border border-input bg-background text-foreground px-3 py-2 text-base sm:text-sm ring-offset-background placeholder:text-gray-500 dark:placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 resize-y',
|
|
110
|
+
autosize && 'min-h-0 resize-none',
|
|
66
111
|
ariaInvalid === true || ariaInvalid === 'true'
|
|
67
112
|
? 'border-destructive focus-visible:ring-destructive'
|
|
68
113
|
: '',
|
|
@@ -17,8 +17,21 @@ interface Props {
|
|
|
17
17
|
/** Whether the textarea has an error */
|
|
18
18
|
'aria-invalid'?: boolean | 'true' | 'false';
|
|
19
19
|
onValueChange?: (value: string) => void;
|
|
20
|
+
/** Native input listener; runs after the value is updated */
|
|
21
|
+
oninput?: (e: Event & {
|
|
22
|
+
currentTarget: HTMLTextAreaElement;
|
|
23
|
+
}) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Grow with the content instead of showing a resize handle. `rows` is the
|
|
26
|
+
* minimum height; past `maxRows` the textarea scrolls.
|
|
27
|
+
*/
|
|
28
|
+
autosize?: boolean;
|
|
29
|
+
/** Upper bound for `autosize`, in rows (default 8) */
|
|
30
|
+
maxRows?: number;
|
|
31
|
+
/** The textarea element (bindable), e.g. to focus it */
|
|
32
|
+
ref?: HTMLTextAreaElement | null;
|
|
20
33
|
[key: string]: unknown;
|
|
21
34
|
}
|
|
22
|
-
declare const Textarea: import("svelte").Component<Props, {}, "value">;
|
|
35
|
+
declare const Textarea: import("svelte").Component<Props, {}, "value" | "ref">;
|
|
23
36
|
type Textarea = ReturnType<typeof Textarea>;
|
|
24
37
|
export default Textarea;
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export { default as Checkbox } from './components/Checkbox.svelte';
|
|
|
17
17
|
export { default as Switch } from './components/Switch.svelte';
|
|
18
18
|
export { default as Separator } from './components/Separator.svelte';
|
|
19
19
|
export { default as Icon, type IconName } from './components/Icon.svelte';
|
|
20
|
+
export { default as CopyButton } from './components/CopyButton.svelte';
|
|
21
|
+
export { default as Kbd, formatKey, isApplePlatform } from './components/Kbd.svelte';
|
|
20
22
|
export { default as Card } from './components/Card.svelte';
|
|
21
23
|
export { default as CardHeader } from './components/CardHeader.svelte';
|
|
22
24
|
export { default as CardTitle } from './components/CardTitle.svelte';
|
|
@@ -35,14 +37,19 @@ export { default as ProgressBar } from './components/ProgressBar.svelte';
|
|
|
35
37
|
export { default as LoadingOverlay } from './components/LoadingOverlay.svelte';
|
|
36
38
|
export { default as LoadingPage } from './components/LoadingPage.svelte';
|
|
37
39
|
export { default as LoadingSection } from './components/LoadingSection.svelte';
|
|
40
|
+
export { default as EmptyState } from './components/EmptyState.svelte';
|
|
38
41
|
export { default as LiveRegion } from './components/LiveRegion.svelte';
|
|
39
42
|
export { default as SkipLink } from './components/SkipLink.svelte';
|
|
40
43
|
export { default as Dialog } from './components/Dialog.svelte';
|
|
41
44
|
export { default as AlertDialog } from './components/AlertDialog.svelte';
|
|
42
45
|
export { default as Tooltip } from './components/Tooltip.svelte';
|
|
46
|
+
export { default as Popover } from './components/Popover.svelte';
|
|
47
|
+
export { default as Collapsible } from './components/Collapsible.svelte';
|
|
43
48
|
export { default as DropdownMenu } from './components/DropdownMenu.svelte';
|
|
49
|
+
export { default as ContextMenu } from './components/ContextMenu.svelte';
|
|
44
50
|
export { default as AppLauncher, type AppLauncherItem } from './components/AppLauncher.svelte';
|
|
45
51
|
export { default as AppLauncherGrid } from './components/AppLauncherGrid.svelte';
|
|
52
|
+
export { default as ResizablePanes, clampSize } from './components/ResizablePanes.svelte';
|
|
46
53
|
export { default as DataPanel } from './components/DataPanel.svelte';
|
|
47
54
|
export { default as DataPanelHeader } from './components/DataPanelHeader.svelte';
|
|
48
55
|
export { default as DataPanelContent } from './components/DataPanelContent.svelte';
|
package/dist/lib/index.js
CHANGED
|
@@ -18,6 +18,8 @@ export { default as Checkbox } from './components/Checkbox.svelte';
|
|
|
18
18
|
export { default as Switch } from './components/Switch.svelte';
|
|
19
19
|
export { default as Separator } from './components/Separator.svelte';
|
|
20
20
|
export { default as Icon } from './components/Icon.svelte';
|
|
21
|
+
export { default as CopyButton } from './components/CopyButton.svelte';
|
|
22
|
+
export { default as Kbd, formatKey, isApplePlatform } from './components/Kbd.svelte';
|
|
21
23
|
// Card Components
|
|
22
24
|
export { default as Card } from './components/Card.svelte';
|
|
23
25
|
export { default as CardHeader } from './components/CardHeader.svelte';
|
|
@@ -41,6 +43,7 @@ export { default as ProgressBar } from './components/ProgressBar.svelte';
|
|
|
41
43
|
export { default as LoadingOverlay } from './components/LoadingOverlay.svelte';
|
|
42
44
|
export { default as LoadingPage } from './components/LoadingPage.svelte';
|
|
43
45
|
export { default as LoadingSection } from './components/LoadingSection.svelte';
|
|
46
|
+
export { default as EmptyState } from './components/EmptyState.svelte';
|
|
44
47
|
// Accessibility Components
|
|
45
48
|
export { default as LiveRegion } from './components/LiveRegion.svelte';
|
|
46
49
|
export { default as SkipLink } from './components/SkipLink.svelte';
|
|
@@ -48,9 +51,14 @@ export { default as SkipLink } from './components/SkipLink.svelte';
|
|
|
48
51
|
export { default as Dialog } from './components/Dialog.svelte';
|
|
49
52
|
export { default as AlertDialog } from './components/AlertDialog.svelte';
|
|
50
53
|
export { default as Tooltip } from './components/Tooltip.svelte';
|
|
54
|
+
export { default as Popover } from './components/Popover.svelte';
|
|
55
|
+
export { default as Collapsible } from './components/Collapsible.svelte';
|
|
51
56
|
export { default as DropdownMenu } from './components/DropdownMenu.svelte';
|
|
57
|
+
export { default as ContextMenu } from './components/ContextMenu.svelte';
|
|
52
58
|
export { default as AppLauncher } from './components/AppLauncher.svelte';
|
|
53
59
|
export { default as AppLauncherGrid } from './components/AppLauncherGrid.svelte';
|
|
60
|
+
// Layout primitives
|
|
61
|
+
export { default as ResizablePanes, clampSize } from './components/ResizablePanes.svelte';
|
|
54
62
|
// DataPanel Components
|
|
55
63
|
export { default as DataPanel } from './components/DataPanel.svelte';
|
|
56
64
|
export { default as DataPanelHeader } from './components/DataPanelHeader.svelte';
|