@runwisp/ui 1.4.0 → 1.5.1
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/focusTrap.ts +3 -3
- 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/Badge.svelte +1 -1
- package/src/lib/components/CodeBlock.svelte +84 -35
- package/src/lib/components/CronInput.svelte +5 -2
- package/src/lib/components/DataGrid.svelte +100 -138
- 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/FilterBar.svelte +1 -1
- package/src/lib/components/FilterPill.svelte +2 -2
- package/src/lib/components/FormField.svelte +1 -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 +40 -42
- 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/SearchInput.svelte +1 -1
- package/src/lib/components/Select.svelte +16 -47
- package/src/lib/components/Sparkline.svelte +2 -2
- package/src/lib/components/StatusDot.svelte +1 -1
- 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/Tooltip.svelte +1 -1
- package/src/lib/components/accordion.svelte.ts +19 -0
- package/src/lib/components/dashboard/RunDetailPanel.svelte +106 -79
- package/src/lib/components/dashboard/RunFilterPopover.svelte +12 -21
- package/src/lib/components/dashboard/RunsList.svelte +17 -26
- package/src/lib/components/dashboard/cron-format.test.ts +1 -1
- package/src/lib/components/dashboard/cron-format.ts +3 -3
- package/src/lib/components/dashboard/run-filters.test.ts +4 -60
- package/src/lib/components/dashboard/run-filters.ts +29 -144
- package/src/lib/components/dashboard/run-helpers.test.ts +25 -6
- package/src/lib/components/dashboard/run-helpers.ts +40 -39
- package/src/lib/components/dashboard/status-config.ts +38 -83
- package/src/lib/components/dashboard/types.ts +0 -26
- package/src/lib/components/data-grid-selection.ts +2 -9
- package/src/lib/components/filter-spec.ts +3 -3
- package/src/lib/components/radio-group.ts +11 -0
- package/src/lib/index.ts +8 -43
- package/src/lib/log-console/LogCache.svelte.ts +1 -1
- package/src/lib/log-console/LogFetcher.svelte.ts +3 -14
- package/src/lib/log-console/ansi.ts +1 -1
- 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-tokens.css +21 -21
- package/src/lib/theme.css +22 -16
- package/src/lib/utils/clipboard.svelte.test.ts +92 -0
- package/src/lib/utils/clipboard.svelte.ts +73 -0
- package/src/lib/utils/format.test.ts +3 -3
- 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 +5 -14
- 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/LogConsole.stories.svelte +2 -2
- 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
package/src/lib/utils/format.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
// Signed magnitude thresholds for Intl.RelativeTimeFormat, largest-first
|
|
5
|
-
// division.
|
|
5
|
+
// division. The buckets match date-fns' formatDistance.
|
|
6
6
|
const RELATIVE_DIVISIONS: [amount: number, unit: Intl.RelativeTimeFormatUnit][] = [
|
|
7
7
|
[60, "seconds"],
|
|
8
8
|
[60, "minutes"],
|
|
@@ -13,105 +13,88 @@ const RELATIVE_DIVISIONS: [amount: number, unit: Intl.RelativeTimeFormatUnit][]
|
|
|
13
13
|
[Number.POSITIVE_INFINITY, "years"],
|
|
14
14
|
];
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
let delta = (date.getTime() - base.getTime()) / 1000;
|
|
16
|
+
const relativeTime = new Intl.RelativeTimeFormat(undefined, { numeric: "always" });
|
|
17
|
+
|
|
18
|
+
/** "N units ago" / "in N units" for `date` relative to `now`. */
|
|
19
|
+
export function formatRelativeTime(date: string | Date, now: Date = new Date()): string {
|
|
20
|
+
let delta = (new Date(date).getTime() - now.getTime()) / 1000;
|
|
22
21
|
for (const [amount, unit] of RELATIVE_DIVISIONS) {
|
|
23
22
|
if (Math.abs(delta) < amount) {
|
|
24
|
-
return
|
|
23
|
+
return relativeTime.format(Math.round(delta), unit);
|
|
25
24
|
}
|
|
26
25
|
delta /= amount;
|
|
27
26
|
}
|
|
28
|
-
return
|
|
27
|
+
return relativeTime.format(Math.round(delta), "years");
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
36
|
-
value /= 1024;
|
|
37
|
-
unitIndex++;
|
|
38
|
-
}
|
|
39
|
-
const unit = units[unitIndex] ?? "B";
|
|
40
|
-
const digits = value < 10 && unitIndex > 0 ? 1 : 0;
|
|
41
|
-
return `${new Intl.NumberFormat("en", { maximumFractionDigits: digits }).format(value)} ${unit}`;
|
|
30
|
+
/** A locale date/time formatter for one fixed option set, built once. */
|
|
31
|
+
function dateFormatter(options: Intl.DateTimeFormatOptions): (date: string | Date) => string {
|
|
32
|
+
const format = new Intl.DateTimeFormat(undefined, options);
|
|
33
|
+
return (date) => format.format(new Date(date));
|
|
42
34
|
}
|
|
43
35
|
|
|
44
|
-
export
|
|
45
|
-
|
|
46
|
-
|
|
36
|
+
export const formatDateTime = dateFormatter({
|
|
37
|
+
year: "numeric",
|
|
38
|
+
month: "short",
|
|
39
|
+
day: "numeric",
|
|
40
|
+
hour: "numeric",
|
|
41
|
+
minute: "2-digit",
|
|
42
|
+
});
|
|
47
43
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const diffDays = diffMs / (1000 * 60 * 60 * 24);
|
|
44
|
+
/** Wall-clock time of day, seconds included, 24-hour, e.g. "17:15:02". */
|
|
45
|
+
export const formatClockTime = dateFormatter({
|
|
46
|
+
hour: "2-digit",
|
|
47
|
+
minute: "2-digit",
|
|
48
|
+
second: "2-digit",
|
|
49
|
+
hour12: false,
|
|
50
|
+
});
|
|
56
51
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
});
|
|
64
|
-
} else {
|
|
65
|
-
absolute = date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|
66
|
-
}
|
|
52
|
+
/** Calendar date without the time, e.g. "22 Jun 2026". */
|
|
53
|
+
export const formatCalendarDate = dateFormatter({
|
|
54
|
+
year: "numeric",
|
|
55
|
+
month: "short",
|
|
56
|
+
day: "numeric",
|
|
57
|
+
});
|
|
67
58
|
|
|
68
|
-
|
|
69
|
-
}
|
|
59
|
+
/** Time of day, no seconds, 24-hour, e.g. "17:15". */
|
|
60
|
+
export const formatTimeHM = dateFormatter({ hour: "2-digit", minute: "2-digit", hour12: false });
|
|
70
61
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
year: "numeric",
|
|
74
|
-
month: "short",
|
|
75
|
-
day: "numeric",
|
|
76
|
-
hour: "numeric",
|
|
77
|
-
minute: "2-digit",
|
|
78
|
-
});
|
|
79
|
-
}
|
|
62
|
+
/** Day and month, no year, e.g. "22 Jun". */
|
|
63
|
+
export const formatDayMonth = dateFormatter({ month: "short", day: "numeric" });
|
|
80
64
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
65
|
+
export const formatFullDateTime = dateFormatter({
|
|
66
|
+
year: "numeric",
|
|
67
|
+
month: "short",
|
|
68
|
+
day: "2-digit",
|
|
69
|
+
hour: "2-digit",
|
|
70
|
+
minute: "2-digit",
|
|
71
|
+
second: "2-digit",
|
|
72
|
+
hour12: false,
|
|
73
|
+
});
|
|
90
74
|
|
|
91
|
-
|
|
92
|
-
export function formatCalendarDate(dateStr: string): string {
|
|
93
|
-
return new Date(dateStr).toLocaleDateString(undefined, {
|
|
94
|
-
year: "numeric",
|
|
95
|
-
month: "short",
|
|
96
|
-
day: "numeric",
|
|
97
|
-
});
|
|
98
|
-
}
|
|
75
|
+
const formatShortTime = dateFormatter({ hour: "numeric", minute: "2-digit", hour12: false });
|
|
99
76
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
77
|
+
export function formatRelativeTimeWithAbsolute(
|
|
78
|
+
dateStr: string | Date,
|
|
79
|
+
now: Date = new Date(),
|
|
80
|
+
): string {
|
|
81
|
+
const date = new Date(dateStr);
|
|
82
|
+
const diffDays = Math.abs(now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24);
|
|
83
|
+
const absolute = diffDays < 1 ? formatShortTime(date) : formatDayMonth(date);
|
|
84
|
+
return `${formatRelativeTime(date, now)} (${absolute})`;
|
|
107
85
|
}
|
|
108
86
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
87
|
+
export function formatBytes(bytes: number): string {
|
|
88
|
+
const units: [string, ...string[]] = ["B", "KB", "MB", "GB", "TB", "PB"];
|
|
89
|
+
let value = bytes;
|
|
90
|
+
let unitIndex = 0;
|
|
91
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
92
|
+
value /= 1024;
|
|
93
|
+
unitIndex++;
|
|
94
|
+
}
|
|
95
|
+
const unit = units[unitIndex] ?? "B";
|
|
96
|
+
const digits = value < 10 && unitIndex > 0 ? 1 : 0;
|
|
97
|
+
return `${new Intl.NumberFormat("en", { maximumFractionDigits: digits }).format(value)} ${unit}`;
|
|
115
98
|
}
|
|
116
99
|
|
|
117
100
|
export function formatDuration(ms: number): string {
|
|
@@ -125,15 +108,3 @@ export function formatDuration(ms: number): string {
|
|
|
125
108
|
const remM = m % 60;
|
|
126
109
|
return remM > 0 ? String(h) + "h " + String(remM) + "m" : String(h) + "h";
|
|
127
110
|
}
|
|
128
|
-
|
|
129
|
-
export function formatFullDateTime(date: Date | string): string {
|
|
130
|
-
return new Intl.DateTimeFormat(undefined, {
|
|
131
|
-
year: "numeric",
|
|
132
|
-
month: "short",
|
|
133
|
-
day: "2-digit",
|
|
134
|
-
hour: "2-digit",
|
|
135
|
-
minute: "2-digit",
|
|
136
|
-
second: "2-digit",
|
|
137
|
-
hour12: false,
|
|
138
|
-
}).format(new Date(date));
|
|
139
|
-
}
|
package/src/lib/utils/id.test.ts
CHANGED
|
@@ -5,16 +5,12 @@ import { describe, expect, it } from "vitest";
|
|
|
5
5
|
import { formatShortId } from "./id.js";
|
|
6
6
|
|
|
7
7
|
describe("formatShortId", () => {
|
|
8
|
-
it("returns the last 8 characters
|
|
8
|
+
it("returns the last 8 characters", () => {
|
|
9
9
|
expect(formatShortId("01HVZ4XYZ123456ABCDEF")).toBe("56ABCDEF");
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
it("
|
|
13
|
-
expect(formatShortId("
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("returns the whole string when shorter than the requested length", () => {
|
|
17
|
-
expect(formatShortId("abc", 8)).toBe("abc");
|
|
12
|
+
it("returns the whole string when shorter than 8", () => {
|
|
13
|
+
expect(formatShortId("abc")).toBe("abc");
|
|
18
14
|
});
|
|
19
15
|
|
|
20
16
|
it("returns an empty string for an empty input", () => {
|
package/src/lib/utils/id.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: PoppyCake, s.r.o.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/** The last 8 characters of a ULID, the part that differs between runs. */
|
|
5
|
+
export function formatShortId(id: string): string {
|
|
6
|
+
return id.slice(-8);
|
|
6
7
|
}
|
|
@@ -89,12 +89,6 @@ describe("themeStore", () => {
|
|
|
89
89
|
expect(themeStore.resolved).toBe("dark");
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
it("falls back to the legacy storage key", async () => {
|
|
93
|
-
stubBrowser({ storage: { "runwisp-theme": "light" } });
|
|
94
|
-
const { themeStore } = await import("./theme.svelte.js");
|
|
95
|
-
expect(themeStore.preference).toBe("light");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
92
|
it("ignores an invalid stored value", async () => {
|
|
99
93
|
stubBrowser({ storage: { "runwisp:theme": "purple" } });
|
|
100
94
|
const { themeStore } = await import("./theme.svelte.js");
|
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
export type ThemePreference = "auto" | "light" | "dark";
|
|
5
|
-
|
|
5
|
+
type ResolvedTheme = "light" | "dark";
|
|
6
6
|
|
|
7
7
|
// Shared by every RunWisp surface. Keep byte-for-byte in sync with the
|
|
8
8
|
// no-flash <head> scripts in each app's app.html / layout.
|
|
9
|
-
|
|
10
|
-
// The daemon dashboard used this key before the store moved here.
|
|
11
|
-
const LEGACY_STORAGE_KEY = "runwisp-theme";
|
|
9
|
+
const THEME_STORAGE_KEY = "runwisp:theme";
|
|
12
10
|
|
|
13
11
|
function isPreference(value: string | null): value is ThemePreference {
|
|
14
12
|
return value === "auto" || value === "light" || value === "dark";
|
|
@@ -16,7 +14,7 @@ function isPreference(value: string | null): value is ThemePreference {
|
|
|
16
14
|
|
|
17
15
|
function readStored(): string | null {
|
|
18
16
|
try {
|
|
19
|
-
return localStorage.getItem(THEME_STORAGE_KEY)
|
|
17
|
+
return localStorage.getItem(THEME_STORAGE_KEY);
|
|
20
18
|
} catch {
|
|
21
19
|
return null; // private mode / embedded contexts
|
|
22
20
|
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: PoppyCake, s.r.o.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
type ToastType = "success" | "error" | "warning" | "info";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export interface ToastAction {
|
|
6
|
+
interface ToastAction {
|
|
9
7
|
label: string;
|
|
10
8
|
onClick: () => void;
|
|
11
9
|
}
|
|
@@ -18,7 +16,7 @@ export interface Toast {
|
|
|
18
16
|
action?: ToastAction;
|
|
19
17
|
}
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
interface ToastOptions {
|
|
22
20
|
duration?: number;
|
|
23
21
|
action?: ToastAction;
|
|
24
22
|
}
|
|
@@ -28,6 +26,7 @@ const DEFAULT_DURATION = 5000;
|
|
|
28
26
|
class ToastStore {
|
|
29
27
|
items = $state<Toast[]>([]);
|
|
30
28
|
private readonly timers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
29
|
+
private nextId = 0;
|
|
31
30
|
|
|
32
31
|
add(type: ToastType, message: string, opts: ToastOptions = {}): string {
|
|
33
32
|
const duration = opts.duration ?? DEFAULT_DURATION;
|
|
@@ -37,7 +36,7 @@ class ToastStore {
|
|
|
37
36
|
return existing.id;
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
const id =
|
|
39
|
+
const id = `toast-${String(++this.nextId)}`;
|
|
41
40
|
const next: Toast = { id, type, message, duration };
|
|
42
41
|
if (opts.action) next.action = opts.action;
|
|
43
42
|
this.items = [...this.items, next];
|
|
@@ -88,14 +87,6 @@ class ToastStore {
|
|
|
88
87
|
info(message: string, opts?: ToastOptions): string {
|
|
89
88
|
return this.add("info", message, opts);
|
|
90
89
|
}
|
|
91
|
-
|
|
92
|
-
clear() {
|
|
93
|
-
for (const timer of this.timers.values()) {
|
|
94
|
-
clearTimeout(timer);
|
|
95
|
-
}
|
|
96
|
-
this.timers.clear();
|
|
97
|
-
this.items = [];
|
|
98
|
-
}
|
|
99
90
|
}
|
|
100
91
|
|
|
101
92
|
export const toast = new ToastStore();
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script module>
|
|
5
|
+
import { defineMeta } from "@storybook/addon-svelte-csf";
|
|
6
|
+
import { LifeBuoy, Trash } from "@lucide/svelte";
|
|
7
|
+
import Accordion from "$lib/components/Accordion.svelte";
|
|
8
|
+
import AccordionItem from "$lib/components/AccordionItem.svelte";
|
|
9
|
+
import Toggle from "$lib/components/Toggle.svelte";
|
|
10
|
+
|
|
11
|
+
const { Story } = defineMeta({
|
|
12
|
+
title: "Navigation/Accordion",
|
|
13
|
+
component: Accordion,
|
|
14
|
+
tags: ["autodocs"],
|
|
15
|
+
});
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<Story name="Multiple" asChild>
|
|
19
|
+
<Accordion class="max-w-lg">
|
|
20
|
+
<AccordionItem title="Retries" open>Retry a failed run up to three times.</AccordionItem>
|
|
21
|
+
<AccordionItem title="Timeouts">Kill the run after 10 minutes.</AccordionItem>
|
|
22
|
+
<AccordionItem title="Notifications">Send failures to Slack.</AccordionItem>
|
|
23
|
+
</Accordion>
|
|
24
|
+
</Story>
|
|
25
|
+
|
|
26
|
+
<Story name="Single" asChild>
|
|
27
|
+
<Accordion type="single" class="max-w-lg">
|
|
28
|
+
<AccordionItem title="Retries" open>Opening another section closes this one.</AccordionItem>
|
|
29
|
+
<AccordionItem title="Timeouts">Kill the run after 10 minutes.</AccordionItem>
|
|
30
|
+
<AccordionItem title="Notifications">Send failures to Slack.</AccordionItem>
|
|
31
|
+
</Accordion>
|
|
32
|
+
</Story>
|
|
33
|
+
|
|
34
|
+
<Story name="Framed, default open" asChild>
|
|
35
|
+
<AccordionItem variant="framed" open class="max-w-lg">
|
|
36
|
+
{#snippet title()}
|
|
37
|
+
<span class="flex items-center gap-2.5">
|
|
38
|
+
<LifeBuoy size={16} class="text-on-surface-faint" />
|
|
39
|
+
Schedule recovery
|
|
40
|
+
<span class="font-sans text-xs text-on-surface-faint">Missed ticks & jitter</span>
|
|
41
|
+
</span>
|
|
42
|
+
{/snippet}
|
|
43
|
+
What to do with cron ticks missed while the scheduler was offline.
|
|
44
|
+
</AccordionItem>
|
|
45
|
+
</Story>
|
|
46
|
+
|
|
47
|
+
<Story name="Framed with actions" asChild>
|
|
48
|
+
<AccordionItem variant="framed" title="Node.js 22" class="max-w-lg">
|
|
49
|
+
{#snippet actions()}
|
|
50
|
+
<Toggle checked size="sm" />
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
aria-label="Remove"
|
|
54
|
+
class="text-on-surface-faint hover:text-danger-surface"
|
|
55
|
+
>
|
|
56
|
+
<Trash size={14} />
|
|
57
|
+
</button>
|
|
58
|
+
{/snippet}
|
|
59
|
+
<pre class="font-mono text-xs">RUN apk add --no-cache nodejs npm</pre>
|
|
60
|
+
</AccordionItem>
|
|
61
|
+
</Story>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script module>
|
|
5
|
+
import { defineMeta } from "@storybook/addon-svelte-csf";
|
|
6
|
+
import CodeBlock from "$lib/components/CodeBlock.svelte";
|
|
7
|
+
|
|
8
|
+
const helloTask = '[tasks.hello]\ncron = "*/5 * * * *"\nrun = "echo hello"';
|
|
9
|
+
const backupTask = '[tasks.backup]\ncron = "0 3 * * *"\nkeep_runs = 60';
|
|
10
|
+
|
|
11
|
+
const { Story } = defineMeta({
|
|
12
|
+
title: "Data Display/CodeBlock",
|
|
13
|
+
component: CodeBlock,
|
|
14
|
+
tags: ["autodocs"],
|
|
15
|
+
argTypes: {
|
|
16
|
+
variant: { control: "select", options: ["terminal", "surface"] },
|
|
17
|
+
size: { control: "select", options: ["sm", "md"] },
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<Story
|
|
23
|
+
name="Default"
|
|
24
|
+
args={{
|
|
25
|
+
code: "export RUNWISP_STATION_TOKEN=rw_xxx\nexport RUNWISP_STATION_URL=https://app.runwisp.com\nrunwisp station",
|
|
26
|
+
}}
|
|
27
|
+
/>
|
|
28
|
+
|
|
29
|
+
<Story
|
|
30
|
+
name="Filename"
|
|
31
|
+
args={{ code: "FROM alpine:3.20\nRUN apk add --no-cache curl", filename: "Dockerfile" }}
|
|
32
|
+
/>
|
|
33
|
+
|
|
34
|
+
<Story
|
|
35
|
+
name="Shell prompt"
|
|
36
|
+
args={{ code: "curl -fsSL https://get.runwisp.com/install.sh | bash", prompt: "$" }}
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
<Story name="Surface variant" asChild>
|
|
40
|
+
<CodeBlock variant="surface" size="sm" copyable={false} code={helloTask} />
|
|
41
|
+
</Story>
|
|
42
|
+
|
|
43
|
+
<!-- Rich children render the markup; `code` keeps the copied text plain. -->
|
|
44
|
+
<Story name="Highlighted children" asChild>
|
|
45
|
+
<!-- No whitespace between the line spans: inside <pre> it would render as blank lines. -->
|
|
46
|
+
<CodeBlock filename="runwisp.toml" code={backupTask}
|
|
47
|
+
><span class="block text-[var(--rw-syn-fn)]">[tasks.backup]</span><span class="block"
|
|
48
|
+
><span class="text-[var(--rw-syn-kw)]">cron</span> =
|
|
49
|
+
<span class="text-[var(--rw-syn-str)]">"0 3 * * *"</span></span
|
|
50
|
+
><span class="block"
|
|
51
|
+
><span class="text-[var(--rw-syn-kw)]">keep_runs</span> =
|
|
52
|
+
<span class="text-[var(--rw-syn-num)]">60</span></span
|
|
53
|
+
></CodeBlock
|
|
54
|
+
>
|
|
55
|
+
</Story>
|
|
@@ -138,11 +138,6 @@
|
|
|
138
138
|
|
|
139
139
|
<Story name="Sortable" args={{ columns, data: tasks, sortKey: "name", sortDirection: "asc" }} />
|
|
140
140
|
|
|
141
|
-
<Story
|
|
142
|
-
name="Filterable"
|
|
143
|
-
args={{ columns, data: tasks, filterable: true, filterPlaceholder: "Filter tasks…" }}
|
|
144
|
-
/>
|
|
145
|
-
|
|
146
141
|
<Story name="Paginated" args={{ columns, data: tasks, paginate: true, pageSize: 2 }} />
|
|
147
142
|
|
|
148
143
|
<Story name="Filters, Sort, Selection, Pagination" asChild>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script module>
|
|
5
|
+
import { defineMeta } from "@storybook/addon-svelte-csf";
|
|
6
|
+
import Heading from "$lib/components/Heading.svelte";
|
|
7
|
+
import Text from "$lib/components/Text.svelte";
|
|
8
|
+
|
|
9
|
+
const { Story } = defineMeta({
|
|
10
|
+
title: "Core/Heading",
|
|
11
|
+
component: Heading,
|
|
12
|
+
tags: ["autodocs"],
|
|
13
|
+
});
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<Story name="Levels" asChild>
|
|
17
|
+
<div class="space-y-3">
|
|
18
|
+
<Heading level={1}>Level 1</Heading>
|
|
19
|
+
<Heading level={2}>Level 2</Heading>
|
|
20
|
+
<Heading level={3}>Level 3</Heading>
|
|
21
|
+
<Heading level={4}>Level 4</Heading>
|
|
22
|
+
</div>
|
|
23
|
+
</Story>
|
|
24
|
+
|
|
25
|
+
<!-- A card title: the semantic level stays, the size shrinks. -->
|
|
26
|
+
<Story name="Card title" asChild>
|
|
27
|
+
<div class="max-w-sm rounded-[4px] border border-outline bg-surface-raised p-4">
|
|
28
|
+
<Heading level={2} size="sm" id="resources-title">System resources</Heading>
|
|
29
|
+
<Text size="sm" color="muted" class="mt-2">CPU and memory over the last hour.</Text>
|
|
30
|
+
</div>
|
|
31
|
+
</Story>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script module>
|
|
5
|
+
import { defineMeta } from "@storybook/addon-svelte-csf";
|
|
6
|
+
import Kbd from "$lib/components/Kbd.svelte";
|
|
7
|
+
|
|
8
|
+
const { Story } = defineMeta({
|
|
9
|
+
title: "Core/Kbd",
|
|
10
|
+
component: Kbd,
|
|
11
|
+
tags: ["autodocs"],
|
|
12
|
+
argTypes: {
|
|
13
|
+
size: { control: "select", options: ["xs", "sm"] },
|
|
14
|
+
tone: { control: "select", options: ["default", "faint", "console"] },
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<Story name="Default" args={{ keys: ["Ctrl", "K"] }} />
|
|
20
|
+
|
|
21
|
+
<Story name="Faint in an input" asChild>
|
|
22
|
+
<div
|
|
23
|
+
class="flex h-9 max-w-xs items-center justify-between rounded-[3px] border border-outline bg-surface-sunken/60 px-3 font-mono text-sm text-on-surface-faint"
|
|
24
|
+
>
|
|
25
|
+
Search tasks…
|
|
26
|
+
<Kbd keys="⌘K" size="xs" tone="faint" />
|
|
27
|
+
</div>
|
|
28
|
+
</Story>
|
|
29
|
+
|
|
30
|
+
<Story name="Console" asChild>
|
|
31
|
+
<div
|
|
32
|
+
class="flex items-center gap-1.5 bg-[var(--rw-con-bg)] px-3 py-2 font-mono text-xs text-[var(--rw-con-dim)]"
|
|
33
|
+
>
|
|
34
|
+
Restore <Kbd keys="Esc" size="xs" tone="console" />
|
|
35
|
+
</div>
|
|
36
|
+
</Story>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script module>
|
|
5
|
+
import { defineMeta } from "@storybook/addon-svelte-csf";
|
|
6
|
+
import Link from "$lib/components/Link.svelte";
|
|
7
|
+
|
|
8
|
+
const { Story } = defineMeta({
|
|
9
|
+
title: "Core/Link",
|
|
10
|
+
component: Link,
|
|
11
|
+
tags: ["autodocs"],
|
|
12
|
+
argTypes: {
|
|
13
|
+
variant: { control: "select", options: ["default", "muted", "primary", "danger"] },
|
|
14
|
+
underline: { control: "select", options: ["always", "hover", "none"] },
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<Story name="Variants" asChild>
|
|
20
|
+
<div class="flex flex-col gap-2 text-sm">
|
|
21
|
+
<Link href="#tasks">Default</Link>
|
|
22
|
+
<Link href="#tasks" variant="muted">Muted</Link>
|
|
23
|
+
<Link href="#tasks" variant="primary">Primary</Link>
|
|
24
|
+
<Link href="#tasks" variant="danger">Danger</Link>
|
|
25
|
+
</div>
|
|
26
|
+
</Story>
|
|
27
|
+
|
|
28
|
+
<Story name="External" asChild>
|
|
29
|
+
<Link
|
|
30
|
+
href="https://docs.runwisp.com/configuration/tasks/"
|
|
31
|
+
external
|
|
32
|
+
variant="primary"
|
|
33
|
+
title="Opens in a new tab"
|
|
34
|
+
class="text-sm font-medium"
|
|
35
|
+
>
|
|
36
|
+
Task configuration docs →
|
|
37
|
+
</Link>
|
|
38
|
+
</Story>
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
` ${ESC}32m✓${ESC}0m users ${ESC}90m(184,221 rows, 48 MB)${ESC}0m`,
|
|
25
25
|
` ${ESC}32m✓${ESC}0m sessions ${ESC}90m(902,114 rows, 210 MB)${ESC}0m`,
|
|
26
26
|
` ${ESC}32m✓${ESC}0m invoices ${ESC}90m(45,002 rows, 12 MB)${ESC}0m`,
|
|
27
|
-
`${ESC}33m[warn]${ESC}0m table ${ESC}1maudit_log${ESC}0m exceeds 1 GB
|
|
27
|
+
`${ESC}33m[warn]${ESC}0m table ${ESC}1maudit_log${ESC}0m exceeds 1 GB, consider partitioning`,
|
|
28
28
|
` ${ESC}32m✓${ESC}0m audit_log ${ESC}90m(7,212,330 rows, 1.2 GB)${ESC}0m`,
|
|
29
29
|
`${ESC}31m[error]${ESC}0m checksum mismatch on chunk 7, retrying ${ESC}90m(attempt 1/3)${ESC}0m`,
|
|
30
30
|
` ${ESC}32m✓${ESC}0m retry succeeded`,
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
const LONG_LINES = [
|
|
54
54
|
`${ESC}36m[info]${ESC}0m starting request trace`,
|
|
55
55
|
`${ESC}90m2026-06-05T03:00:00Z${ESC}0m GET /api/v1/tasks?include=runs,logs,schedule&limit=100&offset=0 → ${ESC}32m200${ESC}0m ${ESC}90m{"trace":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","upstream":"https://internal.example.com/very/long/path/segment/that/keeps/going/and/going","latency_ms":1287,"bytes":204812}${ESC}0m`,
|
|
56
|
-
`${ESC}31m[error]${ESC}0m panic: runtime error: index out of range [12] with length 8
|
|
56
|
+
`${ESC}31m[error]${ESC}0m panic: runtime error: index out of range [12] with length 8, goroutine 42 [running]: main.process(0xc0000b4000, 0x1f4) /home/runner/work/app/app/internal/pipeline/process.go:317 +0x4a5`,
|
|
57
57
|
" short line in between",
|
|
58
58
|
`tab-separated\tcolumns\there\tshould\tstay\taligned\tand\textend\tfar\tpast\tthe\tviewport\tedge\tto\tprove\thorizontal\tscroll`,
|
|
59
59
|
`${ESC}32m[done]${ESC}0m ${"=".repeat(400)}|END`,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script module>
|
|
5
|
+
import { defineMeta } from "@storybook/addon-svelte-csf";
|
|
6
|
+
import PageHeader from "$lib/components/PageHeader.svelte";
|
|
7
|
+
import Button from "$lib/components/Button.svelte";
|
|
8
|
+
|
|
9
|
+
const { Story } = defineMeta({
|
|
10
|
+
title: "Layout/PageHeader",
|
|
11
|
+
component: PageHeader,
|
|
12
|
+
tags: ["autodocs"],
|
|
13
|
+
});
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<Story
|
|
17
|
+
name="Default"
|
|
18
|
+
args={{ title: "Tasks", subtitle: "Scheduled tasks and background processes." }}
|
|
19
|
+
/>
|
|
20
|
+
|
|
21
|
+
<Story name="Rich subtitle and actions" asChild>
|
|
22
|
+
<PageHeader title="Notifications">
|
|
23
|
+
{#snippet subtitle()}
|
|
24
|
+
Recent notifications across all tasks ·
|
|
25
|
+
<span class="font-mono font-medium text-primary-soft-text tabular-nums">3 unread</span>
|
|
26
|
+
{/snippet}
|
|
27
|
+
{#snippet actions()}
|
|
28
|
+
<Button size="sm" variant="secondary">Mark all read</Button>
|
|
29
|
+
{/snippet}
|
|
30
|
+
</PageHeader>
|
|
31
|
+
</Story>
|
|
@@ -63,3 +63,29 @@
|
|
|
63
63
|
<p class="text-sm text-on-surface-muted">Processing 847 of 1,234 records</p>
|
|
64
64
|
</div>
|
|
65
65
|
</Story>
|
|
66
|
+
|
|
67
|
+
<Story name="Thresholds" asChild>
|
|
68
|
+
<div class="max-w-md space-y-4">
|
|
69
|
+
<Progress
|
|
70
|
+
value={42}
|
|
71
|
+
size="sm"
|
|
72
|
+
thresholds={{ warning: 70, danger: 90 }}
|
|
73
|
+
label="CPU"
|
|
74
|
+
showLabel
|
|
75
|
+
/>
|
|
76
|
+
<Progress
|
|
77
|
+
value={76}
|
|
78
|
+
size="sm"
|
|
79
|
+
thresholds={{ warning: 70, danger: 90 }}
|
|
80
|
+
label="Memory"
|
|
81
|
+
showLabel
|
|
82
|
+
/>
|
|
83
|
+
<Progress
|
|
84
|
+
value={95}
|
|
85
|
+
size="sm"
|
|
86
|
+
thresholds={{ warning: 70, danger: 90 }}
|
|
87
|
+
label="Disk"
|
|
88
|
+
showLabel
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
</Story>
|