@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
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
3
|
|
|
4
4
|
<script lang="ts">
|
|
5
|
+
import { getContext } from "svelte";
|
|
6
|
+
import { RADIO_GROUP_KEY, type RadioGroupContext } from "./radio-group.js";
|
|
7
|
+
|
|
5
8
|
type RadioSize = "sm" | "md" | "lg";
|
|
6
9
|
|
|
7
10
|
interface Props {
|
|
8
11
|
value: string;
|
|
12
|
+
/** Selected value when used without a RadioGroup; inside one, the group owns it. */
|
|
9
13
|
groupValue?: string;
|
|
10
|
-
name
|
|
14
|
+
/** Defaults to the enclosing RadioGroup's name. */
|
|
15
|
+
name?: string;
|
|
11
16
|
label: string;
|
|
12
17
|
description?: string;
|
|
13
18
|
disabled?: boolean;
|
|
@@ -32,7 +37,14 @@
|
|
|
32
37
|
lg: { radio: "h-6 w-6", dot: "h-2.5 w-2.5", text: "text-base" },
|
|
33
38
|
};
|
|
34
39
|
|
|
35
|
-
const
|
|
40
|
+
const group = getContext<RadioGroupContext | undefined>(RADIO_GROUP_KEY);
|
|
41
|
+
|
|
42
|
+
const isChecked = $derived((group ? group.value : groupValue) === value);
|
|
43
|
+
|
|
44
|
+
function select() {
|
|
45
|
+
if (group) group.value = value;
|
|
46
|
+
else groupValue = value;
|
|
47
|
+
}
|
|
36
48
|
</script>
|
|
37
49
|
|
|
38
50
|
<label
|
|
@@ -43,11 +55,11 @@
|
|
|
43
55
|
<div class="relative flex shrink-0 items-center justify-center">
|
|
44
56
|
<input
|
|
45
57
|
type="radio"
|
|
46
|
-
{name}
|
|
58
|
+
name={name ?? group?.name}
|
|
47
59
|
{value}
|
|
48
60
|
{disabled}
|
|
49
61
|
checked={isChecked}
|
|
50
|
-
onchange={
|
|
62
|
+
onchange={select}
|
|
51
63
|
class="
|
|
52
64
|
peer {sizeClasses[size].radio}
|
|
53
65
|
cursor-pointer appearance-none rounded-full
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
3
|
|
|
4
4
|
<script lang="ts">
|
|
5
|
-
import type
|
|
5
|
+
import { setContext, type Snippet } from "svelte";
|
|
6
|
+
import { RADIO_GROUP_KEY, type RadioGroupContext } from "./radio-group.js";
|
|
6
7
|
|
|
7
8
|
type Orientation = "horizontal" | "vertical";
|
|
8
9
|
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
|
|
20
21
|
let {
|
|
21
22
|
value = $bindable(""),
|
|
22
|
-
name
|
|
23
|
+
name,
|
|
23
24
|
label,
|
|
24
25
|
error,
|
|
25
26
|
hint,
|
|
@@ -28,6 +29,18 @@
|
|
|
28
29
|
class: className = "",
|
|
29
30
|
}: Props = $props();
|
|
30
31
|
|
|
32
|
+
setContext<RadioGroupContext>(RADIO_GROUP_KEY, {
|
|
33
|
+
get name() {
|
|
34
|
+
return name;
|
|
35
|
+
},
|
|
36
|
+
get value() {
|
|
37
|
+
return value;
|
|
38
|
+
},
|
|
39
|
+
set value(next: string) {
|
|
40
|
+
value = next;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
31
44
|
const orientationClasses: Record<Orientation, string> = {
|
|
32
45
|
horizontal: "flex flex-row flex-wrap gap-x-6 gap-y-3",
|
|
33
46
|
vertical: "flex flex-col gap-3",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
// External writes to `value` (Reset / clear-all) win immediately: adopt them
|
|
42
42
|
// and drop any pending debounce. Depends only on `value` (raw is read
|
|
43
43
|
// untracked), so typing never re-enters here. Tiny race: a reset that leaves
|
|
44
|
-
// `value` unchanged ("" -> "") can't cancel a first-keystroke debounce
|
|
44
|
+
// `value` unchanged ("" -> "") can't cancel a first-keystroke debounce,
|
|
45
45
|
// negligible at 250ms. ponytail: guarded two-way sync, fine for one field.
|
|
46
46
|
$effect(() => {
|
|
47
47
|
const v = value;
|
|
@@ -16,10 +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 {
|
|
22
|
-
import {
|
|
21
|
+
import { floating, isOutsideClick } from "../actions/floating.js";
|
|
22
|
+
import { portal } from "../actions/portal.js";
|
|
23
|
+
import { trapFocus } from "../actions/focusTrap.js";
|
|
23
24
|
|
|
24
25
|
type SelectSize = "sm" | "md" | "lg";
|
|
25
26
|
|
|
@@ -32,13 +33,15 @@
|
|
|
32
33
|
searchable?: boolean;
|
|
33
34
|
size?: SelectSize;
|
|
34
35
|
error?: string;
|
|
35
|
-
hint?: string;
|
|
36
|
+
hint?: string | undefined;
|
|
36
37
|
class?: string;
|
|
37
38
|
id?: string;
|
|
38
39
|
name?: string;
|
|
39
40
|
onchange?: (value: unknown) => void;
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
const uid = $props.id();
|
|
44
|
+
|
|
42
45
|
let {
|
|
43
46
|
value = $bindable(),
|
|
44
47
|
options = [],
|
|
@@ -50,7 +53,7 @@
|
|
|
50
53
|
error,
|
|
51
54
|
hint,
|
|
52
55
|
class: className = "",
|
|
53
|
-
id = `select-${
|
|
56
|
+
id = `select-${uid}`,
|
|
54
57
|
name,
|
|
55
58
|
onchange,
|
|
56
59
|
onsearch,
|
|
@@ -62,7 +65,6 @@
|
|
|
62
65
|
let menuEl = $state<HTMLElement | null>(null);
|
|
63
66
|
let searchInputEl = $state<HTMLInputElement | null>(null);
|
|
64
67
|
let focusedIndex = $state(-1);
|
|
65
|
-
let cleanupFloating = $state<(() => void) | null>(null);
|
|
66
68
|
|
|
67
69
|
$effect(() => {
|
|
68
70
|
if (searchable && onsearch) {
|
|
@@ -107,31 +109,6 @@
|
|
|
107
109
|
return list;
|
|
108
110
|
});
|
|
109
111
|
|
|
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
112
|
function toggle() {
|
|
136
113
|
if (disabled) return;
|
|
137
114
|
isOpen = !isOpen;
|
|
@@ -141,14 +118,10 @@
|
|
|
141
118
|
if (focusedIndex === -1) focusedIndex = 0;
|
|
142
119
|
|
|
143
120
|
void tick().then(() => {
|
|
144
|
-
void setupFloating();
|
|
145
121
|
if (searchable && searchInputEl) {
|
|
146
122
|
searchInputEl.focus();
|
|
147
123
|
}
|
|
148
124
|
});
|
|
149
|
-
} else {
|
|
150
|
-
cleanupFloating?.();
|
|
151
|
-
cleanupFloating = null;
|
|
152
125
|
}
|
|
153
126
|
}
|
|
154
127
|
|
|
@@ -157,7 +130,6 @@
|
|
|
157
130
|
value = option.value;
|
|
158
131
|
onchange?.(value);
|
|
159
132
|
isOpen = false;
|
|
160
|
-
cleanupFloating?.();
|
|
161
133
|
}
|
|
162
134
|
|
|
163
135
|
function handleKeydown(e: KeyboardEvent) {
|
|
@@ -172,7 +144,6 @@
|
|
|
172
144
|
switch (e.key) {
|
|
173
145
|
case "Escape":
|
|
174
146
|
isOpen = false;
|
|
175
|
-
cleanupFloating?.();
|
|
176
147
|
triggerEl?.focus();
|
|
177
148
|
break;
|
|
178
149
|
case "ArrowDown":
|
|
@@ -212,18 +183,9 @@
|
|
|
212
183
|
}
|
|
213
184
|
|
|
214
185
|
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?.();
|
|
186
|
+
if (isOpen && isOutsideClick(e, triggerEl, menuEl)) isOpen = false;
|
|
221
187
|
}
|
|
222
188
|
|
|
223
|
-
onDestroy(() => {
|
|
224
|
-
cleanupFloating?.();
|
|
225
|
-
});
|
|
226
|
-
|
|
227
189
|
const sizeClasses = {
|
|
228
190
|
sm: "px-2.5 py-1.5 text-xs",
|
|
229
191
|
md: "px-3.5 py-2 text-sm",
|
|
@@ -303,6 +265,13 @@
|
|
|
303
265
|
<div
|
|
304
266
|
use:portal
|
|
305
267
|
use:trapFocus={{ autoFocus: false }}
|
|
268
|
+
use:floating={{
|
|
269
|
+
reference: triggerEl,
|
|
270
|
+
placement: "bottom-start",
|
|
271
|
+
offset: 6,
|
|
272
|
+
padding: 10,
|
|
273
|
+
matchWidth: true,
|
|
274
|
+
}}
|
|
306
275
|
bind:this={menuEl}
|
|
307
276
|
class="z-[9999] min-w-[200px]"
|
|
308
277
|
>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
// `currentColor` by default so the caller can theme us with a Tailwind
|
|
15
|
-
// text-* class
|
|
15
|
+
// text-* class, that's the only way to flip on .dark without burning a
|
|
16
16
|
// hardcoded hex in here.
|
|
17
17
|
let {
|
|
18
18
|
data,
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
return `${pathD} L${lastX.toFixed(1)},${height} L0,${height} Z`;
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
// Use `fill-opacity` rather than appending "18" to a hex
|
|
50
|
+
// Use `fill-opacity` rather than appending "18" to a hex, that trick
|
|
51
51
|
// breaks for `currentColor` / `oklch()` / `var(...)` values.
|
|
52
52
|
let resolvedFill = $derived(fillColor ?? color);
|
|
53
53
|
let resolvedFillOpacity = $derived(fillColor === undefined ? fillOpacity : 1);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<script lang="ts">
|
|
9
9
|
interface Props {
|
|
10
10
|
tone?: StatusDotTone;
|
|
11
|
-
/** Radar ping around the dot
|
|
11
|
+
/** Radar ping around the dot, use for "live" states only. */
|
|
12
12
|
pulse?: boolean;
|
|
13
13
|
size?: "sm" | "md" | "lg";
|
|
14
14
|
class?: string;
|
|
@@ -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>
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
{@render children()}
|
|
46
46
|
|
|
47
47
|
<!-- Hover intent: 100ms before it appears, so sweeping the pointer across a
|
|
48
|
-
dense row doesn't flash a trail of bubbles. Still no fade
|
|
48
|
+
dense row doesn't flash a trail of bubbles. Still no fade, `duration-0`
|
|
49
49
|
keeps the appearance instant once the delay is served, per DESIGN.md. -->
|
|
50
50
|
<div
|
|
51
51
|
class="
|
|
@@ -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
|
+
}
|