@orochibraru/ui 1.0.0 → 1.0.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/README.md +84 -3
- package/dist/components/blocks/accent-toggle.svelte +7 -3
- package/dist/components/blocks/accent-toggle.svelte.d.ts +1 -1
- package/dist/components/blocks/bottom-nav.svelte +1 -1
- package/dist/components/blocks/command-palette.svelte +268 -0
- package/dist/components/blocks/command-palette.svelte.d.ts +37 -0
- package/dist/components/blocks/error-page.svelte +23 -7
- package/dist/components/blocks/error-page.svelte.d.ts +4 -0
- package/dist/components/blocks/fab.svelte +1 -1
- package/dist/components/blocks/install-prompt.svelte +14 -8
- package/dist/components/blocks/install-prompt.svelte.d.ts +2 -1
- package/dist/components/blocks/music-player/labelled-slider.svelte +3 -12
- package/dist/components/blocks/music-player/labelled-slider.svelte.d.ts +1 -2
- package/dist/components/blocks/music-player.svelte +13 -12
- package/dist/components/blocks/otp-field.svelte +77 -33
- package/dist/components/blocks/otp-field.svelte.d.ts +7 -0
- package/dist/components/blocks/password-input.svelte +56 -16
- package/dist/components/blocks/password-input.svelte.d.ts +3 -0
- package/dist/components/blocks/stepper/stepper-phone-header.svelte +93 -0
- package/dist/components/blocks/stepper/stepper-phone-header.svelte.d.ts +13 -0
- package/dist/components/blocks/stepper.svelte +175 -0
- package/dist/components/blocks/stepper.svelte.d.ts +30 -0
- package/dist/components/blocks/time-range-picker.svelte +229 -0
- package/dist/components/blocks/time-range-picker.svelte.d.ts +33 -0
- package/dist/components/blocks/time-series-chart/data-table.svelte +43 -0
- package/dist/components/blocks/time-series-chart/data-table.svelte.d.ts +15 -0
- package/dist/components/blocks/time-series-chart/legend.svelte +23 -0
- package/dist/components/blocks/time-series-chart/legend.svelte.d.ts +10 -0
- package/dist/components/blocks/time-series-chart/marks.svelte +118 -0
- package/dist/components/blocks/time-series-chart/marks.svelte.d.ts +29 -0
- package/dist/components/blocks/time-series-chart/messages.d.ts +13 -0
- package/dist/components/blocks/time-series-chart/messages.js +12 -0
- package/dist/components/blocks/time-series-chart/readout.svelte +44 -0
- package/dist/components/blocks/time-series-chart/readout.svelte.d.ts +15 -0
- package/dist/components/blocks/time-series-chart/scale.d.ts +24 -0
- package/dist/components/blocks/time-series-chart/scale.js +136 -0
- package/dist/components/blocks/time-series-chart/time.d.ts +7 -0
- package/dist/components/blocks/time-series-chart/time.js +96 -0
- package/dist/components/blocks/time-series-chart.svelte +492 -0
- package/dist/components/blocks/time-series-chart.svelte.d.ts +18 -0
- package/dist/components/blocks/top-loading-bar.svelte +1 -1
- package/dist/components/blocks/video-player/center-controls.svelte +7 -7
- package/dist/components/blocks/video-player/control-bar.svelte +41 -35
- package/dist/components/blocks/video-player/control-button.svelte +1 -1
- package/dist/components/blocks/video-player/option-menu.svelte +1 -1
- package/dist/components/blocks/video-player/scrub-bar.svelte +39 -12
- package/dist/components/blocks/video-player/scrub-bar.svelte.d.ts +2 -0
- package/dist/components/blocks/video-player.svelte +2 -2
- package/dist/components/kit/accordion.svelte +70 -0
- package/dist/components/kit/accordion.svelte.d.ts +37 -0
- package/dist/components/kit/avatar-group.svelte +84 -0
- package/dist/components/kit/avatar-group.svelte.d.ts +14 -0
- package/dist/components/kit/bar-list.svelte +104 -0
- package/dist/components/kit/bar-list.svelte.d.ts +22 -0
- package/dist/components/kit/breadcrumbs.svelte +102 -0
- package/dist/components/kit/breadcrumbs.svelte.d.ts +14 -0
- package/dist/components/kit/checkbox-list.svelte +98 -0
- package/dist/components/kit/checkbox-list.svelte.d.ts +15 -0
- package/dist/components/kit/checkbox.svelte +84 -0
- package/dist/components/kit/checkbox.svelte.d.ts +8 -0
- package/dist/components/kit/confirm-dialog.svelte +127 -0
- package/dist/components/kit/confirm-dialog.svelte.d.ts +19 -0
- package/dist/components/kit/data-table.svelte +280 -0
- package/dist/components/kit/data-table.svelte.d.ts +46 -0
- package/dist/components/kit/date-field.svelte +210 -0
- package/dist/components/kit/date-field.svelte.d.ts +14 -0
- package/dist/components/kit/field-frame.svelte +71 -0
- package/dist/components/kit/field-frame.svelte.d.ts +26 -0
- package/dist/components/kit/fieldset-frame.svelte +69 -0
- package/dist/components/kit/fieldset-frame.svelte.d.ts +14 -0
- package/dist/components/kit/file-dropzone/files.d.ts +14 -0
- package/dist/components/kit/file-dropzone/files.js +69 -0
- package/dist/components/kit/file-dropzone.svelte +253 -0
- package/dist/components/kit/file-dropzone.svelte.d.ts +25 -0
- package/dist/components/kit/form.svelte +335 -0
- package/dist/components/kit/form.svelte.d.ts +61 -0
- package/dist/components/kit/input.svelte +73 -0
- package/dist/components/kit/input.svelte.d.ts +9 -0
- package/dist/components/kit/menu.svelte +163 -0
- package/dist/components/kit/menu.svelte.d.ts +28 -0
- package/dist/components/kit/pagination.svelte +134 -0
- package/dist/components/kit/pagination.svelte.d.ts +17 -0
- package/dist/components/kit/radio-list.svelte +131 -0
- package/dist/components/kit/radio-list.svelte.d.ts +15 -0
- package/dist/components/kit/segmented-control/segment.svelte +47 -0
- package/dist/components/kit/segmented-control/segment.svelte.d.ts +11 -0
- package/dist/components/kit/segmented-control.svelte +70 -0
- package/dist/components/kit/segmented-control.svelte.d.ts +16 -0
- package/dist/components/kit/select.svelte +244 -0
- package/dist/components/kit/select.svelte.d.ts +26 -0
- package/dist/components/kit/status-badge.svelte +54 -0
- package/dist/components/kit/status-badge.svelte.d.ts +31 -0
- package/dist/components/kit/switch.svelte +84 -0
- package/dist/components/kit/switch.svelte.d.ts +8 -0
- package/dist/components/kit/tabs.svelte +86 -0
- package/dist/components/kit/tabs.svelte.d.ts +20 -0
- package/dist/components/kit/textarea.svelte +92 -0
- package/dist/components/kit/textarea.svelte.d.ts +11 -0
- package/dist/components/ui/drawer/drawer-content.svelte +1 -1
- package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
- package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
- package/dist/components/ui/sonner/sonner.svelte +2 -4
- package/dist/mcp-content.json +1 -1
- package/dist/mcp.js +11 -11
- package/dist/server/hooks.d.ts +9 -0
- package/dist/server/hooks.js +41 -0
- package/dist/server/logger.d.ts +39 -0
- package/dist/server/logger.js +144 -0
- package/dist/server/telemetry.d.ts +23 -0
- package/dist/server/telemetry.js +82 -0
- package/dist/theme.css +18 -6
- package/package.json +40 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type FieldProps } from "./field-frame.svelte";
|
|
2
|
+
interface Option {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
type Props = FieldProps & {
|
|
9
|
+
options: readonly Option[];
|
|
10
|
+
value?: string[];
|
|
11
|
+
orientation?: "vertical" | "horizontal";
|
|
12
|
+
};
|
|
13
|
+
declare const CheckboxList: import("svelte").Component<Props, {}, "value">;
|
|
14
|
+
type CheckboxList = ReturnType<typeof CheckboxList>;
|
|
15
|
+
export default CheckboxList;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Checkbox } from "../ui/checkbox/index.js";
|
|
3
|
+
import * as Field from "../ui/field/index.js";
|
|
4
|
+
import { cn } from "../../utils.js";
|
|
5
|
+
|
|
6
|
+
import { errorList, type FieldProps } from "./field-frame.svelte";
|
|
7
|
+
|
|
8
|
+
type Props = FieldProps & {
|
|
9
|
+
checked?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
checked = $bindable(false),
|
|
14
|
+
name,
|
|
15
|
+
label,
|
|
16
|
+
description,
|
|
17
|
+
error,
|
|
18
|
+
required = false,
|
|
19
|
+
disabled = false,
|
|
20
|
+
id,
|
|
21
|
+
class: className,
|
|
22
|
+
}: Props = $props();
|
|
23
|
+
|
|
24
|
+
const generatedId = $props.id();
|
|
25
|
+
const checkboxId = $derived(id ?? generatedId);
|
|
26
|
+
const errors = $derived(errorList(error));
|
|
27
|
+
const invalid = $derived(errors.length > 0);
|
|
28
|
+
const describedBy = $derived(
|
|
29
|
+
invalid
|
|
30
|
+
? `${checkboxId}-error`
|
|
31
|
+
: description
|
|
32
|
+
? `${checkboxId}-description`
|
|
33
|
+
: undefined,
|
|
34
|
+
);
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<div
|
|
38
|
+
class={cn("flex w-full flex-col", className)}
|
|
39
|
+
data-invalid={invalid || undefined}
|
|
40
|
+
>
|
|
41
|
+
<div
|
|
42
|
+
class="group/row relative flex min-h-11 items-start gap-3 py-2.5 has-disabled:opacity-50"
|
|
43
|
+
>
|
|
44
|
+
<Checkbox
|
|
45
|
+
id={checkboxId}
|
|
46
|
+
{name}
|
|
47
|
+
{required}
|
|
48
|
+
{disabled}
|
|
49
|
+
bind:checked
|
|
50
|
+
aria-invalid={invalid || undefined}
|
|
51
|
+
aria-describedby={describedBy}
|
|
52
|
+
class="z-10 size-5"
|
|
53
|
+
/>
|
|
54
|
+
<span class="min-w-0 flex-1">
|
|
55
|
+
{#if label}
|
|
56
|
+
<label
|
|
57
|
+
for={checkboxId}
|
|
58
|
+
class={cn(
|
|
59
|
+
"block cursor-pointer text-sm leading-snug font-medium after:absolute after:inset-0 group-has-disabled/row:cursor-not-allowed",
|
|
60
|
+
invalid && "text-destructive",
|
|
61
|
+
)}
|
|
62
|
+
>
|
|
63
|
+
{label}
|
|
64
|
+
{#if required}
|
|
65
|
+
<span class="text-destructive" aria-hidden="true">*</span>
|
|
66
|
+
{/if}
|
|
67
|
+
</label>
|
|
68
|
+
{/if}
|
|
69
|
+
{#if description && !invalid}
|
|
70
|
+
<span
|
|
71
|
+
id="{checkboxId}-description"
|
|
72
|
+
class="text-muted-foreground mt-0.5 block text-sm">{description}</span
|
|
73
|
+
>
|
|
74
|
+
{/if}
|
|
75
|
+
</span>
|
|
76
|
+
</div>
|
|
77
|
+
{#if invalid}
|
|
78
|
+
<Field.Error
|
|
79
|
+
id="{checkboxId}-error"
|
|
80
|
+
class="pl-8"
|
|
81
|
+
errors={errors.map((message) => ({ message }))}
|
|
82
|
+
/>
|
|
83
|
+
{/if}
|
|
84
|
+
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Checkbox } from "../ui/checkbox/index.js";
|
|
2
|
+
import { type FieldProps } from "./field-frame.svelte";
|
|
3
|
+
type Props = FieldProps & {
|
|
4
|
+
checked?: boolean;
|
|
5
|
+
};
|
|
6
|
+
declare const Checkbox: import("svelte").Component<Props, {}, "checked">;
|
|
7
|
+
type Checkbox = ReturnType<typeof Checkbox>;
|
|
8
|
+
export default Checkbox;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import { MediaQuery } from "svelte/reactivity";
|
|
4
|
+
|
|
5
|
+
import * as AlertDialog from "../ui/alert-dialog/index.js";
|
|
6
|
+
import { Button } from "../ui/button/index.js";
|
|
7
|
+
import * as Drawer from "../ui/drawer/index.js";
|
|
8
|
+
import { Spinner } from "../ui/spinner/index.js";
|
|
9
|
+
|
|
10
|
+
const defaultMessages = {
|
|
11
|
+
confirm: "Confirm",
|
|
12
|
+
cancel: "Cancel",
|
|
13
|
+
working: "Working…",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
open?: boolean;
|
|
18
|
+
title: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
confirmLabel?: string;
|
|
21
|
+
cancelLabel?: string;
|
|
22
|
+
destructive?: boolean;
|
|
23
|
+
/** A returned promise keeps the dialog open with a spinner until it settles: it closes on success and stays open on rejection, so report the error yourself. */
|
|
24
|
+
onconfirm: () => unknown;
|
|
25
|
+
children?: Snippet;
|
|
26
|
+
messages?: Partial<typeof defaultMessages>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
open = $bindable(false),
|
|
31
|
+
title,
|
|
32
|
+
description,
|
|
33
|
+
confirmLabel,
|
|
34
|
+
cancelLabel,
|
|
35
|
+
destructive = false,
|
|
36
|
+
onconfirm,
|
|
37
|
+
children,
|
|
38
|
+
messages = {},
|
|
39
|
+
}: Props = $props();
|
|
40
|
+
|
|
41
|
+
const desktop = new MediaQuery("(min-width: 768px)");
|
|
42
|
+
const text = $derived({ ...defaultMessages, ...messages });
|
|
43
|
+
|
|
44
|
+
let pending = $state(false);
|
|
45
|
+
|
|
46
|
+
function setOpen(next: boolean) {
|
|
47
|
+
if (!pending) {
|
|
48
|
+
open = next;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function confirm() {
|
|
53
|
+
const result = onconfirm();
|
|
54
|
+
if (!(result instanceof Promise)) {
|
|
55
|
+
open = false;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
pending = true;
|
|
59
|
+
const succeeded = await result.then(
|
|
60
|
+
() => true,
|
|
61
|
+
() => false,
|
|
62
|
+
);
|
|
63
|
+
pending = false;
|
|
64
|
+
if (succeeded) {
|
|
65
|
+
open = false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
{#snippet actions()}
|
|
71
|
+
<Button
|
|
72
|
+
variant="outline"
|
|
73
|
+
class="h-11 rounded-xl md:h-9"
|
|
74
|
+
disabled={pending}
|
|
75
|
+
onclick={() => setOpen(false)}
|
|
76
|
+
>
|
|
77
|
+
{cancelLabel ?? text.cancel}
|
|
78
|
+
</Button>
|
|
79
|
+
<Button
|
|
80
|
+
variant={destructive ? "destructive" : "default"}
|
|
81
|
+
class="h-11 rounded-xl md:h-9"
|
|
82
|
+
disabled={pending}
|
|
83
|
+
aria-busy={pending || undefined}
|
|
84
|
+
onclick={confirm}
|
|
85
|
+
>
|
|
86
|
+
{#if pending}
|
|
87
|
+
<Spinner aria-label={text.working} />
|
|
88
|
+
{/if}
|
|
89
|
+
{confirmLabel ?? text.confirm}
|
|
90
|
+
</Button>
|
|
91
|
+
{/snippet}
|
|
92
|
+
|
|
93
|
+
{#if desktop.current}
|
|
94
|
+
<AlertDialog.Root bind:open={() => open, setOpen}>
|
|
95
|
+
<AlertDialog.Content>
|
|
96
|
+
<AlertDialog.Header>
|
|
97
|
+
<AlertDialog.Title>{title}</AlertDialog.Title>
|
|
98
|
+
{#if description}
|
|
99
|
+
<AlertDialog.Description>{description}</AlertDialog.Description>
|
|
100
|
+
{/if}
|
|
101
|
+
</AlertDialog.Header>
|
|
102
|
+
{#if children}
|
|
103
|
+
<div class="grid gap-3">{@render children()}</div>
|
|
104
|
+
{/if}
|
|
105
|
+
<AlertDialog.Footer>
|
|
106
|
+
{@render actions()}
|
|
107
|
+
</AlertDialog.Footer>
|
|
108
|
+
</AlertDialog.Content>
|
|
109
|
+
</AlertDialog.Root>
|
|
110
|
+
{:else}
|
|
111
|
+
<Drawer.Root bind:open={() => open, setOpen} dismissible={!pending}>
|
|
112
|
+
<Drawer.Content>
|
|
113
|
+
<Drawer.Header class="px-5 text-left">
|
|
114
|
+
<Drawer.Title>{title}</Drawer.Title>
|
|
115
|
+
{#if description}
|
|
116
|
+
<Drawer.Description>{description}</Drawer.Description>
|
|
117
|
+
{/if}
|
|
118
|
+
</Drawer.Header>
|
|
119
|
+
{#if children}
|
|
120
|
+
<div class="grid gap-3 px-5">{@render children()}</div>
|
|
121
|
+
{/if}
|
|
122
|
+
<div class="flex flex-col-reverse gap-2 px-5 pt-4 pb-4">
|
|
123
|
+
{@render actions()}
|
|
124
|
+
</div>
|
|
125
|
+
</Drawer.Content>
|
|
126
|
+
</Drawer.Root>
|
|
127
|
+
{/if}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
declare const ConfirmDialog: import("svelte").Component<{
|
|
3
|
+
open?: boolean;
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
confirmLabel?: string;
|
|
7
|
+
cancelLabel?: string;
|
|
8
|
+
destructive?: boolean;
|
|
9
|
+
/** A returned promise keeps the dialog open with a spinner until it settles: it closes on success and stays open on rejection, so report the error yourself. */
|
|
10
|
+
onconfirm: () => unknown;
|
|
11
|
+
children?: Snippet;
|
|
12
|
+
messages?: Partial<{
|
|
13
|
+
confirm: string;
|
|
14
|
+
cancel: string;
|
|
15
|
+
working: string;
|
|
16
|
+
}>;
|
|
17
|
+
}, {}, "open">;
|
|
18
|
+
type ConfirmDialog = ReturnType<typeof ConfirmDialog>;
|
|
19
|
+
export default ConfirmDialog;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type DataTableColumn<Row extends object> = {
|
|
3
|
+
[Key in keyof Row & string]: {
|
|
4
|
+
key: Key;
|
|
5
|
+
label: string;
|
|
6
|
+
align?: "left" | "center" | "right";
|
|
7
|
+
format?: (value: Row[Key], row: Row) => string;
|
|
8
|
+
sortable?: boolean;
|
|
9
|
+
hideOnMobile?: boolean;
|
|
10
|
+
};
|
|
11
|
+
}[keyof Row & string];
|
|
12
|
+
|
|
13
|
+
function isMissing(value: unknown): boolean {
|
|
14
|
+
return value === null || value === undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function compareValues(left: unknown, right: unknown): number {
|
|
18
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
19
|
+
return left - right;
|
|
20
|
+
}
|
|
21
|
+
if (left instanceof Date && right instanceof Date) {
|
|
22
|
+
return left.getTime() - right.getTime();
|
|
23
|
+
}
|
|
24
|
+
return String(left).localeCompare(String(right), undefined, {
|
|
25
|
+
numeric: true,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<script lang="ts" generics="Row extends object">
|
|
31
|
+
import ArrowDownIcon from "@lucide/svelte/icons/arrow-down";
|
|
32
|
+
import ArrowUpIcon from "@lucide/svelte/icons/arrow-up";
|
|
33
|
+
import ChevronsUpDownIcon from "@lucide/svelte/icons/chevrons-up-down";
|
|
34
|
+
import type { Snippet } from "svelte";
|
|
35
|
+
|
|
36
|
+
import { Skeleton } from "../ui/skeleton/index.js";
|
|
37
|
+
import * as Table from "../ui/table/index.js";
|
|
38
|
+
import { cn } from "../../utils.js";
|
|
39
|
+
|
|
40
|
+
const defaultMessages = {
|
|
41
|
+
empty: "Nothing here yet.",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
interface Props {
|
|
45
|
+
columns: DataTableColumn<Row>[];
|
|
46
|
+
rows: Row[];
|
|
47
|
+
cell?: Snippet<[Row, DataTableColumn<Row>, string]>;
|
|
48
|
+
rowHref?: (row: Row) => string;
|
|
49
|
+
onrowclick?: (row: Row) => void;
|
|
50
|
+
loading?: boolean;
|
|
51
|
+
messages?: Partial<typeof defaultMessages>;
|
|
52
|
+
class?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const {
|
|
56
|
+
columns,
|
|
57
|
+
rows,
|
|
58
|
+
cell,
|
|
59
|
+
rowHref,
|
|
60
|
+
onrowclick,
|
|
61
|
+
loading = false,
|
|
62
|
+
messages = {},
|
|
63
|
+
class: className,
|
|
64
|
+
}: Props = $props();
|
|
65
|
+
|
|
66
|
+
const text = $derived({ ...defaultMessages, ...messages });
|
|
67
|
+
const interactive = $derived(Boolean(rowHref || onrowclick));
|
|
68
|
+
const firstColumn = $derived(columns[0]);
|
|
69
|
+
const detailColumns = $derived(
|
|
70
|
+
columns.slice(1).filter((column) => !column.hideOnMobile),
|
|
71
|
+
);
|
|
72
|
+
const placeholders = [0, 1, 2, 3, 4];
|
|
73
|
+
const alignment = {
|
|
74
|
+
left: "text-left",
|
|
75
|
+
center: "text-center",
|
|
76
|
+
right: "text-right",
|
|
77
|
+
};
|
|
78
|
+
const raisedControls =
|
|
79
|
+
"[&_a]:relative [&_a]:z-10 [&_button]:relative [&_button]:z-10";
|
|
80
|
+
|
|
81
|
+
let sortKey = $state<keyof Row & string>();
|
|
82
|
+
let descending = $state(false);
|
|
83
|
+
|
|
84
|
+
const sortedRows = $derived.by(() => {
|
|
85
|
+
const key = sortKey;
|
|
86
|
+
if (key === undefined) {
|
|
87
|
+
return rows;
|
|
88
|
+
}
|
|
89
|
+
return rows.toSorted((left, right) => {
|
|
90
|
+
const leftValue = left[key];
|
|
91
|
+
const rightValue = right[key];
|
|
92
|
+
if (isMissing(leftValue) || isMissing(rightValue)) {
|
|
93
|
+
return Number(isMissing(leftValue)) - Number(isMissing(rightValue));
|
|
94
|
+
}
|
|
95
|
+
const order = compareValues(leftValue, rightValue);
|
|
96
|
+
return descending ? -order : order;
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
function toggleSort(key: keyof Row & string) {
|
|
101
|
+
if (sortKey === key) {
|
|
102
|
+
descending = !descending;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
sortKey = key;
|
|
106
|
+
descending = false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function display(row: Row, column: DataTableColumn<Row>): string {
|
|
110
|
+
const value = row[column.key];
|
|
111
|
+
const format = column.format as
|
|
112
|
+
((value: unknown, row: Row) => string) | undefined;
|
|
113
|
+
if (format) {
|
|
114
|
+
return format(value, row);
|
|
115
|
+
}
|
|
116
|
+
return isMissing(value) ? "–" : String(value);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function ariaSort(column: DataTableColumn<Row>) {
|
|
120
|
+
if (!column.sortable || sortKey !== column.key) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
return descending ? "descending" : "ascending";
|
|
124
|
+
}
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
{#snippet content(row: Row, column: DataTableColumn<Row>)}
|
|
128
|
+
{#if cell}
|
|
129
|
+
{@render cell(row, column, display(row, column))}
|
|
130
|
+
{:else}
|
|
131
|
+
{display(row, column)}
|
|
132
|
+
{/if}
|
|
133
|
+
{/snippet}
|
|
134
|
+
|
|
135
|
+
{#snippet primary(row: Row, column: DataTableColumn<Row>)}
|
|
136
|
+
{#if rowHref}
|
|
137
|
+
<a
|
|
138
|
+
href={rowHref(row)}
|
|
139
|
+
class="font-medium outline-none after:absolute after:inset-0 after:rounded-[inherit] focus-visible:after:ring-3 focus-visible:after:ring-ring/50"
|
|
140
|
+
>
|
|
141
|
+
{@render content(row, column)}
|
|
142
|
+
</a>
|
|
143
|
+
{:else if onrowclick}
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
onclick={() => onrowclick(row)}
|
|
147
|
+
class="text-left font-medium outline-none after:absolute after:inset-0 after:rounded-[inherit] focus-visible:after:ring-3 focus-visible:after:ring-ring/50"
|
|
148
|
+
>
|
|
149
|
+
{@render content(row, column)}
|
|
150
|
+
</button>
|
|
151
|
+
{:else}
|
|
152
|
+
<span class="font-medium">{@render content(row, column)}</span>
|
|
153
|
+
{/if}
|
|
154
|
+
{/snippet}
|
|
155
|
+
|
|
156
|
+
{#if !loading && rows.length === 0}
|
|
157
|
+
<p
|
|
158
|
+
class={cn(
|
|
159
|
+
"glass text-muted-foreground w-full rounded-2xl px-4 py-10 text-center text-sm",
|
|
160
|
+
className,
|
|
161
|
+
)}
|
|
162
|
+
>
|
|
163
|
+
{text.empty}
|
|
164
|
+
</p>
|
|
165
|
+
{:else}
|
|
166
|
+
<div class={cn("w-full", className)} aria-busy={loading || undefined}>
|
|
167
|
+
<div class="glass hidden overflow-hidden rounded-2xl md:block">
|
|
168
|
+
<Table.Root>
|
|
169
|
+
<Table.Header>
|
|
170
|
+
<Table.Row class="hover:bg-transparent">
|
|
171
|
+
{#each columns as column, index (index)}
|
|
172
|
+
<Table.Head
|
|
173
|
+
aria-sort={ariaSort(column)}
|
|
174
|
+
class={cn(
|
|
175
|
+
"text-muted-foreground h-11 px-4 text-xs",
|
|
176
|
+
alignment[column.align ?? "left"],
|
|
177
|
+
)}
|
|
178
|
+
>
|
|
179
|
+
{#if column.sortable}
|
|
180
|
+
<button
|
|
181
|
+
type="button"
|
|
182
|
+
onclick={() => toggleSort(column.key)}
|
|
183
|
+
class="hover:text-foreground focus-visible:ring-ring/50 inline-flex items-center gap-1 rounded-sm font-medium outline-none focus-visible:ring-3"
|
|
184
|
+
>
|
|
185
|
+
{column.label}
|
|
186
|
+
{#if sortKey !== column.key}
|
|
187
|
+
<ChevronsUpDownIcon class="size-3.5 opacity-50" />
|
|
188
|
+
{:else if descending}
|
|
189
|
+
<ArrowDownIcon class="size-3.5" />
|
|
190
|
+
{:else}
|
|
191
|
+
<ArrowUpIcon class="size-3.5" />
|
|
192
|
+
{/if}
|
|
193
|
+
</button>
|
|
194
|
+
{:else}
|
|
195
|
+
{column.label}
|
|
196
|
+
{/if}
|
|
197
|
+
</Table.Head>
|
|
198
|
+
{/each}
|
|
199
|
+
</Table.Row>
|
|
200
|
+
</Table.Header>
|
|
201
|
+
<Table.Body>
|
|
202
|
+
{#if loading}
|
|
203
|
+
{#each placeholders as placeholder (placeholder)}
|
|
204
|
+
<Table.Row class="hover:bg-transparent">
|
|
205
|
+
{#each columns as column, index (index)}
|
|
206
|
+
<Table.Cell class="px-4 py-3">
|
|
207
|
+
<Skeleton class="h-4 w-full" />
|
|
208
|
+
</Table.Cell>
|
|
209
|
+
{/each}
|
|
210
|
+
</Table.Row>
|
|
211
|
+
{/each}
|
|
212
|
+
{:else}
|
|
213
|
+
{#each sortedRows as row, rowIndex (rowIndex)}
|
|
214
|
+
<Table.Row
|
|
215
|
+
class={cn(
|
|
216
|
+
"relative",
|
|
217
|
+
interactive ? "cursor-pointer" : "hover:bg-transparent",
|
|
218
|
+
)}
|
|
219
|
+
>
|
|
220
|
+
{#each columns as column, index (index)}
|
|
221
|
+
<Table.Cell
|
|
222
|
+
class={cn(
|
|
223
|
+
"tabular px-4 py-3",
|
|
224
|
+
alignment[column.align ?? "left"],
|
|
225
|
+
index > 0 && raisedControls,
|
|
226
|
+
)}
|
|
227
|
+
>
|
|
228
|
+
{#if index === 0}
|
|
229
|
+
{@render primary(row, column)}
|
|
230
|
+
{:else}
|
|
231
|
+
{@render content(row, column)}
|
|
232
|
+
{/if}
|
|
233
|
+
</Table.Cell>
|
|
234
|
+
{/each}
|
|
235
|
+
</Table.Row>
|
|
236
|
+
{/each}
|
|
237
|
+
{/if}
|
|
238
|
+
</Table.Body>
|
|
239
|
+
</Table.Root>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<ul class="flex flex-col gap-3 md:hidden">
|
|
243
|
+
{#if loading}
|
|
244
|
+
{#each placeholders as placeholder (placeholder)}
|
|
245
|
+
<li class="glass flex flex-col gap-3 rounded-2xl p-4">
|
|
246
|
+
<Skeleton class="h-5 w-1/2" />
|
|
247
|
+
<Skeleton class="h-4 w-full" />
|
|
248
|
+
</li>
|
|
249
|
+
{/each}
|
|
250
|
+
{:else if firstColumn}
|
|
251
|
+
{#each sortedRows as row, rowIndex (rowIndex)}
|
|
252
|
+
<li class="glass relative flex flex-col gap-2 rounded-2xl p-4">
|
|
253
|
+
<div class="min-w-0 wrap-break-word">
|
|
254
|
+
{@render primary(row, firstColumn)}
|
|
255
|
+
</div>
|
|
256
|
+
{#if detailColumns.length > 0}
|
|
257
|
+
<dl class="flex flex-col gap-1.5 text-sm">
|
|
258
|
+
{#each detailColumns as column, index (index)}
|
|
259
|
+
<div class="flex items-baseline justify-between gap-4">
|
|
260
|
+
<dt class="text-muted-foreground shrink-0">
|
|
261
|
+
{column.label}
|
|
262
|
+
</dt>
|
|
263
|
+
<dd
|
|
264
|
+
class={cn(
|
|
265
|
+
"tabular min-w-0 text-right wrap-break-word",
|
|
266
|
+
raisedControls,
|
|
267
|
+
)}
|
|
268
|
+
>
|
|
269
|
+
{@render content(row, column)}
|
|
270
|
+
</dd>
|
|
271
|
+
</div>
|
|
272
|
+
{/each}
|
|
273
|
+
</dl>
|
|
274
|
+
{/if}
|
|
275
|
+
</li>
|
|
276
|
+
{/each}
|
|
277
|
+
{/if}
|
|
278
|
+
</ul>
|
|
279
|
+
</div>
|
|
280
|
+
{/if}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type DataTableColumn<Row extends object> = {
|
|
2
|
+
[Key in keyof Row & string]: {
|
|
3
|
+
key: Key;
|
|
4
|
+
label: string;
|
|
5
|
+
align?: "left" | "center" | "right";
|
|
6
|
+
format?: (value: Row[Key], row: Row) => string;
|
|
7
|
+
sortable?: boolean;
|
|
8
|
+
hideOnMobile?: boolean;
|
|
9
|
+
};
|
|
10
|
+
}[keyof Row & string];
|
|
11
|
+
import type { Snippet } from "svelte";
|
|
12
|
+
declare function $$render<Row extends object>(): {
|
|
13
|
+
props: {
|
|
14
|
+
columns: DataTableColumn<Row>[];
|
|
15
|
+
rows: Row[];
|
|
16
|
+
cell?: Snippet<[Row, DataTableColumn<Row>, string]>;
|
|
17
|
+
rowHref?: (row: Row) => string;
|
|
18
|
+
onrowclick?: (row: Row) => void;
|
|
19
|
+
loading?: boolean;
|
|
20
|
+
messages?: Partial<{
|
|
21
|
+
empty: string;
|
|
22
|
+
}>;
|
|
23
|
+
class?: string;
|
|
24
|
+
};
|
|
25
|
+
exports: {};
|
|
26
|
+
bindings: "";
|
|
27
|
+
slots: {};
|
|
28
|
+
events: {};
|
|
29
|
+
};
|
|
30
|
+
declare class __sveltets_Render<Row extends object> {
|
|
31
|
+
props(): ReturnType<typeof $$render<Row>>['props'];
|
|
32
|
+
events(): ReturnType<typeof $$render<Row>>['events'];
|
|
33
|
+
slots(): ReturnType<typeof $$render<Row>>['slots'];
|
|
34
|
+
bindings(): "";
|
|
35
|
+
exports(): {};
|
|
36
|
+
}
|
|
37
|
+
interface $$IsomorphicComponent {
|
|
38
|
+
new <Row extends object>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Row>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Row>['props']>, ReturnType<__sveltets_Render<Row>['events']>, ReturnType<__sveltets_Render<Row>['slots']>> & {
|
|
39
|
+
$$bindings?: ReturnType<__sveltets_Render<Row>['bindings']>;
|
|
40
|
+
} & ReturnType<__sveltets_Render<Row>['exports']>;
|
|
41
|
+
<Row extends object>(internal: unknown, props: ReturnType<__sveltets_Render<Row>['props']> & {}): ReturnType<__sveltets_Render<Row>['exports']>;
|
|
42
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
43
|
+
}
|
|
44
|
+
declare const DataTable: $$IsomorphicComponent;
|
|
45
|
+
type DataTable<Row extends object> = InstanceType<typeof DataTable<Row>>;
|
|
46
|
+
export default DataTable;
|