@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.
- package/.claude/skills/svelte-ui/SKILL.md +5 -5
- package/dist/components/Carousel/Carousel.svelte +147 -0
- package/dist/components/Carousel/Carousel.svelte.d.ts +38 -0
- package/dist/components/Carousel/index.d.ts +1 -0
- package/dist/components/Carousel/index.js +1 -0
- package/dist/components/CommandPalette/CommandPalette.svelte +203 -0
- package/dist/components/CommandPalette/CommandPalette.svelte.d.ts +23 -0
- package/dist/components/CommandPalette/index.d.ts +2 -0
- package/dist/components/CommandPalette/index.js +1 -0
- package/dist/components/ContextMenu/ContextMenu.svelte +58 -0
- package/dist/components/ContextMenu/ContextMenu.svelte.d.ts +16 -0
- package/dist/components/ContextMenu/index.d.ts +1 -0
- package/dist/components/ContextMenu/index.js +1 -0
- package/dist/components/EmptyState/EmptyState.svelte +68 -0
- package/dist/components/EmptyState/EmptyState.svelte.d.ts +19 -0
- package/dist/components/EmptyState/index.d.ts +1 -0
- package/dist/components/EmptyState/index.js +1 -0
- package/dist/components/Kbd/Kbd.svelte +53 -0
- package/dist/components/Kbd/Kbd.svelte.d.ts +15 -0
- package/dist/components/Kbd/index.d.ts +1 -0
- package/dist/components/Kbd/index.js +1 -0
- package/dist/components/ScrollArea/ScrollArea.svelte +71 -0
- package/dist/components/ScrollArea/ScrollArea.svelte.d.ts +15 -0
- package/dist/components/ScrollArea/index.d.ts +1 -0
- package/dist/components/ScrollArea/index.js +1 -0
- package/dist/components/Splitter/Splitter.svelte +127 -0
- package/dist/components/Splitter/Splitter.svelte.d.ts +18 -0
- package/dist/components/Splitter/index.d.ts +1 -0
- package/dist/components/Splitter/index.js +1 -0
- package/dist/components/Stat/Stat.svelte +95 -0
- package/dist/components/Stat/Stat.svelte.d.ts +21 -0
- package/dist/components/Stat/index.d.ts +1 -0
- package/dist/components/Stat/index.js +1 -0
- package/dist/components/Timeline/Timeline.svelte +98 -0
- package/dist/components/Timeline/Timeline.svelte.d.ts +21 -0
- package/dist/components/Timeline/index.d.ts +2 -0
- package/dist/components/Timeline/index.js +1 -0
- package/dist/components/Tree/Tree.svelte +178 -0
- package/dist/components/Tree/Tree.svelte.d.ts +26 -0
- package/dist/components/Tree/index.d.ts +2 -0
- package/dist/components/Tree/index.js +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +10 -0
- package/docs/components.md +106 -0
- package/docs/usage.md +166 -0
- 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 @@
|
|
|
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";
|