@runwisp/ui 0.6.0 → 0.8.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.
Files changed (48) hide show
  1. package/package.json +4 -12
  2. package/src/lib/components/Alert.svelte +4 -4
  3. package/src/lib/components/Avatar.svelte +8 -8
  4. package/src/lib/components/Badge.svelte +23 -23
  5. package/src/lib/components/CronInput.svelte +1 -1
  6. package/src/lib/components/Dropdown.svelte +16 -3
  7. package/src/lib/components/DurationInput.svelte +4 -2
  8. package/src/lib/components/EmptyState.svelte +1 -1
  9. package/src/lib/components/FormField.svelte +4 -2
  10. package/src/lib/components/Input.svelte +4 -2
  11. package/src/lib/components/LogConsole.svelte +76 -29
  12. package/src/lib/components/Progress.svelte +4 -4
  13. package/src/lib/components/RadioGroup.svelte +3 -1
  14. package/src/lib/components/Select.svelte +1 -1
  15. package/src/lib/components/Sparkline.svelte +11 -3
  16. package/src/lib/components/TaskCard.svelte +4 -4
  17. package/src/lib/components/Textarea.svelte +2 -2
  18. package/src/lib/components/ToastContainer.svelte +4 -4
  19. package/src/lib/components/Tooltip.svelte +11 -2
  20. package/src/lib/components/dashboard/RunDetailPanel.svelte +27 -17
  21. package/src/lib/components/dashboard/RunsList.svelte +19 -2
  22. package/src/lib/components/dashboard/cron-format.test.ts +67 -0
  23. package/src/lib/components/dashboard/cron-format.ts +71 -0
  24. package/src/lib/components/dashboard/status-config.test.ts +18 -0
  25. package/src/lib/components/dashboard/status-config.ts +19 -0
  26. package/src/lib/index.ts +1 -4
  27. package/src/lib/log-console/ansi.test.ts +50 -0
  28. package/src/lib/log-console/ansi.ts +39 -0
  29. package/src/lib/theme-tokens.css +33 -0
  30. package/src/lib/theme.css +6 -0
  31. package/src/lib/utils/format.test.ts +37 -1
  32. package/src/lib/utils/format.ts +8 -6
  33. package/src/stories/Avatar.stories.svelte +1 -1
  34. package/src/stories/Breadcrumb.stories.svelte +3 -3
  35. package/src/stories/Card.stories.svelte +21 -19
  36. package/src/stories/Checkbox.stories.svelte +2 -2
  37. package/src/stories/DataGrid.stories.svelte +1 -1
  38. package/src/stories/Dropdown.stories.svelte +3 -3
  39. package/src/stories/EmptyState.stories.svelte +1 -1
  40. package/src/stories/LogConsole.stories.svelte +75 -0
  41. package/src/stories/LogConsoleDemo.svelte +60 -0
  42. package/src/stories/Logo.stories.svelte +9 -9
  43. package/src/stories/Modal.stories.svelte +12 -10
  44. package/src/stories/Progress.stories.svelte +6 -6
  45. package/src/stories/Tabs.stories.svelte +16 -12
  46. package/src/stories/Toggle.stories.svelte +2 -2
  47. package/src/stories/Tooltip.stories.svelte +5 -5
  48. package/src/lib/utils/toast.ts +0 -60
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.6.0",
4
+ "version": "0.8.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -15,14 +15,6 @@
15
15
  "publishConfig": {
16
16
  "access": "public"
17
17
  },
18
- "scripts": {
19
- "dev": "storybook dev -p 6006 --no-open",
20
- "build": "echo '@runwisp/ui: no build step (raw source)'",
21
- "check": "bun run check:types && bun run check:lint",
22
- "check:types": "svelte-check --tsconfig ./tsconfig.json",
23
- "check:lint": "prettier --check . && eslint .",
24
- "format": "prettier --write ."
25
- },
26
18
  "files": [
27
19
  "src"
28
20
  ],
@@ -58,8 +50,8 @@
58
50
  "@chromatic-com/storybook": "^4.1.3",
59
51
  "@eslint/compat": "^1.4.0",
60
52
  "@eslint/js": "^9.39.1",
61
- "@runwisp/eslint-config": "0.6.0",
62
- "@runwisp/typescript-config": "0.6.0",
53
+ "@runwisp/eslint-config": "0.8.0",
54
+ "@runwisp/typescript-config": "0.8.0",
63
55
  "@storybook/addon-a11y": "^10.1.9",
64
56
  "@storybook/addon-docs": "^10.1.9",
65
57
  "@storybook/addon-svelte-csf": "^5.0.10",
@@ -107,7 +99,7 @@
107
99
  "@floating-ui/dom": "^1.7.4",
108
100
  "@fontsource/tasa-orbiter": "^5.2.2",
109
101
  "@lucide/svelte": "^0.561.0",
110
- "@runwisp/common": "0.6.0",
102
+ "@runwisp/common": "0.8.0",
111
103
  "@tanstack/svelte-virtual": "^3.13.25",
112
104
  "ansi-to-html": "^0.7.2",
113
105
  "cron-parser": "^5.4.0",
@@ -35,25 +35,25 @@
35
35
  > = {
36
36
  info: {
37
37
  bg: "bg-info-soft",
38
- border: "border-aurora-200",
38
+ border: "border-info-soft-border",
39
39
  icon: Info,
40
40
  iconColor: "text-info",
41
41
  },
42
42
  success: {
43
43
  bg: "bg-success-soft",
44
- border: "border-success-200",
44
+ border: "border-success-soft-border",
45
45
  icon: CircleCheckBig,
46
46
  iconColor: "text-success-surface",
47
47
  },
48
48
  warning: {
49
49
  bg: "bg-warning-soft",
50
- border: "border-warning-200",
50
+ border: "border-warning-soft-border",
51
51
  icon: TriangleAlert,
52
52
  iconColor: "text-warning-surface",
53
53
  },
54
54
  danger: {
55
55
  bg: "bg-danger-soft",
56
- border: "border-danger-200",
56
+ border: "border-danger-soft-border",
57
57
  icon: CircleX,
58
58
  iconColor: "text-danger-surface",
59
59
  },
@@ -30,10 +30,10 @@
30
30
  };
31
31
 
32
32
  const statusClasses: Record<string, string> = {
33
- online: "bg-success-500",
33
+ online: "bg-success-surface",
34
34
  offline: "bg-on-surface-faint",
35
- busy: "bg-danger-500",
36
- away: "bg-warning-500",
35
+ busy: "bg-danger-surface",
36
+ away: "bg-warning-surface",
37
37
  };
38
38
 
39
39
  const statusSizes: Record<string, string> = {
@@ -46,11 +46,11 @@
46
46
 
47
47
  // Generate a consistent color based on name
48
48
  const colors = [
49
- "bg-wisp-500",
50
- "bg-aurora-500",
51
- "bg-success-500",
52
- "bg-warning-500",
53
- "bg-danger-500",
49
+ "bg-primary",
50
+ "bg-info",
51
+ "bg-success-surface",
52
+ "bg-warning-surface",
53
+ "bg-danger-surface",
54
54
  ];
55
55
 
56
56
  const colorIndex = $derived(
@@ -35,28 +35,28 @@
35
35
 
36
36
  const variantClasses: Record<BadgeVariant, { solid: string; outline: string }> = {
37
37
  default: {
38
- solid: "bg-mist-100 text-mist-700",
39
- outline: "bg-transparent border border-mist-300 text-mist-700",
38
+ solid: "bg-surface-sunken text-on-surface-muted",
39
+ outline: "bg-transparent border border-outline-hover text-on-surface-muted",
40
40
  },
41
41
  primary: {
42
- solid: "bg-wisp-100 text-wisp-700",
43
- outline: "bg-transparent border border-wisp-300 text-wisp-700",
42
+ solid: "bg-primary-soft text-primary-soft-text",
43
+ outline: "bg-transparent border border-wisp-300 text-primary-soft-text",
44
44
  },
45
45
  success: {
46
- solid: "bg-success-100 text-success-700",
47
- outline: "bg-transparent border border-success-300 text-success-700",
46
+ solid: "bg-success-soft text-success-soft-text",
47
+ outline: "bg-transparent border border-success-300 text-success-soft-text",
48
48
  },
49
49
  warning: {
50
- solid: "bg-warning-100 text-warning-700",
51
- outline: "bg-transparent border border-warning-400 text-warning-700",
50
+ solid: "bg-warning-soft text-warning-soft-text",
51
+ outline: "bg-transparent border border-warning-400 text-warning-soft-text",
52
52
  },
53
53
  danger: {
54
- solid: "bg-danger-100 text-danger-700",
55
- outline: "bg-transparent border border-danger-300 text-danger-700",
54
+ solid: "bg-danger-soft text-danger-soft-text",
55
+ outline: "bg-transparent border border-danger-300 text-danger-soft-text",
56
56
  },
57
57
  info: {
58
- solid: "bg-aurora-100 text-aurora-700",
59
- outline: "bg-transparent border border-aurora-300 text-aurora-700",
58
+ solid: "bg-info-soft text-info-soft-text",
59
+ outline: "bg-transparent border border-aurora-300 text-info-soft-text",
60
60
  },
61
61
  };
62
62
 
@@ -68,20 +68,20 @@
68
68
 
69
69
  const dotColors: Record<BadgeVariant, string> = {
70
70
  default: "bg-mist-500",
71
- primary: "bg-wisp-500",
72
- success: "bg-success-500",
73
- warning: "bg-warning-500",
74
- danger: "bg-danger-500",
75
- info: "bg-aurora-500",
71
+ primary: "bg-primary",
72
+ success: "bg-success-surface",
73
+ warning: "bg-warning-surface",
74
+ danger: "bg-danger-surface",
75
+ info: "bg-info",
76
76
  };
77
77
 
78
78
  const indicatorTextColors: Record<BadgeVariant, string> = {
79
- default: "text-mist-500",
80
- primary: "text-wisp-500",
81
- success: "text-success-500",
82
- warning: "text-warning-500",
83
- danger: "text-danger-500",
84
- info: "text-aurora-500",
79
+ default: "text-on-surface-muted",
80
+ primary: "text-primary",
81
+ success: "text-success-soft-text",
82
+ warning: "text-warning-soft-text",
83
+ danger: "text-danger-soft-text",
84
+ info: "text-info",
85
85
  };
86
86
 
87
87
  const spinnerSizeClasses: Record<BadgeSize, string> = {
@@ -88,7 +88,7 @@
88
88
  <!-- Live Feedback -->
89
89
  <div
90
90
  class="flex items-start gap-3 rounded-md border p-3 transition-colors duration-200
91
- {isError ? 'border-danger-200 bg-danger-soft' : 'border-outline bg-surface-sunken'}"
91
+ {isError ? 'border-danger-soft-border bg-danger-soft' : 'border-outline bg-surface-sunken'}"
92
92
  >
93
93
  <Info size={18} class="mt-0.5 {isError ? 'text-danger-surface' : 'text-primary'}" />
94
94
  <div>
@@ -5,7 +5,7 @@
5
5
  import type { Snippet } from "svelte";
6
6
  import type { Component } from "svelte";
7
7
  import { tick, onDestroy } from "svelte";
8
- import { EllipsisVertical } from "@lucide/svelte";
8
+ import { EllipsisVertical, Check } from "@lucide/svelte";
9
9
  import { scale } from "svelte/transition";
10
10
  import { quintOut } from "svelte/easing";
11
11
  import { computePosition, autoUpdate, flip, shift, offset } from "@floating-ui/dom";
@@ -18,6 +18,7 @@
18
18
  danger?: boolean;
19
19
  disabled?: boolean;
20
20
  divider?: boolean;
21
+ selected?: boolean;
21
22
  title?: string;
22
23
  }
23
24
 
@@ -26,10 +27,18 @@
26
27
  trigger?: Snippet;
27
28
  children?: Snippet;
28
29
  align?: "left" | "right";
30
+ triggerLabel?: string;
29
31
  class?: string;
30
32
  }
31
33
 
32
- let { items = [], trigger, children, align = "right", class: className = "" }: Props = $props();
34
+ let {
35
+ items = [],
36
+ trigger,
37
+ children,
38
+ align = "right",
39
+ triggerLabel = "Options",
40
+ class: className = "",
41
+ }: Props = $props();
33
42
 
34
43
  let open = $state(false);
35
44
  let triggerEl = $state<HTMLElement | null>(null);
@@ -116,7 +125,8 @@
116
125
  toggle(!open);
117
126
  }}
118
127
  bind:this={triggerEl}
119
- aria-label="Options"
128
+ aria-label={triggerLabel}
129
+ aria-expanded={open}
120
130
  class="rounded-lg p-2 text-on-surface-muted transition-colors hover:bg-surface-sunken hover:text-on-surface focus:ring-2 focus:ring-outline focus:outline-none active:bg-surface-sunken"
121
131
  >
122
132
  {#if trigger}
@@ -186,6 +196,9 @@
186
196
  <Icon size={16} class="opacity-70" />
187
197
  {/if}
188
198
  {item.label}
199
+ {#if item.selected}
200
+ <Check size={16} class="ml-auto text-primary" />
201
+ {/if}
189
202
  </button>
190
203
  {/if}
191
204
  {/each}
@@ -147,7 +147,7 @@
147
147
 
148
148
  {#if error}
149
149
  <div
150
- class="pointer-events-none absolute top-1/2 right-3 -translate-y-1/2 text-danger-500"
150
+ class="pointer-events-none absolute top-1/2 right-3 -translate-y-1/2 text-danger-soft-text"
151
151
  >
152
152
  <CircleAlert size={18} />
153
153
  </div>
@@ -161,7 +161,9 @@
161
161
  {parsedHuman}
162
162
  </p>
163
163
  {:else if error}
164
- <p class="animate-in slide-in-from-top-1 fade-in text-sm text-danger-600 duration-200">
164
+ <p
165
+ class="animate-in slide-in-from-top-1 fade-in text-sm text-danger-soft-text duration-200"
166
+ >
165
167
  {error}
166
168
  </p>
167
169
  {:else if hint}
@@ -8,7 +8,7 @@
8
8
 
9
9
  interface Props {
10
10
  title?: string;
11
- description?: string;
11
+ description?: string | undefined;
12
12
  icon?: Component;
13
13
  iconSize?: number;
14
14
  actions?: Snippet;
@@ -32,7 +32,7 @@
32
32
  <label for={generatedId} class="block text-sm font-medium text-on-surface-muted">
33
33
  {label}
34
34
  {#if required}
35
- <span class="text-danger-500">*</span>
35
+ <span class="text-danger-soft-text">*</span>
36
36
  {/if}
37
37
  </label>
38
38
  {/if}
@@ -44,7 +44,9 @@
44
44
  {@render children()}
45
45
 
46
46
  {#if error}
47
- <p class="animate-in slide-in-from-top-1 fade-in text-sm text-danger-600 duration-200">
47
+ <p
48
+ class="animate-in slide-in-from-top-1 fade-in text-sm text-danger-soft-text duration-200"
49
+ >
48
50
  {error}
49
51
  </p>
50
52
  {:else if hint}
@@ -91,7 +91,7 @@
91
91
 
92
92
  {#if error}
93
93
  <div
94
- class="pointer-events-none absolute top-1/2 right-3 -translate-y-1/2 text-danger-500"
94
+ class="pointer-events-none absolute top-1/2 right-3 -translate-y-1/2 text-danger-soft-text"
95
95
  >
96
96
  <CircleAlert size={18} />
97
97
  </div>
@@ -105,7 +105,9 @@
105
105
  </div>
106
106
 
107
107
  {#if error}
108
- <p class="animate-in slide-in-from-top-1 fade-in text-sm text-danger-600 duration-200">
108
+ <p
109
+ class="animate-in slide-in-from-top-1 fade-in text-sm text-danger-soft-text duration-200"
110
+ >
109
111
  {error}
110
112
  </p>
111
113
  {:else if hint}
@@ -3,27 +3,34 @@
3
3
 
4
4
  <script lang="ts">
5
5
  import { untrack } from "svelte";
6
- import Convert from "ansi-to-html";
6
+ import { ansiLineToHtml } 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
9
  import type { FetchLogsFn, LogEvent } from "../log-console/types.js";
10
10
  import { formatBytes } from "../utils/format.js";
11
11
 
12
- // Convert ANSI escape sequences in a single log line to safe HTML spans.
13
- // A fresh converter per call ensures no colour state leaks between
14
- // independently virtualised lines.
15
- function ansiLineToHtml(text: string): string {
16
- return new Convert({ escapeXML: true }).toHtml(text);
17
- }
18
-
19
12
  interface Props {
20
13
  fetchLogs?: FetchLogsFn;
21
14
  chunkSize?: number;
22
15
  lineHeight?: number;
23
16
  class?: string;
17
+ // highlightLine, when set, scrolls the console so the given absolute
18
+ // line number is visible and pulses a short-lived flash on that row.
19
+ // The pulse is one-shot: changing the prop to a new line restarts the
20
+ // animation; setting it to null clears immediately.
21
+ highlightLine?: number | null;
24
22
  }
25
23
 
26
- let { fetchLogs, chunkSize = 4096, lineHeight = 20, class: className = "" }: Props = $props();
24
+ let {
25
+ fetchLogs,
26
+ chunkSize = 4096,
27
+ lineHeight = 20,
28
+ class: className = "",
29
+ highlightLine = null,
30
+ }: Props = $props();
31
+
32
+ let flashLine = $state<number | null>(null);
33
+ let flashTimer: ReturnType<typeof setTimeout> | null = null;
27
34
 
28
35
  const cache = new LogCache();
29
36
  let fetcher: LogFetcher | null = $state(null);
@@ -202,6 +209,30 @@
202
209
  scrollTop = 0;
203
210
  }
204
211
 
212
+ // Search-hit deep-link: when highlightLine flips to a non-null number,
213
+ // scroll the viewport so that line is centred, and pulse a one-shot
214
+ // flash class for ~1.5s. Cleaning up the timer on prop change keeps a
215
+ // rapid sequence of jumps from leaking timers.
216
+ $effect(() => {
217
+ const target = highlightLine;
218
+ if (target === null || target === undefined) {
219
+ flashLine = null;
220
+ return;
221
+ }
222
+ if (!containerEl) return;
223
+ const targetY = lineTop(target) - containerHeight / 2 + lineHeight / 2;
224
+ const clamped = Math.max(0, Math.min(targetY, totalHeight - containerHeight));
225
+ containerEl.scrollTo({ top: clamped, behavior: "smooth" });
226
+ isAutoScroll = false;
227
+ userScrolledUp = true;
228
+ flashLine = target;
229
+ if (flashTimer !== null) clearTimeout(flashTimer);
230
+ flashTimer = setTimeout(() => {
231
+ flashLine = null;
232
+ flashTimer = null;
233
+ }, 1500);
234
+ });
235
+
205
236
  $effect(() => {
206
237
  if (!containerEl) return;
207
238
  containerHeight = containerEl.clientHeight;
@@ -218,13 +249,13 @@
218
249
  </script>
219
250
 
220
251
  <div
221
- class="log-console relative flex h-full w-full flex-col overflow-hidden bg-slate-950 font-mono text-sm {className}"
252
+ class="log-console relative flex h-full w-full flex-col overflow-hidden bg-mist-950 font-mono text-sm {className}"
222
253
  >
223
254
  <div bind:this={containerEl} class="flex-1 overflow-auto" onscroll={onScroll}>
224
255
  <div class="relative" style="height: {totalHeight}px; min-height: 100%;">
225
256
  {#if cache.firstAvailableLine > 0}
226
257
  <div
227
- class="absolute right-0 left-0 flex items-center bg-amber-950/60 text-amber-400"
258
+ class="absolute right-0 left-0 flex items-center bg-warning-700/20 text-warning-400"
228
259
  style="top: 0px; height: {lineHeight}px;"
229
260
  >
230
261
  <div
@@ -242,21 +273,24 @@
242
273
 
243
274
  {#each renderedLines as line (line.num)}
244
275
  <div
245
- class="log-line absolute right-0 left-0 flex items-center hover:bg-slate-900/50"
276
+ class="log-line absolute right-0 left-0 flex items-center hover:bg-mist-900/50 {flashLine ===
277
+ line.num
278
+ ? 'log-line--flash'
279
+ : ''}"
246
280
  style="top: {lineTop(line.num)}px; height: {lineHeight}px;"
247
281
  >
248
282
  <div
249
- class="flex-shrink-0 pr-3 text-right text-slate-600 select-none"
283
+ class="flex-shrink-0 pr-3 text-right text-mist-500 select-none"
250
284
  style="width: {gutterWidth}px;"
251
285
  >
252
286
  {line.num + 1}
253
287
  </div>
254
- <div class="flex-1 truncate pr-4 text-slate-300">
288
+ <div class="flex-1 truncate pr-4 text-mist-200">
255
289
  {#if line.text !== undefined}
256
290
  <!-- eslint-disable-next-line svelte/no-at-html-tags -->
257
291
  <span class="whitespace-pre">{@html ansiLineToHtml(line.text)}</span>
258
292
  {:else}
259
- <span class="text-slate-700 italic">Loading...</span>
293
+ <span class="text-mist-600 italic">Loading...</span>
260
294
  {/if}
261
295
  </div>
262
296
  </div>
@@ -271,7 +305,7 @@
271
305
  class="flex-shrink-0 pr-3 text-right select-none"
272
306
  style="width: {gutterWidth}px;"
273
307
  ></div>
274
- <div class="flex items-center gap-1 text-slate-500">
308
+ <div class="flex items-center gap-1 text-mist-400">
275
309
  <span class="dot" style="animation-delay: 0ms;"></span>
276
310
  <span class="dot" style="animation-delay: 150ms;"></span>
277
311
  <span class="dot" style="animation-delay: 300ms;"></span>
@@ -288,7 +322,7 @@
288
322
  )}px; height: {lineHeight}px;"
289
323
  >
290
324
  <div
291
- class="flex-shrink-0 pr-3 text-right text-slate-800 opacity-0 select-none"
325
+ class="flex-shrink-0 pr-3 text-right text-mist-700 opacity-0 select-none"
292
326
  style="width: {gutterWidth}px;"
293
327
  >
294
328
  ~
@@ -298,16 +332,16 @@
298
332
  {/if}
299
333
 
300
334
  {#if cache.totalLines === 0 && !fetcher?.isFetching}
301
- <div class="absolute inset-0 flex items-center justify-center text-slate-600">
335
+ <div class="absolute inset-0 flex items-center justify-center text-mist-500">
302
336
  <div class="text-center">
303
337
  <div class="mb-1 text-lg">No output yet</div>
304
- <div class="text-sm text-slate-700">Waiting for logs...</div>
338
+ <div class="text-sm text-mist-600">Waiting for logs...</div>
305
339
  </div>
306
340
  </div>
307
341
  {/if}
308
342
 
309
343
  {#if cache.totalLines === 0 && fetcher?.isFetching}
310
- <div class="absolute inset-0 flex items-center justify-center text-slate-600">
344
+ <div class="absolute inset-0 flex items-center justify-center text-mist-500">
311
345
  <div class="flex items-center gap-2">
312
346
  <span class="dot" style="animation-delay: 0ms;"></span>
313
347
  <span class="dot" style="animation-delay: 150ms;"></span>
@@ -322,11 +356,11 @@
322
356
  {#if userScrolledUp && cache.totalLines > 0}
323
357
  <button
324
358
  class="absolute right-4 bottom-12 z-10 flex items-center gap-2 rounded-full border
325
- border-slate-700 bg-slate-800
359
+ border-mist-700 bg-mist-800
326
360
  px-3 py-1.5 text-xs
327
- font-medium text-slate-300 shadow-lg
361
+ font-medium text-mist-200 shadow-lg
328
362
  transition-all duration-200 hover:scale-105
329
- hover:bg-slate-700 active:scale-95"
363
+ hover:bg-mist-700 active:scale-95"
330
364
  onclick={enableAutoScroll}
331
365
  >
332
366
  <svg
@@ -343,20 +377,20 @@
343
377
  {/if}
344
378
 
345
379
  <div
346
- class="flex flex-shrink-0 items-center justify-between border-t border-slate-800
347
- bg-slate-900/80 px-3 py-1.5 text-xs text-slate-500"
380
+ class="flex flex-shrink-0 items-center justify-between border-t border-mist-800
381
+ bg-mist-900/80 px-3 py-1.5 text-xs text-mist-400"
348
382
  >
349
383
  <div class="flex items-center gap-3">
350
384
  {#if isStreaming}
351
- <div class="flex items-center gap-1.5 text-aurora-400">
385
+ <div class="flex items-center gap-1.5 text-info">
352
386
  <div class="h-1.5 w-1.5 animate-pulse rounded-full bg-aurora-400"></div>
353
387
  Streaming
354
388
  </div>
355
389
  {:else if cache.finished}
356
- <span class="text-slate-600">Stream ended</span>
390
+ <span class="text-mist-500">Stream ended</span>
357
391
  {/if}
358
392
  {#if fetcher?.isFetching}
359
- <span class="text-slate-600">Fetching...</span>
393
+ <span class="text-mist-500">Fetching...</span>
360
394
  {/if}
361
395
  </div>
362
396
  <div class="flex items-center gap-4">
@@ -365,7 +399,7 @@
365
399
  <span>{formatBytes(cache.totalBytes)}</span>
366
400
  {/if}
367
401
  {#if isAutoScroll}
368
- <span class="text-aurora-500">Auto-scroll</span>
402
+ <span class="text-info">Auto-scroll</span>
369
403
  {/if}
370
404
  </div>
371
405
  </div>
@@ -376,6 +410,19 @@
376
410
  --log-line-height: 20px;
377
411
  }
378
412
 
413
+ .log-line--flash {
414
+ animation: log-line-flash 1.5s ease-out;
415
+ }
416
+
417
+ @keyframes log-line-flash {
418
+ 0% {
419
+ background-color: oklch(0.8 0.18 70 / 0.45);
420
+ }
421
+ 100% {
422
+ background-color: transparent;
423
+ }
424
+ }
425
+
379
426
  .dot {
380
427
  display: inline-block;
381
428
  width: 4px;
@@ -28,10 +28,10 @@
28
28
  const percentage = $derived(Math.min(Math.max((value / max) * 100, 0), 100));
29
29
 
30
30
  const variantClasses: Record<ProgressVariant, string> = {
31
- default: "bg-wisp-500",
32
- success: "bg-success-500",
33
- warning: "bg-warning-500",
34
- danger: "bg-danger-500",
31
+ default: "bg-primary",
32
+ success: "bg-success-surface",
33
+ warning: "bg-warning-surface",
34
+ danger: "bg-danger-surface",
35
35
  };
36
36
 
37
37
  const sizeClasses: Record<ProgressSize, string> = {
@@ -44,7 +44,9 @@
44
44
  </div>
45
45
 
46
46
  {#if error}
47
- <p class="animate-in slide-in-from-top-1 fade-in text-sm text-danger-600 duration-200">
47
+ <p
48
+ class="animate-in slide-in-from-top-1 fade-in text-sm text-danger-soft-text duration-200"
49
+ >
48
50
  {error}
49
51
  </p>
50
52
  {:else if hint}
@@ -385,7 +385,7 @@
385
385
  {/if}
386
386
 
387
387
  {#if error}
388
- <p class="animate-in slide-in-from-top-1 fade-in mt-1.5 text-sm text-danger-600">
388
+ <p class="animate-in slide-in-from-top-1 fade-in mt-1.5 text-sm text-danger-soft-text">
389
389
  {error}
390
390
  </p>
391
391
  {:else if hint}
@@ -6,14 +6,19 @@
6
6
  data: number[];
7
7
  color?: string;
8
8
  fillColor?: string;
9
+ fillOpacity?: number;
9
10
  height?: number;
10
11
  class?: string;
11
12
  }
12
13
 
14
+ // `currentColor` by default so the caller can theme us with a Tailwind
15
+ // text-* class — that's the only way to flip on .dark without burning a
16
+ // hardcoded hex in here.
13
17
  let {
14
18
  data,
15
- color = "#6366f1",
19
+ color = "currentColor",
16
20
  fillColor,
21
+ fillOpacity = 0.12,
17
22
  height = 40,
18
23
  class: className = "",
19
24
  }: Props = $props();
@@ -42,7 +47,10 @@
42
47
  return `${pathD} L${lastX.toFixed(1)},${height} L0,${height} Z`;
43
48
  });
44
49
 
45
- let resolvedFill = $derived(fillColor ?? color + "18");
50
+ // Use `fill-opacity` rather than appending "18" to a hex — that trick
51
+ // breaks for `currentColor` / `oklch()` / `var(...)` values.
52
+ let resolvedFill = $derived(fillColor ?? color);
53
+ let resolvedFillOpacity = $derived(fillColor === undefined ? fillOpacity : 1);
46
54
  </script>
47
55
 
48
56
  <svg
@@ -52,7 +60,7 @@
52
60
  style="height: {height}px"
53
61
  >
54
62
  {#if data.length >= 2}
55
- <path d={fillD} fill={resolvedFill} />
63
+ <path d={fillD} fill={resolvedFill} fill-opacity={resolvedFillOpacity} />
56
64
  <path d={pathD} fill="none" stroke={color} stroke-width="1.5" stroke-linejoin="round" />
57
65
  {/if}
58
66
  </svg>
@@ -16,15 +16,15 @@
16
16
  let { accent = "wisp", onclick, children, class: className = "" }: Props = $props();
17
17
 
18
18
  const accentClasses: Record<TaskCardAccent, string> = {
19
- danger: "hover:border-danger-200",
20
- wisp: "hover:border-wisp-200",
21
- aurora: "hover:border-aurora-200",
19
+ danger: "hover:border-danger-soft-border",
20
+ wisp: "hover:border-primary-soft-border",
21
+ aurora: "hover:border-info-soft-border",
22
22
  };
23
23
  </script>
24
24
 
25
25
  <button
26
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[
27
+ class="group w-full rounded-lg border border-outline-faint bg-surface-raised p-3 text-left transition-all hover:shadow-sm {accentClasses[
28
28
  accent
29
29
  ]} {className}"
30
30
  {onclick}