@runwisp/ui 1.3.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.
Files changed (73) hide show
  1. package/package.json +5 -6
  2. package/src/lib/actions/floating.ts +73 -0
  3. package/src/lib/actions/portal.ts +5 -1
  4. package/src/lib/components/Accordion.svelte +6 -2
  5. package/src/lib/components/AccordionItem.svelte +80 -23
  6. package/src/lib/components/Avatar.svelte +0 -1
  7. package/src/lib/components/CodeBlock.svelte +84 -35
  8. package/src/lib/components/CronInput.svelte +5 -2
  9. package/src/lib/components/DataGrid.svelte +95 -133
  10. package/src/lib/components/Drawer.svelte +2 -6
  11. package/src/lib/components/Dropdown.svelte +19 -57
  12. package/src/lib/components/EmptyState.svelte +2 -1
  13. package/src/lib/components/ErrorState.svelte +2 -1
  14. package/src/lib/components/Heading.svelte +19 -12
  15. package/src/lib/components/Kbd.svelte +25 -10
  16. package/src/lib/components/Link.svelte +7 -1
  17. package/src/lib/components/LogConsole.svelte +37 -34
  18. package/src/lib/components/Modal.svelte +44 -33
  19. package/src/lib/components/NotificationBell.svelte +2 -1
  20. package/src/lib/components/PageContainer.svelte +1 -10
  21. package/src/lib/components/PageHeader.svelte +8 -4
  22. package/src/lib/components/Popover.svelte +9 -51
  23. package/src/lib/components/Progress.svelte +19 -6
  24. package/src/lib/components/Radio.svelte +16 -4
  25. package/src/lib/components/RadioGroup.svelte +15 -2
  26. package/src/lib/components/Select.svelte +13 -45
  27. package/src/lib/components/Skeleton.svelte +6 -6
  28. package/src/lib/components/Table.svelte +13 -4
  29. package/src/lib/components/TableBody.svelte +2 -1
  30. package/src/lib/components/TableCell.svelte +38 -8
  31. package/src/lib/components/TableHead.svelte +12 -4
  32. package/src/lib/components/TableRow.svelte +23 -3
  33. package/src/lib/components/TaskCard.svelte +1 -1
  34. package/src/lib/components/TimezoneSelect.svelte +2 -2
  35. package/src/lib/components/accordion.svelte.ts +19 -0
  36. package/src/lib/components/dashboard/RunDetailPanel.svelte +141 -67
  37. package/src/lib/components/dashboard/RunFilterPopover.svelte +7 -16
  38. package/src/lib/components/dashboard/RunsList.svelte +57 -37
  39. package/src/lib/components/dashboard/cron-format.ts +1 -1
  40. package/src/lib/components/dashboard/run-filters.test.ts +2 -58
  41. package/src/lib/components/dashboard/run-filters.ts +5 -53
  42. package/src/lib/components/dashboard/run-helpers.test.ts +17 -7
  43. package/src/lib/components/dashboard/run-helpers.ts +12 -1
  44. package/src/lib/components/dashboard/status-config.ts +38 -83
  45. package/src/lib/components/dashboard/types.ts +0 -26
  46. package/src/lib/components/radio-group.ts +11 -0
  47. package/src/lib/index.ts +8 -43
  48. package/src/lib/log-console/LogFetcher.svelte.ts +3 -14
  49. package/src/lib/log-console/highlight-scroll.test.ts +59 -0
  50. package/src/lib/log-console/highlight-scroll.ts +41 -0
  51. package/src/lib/theme.css +10 -4
  52. package/src/lib/utils/clipboard.svelte.test.ts +92 -0
  53. package/src/lib/utils/clipboard.svelte.ts +73 -0
  54. package/src/lib/utils/format.ts +65 -94
  55. package/src/lib/utils/id.test.ts +3 -7
  56. package/src/lib/utils/id.ts +3 -2
  57. package/src/lib/utils/theme.svelte.test.ts +0 -6
  58. package/src/lib/utils/theme.svelte.ts +3 -5
  59. package/src/lib/utils/toast.svelte.ts +3 -11
  60. package/src/stories/Accordion.stories.svelte +61 -0
  61. package/src/stories/Button.stories.svelte +2 -2
  62. package/src/stories/CodeBlock.stories.svelte +55 -0
  63. package/src/stories/DataGrid.stories.svelte +2 -7
  64. package/src/stories/Dropdown.stories.svelte +3 -3
  65. package/src/stories/Heading.stories.svelte +31 -0
  66. package/src/stories/Kbd.stories.svelte +36 -0
  67. package/src/stories/Link.stories.svelte +38 -0
  68. package/src/stories/PageHeader.stories.svelte +31 -0
  69. package/src/stories/Progress.stories.svelte +26 -0
  70. package/src/stories/RadioGroup.stories.svelte +43 -0
  71. package/src/stories/Table.stories.svelte +69 -0
  72. package/src/svelte-shims.d.ts +0 -5
  73. package/src/lib/components/dashboard/status-config.test.ts +0 -18
@@ -16,9 +16,11 @@
16
16
  </script>
17
17
 
18
18
  <script lang="ts">
19
- import { tick, onDestroy } from "svelte";
19
+ import { tick } from "svelte";
20
20
  import { ChevronDown, Check, Search } from "@lucide/svelte";
21
- import { computePosition, autoUpdate, flip, shift, offset } from "@floating-ui/dom";
21
+ import { floating, isOutsideClick } from "../actions/floating.js";
22
+ import { portal } from "../actions/portal.js";
23
+ import { trapFocus } from "../actions/focusTrap.js";
22
24
  import { generateUlid } from "@runwisp/common";
23
25
 
24
26
  type SelectSize = "sm" | "md" | "lg";
@@ -32,7 +34,7 @@
32
34
  searchable?: boolean;
33
35
  size?: SelectSize;
34
36
  error?: string;
35
- hint?: string;
37
+ hint?: string | undefined;
36
38
  class?: string;
37
39
  id?: string;
38
40
  name?: string;
@@ -62,7 +64,6 @@
62
64
  let menuEl = $state<HTMLElement | null>(null);
63
65
  let searchInputEl = $state<HTMLInputElement | null>(null);
64
66
  let focusedIndex = $state(-1);
65
- let cleanupFloating = $state<(() => void) | null>(null);
66
67
 
67
68
  $effect(() => {
68
69
  if (searchable && onsearch) {
@@ -107,31 +108,6 @@
107
108
  return list;
108
109
  });
109
110
 
110
- import { portal } from "../actions/portal.js";
111
- import { trapFocus } from "../actions/focusTrap.js";
112
-
113
- async function setupFloating() {
114
- if (!triggerEl || !menuEl) return;
115
-
116
- menuEl.style.width = `${triggerEl.offsetWidth}px`;
117
-
118
- cleanupFloating = autoUpdate(triggerEl, menuEl, () => {
119
- if (!triggerEl || !menuEl) return;
120
- void computePosition(triggerEl, menuEl, {
121
- placement: "bottom-start",
122
- middleware: [offset(6), flip(), shift({ padding: 10 })],
123
- }).then(({ x, y }) => {
124
- if (menuEl) {
125
- Object.assign(menuEl.style, {
126
- left: `${x}px`,
127
- top: `${y}px`,
128
- position: "absolute",
129
- });
130
- }
131
- });
132
- });
133
- }
134
-
135
111
  function toggle() {
136
112
  if (disabled) return;
137
113
  isOpen = !isOpen;
@@ -141,14 +117,10 @@
141
117
  if (focusedIndex === -1) focusedIndex = 0;
142
118
 
143
119
  void tick().then(() => {
144
- void setupFloating();
145
120
  if (searchable && searchInputEl) {
146
121
  searchInputEl.focus();
147
122
  }
148
123
  });
149
- } else {
150
- cleanupFloating?.();
151
- cleanupFloating = null;
152
124
  }
153
125
  }
154
126
 
@@ -157,7 +129,6 @@
157
129
  value = option.value;
158
130
  onchange?.(value);
159
131
  isOpen = false;
160
- cleanupFloating?.();
161
132
  }
162
133
 
163
134
  function handleKeydown(e: KeyboardEvent) {
@@ -172,7 +143,6 @@
172
143
  switch (e.key) {
173
144
  case "Escape":
174
145
  isOpen = false;
175
- cleanupFloating?.();
176
146
  triggerEl?.focus();
177
147
  break;
178
148
  case "ArrowDown":
@@ -212,18 +182,9 @@
212
182
  }
213
183
 
214
184
  function handleOutsideClick(e: MouseEvent) {
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?.();
185
+ if (isOpen && isOutsideClick(e, triggerEl, menuEl)) isOpen = false;
221
186
  }
222
187
 
223
- onDestroy(() => {
224
- cleanupFloating?.();
225
- });
226
-
227
188
  const sizeClasses = {
228
189
  sm: "px-2.5 py-1.5 text-xs",
229
190
  md: "px-3.5 py-2 text-sm",
@@ -303,6 +264,13 @@
303
264
  <div
304
265
  use:portal
305
266
  use:trapFocus={{ autoFocus: false }}
267
+ use:floating={{
268
+ reference: triggerEl,
269
+ placement: "bottom-start",
270
+ offset: 6,
271
+ padding: 10,
272
+ matchWidth: true,
273
+ }}
306
274
  bind:this={menuEl}
307
275
  class="z-[9999] min-w-[200px]"
308
276
  >
@@ -30,7 +30,7 @@
30
30
 
31
31
  {#if shape === "circle"}
32
32
  <div
33
- class="animate-pulse rounded-full bg-surface-sunken {className}"
33
+ class="animate-pulse rounded-full bg-outline-hover {className}"
34
34
  style="width: {width ?? '2.5rem'}; height: {height ?? width ?? '2.5rem'}"
35
35
  role="status"
36
36
  aria-label="Loading"
@@ -39,7 +39,7 @@
39
39
  </div>
40
40
  {:else if shape === "rect"}
41
41
  <div
42
- class="animate-pulse rounded-[4px] bg-surface-sunken {className}"
42
+ class="animate-pulse rounded-[4px] bg-outline-hover {className}"
43
43
  style="width: {width ?? '100%'}; height: {height ?? '2rem'}"
44
44
  role="status"
45
45
  aria-label="Loading"
@@ -50,8 +50,8 @@
50
50
  <div class="animate-pulse space-y-6 p-6 {className}" role="status" aria-label="Loading content">
51
51
  {#if header}
52
52
  <div class="space-y-3">
53
- <div class="h-7 w-48 rounded-[4px] bg-surface-sunken"></div>
54
- <div class="h-4 w-72 rounded-[3px] bg-surface-sunken"></div>
53
+ <div class="h-7 w-48 rounded-[4px] bg-outline-hover"></div>
54
+ <div class="h-4 w-72 rounded-[3px] bg-outline-hover"></div>
55
55
  </div>
56
56
  {/if}
57
57
 
@@ -59,11 +59,11 @@
59
59
  {#each Array.from({ length: rows }, (_, i) => i) as i (i)}
60
60
  <div class="space-y-2">
61
61
  <div
62
- class="h-4 rounded-[3px] bg-surface-sunken"
62
+ class="h-4 rounded-[3px] bg-outline-hover"
63
63
  style="width: {70 + ((i * 17) % 30)}%"
64
64
  ></div>
65
65
  <div
66
- class="h-4 rounded-[3px] bg-surface-sunken"
66
+ class="h-4 rounded-[3px] bg-outline-hover"
67
67
  style="width: {40 + ((i * 23) % 40)}%"
68
68
  ></div>
69
69
  </div>
@@ -6,14 +6,23 @@
6
6
 
7
7
  interface Props {
8
8
  children: Snippet;
9
+ /** Render only the <table>, without the scrolling bordered frame, for a
10
+ * parent that supplies its own (as DataGrid does). */
11
+ bare?: boolean;
9
12
  class?: string;
10
13
  }
11
14
 
12
- let { children, class: className = "" }: Props = $props();
15
+ let { children, bare = false, class: className = "" }: Props = $props();
13
16
  </script>
14
17
 
15
- <div class="overflow-x-auto rounded-[4px] border border-outline {className}">
16
- <table class="w-full text-sm">
18
+ {#if bare}
19
+ <table class="w-full text-sm {className}">
17
20
  {@render children()}
18
21
  </table>
19
- </div>
22
+ {:else}
23
+ <div class="overflow-x-auto rounded-[4px] border border-outline {className}">
24
+ <table class="w-full text-sm">
25
+ {@render children()}
26
+ </table>
27
+ </div>
28
+ {/if}
@@ -12,6 +12,7 @@
12
12
  let { children, class: className = "" }: Props = $props();
13
13
  </script>
14
14
 
15
- <tbody class={className}>
15
+ <!-- The last row drops its divider: the frame below already draws that line. -->
16
+ <tbody class="[&>tr:last-child]:border-b-0 {className}">
16
17
  {@render children()}
17
18
  </tbody>
@@ -3,17 +3,30 @@
3
3
 
4
4
  <script lang="ts">
5
5
  import type { Snippet } from "svelte";
6
+ import type { HTMLTdAttributes, HTMLThAttributes } from "svelte/elements";
6
7
 
7
8
  type CellAlign = "left" | "center" | "right";
9
+ type CellDensity = "compact" | "default" | "comfortable" | "none";
8
10
 
9
- interface Props {
11
+ interface Props extends Omit<
12
+ HTMLTdAttributes & HTMLThAttributes,
13
+ "align" | "children" | "class"
14
+ > {
10
15
  header?: boolean;
11
- align?: CellAlign;
12
- children: Snippet;
16
+ align?: CellAlign | undefined;
17
+ density?: CellDensity;
18
+ children?: Snippet;
13
19
  class?: string;
14
20
  }
15
21
 
16
- let { header = false, align = "left", children, class: className = "" }: Props = $props();
22
+ let {
23
+ header = false,
24
+ align = "left",
25
+ density = "default",
26
+ children,
27
+ class: className = "",
28
+ ...restProps
29
+ }: Props = $props();
17
30
 
18
31
  const alignClasses: Record<CellAlign, string> = {
19
32
  left: "text-left",
@@ -21,14 +34,31 @@
21
34
  right: "text-right",
22
35
  };
23
36
 
37
+ const densityClasses: Record<CellDensity, string> = {
38
+ compact: "px-3 py-2",
39
+ default: "px-4 py-3",
40
+ comfortable: "px-5 py-3.5",
41
+ none: "",
42
+ };
43
+
44
+ // A header with aria-sort set lights up with a primary underline, so a
45
+ // sorted column is marked for sighted users and screen readers alike.
46
+ const headerClasses = `
47
+ font-mono text-xs tracking-[0.08em] text-on-surface-faint uppercase
48
+ aria-[sort=ascending]:text-on-surface aria-[sort=descending]:text-on-surface
49
+ aria-[sort=ascending]:shadow-[inset_0_-2px_0_var(--color-primary)]
50
+ aria-[sort=descending]:shadow-[inset_0_-2px_0_var(--color-primary)]
51
+ `;
52
+
24
53
  const tag = $derived(header ? "th" : "td");
25
54
  </script>
26
55
 
27
56
  <svelte:element
28
57
  this={tag}
29
- class="px-4 py-3 {alignClasses[align]} {header
30
- ? 'font-mono text-xs tracking-wide text-on-surface-faint uppercase'
31
- : 'text-on-surface'} {className}"
58
+ class="{densityClasses[density]} {alignClasses[align]} {header
59
+ ? headerClasses
60
+ : ''} {className}"
61
+ {...restProps}
32
62
  >
33
- {@render children()}
63
+ {@render children?.()}
34
64
  </svelte:element>
@@ -3,17 +3,25 @@
3
3
 
4
4
  <script lang="ts">
5
5
  import type { Snippet } from "svelte";
6
+ import TableRow from "./TableRow.svelte";
6
7
 
7
8
  interface Props {
8
9
  children: Snippet;
10
+ /** Wrap children in one header row. Turn off to pass your own
11
+ * TableRow(s), e.g. for a two-row header. */
12
+ row?: boolean;
9
13
  class?: string;
10
14
  }
11
15
 
12
- let { children, class: className = "" }: Props = $props();
16
+ let { children, row = true, class: className = "" }: Props = $props();
13
17
  </script>
14
18
 
15
- <thead class="bg-surface-sunken/50 {className}">
16
- <tr>
19
+ <thead class={className}>
20
+ {#if row}
21
+ <TableRow>
22
+ {@render children()}
23
+ </TableRow>
24
+ {:else}
17
25
  {@render children()}
18
- </tr>
26
+ {/if}
19
27
  </thead>
@@ -3,16 +3,36 @@
3
3
 
4
4
  <script lang="ts">
5
5
  import type { Snippet } from "svelte";
6
+ import type { HTMLAttributes } from "svelte/elements";
6
7
 
7
- interface Props {
8
+ type RowBorder = "default" | "faint" | "none";
9
+
10
+ interface Props extends Omit<HTMLAttributes<HTMLTableRowElement>, "children" | "class"> {
8
11
  children: Snippet;
9
12
  hoverable?: boolean;
13
+ /** Bottom divider: `default` under a header, `faint` between body rows. */
14
+ border?: RowBorder;
10
15
  class?: string;
11
16
  }
12
17
 
13
- let { children, hoverable = false, class: className = "" }: Props = $props();
18
+ let {
19
+ children,
20
+ hoverable = false,
21
+ border = "default",
22
+ class: className = "",
23
+ ...restProps
24
+ }: Props = $props();
25
+
26
+ const borderClasses: Record<RowBorder, string> = {
27
+ default: "border-b border-outline",
28
+ faint: "border-b border-outline-faint",
29
+ none: "",
30
+ };
14
31
  </script>
15
32
 
16
- <tr class="border-b border-outline {hoverable ? 'hover:bg-surface-sunken' : ''} {className}">
33
+ <tr
34
+ class="{borderClasses[border]} {hoverable ? 'hover:bg-surface-sunken' : ''} {className}"
35
+ {...restProps}
36
+ >
17
37
  {@render children()}
18
38
  </tr>
@@ -4,7 +4,7 @@
4
4
  <script lang="ts">
5
5
  import type { Snippet } from "svelte";
6
6
 
7
- export type TaskCardAccent = "danger" | "wisp" | "aurora";
7
+ type TaskCardAccent = "danger" | "wisp" | "aurora";
8
8
 
9
9
  interface Props {
10
10
  accent?: TaskCardAccent;
@@ -11,12 +11,12 @@
11
11
  label = "Timezone",
12
12
  disabled = false,
13
13
  hint,
14
- } = $props<{
14
+ }: {
15
15
  value: string;
16
16
  label?: string;
17
17
  disabled?: boolean;
18
18
  hint?: string;
19
- }>();
19
+ } = $props();
20
20
 
21
21
  type ZoneOption = {
22
22
  value: string;
@@ -0,0 +1,19 @@
1
+ // SPDX-FileCopyrightText: PoppyCake, s.r.o.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ export const ACCORDION_KEY = Symbol("accordion");
5
+
6
+ // Shared by an Accordion and its items. In single mode an item records itself
7
+ // as `active` when it opens, and every other item closes in response.
8
+ export class AccordionGroup {
9
+ active = $state<string | null>(null);
10
+ readonly #isSingle: () => boolean;
11
+
12
+ constructor(isSingle: () => boolean) {
13
+ this.#isSingle = isSingle;
14
+ }
15
+
16
+ get single(): boolean {
17
+ return this.#isSingle();
18
+ }
19
+ }