@nqmcreative/ui 0.2.0 → 0.2.2

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.
Files changed (141) hide show
  1. package/README.md +7 -5
  2. package/cli/index.mjs +16 -3
  3. package/dist/styles/matte/index.d.ts +1 -1
  4. package/dist/styles/sprout/Accordion.svelte +17 -0
  5. package/dist/styles/sprout/Accordion.svelte.d.ts +8 -0
  6. package/dist/styles/sprout/AccordionItem.svelte +46 -0
  7. package/dist/styles/sprout/AccordionItem.svelte.d.ts +14 -0
  8. package/dist/styles/sprout/Alert.svelte +100 -0
  9. package/dist/styles/sprout/Alert.svelte.d.ts +18 -0
  10. package/dist/styles/sprout/Avatar.svelte +60 -0
  11. package/dist/styles/sprout/Avatar.svelte.d.ts +16 -0
  12. package/dist/styles/sprout/AvatarGroup.svelte +17 -0
  13. package/dist/styles/sprout/AvatarGroup.svelte.d.ts +8 -0
  14. package/dist/styles/sprout/Badge.svelte +58 -0
  15. package/dist/styles/sprout/Badge.svelte.d.ts +16 -0
  16. package/dist/styles/sprout/Breadcrumb.svelte +49 -0
  17. package/dist/styles/sprout/Breadcrumb.svelte.d.ts +14 -0
  18. package/dist/styles/sprout/Button.svelte +105 -0
  19. package/dist/styles/sprout/Button.svelte.d.ts +21 -0
  20. package/dist/styles/sprout/CTASection.svelte +77 -0
  21. package/dist/styles/sprout/CTASection.svelte.d.ts +18 -0
  22. package/dist/styles/sprout/Calendar.svelte +202 -0
  23. package/dist/styles/sprout/Calendar.svelte.d.ts +22 -0
  24. package/dist/styles/sprout/Card.svelte +82 -0
  25. package/dist/styles/sprout/Card.svelte.d.ts +21 -0
  26. package/dist/styles/sprout/Checkbox.svelte +76 -0
  27. package/dist/styles/sprout/Checkbox.svelte.d.ts +15 -0
  28. package/dist/styles/sprout/CheckboxGroup.svelte +112 -0
  29. package/dist/styles/sprout/CheckboxGroup.svelte.d.ts +30 -0
  30. package/dist/styles/sprout/Combobox.svelte +267 -0
  31. package/dist/styles/sprout/Combobox.svelte.d.ts +29 -0
  32. package/dist/styles/sprout/CommandPalette.svelte +210 -0
  33. package/dist/styles/sprout/CommandPalette.svelte.d.ts +32 -0
  34. package/dist/styles/sprout/ConfirmDialog.svelte +97 -0
  35. package/dist/styles/sprout/ConfirmDialog.svelte.d.ts +22 -0
  36. package/dist/styles/sprout/ContextMenu.svelte +89 -0
  37. package/dist/styles/sprout/ContextMenu.svelte.d.ts +15 -0
  38. package/dist/styles/sprout/DatePicker.svelte +193 -0
  39. package/dist/styles/sprout/DatePicker.svelte.d.ts +23 -0
  40. package/dist/styles/sprout/Divider.svelte +30 -0
  41. package/dist/styles/sprout/Divider.svelte.d.ts +10 -0
  42. package/dist/styles/sprout/Drawer.svelte +166 -0
  43. package/dist/styles/sprout/Drawer.svelte.d.ts +20 -0
  44. package/dist/styles/sprout/Dropdown.svelte +95 -0
  45. package/dist/styles/sprout/Dropdown.svelte.d.ts +23 -0
  46. package/dist/styles/sprout/Dropzone.svelte +211 -0
  47. package/dist/styles/sprout/Dropzone.svelte.d.ts +29 -0
  48. package/dist/styles/sprout/EmptyState.svelte +71 -0
  49. package/dist/styles/sprout/EmptyState.svelte.d.ts +18 -0
  50. package/dist/styles/sprout/FeatureCard.svelte +61 -0
  51. package/dist/styles/sprout/FeatureCard.svelte.d.ts +16 -0
  52. package/dist/styles/sprout/FeatureGrid.svelte +62 -0
  53. package/dist/styles/sprout/FeatureGrid.svelte.d.ts +16 -0
  54. package/dist/styles/sprout/Field.svelte +42 -0
  55. package/dist/styles/sprout/Field.svelte.d.ts +16 -0
  56. package/dist/styles/sprout/Footer.svelte +91 -0
  57. package/dist/styles/sprout/Footer.svelte.d.ts +25 -0
  58. package/dist/styles/sprout/HeroSection.svelte +86 -0
  59. package/dist/styles/sprout/HeroSection.svelte.d.ts +21 -0
  60. package/dist/styles/sprout/Input.svelte +61 -0
  61. package/dist/styles/sprout/Input.svelte.d.ts +18 -0
  62. package/dist/styles/sprout/InputGroup.svelte +20 -0
  63. package/dist/styles/sprout/InputGroup.svelte.d.ts +11 -0
  64. package/dist/styles/sprout/Kbd.svelte +18 -0
  65. package/dist/styles/sprout/Kbd.svelte.d.ts +8 -0
  66. package/dist/styles/sprout/Label.svelte +16 -0
  67. package/dist/styles/sprout/Label.svelte.d.ts +9 -0
  68. package/dist/styles/sprout/Link.svelte +44 -0
  69. package/dist/styles/sprout/Link.svelte.d.ts +16 -0
  70. package/dist/styles/sprout/LocaleProvider.svelte +24 -0
  71. package/dist/styles/sprout/LocaleProvider.svelte.d.ts +10 -0
  72. package/dist/styles/sprout/LogoCloud.svelte +61 -0
  73. package/dist/styles/sprout/LogoCloud.svelte.d.ts +19 -0
  74. package/dist/styles/sprout/MenuItem.svelte +64 -0
  75. package/dist/styles/sprout/MenuItem.svelte.d.ts +18 -0
  76. package/dist/styles/sprout/MenuSeparator.svelte +22 -0
  77. package/dist/styles/sprout/MenuSeparator.svelte.d.ts +8 -0
  78. package/dist/styles/sprout/Modal.svelte +119 -0
  79. package/dist/styles/sprout/Modal.svelte.d.ts +21 -0
  80. package/dist/styles/sprout/MultiSelect.svelte +288 -0
  81. package/dist/styles/sprout/MultiSelect.svelte.d.ts +31 -0
  82. package/dist/styles/sprout/Navbar.svelte +201 -0
  83. package/dist/styles/sprout/Navbar.svelte.d.ts +33 -0
  84. package/dist/styles/sprout/NumberInput.svelte +97 -0
  85. package/dist/styles/sprout/NumberInput.svelte.d.ts +15 -0
  86. package/dist/styles/sprout/Pagination.svelte +84 -0
  87. package/dist/styles/sprout/Pagination.svelte.d.ts +15 -0
  88. package/dist/styles/sprout/PasswordInput.svelte +101 -0
  89. package/dist/styles/sprout/PasswordInput.svelte.d.ts +12 -0
  90. package/dist/styles/sprout/Popover.svelte +90 -0
  91. package/dist/styles/sprout/Popover.svelte.d.ts +19 -0
  92. package/dist/styles/sprout/PricingCard.svelte +109 -0
  93. package/dist/styles/sprout/PricingCard.svelte.d.ts +25 -0
  94. package/dist/styles/sprout/Progress.svelte +59 -0
  95. package/dist/styles/sprout/Progress.svelte.d.ts +17 -0
  96. package/dist/styles/sprout/Radio.svelte +56 -0
  97. package/dist/styles/sprout/Radio.svelte.d.ts +15 -0
  98. package/dist/styles/sprout/RadioGroup.svelte +96 -0
  99. package/dist/styles/sprout/RadioGroup.svelte.d.ts +27 -0
  100. package/dist/styles/sprout/SegmentedControl.svelte +89 -0
  101. package/dist/styles/sprout/SegmentedControl.svelte.d.ts +25 -0
  102. package/dist/styles/sprout/Select.svelte +62 -0
  103. package/dist/styles/sprout/Select.svelte.d.ts +20 -0
  104. package/dist/styles/sprout/Sidebar.svelte +206 -0
  105. package/dist/styles/sprout/Sidebar.svelte.d.ts +36 -0
  106. package/dist/styles/sprout/Skeleton.svelte +46 -0
  107. package/dist/styles/sprout/Skeleton.svelte.d.ts +13 -0
  108. package/dist/styles/sprout/Slider.svelte +80 -0
  109. package/dist/styles/sprout/Slider.svelte.d.ts +19 -0
  110. package/dist/styles/sprout/Spinner.svelte +34 -0
  111. package/dist/styles/sprout/Spinner.svelte.d.ts +13 -0
  112. package/dist/styles/sprout/Stat.svelte +51 -0
  113. package/dist/styles/sprout/Stat.svelte.d.ts +16 -0
  114. package/dist/styles/sprout/StatsBand.svelte +64 -0
  115. package/dist/styles/sprout/StatsBand.svelte.d.ts +20 -0
  116. package/dist/styles/sprout/Steps.svelte +143 -0
  117. package/dist/styles/sprout/Steps.svelte.d.ts +24 -0
  118. package/dist/styles/sprout/Switch.svelte +65 -0
  119. package/dist/styles/sprout/Switch.svelte.d.ts +14 -0
  120. package/dist/styles/sprout/Table.svelte +218 -0
  121. package/dist/styles/sprout/Table.svelte.d.ts +66 -0
  122. package/dist/styles/sprout/Tabs.svelte +103 -0
  123. package/dist/styles/sprout/Tabs.svelte.d.ts +23 -0
  124. package/dist/styles/sprout/Testimonial.svelte +65 -0
  125. package/dist/styles/sprout/Testimonial.svelte.d.ts +19 -0
  126. package/dist/styles/sprout/Textarea.svelte +55 -0
  127. package/dist/styles/sprout/Textarea.svelte.d.ts +12 -0
  128. package/dist/styles/sprout/ThemeToggle.svelte +131 -0
  129. package/dist/styles/sprout/ThemeToggle.svelte.d.ts +16 -0
  130. package/dist/styles/sprout/Toaster.svelte +133 -0
  131. package/dist/styles/sprout/Toaster.svelte.d.ts +8 -0
  132. package/dist/styles/sprout/Tooltip.svelte +34 -0
  133. package/dist/styles/sprout/Tooltip.svelte.d.ts +12 -0
  134. package/dist/styles/sprout/fonts.css +78 -0
  135. package/dist/styles/sprout/index.d.ts +109 -0
  136. package/dist/styles/sprout/index.js +83 -0
  137. package/dist/styles/sprout/lift.d.ts +40 -0
  138. package/dist/styles/sprout/lift.js +36 -0
  139. package/dist/styles/sprout/theme.css +255 -0
  140. package/package.json +269 -3
  141. package/registry.json +137 -65
@@ -0,0 +1,267 @@
1
+ <script lang="ts">
2
+ import { anchored } from '../../core/actions/anchor.js';
3
+ import { clickOutside, portal } from '../../core/actions/dismissable.js';
4
+ import { ListCursor, groupItems, matchQuery, revealIndex } from '../../core/list.svelte.js';
5
+ import { toneFocusWithinBorder, toneSoft, toneText, type Tone } from '../../core/tones.js';
6
+ import { useLocale } from '../../core/locale.svelte.js';
7
+ import { edge, soft } from './lift.js';
8
+
9
+ export interface ComboboxOption {
10
+ value: string;
11
+ label: string;
12
+ /** Second line under the label. */
13
+ description?: string;
14
+ disabled?: boolean;
15
+ /** Options sharing a group are rendered under one heading. */
16
+ group?: string;
17
+ }
18
+
19
+ interface Props {
20
+ /** Bindable — the selected option's `value`, or `''`. */
21
+ value?: string;
22
+ options: ComboboxOption[];
23
+ placeholder?: string;
24
+ tone?: Tone;
25
+ invalid?: boolean;
26
+ disabled?: boolean;
27
+ /** Shows a clear button once something is selected. */
28
+ clearable?: boolean;
29
+ emptyText?: string;
30
+ id?: string;
31
+ name?: string;
32
+ onchange?: (value: string) => void;
33
+ class?: string;
34
+ }
35
+
36
+ let {
37
+ value = $bindable(''),
38
+ options,
39
+ placeholder,
40
+ tone = 'brand',
41
+ invalid = false,
42
+ disabled = false,
43
+ clearable = true,
44
+ emptyText,
45
+ id,
46
+ name,
47
+ onchange,
48
+ class: className = ''
49
+ }: Props = $props();
50
+
51
+ const t = useLocale();
52
+
53
+ let open = $state(false);
54
+ let query = $state('');
55
+ let wrapper: HTMLElement | null = $state(null);
56
+ let inputEl: HTMLInputElement | null = $state(null);
57
+ let listEl: HTMLElement | null = $state(null);
58
+
59
+ const selected = $derived(options.find((option) => option.value === value));
60
+
61
+ const matches = $derived(
62
+ matchQuery(options, query, (option) => `${option.label} ${option.description ?? ''}`)
63
+ );
64
+
65
+ /** Options in render order, grouped when any option declares a `group`. */
66
+ const groups = $derived(groupItems(matches));
67
+ const flat = $derived(groups.flatMap(([, items]) => items));
68
+
69
+ const cursor = new ListCursor(() => flat);
70
+ const active = $derived(cursor.index);
71
+
72
+ /**
73
+ * Set while we hand focus back to the field after a choice. Focusing fires
74
+ * `focus` synchronously, and this field opens on focus — without the guard,
75
+ * picking an option would immediately reopen the list it just closed.
76
+ */
77
+ let restoring = false;
78
+
79
+ function openList() {
80
+ if (disabled || open || restoring) return;
81
+ open = true;
82
+ query = '';
83
+ cursor.reset((option) => option.value === value);
84
+ }
85
+
86
+ function choose(option: ComboboxOption) {
87
+ if (option.disabled) return;
88
+ value = option.value;
89
+ onchange?.(option.value);
90
+ open = false;
91
+ query = '';
92
+ restoring = true;
93
+ inputEl?.focus();
94
+ restoring = false;
95
+ }
96
+
97
+ function clear(event: MouseEvent) {
98
+ event.stopPropagation();
99
+ value = '';
100
+ query = '';
101
+ onchange?.('');
102
+ restoring = true;
103
+ inputEl?.focus();
104
+ restoring = false;
105
+ }
106
+
107
+ function move(step: number) {
108
+ cursor.move(step);
109
+ revealIndex(listEl, cursor.index);
110
+ }
111
+
112
+ function onKeydown(event: KeyboardEvent) {
113
+ switch (event.key) {
114
+ case 'ArrowDown':
115
+ event.preventDefault();
116
+ if (!open) openList();
117
+ else move(1);
118
+ break;
119
+ case 'ArrowUp':
120
+ event.preventDefault();
121
+ if (open) move(-1);
122
+ break;
123
+ case 'Enter':
124
+ if (open && cursor.current) {
125
+ event.preventDefault();
126
+ choose(cursor.current);
127
+ }
128
+ break;
129
+ case 'Escape':
130
+ if (open) {
131
+ event.preventDefault();
132
+ event.stopPropagation();
133
+ open = false;
134
+ query = '';
135
+ }
136
+ break;
137
+ case 'Tab':
138
+ open = false;
139
+ break;
140
+ }
141
+ }
142
+ </script>
143
+
144
+ <div
145
+ bind:this={wrapper}
146
+ class="relative inline-flex w-full items-center gap-2 rounded-xl border bg-bg pr-1.5 pl-3 {edge} transition-colors duration-150 ease-brand-out
147
+ {invalid ? 'border-danger' : `border-hairline-strong ${toneFocusWithinBorder[tone]}`}
148
+ {disabled ? 'pointer-events-none opacity-50' : ''} {className}"
149
+ >
150
+ <input
151
+ bind:this={inputEl}
152
+ {id}
153
+ {disabled}
154
+ type="text"
155
+ role="combobox"
156
+ autocomplete="off"
157
+ aria-expanded={open}
158
+ aria-controls="{id ?? 'combobox'}-listbox"
159
+ aria-activedescendant={open && flat[active] ? `${id ?? 'combobox'}-opt-${active}` : undefined}
160
+ aria-invalid={invalid ? 'true' : undefined}
161
+ placeholder={selected ? selected.label : (placeholder ?? t.current.comboboxPlaceholder)}
162
+ value={open ? query : (selected?.label ?? '')}
163
+ oninput={(event) => {
164
+ query = event.currentTarget.value;
165
+ open = true;
166
+ cursor.index = 0;
167
+ }}
168
+ onfocus={openList}
169
+ onkeydown={onKeydown}
170
+ class="h-10 w-full min-w-0 bg-transparent font-sans text-sm text-text placeholder:text-text-muted focus:outline-none"
171
+ />
172
+
173
+ {#if name}<input type="hidden" {name} {value} />{/if}
174
+
175
+ {#if clearable && value && !disabled}
176
+ <button
177
+ type="button"
178
+ onclick={clear}
179
+ aria-label={t.current.clear}
180
+ class="shrink-0 rounded p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
181
+ >
182
+ <svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
183
+ <path
184
+ d="m2 2 10 10M12 2 2 12"
185
+ stroke="currentColor"
186
+ stroke-width="1.6"
187
+ stroke-linecap="round"
188
+ />
189
+ </svg>
190
+ </button>
191
+ {/if}
192
+
193
+ <svg
194
+ class="pointer-events-none mr-1.5 size-4 shrink-0 text-text-muted transition-transform duration-150 {open
195
+ ? 'rotate-180'
196
+ : ''}"
197
+ viewBox="0 0 16 16"
198
+ fill="none"
199
+ aria-hidden="true"
200
+ >
201
+ <path d="m4 6 4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
202
+ </svg>
203
+ </div>
204
+
205
+ {#if open}
206
+ <div
207
+ bind:this={listEl}
208
+ use:portal
209
+ use:anchored={{ anchor: wrapper, placement: 'bottom-start', offset: 4, matchWidth: true }}
210
+ use:clickOutside={{ onoutside: () => (open = false), ignore: [wrapper] }}
211
+ id="{id ?? 'combobox'}-listbox"
212
+ role="listbox"
213
+ class="z-50 max-h-64 overflow-y-auto rounded-2xl border border-hairline bg-bg py-1 font-sans {soft}"
214
+ >
215
+ {#if flat.length === 0}
216
+ <p class="px-3 py-6 text-center text-sm text-text-muted">
217
+ {emptyText ?? t.current.noMatches}
218
+ </p>
219
+ {:else}
220
+ {#each groups as [group, items] (group)}
221
+ {#if group}
222
+ <p class="px-3 pt-2 pb-1 text-[11px] font-semibold text-text-muted">{group}</p>
223
+ {/if}
224
+ {#each items as option (option.value)}
225
+ {@const index = flat.indexOf(option)}
226
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
227
+ <div
228
+ id="{id ?? 'combobox'}-opt-{index}"
229
+ data-index={index}
230
+ role="option"
231
+ tabindex="-1"
232
+ aria-selected={option.value === value}
233
+ aria-disabled={option.disabled}
234
+ onclick={() => choose(option)}
235
+ onpointermove={() => (cursor.index = index)}
236
+ class="mx-1 flex cursor-pointer flex-col gap-0.5 rounded-xl px-2.5 py-2 text-sm transition-colors duration-100
237
+ {option.disabled ? 'pointer-events-none opacity-40' : ''}
238
+ {index === active ? toneSoft[tone] : 'text-text-secondary'}"
239
+ >
240
+ <span class="flex items-center gap-2">
241
+ <span class="min-w-0 flex-1 truncate">{option.label}</span>
242
+ {#if option.value === value}
243
+ <svg
244
+ class="size-3.5 shrink-0 {toneText[tone]}"
245
+ viewBox="0 0 14 14"
246
+ fill="none"
247
+ aria-hidden="true"
248
+ >
249
+ <path
250
+ d="m2 7 3.2 3.2L12 3.6"
251
+ stroke="currentColor"
252
+ stroke-width="1.8"
253
+ stroke-linecap="round"
254
+ stroke-linejoin="round"
255
+ />
256
+ </svg>
257
+ {/if}
258
+ </span>
259
+ {#if option.description}
260
+ <span class="truncate text-xs text-text-muted">{option.description}</span>
261
+ {/if}
262
+ </div>
263
+ {/each}
264
+ {/each}
265
+ {/if}
266
+ </div>
267
+ {/if}
@@ -0,0 +1,29 @@
1
+ import { type Tone } from '../../core/tones.js';
2
+ export interface ComboboxOption {
3
+ value: string;
4
+ label: string;
5
+ /** Second line under the label. */
6
+ description?: string;
7
+ disabled?: boolean;
8
+ /** Options sharing a group are rendered under one heading. */
9
+ group?: string;
10
+ }
11
+ interface Props {
12
+ /** Bindable — the selected option's `value`, or `''`. */
13
+ value?: string;
14
+ options: ComboboxOption[];
15
+ placeholder?: string;
16
+ tone?: Tone;
17
+ invalid?: boolean;
18
+ disabled?: boolean;
19
+ /** Shows a clear button once something is selected. */
20
+ clearable?: boolean;
21
+ emptyText?: string;
22
+ id?: string;
23
+ name?: string;
24
+ onchange?: (value: string) => void;
25
+ class?: string;
26
+ }
27
+ declare const Combobox: import("svelte").Component<Props, {}, "value">;
28
+ type Combobox = ReturnType<typeof Combobox>;
29
+ export default Combobox;
@@ -0,0 +1,210 @@
1
+ <script module lang="ts">
2
+ export interface CommandItem {
3
+ id: string;
4
+ label: string;
5
+ /** Second line under the label. */
6
+ description?: string;
7
+ /** Items sharing a group are rendered under one heading. */
8
+ group?: string;
9
+ /** Right-aligned hint, e.g. `'⌘K'`. */
10
+ shortcut?: string;
11
+ /** Extra words to match on that aren't in the label. */
12
+ keywords?: string;
13
+ disabled?: boolean;
14
+ onselect?: () => void;
15
+ }
16
+ </script>
17
+
18
+ <script lang="ts">
19
+ import { focusTrap } from '../../core/actions/dismissable.js';
20
+ import { syncDialog } from '../../core/dialog.svelte.js';
21
+ import { ListCursor, groupItems, matchQuery, revealIndex } from '../../core/list.svelte.js';
22
+ import { useLocale } from '../../core/locale.svelte.js';
23
+ import { toneSoft, type Tone } from '../../core/tones.js';
24
+ import Kbd from './Kbd.svelte';
25
+ import { float } from './lift.js';
26
+
27
+ interface Props {
28
+ open?: boolean;
29
+ items: CommandItem[];
30
+ placeholder?: string;
31
+ emptyText?: string;
32
+ tone?: Tone;
33
+ /**
34
+ * Key that opens the palette with Cmd/Ctrl held. Default `'k'`.
35
+ * Pass `null` to wire your own trigger only.
36
+ */
37
+ hotkey?: string | null;
38
+ onselect?: (item: CommandItem) => void;
39
+ class?: string;
40
+ }
41
+
42
+ let {
43
+ open = $bindable(false),
44
+ items,
45
+ placeholder,
46
+ emptyText,
47
+ tone = 'brand',
48
+ hotkey = 'k',
49
+ onselect,
50
+ class: className = ''
51
+ }: Props = $props();
52
+
53
+ const t = useLocale();
54
+
55
+ let dialog: HTMLDialogElement | null = $state(null);
56
+ let inputEl: HTMLInputElement | null = $state(null);
57
+ let listEl: HTMLElement | null = $state(null);
58
+ let query = $state('');
59
+
60
+ const matches = $derived(
61
+ matchQuery(
62
+ items,
63
+ query,
64
+ (item) => `${item.label} ${item.description ?? ''} ${item.keywords ?? ''} ${item.group ?? ''}`
65
+ )
66
+ );
67
+
68
+ const groups = $derived(groupItems(matches));
69
+ const flat = $derived(groups.flatMap(([, group]) => group));
70
+
71
+ const cursor = new ListCursor(() => flat);
72
+ const active = $derived(cursor.index);
73
+
74
+ $effect(() => {
75
+ if (!dialog) return;
76
+ const wasOpen = dialog.open;
77
+ syncDialog(dialog, open);
78
+ if (open && !wasOpen) {
79
+ query = '';
80
+ cursor.reset();
81
+ queueMicrotask(() => inputEl?.focus());
82
+ }
83
+ });
84
+
85
+ $effect(() => {
86
+ if (!hotkey) return;
87
+ const onKeydown = (event: KeyboardEvent) => {
88
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === hotkey.toLowerCase()) {
89
+ event.preventDefault();
90
+ open = !open;
91
+ }
92
+ };
93
+ window.addEventListener('keydown', onKeydown);
94
+ return () => window.removeEventListener('keydown', onKeydown);
95
+ });
96
+
97
+ function run(item: CommandItem) {
98
+ if (item.disabled) return;
99
+ open = false;
100
+ item.onselect?.();
101
+ onselect?.(item);
102
+ }
103
+
104
+ function move(step: number) {
105
+ cursor.move(step);
106
+ revealIndex(listEl, cursor.index);
107
+ }
108
+
109
+ function onKeydown(event: KeyboardEvent) {
110
+ switch (event.key) {
111
+ case 'ArrowDown':
112
+ event.preventDefault();
113
+ move(1);
114
+ break;
115
+ case 'ArrowUp':
116
+ event.preventDefault();
117
+ move(-1);
118
+ break;
119
+ case 'Enter':
120
+ if (cursor.current) {
121
+ event.preventDefault();
122
+ run(cursor.current);
123
+ }
124
+ break;
125
+ case 'Escape':
126
+ event.preventDefault();
127
+ open = false;
128
+ break;
129
+ }
130
+ }
131
+ </script>
132
+
133
+ <dialog
134
+ bind:this={dialog}
135
+ onclose={() => (open = false)}
136
+ onclick={(event) => {
137
+ if (event.target === dialog) open = false;
138
+ }}
139
+ class="mx-auto mt-[12vh] mb-auto w-[min(36rem,calc(100vw-2rem))] max-w-none rounded-[28px] border border-hairline bg-bg p-0 text-text {float} backdrop:bg-text/30 backdrop:backdrop-blur-[1px] {className}"
140
+ >
141
+ <div use:focusTrap={{ autofocus: false }} onkeydown={onKeydown} role="none" class="flex flex-col">
142
+ <div class="flex items-center gap-3 border-b border-hairline px-4">
143
+ <svg
144
+ class="size-4 shrink-0 text-text-muted"
145
+ viewBox="0 0 16 16"
146
+ fill="none"
147
+ aria-hidden="true"
148
+ >
149
+ <circle cx="7" cy="7" r="4.5" stroke="currentColor" stroke-width="1.5" />
150
+ <path d="m10.5 10.5 3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
151
+ </svg>
152
+ <input
153
+ bind:this={inputEl}
154
+ bind:value={query}
155
+ oninput={() => cursor.reset()}
156
+ type="text"
157
+ autocomplete="off"
158
+ aria-label={placeholder ?? t.current.commandPlaceholder}
159
+ placeholder={placeholder ?? t.current.commandPlaceholder}
160
+ class="h-12 w-full min-w-0 bg-transparent font-sans text-sm text-text placeholder:text-text-muted focus:outline-none"
161
+ />
162
+ <Kbd>esc</Kbd>
163
+ </div>
164
+
165
+ <div
166
+ bind:this={listEl}
167
+ role="listbox"
168
+ aria-label="Commands"
169
+ class="max-h-80 overflow-y-auto py-1"
170
+ >
171
+ {#if flat.length === 0}
172
+ <p class="px-4 py-10 text-center font-sans text-sm text-text-muted">
173
+ {emptyText ?? t.current.noResults}
174
+ </p>
175
+ {:else}
176
+ {#each groups as [group, groupItems] (group)}
177
+ {#if group}
178
+ <p class="px-4 pt-3 pb-1 text-[11px] font-semibold text-text-muted">{group}</p>
179
+ {/if}
180
+ {#each groupItems as item (item.id)}
181
+ {@const index = flat.indexOf(item)}
182
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
183
+ <div
184
+ data-index={index}
185
+ role="option"
186
+ tabindex="-1"
187
+ aria-selected={index === active}
188
+ aria-disabled={item.disabled}
189
+ onclick={() => run(item)}
190
+ onpointermove={() => (cursor.index = index)}
191
+ class="mx-2 flex cursor-pointer items-center gap-3 rounded-xl px-2.5 py-2 font-sans text-sm transition-colors duration-100
192
+ {item.disabled ? 'pointer-events-none opacity-40' : ''}
193
+ {index === active ? toneSoft[tone] : 'text-text-secondary'}"
194
+ >
195
+ <span class="flex min-w-0 flex-1 flex-col gap-0.5">
196
+ <span class="truncate">{item.label}</span>
197
+ {#if item.description}
198
+ <span class="truncate text-xs text-text-muted">{item.description}</span>
199
+ {/if}
200
+ </span>
201
+ {#if item.shortcut}
202
+ <span class="shrink-0 text-[11px] text-text-muted">{item.shortcut}</span>
203
+ {/if}
204
+ </div>
205
+ {/each}
206
+ {/each}
207
+ {/if}
208
+ </div>
209
+ </div>
210
+ </dialog>
@@ -0,0 +1,32 @@
1
+ export interface CommandItem {
2
+ id: string;
3
+ label: string;
4
+ /** Second line under the label. */
5
+ description?: string;
6
+ /** Items sharing a group are rendered under one heading. */
7
+ group?: string;
8
+ /** Right-aligned hint, e.g. `'⌘K'`. */
9
+ shortcut?: string;
10
+ /** Extra words to match on that aren't in the label. */
11
+ keywords?: string;
12
+ disabled?: boolean;
13
+ onselect?: () => void;
14
+ }
15
+ import { type Tone } from '../../core/tones.js';
16
+ interface Props {
17
+ open?: boolean;
18
+ items: CommandItem[];
19
+ placeholder?: string;
20
+ emptyText?: string;
21
+ tone?: Tone;
22
+ /**
23
+ * Key that opens the palette with Cmd/Ctrl held. Default `'k'`.
24
+ * Pass `null` to wire your own trigger only.
25
+ */
26
+ hotkey?: string | null;
27
+ onselect?: (item: CommandItem) => void;
28
+ class?: string;
29
+ }
30
+ declare const CommandPalette: import("svelte").Component<Props, {}, "open">;
31
+ type CommandPalette = ReturnType<typeof CommandPalette>;
32
+ export default CommandPalette;
@@ -0,0 +1,97 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import Button from './Button.svelte';
4
+ import Modal from './Modal.svelte';
5
+ import { useLocale } from '../../core/locale.svelte.js';
6
+ import { toneSoft, type Tone } from '../../core/tones.js';
7
+
8
+ interface Props {
9
+ open?: boolean;
10
+ title: string;
11
+ description?: string;
12
+ /** Drives the confirm button and the glyph. `danger` for destructive acts. */
13
+ tone?: Tone;
14
+ confirmLabel?: string;
15
+ cancelLabel?: string;
16
+ /** Spins the confirm button and blocks dismissal while the action runs. */
17
+ loading?: boolean;
18
+ /** Awaited — the dialog stays open and busy until it settles. */
19
+ onconfirm?: () => void | Promise<void>;
20
+ oncancel?: () => void;
21
+ icon?: Snippet;
22
+ /** Extra content between the description and the buttons. */
23
+ children?: Snippet;
24
+ }
25
+
26
+ let {
27
+ open = $bindable(false),
28
+ title,
29
+ description,
30
+ tone = 'danger',
31
+ confirmLabel,
32
+ cancelLabel,
33
+ loading = $bindable(false),
34
+ onconfirm,
35
+ oncancel,
36
+ icon,
37
+ children
38
+ }: Props = $props();
39
+
40
+ const t = useLocale();
41
+
42
+ async function confirm() {
43
+ try {
44
+ loading = true;
45
+ await onconfirm?.();
46
+ open = false;
47
+ } finally {
48
+ loading = false;
49
+ }
50
+ }
51
+
52
+ function cancel() {
53
+ if (loading) return;
54
+ open = false;
55
+ oncancel?.();
56
+ }
57
+ </script>
58
+
59
+ <Modal
60
+ bind:open
61
+ size="sm"
62
+ dismissible={!loading}
63
+ showClose={false}
64
+ onclose={() => (loading = false)}
65
+ >
66
+ <div class="flex items-start gap-4">
67
+ <span class="flex size-10 shrink-0 items-center justify-center rounded-full {toneSoft[tone]}">
68
+ {#if icon}
69
+ {@render icon()}
70
+ {:else}
71
+ <svg class="size-5" viewBox="0 0 20 20" fill="none" aria-hidden="true">
72
+ <circle cx="10" cy="10" r="7.5" stroke="currentColor" stroke-width="1.5" />
73
+ <path d="M10 5.75v5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" />
74
+ <circle cx="10" cy="14" r="1" fill="currentColor" />
75
+ </svg>
76
+ {/if}
77
+ </span>
78
+ <div class="flex min-w-0 flex-1 flex-col gap-1.5">
79
+ <h2 class="font-heading text-base font-semibold text-text">{title}</h2>
80
+ {#if description}
81
+ <p class="text-[13px] leading-relaxed text-text-secondary">{description}</p>
82
+ {/if}
83
+ {#if children}
84
+ {@render children()}
85
+ {/if}
86
+ </div>
87
+ </div>
88
+
89
+ {#snippet footer()}
90
+ <Button variant="outline" tone="neutral" onclick={cancel} disabled={loading}>
91
+ {cancelLabel ?? t.current.cancel}
92
+ </Button>
93
+ <Button {tone} onclick={confirm} {loading}>
94
+ {confirmLabel ?? t.current.confirm}
95
+ </Button>
96
+ {/snippet}
97
+ </Modal>
@@ -0,0 +1,22 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type Tone } from '../../core/tones.js';
3
+ interface Props {
4
+ open?: boolean;
5
+ title: string;
6
+ description?: string;
7
+ /** Drives the confirm button and the glyph. `danger` for destructive acts. */
8
+ tone?: Tone;
9
+ confirmLabel?: string;
10
+ cancelLabel?: string;
11
+ /** Spins the confirm button and blocks dismissal while the action runs. */
12
+ loading?: boolean;
13
+ /** Awaited — the dialog stays open and busy until it settles. */
14
+ onconfirm?: () => void | Promise<void>;
15
+ oncancel?: () => void;
16
+ icon?: Snippet;
17
+ /** Extra content between the description and the buttons. */
18
+ children?: Snippet;
19
+ }
20
+ declare const ConfirmDialog: import("svelte").Component<Props, {}, "open" | "loading">;
21
+ type ConfirmDialog = ReturnType<typeof ConfirmDialog>;
22
+ export default ConfirmDialog;