@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.
- package/package.json +5 -6
- 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 +37 -34
- 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/Skeleton.svelte +6 -6
- 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 +141 -67
- package/src/lib/components/dashboard/RunFilterPopover.svelte +7 -16
- package/src/lib/components/dashboard/RunsList.svelte +57 -37
- 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/Button.stories.svelte +2 -2
- package/src/stories/CodeBlock.stories.svelte +55 -0
- package/src/stories/DataGrid.stories.svelte +2 -7
- package/src/stories/Dropdown.stories.svelte +3 -3
- 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
|
@@ -16,9 +16,11 @@
|
|
|
16
16
|
</script>
|
|
17
17
|
|
|
18
18
|
<script lang="ts">
|
|
19
|
-
import { tick
|
|
19
|
+
import { tick } from "svelte";
|
|
20
20
|
import { ChevronDown, Check, Search } from "@lucide/svelte";
|
|
21
|
-
import {
|
|
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 (
|
|
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-
|
|
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-
|
|
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-
|
|
54
|
-
<div class="h-4 w-72 rounded-[3px] bg-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
+
align?: CellAlign | undefined;
|
|
17
|
+
density?: CellDensity;
|
|
18
|
+
children?: Snippet;
|
|
13
19
|
class?: string;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
|
-
let {
|
|
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="
|
|
30
|
-
?
|
|
31
|
-
: '
|
|
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=
|
|
16
|
-
|
|
19
|
+
<thead class={className}>
|
|
20
|
+
{#if row}
|
|
21
|
+
<TableRow>
|
|
22
|
+
{@render children()}
|
|
23
|
+
</TableRow>
|
|
24
|
+
{:else}
|
|
17
25
|
{@render children()}
|
|
18
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
33
|
+
<tr
|
|
34
|
+
class="{borderClasses[border]} {hoverable ? 'hover:bg-surface-sunken' : ''} {className}"
|
|
35
|
+
{...restProps}
|
|
36
|
+
>
|
|
17
37
|
{@render children()}
|
|
18
38
|
</tr>
|
|
@@ -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
|
+
}
|