@svadmin/ui 0.58.0 → 0.59.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/dist/components/AICommandBar.svelte +1 -0
- package/dist/components/AutoTable.svelte +23 -18
- package/dist/components/CommandPalette.svelte +1 -0
- package/dist/components/EmptyState.svelte +4 -2
- package/dist/components/Layout.svelte +8 -1
- package/dist/components/StatsCard.svelte +5 -3
- package/dist/components/TaskQueueDrawer.svelte +10 -1
- package/dist/components/ui/input/input.svelte +2 -2
- package/package.json +1 -1
|
@@ -111,6 +111,7 @@
|
|
|
111
111
|
|
|
112
112
|
<Dialog.Dialog bind:open onOpenChange={(v: boolean) => !v && close()}>
|
|
113
113
|
<Dialog.DialogContent class="overflow-hidden p-0 sm:max-w-[600px] border-none shadow-2xl">
|
|
114
|
+
<Dialog.DialogTitle class="sr-only">{i18n.t('common.actions')}</Dialog.DialogTitle>
|
|
114
115
|
<Command.Root
|
|
115
116
|
shouldFilter={!aiMode}
|
|
116
117
|
class="flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground [&_[data-command-group-heading]]:text-muted-foreground [&_[data-command-group-heading]]:px-2 [&_[data-command-group-heading]]:py-1.5 [&_[data-command-group-heading]]:text-xs [&_[data-command-group-heading]]:font-semibold [&_[data-command-group-heading]]:uppercase [&_[data-command-group-heading]]:tracking-wider"
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
import * as DropdownMenu from './ui/dropdown-menu/index.js';
|
|
77
77
|
import * as PaginationUI from './ui/pagination/index.js';
|
|
78
78
|
import * as ContextMenu from './ui/context-menu/index.js';
|
|
79
|
+
import * as Select from './ui/select/index.js';
|
|
79
80
|
import {
|
|
80
81
|
Plus, Pencil, Trash2,
|
|
81
82
|
Search, Download, ChevronDown, ChevronUp, SlidersHorizontal, Filter as FilterIcon,
|
|
@@ -87,6 +88,7 @@
|
|
|
87
88
|
import TooltipButton from './TooltipButton.svelte';
|
|
88
89
|
import InlineEdit from './InlineEdit.svelte';
|
|
89
90
|
import DraggableHeader from './DraggableHeader.svelte';
|
|
91
|
+
import DataState from './content/DataState.svelte';
|
|
90
92
|
import type { Snippet } from 'svelte';
|
|
91
93
|
|
|
92
94
|
const i18n = useTranslation();
|
|
@@ -1172,17 +1174,17 @@
|
|
|
1172
1174
|
<div class="space-y-1">
|
|
1173
1175
|
<label class="text-xs text-muted-foreground" for="filter-{field.key}">{field.label}</label>
|
|
1174
1176
|
{#if field.type === "select" && field.options}
|
|
1175
|
-
<
|
|
1177
|
+
<Select.Root
|
|
1176
1178
|
id="filter-{field.key}"
|
|
1177
|
-
class="h-9 text-sm
|
|
1179
|
+
class="h-9 text-sm"
|
|
1178
1180
|
value={filterValues[field.key] ?? ""}
|
|
1179
|
-
onchange={(e) => setFilterValue(field.key, e.currentTarget.value)}
|
|
1181
|
+
onchange={(e) => setFilterValue(field.key, (e.currentTarget as HTMLSelectElement).value)}
|
|
1180
1182
|
>
|
|
1181
1183
|
<option value="">{i18n.t("common.all")}</option>
|
|
1182
1184
|
{#each field.options as opt, _i (_i)}
|
|
1183
1185
|
<option value={opt.value}>{opt.label}</option>
|
|
1184
1186
|
{/each}
|
|
1185
|
-
</
|
|
1187
|
+
</Select.Root>
|
|
1186
1188
|
{:else}
|
|
1187
1189
|
<Input
|
|
1188
1190
|
id="filter-{field.key}"
|
|
@@ -1275,12 +1277,12 @@
|
|
|
1275
1277
|
</div>
|
|
1276
1278
|
<div class="space-y-1">
|
|
1277
1279
|
<label class="text-xs text-muted-foreground" for="saved-list-view">{i18n.t("common.currentView")}</label>
|
|
1278
|
-
<
|
|
1280
|
+
<Select.Root
|
|
1279
1281
|
id="saved-list-view"
|
|
1280
|
-
class="h-9 w-full
|
|
1282
|
+
class="h-9 w-full"
|
|
1281
1283
|
value={activeSavedViewId ?? ""}
|
|
1282
1284
|
onchange={(event) => {
|
|
1283
|
-
const id = event.currentTarget.value;
|
|
1285
|
+
const id = (event.currentTarget as HTMLSelectElement).value;
|
|
1284
1286
|
const view = savedViews.find((candidate) => candidate.id === id);
|
|
1285
1287
|
if (view) applySavedView(view);
|
|
1286
1288
|
else {
|
|
@@ -1293,7 +1295,7 @@
|
|
|
1293
1295
|
{#each savedViews as view (view.id)}
|
|
1294
1296
|
<option value={view.id}>{view.name}</option>
|
|
1295
1297
|
{/each}
|
|
1296
|
-
</
|
|
1298
|
+
</Select.Root>
|
|
1297
1299
|
</div>
|
|
1298
1300
|
<div class="flex gap-2">
|
|
1299
1301
|
<Input
|
|
@@ -1409,9 +1411,11 @@
|
|
|
1409
1411
|
{/each}
|
|
1410
1412
|
</div>
|
|
1411
1413
|
{:else if query.error}
|
|
1412
|
-
<
|
|
1413
|
-
|
|
1414
|
-
|
|
1414
|
+
<DataState
|
|
1415
|
+
state="error"
|
|
1416
|
+
description={i18n.t('common.loadFailed', { message: (query.error as Error).message })}
|
|
1417
|
+
retry={() => listResult.refetch()}
|
|
1418
|
+
/>
|
|
1415
1419
|
{:else}
|
|
1416
1420
|
<div in:fade={{ duration: 150 }}>
|
|
1417
1421
|
<!-- Desktop Table (hidden on mobile) -->
|
|
@@ -1475,7 +1479,7 @@
|
|
|
1475
1479
|
{/each}
|
|
1476
1480
|
</Table.Header>
|
|
1477
1481
|
<Table.Body>
|
|
1478
|
-
{#each tableView.rows as row
|
|
1482
|
+
{#each tableView.rows as row (row.id)}
|
|
1479
1483
|
{@const record = row.original}
|
|
1480
1484
|
{@const id = record[primaryKey] as string | number}
|
|
1481
1485
|
{@const visibleCells = row_getVisibleCells(row).filter((cell) => isColumnVisible(cell.column.id))}
|
|
@@ -1702,7 +1706,7 @@
|
|
|
1702
1706
|
</div>
|
|
1703
1707
|
<!-- Card fields -->
|
|
1704
1708
|
<div class="space-y-2">
|
|
1705
|
-
{#each visibleFields.filter((field) => isColumnVisible(field.key)).slice(0, 6) as field
|
|
1709
|
+
{#each visibleFields.filter((field) => isColumnVisible(field.key)).slice(0, 6) as field (field.key)}
|
|
1706
1710
|
{@const value = record[field.key]}
|
|
1707
1711
|
<div class="flex items-start justify-between gap-4">
|
|
1708
1712
|
<span class="text-xs font-medium text-muted-foreground shrink-0">{field.label}</span>
|
|
@@ -1737,7 +1741,7 @@
|
|
|
1737
1741
|
{#if emptyState}
|
|
1738
1742
|
{@render emptyState()}
|
|
1739
1743
|
{:else}
|
|
1740
|
-
|
|
1744
|
+
<DataState state="empty" class="border-0 bg-transparent px-2 py-4" />
|
|
1741
1745
|
{/if}
|
|
1742
1746
|
</div>
|
|
1743
1747
|
{/each}
|
|
@@ -1751,11 +1755,12 @@
|
|
|
1751
1755
|
<div class="flex flex-col sm:flex-row items-center justify-between gap-3 px-1 py-2 text-sm text-muted-foreground">
|
|
1752
1756
|
<div class="flex shrink-0 items-center gap-3 leading-8">
|
|
1753
1757
|
<span class="shrink-0 whitespace-nowrap">{i18n.t('common.total', { total: query.data?.total ?? 0 })}</span>
|
|
1754
|
-
<
|
|
1755
|
-
|
|
1758
|
+
<Select.Root
|
|
1759
|
+
aria-label={i18n.t('common.perPage')}
|
|
1760
|
+
class="h-8 w-[78px] shrink-0"
|
|
1756
1761
|
value={String(pagination.pageSize ?? 10)}
|
|
1757
1762
|
onchange={(e) => {
|
|
1758
|
-
const size = Number((e.
|
|
1763
|
+
const size = Number((e.currentTarget as HTMLSelectElement).value);
|
|
1759
1764
|
if (!isNaN(size)) {
|
|
1760
1765
|
markSavedViewDirty();
|
|
1761
1766
|
pagination = { ...pagination, pageSize: size, current: 1 };
|
|
@@ -1766,7 +1771,7 @@
|
|
|
1766
1771
|
<option value="20">20</option>
|
|
1767
1772
|
<option value="50">50</option>
|
|
1768
1773
|
<option value="100">100</option>
|
|
1769
|
-
</
|
|
1774
|
+
</Select.Root>
|
|
1770
1775
|
</div>
|
|
1771
1776
|
<PaginationUI.Root>
|
|
1772
1777
|
<PaginationUI.Content>
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
|
|
46
46
|
<Dialog.Dialog bind:open>
|
|
47
47
|
<Dialog.DialogContent class="overflow-hidden p-0 sm:max-w-[560px]">
|
|
48
|
+
<Dialog.DialogTitle class="sr-only">{i18n.t('common.search')}</Dialog.DialogTitle>
|
|
48
49
|
<Command.Root class="flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground [&_[data-command-group-heading]]:text-muted-foreground [&_[data-command-group-heading]]:px-2 [&_[data-command-group-heading]]:py-1.5 [&_[data-command-group-heading]]:text-xs [&_[data-command-group-heading]]:font-semibold [&_[data-command-group-heading]]:uppercase [&_[data-command-group-heading]]:tracking-wider">
|
|
49
50
|
<div class="flex items-center border-b px-3" data-cmdk-input-wrapper="">
|
|
50
51
|
<Search class="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
|
@@ -17,8 +17,10 @@
|
|
|
17
17
|
</script>
|
|
18
18
|
|
|
19
19
|
<div
|
|
20
|
-
class="flex flex-col items-center justify-center rounded-lg bg-card/50 px-8 py-10 text-center shadow-
|
|
21
|
-
in:scale={{ start: 0.95, duration:
|
|
20
|
+
class="flex flex-col items-center justify-center rounded-lg bg-card/50 px-8 py-10 text-center shadow-xs"
|
|
21
|
+
in:scale={{ start: 0.95, duration: 200, opacity: 0 }}
|
|
22
|
+
role="status"
|
|
23
|
+
aria-live="polite"
|
|
22
24
|
>
|
|
23
25
|
<div class="h-20 w-20 rounded-full bg-muted flex items-center justify-center mb-4">
|
|
24
26
|
<PackageOpen class="h-10 w-10 text-muted-foreground" />
|
|
@@ -170,7 +170,14 @@
|
|
|
170
170
|
</div>
|
|
171
171
|
|
|
172
172
|
<!-- Mobile sidebar via Sheet -->
|
|
173
|
-
<Sheet.Root
|
|
173
|
+
<Sheet.Root
|
|
174
|
+
bind:open={mobileMenuOpen}
|
|
175
|
+
side="left"
|
|
176
|
+
role="dialog"
|
|
177
|
+
aria-modal="true"
|
|
178
|
+
aria-labelledby={`${layoutScope}-mobile-navigation-title`}
|
|
179
|
+
>
|
|
180
|
+
<Sheet.Title id={`${layoutScope}-mobile-navigation-title`} class="sr-only">{title}</Sheet.Title>
|
|
174
181
|
<div class="md:hidden">
|
|
175
182
|
<Sidebar collapsed={false} {identity} {title} {menu} {routeMode} onToggle={() => { mobileMenuOpen = false; }} onLogout={handleLogout} />
|
|
176
183
|
</div>
|
|
@@ -60,11 +60,13 @@
|
|
|
60
60
|
<p class="text-xl font-mono font-semibold text-foreground tracking-tight">{value}</p>
|
|
61
61
|
{#if trend}
|
|
62
62
|
<span
|
|
63
|
-
class="text-xs font-medium {trend.value >= 0 ? 'text-success' : 'text-destructive'}"
|
|
63
|
+
class="text-xs font-medium inline-flex items-center gap-0.5 {trend.value >= 0 ? 'text-success' : 'text-destructive'}"
|
|
64
|
+
aria-label="{trend.value >= 0 ? '上升' : '下降'} {Math.abs(trend.value)}%"
|
|
64
65
|
>
|
|
65
|
-
{trend.value >= 0 ? '↑' : '↓'}
|
|
66
|
+
<span aria-hidden="true">{trend.value >= 0 ? '↑' : '↓'}</span>
|
|
67
|
+
<span>{Math.abs(trend.value)}%</span>
|
|
66
68
|
{#if trend.label}
|
|
67
|
-
<span class="text-muted-foreground">{trend.label}</span>
|
|
69
|
+
<span class="text-muted-foreground ml-0.5">{trend.label}</span>
|
|
68
70
|
{/if}
|
|
69
71
|
</span>
|
|
70
72
|
{/if}
|
|
@@ -227,10 +227,19 @@
|
|
|
227
227
|
{/if}
|
|
228
228
|
</Button>
|
|
229
229
|
|
|
230
|
-
<Sheet.Root
|
|
230
|
+
<Sheet.Root
|
|
231
|
+
bind:open
|
|
232
|
+
side="right"
|
|
233
|
+
class="w-full max-w-6xl border-l border-border bg-card p-0"
|
|
234
|
+
role="dialog"
|
|
235
|
+
aria-modal="true"
|
|
236
|
+
aria-labelledby="svadmin-task-queue-title"
|
|
237
|
+
onClose={() => open = false}
|
|
238
|
+
>
|
|
231
239
|
<Sheet.Content class="h-full gap-0 p-0">
|
|
232
240
|
<div class="flex h-full flex-col">
|
|
233
241
|
<div class="border-b border-border bg-muted/30 px-4 py-3">
|
|
242
|
+
<Sheet.Title id="svadmin-task-queue-title" class="sr-only">{resolvedTitle}</Sheet.Title>
|
|
234
243
|
<div class="flex flex-wrap items-center gap-2">
|
|
235
244
|
<ListTodo class="h-5 w-5 text-muted-foreground" />
|
|
236
245
|
<h2 class="text-sm font-semibold">{resolvedTitle}</h2>
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
bind:this={ref}
|
|
33
33
|
data-slot={dataSlot}
|
|
34
34
|
class={cn(
|
|
35
|
-
"dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-9 rounded-lg border bg-transparent
|
|
35
|
+
"dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-9 rounded-lg border bg-transparent p-1 pr-3 text-xs md:text-sm text-muted-foreground transition-colors file:mr-3 file:inline-flex file:h-7 file:cursor-pointer file:items-center file:rounded-md file:border file:border-border/60 file:bg-muted file:px-2.5 file:text-xs file:font-medium file:text-foreground file:transition-colors hover:file:bg-muted/80 focus-visible:ring-3 aria-invalid:ring-3 placeholder:text-muted-foreground w-full min-w-0 outline-none cursor-pointer disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
36
36
|
className
|
|
37
37
|
)}
|
|
38
38
|
type="file"
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
bind:this={ref}
|
|
45
45
|
data-slot={dataSlot}
|
|
46
46
|
class={cn(
|
|
47
|
-
"dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-9 rounded-lg border bg-transparent px-3 py-1.5 text-base transition-colors
|
|
47
|
+
"dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-9 rounded-lg border bg-transparent px-3 py-1.5 text-base transition-colors focus-visible:ring-3 aria-invalid:ring-3 md:text-sm placeholder:text-muted-foreground w-full min-w-0 outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
48
48
|
className
|
|
49
49
|
)}
|
|
50
50
|
{type}
|