@spaethtech/svelte-ui 0.14.1-dev.65.3b37f4b → 0.15.1-dev.67.fbf3875

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 (46) hide show
  1. package/.claude/skills/svelte-ui/SKILL.md +5 -5
  2. package/dist/components/Carousel/Carousel.svelte +147 -0
  3. package/dist/components/Carousel/Carousel.svelte.d.ts +38 -0
  4. package/dist/components/Carousel/index.d.ts +1 -0
  5. package/dist/components/Carousel/index.js +1 -0
  6. package/dist/components/CommandPalette/CommandPalette.svelte +203 -0
  7. package/dist/components/CommandPalette/CommandPalette.svelte.d.ts +23 -0
  8. package/dist/components/CommandPalette/index.d.ts +2 -0
  9. package/dist/components/CommandPalette/index.js +1 -0
  10. package/dist/components/ContextMenu/ContextMenu.svelte +58 -0
  11. package/dist/components/ContextMenu/ContextMenu.svelte.d.ts +16 -0
  12. package/dist/components/ContextMenu/index.d.ts +1 -0
  13. package/dist/components/ContextMenu/index.js +1 -0
  14. package/dist/components/EmptyState/EmptyState.svelte +68 -0
  15. package/dist/components/EmptyState/EmptyState.svelte.d.ts +19 -0
  16. package/dist/components/EmptyState/index.d.ts +1 -0
  17. package/dist/components/EmptyState/index.js +1 -0
  18. package/dist/components/Kbd/Kbd.svelte +53 -0
  19. package/dist/components/Kbd/Kbd.svelte.d.ts +15 -0
  20. package/dist/components/Kbd/index.d.ts +1 -0
  21. package/dist/components/Kbd/index.js +1 -0
  22. package/dist/components/ScrollArea/ScrollArea.svelte +71 -0
  23. package/dist/components/ScrollArea/ScrollArea.svelte.d.ts +15 -0
  24. package/dist/components/ScrollArea/index.d.ts +1 -0
  25. package/dist/components/ScrollArea/index.js +1 -0
  26. package/dist/components/Splitter/Splitter.svelte +127 -0
  27. package/dist/components/Splitter/Splitter.svelte.d.ts +18 -0
  28. package/dist/components/Splitter/index.d.ts +1 -0
  29. package/dist/components/Splitter/index.js +1 -0
  30. package/dist/components/Stat/Stat.svelte +95 -0
  31. package/dist/components/Stat/Stat.svelte.d.ts +21 -0
  32. package/dist/components/Stat/index.d.ts +1 -0
  33. package/dist/components/Stat/index.js +1 -0
  34. package/dist/components/Timeline/Timeline.svelte +98 -0
  35. package/dist/components/Timeline/Timeline.svelte.d.ts +21 -0
  36. package/dist/components/Timeline/index.d.ts +2 -0
  37. package/dist/components/Timeline/index.js +1 -0
  38. package/dist/components/Tree/Tree.svelte +178 -0
  39. package/dist/components/Tree/Tree.svelte.d.ts +26 -0
  40. package/dist/components/Tree/index.d.ts +2 -0
  41. package/dist/components/Tree/index.js +1 -0
  42. package/dist/index.d.ts +13 -0
  43. package/dist/index.js +10 -0
  44. package/docs/components.md +106 -0
  45. package/docs/usage.md +166 -0
  46. package/package.json +1 -1
@@ -182,8 +182,8 @@ examples):
182
182
  `ctx.restoreFocus` likewise) · `ConfirmDialog` (title + message + Confirm/Cancel, built on Dialog)
183
183
  · **`Drawer`** (edge-anchored sliding panel / "sheet"; `side` left/right/top/bottom, `size` CSS
184
184
  length, `bind:open`, `dismissible`/`backdrop`; same modal mechanics as Dialog — a pure shell, put a
185
- Card inside) · `Popup` `Menu` · `tooltip` (a Svelte `use:` action) · `anchored` (positioning engine)
186
- - **Navigation:** **`Breadcrumbs`** (trail of `BreadcrumbItem` {`label`,`href`,`onclick`,`icon`}; last =
185
+ Card inside) · `Popup` `Menu` · **`ContextMenu`** (right-click menu at the pointer; `items` MenuItem[]; on `Menu`) · **`CommandPalette`** (⌘K/Ctrl+K fuzzy command launcher; `bind:open`, `commands` Command[] {`label`,`keywords`,`group`,`icon`,`shortcut`,`disabled`,`onrun`}, global `hotkey` default `"mod+k"`; ↑/↓/Enter/Esc; on `Input`+`Kbd`) · `tooltip` (a Svelte `use:` action) · `anchored` (positioning engine)
186
+ - **Navigation:** **`Tree`** (hierarchical expandable tree — `items` of `TreeNode`; `bind:expanded`/`selected`; roving-tabindex keyboard; ARIA `tree`) · **`Breadcrumbs`** (trail of `BreadcrumbItem` {`label`,`href`,`onclick`,`icon`}; last =
187
187
  current page; `maxItems` collapses the middle to a `⋯` Menu; `separator` slot) · `TabStrip` (tabs;
188
188
  `size`/`variant`, `gap`/`pad` bordered spacer cells, `height` for a
189
189
  fixed-height bar with bottom-aligned tabs, `activeSurface` to flow the active tab into the content
@@ -193,14 +193,14 @@ examples):
193
193
  `interactMode` `'hover'|'click'` popouts; `currentPath` active-match (per-item `activeMatch` for
194
194
  query-keyed/exact rules); `bind:open`/`bind:api`/`bind:activeMode`; `publishWidthVar` → CSS width var;
195
195
  popouts expose `data-sbm-popout`/`data-open`)
196
- - **Feedback:** `Alert` `Banner` `Badge` `Toaster` + `toast` · **`Spinner`** (indeterminate loading
196
+ - **Feedback:** `Alert` `Banner` `Badge` `Toaster` + `toast` · **`Kbd`** (keyboard keycap; `text`/`keys` combo) · **`Spinner`** (indeterminate loading
197
197
  ring; `variant`/`size`, omit `variant` to inherit `currentColor`. `Button` has a `loading` prop that
198
198
  renders one in place of its icon and blocks interaction.) · **`Progress`** (horizontal bar;
199
199
  determinate `value`/`max` or `indeterminate` sweep; `variant`/`size` = 4/8/12px; `label`/`showValue`.)
200
200
  · **`Skeleton`** (neutral loading placeholder; `shape` text/rect/circle, `lines`, `width`/`height`,
201
201
  `animate` shimmer. No `variant` — it's absent-content, not a themed surface. Compose to sketch a card.)
202
202
  - **Layout:** `Card` `CardHeader` `CardBody` `CardFooter` · **`Grid`** (auto-placed equal-cell grid;
203
- `columns` + `gap`) · **`Divider`** (thin `role="separator"` rule; `orientation` h/v, optional inline
203
+ `columns` + `gap`) · **`ScrollArea`** (themed thin-scrollbar container; `orientation`, `maxHeight`, `variant` thumb) · **`Splitter`** (two resizable panes + draggable divider; `bind:size` %, `orientation`, keyboard) · **`Carousel`** (sliding viewport; `items` + `slide` snippet, `bind:index`, `autoplay`, controls + dots) · **`Stat`** (metric/KPI card — `label`/`value`/`delta` up-down + `icon`; on `Card`) · **`Timeline`** (vertical event list — `items` of `TimelineItem` {`title`,`time`,`description`,`icon`,`variant`}) · **`Divider`** (thin `role="separator"` rule; `orientation` h/v, optional inline
204
204
  `label`+`labelAlign`; `--ui-border-color`)
205
205
  - **Flow:** **`Stepper`** (linear multi-step indicator — `steps` of `StepItem` {`label`,`description`,
206
206
  `icon`}, `bind:current`, `orientation` h/v, `onstep` clickable; done/current/upcoming markers +
@@ -208,7 +208,7 @@ examples):
208
208
  - **Disclosure:** **`Disclosure`** (one expand/collapse section; `title`/`header`, `bind:open`, panel
209
209
  stays mounted so state is preserved) · **`Accordion`** (coordinated group — `multiple` single/multi,
210
210
  `bind:value`; children are `<Disclosure value="…">`, no per-item `bind:open`)
211
- - **Display / theme:** **`Chip`** (interactive pill — `selected` toggle fill, `removable` ×/`onremove`,
211
+ - **Display / theme:** **`EmptyState`** (zero-data placeholder — icon + title + description + `action`) · **`Chip`** (interactive pill — `selected` toggle fill, `removable` ×/`onremove`,
212
212
  `onclick`/`href`, `icon`; the interactive counterpart to display-only `Badge`) · **`Avatar`** (image →
213
213
  `name` initials → `icon` → glyph fallback; `size`, `variant` fill, `shape`, `status` dot) +
214
214
  **`AvatarGroup`** (overlapped stack; `items`+`max` → `+N`) ·
@@ -0,0 +1,147 @@
1
+ <!--
2
+ /**
3
+ * Carousel — a sliding, single-item-at-a-time viewport with prev/next controls + dot indicators.
4
+ * Data-driven (`items` + a `slide` snippet); `bind:index`, optional `autoplay`, `loop`, keyboard.
5
+ * ARIA carousel roles. Composes `Button`. See Carousel.spec.md.
6
+ */
7
+ -->
8
+ <script lang="ts" module>
9
+ import type { Snippet } from "svelte";
10
+ </script>
11
+
12
+ <script lang="ts" generics="T">
13
+ import Button from "../Button.svelte";
14
+ import IconPrev from "~icons/mdi/chevron-left";
15
+ import IconNext from "~icons/mdi/chevron-right";
16
+ import type { Variant } from "../../types/variants.js";
17
+ import { variantToken } from "../../types/variants.js";
18
+
19
+ let {
20
+ items,
21
+ slide,
22
+ index = $bindable(0),
23
+ loop = true,
24
+ autoplay,
25
+ controls = true,
26
+ indicators = true,
27
+ variant = "primary",
28
+ ariaLabel = "Carousel",
29
+ class: cls = "",
30
+ }: {
31
+ items: T[];
32
+ slide: Snippet<[T, number]>;
33
+ index?: number;
34
+ loop?: boolean;
35
+ /** Auto-advance interval in ms; pauses on hover/focus. Omit to disable. */
36
+ autoplay?: number;
37
+ controls?: boolean;
38
+ indicators?: boolean;
39
+ variant?: Variant;
40
+ ariaLabel?: string;
41
+ class?: string;
42
+ } = $props();
43
+
44
+ const n = $derived(items.length);
45
+ const token = $derived(variantToken[variant]);
46
+ let paused = $state(false);
47
+
48
+ const go = (i: number) => {
49
+ index = loop ? (i + n) % n : Math.min(Math.max(i, 0), n - 1);
50
+ };
51
+ const next = () => go(index + 1);
52
+ const prev = () => go(index - 1);
53
+
54
+ $effect(() => {
55
+ index = Math.min(Math.max(index, 0), Math.max(0, n - 1));
56
+ });
57
+
58
+ // Autoplay — advances on an interval, paused while hovered/focused.
59
+ $effect(() => {
60
+ if (!autoplay || paused || n <= 1) return;
61
+ const t = setInterval(() => go(index + 1), autoplay);
62
+ return () => clearInterval(t);
63
+ });
64
+
65
+ function onKeydown(e: KeyboardEvent) {
66
+ if (e.key === "ArrowLeft") {
67
+ e.preventDefault();
68
+ prev();
69
+ } else if (e.key === "ArrowRight") {
70
+ e.preventDefault();
71
+ next();
72
+ }
73
+ }
74
+ </script>
75
+
76
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions a11y_no_noninteractive_tabindex (focusable carousel region for keyboard nav) -->
77
+ <div
78
+ role="region"
79
+ aria-roledescription="carousel"
80
+ aria-label={ariaLabel}
81
+ tabindex="0"
82
+ class="relative overflow-hidden rounded-[var(--ui-border-radius)] focus-visible:[outline:2px_solid_color-mix(in_srgb,var(--ui-color-text)_70%,transparent)] {cls}"
83
+ onkeydown={onKeydown}
84
+ onmouseenter={() => (paused = true)}
85
+ onmouseleave={() => (paused = false)}
86
+ onfocusin={() => (paused = true)}
87
+ onfocusout={() => (paused = false)}
88
+ >
89
+ <div
90
+ class="flex transition-transform duration-300 ease-out"
91
+ style="transform: translateX(-{index * 100}%);"
92
+ >
93
+ {#each items as item, i (i)}
94
+ <div
95
+ class="w-full shrink-0"
96
+ role="group"
97
+ aria-roledescription="slide"
98
+ aria-label={`${i + 1} of ${n}`}
99
+ aria-hidden={i !== index}
100
+ >
101
+ {@render slide(item, i)}
102
+ </div>
103
+ {/each}
104
+ </div>
105
+
106
+ {#if controls && n > 1}
107
+ <div class="absolute inset-y-0 left-2 flex items-center">
108
+ <Button
109
+ title="Previous"
110
+ variant="secondary"
111
+ onclick={prev}
112
+ disabled={!loop && index === 0}
113
+ class="!rounded-full shadow"
114
+ >
115
+ {#snippet icon()}<IconPrev />{/snippet}
116
+ </Button>
117
+ </div>
118
+ <div class="absolute inset-y-0 right-2 flex items-center">
119
+ <Button
120
+ title="Next"
121
+ variant="secondary"
122
+ onclick={next}
123
+ disabled={!loop && index === n - 1}
124
+ class="!rounded-full shadow"
125
+ >
126
+ {#snippet icon()}<IconNext />{/snippet}
127
+ </Button>
128
+ </div>
129
+ {/if}
130
+
131
+ {#if indicators && n > 1}
132
+ <div class="absolute inset-x-0 bottom-2 flex items-center justify-center gap-1.5">
133
+ {#each items as _, i (i)}
134
+ <button
135
+ type="button"
136
+ aria-label={`Go to slide ${i + 1}`}
137
+ aria-current={i === index}
138
+ onclick={() => go(i)}
139
+ class="h-2 rounded-full transition-all {i === index ? 'w-5' : 'w-2'}"
140
+ style="background-color: {i === index
141
+ ? `var(${token})`
142
+ : 'color-mix(in srgb, var(--ui-color-background) 70%, var(--ui-color-text))'};"
143
+ ></button>
144
+ {/each}
145
+ </div>
146
+ {/if}
147
+ </div>
@@ -0,0 +1,38 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { Variant } from "../../types/variants.js";
3
+ declare function $$render<T>(): {
4
+ props: {
5
+ items: T[];
6
+ slide: Snippet<[T, number]>;
7
+ index?: number;
8
+ loop?: boolean;
9
+ /** Auto-advance interval in ms; pauses on hover/focus. Omit to disable. */
10
+ autoplay?: number;
11
+ controls?: boolean;
12
+ indicators?: boolean;
13
+ variant?: Variant;
14
+ ariaLabel?: string;
15
+ class?: string;
16
+ };
17
+ exports: {};
18
+ bindings: "index";
19
+ slots: {};
20
+ events: {};
21
+ };
22
+ declare class __sveltets_Render<T> {
23
+ props(): ReturnType<typeof $$render<T>>['props'];
24
+ events(): ReturnType<typeof $$render<T>>['events'];
25
+ slots(): ReturnType<typeof $$render<T>>['slots'];
26
+ bindings(): "index";
27
+ exports(): {};
28
+ }
29
+ interface $$IsomorphicComponent {
30
+ new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
31
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
32
+ } & ReturnType<__sveltets_Render<T>['exports']>;
33
+ <T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
34
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
35
+ }
36
+ declare const Carousel: $$IsomorphicComponent;
37
+ type Carousel<T> = InstanceType<typeof Carousel<T>>;
38
+ export default Carousel;
@@ -0,0 +1 @@
1
+ export { default as Carousel } from "./Carousel.svelte";
@@ -0,0 +1 @@
1
+ export { default as Carousel } from "./Carousel.svelte";
@@ -0,0 +1,203 @@
1
+ <!--
2
+ /**
3
+ * CommandPalette — a ⌘K/Ctrl+K fuzzy command launcher: a top-centered overlay with a search `Input`
4
+ * and a filtered, grouped, keyboard-navigable command list. `bind:open`, a global `hotkey`, per-command
5
+ * `shortcut` (shown as `Kbd`). Portals to <body> (scroll-lock + backdrop + focus). See spec.
6
+ */
7
+ -->
8
+ <script lang="ts" module>
9
+ import type { Snippet } from "svelte";
10
+ export type Command = {
11
+ id?: string;
12
+ label: string;
13
+ keywords?: string[];
14
+ group?: string;
15
+ icon?: Snippet;
16
+ shortcut?: string[];
17
+ disabled?: boolean;
18
+ onrun: () => void;
19
+ };
20
+ </script>
21
+
22
+ <script lang="ts">
23
+ import Input from "../Input.svelte";
24
+ import Kbd from "../Kbd/Kbd.svelte";
25
+ import IconSearch from "~icons/mdi/magnify";
26
+
27
+ let {
28
+ open = $bindable(false),
29
+ commands,
30
+ hotkey = "mod+k",
31
+ placeholder = "Type a command or search…",
32
+ empty = "No matching commands",
33
+ onopen,
34
+ }: {
35
+ open?: boolean;
36
+ commands: Command[];
37
+ /** Global open hotkey — `mod` = ⌘ on macOS, Ctrl elsewhere. Set `""` to disable. */
38
+ hotkey?: string;
39
+ placeholder?: string;
40
+ empty?: string;
41
+ onopen?: () => void;
42
+ } = $props();
43
+
44
+ let query = $state("");
45
+ let highlight = $state(0);
46
+ let inputEl = $state<HTMLInputElement>();
47
+ let previouslyFocused: HTMLElement | null = null;
48
+
49
+ const isMac =
50
+ typeof navigator !== "undefined" && /mac|iphone|ipad/i.test(navigator.platform || navigator.userAgent);
51
+
52
+ const filtered = $derived.by(() => {
53
+ const q = query.trim().toLowerCase();
54
+ if (!q) return commands;
55
+ return commands.filter(
56
+ (c) =>
57
+ c.label.toLowerCase().includes(q) ||
58
+ (c.keywords ?? []).some((k) => k.toLowerCase().includes(q)),
59
+ );
60
+ });
61
+ // Group in first-seen order for rendering; `filtered` is the flat list the highlight indexes into.
62
+ const groups = $derived.by(() => {
63
+ const map = new Map<string, Command[]>();
64
+ for (const c of filtered) {
65
+ const g = c.group ?? "";
66
+ (map.get(g) ?? map.set(g, []).get(g)!).push(c);
67
+ }
68
+ return [...map.entries()].map(([name, items]) => ({ name, items }));
69
+ });
70
+
71
+ function portal(node: HTMLElement) {
72
+ document.body.appendChild(node);
73
+ return { destroy: () => node.remove() };
74
+ }
75
+
76
+ // Global hotkey — toggles open. `mod+k` → ⌘K (mac) / Ctrl+K.
77
+ function onGlobalKey(e: KeyboardEvent) {
78
+ if (!hotkey) return;
79
+ const parts = hotkey.toLowerCase().split("+");
80
+ const key = parts[parts.length - 1];
81
+ const needMod = parts.includes("mod");
82
+ const modOk = needMod ? (isMac ? e.metaKey : e.ctrlKey) : true;
83
+ if (modOk && e.key.toLowerCase() === key) {
84
+ e.preventDefault();
85
+ open = !open;
86
+ }
87
+ }
88
+
89
+ // On open: remember focus, reset query/highlight, focus the input. On close: restore focus.
90
+ $effect(() => {
91
+ if (!open) return;
92
+ previouslyFocused = document.activeElement as HTMLElement | null;
93
+ query = "";
94
+ highlight = 0;
95
+ onopen?.();
96
+ const sbw = window.innerWidth - document.documentElement.clientWidth;
97
+ const prevOverflow = document.body.style.overflow;
98
+ const prevPad = document.body.style.paddingRight;
99
+ document.body.style.overflow = "hidden";
100
+ if (sbw > 0) document.body.style.paddingRight = `${sbw}px`;
101
+ queueMicrotask(() => inputEl?.focus());
102
+ return () => {
103
+ document.body.style.overflow = prevOverflow;
104
+ document.body.style.paddingRight = prevPad;
105
+ previouslyFocused?.focus?.();
106
+ };
107
+ });
108
+ // Keep the highlight in range as the filter changes.
109
+ $effect(() => {
110
+ if (highlight > filtered.length - 1) highlight = Math.max(0, filtered.length - 1);
111
+ });
112
+
113
+ function run(c: Command | undefined) {
114
+ if (!c || c.disabled) return;
115
+ open = false;
116
+ c.onrun();
117
+ }
118
+ function onKeydown(e: KeyboardEvent) {
119
+ const len = filtered.length;
120
+ if (e.key === "ArrowDown") {
121
+ e.preventDefault();
122
+ if (len) highlight = (highlight + 1) % len;
123
+ } else if (e.key === "ArrowUp") {
124
+ e.preventDefault();
125
+ if (len) highlight = (highlight - 1 + len) % len;
126
+ } else if (e.key === "Enter") {
127
+ e.preventDefault();
128
+ run(filtered[highlight]);
129
+ } else if (e.key === "Escape") {
130
+ e.preventDefault();
131
+ open = false;
132
+ }
133
+ }
134
+ </script>
135
+
136
+ <svelte:window onkeydown={onGlobalKey} />
137
+
138
+ {#if open}
139
+ <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
140
+ <div use:portal class="fixed inset-0 z-[9999] flex justify-center bg-black/40 p-4 pt-[12vh]" role="presentation"
141
+ onclick={(e) => { if (e.target === e.currentTarget) open = false; }}>
142
+ <div
143
+ class="h-max w-full max-w-xl overflow-hidden rounded-[var(--ui-border-radius)] border shadow-2xl [background-color:var(--ui-color-background)] [border-color:var(--ui-border-color)]"
144
+ role="dialog"
145
+ aria-modal="true"
146
+ aria-label="Command palette"
147
+ >
148
+ <Input
149
+ bind:value={query}
150
+ bind:element={inputEl}
151
+ {placeholder}
152
+ borderless
153
+ icon={searchIcon}
154
+ iconPosition="left"
155
+ role="combobox"
156
+ aria-expanded="true"
157
+ aria-controls="cmdp-list"
158
+ aria-activedescendant={filtered.length ? `cmdp-opt-${highlight}` : undefined}
159
+ onkeydown={onKeydown}
160
+ class="[&_input]:h-12 border-b [border-color:var(--ui-border-color)] rounded-none"
161
+ />
162
+
163
+ <ul id="cmdp-list" role="listbox" class="max-h-80 overflow-y-auto p-1">
164
+ {#if filtered.length === 0}
165
+ <li class="px-3 py-8 text-center text-sm [color:color-mix(in_srgb,var(--ui-color-text)_55%,transparent)]">
166
+ {empty}
167
+ </li>
168
+ {:else}
169
+ {#each groups as g (g.name)}
170
+ {#if g.name}
171
+ <li class="px-3 pb-1 pt-3 text-xs font-medium uppercase tracking-wide [color:color-mix(in_srgb,var(--ui-color-text)_45%,transparent)]">
172
+ {g.name}
173
+ </li>
174
+ {/if}
175
+ {#each g.items as c (c.id ?? c.label)}
176
+ {@const idx = filtered.indexOf(c)}
177
+ <li>
178
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
179
+ <div
180
+ id={`cmdp-opt-${idx}`}
181
+ role="option"
182
+ aria-selected={idx === highlight}
183
+ aria-disabled={c.disabled || undefined}
184
+ onclick={() => run(c)}
185
+ onpointermove={() => (highlight = idx)}
186
+ class="flex cursor-pointer items-center gap-3 rounded-[var(--ui-border-radius)] px-3 py-2 text-sm {c.disabled
187
+ ? 'opacity-40'
188
+ : ''} {idx === highlight ? '[background-color:var(--ui-color-hover)]' : ''}"
189
+ >
190
+ {#if c.icon}<span class="inline-flex shrink-0 [&_svg]:w-4 [&_svg]:h-4 [color:var(--ui-color-secondary)]">{@render c.icon()}</span>{/if}
191
+ <span class="min-w-0 flex-1 truncate [color:var(--ui-color-text)]">{c.label}</span>
192
+ {#if c.shortcut}<Kbd keys={c.shortcut} size="sm" />{/if}
193
+ </div>
194
+ </li>
195
+ {/each}
196
+ {/each}
197
+ {/if}
198
+ </ul>
199
+ </div>
200
+ </div>
201
+ {/if}
202
+
203
+ {#snippet searchIcon()}<IconSearch />{/snippet}
@@ -0,0 +1,23 @@
1
+ import type { Snippet } from "svelte";
2
+ export type Command = {
3
+ id?: string;
4
+ label: string;
5
+ keywords?: string[];
6
+ group?: string;
7
+ icon?: Snippet;
8
+ shortcut?: string[];
9
+ disabled?: boolean;
10
+ onrun: () => void;
11
+ };
12
+ type $$ComponentProps = {
13
+ open?: boolean;
14
+ commands: Command[];
15
+ /** Global open hotkey — `mod` = ⌘ on macOS, Ctrl elsewhere. Set `""` to disable. */
16
+ hotkey?: string;
17
+ placeholder?: string;
18
+ empty?: string;
19
+ onopen?: () => void;
20
+ };
21
+ declare const CommandPalette: import("svelte").Component<$$ComponentProps, {}, "open">;
22
+ type CommandPalette = ReturnType<typeof CommandPalette>;
23
+ export default CommandPalette;
@@ -0,0 +1,2 @@
1
+ export { default as CommandPalette } from "./CommandPalette.svelte";
2
+ export type { Command } from "./CommandPalette.svelte";
@@ -0,0 +1 @@
1
+ export { default as CommandPalette } from "./CommandPalette.svelte";
@@ -0,0 +1,58 @@
1
+ <!--
2
+ /**
3
+ * ContextMenu — a right-click menu. Wraps its `children` and, on contextmenu, opens the shared `Menu`
4
+ * anchored to a 0-size element positioned at the pointer. Composes `Menu` + `MenuItem[]`.
5
+ * See ContextMenu.spec.md.
6
+ */
7
+ -->
8
+ <script lang="ts">
9
+ import type { Snippet } from "svelte";
10
+ import Menu from "../Menu.svelte";
11
+ import type { MenuItem } from "../../data/table/types.js";
12
+ import type { Variant } from "../../types/variants.js";
13
+ import type { Size } from "../../types/sizes.js";
14
+ import type { Responsive } from "../../types/responsive.js";
15
+
16
+ let {
17
+ items,
18
+ size = "md",
19
+ variant,
20
+ disabled = false,
21
+ class: cls = "",
22
+ children,
23
+ }: {
24
+ items: MenuItem[];
25
+ size?: Responsive<Size>;
26
+ variant?: Variant;
27
+ disabled?: boolean;
28
+ class?: string;
29
+ children: Snippet;
30
+ } = $props();
31
+
32
+ let open = $state(false);
33
+ let anchorEl = $state<HTMLElement>();
34
+ let x = $state(0);
35
+ let y = $state(0);
36
+
37
+ function onContextMenu(e: MouseEvent) {
38
+ if (disabled) return;
39
+ e.preventDefault();
40
+ x = e.clientX;
41
+ y = e.clientY;
42
+ open = true;
43
+ }
44
+ </script>
45
+
46
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
47
+ <div class="contents {cls}" oncontextmenu={onContextMenu}>
48
+ {@render children()}
49
+ </div>
50
+
51
+ <!-- Virtual anchor: a 0-size fixed point at the pointer that `Menu` positions against. -->
52
+ <span
53
+ bind:this={anchorEl}
54
+ aria-hidden="true"
55
+ style="position: fixed; left: {x}px; top: {y}px; width: 0; height: 0; pointer-events: none;"
56
+ ></span>
57
+
58
+ <Menu anchor={anchorEl} bind:open {items} {size} {variant} side="bottom" align="start" />
@@ -0,0 +1,16 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { MenuItem } from "../../data/table/types.js";
3
+ import type { Variant } from "../../types/variants.js";
4
+ import type { Size } from "../../types/sizes.js";
5
+ import type { Responsive } from "../../types/responsive.js";
6
+ type $$ComponentProps = {
7
+ items: MenuItem[];
8
+ size?: Responsive<Size>;
9
+ variant?: Variant;
10
+ disabled?: boolean;
11
+ class?: string;
12
+ children: Snippet;
13
+ };
14
+ declare const ContextMenu: import("svelte").Component<$$ComponentProps, {}, "">;
15
+ type ContextMenu = ReturnType<typeof ContextMenu>;
16
+ export default ContextMenu;
@@ -0,0 +1 @@
1
+ export { default as ContextMenu } from "./ContextMenu.svelte";
@@ -0,0 +1 @@
1
+ export { default as ContextMenu } from "./ContextMenu.svelte";
@@ -0,0 +1,68 @@
1
+ <!--
2
+ /**
3
+ * EmptyState — a zero-data / empty placeholder: a centered icon + title + description + optional
4
+ * action slot. Shared `size`/`variant` axes, `--ui-*` tokens. See EmptyState.spec.md.
5
+ */
6
+ -->
7
+ <script lang="ts">
8
+ import type { Snippet } from "svelte";
9
+ import IconInbox from "~icons/mdi/inbox-outline";
10
+ import type { Variant } from "../../types/variants.js";
11
+ import { variantToken } from "../../types/variants.js";
12
+ import type { Size } from "../../types/sizes.js";
13
+ import { responsiveClasses, type Responsive } from "../../types/responsive.js";
14
+
15
+ let {
16
+ title,
17
+ description,
18
+ icon,
19
+ action,
20
+ variant = "secondary",
21
+ size = "md",
22
+ class: cls = "",
23
+ children,
24
+ }: {
25
+ title?: string;
26
+ description?: string;
27
+ /** Large centered icon; defaults to an inbox glyph. */
28
+ icon?: Snippet;
29
+ /** Action slot (CTA buttons) below the text. */
30
+ action?: Snippet;
31
+ variant?: Variant;
32
+ size?: Responsive<Size>;
33
+ class?: string;
34
+ children?: Snippet;
35
+ } = $props();
36
+
37
+ const token = $derived(variantToken[variant]);
38
+ const iconBox: Record<Size, string> = {
39
+ sm: "w-10 h-10 [&_svg]:w-6 [&_svg]:h-6",
40
+ md: "w-14 h-14 [&_svg]:w-8 [&_svg]:h-8",
41
+ lg: "w-20 h-20 [&_svg]:w-12 [&_svg]:h-12",
42
+ };
43
+ const titleSize: Record<Size, string> = { sm: "text-base", md: "text-lg", lg: "text-2xl" };
44
+ const padSize: Record<Size, string> = { sm: "py-6 gap-2", md: "py-10 gap-3", lg: "py-16 gap-4" };
45
+ </script>
46
+
47
+ <div class="flex flex-col items-center text-center {responsiveClasses(size, padSize)} {cls}">
48
+ <div
49
+ class="inline-flex items-center justify-center rounded-full {responsiveClasses(size, iconBox)}"
50
+ style="background-color: color-mix(in srgb, var({token}) 14%, transparent); color: color-mix(in srgb, var({token}) var(--ui-tint-strong), var(--ui-color-text));"
51
+ aria-hidden="true"
52
+ >
53
+ {#if icon}{@render icon()}{:else}<IconInbox />{/if}
54
+ </div>
55
+
56
+ {#if title}
57
+ <h3 class="font-semibold [color:var(--ui-color-text)] {responsiveClasses(size, titleSize)}">
58
+ {title}
59
+ </h3>
60
+ {/if}
61
+ {#if description}
62
+ <p class="max-w-sm text-sm [color:color-mix(in_srgb,var(--ui-color-text)_60%,transparent)]">
63
+ {description}
64
+ </p>
65
+ {/if}
66
+ {#if children}{@render children()}{/if}
67
+ {#if action}<div class="mt-1 flex flex-wrap items-center justify-center gap-2">{@render action()}</div>{/if}
68
+ </div>
@@ -0,0 +1,19 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { Variant } from "../../types/variants.js";
3
+ import type { Size } from "../../types/sizes.js";
4
+ import { type Responsive } from "../../types/responsive.js";
5
+ type $$ComponentProps = {
6
+ title?: string;
7
+ description?: string;
8
+ /** Large centered icon; defaults to an inbox glyph. */
9
+ icon?: Snippet;
10
+ /** Action slot (CTA buttons) below the text. */
11
+ action?: Snippet;
12
+ variant?: Variant;
13
+ size?: Responsive<Size>;
14
+ class?: string;
15
+ children?: Snippet;
16
+ };
17
+ declare const EmptyState: import("svelte").Component<$$ComponentProps, {}, "">;
18
+ type EmptyState = ReturnType<typeof EmptyState>;
19
+ export default EmptyState;
@@ -0,0 +1 @@
1
+ export { default as EmptyState } from "./EmptyState.svelte";
@@ -0,0 +1 @@
1
+ export { default as EmptyState } from "./EmptyState.svelte";