@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
|
@@ -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";
|
|
@@ -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";
|
|
@@ -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";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Stat } from "./Stat.svelte";
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
/**
|
|
3
|
+
* Timeline — a vertical sequence of events: a marker (dot or icon) on a connecting line, with a
|
|
4
|
+
* title / time / description per item. Per-item `variant` colours its marker. `--ui-*` tokens.
|
|
5
|
+
* See Timeline.spec.md.
|
|
6
|
+
*/
|
|
7
|
+
-->
|
|
8
|
+
<script lang="ts" module>
|
|
9
|
+
import type { Snippet } from "svelte";
|
|
10
|
+
import type { Variant } from "../../types/variants.js";
|
|
11
|
+
export type TimelineItem = {
|
|
12
|
+
title: string;
|
|
13
|
+
time?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
icon?: Snippet;
|
|
16
|
+
variant?: Variant;
|
|
17
|
+
content?: Snippet;
|
|
18
|
+
};
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<script lang="ts">
|
|
22
|
+
import { variantToken } from "../../types/variants.js";
|
|
23
|
+
import type { Size } from "../../types/sizes.js";
|
|
24
|
+
import { responsiveClasses, type Responsive } from "../../types/responsive.js";
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
items,
|
|
28
|
+
variant = "primary",
|
|
29
|
+
size = "md",
|
|
30
|
+
class: cls = "",
|
|
31
|
+
}: {
|
|
32
|
+
items: TimelineItem[];
|
|
33
|
+
variant?: Variant;
|
|
34
|
+
size?: Responsive<Size>;
|
|
35
|
+
class?: string;
|
|
36
|
+
} = $props();
|
|
37
|
+
|
|
38
|
+
const dotSize: Record<Size, string> = { sm: "w-2.5 h-2.5", md: "w-3 h-3", lg: "w-3.5 h-3.5" };
|
|
39
|
+
const iconBox: Record<Size, string> = {
|
|
40
|
+
sm: "w-6 h-6 [&_svg]:w-3.5 [&_svg]:h-3.5",
|
|
41
|
+
md: "w-8 h-8 [&_svg]:w-4 [&_svg]:h-4",
|
|
42
|
+
lg: "w-9 h-9 [&_svg]:w-5 [&_svg]:h-5",
|
|
43
|
+
};
|
|
44
|
+
const titleSize: Record<Size, string> = { sm: "text-sm", md: "text-base", lg: "text-lg" };
|
|
45
|
+
const tokenFor = (item: TimelineItem) => variantToken[item.variant ?? variant];
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<ol class="flex flex-col {cls}">
|
|
49
|
+
{#each items as item, i (i)}
|
|
50
|
+
{@const last = i === items.length - 1}
|
|
51
|
+
<li class="flex gap-3">
|
|
52
|
+
<!-- Marker + connecting line. -->
|
|
53
|
+
<div class="flex flex-col items-center">
|
|
54
|
+
{#if item.icon}
|
|
55
|
+
<span
|
|
56
|
+
class="inline-flex shrink-0 items-center justify-center rounded-full {responsiveClasses(
|
|
57
|
+
size,
|
|
58
|
+
iconBox,
|
|
59
|
+
)}"
|
|
60
|
+
style="background-color: color-mix(in srgb, var({tokenFor(item)}) 16%, transparent); color: color-mix(in srgb, var({tokenFor(item)}) var(--ui-tint-strong), var(--ui-color-text));"
|
|
61
|
+
aria-hidden="true"
|
|
62
|
+
>
|
|
63
|
+
{@render item.icon()}
|
|
64
|
+
</span>
|
|
65
|
+
{:else}
|
|
66
|
+
<span
|
|
67
|
+
class="mt-1.5 shrink-0 rounded-full {responsiveClasses(size, dotSize)}"
|
|
68
|
+
style="background-color: var({tokenFor(item)});"
|
|
69
|
+
aria-hidden="true"
|
|
70
|
+
></span>
|
|
71
|
+
{/if}
|
|
72
|
+
{#if !last}
|
|
73
|
+
<span class="my-1 w-px flex-1 [background-color:var(--ui-border-color)]" aria-hidden="true"></span>
|
|
74
|
+
{/if}
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<!-- Content. -->
|
|
78
|
+
<div class="min-w-0 flex-1 pb-6">
|
|
79
|
+
<div class="flex flex-wrap items-baseline gap-x-2">
|
|
80
|
+
<span class="font-medium [color:var(--ui-color-text)] {responsiveClasses(size, titleSize)}">
|
|
81
|
+
{item.title}
|
|
82
|
+
</span>
|
|
83
|
+
{#if item.time}
|
|
84
|
+
<span class="text-xs [color:color-mix(in_srgb,var(--ui-color-text)_50%,transparent)]">
|
|
85
|
+
{item.time}
|
|
86
|
+
</span>
|
|
87
|
+
{/if}
|
|
88
|
+
</div>
|
|
89
|
+
{#if item.description}
|
|
90
|
+
<p class="mt-0.5 text-sm [color:color-mix(in_srgb,var(--ui-color-text)_65%,transparent)]">
|
|
91
|
+
{item.description}
|
|
92
|
+
</p>
|
|
93
|
+
{/if}
|
|
94
|
+
{#if item.content}<div class="mt-2">{@render item.content()}</div>{/if}
|
|
95
|
+
</div>
|
|
96
|
+
</li>
|
|
97
|
+
{/each}
|
|
98
|
+
</ol>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { Variant } from "../../types/variants.js";
|
|
3
|
+
export type TimelineItem = {
|
|
4
|
+
title: string;
|
|
5
|
+
time?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
icon?: Snippet;
|
|
8
|
+
variant?: Variant;
|
|
9
|
+
content?: Snippet;
|
|
10
|
+
};
|
|
11
|
+
import type { Size } from "../../types/sizes.js";
|
|
12
|
+
import { type Responsive } from "../../types/responsive.js";
|
|
13
|
+
type $$ComponentProps = {
|
|
14
|
+
items: TimelineItem[];
|
|
15
|
+
variant?: Variant;
|
|
16
|
+
size?: Responsive<Size>;
|
|
17
|
+
class?: string;
|
|
18
|
+
};
|
|
19
|
+
declare const Timeline: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
20
|
+
type Timeline = ReturnType<typeof Timeline>;
|
|
21
|
+
export default Timeline;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Timeline } from "./Timeline.svelte";
|