@runwisp/ui 1.4.0 → 1.5.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/package.json +4 -5
- package/src/lib/actions/floating.ts +73 -0
- package/src/lib/actions/portal.ts +5 -1
- package/src/lib/components/Accordion.svelte +6 -2
- package/src/lib/components/AccordionItem.svelte +80 -23
- package/src/lib/components/Avatar.svelte +0 -1
- package/src/lib/components/CodeBlock.svelte +84 -35
- package/src/lib/components/CronInput.svelte +5 -2
- package/src/lib/components/DataGrid.svelte +95 -133
- package/src/lib/components/Drawer.svelte +2 -6
- package/src/lib/components/Dropdown.svelte +19 -57
- package/src/lib/components/EmptyState.svelte +2 -1
- package/src/lib/components/ErrorState.svelte +2 -1
- package/src/lib/components/Heading.svelte +19 -12
- package/src/lib/components/Kbd.svelte +25 -10
- package/src/lib/components/Link.svelte +7 -1
- package/src/lib/components/LogConsole.svelte +30 -32
- package/src/lib/components/Modal.svelte +44 -33
- package/src/lib/components/NotificationBell.svelte +2 -1
- package/src/lib/components/PageContainer.svelte +1 -10
- package/src/lib/components/PageHeader.svelte +8 -4
- package/src/lib/components/Popover.svelte +9 -51
- package/src/lib/components/Progress.svelte +19 -6
- package/src/lib/components/Radio.svelte +16 -4
- package/src/lib/components/RadioGroup.svelte +15 -2
- package/src/lib/components/Select.svelte +13 -45
- package/src/lib/components/Table.svelte +13 -4
- package/src/lib/components/TableBody.svelte +2 -1
- package/src/lib/components/TableCell.svelte +38 -8
- package/src/lib/components/TableHead.svelte +12 -4
- package/src/lib/components/TableRow.svelte +23 -3
- package/src/lib/components/TaskCard.svelte +1 -1
- package/src/lib/components/TimezoneSelect.svelte +2 -2
- package/src/lib/components/accordion.svelte.ts +19 -0
- package/src/lib/components/dashboard/RunDetailPanel.svelte +91 -63
- package/src/lib/components/dashboard/RunFilterPopover.svelte +7 -16
- package/src/lib/components/dashboard/RunsList.svelte +9 -18
- package/src/lib/components/dashboard/cron-format.ts +1 -1
- package/src/lib/components/dashboard/run-filters.test.ts +2 -58
- package/src/lib/components/dashboard/run-filters.ts +5 -53
- package/src/lib/components/dashboard/run-helpers.test.ts +17 -7
- package/src/lib/components/dashboard/run-helpers.ts +12 -1
- package/src/lib/components/dashboard/status-config.ts +38 -83
- package/src/lib/components/dashboard/types.ts +0 -26
- package/src/lib/components/radio-group.ts +11 -0
- package/src/lib/index.ts +8 -43
- package/src/lib/log-console/LogFetcher.svelte.ts +3 -14
- package/src/lib/log-console/highlight-scroll.test.ts +59 -0
- package/src/lib/log-console/highlight-scroll.ts +41 -0
- package/src/lib/theme.css +10 -4
- package/src/lib/utils/clipboard.svelte.test.ts +92 -0
- package/src/lib/utils/clipboard.svelte.ts +73 -0
- package/src/lib/utils/format.ts +65 -94
- package/src/lib/utils/id.test.ts +3 -7
- package/src/lib/utils/id.ts +3 -2
- package/src/lib/utils/theme.svelte.test.ts +0 -6
- package/src/lib/utils/theme.svelte.ts +3 -5
- package/src/lib/utils/toast.svelte.ts +3 -11
- package/src/stories/Accordion.stories.svelte +61 -0
- package/src/stories/CodeBlock.stories.svelte +55 -0
- package/src/stories/DataGrid.stories.svelte +0 -5
- package/src/stories/Heading.stories.svelte +31 -0
- package/src/stories/Kbd.stories.svelte +36 -0
- package/src/stories/Link.stories.svelte +38 -0
- package/src/stories/PageHeader.stories.svelte +31 -0
- package/src/stories/Progress.stories.svelte +26 -0
- package/src/stories/RadioGroup.stories.svelte +43 -0
- package/src/stories/Table.stories.svelte +69 -0
- package/src/svelte-shims.d.ts +0 -5
- package/src/lib/components/dashboard/status-config.test.ts +0 -18
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
<script lang="ts">
|
|
5
5
|
import type { Snippet } from "svelte";
|
|
6
|
+
import type { HTMLAnchorAttributes } from "svelte/elements";
|
|
6
7
|
|
|
7
8
|
type LinkVariant = "default" | "muted" | "primary" | "danger";
|
|
8
9
|
type LinkUnderline = "always" | "hover" | "none";
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
// Use Link for off-site URLs (`external` opens a new tab with rel=noopener
|
|
12
|
+
// noreferrer). An in-app href must still come from SvelteKit's resolve():
|
|
13
|
+
// being a component, Link sits outside svelte/no-navigation-without-resolve.
|
|
14
|
+
interface Props extends Omit<HTMLAnchorAttributes, "href" | "children" | "class"> {
|
|
11
15
|
href: string;
|
|
12
16
|
variant?: LinkVariant;
|
|
13
17
|
external?: boolean;
|
|
@@ -23,6 +27,7 @@
|
|
|
23
27
|
underline = "hover",
|
|
24
28
|
children,
|
|
25
29
|
class: className = "",
|
|
30
|
+
...restProps
|
|
26
31
|
}: Props = $props();
|
|
27
32
|
|
|
28
33
|
const baseClasses = `
|
|
@@ -50,6 +55,7 @@
|
|
|
50
55
|
class="{baseClasses} {variantClasses[variant]} {underlineClasses[underline]} {className}"
|
|
51
56
|
target={external ? "_blank" : undefined}
|
|
52
57
|
rel={external ? "noopener noreferrer" : undefined}
|
|
58
|
+
{...restProps}
|
|
53
59
|
>
|
|
54
60
|
{@render children()}
|
|
55
61
|
</a>
|
|
@@ -6,13 +6,12 @@
|
|
|
6
6
|
import { ansiLineToHtml, visibleColumns } from "../log-console/ansi.js";
|
|
7
7
|
import { LogCache } from "../log-console/LogCache.svelte.js";
|
|
8
8
|
import { LogFetcher } from "../log-console/LogFetcher.svelte.js";
|
|
9
|
+
import { createHighlightScroll } from "../log-console/highlight-scroll.js";
|
|
9
10
|
import type { FetchLogsFn, LogEvent } from "../log-console/types.js";
|
|
10
11
|
import { formatBytes } from "../utils/format.js";
|
|
11
12
|
|
|
12
13
|
interface Props {
|
|
13
14
|
fetchLogs?: FetchLogsFn;
|
|
14
|
-
chunkSize?: number;
|
|
15
|
-
lineHeight?: number;
|
|
16
15
|
class?: string;
|
|
17
16
|
// highlightLine, when set, scrolls the console so the given absolute
|
|
18
17
|
// line number is visible and pulses a short-lived flash on that row.
|
|
@@ -51,8 +50,6 @@
|
|
|
51
50
|
|
|
52
51
|
let {
|
|
53
52
|
fetchLogs,
|
|
54
|
-
chunkSize = 4096,
|
|
55
|
-
lineHeight = 20,
|
|
56
53
|
class: className = "",
|
|
57
54
|
highlightLine = null,
|
|
58
55
|
fetchLineHistory,
|
|
@@ -63,6 +60,8 @@
|
|
|
63
60
|
wrap = $bindable(false),
|
|
64
61
|
}: Props = $props();
|
|
65
62
|
|
|
63
|
+
const lineHeight = 20; // px per rendered row
|
|
64
|
+
|
|
66
65
|
// --- Frame-history inline expansion (single expansion at a time) ---
|
|
67
66
|
// expandedLine is the absolute line number whose history block is open;
|
|
68
67
|
// expandedFrames holds the fetched whole-region frames (null while loading).
|
|
@@ -271,7 +270,7 @@
|
|
|
271
270
|
const i = lineNum - cache.firstAvailableLine;
|
|
272
271
|
if (i <= 0) return base;
|
|
273
272
|
if (i >= prefixSums.length) {
|
|
274
|
-
const last = prefixSums.
|
|
273
|
+
const last = prefixSums.at(-1) ?? 0;
|
|
275
274
|
return last * lineHeight + base;
|
|
276
275
|
}
|
|
277
276
|
return (prefixSums[i] ?? 0) * lineHeight + base;
|
|
@@ -345,11 +344,8 @@
|
|
|
345
344
|
let overlayRows = $derived(cache.overlayRows);
|
|
346
345
|
|
|
347
346
|
let totalHeight = $derived.by(() => {
|
|
348
|
-
const truncationBannerHeight = cache.firstAvailableLine > 0 ? lineHeight : 0;
|
|
349
347
|
const linesHeight = wrap
|
|
350
|
-
? (prefixSums.
|
|
351
|
-
? (prefixSums[prefixSums.length - 1] ?? 0)
|
|
352
|
-
: Math.max(0, cache.totalLines - cache.firstAvailableLine)) *
|
|
348
|
+
? (prefixSums.at(-1) ?? Math.max(0, cache.totalLines - cache.firstAvailableLine)) *
|
|
353
349
|
lineHeight +
|
|
354
350
|
truncationBannerHeight
|
|
355
351
|
: (cache.totalLines - cache.firstAvailableLine) * lineHeight + truncationBannerHeight;
|
|
@@ -410,8 +406,7 @@
|
|
|
410
406
|
// viewport lands at the end of the log without racing the auto-scroll.
|
|
411
407
|
$effect(() => {
|
|
412
408
|
const fn = fetchLogs;
|
|
413
|
-
|
|
414
|
-
fetcher = new LogFetcher(cache, fn, cs, (min, max) => {
|
|
409
|
+
fetcher = new LogFetcher(cache, fn, (min, max) => {
|
|
415
410
|
measureRange(min, max);
|
|
416
411
|
if (isAutoScroll && !userScrolledUp) {
|
|
417
412
|
requestAnimationFrame(() => scrollToBottom());
|
|
@@ -538,29 +533,32 @@
|
|
|
538
533
|
scrollTop = 0;
|
|
539
534
|
}
|
|
540
535
|
|
|
541
|
-
// Search-hit deep-link:
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
return;
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
536
|
+
// Search-hit deep-link: scroll the viewport so the highlighted line is
|
|
537
|
+
// centred, once per highlightLine, and pulse a one-shot flash class for
|
|
538
|
+
// ~1.5s. createHighlightScroll keeps later layout changes (new lines on a
|
|
539
|
+
// live run, resizes) from pulling the view back to the hit.
|
|
540
|
+
const syncHighlightScroll = createHighlightScroll({
|
|
541
|
+
line: () => highlightLine,
|
|
542
|
+
ready: (line) => containerEl !== null && containerHeight > 0 && line < cache.totalLines,
|
|
543
|
+
reveal: (line) => {
|
|
544
|
+
if (!containerEl) return;
|
|
545
|
+
const targetY = lineTop(line) - containerHeight / 2 + lineHeight / 2;
|
|
546
|
+
const clamped = Math.max(0, Math.min(targetY, totalHeight - containerHeight));
|
|
547
|
+
containerEl.scrollTo({ top: clamped, behavior: "smooth" });
|
|
548
|
+
isAutoScroll = false;
|
|
549
|
+
userScrolledUp = true;
|
|
550
|
+
flashLine = line;
|
|
551
|
+
if (flashTimer !== null) clearTimeout(flashTimer);
|
|
552
|
+
flashTimer = setTimeout(() => {
|
|
553
|
+
flashLine = null;
|
|
554
|
+
flashTimer = null;
|
|
555
|
+
}, 1500);
|
|
556
|
+
},
|
|
557
|
+
clear: () => {
|
|
560
558
|
flashLine = null;
|
|
561
|
-
|
|
562
|
-
}, 1500);
|
|
559
|
+
},
|
|
563
560
|
});
|
|
561
|
+
$effect(syncHighlightScroll);
|
|
564
562
|
|
|
565
563
|
$effect(() => {
|
|
566
564
|
if (!containerEl) return;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<script lang="ts">
|
|
5
5
|
import type { Snippet } from "svelte";
|
|
6
6
|
import { X } from "@lucide/svelte";
|
|
7
|
+
import Heading from "./Heading.svelte";
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
9
10
|
open?: boolean;
|
|
@@ -20,7 +21,6 @@
|
|
|
20
21
|
class?: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
import { portal } from "../actions/portal.js";
|
|
24
24
|
import { trapFocus } from "../actions/focusTrap.js";
|
|
25
25
|
|
|
26
26
|
let {
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
class: className = "",
|
|
37
37
|
}: Props = $props();
|
|
38
38
|
|
|
39
|
+
const titleId = $props.id();
|
|
40
|
+
|
|
39
41
|
const sizeClasses: Record<string, string> = {
|
|
40
42
|
sm: "max-w-sm",
|
|
41
43
|
md: "max-w-md",
|
|
@@ -49,35 +51,52 @@
|
|
|
49
51
|
onClose?.();
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
// showModal() puts the dialog in the top layer and makes the page behind it
|
|
55
|
+
// inert. The dialog stays open while mounted; `open` drives (un)mounting.
|
|
56
|
+
function showModal(node: HTMLDialogElement) {
|
|
57
|
+
node.showModal();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function handleCancel(e: Event) {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
if (closable) handleClose();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Chrome ignores preventDefault() on a repeated Escape and closes anyway;
|
|
66
|
+
// reopen so a non-closable modal stays up while mounted.
|
|
67
|
+
function handleNativeClose(e: Event & { currentTarget: HTMLDialogElement }) {
|
|
68
|
+
if (open) e.currentTarget.showModal();
|
|
56
69
|
}
|
|
57
|
-
</script>
|
|
58
70
|
|
|
59
|
-
|
|
71
|
+
// Only a press that starts and ends on the backdrop closes the modal, so a
|
|
72
|
+
// text selection dragged out of the panel doesn't.
|
|
73
|
+
let pressedBackdrop = false;
|
|
74
|
+
function handlePointerDown(e: PointerEvent) {
|
|
75
|
+
pressedBackdrop = e.target === e.currentTarget;
|
|
76
|
+
}
|
|
77
|
+
function handleBackdropClick(e: MouseEvent) {
|
|
78
|
+
if (closable && pressedBackdrop && e.target === e.currentTarget) handleClose();
|
|
79
|
+
}
|
|
80
|
+
</script>
|
|
60
81
|
|
|
61
82
|
{#if open}
|
|
62
|
-
<
|
|
63
|
-
use:
|
|
83
|
+
<dialog
|
|
84
|
+
use:showModal
|
|
85
|
+
oncancel={handleCancel}
|
|
86
|
+
onclose={handleNativeClose}
|
|
87
|
+
onpointerdown={handlePointerDown}
|
|
88
|
+
onclick={handleBackdropClick}
|
|
89
|
+
aria-labelledby={title ? titleId : undefined}
|
|
64
90
|
class="
|
|
65
|
-
fixed inset-0
|
|
66
|
-
|
|
67
|
-
|
|
91
|
+
fixed inset-0 m-0 flex h-full max-h-none w-full max-w-none
|
|
92
|
+
items-center justify-center border-0 bg-transparent p-4
|
|
93
|
+
text-inherit backdrop:bg-transparent
|
|
68
94
|
"
|
|
69
95
|
>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
aria-label="Close modal"
|
|
75
|
-
tabindex="-1"
|
|
76
|
-
onclick={handleClose}
|
|
77
|
-
></button>
|
|
78
|
-
{:else}
|
|
79
|
-
<div class="absolute inset-0 z-0 bg-backdrop backdrop-blur-sm"></div>
|
|
80
|
-
{/if}
|
|
96
|
+
<div
|
|
97
|
+
class="pointer-events-none absolute inset-0 z-0 bg-backdrop backdrop-blur-sm"
|
|
98
|
+
aria-hidden="true"
|
|
99
|
+
></div>
|
|
81
100
|
|
|
82
101
|
<div
|
|
83
102
|
use:trapFocus
|
|
@@ -87,9 +106,6 @@
|
|
|
87
106
|
rounded-[4px] border border-outline bg-surface-overlay shadow-lg
|
|
88
107
|
{className}
|
|
89
108
|
"
|
|
90
|
-
role="dialog"
|
|
91
|
-
aria-modal="true"
|
|
92
|
-
aria-labelledby={title ? "modal-title" : undefined}
|
|
93
109
|
>
|
|
94
110
|
{#if header}
|
|
95
111
|
<div class="border-b border-outline px-6 py-4">
|
|
@@ -101,12 +117,7 @@
|
|
|
101
117
|
>
|
|
102
118
|
<div>
|
|
103
119
|
{#if title}
|
|
104
|
-
<
|
|
105
|
-
id="modal-title"
|
|
106
|
-
class="font-mono text-lg font-semibold text-on-surface"
|
|
107
|
-
>
|
|
108
|
-
{title}
|
|
109
|
-
</h2>
|
|
120
|
+
<Heading level={2} size="lg" id={titleId}>{title}</Heading>
|
|
110
121
|
{/if}
|
|
111
122
|
{#if description}
|
|
112
123
|
<p class="mt-1 text-sm text-on-surface-muted">{description}</p>
|
|
@@ -136,5 +147,5 @@
|
|
|
136
147
|
</div>
|
|
137
148
|
{/if}
|
|
138
149
|
</div>
|
|
139
|
-
</
|
|
150
|
+
</dialog>
|
|
140
151
|
{/if}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
<script lang="ts">
|
|
10
10
|
import type { Snippet } from "svelte";
|
|
11
11
|
import { Bell } from "@lucide/svelte";
|
|
12
|
+
import Heading from "./Heading.svelte";
|
|
12
13
|
import Popover from "./Popover.svelte";
|
|
13
14
|
|
|
14
15
|
interface Props {
|
|
@@ -60,7 +61,7 @@
|
|
|
60
61
|
|
|
61
62
|
<div role="dialog" aria-label={title} class="w-96 max-w-[90vw]">
|
|
62
63
|
<div class="mb-2 flex items-center justify-between gap-2">
|
|
63
|
-
<
|
|
64
|
+
<Heading level={2} size="sm">{title}</Heading>
|
|
64
65
|
{#if actions}
|
|
65
66
|
<div class="flex items-center gap-3 text-xs">{@render actions()}</div>
|
|
66
67
|
{/if}
|
|
@@ -4,16 +4,7 @@
|
|
|
4
4
|
<script lang="ts">
|
|
5
5
|
import type { Snippet } from "svelte";
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* PageContainer — the single, standardized page-level wrapper.
|
|
9
|
-
*
|
|
10
|
-
* Variants:
|
|
11
|
-
* "standard" – Max-width + centering + padding + flex-col + min-h-full.
|
|
12
|
-
* "full" – Same as standard but h-full (fills viewport).
|
|
13
|
-
* "wide" – Max-width + centering only (consumer provides padding).
|
|
14
|
-
* "narrow" – Narrower max-width + centering.
|
|
15
|
-
* "flush" – Fills container height, no max-width.
|
|
16
|
-
*/
|
|
7
|
+
/** The standard page-level wrapper. See variantClasses for each variant's layout. */
|
|
17
8
|
let {
|
|
18
9
|
children,
|
|
19
10
|
variant = "standard",
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
|
|
4
4
|
<script lang="ts">
|
|
5
5
|
import type { Snippet } from "svelte";
|
|
6
|
+
import Heading from "./Heading.svelte";
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
title: string;
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
/** Plain text, or a snippet when the line carries tokens (counts, names). */
|
|
11
|
+
subtitle?: string | Snippet;
|
|
12
|
+
actions?: Snippet | undefined;
|
|
11
13
|
class?: string;
|
|
12
14
|
}
|
|
13
15
|
|
|
@@ -16,8 +18,10 @@
|
|
|
16
18
|
|
|
17
19
|
<div class="flex shrink-0 flex-wrap items-start justify-between gap-3 px-1 {className}">
|
|
18
20
|
<div class="min-w-0">
|
|
19
|
-
<
|
|
20
|
-
{#if subtitle}
|
|
21
|
+
<Heading level={1} size="2xl">{title}</Heading>
|
|
22
|
+
{#if typeof subtitle === "function"}
|
|
23
|
+
<p class="mt-0.5 text-sm text-on-surface-muted">{@render subtitle()}</p>
|
|
24
|
+
{:else if subtitle}
|
|
21
25
|
<p class="mt-0.5 text-sm text-on-surface-muted">{subtitle}</p>
|
|
22
26
|
{/if}
|
|
23
27
|
</div>
|
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
<script lang="ts">
|
|
5
5
|
import type { Snippet } from "svelte";
|
|
6
6
|
import type { Placement } from "@floating-ui/dom";
|
|
7
|
-
import { onDestroy } from "svelte";
|
|
8
|
-
import { computePosition, autoUpdate, flip, shift, offset } from "@floating-ui/dom";
|
|
9
7
|
import { fade } from "svelte/transition";
|
|
8
|
+
import { floating, isOutsideClick } from "../actions/floating.js";
|
|
10
9
|
import { portal } from "../actions/portal.js";
|
|
11
10
|
import { trapFocus } from "../actions/focusTrap.js";
|
|
12
11
|
|
|
@@ -33,7 +32,6 @@
|
|
|
33
32
|
|
|
34
33
|
let triggerEl = $state<HTMLElement | null>(null);
|
|
35
34
|
let contentEl = $state<HTMLElement | null>(null);
|
|
36
|
-
let cleanupFloating = $state<(() => void) | null>(null);
|
|
37
35
|
|
|
38
36
|
// Track the `md` breakpoint only when sheet mode is requested, so the
|
|
39
37
|
// content can switch between floating (desktop) and bottom sheet (phone)
|
|
@@ -50,64 +48,17 @@
|
|
|
50
48
|
|
|
51
49
|
const asSheet = $derived(mobileSheet && !isDesktop);
|
|
52
50
|
|
|
53
|
-
function teardown() {
|
|
54
|
-
cleanupFloating?.();
|
|
55
|
-
cleanupFloating = null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function setupFloating() {
|
|
59
|
-
if (!triggerEl || !contentEl) return;
|
|
60
|
-
cleanupFloating = autoUpdate(triggerEl, contentEl, () => {
|
|
61
|
-
if (!triggerEl || !contentEl) return;
|
|
62
|
-
void computePosition(triggerEl, contentEl, {
|
|
63
|
-
placement,
|
|
64
|
-
middleware: [offset(8), flip(), shift({ padding: 8 })],
|
|
65
|
-
}).then(({ x, y }) => {
|
|
66
|
-
if (contentEl) {
|
|
67
|
-
Object.assign(contentEl.style, {
|
|
68
|
-
left: `${String(x)}px`,
|
|
69
|
-
top: `${String(y)}px`,
|
|
70
|
-
position: "absolute",
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Position the content while open: float it on desktop, or pin it as a
|
|
78
|
-
// sheet on phone (clearing any inline coords floating-ui left behind).
|
|
79
|
-
$effect(() => {
|
|
80
|
-
if (!open || !contentEl) {
|
|
81
|
-
teardown();
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
if (asSheet) {
|
|
85
|
-
teardown();
|
|
86
|
-
contentEl.style.removeProperty("left");
|
|
87
|
-
contentEl.style.removeProperty("top");
|
|
88
|
-
contentEl.style.removeProperty("position");
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
setupFloating();
|
|
92
|
-
return teardown;
|
|
93
|
-
});
|
|
94
|
-
|
|
95
51
|
function toggle() {
|
|
96
52
|
open = !open;
|
|
97
53
|
}
|
|
98
54
|
|
|
99
55
|
function handleOutsideClick(e: MouseEvent) {
|
|
100
|
-
|
|
101
|
-
if (triggerEl && path.includes(triggerEl)) return;
|
|
102
|
-
if (contentEl && path.includes(contentEl)) return;
|
|
103
|
-
if (open) open = false;
|
|
56
|
+
if (open && isOutsideClick(e, triggerEl, contentEl)) open = false;
|
|
104
57
|
}
|
|
105
58
|
|
|
106
59
|
function handleKeyDown(e: KeyboardEvent) {
|
|
107
60
|
if (e.key === "Escape" && open) open = false;
|
|
108
61
|
}
|
|
109
|
-
|
|
110
|
-
onDestroy(teardown);
|
|
111
62
|
</script>
|
|
112
63
|
|
|
113
64
|
<svelte:window onclick={handleOutsideClick} onkeydown={handleKeyDown} />
|
|
@@ -146,6 +97,13 @@
|
|
|
146
97
|
<div
|
|
147
98
|
use:portal
|
|
148
99
|
use:trapFocus
|
|
100
|
+
use:floating={{
|
|
101
|
+
reference: triggerEl,
|
|
102
|
+
placement,
|
|
103
|
+
offset: 8,
|
|
104
|
+
padding: 8,
|
|
105
|
+
disabled: asSheet,
|
|
106
|
+
}}
|
|
149
107
|
bind:this={contentEl}
|
|
150
108
|
class="
|
|
151
109
|
z-[9999] max-h-[80vh] overflow-y-auto
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
size?: ProgressSize;
|
|
13
13
|
showLabel?: boolean;
|
|
14
14
|
label?: string;
|
|
15
|
+
/** Percent at which the bar turns warning / danger, overriding `variant`
|
|
16
|
+
* (e.g. `{ warning: 70, danger: 90 }` for CPU or memory). */
|
|
17
|
+
thresholds?: { warning?: number; danger?: number };
|
|
15
18
|
class?: string;
|
|
16
19
|
}
|
|
17
20
|
|
|
@@ -22,11 +25,18 @@
|
|
|
22
25
|
size = "md",
|
|
23
26
|
showLabel = false,
|
|
24
27
|
label,
|
|
28
|
+
thresholds,
|
|
25
29
|
class: className = "",
|
|
26
30
|
}: Props = $props();
|
|
27
31
|
|
|
28
32
|
const percentage = $derived(Math.min(Math.max((value / max) * 100, 0), 100));
|
|
29
33
|
|
|
34
|
+
const tone = $derived.by((): ProgressVariant => {
|
|
35
|
+
if (thresholds?.danger !== undefined && percentage >= thresholds.danger) return "danger";
|
|
36
|
+
if (thresholds?.warning !== undefined && percentage >= thresholds.warning) return "warning";
|
|
37
|
+
return variant;
|
|
38
|
+
});
|
|
39
|
+
|
|
30
40
|
const variantClasses: Record<ProgressVariant, string> = {
|
|
31
41
|
default: "bg-primary",
|
|
32
42
|
success: "bg-success-surface",
|
|
@@ -55,16 +65,19 @@
|
|
|
55
65
|
</div>
|
|
56
66
|
{/if}
|
|
57
67
|
|
|
58
|
-
<div
|
|
68
|
+
<div
|
|
69
|
+
class="w-full {sizeClasses[size]} overflow-hidden rounded-full bg-surface-sunken"
|
|
70
|
+
role="progressbar"
|
|
71
|
+
aria-label={label}
|
|
72
|
+
aria-valuenow={value}
|
|
73
|
+
aria-valuemin={0}
|
|
74
|
+
aria-valuemax={max}
|
|
75
|
+
>
|
|
59
76
|
<div
|
|
60
77
|
class="{sizeClasses[size]} {variantClasses[
|
|
61
|
-
|
|
78
|
+
tone
|
|
62
79
|
]} rounded-full transition-[width] duration-200"
|
|
63
80
|
style="width: {percentage}%"
|
|
64
|
-
role="progressbar"
|
|
65
|
-
aria-valuenow={value}
|
|
66
|
-
aria-valuemin={0}
|
|
67
|
-
aria-valuemax={max}
|
|
68
81
|
></div>
|
|
69
82
|
</div>
|
|
70
83
|
</div>
|
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
3
|
|
|
4
4
|
<script lang="ts">
|
|
5
|
+
import { getContext } from "svelte";
|
|
6
|
+
import { RADIO_GROUP_KEY, type RadioGroupContext } from "./radio-group.js";
|
|
7
|
+
|
|
5
8
|
type RadioSize = "sm" | "md" | "lg";
|
|
6
9
|
|
|
7
10
|
interface Props {
|
|
8
11
|
value: string;
|
|
12
|
+
/** Selected value when used without a RadioGroup; inside one, the group owns it. */
|
|
9
13
|
groupValue?: string;
|
|
10
|
-
name
|
|
14
|
+
/** Defaults to the enclosing RadioGroup's name. */
|
|
15
|
+
name?: string;
|
|
11
16
|
label: string;
|
|
12
17
|
description?: string;
|
|
13
18
|
disabled?: boolean;
|
|
@@ -32,7 +37,14 @@
|
|
|
32
37
|
lg: { radio: "h-6 w-6", dot: "h-2.5 w-2.5", text: "text-base" },
|
|
33
38
|
};
|
|
34
39
|
|
|
35
|
-
const
|
|
40
|
+
const group = getContext<RadioGroupContext | undefined>(RADIO_GROUP_KEY);
|
|
41
|
+
|
|
42
|
+
const isChecked = $derived((group ? group.value : groupValue) === value);
|
|
43
|
+
|
|
44
|
+
function select() {
|
|
45
|
+
if (group) group.value = value;
|
|
46
|
+
else groupValue = value;
|
|
47
|
+
}
|
|
36
48
|
</script>
|
|
37
49
|
|
|
38
50
|
<label
|
|
@@ -43,11 +55,11 @@
|
|
|
43
55
|
<div class="relative flex shrink-0 items-center justify-center">
|
|
44
56
|
<input
|
|
45
57
|
type="radio"
|
|
46
|
-
{name}
|
|
58
|
+
name={name ?? group?.name}
|
|
47
59
|
{value}
|
|
48
60
|
{disabled}
|
|
49
61
|
checked={isChecked}
|
|
50
|
-
onchange={
|
|
62
|
+
onchange={select}
|
|
51
63
|
class="
|
|
52
64
|
peer {sizeClasses[size].radio}
|
|
53
65
|
cursor-pointer appearance-none rounded-full
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
3
|
|
|
4
4
|
<script lang="ts">
|
|
5
|
-
import type
|
|
5
|
+
import { setContext, type Snippet } from "svelte";
|
|
6
|
+
import { RADIO_GROUP_KEY, type RadioGroupContext } from "./radio-group.js";
|
|
6
7
|
|
|
7
8
|
type Orientation = "horizontal" | "vertical";
|
|
8
9
|
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
|
|
20
21
|
let {
|
|
21
22
|
value = $bindable(""),
|
|
22
|
-
name
|
|
23
|
+
name,
|
|
23
24
|
label,
|
|
24
25
|
error,
|
|
25
26
|
hint,
|
|
@@ -28,6 +29,18 @@
|
|
|
28
29
|
class: className = "",
|
|
29
30
|
}: Props = $props();
|
|
30
31
|
|
|
32
|
+
setContext<RadioGroupContext>(RADIO_GROUP_KEY, {
|
|
33
|
+
get name() {
|
|
34
|
+
return name;
|
|
35
|
+
},
|
|
36
|
+
get value() {
|
|
37
|
+
return value;
|
|
38
|
+
},
|
|
39
|
+
set value(next: string) {
|
|
40
|
+
value = next;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
31
44
|
const orientationClasses: Record<Orientation, string> = {
|
|
32
45
|
horizontal: "flex flex-row flex-wrap gap-x-6 gap-y-3",
|
|
33
46
|
vertical: "flex flex-col gap-3",
|