@svadmin/ui 0.24.0 → 0.25.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
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import * as DropdownMenu from './ui/dropdown-menu/index.js';
|
|
29
29
|
import * as PaginationUI from './ui/pagination/index.js';
|
|
30
30
|
import * as ContextMenu from './ui/context-menu/index.js';
|
|
31
|
-
import {
|
|
31
|
+
import { Pagination } from './ui/pagination/index.js';
|
|
32
32
|
import {
|
|
33
33
|
Plus, Pencil, Trash2,
|
|
34
34
|
Search, Download, Upload, ChevronDown, ChevronUp, SlidersHorizontal, Filter as FilterIcon,
|
|
@@ -60,6 +60,8 @@
|
|
|
60
60
|
pagination?: { current: number; pageSize: number };
|
|
61
61
|
/** Externally controlled sorters */
|
|
62
62
|
sorters?: Sort[];
|
|
63
|
+
/** Custom batch actions to render when rows are selected */
|
|
64
|
+
batchActions?: Snippet<[{ selectedIds: string[] }]>;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
let {
|
|
@@ -71,6 +73,7 @@
|
|
|
71
73
|
rowActions,
|
|
72
74
|
emptyState,
|
|
73
75
|
expandedRowRender,
|
|
76
|
+
batchActions,
|
|
74
77
|
pagination: externalPagination,
|
|
75
78
|
sorters: externalSorters,
|
|
76
79
|
}: Props = $props();
|
|
@@ -353,10 +356,20 @@
|
|
|
353
356
|
</Button>
|
|
354
357
|
{/if}
|
|
355
358
|
{#if selectedCount > 0 && canDelete}
|
|
356
|
-
<Button
|
|
359
|
+
<Button
|
|
360
|
+
variant="destructive"
|
|
361
|
+
size="sm"
|
|
362
|
+
class="h-9 whitespace-nowrap"
|
|
363
|
+
onclick={confirmBatchDelete}
|
|
364
|
+
>
|
|
357
365
|
<Trash2 class="h-4 w-4" data-icon="inline-start" /> {t('common.batchDelete', { count: selectedCount })}
|
|
358
366
|
</Button>
|
|
359
367
|
{/if}
|
|
368
|
+
|
|
369
|
+
{#if selectedCount > 0 && batchActions}
|
|
370
|
+
{@render batchActions({ selectedIds: Object.keys(rowSelection) })}
|
|
371
|
+
{/if}
|
|
372
|
+
|
|
360
373
|
<!-- Column Visibility Picker (DropdownMenu) -->
|
|
361
374
|
<DropdownMenu.Root>
|
|
362
375
|
<DropdownMenu.Trigger>
|
|
@@ -388,64 +401,80 @@
|
|
|
388
401
|
</div>
|
|
389
402
|
</div>
|
|
390
403
|
|
|
391
|
-
<!-- Search -->
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
<
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
+
<!-- Search and Advanced Filters -->
|
|
405
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
406
|
+
{#if searchableFields.length > 0}
|
|
407
|
+
<div class="relative max-w-sm flex-1 sm:min-w-[250px]">
|
|
408
|
+
<Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
409
|
+
<Input
|
|
410
|
+
type="text"
|
|
411
|
+
bind:value={searchText}
|
|
412
|
+
oninput={() => { pagination = { ...pagination, current: 1 }; }}
|
|
413
|
+
placeholder={t('common.search')}
|
|
414
|
+
class="pl-10 h-9"
|
|
415
|
+
/>
|
|
416
|
+
</div>
|
|
417
|
+
{/if}
|
|
404
418
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
{/if}
|
|
416
|
-
</Button>
|
|
417
|
-
{/snippet}
|
|
418
|
-
</Popover.Trigger>
|
|
419
|
-
<Popover.Content class="w-80">
|
|
420
|
-
<div class="space-y-3">
|
|
421
|
-
<h4 class="font-medium text-sm">{t('common.filter')}</h4>
|
|
422
|
-
{#each filterableFields as field}
|
|
423
|
-
<div class="space-y-1">
|
|
424
|
-
<label class="text-xs text-muted-foreground" for="filter-{field.key}">{field.label}</label>
|
|
425
|
-
<Input
|
|
426
|
-
id="filter-{field.key}"
|
|
427
|
-
type="text"
|
|
428
|
-
bind:value={filterValues[field.key]}
|
|
429
|
-
placeholder={field.label}
|
|
430
|
-
class="h-8 text-sm"
|
|
431
|
-
/>
|
|
432
|
-
</div>
|
|
433
|
-
{/each}
|
|
434
|
-
<div class="flex gap-2">
|
|
435
|
-
<Button size="sm" class="flex-1" onclick={() => { pagination = { ...pagination, current: 1 }; }}>
|
|
436
|
-
{t('common.confirm')}
|
|
437
|
-
</Button>
|
|
438
|
-
<Button variant="outline" size="sm" onclick={() => {
|
|
439
|
-
filterValues = {};
|
|
440
|
-
pagination = { ...pagination, current: 1 };
|
|
441
|
-
}}>
|
|
442
|
-
{t('common.reset')}
|
|
419
|
+
{#if filterableFields.length > 0}
|
|
420
|
+
<Popover.Root>
|
|
421
|
+
<Popover.Trigger>
|
|
422
|
+
{#snippet child({ props })}
|
|
423
|
+
<Button variant="outline" size="sm" class="h-9 px-3" {...props}>
|
|
424
|
+
<FilterIcon class="h-4 w-4" data-icon="inline-start" />
|
|
425
|
+
{t('common.filter')}
|
|
426
|
+
{#if activeFilterCount > 0}
|
|
427
|
+
<Badge variant="secondary" class="ml-1 h-5 min-w-5 px-1">{activeFilterCount}</Badge>
|
|
428
|
+
{/if}
|
|
443
429
|
</Button>
|
|
430
|
+
{/snippet}
|
|
431
|
+
</Popover.Trigger>
|
|
432
|
+
<Popover.Content class="w-80">
|
|
433
|
+
<div class="space-y-3">
|
|
434
|
+
<h4 class="font-medium text-sm">{t('common.filter')}</h4>
|
|
435
|
+
{#each filterableFields as field}
|
|
436
|
+
<div class="space-y-1">
|
|
437
|
+
<label class="text-xs text-muted-foreground" for="filter-{field.key}">{field.label}</label>
|
|
438
|
+
{#if field.type === 'select' && field.options}
|
|
439
|
+
<select
|
|
440
|
+
id="filter-{field.key}"
|
|
441
|
+
class="h-9 text-sm w-full font-normal flex w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
442
|
+
value={filterValues[field.key] ?? ''}
|
|
443
|
+
onchange={(e) => filterValues[field.key] = (e.currentTarget as HTMLSelectElement).value}
|
|
444
|
+
>
|
|
445
|
+
<option value="">全部</option>
|
|
446
|
+
{#each field.options as opt}
|
|
447
|
+
<option value={opt.value}>{opt.label}</option>
|
|
448
|
+
{/each}
|
|
449
|
+
</select>
|
|
450
|
+
{:else}
|
|
451
|
+
<Input
|
|
452
|
+
id="filter-{field.key}"
|
|
453
|
+
type="text"
|
|
454
|
+
value={filterValues[field.key] ?? ''}
|
|
455
|
+
oninput={(e) => filterValues[field.key] = (e.currentTarget as HTMLInputElement).value}
|
|
456
|
+
placeholder={field.label}
|
|
457
|
+
class="h-9 text-sm"
|
|
458
|
+
/>
|
|
459
|
+
{/if}
|
|
460
|
+
</div>
|
|
461
|
+
{/each}
|
|
462
|
+
<div class="flex gap-2 pt-2">
|
|
463
|
+
<Button size="sm" class="flex-1" onclick={() => { pagination = { ...pagination, current: 1 }; }}>
|
|
464
|
+
{t('common.confirm')}
|
|
465
|
+
</Button>
|
|
466
|
+
<Button variant="outline" size="sm" onclick={() => {
|
|
467
|
+
filterValues = {};
|
|
468
|
+
pagination = { ...pagination, current: 1 };
|
|
469
|
+
}}>
|
|
470
|
+
{t('common.reset')}
|
|
471
|
+
</Button>
|
|
472
|
+
</div>
|
|
444
473
|
</div>
|
|
445
|
-
</
|
|
446
|
-
</Popover.
|
|
447
|
-
|
|
448
|
-
|
|
474
|
+
</Popover.Content>
|
|
475
|
+
</Popover.Root>
|
|
476
|
+
{/if}
|
|
477
|
+
</div>
|
|
449
478
|
|
|
450
479
|
<!-- Table (TanStack-powered) -->
|
|
451
480
|
<div class="rounded-lg bg-card shadow-sm ring-1 ring-border/10 overflow-hidden" role="region" aria-label="{resource.label} {t('common.list')}">
|
|
@@ -741,18 +770,21 @@
|
|
|
741
770
|
<div class="flex flex-col sm:flex-row items-center justify-between gap-2 text-sm text-muted-foreground">
|
|
742
771
|
<div class="flex items-center gap-2">
|
|
743
772
|
<span>{t('common.total', { total: query.data?.total ?? 0 })}</span>
|
|
744
|
-
<
|
|
745
|
-
class="
|
|
773
|
+
<select
|
|
774
|
+
class="h-8 w-[70px] px-1 py-1 flex items-center justify-between rounded-md border border-input bg-background text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
746
775
|
value={String(pagination.pageSize ?? 10)}
|
|
747
|
-
onchange={(e
|
|
776
|
+
onchange={(e) => {
|
|
748
777
|
const size = Number((e.target as HTMLSelectElement).value);
|
|
749
|
-
|
|
778
|
+
if (!isNaN(size)) {
|
|
779
|
+
pagination = { ...pagination, pageSize: size, current: 1 };
|
|
780
|
+
}
|
|
750
781
|
}}
|
|
751
782
|
>
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
783
|
+
<option value="10">10</option>
|
|
784
|
+
<option value="20">20</option>
|
|
785
|
+
<option value="50">50</option>
|
|
786
|
+
<option value="100">100</option>
|
|
787
|
+
</select>
|
|
756
788
|
</div>
|
|
757
789
|
<PaginationUI.Root>
|
|
758
790
|
<PaginationUI.Content>
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { Avatar } from './ui/avatar/index.js';
|
|
15
15
|
import {
|
|
16
16
|
LayoutDashboard, FileText, Users, Settings, Home,
|
|
17
|
-
ChevronLeft, ChevronDown, LogOut, Sun, Moon, Palette
|
|
17
|
+
ChevronLeft, ChevronRight, ChevronDown, LogOut, Sun, Moon, Palette
|
|
18
18
|
} from 'lucide-svelte';
|
|
19
19
|
|
|
20
20
|
let { collapsed, identity, title, onToggle, onLogout, menu, routeMode = 'auto' }: {
|
|
@@ -181,7 +181,9 @@
|
|
|
181
181
|
<ChevronLeft class="h-4 w-4" />
|
|
182
182
|
</TooltipButton>
|
|
183
183
|
{:else}
|
|
184
|
-
<
|
|
184
|
+
<TooltipButton tooltip={t('common.toggleSidebar')} variant="ghost" size="icon-sm" onclick={onToggle} class="mx-auto mt-2 mb-2 block">
|
|
185
|
+
<ChevronRight class="h-4 w-4" />
|
|
186
|
+
</TooltipButton>
|
|
185
187
|
{/if}
|
|
186
188
|
</div>
|
|
187
189
|
|