@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/ui",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -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 { Select } from './ui/select/index.js';
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 variant="destructive" size="sm" onclick={confirmBatchDelete}>
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
- {#if searchableFields.length > 0}
393
- <div class="relative max-w-sm">
394
- <Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
395
- <Input
396
- type="text"
397
- bind:value={searchText}
398
- oninput={() => { pagination = { ...pagination, current: 1 }; }}
399
- placeholder={t('common.search')}
400
- class="pl-10"
401
- />
402
- </div>
403
- {/if}
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
- <!-- Advanced Filters (Popover) -->
406
- {#if filterableFields.length > 0}
407
- <Popover.Root>
408
- <Popover.Trigger>
409
- {#snippet child({ props })}
410
- <Button variant="outline" size="sm" {...props}>
411
- <FilterIcon class="h-4 w-4" data-icon="inline-start" />
412
- {t('common.filter')}
413
- {#if activeFilterCount > 0}
414
- <Badge variant="secondary" class="ml-1 h-5 min-w-5 px-1">{activeFilterCount}</Badge>
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
- </div>
446
- </Popover.Content>
447
- </Popover.Root>
448
- {/if}
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
- <Select
745
- class="hidden sm:inline-block h-8 w-auto text-xs"
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: Event) => {
776
+ onchange={(e) => {
748
777
  const size = Number((e.target as HTMLSelectElement).value);
749
- pagination = { ...pagination, pageSize: size, current: 1 };
778
+ if (!isNaN(size)) {
779
+ pagination = { ...pagination, pageSize: size, current: 1 };
780
+ }
750
781
  }}
751
782
  >
752
- {#each [10, 20, 50, 100] as size}
753
- <option value={size}>{size} / {t('common.perPage')}</option>
754
- {/each}
755
- </Select>
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
- <div></div>
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