@runwisp/ui 0.4.0 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@runwisp/ui",
3
3
  "description": "Svelte 5 UI component library for RunWisp dashboards and admin interfaces.",
4
- "version": "0.4.0",
4
+ "version": "0.6.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -18,20 +18,11 @@
18
18
  "scripts": {
19
19
  "dev": "storybook dev -p 6006 --no-open",
20
20
  "build": "echo '@runwisp/ui: no build step (raw source)'",
21
- "check": "svelte-check --tsconfig ./tsconfig.json && prettier --check . && eslint .",
21
+ "check": "bun run check:types && bun run check:lint",
22
+ "check:types": "svelte-check --tsconfig ./tsconfig.json",
23
+ "check:lint": "prettier --check . && eslint .",
22
24
  "format": "prettier --write ."
23
25
  },
24
- "nx": {
25
- "targets": {
26
- "build": {
27
- "cache": true,
28
- "inputs": [
29
- "production"
30
- ],
31
- "outputs": []
32
- }
33
- }
34
- },
35
26
  "files": [
36
27
  "src"
37
28
  ],
@@ -67,8 +58,8 @@
67
58
  "@chromatic-com/storybook": "^4.1.3",
68
59
  "@eslint/compat": "^1.4.0",
69
60
  "@eslint/js": "^9.39.1",
70
- "@runwisp/eslint-config": "0.4.0",
71
- "@runwisp/typescript-config": "0.4.0",
61
+ "@runwisp/eslint-config": "0.6.0",
62
+ "@runwisp/typescript-config": "0.6.0",
72
63
  "@storybook/addon-a11y": "^10.1.9",
73
64
  "@storybook/addon-docs": "^10.1.9",
74
65
  "@storybook/addon-svelte-csf": "^5.0.10",
@@ -116,7 +107,8 @@
116
107
  "@floating-ui/dom": "^1.7.4",
117
108
  "@fontsource/tasa-orbiter": "^5.2.2",
118
109
  "@lucide/svelte": "^0.561.0",
119
- "@runwisp/common": "0.4.0",
110
+ "@runwisp/common": "0.6.0",
111
+ "@tanstack/svelte-virtual": "^3.13.25",
120
112
  "ansi-to-html": "^0.7.2",
121
113
  "cron-parser": "^5.4.0",
122
114
  "cronstrue": "^3.9.0",
package/src/app.html CHANGED
@@ -4,6 +4,7 @@
4
4
  <meta charset="utf-8" />
5
5
  <link rel="icon" href="%sveltekit.assets%/favicon.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <title>RunWisp</title>
7
8
  %sveltekit.head%
8
9
  </head>
9
10
  <body data-sveltekit-preload-data="hover">
@@ -8,9 +8,7 @@ export function portal(node: HTMLElement) {
8
8
  document.body.appendChild(node);
9
9
  return {
10
10
  destroy() {
11
- if (node.parentNode) {
12
- node.parentNode.removeChild(node);
13
- }
11
+ node.remove();
14
12
  },
15
13
  };
16
14
  }
@@ -26,8 +26,6 @@
26
26
  open = !open;
27
27
  }
28
28
  }
29
-
30
- const isSnippet = $derived(typeof title === "function");
31
29
  </script>
32
30
 
33
31
  <div class="group {className}">
@@ -44,8 +42,8 @@
44
42
  aria-expanded={open}
45
43
  >
46
44
  <span class="flex-1">
47
- {#if isSnippet && title}
48
- {@render (title as Snippet)()}
45
+ {#if typeof title === "function"}
46
+ {@render title()}
49
47
  {:else if title}
50
48
  {title}
51
49
  {/if}
@@ -89,21 +89,17 @@
89
89
  md: "h-3.5 w-3.5",
90
90
  lg: "h-4 w-4",
91
91
  };
92
-
93
- let safeVariant = $derived(
94
- (Object.hasOwn(variantClasses, variant) ? variant : "default") as BadgeVariant,
95
- );
96
92
  </script>
97
93
 
98
94
  <span
99
95
  class="{baseClasses} {outline
100
- ? variantClasses[safeVariant].outline
101
- : variantClasses[safeVariant].solid} {sizeClasses[size]} {className}"
96
+ ? variantClasses[variant].outline
97
+ : variantClasses[variant].solid} {sizeClasses[size]} {className}"
102
98
  >
103
99
  {#if dot}
104
100
  {#if loading}
105
101
  <svg
106
- class="{spinnerSizeClasses[size]} animate-spin {indicatorTextColors[safeVariant]}"
102
+ class="{spinnerSizeClasses[size]} animate-spin {indicatorTextColors[variant]}"
107
103
  viewBox="0 0 24 24"
108
104
  fill="none"
109
105
  >
@@ -122,7 +118,7 @@
122
118
  ></path>
123
119
  </svg>
124
120
  {:else}
125
- <span class="h-1.5 w-1.5 rounded-full {dotColors[safeVariant]}"></span>
121
+ <span class="h-1.5 w-1.5 rounded-full {dotColors[variant]}"></span>
126
122
  {/if}
127
123
  {/if}
128
124
  {#if children}
@@ -8,11 +8,12 @@
8
8
  padding?: "none" | "sm" | "md" | "lg";
9
9
  hover?: boolean;
10
10
  border?: boolean;
11
- shadow?: "none" | "sm" | "md" | "lg";
11
+ shadow?: "none" | "sm" | "md" | "lg" | "xl";
12
12
  header?: Snippet;
13
13
  footer?: Snippet;
14
14
  children?: Snippet;
15
15
  class?: string;
16
+ bodyClass?: string;
16
17
  }
17
18
 
18
19
  let {
@@ -24,6 +25,7 @@
24
25
  footer,
25
26
  children,
26
27
  class: className = "",
28
+ bodyClass = "",
27
29
  }: Props = $props();
28
30
 
29
31
  const paddingClasses: Record<string, string> = {
@@ -38,6 +40,7 @@
38
40
  sm: "shadow-sm",
39
41
  md: "shadow-md",
40
42
  lg: "shadow-lg",
43
+ xl: "shadow-xl shadow-mist-900/10",
41
44
  };
42
45
  </script>
43
46
 
@@ -56,7 +59,7 @@
56
59
  </div>
57
60
  {/if}
58
61
 
59
- <div class={paddingClasses[padding]}>
62
+ <div class="{paddingClasses[padding]} {bodyClass}">
60
63
  {#if children}
61
64
  {@render children()}
62
65
  {/if}
@@ -176,7 +176,7 @@
176
176
  <Input
177
177
  value={filterQuery}
178
178
  oninput={(e) => {
179
- filterQuery = (e.currentTarget as HTMLInputElement).value;
179
+ filterQuery = e.currentTarget.value;
180
180
  if (paginate && !total) page = 1;
181
181
  }}
182
182
  type="search"
@@ -308,8 +308,7 @@
308
308
  <td
309
309
  class="px-4 {compact ? 'py-2' : 'py-3'} cursor-pointer"
310
310
  onclick={(e) => {
311
- if ((e.target as HTMLElement).tagName !== "INPUT")
312
- toggleRow(row);
311
+ if (!(e.target instanceof HTMLInputElement)) toggleRow(row);
313
312
  }}
314
313
  >
315
314
  <Checkbox
@@ -90,9 +90,9 @@
90
90
  }
91
91
 
92
92
  function handleOutsideClick(e: MouseEvent) {
93
- const target = e.target as HTMLElement;
94
- if (triggerEl?.contains(target)) return;
95
- if (menuEl?.contains(target)) return;
93
+ const path = e.composedPath();
94
+ if (triggerEl && path.includes(triggerEl)) return;
95
+ if (menuEl && path.includes(menuEl)) return;
96
96
  if (open) toggle(false);
97
97
  }
98
98
 
@@ -85,8 +85,8 @@
85
85
  updateFromValue();
86
86
  });
87
87
 
88
- function handleInput(e: Event) {
89
- const val = (e.target as HTMLInputElement).value;
88
+ function handleInput(e: Event & { currentTarget: EventTarget & HTMLInputElement }) {
89
+ const val = e.currentTarget.value;
90
90
  textValue = val;
91
91
 
92
92
  const msValue = parseToMs(val);
@@ -22,13 +22,12 @@
22
22
  3: "2xl",
23
23
  4: "xl",
24
24
  5: "lg",
25
- 6: "base" as HeadingSize,
25
+ 6: "md",
26
26
  };
27
27
 
28
28
  const sizeClasses: Record<string, string> = {
29
29
  xs: "text-xs font-semibold",
30
30
  sm: "text-sm font-semibold",
31
- base: "text-base font-semibold",
32
31
  md: "text-base font-semibold",
33
32
  lg: "text-lg font-semibold",
34
33
  xl: "text-xl font-bold",
@@ -111,10 +111,11 @@
111
111
 
112
112
  // Request missing data for the visible range (read-only w.r.t. cache).
113
113
  $effect(() => {
114
- if (fetcher && cache.totalLines > 0) {
115
- fetcher.pruneQueue(visibleStart, visibleEnd);
114
+ const f = fetcher;
115
+ if (f && cache.totalLines > 0) {
116
+ f.pruneQueue(visibleStart, visibleEnd);
116
117
  untrack(() => {
117
- fetcher!.maybeRequestMissing(visibleStart, visibleEnd);
118
+ f.maybeRequestMissing(visibleStart, visibleEnd);
118
119
  });
119
120
  }
120
121
  });
@@ -136,8 +137,8 @@
136
137
  prevTotalLines = currentTotal;
137
138
  });
138
139
 
139
- function onScroll(e: Event) {
140
- const target = e.target as HTMLDivElement;
140
+ function onScroll(e: Event & { currentTarget: EventTarget & HTMLDivElement }) {
141
+ const target = e.currentTarget;
141
142
  scrollTop = target.scrollTop;
142
143
 
143
144
  const scrollHeight = target.scrollHeight;
@@ -0,0 +1,29 @@
1
+ <!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
2
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
3
+
4
+ <script lang="ts">
5
+ import type { Snippet } from "svelte";
6
+
7
+ interface Props {
8
+ title: string;
9
+ subtitle?: string;
10
+ actions?: Snippet;
11
+ class?: string;
12
+ }
13
+
14
+ let { title, subtitle, actions, class: className = "" }: Props = $props();
15
+ </script>
16
+
17
+ <div class="flex shrink-0 flex-wrap items-start justify-between gap-3 px-1 {className}">
18
+ <div class="min-w-0">
19
+ <h1 class="text-2xl font-bold tracking-tight text-on-surface">{title}</h1>
20
+ {#if subtitle}
21
+ <p class="mt-0.5 text-sm text-on-surface-muted">{subtitle}</p>
22
+ {/if}
23
+ </div>
24
+ {#if actions}
25
+ <div class="flex shrink-0 items-center gap-2">
26
+ {@render actions()}
27
+ </div>
28
+ {/if}
29
+ </div>
@@ -36,8 +36,8 @@
36
36
  onPageChange?.(clamped);
37
37
  }
38
38
 
39
- function handlePageSizeChange(e: Event) {
40
- const newSize = Number((e.target as HTMLSelectElement).value);
39
+ function handlePageSizeChange(e: Event & { currentTarget: EventTarget & HTMLSelectElement }) {
40
+ const newSize = Number(e.currentTarget.value);
41
41
  onPageSizeChange?.(newSize);
42
42
  }
43
43
  </script>
@@ -64,9 +64,9 @@
64
64
  }
65
65
 
66
66
  function handleOutsideClick(e: MouseEvent) {
67
- const target = e.target as HTMLElement;
68
- if (triggerEl?.contains(target)) return;
69
- if (contentEl?.contains(target)) return;
67
+ const path = e.composedPath();
68
+ if (triggerEl && path.includes(triggerEl)) return;
69
+ if (contentEl && path.includes(contentEl)) return;
70
70
  if (open) {
71
71
  open = false;
72
72
  cleanupFloating?.();
@@ -212,16 +212,12 @@
212
212
  }
213
213
 
214
214
  function handleOutsideClick(e: MouseEvent) {
215
- if (
216
- isOpen &&
217
- triggerEl &&
218
- !triggerEl.contains(e.target as Node) &&
219
- menuEl &&
220
- !menuEl.contains(e.target as Node)
221
- ) {
222
- isOpen = false;
223
- cleanupFloating?.();
224
- }
215
+ if (!isOpen) return;
216
+ const path = e.composedPath();
217
+ if (triggerEl && path.includes(triggerEl)) return;
218
+ if (menuEl && path.includes(menuEl)) return;
219
+ isOpen = false;
220
+ cleanupFloating?.();
225
221
  }
226
222
 
227
223
  onDestroy(() => {
@@ -0,0 +1,33 @@
1
+ <!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
2
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
3
+
4
+ <script lang="ts">
5
+ import type { Snippet } from "svelte";
6
+
7
+ export type TaskCardAccent = "danger" | "wisp" | "aurora";
8
+
9
+ interface Props {
10
+ accent?: TaskCardAccent;
11
+ onclick?: () => void;
12
+ children: Snippet;
13
+ class?: string;
14
+ }
15
+
16
+ let { accent = "wisp", onclick, children, class: className = "" }: Props = $props();
17
+
18
+ const accentClasses: Record<TaskCardAccent, string> = {
19
+ danger: "hover:border-danger-200",
20
+ wisp: "hover:border-wisp-200",
21
+ aurora: "hover:border-aurora-200",
22
+ };
23
+ </script>
24
+
25
+ <button
26
+ type="button"
27
+ class="group w-full rounded-lg border border-mist-100 bg-surface-raised p-3 text-left transition-all hover:shadow-sm {accentClasses[
28
+ accent
29
+ ]} {className}"
30
+ {onclick}
31
+ >
32
+ {@render children()}
33
+ </button>
@@ -72,6 +72,18 @@
72
72
  aria-hidden="true"
73
73
  />
74
74
  <p class="flex-1 text-sm font-medium">{toastItem.message}</p>
75
+ {#if toastItem.action}
76
+ {@const action = toastItem.action}
77
+ <button
78
+ onclick={() => {
79
+ action.onClick();
80
+ toast.remove(toastItem.id);
81
+ }}
82
+ class="flex-shrink-0 rounded-md px-2 py-1 text-xs font-semibold text-on-surface underline-offset-2 transition-colors hover:bg-surface-sunken hover:underline"
83
+ >
84
+ {action.label}
85
+ </button>
86
+ {/if}
75
87
  <button
76
88
  onclick={() => toast.remove(toastItem.id)}
77
89
  class="flex-shrink-0 rounded-lg p-1 text-on-surface-muted transition-colors hover:bg-surface-sunken hover:text-on-surface"
@@ -3,8 +3,17 @@
3
3
 
4
4
  <script lang="ts">
5
5
  import { untrack } from "svelte";
6
- import { Server, Hash, Terminal as TerminalIcon, Download } from "@lucide/svelte";
6
+ import {
7
+ Server,
8
+ Hash,
9
+ Terminal as TerminalIcon,
10
+ Download,
11
+ Trash2,
12
+ MousePointerClick,
13
+ } from "@lucide/svelte";
7
14
  import Badge from "../Badge.svelte";
15
+ import Button from "../Button.svelte";
16
+ import EmptyState from "../EmptyState.svelte";
8
17
  import LogConsole from "../LogConsole.svelte";
9
18
  import type { Run } from "./types.js";
10
19
  import type { LogEvent, LogSlice } from "../../log-console/types.js";
@@ -18,6 +27,7 @@
18
27
  fetchLogs,
19
28
  streamLogs,
20
29
  showTaskName = false,
30
+ onDelete,
21
31
  }: {
22
32
  run: Run | undefined;
23
33
  fetchLogs: (
@@ -31,8 +41,15 @@
31
41
  initialState?: { fromLine: number },
32
42
  ) => () => void;
33
43
  showTaskName?: boolean;
44
+ onDelete?: (runId: string) => void;
34
45
  } = $props();
35
46
 
47
+ let canDelete = $derived.by(() => {
48
+ if (!run || !onDelete) return false;
49
+ const status = runDisplayStatus(run);
50
+ return status !== "running" && status !== "pending";
51
+ });
52
+
36
53
  const TAIL_LINES = 1000;
37
54
 
38
55
  let logConsole = $state<{ onStream: (event: LogEvent) => void } | null>(null);
@@ -109,6 +126,18 @@
109
126
  >
110
127
  {runDisplayStatus(run).toUpperCase()}
111
128
  </Badge>
129
+ {#if canDelete}
130
+ <Button
131
+ variant="ghost"
132
+ size="xs"
133
+ class="ml-1 text-danger-surface hover:bg-danger-soft"
134
+ onclick={() => onDelete?.(run.id)}
135
+ title="Delete this run"
136
+ aria-label="Delete run"
137
+ >
138
+ {#snippet icon()}<Trash2 size={14} />{/snippet}
139
+ </Button>
140
+ {/if}
112
141
  </div>
113
142
  <div
114
143
  class="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-on-surface-muted"
@@ -240,13 +269,11 @@
240
269
  {/key}
241
270
  </div>
242
271
  {:else}
243
- <div
244
- class="flex flex-1 flex-col items-center justify-center bg-surface-sunken/30 text-on-surface-faint"
245
- >
246
- <div class="mb-4 rounded-full border border-outline bg-surface-raised p-4 shadow-sm">
247
- <Server size={32} class="text-outline-faint" />
248
- </div>
249
- <h3 class="mb-1 font-medium text-on-surface">No Run Selected</h3>
250
- <p class="text-sm">Select a run from the history on the left to view details.</p>
272
+ <div class="flex flex-1 items-center justify-center bg-surface-sunken/30">
273
+ <EmptyState
274
+ title="Select a run"
275
+ description="Pick a run from the list to view details and logs."
276
+ icon={MousePointerClick}
277
+ />
251
278
  </div>
252
279
  {/if}