@classic-homes/theme-svelte 1.19.1 → 1.20.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/__tests__/code-field.test.js +65 -0
- package/dist/lib/components/CodeField.svelte +143 -0
- package/dist/lib/components/CodeField.svelte.d.ts +32 -0
- package/dist/lib/components/Dialog.svelte +7 -0
- package/dist/lib/components/Dialog.svelte.d.ts +5 -0
- package/dist/lib/components/Icon.svelte +31 -1
- package/dist/lib/components/Icon.svelte.d.ts +1 -1
- package/dist/lib/components/studio/InsertPalette.svelte +14 -1
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeField: Tab indents instead of leaving the field, with Escape as the way
|
|
3
|
+
* out, and indenting a selection works line by line.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
6
|
+
import { mount, unmount, flushSync } from 'svelte';
|
|
7
|
+
import CodeField from '../lib/components/CodeField.svelte';
|
|
8
|
+
let instance = null;
|
|
9
|
+
function render(props) {
|
|
10
|
+
const host = document.createElement('div');
|
|
11
|
+
document.body.appendChild(host);
|
|
12
|
+
instance = mount(CodeField, { target: host, props });
|
|
13
|
+
flushSync();
|
|
14
|
+
return host.querySelector('textarea');
|
|
15
|
+
}
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
if (instance)
|
|
18
|
+
unmount(instance);
|
|
19
|
+
instance = null;
|
|
20
|
+
document.body.innerHTML = '';
|
|
21
|
+
});
|
|
22
|
+
function press(field, key, shiftKey = false) {
|
|
23
|
+
const event = new KeyboardEvent('keydown', { key, shiftKey, bubbles: true, cancelable: true });
|
|
24
|
+
field.dispatchEvent(event);
|
|
25
|
+
return event;
|
|
26
|
+
}
|
|
27
|
+
describe('CodeField', () => {
|
|
28
|
+
it('is a labelled, monospaced, unspellchecked field with its keyboard hint', () => {
|
|
29
|
+
const field = render({ value: 'graph TD', label: 'Diagram source' });
|
|
30
|
+
expect(field.getAttribute('aria-label')).toBe('Diagram source');
|
|
31
|
+
expect(field.getAttribute('spellcheck')).toBe('false');
|
|
32
|
+
expect(field.className).toContain('font-mono');
|
|
33
|
+
const hint = document.getElementById(field.getAttribute('aria-describedby').split(' ')[0]);
|
|
34
|
+
expect(hint?.textContent).toMatch(/Escape, then Tab/);
|
|
35
|
+
});
|
|
36
|
+
it('indents at the cursor on Tab and reports the new value', () => {
|
|
37
|
+
const onValueChange = vi.fn();
|
|
38
|
+
const field = render({ value: 'ab', label: 'Code', onValueChange });
|
|
39
|
+
field.setSelectionRange(1, 1);
|
|
40
|
+
expect(press(field, 'Tab').defaultPrevented).toBe(true);
|
|
41
|
+
expect(field.value).toBe('a b');
|
|
42
|
+
expect(onValueChange).toHaveBeenLastCalledWith('a b');
|
|
43
|
+
});
|
|
44
|
+
it('indents and outdents every selected line', () => {
|
|
45
|
+
const field = render({ value: 'one\ntwo\nthree', label: 'Code' });
|
|
46
|
+
field.setSelectionRange(0, 7);
|
|
47
|
+
press(field, 'Tab');
|
|
48
|
+
expect(field.value).toBe(' one\n two\nthree');
|
|
49
|
+
field.setSelectionRange(0, 11);
|
|
50
|
+
press(field, 'Tab', true);
|
|
51
|
+
expect(field.value).toBe('one\ntwo\nthree');
|
|
52
|
+
});
|
|
53
|
+
it('lets Tab leave after Escape', () => {
|
|
54
|
+
const field = render({ value: 'x', label: 'Code' });
|
|
55
|
+
press(field, 'Escape');
|
|
56
|
+
expect(press(field, 'Tab').defaultPrevented).toBe(false);
|
|
57
|
+
expect(field.value).toBe('x');
|
|
58
|
+
// and only once
|
|
59
|
+
expect(press(field, 'Tab').defaultPrevented).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
it('does not indent a read-only field', () => {
|
|
62
|
+
const field = render({ value: 'x', label: 'Code', readonly: true });
|
|
63
|
+
expect(press(field, 'Tab').defaultPrevented).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../utils.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CodeField - a monospaced textarea for code, markup or diagram source.
|
|
6
|
+
*
|
|
7
|
+
* Tab indents (the selected lines, or at the cursor) and Shift+Tab
|
|
8
|
+
* outdents, as in a code editor. So keyboard users are never trapped,
|
|
9
|
+
* Escape releases Tab for one press: Escape, then Tab, moves focus on —
|
|
10
|
+
* the hint saying so is announced with the field. No syntax highlighting:
|
|
11
|
+
* this is a field, not an IDE.
|
|
12
|
+
*/
|
|
13
|
+
interface Props {
|
|
14
|
+
value?: string;
|
|
15
|
+
/** Accessible name. Use this or aria-labelledby. */
|
|
16
|
+
label?: string;
|
|
17
|
+
id?: string;
|
|
18
|
+
rows?: number;
|
|
19
|
+
/** Spaces inserted per indent level (default 2). */
|
|
20
|
+
indent?: number;
|
|
21
|
+
/** Soft-wrap long lines (default false: scroll sideways, as code reads best). */
|
|
22
|
+
wrap?: boolean;
|
|
23
|
+
placeholder?: string;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
readonly?: boolean;
|
|
26
|
+
'aria-labelledby'?: string;
|
|
27
|
+
'aria-describedby'?: string;
|
|
28
|
+
'aria-invalid'?: boolean | 'true' | 'false';
|
|
29
|
+
class?: string;
|
|
30
|
+
onValueChange?: (value: string) => void;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let {
|
|
35
|
+
value = $bindable(''),
|
|
36
|
+
label,
|
|
37
|
+
id,
|
|
38
|
+
rows = 8,
|
|
39
|
+
indent = 2,
|
|
40
|
+
wrap = false,
|
|
41
|
+
placeholder,
|
|
42
|
+
disabled = false,
|
|
43
|
+
readonly = false,
|
|
44
|
+
'aria-labelledby': ariaLabelledby,
|
|
45
|
+
'aria-describedby': ariaDescribedby,
|
|
46
|
+
'aria-invalid': ariaInvalid,
|
|
47
|
+
class: className,
|
|
48
|
+
onValueChange,
|
|
49
|
+
...restProps
|
|
50
|
+
}: Props = $props();
|
|
51
|
+
|
|
52
|
+
const uid = $props.id();
|
|
53
|
+
const hintId = `${uid}-hint`;
|
|
54
|
+
/** Set by Escape: the next Tab leaves the field instead of indenting. */
|
|
55
|
+
let releaseTab = false;
|
|
56
|
+
|
|
57
|
+
function emit(target: HTMLTextAreaElement) {
|
|
58
|
+
value = target.value;
|
|
59
|
+
onValueChange?.(target.value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Replace a range through the browser's editing pipeline when it can, so
|
|
64
|
+
* the change lands on the native undo stack.
|
|
65
|
+
*/
|
|
66
|
+
function replace(target: HTMLTextAreaElement, start: number, end: number, text: string) {
|
|
67
|
+
target.setSelectionRange(start, end);
|
|
68
|
+
const viaEditing =
|
|
69
|
+
typeof document.execCommand === 'function' && document.execCommand('insertText', false, text);
|
|
70
|
+
if (!viaEditing) target.setRangeText(text, start, end, 'end');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
74
|
+
const target = event.currentTarget as HTMLTextAreaElement;
|
|
75
|
+
if (event.key === 'Escape') {
|
|
76
|
+
releaseTab = true;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (event.key !== 'Tab' || readonly || disabled) {
|
|
80
|
+
releaseTab = false;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (releaseTab) {
|
|
84
|
+
releaseTab = false;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
|
|
89
|
+
const pad = ' '.repeat(indent);
|
|
90
|
+
const { selectionStart: start, selectionEnd: end, value: text } = target;
|
|
91
|
+
const lineStart = text.lastIndexOf('\n', start - 1) + 1;
|
|
92
|
+
const multiline = text.slice(start, end).includes('\n');
|
|
93
|
+
|
|
94
|
+
if (!event.shiftKey && !multiline) {
|
|
95
|
+
replace(target, start, end, pad);
|
|
96
|
+
} else {
|
|
97
|
+
// Indent or outdent every line the selection touches.
|
|
98
|
+
const lineEnd = end > start && text[end - 1] === '\n' ? end - 1 : end;
|
|
99
|
+
const block = text.slice(lineStart, lineEnd);
|
|
100
|
+
const outdent = new RegExp(`^ {1,${indent}}`, 'gm');
|
|
101
|
+
const changed = event.shiftKey ? block.replace(outdent, '') : block.replace(/^/gm, pad);
|
|
102
|
+
if (changed === block) return;
|
|
103
|
+
const firstLineShift = event.shiftKey
|
|
104
|
+
? -(block.match(new RegExp(`^ {1,${indent}}`))?.[0].length ?? 0)
|
|
105
|
+
: indent;
|
|
106
|
+
replace(target, lineStart, lineEnd, changed);
|
|
107
|
+
target.setSelectionRange(
|
|
108
|
+
Math.max(lineStart, start + firstLineShift),
|
|
109
|
+
lineEnd + (changed.length - block.length)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
emit(target);
|
|
113
|
+
}
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<textarea
|
|
117
|
+
{value}
|
|
118
|
+
{id}
|
|
119
|
+
{rows}
|
|
120
|
+
{placeholder}
|
|
121
|
+
{disabled}
|
|
122
|
+
{readonly}
|
|
123
|
+
spellcheck="false"
|
|
124
|
+
autocapitalize="off"
|
|
125
|
+
autocomplete="off"
|
|
126
|
+
wrap={wrap ? 'soft' : 'off'}
|
|
127
|
+
aria-label={label}
|
|
128
|
+
aria-labelledby={ariaLabelledby}
|
|
129
|
+
aria-describedby={[hintId, ariaDescribedby].filter(Boolean).join(' ')}
|
|
130
|
+
aria-invalid={ariaInvalid}
|
|
131
|
+
class={cn(
|
|
132
|
+
'flex w-full rounded-md border border-input bg-background px-3 py-2 font-mono text-sm leading-relaxed text-foreground ring-offset-background [tab-size:2] placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:placeholder:text-gray-400 resize-y',
|
|
133
|
+
!wrap && 'whitespace-pre overflow-x-auto',
|
|
134
|
+
ariaInvalid === true || ariaInvalid === 'true'
|
|
135
|
+
? 'border-destructive focus-visible:ring-destructive'
|
|
136
|
+
: '',
|
|
137
|
+
className
|
|
138
|
+
)}
|
|
139
|
+
oninput={(event) => emit(event.currentTarget)}
|
|
140
|
+
onkeydown={handleKeydown}
|
|
141
|
+
{...restProps}
|
|
142
|
+
></textarea>
|
|
143
|
+
<span id={hintId} class="sr-only">Tab indents. Press Escape, then Tab, to leave the field.</span>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeField - a monospaced textarea for code, markup or diagram source.
|
|
3
|
+
*
|
|
4
|
+
* Tab indents (the selected lines, or at the cursor) and Shift+Tab
|
|
5
|
+
* outdents, as in a code editor. So keyboard users are never trapped,
|
|
6
|
+
* Escape releases Tab for one press: Escape, then Tab, moves focus on —
|
|
7
|
+
* the hint saying so is announced with the field. No syntax highlighting:
|
|
8
|
+
* this is a field, not an IDE.
|
|
9
|
+
*/
|
|
10
|
+
interface Props {
|
|
11
|
+
value?: string;
|
|
12
|
+
/** Accessible name. Use this or aria-labelledby. */
|
|
13
|
+
label?: string;
|
|
14
|
+
id?: string;
|
|
15
|
+
rows?: number;
|
|
16
|
+
/** Spaces inserted per indent level (default 2). */
|
|
17
|
+
indent?: number;
|
|
18
|
+
/** Soft-wrap long lines (default false: scroll sideways, as code reads best). */
|
|
19
|
+
wrap?: boolean;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
readonly?: boolean;
|
|
23
|
+
'aria-labelledby'?: string;
|
|
24
|
+
'aria-describedby'?: string;
|
|
25
|
+
'aria-invalid'?: boolean | 'true' | 'false';
|
|
26
|
+
class?: string;
|
|
27
|
+
onValueChange?: (value: string) => void;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
declare const CodeField: import("svelte").Component<Props, {}, "value">;
|
|
31
|
+
type CodeField = ReturnType<typeof CodeField>;
|
|
32
|
+
export default CodeField;
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
headerActions?: Snippet;
|
|
29
29
|
/** Whether to show the close button (default: true) */
|
|
30
30
|
showClose?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Called before focus returns to the trigger on close; `preventDefault()`
|
|
33
|
+
* keeps focus where the caller put it (e.g. a field it just revealed).
|
|
34
|
+
*/
|
|
35
|
+
onCloseAutoFocus?: (event: Event) => void;
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
// Every size is clamped to the viewport minus a 1rem gutter on phones; the
|
|
@@ -54,6 +59,7 @@
|
|
|
54
59
|
header,
|
|
55
60
|
headerActions,
|
|
56
61
|
showClose = true,
|
|
62
|
+
onCloseAutoFocus,
|
|
57
63
|
}: Props = $props();
|
|
58
64
|
|
|
59
65
|
function handleOpenChange(newOpen: boolean) {
|
|
@@ -83,6 +89,7 @@
|
|
|
83
89
|
class="fixed inset-0 z-50 bg-foreground/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
|
84
90
|
/>
|
|
85
91
|
<DialogPrimitive.Content
|
|
92
|
+
{onCloseAutoFocus}
|
|
86
93
|
class={cn(
|
|
87
94
|
'fixed left-[50%] top-[50%] z-50 flex max-h-[calc(100dvh-2rem)] w-full translate-x-[-50%] translate-y-[-50%] flex-col gap-4 border border-border bg-background 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%] sm:rounded-lg overflow-hidden',
|
|
88
95
|
sizeClasses[size],
|
|
@@ -24,6 +24,11 @@ interface Props {
|
|
|
24
24
|
headerActions?: Snippet;
|
|
25
25
|
/** Whether to show the close button (default: true) */
|
|
26
26
|
showClose?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Called before focus returns to the trigger on close; `preventDefault()`
|
|
29
|
+
* keeps focus where the caller put it (e.g. a field it just revealed).
|
|
30
|
+
*/
|
|
31
|
+
onCloseAutoFocus?: (event: Event) => void;
|
|
27
32
|
}
|
|
28
33
|
declare const Dialog: import("svelte").Component<Props, {}, "open">;
|
|
29
34
|
type Dialog = ReturnType<typeof Dialog>;
|
|
@@ -256,7 +256,18 @@
|
|
|
256
256
|
| 'log-out'
|
|
257
257
|
| 'slash'
|
|
258
258
|
| 'percent'
|
|
259
|
-
| 'hash'
|
|
259
|
+
| 'hash'
|
|
260
|
+
// Text editing
|
|
261
|
+
| 'bold'
|
|
262
|
+
| 'italic'
|
|
263
|
+
| 'heading'
|
|
264
|
+
| 'list-ordered'
|
|
265
|
+
| 'indent'
|
|
266
|
+
| 'outdent'
|
|
267
|
+
| 'quote'
|
|
268
|
+
| 'table'
|
|
269
|
+
| 'keyboard'
|
|
270
|
+
| 'unlink';
|
|
260
271
|
|
|
261
272
|
interface Props {
|
|
262
273
|
/** The name of the icon to display */
|
|
@@ -891,6 +902,25 @@
|
|
|
891
902
|
slash: {
|
|
892
903
|
path: 'M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zM4.93 4.93l14.14 14.14',
|
|
893
904
|
},
|
|
905
|
+
// === Text editing ===
|
|
906
|
+
bold: { path: 'M6 4h8a4 4 0 0 1 0 8H6zM6 12h9a4 4 0 0 1 0 8H6z' },
|
|
907
|
+
italic: { path: 'M19 4h-9M14 20H5M15 4L9 20' },
|
|
908
|
+
heading: { path: 'M6 12h12M6 20V4M18 20V4' },
|
|
909
|
+
'list-ordered': {
|
|
910
|
+
path: 'M10 6h11M10 12h11M10 18h11M4 6h1v4M4 10h2M6 18H4c0-1 2-2 2-3s-1-1.5-2-1',
|
|
911
|
+
},
|
|
912
|
+
indent: { path: 'M3 8l4 4-4 4M21 12H11M21 6H11M21 18H11' },
|
|
913
|
+
outdent: { path: 'M7 8l-4 4 4 4M21 12H11M21 6H11M21 18H11' },
|
|
914
|
+
quote: {
|
|
915
|
+
path: 'M3 21c3 0 7-1 7-8V5c0-1.25-.76-2-2-2H4c-1.25 0-2 .75-2 2v6c0 1.25.75 2 2 2h1c0 2.5-.5 4-3 4v4zM15 21c3 0 7-1 7-8V5c0-1.25-.76-2-2-2h-4c-1.25 0-2 .75-2 2v6c0 1.25.75 2 2 2h1c0 2.5-.5 4-3 4v4z',
|
|
916
|
+
},
|
|
917
|
+
table: { path: 'M3 3h18v18H3zM3 9h18M3 15h18M9 3v18M15 3v18' },
|
|
918
|
+
keyboard: {
|
|
919
|
+
path: 'M20 5H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2zM6 9h.01M10 9h.01M14 9h.01M18 9h.01M8 13h.01M12 13h.01M16 13h.01M7 16h10',
|
|
920
|
+
},
|
|
921
|
+
unlink: {
|
|
922
|
+
path: 'M18.84 12.25l1.72-1.71a5 5 0 0 0-7.07-7.07l-1.72 1.71M5.17 11.75l-1.71 1.71a5 5 0 0 0 7.07 7.07l1.71-1.71M8 2v3M2 8h3M16 22v-3M22 16h-3',
|
|
923
|
+
},
|
|
894
924
|
};
|
|
895
925
|
|
|
896
926
|
const iconDef = $derived(icons[name]);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Available icon names for the Icon component.
|
|
3
3
|
* Organized by category for easier discovery.
|
|
4
4
|
*/
|
|
5
|
-
export type IconName = 'close' | 'check' | 'plus' | 'minus' | 'edit' | 'trash' | 'info' | 'alert-circle' | 'alert-triangle' | 'search' | 'settings' | 'home' | 'loading' | 'send' | 'more' | 'more-horizontal' | 'grip' | 'history' | 'copy' | 'download' | 'upload' | 'refresh' | 'save' | 'print' | 'share' | 'cut' | 'paste' | 'undo' | 'redo' | 'archive' | 'power' | 'zap' | 'sparkles' | 'pin' | 'chevron-down' | 'chevron-up' | 'chevron-left' | 'chevron-right' | 'chevrons-down' | 'chevrons-up' | 'chevrons-left' | 'chevrons-right' | 'arrow-left' | 'arrow-right' | 'arrow-up' | 'arrow-down' | 'corner-up-left' | 'corner-up-right' | 'corner-down-left' | 'corner-down-right' | 'menu' | 'external-link' | 'expand' | 'collapse' | 'fullscreen' | 'minimize' | 'user' | 'users' | 'user-plus' | 'user-minus' | 'user-check' | 'user-x' | 'shield' | 'shield-check' | 'shield-off' | 'badge' | 'id-card' | 'message' | 'chat' | 'mail' | 'phone' | 'bell' | 'bell-off' | 'inbox' | 'at-sign' | 'hash' | 'rss' | 'send' | 'message-circle' | 'message-square' | 'file' | 'file-text' | 'file-code' | 'file-plus' | 'file-minus' | 'folder' | 'folder-open' | 'folder-plus' | 'folder-minus' | 'clipboard' | 'clipboard-check' | 'clipboard-list' | 'book' | 'book-open' | 'newspaper' | 'link' | 'image' | 'play' | 'pause' | 'stop' | 'skip-forward' | 'skip-back' | 'rewind' | 'fast-forward' | 'volume' | 'volume-1' | 'volume-2' | 'volume-x' | 'video' | 'video-off' | 'mic' | 'mic-off' | 'music' | 'camera' | 'camera-off' | 'shopping-cart' | 'shopping-bag' | 'credit-card' | 'dollar-sign' | 'percent' | 'tag' | 'tags' | 'package' | 'truck' | 'receipt' | 'wallet' | 'briefcase' | 'eye' | 'eye-off' | 'lock' | 'unlock' | 'key' | 'check-circle' | 'x-circle' | 'help-circle' | 'alert-octagon' | 'circle' | 'square' | 'triangle' | 'star' | 'star-half' | 'heart' | 'bookmark' | 'flag' | 'thumbs-up' | 'thumbs-down' | 'award' | 'trophy' | 'gift' | 'grid' | 'list' | 'columns' | 'sidebar' | 'layers' | 'layout' | 'move' | 'maximize' | 'filter' | 'sort' | 'sort-asc' | 'sort-desc' | 'bar-chart' | 'bar-chart-2' | 'line-chart' | 'pie-chart' | 'trending-up' | 'trending-down' | 'activity' | 'target' | 'calendar' | 'calendar-plus' | 'calendar-minus' | 'calendar-check' | 'clock' | 'timer' | 'hourglass' | 'alarm' | 'sun' | 'moon' | 'cloud' | 'cloud-rain' | 'cloud-snow' | 'wind' | 'thermometer' | 'droplet' | 'umbrella' | 'globe' | 'map' | 'map-pin' | 'navigation' | 'compass' | 'crosshair' | 'smartphone' | 'tablet' | 'laptop' | 'monitor' | 'tv' | 'printer' | 'wifi' | 'wifi-off' | 'bluetooth' | 'battery' | 'battery-charging' | 'cpu' | 'hard-drive' | 'server' | 'database' | 'cloud-upload' | 'cloud-download' | 'terminal' | 'code' | 'code-2' | 'bug' | 'git-branch' | 'git-commit' | 'git-merge' | 'git-pull-request' | 'box' | 'package' | 'lightbulb' | 'tool' | 'wrench' | 'scissors' | 'paperclip' | 'anchor' | 'aperture' | 'loader' | 'rotate-cw' | 'rotate-ccw' | 'log-in' | 'log-out' | 'slash' | 'percent' | 'hash';
|
|
5
|
+
export type IconName = 'close' | 'check' | 'plus' | 'minus' | 'edit' | 'trash' | 'info' | 'alert-circle' | 'alert-triangle' | 'search' | 'settings' | 'home' | 'loading' | 'send' | 'more' | 'more-horizontal' | 'grip' | 'history' | 'copy' | 'download' | 'upload' | 'refresh' | 'save' | 'print' | 'share' | 'cut' | 'paste' | 'undo' | 'redo' | 'archive' | 'power' | 'zap' | 'sparkles' | 'pin' | 'chevron-down' | 'chevron-up' | 'chevron-left' | 'chevron-right' | 'chevrons-down' | 'chevrons-up' | 'chevrons-left' | 'chevrons-right' | 'arrow-left' | 'arrow-right' | 'arrow-up' | 'arrow-down' | 'corner-up-left' | 'corner-up-right' | 'corner-down-left' | 'corner-down-right' | 'menu' | 'external-link' | 'expand' | 'collapse' | 'fullscreen' | 'minimize' | 'user' | 'users' | 'user-plus' | 'user-minus' | 'user-check' | 'user-x' | 'shield' | 'shield-check' | 'shield-off' | 'badge' | 'id-card' | 'message' | 'chat' | 'mail' | 'phone' | 'bell' | 'bell-off' | 'inbox' | 'at-sign' | 'hash' | 'rss' | 'send' | 'message-circle' | 'message-square' | 'file' | 'file-text' | 'file-code' | 'file-plus' | 'file-minus' | 'folder' | 'folder-open' | 'folder-plus' | 'folder-minus' | 'clipboard' | 'clipboard-check' | 'clipboard-list' | 'book' | 'book-open' | 'newspaper' | 'link' | 'image' | 'play' | 'pause' | 'stop' | 'skip-forward' | 'skip-back' | 'rewind' | 'fast-forward' | 'volume' | 'volume-1' | 'volume-2' | 'volume-x' | 'video' | 'video-off' | 'mic' | 'mic-off' | 'music' | 'camera' | 'camera-off' | 'shopping-cart' | 'shopping-bag' | 'credit-card' | 'dollar-sign' | 'percent' | 'tag' | 'tags' | 'package' | 'truck' | 'receipt' | 'wallet' | 'briefcase' | 'eye' | 'eye-off' | 'lock' | 'unlock' | 'key' | 'check-circle' | 'x-circle' | 'help-circle' | 'alert-octagon' | 'circle' | 'square' | 'triangle' | 'star' | 'star-half' | 'heart' | 'bookmark' | 'flag' | 'thumbs-up' | 'thumbs-down' | 'award' | 'trophy' | 'gift' | 'grid' | 'list' | 'columns' | 'sidebar' | 'layers' | 'layout' | 'move' | 'maximize' | 'filter' | 'sort' | 'sort-asc' | 'sort-desc' | 'bar-chart' | 'bar-chart-2' | 'line-chart' | 'pie-chart' | 'trending-up' | 'trending-down' | 'activity' | 'target' | 'calendar' | 'calendar-plus' | 'calendar-minus' | 'calendar-check' | 'clock' | 'timer' | 'hourglass' | 'alarm' | 'sun' | 'moon' | 'cloud' | 'cloud-rain' | 'cloud-snow' | 'wind' | 'thermometer' | 'droplet' | 'umbrella' | 'globe' | 'map' | 'map-pin' | 'navigation' | 'compass' | 'crosshair' | 'smartphone' | 'tablet' | 'laptop' | 'monitor' | 'tv' | 'printer' | 'wifi' | 'wifi-off' | 'bluetooth' | 'battery' | 'battery-charging' | 'cpu' | 'hard-drive' | 'server' | 'database' | 'cloud-upload' | 'cloud-download' | 'terminal' | 'code' | 'code-2' | 'bug' | 'git-branch' | 'git-commit' | 'git-merge' | 'git-pull-request' | 'box' | 'package' | 'lightbulb' | 'tool' | 'wrench' | 'scissors' | 'paperclip' | 'anchor' | 'aperture' | 'loader' | 'rotate-cw' | 'rotate-ccw' | 'log-in' | 'log-out' | 'slash' | 'percent' | 'hash' | 'bold' | 'italic' | 'heading' | 'list-ordered' | 'indent' | 'outdent' | 'quote' | 'table' | 'keyboard' | 'unlink';
|
|
6
6
|
interface Props {
|
|
7
7
|
/** The name of the icon to display */
|
|
8
8
|
name: IconName;
|
|
@@ -56,6 +56,9 @@
|
|
|
56
56
|
}: Props = $props();
|
|
57
57
|
|
|
58
58
|
let query = $state('');
|
|
59
|
+
// Picking an item hands focus to whatever the caller opens next (the new
|
|
60
|
+
// item's first field); only Escape / close returns focus to the trigger.
|
|
61
|
+
let picked = false;
|
|
59
62
|
let listEl: HTMLDivElement | undefined = $state();
|
|
60
63
|
let searchEl: HTMLInputElement | undefined = $state();
|
|
61
64
|
|
|
@@ -79,11 +82,13 @@
|
|
|
79
82
|
$effect(() => {
|
|
80
83
|
if (!open) return;
|
|
81
84
|
query = '';
|
|
85
|
+
picked = false;
|
|
82
86
|
void tick().then(() => searchEl?.focus());
|
|
83
87
|
});
|
|
84
88
|
|
|
85
89
|
function choose(item: InsertPaletteItem) {
|
|
86
90
|
if (item.disabled) return;
|
|
91
|
+
picked = true;
|
|
87
92
|
open = false;
|
|
88
93
|
onSelect(item.id);
|
|
89
94
|
}
|
|
@@ -115,7 +120,15 @@
|
|
|
115
120
|
}
|
|
116
121
|
</script>
|
|
117
122
|
|
|
118
|
-
<Dialog
|
|
123
|
+
<Dialog
|
|
124
|
+
bind:open
|
|
125
|
+
{title}
|
|
126
|
+
{description}
|
|
127
|
+
size={thumbnail ? '2xl' : 'xl'}
|
|
128
|
+
onCloseAutoFocus={(e) => {
|
|
129
|
+
if (picked) e.preventDefault();
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
119
132
|
<div class="space-y-4 pb-4">
|
|
120
133
|
<div class="relative">
|
|
121
134
|
<Icon
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export { default as Input } from './components/Input.svelte';
|
|
|
12
12
|
export { default as PasswordInput } from './components/PasswordInput.svelte';
|
|
13
13
|
export { default as Label } from './components/Label.svelte';
|
|
14
14
|
export { default as Textarea } from './components/Textarea.svelte';
|
|
15
|
+
export { default as CodeField } from './components/CodeField.svelte';
|
|
15
16
|
export { default as Checkbox } from './components/Checkbox.svelte';
|
|
16
17
|
export { default as Switch } from './components/Switch.svelte';
|
|
17
18
|
export { default as Separator } from './components/Separator.svelte';
|
package/dist/lib/index.js
CHANGED
|
@@ -13,6 +13,7 @@ export { default as Input } from './components/Input.svelte';
|
|
|
13
13
|
export { default as PasswordInput } from './components/PasswordInput.svelte';
|
|
14
14
|
export { default as Label } from './components/Label.svelte';
|
|
15
15
|
export { default as Textarea } from './components/Textarea.svelte';
|
|
16
|
+
export { default as CodeField } from './components/CodeField.svelte';
|
|
16
17
|
export { default as Checkbox } from './components/Checkbox.svelte';
|
|
17
18
|
export { default as Switch } from './components/Switch.svelte';
|
|
18
19
|
export { default as Separator } from './components/Separator.svelte';
|