@spaethtech/svelte-ui 0.14.0 → 0.15.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.
Files changed (54) hide show
  1. package/.claude/skills/svelte-ui/SKILL.md +14 -11
  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/Combobox/Combobox.svelte +62 -31
  7. package/dist/components/Combobox/Combobox.svelte.d.ts +1 -1
  8. package/dist/components/CommandPalette/CommandPalette.svelte +203 -0
  9. package/dist/components/CommandPalette/CommandPalette.svelte.d.ts +23 -0
  10. package/dist/components/CommandPalette/index.d.ts +2 -0
  11. package/dist/components/CommandPalette/index.js +1 -0
  12. package/dist/components/ContextMenu/ContextMenu.svelte +58 -0
  13. package/dist/components/ContextMenu/ContextMenu.svelte.d.ts +16 -0
  14. package/dist/components/ContextMenu/index.d.ts +1 -0
  15. package/dist/components/ContextMenu/index.js +1 -0
  16. package/dist/components/EmptyState/EmptyState.svelte +68 -0
  17. package/dist/components/EmptyState/EmptyState.svelte.d.ts +19 -0
  18. package/dist/components/EmptyState/index.d.ts +1 -0
  19. package/dist/components/EmptyState/index.js +1 -0
  20. package/dist/components/Kbd/Kbd.svelte +53 -0
  21. package/dist/components/Kbd/Kbd.svelte.d.ts +15 -0
  22. package/dist/components/Kbd/index.d.ts +1 -0
  23. package/dist/components/Kbd/index.js +1 -0
  24. package/dist/components/Pagination/Pagination.svelte +58 -43
  25. package/dist/components/Pagination/Pagination.svelte.d.ts +7 -2
  26. package/dist/components/Rating.svelte +169 -30
  27. package/dist/components/Rating.svelte.d.ts +22 -4
  28. package/dist/components/ScrollArea/ScrollArea.svelte +71 -0
  29. package/dist/components/ScrollArea/ScrollArea.svelte.d.ts +15 -0
  30. package/dist/components/ScrollArea/index.d.ts +1 -0
  31. package/dist/components/ScrollArea/index.js +1 -0
  32. package/dist/components/Slider/Slider.svelte +5 -3
  33. package/dist/components/Splitter/Splitter.svelte +127 -0
  34. package/dist/components/Splitter/Splitter.svelte.d.ts +18 -0
  35. package/dist/components/Splitter/index.d.ts +1 -0
  36. package/dist/components/Splitter/index.js +1 -0
  37. package/dist/components/Stat/Stat.svelte +95 -0
  38. package/dist/components/Stat/Stat.svelte.d.ts +21 -0
  39. package/dist/components/Stat/index.d.ts +1 -0
  40. package/dist/components/Stat/index.js +1 -0
  41. package/dist/components/Timeline/Timeline.svelte +98 -0
  42. package/dist/components/Timeline/Timeline.svelte.d.ts +21 -0
  43. package/dist/components/Timeline/index.d.ts +2 -0
  44. package/dist/components/Timeline/index.js +1 -0
  45. package/dist/components/TokenInput/TokenInput.svelte +12 -6
  46. package/dist/components/Tree/Tree.svelte +178 -0
  47. package/dist/components/Tree/Tree.svelte.d.ts +26 -0
  48. package/dist/components/Tree/index.d.ts +2 -0
  49. package/dist/components/Tree/index.js +1 -0
  50. package/dist/index.d.ts +13 -2
  51. package/dist/index.js +10 -1
  52. package/docs/components.md +125 -22
  53. package/docs/usage.md +180 -13
  54. package/package.json +1 -1
@@ -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";
@@ -0,0 +1,53 @@
1
+ <!--
2
+ /**
3
+ * Kbd — a keyboard-key display. Render a single key (`text`/children) or a combo (`keys` → joined with
4
+ * "+"). Styled as a keycap from `--ui-*` tokens; shared `size` axis. See Kbd.spec.md.
5
+ */
6
+ -->
7
+ <script lang="ts">
8
+ import type { Snippet } from "svelte";
9
+ import type { Size } from "../../types/sizes.js";
10
+ import { responsiveClasses, type Responsive } from "../../types/responsive.js";
11
+
12
+ let {
13
+ keys,
14
+ text,
15
+ size = "md",
16
+ class: cls = "",
17
+ children,
18
+ }: {
19
+ /** A combo — each string is rendered as its own keycap, joined with "+". */
20
+ keys?: string[];
21
+ /** A single key label (alternative to `keys`/children). */
22
+ text?: string;
23
+ size?: Responsive<Size>;
24
+ class?: string;
25
+ children?: Snippet;
26
+ } = $props();
27
+
28
+ const list = $derived(keys ?? null);
29
+
30
+ const capSize: Record<Size, string> = {
31
+ sm: "text-[0.6875rem] min-w-5 h-5 px-1",
32
+ md: "text-xs min-w-6 h-6 px-1.5",
33
+ lg: "text-sm min-w-7 h-7 px-2",
34
+ };
35
+ // Keycap: subtle surface fill, tinted border with a slightly darker bottom edge (a shallow 3D key).
36
+ const cap =
37
+ "inline-flex items-center justify-center rounded border font-medium [font-family:ui-monospace,SFMono-Regular,Menlo,monospace] [background-color:var(--ui-color-surface)] [border-color:var(--ui-border-color)] [box-shadow:inset_0_-1px_0_var(--ui-border-color)] [color:var(--ui-color-text)] select-none";
38
+ </script>
39
+
40
+ {#if list}
41
+ <span class="inline-flex items-center gap-1 {cls}">
42
+ {#each list as k, i (i)}
43
+ {#if i > 0}<span class="text-xs [color:color-mix(in_srgb,var(--ui-color-text)_50%,transparent)]"
44
+ >+</span
45
+ >{/if}
46
+ <kbd class="{cap} {responsiveClasses(size, capSize)}">{k}</kbd>
47
+ {/each}
48
+ </span>
49
+ {:else}
50
+ <kbd class="{cap} {responsiveClasses(size, capSize)} {cls}"
51
+ >{#if children}{@render children()}{:else}{text}{/if}</kbd
52
+ >
53
+ {/if}
@@ -0,0 +1,15 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { Size } from "../../types/sizes.js";
3
+ import { type Responsive } from "../../types/responsive.js";
4
+ type $$ComponentProps = {
5
+ /** A combo — each string is rendered as its own keycap, joined with "+". */
6
+ keys?: string[];
7
+ /** A single key label (alternative to `keys`/children). */
8
+ text?: string;
9
+ size?: Responsive<Size>;
10
+ class?: string;
11
+ children?: Snippet;
12
+ };
13
+ declare const Kbd: import("svelte").Component<$$ComponentProps, {}, "">;
14
+ type Kbd = ReturnType<typeof Kbd>;
15
+ export default Kbd;
@@ -0,0 +1 @@
1
+ export { default as Kbd } from "./Kbd.svelte";
@@ -0,0 +1 @@
1
+ export { default as Kbd } from "./Kbd.svelte";
@@ -1,14 +1,18 @@
1
1
  <!--
2
2
  /**
3
- * Pagination — a standalone page navigator (numbered buttons + ellipsis, prev/next, optional
4
- * first/last, optional rows-per-page Select + total). The reusable extraction of DataTable's footer
5
- * pager; built only from `Button` (ghost, active = filled variant) and `Select`. See Pagination.spec.md.
3
+ * Pagination — a standalone page navigator laid out like DataTable's footer: a tinted, bordered band
4
+ * with an optional per-page `Select` on the left and a compact `First · Prev · [page]/N · Next · Last`
5
+ * stepper on the right. The page indicator is an editable `NumberInput` for quick-jump. `variant` tints
6
+ * the band surface + button hover (via the shared `.ui-accent` override) — it does NOT fill buttons.
7
+ * Built only from `Button`, `Select`, and `NumberInput`. See Pagination.spec.md.
6
8
  */
7
9
  -->
8
10
  <script lang="ts">
9
11
  import Button from "../Button.svelte";
10
12
  import Select from "../Select.svelte";
13
+ import NumberInput from "../NumberInput.svelte";
11
14
  import type { Variant } from "../../types/variants.js";
15
+ import { variantToken } from "../../types/variants.js";
12
16
  import type { Size } from "../../types/sizes.js";
13
17
  import { responsiveClasses, type Responsive } from "../../types/responsive.js";
14
18
  import IconFirst from "~icons/mdi/page-first";
@@ -21,25 +25,29 @@
21
25
  total,
22
26
  perPage = $bindable(10),
23
27
  perPageOptions,
24
- siblingCount = 1,
25
- boundaryCount = 1,
26
- showEdges = false,
27
- showTotal = false,
28
- variant = "primary",
28
+ showEdges = true,
29
+ showTotal = true,
30
+ variant,
29
31
  size = "md",
32
+ borderless = false,
30
33
  disabled = false,
31
34
  class: cls = "",
32
35
  }: {
33
36
  page?: number;
34
37
  total: number;
35
38
  perPage?: number;
39
+ /** When set, renders the per-page `Select` on the left. */
36
40
  perPageOptions?: number[];
37
- siblingCount?: number;
38
- boundaryCount?: number;
41
+ /** Show the First/Last edge buttons (default true, matching DataTable's footer). */
39
42
  showEdges?: boolean;
43
+ /** Show the "start–end of total" range text (default true). */
40
44
  showTotal?: boolean;
45
+ /** Color axis — tints the band surface + button hover (like DataTable), NOT the buttons. Omit for
46
+ * a neutral surface band; `ghost` makes the band transparent. */
41
47
  variant?: Variant;
42
48
  size?: Responsive<Size>;
49
+ /** Drop the band border/surface, leaving just the controls. */
50
+ borderless?: boolean;
43
51
  disabled?: boolean;
44
52
  class?: string;
45
53
  } = $props();
@@ -51,23 +59,10 @@
51
59
  if (clamped !== page) page = clamped;
52
60
  });
53
61
 
54
- // Page-number list with ellipsis gaps: boundaryCount at each end + siblingCount around the current.
55
- const items = $derived.by<(number | "ellipsis")[]>(() => {
56
- const set = new Set<number>();
57
- for (let i = 1; i <= boundaryCount; i++) {
58
- set.add(i);
59
- set.add(pageCount - i + 1);
60
- }
61
- for (let i = page - siblingCount; i <= page + siblingCount; i++) set.add(i);
62
- const sorted = [...set].filter((n) => n >= 1 && n <= pageCount).sort((a, b) => a - b);
63
- const out: (number | "ellipsis")[] = [];
64
- let prev = 0;
65
- for (const n of sorted) {
66
- if (n - prev > 1) out.push("ellipsis");
67
- out.push(n);
68
- prev = n;
69
- }
70
- return out;
62
+ // The quick-jump field mirrors `page`; committed on change (Enter/blur), not on every keystroke.
63
+ let pageInput = $state(page);
64
+ $effect(() => {
65
+ pageInput = page;
71
66
  });
72
67
 
73
68
  const rangeText = $derived(
@@ -78,18 +73,38 @@
78
73
  const subtle = $derived(
79
74
  `${responsiveClasses(size, { sm: "text-xs", md: "text-sm", lg: "text-base" })} [color:color-mix(in_srgb,var(--ui-color-text)_60%,transparent)]`,
80
75
  );
76
+ const bandPad = $derived(responsiveClasses(size, { sm: "px-2 py-1.5", md: "px-3 py-2", lg: "px-3.5 py-2.5" }));
77
+
78
+ // Band surface + border: variant re-points --ui-accent so --ui-color-surface/hover re-mix from it
79
+ // (the shared .ui-accent mechanism DataTable's footer uses). `ghost` ⇒ transparent band.
80
+ const accentVar = $derived(variant ? `--ui-accent: var(${variantToken[variant]});` : "");
81
+ const sectionBorder = "color-mix(in srgb, var(--ui-color-secondary) var(--ui-tint-border), transparent)";
82
+ const bandBg = $derived(variant === "ghost" ? "transparent" : "var(--ui-color-surface)");
83
+ const bandBorder = $derived(borderless ? "transparent" : sectionBorder);
81
84
 
82
85
  const go = (to: number) => {
83
86
  if (disabled) return;
84
87
  page = Math.min(Math.max(to, 1), pageCount);
88
+ pageInput = page;
85
89
  };
86
90
  const setPerPage = (n: number) => {
87
91
  perPage = n;
88
92
  page = 1;
89
93
  };
94
+ const commitJump = () => {
95
+ if (pageInput == null) {
96
+ pageInput = page;
97
+ return;
98
+ }
99
+ go(pageInput);
100
+ };
90
101
  </script>
91
102
 
92
- <nav aria-label="Pagination" class="flex flex-wrap items-center justify-between gap-3 {cls}">
103
+ <nav
104
+ aria-label="Pagination"
105
+ class="ui-accent flex flex-wrap items-center justify-between gap-3 border rounded-[var(--ui-border-radius)] {bandPad} {cls}"
106
+ style="border-color: {bandBorder}; background-color: {bandBg}; {accentVar}"
107
+ >
93
108
  {#if perPageOptions?.length}
94
109
  <div class="flex items-center gap-2">
95
110
  <Select
@@ -104,7 +119,7 @@
104
119
  </div>
105
120
  {/if}
106
121
 
107
- <div class="flex items-center gap-3">
122
+ <div class="flex items-center gap-3 {perPageOptions?.length ? '' : 'ml-auto'}">
108
123
  {#if showTotal}<span class={subtle}>{rangeText}</span>{/if}
109
124
  <div class="inline-flex items-center gap-0.5">
110
125
  {#snippet navBtn(Icon: typeof IconFirst, to: number, off: boolean, aria: string)}
@@ -123,22 +138,22 @@
123
138
  {#if showEdges}{@render navBtn(IconFirst, 1, page <= 1, "First page")}{/if}
124
139
  {@render navBtn(IconPrev, page - 1, page <= 1, "Previous page")}
125
140
 
126
- {#each items as it, i (i)}
127
- {#if it === "ellipsis"}
128
- <span aria-hidden="true" class="px-1.5 {subtle}">…</span>
129
- {:else}
130
- <Button
131
- variant={it === page ? variant : "ghost"}
141
+ <div class="flex items-center gap-1.5 px-1 {subtle}">
142
+ <span class="inline-block {responsiveClasses(size, { sm: 'w-11', md: 'w-12', lg: 'w-14' })}">
143
+ <NumberInput
144
+ bind:value={pageInput}
145
+ min={1}
146
+ max={pageCount}
147
+ clamp
132
148
  {size}
133
149
  {disabled}
134
- aria-label={`Page ${it}`}
135
- aria-current={it === page ? "page" : undefined}
136
- onclick={() => go(it)}
137
- >
138
- {it}
139
- </Button>
140
- {/if}
141
- {/each}
150
+ inputClass="text-center px-1"
151
+ aria-label="Page number"
152
+ onchange={commitJump}
153
+ />
154
+ </span>
155
+ <span aria-hidden="true">/&nbsp;{pageCount}</span>
156
+ </div>
142
157
 
143
158
  {@render navBtn(IconNext, page + 1, page >= pageCount, "Next page")}
144
159
  {#if showEdges}{@render navBtn(IconLast, pageCount, page >= pageCount, "Last page")}{/if}
@@ -5,13 +5,18 @@ type $$ComponentProps = {
5
5
  page?: number;
6
6
  total: number;
7
7
  perPage?: number;
8
+ /** When set, renders the per-page `Select` on the left. */
8
9
  perPageOptions?: number[];
9
- siblingCount?: number;
10
- boundaryCount?: number;
10
+ /** Show the First/Last edge buttons (default true, matching DataTable's footer). */
11
11
  showEdges?: boolean;
12
+ /** Show the "start–end of total" range text (default true). */
12
13
  showTotal?: boolean;
14
+ /** Color axis — tints the band surface + button hover (like DataTable), NOT the buttons. Omit for
15
+ * a neutral surface band; `ghost` makes the band transparent. */
13
16
  variant?: Variant;
14
17
  size?: Responsive<Size>;
18
+ /** Drop the band border/surface, leaving just the controls. */
19
+ borderless?: boolean;
15
20
  disabled?: boolean;
16
21
  class?: string;
17
22
  };