@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.
- package/.claude/skills/svelte-ui/SKILL.md +14 -11
- 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/Combobox/Combobox.svelte +62 -31
- package/dist/components/Combobox/Combobox.svelte.d.ts +1 -1
- 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/Pagination/Pagination.svelte +58 -43
- package/dist/components/Pagination/Pagination.svelte.d.ts +7 -2
- package/dist/components/Rating.svelte +169 -30
- package/dist/components/Rating.svelte.d.ts +22 -4
- 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/Slider/Slider.svelte +5 -3
- 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/TokenInput/TokenInput.svelte +12 -6
- 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 -2
- package/dist/index.js +10 -1
- package/docs/components.md +125 -22
- package/docs/usage.md +180 -13
- 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
|
-
|
|
7
|
-
|
|
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 {
|
|
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
|
-
|
|
20
|
-
const
|
|
61
|
+
const active = $derived(interactive && !readonly && !disabled);
|
|
62
|
+
const step = $derived(allowHalf ? 0.5 : 1);
|
|
21
63
|
|
|
22
|
-
//
|
|
23
|
-
const
|
|
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
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
{
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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.
|
|
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
|
-
|
|
5
|
-
|
|
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
|
|
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";
|