@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
@@ -1,47 +1,168 @@
1
+ <!--
2
+ /**
3
+ * Rating — star rating in two modes:
4
+ * • Display (default): a read-only average. `average` is a 0–10 score (TMDB-style), shown as 5 stars
5
+ * with halves, plus an optional `count`.
6
+ * • Interactive (`interactive`): a form control — click / hover / arrow-key a `bind:value` (0..max
7
+ * stars), optional half-steps. `role="slider"` (same a11y pattern as our Slider).
8
+ * See usage docs.
9
+ */
10
+ -->
1
11
  <script lang="ts">
2
12
  import { variantToken, type Variant } from "../types/variants.js";
3
13
  import type { Size } from "../types/sizes.js";
4
14
 
5
15
  export type RatingProps = {
6
- average: number;
7
- count: number;
16
+ /** Display mode: a 0–10 average score (TMDB-style), rendered as 5 stars (average / 2). */
17
+ average?: number;
18
+ /** Display mode: number of ratings, shown in parentheses. */
19
+ count?: number;
20
+ /** Interactive mode: the selected rating in stars (0..max), bindable. */
21
+ value?: number;
22
+ /** Enable selection (click / hover / keyboard). Off = read-only display. */
23
+ interactive?: boolean;
24
+ /** Interactive but not editable (shows `value`, no input). */
25
+ readonly?: boolean;
26
+ disabled?: boolean;
27
+ /** Number of stars in interactive mode (default 5). */
28
+ max?: number;
29
+ /** Allow half-star selection/display (interactive). */
30
+ allowHalf?: boolean;
31
+ /** Show the numeric value beside the stars (default: true in display mode, false interactive). */
32
+ showValue?: boolean;
33
+ /** Fired on change (interactive). */
34
+ onchange?: (value: number) => void;
8
35
  /** Filled-star colour. Defaults to gold; a variant tints from its token (e.g. `warning`, `primary`). */
9
36
  variant?: Variant;
10
- /** Star size (sm/md/lg). */
11
37
  size?: Size;
38
+ /** Accessible label for the interactive slider. */
39
+ label?: string;
12
40
  };
13
41
 
14
- let { average, count, variant, size = "md" }: RatingProps = $props();
42
+ let {
43
+ average,
44
+ count,
45
+ value = $bindable(0),
46
+ interactive = false,
47
+ readonly = false,
48
+ disabled = false,
49
+ max = 5,
50
+ allowHalf = false,
51
+ showValue,
52
+ onchange,
53
+ variant,
54
+ size = "md",
55
+ label = "Rating",
56
+ }: RatingProps = $props();
15
57
 
16
58
  const starColor = $derived(variant ? `var(${variantToken[variant]})` : "#ffd700");
17
59
  const starSize: Record<Size, string> = { sm: "1rem", md: "1.2rem", lg: "1.5rem" };
18
60
 
19
- // Validate and sanitize the average rating
20
- const validAverage = isNaN(average) || average < 0 ? 0 : Math.min(average, 10);
61
+ const active = $derived(interactive && !readonly && !disabled);
62
+ const step = $derived(allowHalf ? 0.5 : 1);
21
63
 
22
- // Convert TMDB 10-point scale to 5-star scale
23
- const fiveStarRating = validAverage / 2;
64
+ // Display mode: sanitize the 0–10 average and convert to a 5-point scale.
65
+ const displayFive = $derived.by(() => {
66
+ const a = isNaN(average ?? NaN) || (average ?? 0) < 0 ? 0 : Math.min(average ?? 0, 10);
67
+ return a / 2;
68
+ });
24
69
 
25
- // Calculate how many stars should be filled (ensure they're valid numbers)
26
- const filledStars = Math.max(0, Math.min(5, Math.floor(fiveStarRating)));
27
- const hasHalfStar = fiveStarRating % 1 >= 0.5;
28
- const emptyStars = Math.max(0, 5 - filledStars - (hasHalfStar ? 1 : 0));
70
+ // Hover preview (interactive) overrides `value` while pointing.
71
+ let hover = $state<number | null>(null);
72
+
73
+ const starCount = $derived(interactive ? max : 5);
74
+ // The numeric value the stars are painted from.
75
+ const paint = $derived(interactive ? (hover ?? value) : displayFive);
76
+ const readout = $derived(interactive ? value : displayFive);
77
+ const wantValue = $derived(showValue ?? !interactive);
78
+
79
+ // Fill state for the i-th star (1-based).
80
+ function fillOf(i: number): "full" | "half" | "empty" {
81
+ if (paint >= i) return "full";
82
+ if (paint >= i - 0.5) return "half";
83
+ return "empty";
84
+ }
85
+ // Half-step from the pointer's position within a star (left half = i-0.5).
86
+ function valueAt(i: number, e: MouseEvent): number {
87
+ if (!allowHalf) return i;
88
+ const r = (e.currentTarget as HTMLElement).getBoundingClientRect();
89
+ return e.clientX - r.left < r.width / 2 ? i - 0.5 : i;
90
+ }
91
+ function pick(i: number, e: MouseEvent) {
92
+ if (!active) return;
93
+ value = valueAt(i, e);
94
+ onchange?.(value);
95
+ }
96
+ function preview(i: number, e: MouseEvent) {
97
+ if (!active) return;
98
+ hover = valueAt(i, e);
99
+ }
100
+ function onkeydown(e: KeyboardEvent) {
101
+ if (!active) return;
102
+ let v = value;
103
+ switch (e.key) {
104
+ case "ArrowRight":
105
+ case "ArrowUp":
106
+ v = Math.min(max, value + step);
107
+ break;
108
+ case "ArrowLeft":
109
+ case "ArrowDown":
110
+ v = Math.max(0, value - step);
111
+ break;
112
+ case "Home":
113
+ v = 0;
114
+ break;
115
+ case "End":
116
+ v = max;
117
+ break;
118
+ default:
119
+ return;
120
+ }
121
+ e.preventDefault();
122
+ value = v;
123
+ onchange?.(v);
124
+ }
29
125
  </script>
30
126
 
31
- <div class="rating select-none" style="--star-color: {starColor}; --star-size: {starSize[size]}">
32
- <div class="stars">
33
- {#each Array(filledStars) as _, i}
34
- <span class="star filled">★</span>
35
- {/each}
36
- {#if hasHalfStar}
37
- <span class="star half-filled">☆</span>
38
- {/if}
39
- {#each Array(emptyStars) as _, i}
40
- <span class="star empty">☆</span>
127
+ <div
128
+ class="rating select-none"
129
+ class:disabled
130
+ style="--star-color: {starColor}; --star-size: {starSize[size]}"
131
+ >
132
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
133
+ <div
134
+ class="stars"
135
+ role={active ? "slider" : undefined}
136
+ tabindex={active ? 0 : undefined}
137
+ aria-label={active ? label : undefined}
138
+ aria-valuemin={active ? 0 : undefined}
139
+ aria-valuemax={active ? max : undefined}
140
+ aria-valuenow={active ? value : undefined}
141
+ aria-valuetext={active ? `${value} of ${max} stars` : undefined}
142
+ aria-readonly={interactive && readonly ? true : undefined}
143
+ aria-disabled={disabled || undefined}
144
+ onkeydown={active ? onkeydown : undefined}
145
+ onpointerleave={active ? () => (hover = null) : undefined}
146
+ >
147
+ {#each Array(starCount) as _, idx (idx)}
148
+ {@const i = idx + 1}
149
+ {@const f = fillOf(i)}
150
+ <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
151
+ <span
152
+ class="star {f} {active ? 'cursor-pointer' : ''}"
153
+ onpointermove={active ? (e) => preview(i, e) : undefined}
154
+ onclick={active ? (e) => pick(i, e) : undefined}
155
+ >
156
+ {f === "empty" ? "☆" : "★"}
157
+ </span>
41
158
  {/each}
42
159
  </div>
43
- <span class="rating-value">{fiveStarRating.toFixed(1)}</span>
44
- <span class="rating-count">({count})</span>
160
+ {#if wantValue}
161
+ <span class="rating-value">{readout.toFixed(1)}</span>
162
+ {/if}
163
+ {#if !interactive && count != null}
164
+ <span class="rating-count">({count})</span>
165
+ {/if}
45
166
  </div>
46
167
 
47
168
  <style>
@@ -50,30 +171,48 @@
50
171
  align-items: center;
51
172
  gap: 0.5rem;
52
173
  }
174
+ .rating.disabled {
175
+ opacity: 0.5;
176
+ pointer-events: none;
177
+ }
53
178
 
54
179
  .stars {
55
180
  display: flex;
56
181
  gap: 0.1rem;
57
182
  }
183
+ .stars[role="slider"] {
184
+ border-radius: 4px;
185
+ }
186
+ .stars[role="slider"]:focus-visible {
187
+ outline: 2px solid color-mix(in srgb, var(--ui-color-text) 70%, transparent);
188
+ outline-offset: 2px;
189
+ }
58
190
 
59
191
  .star {
60
192
  font-size: var(--star-size, 1.2rem);
61
193
  line-height: 1;
62
194
  }
63
195
 
64
- .star.filled {
65
- color: var(--star-color, #ffd700);
66
- }
67
-
68
- .star.half-filled {
196
+ .star.full {
69
197
  color: var(--star-color, #ffd700);
70
- opacity: 0.6;
71
198
  }
72
199
 
73
200
  .star.empty {
74
201
  color: var(--ui-color-secondary);
75
202
  }
76
203
 
204
+ /* Half star: a full ★ glyph clipped to a left-gold / right-muted gradient. */
205
+ .star.half {
206
+ background: linear-gradient(
207
+ 90deg,
208
+ var(--star-color, #ffd700) 50%,
209
+ var(--ui-color-secondary) 50%
210
+ );
211
+ -webkit-background-clip: text;
212
+ background-clip: text;
213
+ color: transparent;
214
+ }
215
+
77
216
  .rating-value {
78
217
  font-weight: 600;
79
218
  color: var(--ui-color-text);
@@ -1,13 +1,31 @@
1
1
  import { type Variant } from "../types/variants.js";
2
2
  import type { Size } from "../types/sizes.js";
3
3
  export type RatingProps = {
4
- average: number;
5
- count: number;
4
+ /** Display mode: a 0–10 average score (TMDB-style), rendered as 5 stars (average / 2). */
5
+ average?: number;
6
+ /** Display mode: number of ratings, shown in parentheses. */
7
+ count?: number;
8
+ /** Interactive mode: the selected rating in stars (0..max), bindable. */
9
+ value?: number;
10
+ /** Enable selection (click / hover / keyboard). Off = read-only display. */
11
+ interactive?: boolean;
12
+ /** Interactive but not editable (shows `value`, no input). */
13
+ readonly?: boolean;
14
+ disabled?: boolean;
15
+ /** Number of stars in interactive mode (default 5). */
16
+ max?: number;
17
+ /** Allow half-star selection/display (interactive). */
18
+ allowHalf?: boolean;
19
+ /** Show the numeric value beside the stars (default: true in display mode, false interactive). */
20
+ showValue?: boolean;
21
+ /** Fired on change (interactive). */
22
+ onchange?: (value: number) => void;
6
23
  /** Filled-star colour. Defaults to gold; a variant tints from its token (e.g. `warning`, `primary`). */
7
24
  variant?: Variant;
8
- /** Star size (sm/md/lg). */
9
25
  size?: Size;
26
+ /** Accessible label for the interactive slider. */
27
+ label?: string;
10
28
  };
11
- declare const Rating: import("svelte").Component<RatingProps, {}, "">;
29
+ declare const Rating: import("svelte").Component<RatingProps, {}, "value">;
12
30
  type Rating = ReturnType<typeof Rating>;
13
31
  export default Rating;
@@ -0,0 +1,71 @@
1
+ <!--
2
+ /**
3
+ * ScrollArea — a scroll container with themed, thin scrollbars (from `--ui-*` tokens) that look
4
+ * consistent across browsers. Wraps arbitrary content. See ScrollArea.spec.md.
5
+ */
6
+ -->
7
+ <script lang="ts">
8
+ import type { Snippet } from "svelte";
9
+ import type { Variant } from "../../types/variants.js";
10
+ import { variantToken } from "../../types/variants.js";
11
+
12
+ let {
13
+ orientation = "vertical",
14
+ maxHeight,
15
+ height,
16
+ variant,
17
+ class: cls = "",
18
+ children,
19
+ }: {
20
+ orientation?: "vertical" | "horizontal" | "both";
21
+ /** Cap the height (any CSS length) — content beyond scrolls. */
22
+ maxHeight?: string;
23
+ height?: string;
24
+ /** Thumb colour accent; omit for a neutral text-tint thumb. */
25
+ variant?: Variant;
26
+ class?: string;
27
+ children: Snippet;
28
+ } = $props();
29
+
30
+ const overflow = $derived(
31
+ orientation === "horizontal"
32
+ ? "overflow-x-auto overflow-y-hidden"
33
+ : orientation === "both"
34
+ ? "overflow-auto"
35
+ : "overflow-y-auto overflow-x-hidden",
36
+ );
37
+
38
+ const base = $derived(variant ? `var(${variantToken[variant]})` : "var(--ui-color-text)");
39
+ const style = $derived(
40
+ `--sa-thumb: color-mix(in srgb, ${base} 28%, transparent); --sa-thumb-hover: color-mix(in srgb, ${base} 45%, transparent);` +
41
+ (height ? ` height: ${height};` : "") +
42
+ (maxHeight ? ` max-height: ${maxHeight};` : ""),
43
+ );
44
+ </script>
45
+
46
+ <div class="ui-scroll {overflow} {cls}" {style}>
47
+ {@render children()}
48
+ </div>
49
+
50
+ <style>
51
+ .ui-scroll {
52
+ scrollbar-width: thin;
53
+ scrollbar-color: var(--sa-thumb) transparent;
54
+ }
55
+ .ui-scroll::-webkit-scrollbar {
56
+ width: 10px;
57
+ height: 10px;
58
+ }
59
+ .ui-scroll::-webkit-scrollbar-track {
60
+ background: transparent;
61
+ }
62
+ .ui-scroll::-webkit-scrollbar-thumb {
63
+ background-color: var(--sa-thumb);
64
+ border-radius: 9999px;
65
+ border: 2px solid transparent;
66
+ background-clip: padding-box;
67
+ }
68
+ .ui-scroll::-webkit-scrollbar-thumb:hover {
69
+ background-color: var(--sa-thumb-hover);
70
+ }
71
+ </style>
@@ -0,0 +1,15 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { Variant } from "../../types/variants.js";
3
+ type $$ComponentProps = {
4
+ orientation?: "vertical" | "horizontal" | "both";
5
+ /** Cap the height (any CSS length) — content beyond scrolls. */
6
+ maxHeight?: string;
7
+ height?: string;
8
+ /** Thumb colour accent; omit for a neutral text-tint thumb. */
9
+ variant?: Variant;
10
+ class?: string;
11
+ children: Snippet;
12
+ };
13
+ declare const ScrollArea: import("svelte").Component<$$ComponentProps, {}, "">;
14
+ type ScrollArea = ReturnType<typeof ScrollArea>;
15
+ export default ScrollArea;
@@ -0,0 +1 @@
1
+ export { default as ScrollArea } from "./ScrollArea.svelte";
@@ -0,0 +1 @@
1
+ export { default as ScrollArea } from "./ScrollArea.svelte";
@@ -166,14 +166,16 @@
166
166
 
167
167
  <FieldChrome {label} {aside} {error} {description} {required} {controlId} {size} class={cls}>
168
168
  {#snippet control({ describedBy })}
169
- <div class="flex items-center gap-3">
169
+ <div class="flex items-center gap-3 select-none">
170
170
  <!-- The track is the click-to-position surface; the thumbs below carry role="slider". -->
171
171
  <!-- svelte-ignore a11y_no_static_element_interactions -->
172
+ <!-- Disabled keeps pointer events (so the not-allowed cursor shows) but every handler no-ops on
173
+ `disabled`, so it can't be dragged/keyed. -->
172
174
  <div
173
175
  bind:this={trackEl}
174
176
  onpointerdown={onTrackPointerDown}
175
177
  class="relative flex-1 select-none rounded-full {disabled
176
- ? 'opacity-50 pointer-events-none'
178
+ ? 'opacity-50 cursor-not-allowed'
177
179
  : 'cursor-pointer'} {responsiveClasses(size, trackH)}"
178
180
  style="background-color: color-mix(in srgb, var({token}) 16%, transparent);"
179
181
  >
@@ -198,7 +200,7 @@
198
200
  aria-describedby={describedBy}
199
201
  onkeydown={(e) => onThumbKeydown(e, i)}
200
202
  class="absolute top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 shadow {disabled
201
- ? ''
203
+ ? 'cursor-not-allowed'
202
204
  : 'cursor-grab active:cursor-grabbing'} focus-visible:[outline:2px_solid_color-mix(in_srgb,var(--ui-color-text)_70%,transparent)] focus-visible:[outline-offset:2px] {responsiveClasses(
203
205
  size,
204
206
  thumbSz,
@@ -0,0 +1,127 @@
1
+ <!--
2
+ /**
3
+ * Splitter — two resizable panes (`start`/`end`) divided by a draggable handle. Drag or arrow-key the
4
+ * handle to resize; `bind:size` is the first pane's percentage. Horizontal or vertical. `role=separator`
5
+ * with aria-value*. See Splitter.spec.md.
6
+ */
7
+ -->
8
+ <script lang="ts">
9
+ import type { Snippet } from "svelte";
10
+ import type { Variant } from "../../types/variants.js";
11
+ import { variantToken } from "../../types/variants.js";
12
+
13
+ let {
14
+ start,
15
+ end,
16
+ orientation = "horizontal",
17
+ size = $bindable(50),
18
+ min = 10,
19
+ max = 90,
20
+ step = 2,
21
+ variant = "primary",
22
+ disabled = false,
23
+ class: cls = "",
24
+ }: {
25
+ start: Snippet;
26
+ end: Snippet;
27
+ orientation?: "horizontal" | "vertical";
28
+ /** First pane size, in percent (bindable). */
29
+ size?: number;
30
+ min?: number;
31
+ max?: number;
32
+ step?: number;
33
+ variant?: Variant;
34
+ disabled?: boolean;
35
+ class?: string;
36
+ } = $props();
37
+
38
+ const isH = $derived(orientation === "horizontal");
39
+ const token = $derived(variantToken[variant]);
40
+
41
+ let containerEl = $state<HTMLDivElement>();
42
+ let dragging = $state(false);
43
+ const clamp = (n: number) => Math.min(max, Math.max(min, n));
44
+
45
+ function fromPointer(e: PointerEvent) {
46
+ if (!containerEl) return;
47
+ const r = containerEl.getBoundingClientRect();
48
+ const pct = isH ? ((e.clientX - r.left) / r.width) * 100 : ((e.clientY - r.top) / r.height) * 100;
49
+ size = clamp(pct);
50
+ }
51
+ function onPointerDown(e: PointerEvent) {
52
+ if (disabled) return;
53
+ dragging = true;
54
+ (e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
55
+ e.preventDefault();
56
+ }
57
+ function onPointerMove(e: PointerEvent) {
58
+ if (dragging) fromPointer(e);
59
+ }
60
+ function onPointerUp(e: PointerEvent) {
61
+ dragging = false;
62
+ (e.currentTarget as HTMLElement).releasePointerCapture?.(e.pointerId);
63
+ }
64
+ function onKeydown(e: KeyboardEvent) {
65
+ if (disabled) return;
66
+ const dec = isH ? "ArrowLeft" : "ArrowUp";
67
+ const inc = isH ? "ArrowRight" : "ArrowDown";
68
+ if (e.key === dec) {
69
+ e.preventDefault();
70
+ size = clamp(size - step);
71
+ } else if (e.key === inc) {
72
+ e.preventDefault();
73
+ size = clamp(size + step);
74
+ } else if (e.key === "Home") {
75
+ e.preventDefault();
76
+ size = min;
77
+ } else if (e.key === "End") {
78
+ e.preventDefault();
79
+ size = max;
80
+ }
81
+ }
82
+ </script>
83
+
84
+ <div
85
+ bind:this={containerEl}
86
+ class="flex {isH ? 'flex-row' : 'flex-col'} {cls}"
87
+ style={dragging ? "user-select: none;" : ""}
88
+ >
89
+ <div class="min-w-0 min-h-0 overflow-hidden" style="flex-basis: {size}%;">
90
+ {@render start()}
91
+ </div>
92
+
93
+ <!-- The WAI-ARIA window-splitter pattern: a focusable `separator` with aria-valuenow. -->
94
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex a11y_no_noninteractive_element_interactions -->
95
+ <div
96
+ role="separator"
97
+ aria-orientation={isH ? "vertical" : "horizontal"}
98
+ aria-valuenow={Math.round(size)}
99
+ aria-valuemin={min}
100
+ aria-valuemax={max}
101
+ tabindex={disabled ? -1 : 0}
102
+ onpointerdown={onPointerDown}
103
+ onpointermove={onPointerMove}
104
+ onpointerup={onPointerUp}
105
+ onkeydown={onKeydown}
106
+ class="group relative shrink-0 {isH ? 'w-px cursor-col-resize' : 'h-px cursor-row-resize'} {disabled
107
+ ? 'pointer-events-none'
108
+ : ''} [background-color:var(--ui-border-color)] focus-visible:[outline:2px_solid_color-mix(in_srgb,var(--ui-color-text)_70%,transparent)]"
109
+ >
110
+ <!-- Larger invisible hit-area + a handle bar that accents on hover/drag. -->
111
+ <span
112
+ class="absolute {isH
113
+ ? 'inset-y-0 -inset-x-1.5 cursor-col-resize'
114
+ : 'inset-x-0 -inset-y-1.5 cursor-row-resize'}"
115
+ ></span>
116
+ <span
117
+ class="absolute rounded-full transition-colors {isH
118
+ ? 'inset-y-0 -inset-x-px'
119
+ : 'inset-x-0 -inset-y-px'}"
120
+ style="background-color: {dragging ? `var(${token})` : 'transparent'};"
121
+ ></span>
122
+ </div>
123
+
124
+ <div class="min-w-0 min-h-0 flex-1 overflow-hidden">
125
+ {@render end()}
126
+ </div>
127
+ </div>
@@ -0,0 +1,18 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { Variant } from "../../types/variants.js";
3
+ type $$ComponentProps = {
4
+ start: Snippet;
5
+ end: Snippet;
6
+ orientation?: "horizontal" | "vertical";
7
+ /** First pane size, in percent (bindable). */
8
+ size?: number;
9
+ min?: number;
10
+ max?: number;
11
+ step?: number;
12
+ variant?: Variant;
13
+ disabled?: boolean;
14
+ class?: string;
15
+ };
16
+ declare const Splitter: import("svelte").Component<$$ComponentProps, {}, "size">;
17
+ type Splitter = ReturnType<typeof Splitter>;
18
+ export default Splitter;
@@ -0,0 +1 @@
1
+ export { default as Splitter } from "./Splitter.svelte";
@@ -0,0 +1 @@
1
+ export { default as Splitter } from "./Splitter.svelte";
@@ -0,0 +1,95 @@
1
+ <!--
2
+ /**
3
+ * Stat — a metric/KPI card: label, big value, optional delta (up/down, coloured) + icon. Built on
4
+ * `Card`; shared `size`/`variant` axes, `--ui-*` tokens. See Stat.spec.md.
5
+ */
6
+ -->
7
+ <script lang="ts">
8
+ import type { Snippet } from "svelte";
9
+ import Card from "../Card.svelte";
10
+ import IconUp from "~icons/mdi/trending-up";
11
+ import IconDown from "~icons/mdi/trending-down";
12
+ import type { Variant } from "../../types/variants.js";
13
+ import { variantToken } from "../../types/variants.js";
14
+ import type { Size } from "../../types/sizes.js";
15
+ import { responsiveClasses, type Responsive } from "../../types/responsive.js";
16
+
17
+ let {
18
+ label,
19
+ value,
20
+ delta,
21
+ deltaLabel,
22
+ invertDelta = false,
23
+ icon,
24
+ variant = "secondary",
25
+ size = "md",
26
+ class: cls = "",
27
+ }: {
28
+ label: string;
29
+ value: string | number;
30
+ /** Change vs a baseline (number → arrow + %/value; string → shown as-is). */
31
+ delta?: number | string;
32
+ /** Context after the delta, e.g. "vs last month". */
33
+ deltaLabel?: string;
34
+ /** When true, a NEGATIVE delta is "good" (green) and positive is "bad" (e.g. error rate). */
35
+ invertDelta?: boolean;
36
+ icon?: Snippet;
37
+ variant?: Variant;
38
+ size?: Responsive<Size>;
39
+ class?: string;
40
+ } = $props();
41
+
42
+ const token = $derived(variantToken[variant]);
43
+ const deltaNum = $derived(typeof delta === "number" ? delta : null);
44
+ const up = $derived(deltaNum != null && deltaNum > 0);
45
+ const down = $derived(deltaNum != null && deltaNum < 0);
46
+ // Colour: rising = success unless inverted; falling = danger unless inverted; flat = muted.
47
+ const deltaColor = $derived(
48
+ deltaNum == null || deltaNum === 0
49
+ ? "color-mix(in srgb, var(--ui-color-text) 55%, transparent)"
50
+ : (up ? !invertDelta : invertDelta)
51
+ ? "var(--ui-color-success)"
52
+ : "var(--ui-color-error)",
53
+ );
54
+
55
+ const valueSize: Record<Size, string> = { sm: "text-2xl", md: "text-3xl", lg: "text-4xl" };
56
+ const iconBox: Record<Size, string> = {
57
+ sm: "w-8 h-8 [&_svg]:w-4 [&_svg]:h-4",
58
+ md: "w-10 h-10 [&_svg]:w-5 [&_svg]:h-5",
59
+ lg: "w-12 h-12 [&_svg]:w-6 [&_svg]:h-6",
60
+ };
61
+ </script>
62
+
63
+ <Card class="p-4 {cls}">
64
+ <div class="flex items-start justify-between gap-3">
65
+ <div class="min-w-0">
66
+ <div class="truncate text-sm [color:color-mix(in_srgb,var(--ui-color-text)_60%,transparent)]">
67
+ {label}
68
+ </div>
69
+ <div class="mt-1 font-semibold [color:var(--ui-color-text)] {responsiveClasses(size, valueSize)}">
70
+ {value}
71
+ </div>
72
+ {#if delta != null}
73
+ <div class="mt-1 flex items-center gap-1 text-sm" style="color: {deltaColor};">
74
+ {#if up}<IconUp class="w-4 h-4" />{:else if down}<IconDown class="w-4 h-4" />{/if}
75
+ <span class="font-medium tabular-nums">{delta}</span>
76
+ {#if deltaLabel}<span class="[color:color-mix(in_srgb,var(--ui-color-text)_50%,transparent)]"
77
+ >{deltaLabel}</span
78
+ >{/if}
79
+ </div>
80
+ {/if}
81
+ </div>
82
+ {#if icon}
83
+ <span
84
+ class="inline-flex shrink-0 items-center justify-center rounded-full {responsiveClasses(
85
+ size,
86
+ iconBox,
87
+ )}"
88
+ 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));"
89
+ aria-hidden="true"
90
+ >
91
+ {@render icon()}
92
+ </span>
93
+ {/if}
94
+ </div>
95
+ </Card>
@@ -0,0 +1,21 @@
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
+ label: string;
7
+ value: string | number;
8
+ /** Change vs a baseline (number → arrow + %/value; string → shown as-is). */
9
+ delta?: number | string;
10
+ /** Context after the delta, e.g. "vs last month". */
11
+ deltaLabel?: string;
12
+ /** When true, a NEGATIVE delta is "good" (green) and positive is "bad" (e.g. error rate). */
13
+ invertDelta?: boolean;
14
+ icon?: Snippet;
15
+ variant?: Variant;
16
+ size?: Responsive<Size>;
17
+ class?: string;
18
+ };
19
+ declare const Stat: import("svelte").Component<$$ComponentProps, {}, "">;
20
+ type Stat = ReturnType<typeof Stat>;
21
+ export default Stat;
@@ -0,0 +1 @@
1
+ export { default as Stat } from "./Stat.svelte";