@svadmin/ui 0.20.3 → 0.21.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.20.3",
3
+ "version": "0.21.0",
4
4
  "description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -46,8 +46,10 @@
46
46
  selectable?: boolean;
47
47
  /** Custom header actions (right side) */
48
48
  headerActions?: Snippet;
49
- /** Custom cell renderer per field */
50
- cellRenderer?: Snippet<[{ field: FieldDefinition; value: unknown; record: Record<string, unknown> }]>;
49
+ /** Custom cell renderers by field key */
50
+ columns?: Record<string, Snippet<[{ value: unknown; record: Record<string, unknown> }]>>;
51
+ /** Global fallback cell renderer */
52
+ defaultCellRenderer?: Snippet<[{ field: FieldDefinition; value: unknown; record: Record<string, unknown> }]>;
51
53
  /** Custom row actions (edit/delete column) */
52
54
  rowActions?: Snippet<[{ record: Record<string, unknown>; id: string | number }]>;
53
55
  /** Custom empty state */
@@ -64,7 +66,8 @@
64
66
  resourceName,
65
67
  selectable = true,
66
68
  headerActions,
67
- cellRenderer,
69
+ columns: customColumns,
70
+ defaultCellRenderer,
68
71
  rowActions,
69
72
  emptyState,
70
73
  expandedRowRender,
@@ -560,8 +563,10 @@
560
563
  </div>
561
564
  {:else}
562
565
  {@const field = visibleFields.find(f => f.key === cell.column.id)}
563
- {#if cellRenderer && field}
564
- {@render cellRenderer({ field, value: cell.getValue(), record })}
566
+ {#if customColumns && field && customColumns[field.key]}
567
+ {@render customColumns[field.key]({ value: cell.getValue(), record })}
568
+ {:else if defaultCellRenderer && field}
569
+ {@render defaultCellRenderer({ field, value: cell.getValue(), record })}
565
570
  {:else if field?.type === 'boolean'}
566
571
  <span class="inline-block h-2 w-2 rounded-full {cell.getValue() ? 'bg-success' : 'bg-muted-foreground/30'}"></span>
567
572
  {:else if field?.type === 'date' && cell.getValue()}
@@ -694,8 +699,10 @@
694
699
  <div class="flex items-start justify-between gap-4">
695
700
  <span class="text-xs font-medium text-muted-foreground shrink-0">{field.label}</span>
696
701
  <span class="text-sm text-right truncate max-w-[60%]">
697
- {#if cellRenderer}
698
- {@render cellRenderer({ field, value, record })}
702
+ {#if customColumns && field && customColumns[field.key]}
703
+ {@render customColumns[field.key]({ value, record })}
704
+ {:else if defaultCellRenderer && field}
705
+ {@render defaultCellRenderer({ field, value, record })}
699
706
  {:else if field.type === 'boolean'}
700
707
  <span class="inline-block h-2 w-2 rounded-full {value ? 'bg-success' : 'bg-muted-foreground/30'}"></span>
701
708
  {:else if field.type === 'date' && value}
@@ -18,6 +18,9 @@
18
18
  placeholder?: string;
19
19
  searchable?: boolean;
20
20
  onSearch?: (value: string) => Filter[];
21
+ id?: string;
22
+ class?: string;
23
+ [key: string]: any;
21
24
  }
22
25
 
23
26
  let {
@@ -29,6 +32,7 @@
29
32
  placeholder = 'Select...',
30
33
  searchable = true,
31
34
  onSearch,
35
+ ...restProps
32
36
  }: Props = $props();
33
37
 
34
38
  const { options, query, onSearchChange } = useSelect({
@@ -71,6 +75,7 @@
71
75
  type="button"
72
76
  class="w-full justify-between font-normal"
73
77
  onclick={() => { open = !open; }}
78
+ {...restProps}
74
79
  >
75
80
  {#if selectedLabel}
76
81
  <span class="truncate">{selectedLabel}</span>
@@ -1,12 +1,6 @@
1
- <script context="module">
2
- export interface GridModule {
3
- id: string;
4
- [key: string]: unknown;
5
- }
6
- </script>
7
-
8
1
  <script lang="ts">
9
2
  import type { Snippet } from 'svelte';
3
+ import type { GridModule } from '../types.js';
10
4
 
11
5
  let {
12
6
  modules = $bindable<GridModule[]>([]),
@@ -176,10 +176,11 @@
176
176
  />
177
177
  {:then editorMod}
178
178
  <editorMod.Editor
179
+ id={field.key}
179
180
  value={strVal}
180
181
  placeholder={t('field.enterValue', { label: field.label })}
181
182
  preset="full"
182
- onchange={(html) => onchange(html)}
183
+ onchange={(html: string) => onchange(html)}
183
184
  />
184
185
  {:catch}
185
186
  <Textarea
@@ -206,6 +207,7 @@
206
207
 
207
208
  {:else if field.type === 'relation' && field.resource}
208
209
  <ComboboxField
210
+ id={field.key}
209
211
  resource={field.resource}
210
212
  value={value as string | number | null}
211
213
  onchange={(v) => onchange(v)}
@@ -218,6 +220,7 @@
218
220
  {#if (field.options?.length ?? 0) > 8}
219
221
  <!-- Many options → use ComboboxField-style search -->
220
222
  <ComboboxField
223
+ id={field.key}
221
224
  resource=""
222
225
  value={value as string | number | null}
223
226
  onchange={(v) => onchange(v)}
@@ -242,6 +245,7 @@
242
245
  {#each field.options ?? [] as opt}
243
246
  <label class="flex items-center gap-2 text-sm cursor-pointer hover:bg-muted/50 rounded px-1 py-0.5 transition-colors">
244
247
  <Checkbox
248
+ id={`${field.key}-${opt.value}`}
245
249
  checked={multiVal.includes(opt.value)}
246
250
  onCheckedChange={() => toggleMulti(opt.value)}
247
251
  />
@@ -267,6 +271,7 @@
267
271
  {:else if field.type === 'boolean'}
268
272
  <div class="flex items-center gap-2 pt-1">
269
273
  <Switch
274
+ id={field.key}
270
275
  checked={boolVal}
271
276
  onCheckedChange={(v) => onchange(v)}
272
277
  />
@@ -289,6 +294,7 @@
289
294
  {/each}
290
295
  </div>
291
296
  <Input
297
+ id={field.key}
292
298
  type="text"
293
299
  placeholder={t('field.tagsPlaceholder')}
294
300
  onkeydown={handleTagKeydown}
@@ -309,6 +315,7 @@
309
315
  {#each imagesVal as url, i}
310
316
  <div class="flex items-center gap-2">
311
317
  <Input
318
+ id={i === 0 ? field.key : undefined}
312
319
  type="text"
313
320
  value={url}
314
321
  oninput={(e) => updateImage(i, (e.target as HTMLInputElement).value)}
@@ -1,12 +1,7 @@
1
- <script context="module">
2
- export type RoleInfo = { code: string; name: string; [key: string]: any };
3
- export type ResourceInfo = { code: string; name: string; section?: string; [key: string]: any };
4
- export type ActionInfo = { code: string; name: string; [key: string]: any };
5
- </script>
6
-
7
1
  <script lang="ts">
8
2
  import { Shield, AlertCircle, Check } from 'lucide-svelte';
9
3
  import type { Snippet } from 'svelte';
4
+ import type { RoleInfo, ResourceInfo, ActionInfo } from '../types.js';
10
5
 
11
6
  let {
12
7
  // Basic metadata lists
@@ -17,13 +17,14 @@
17
17
  ChevronLeft, ChevronDown, LogOut, Sun, Moon, Palette
18
18
  } from 'lucide-svelte';
19
19
 
20
- let { collapsed, identity, title, onToggle, onLogout, menu }: {
20
+ let { collapsed, identity, title, onToggle, onLogout, menu, legacyHashRouting = false }: {
21
21
  collapsed: boolean;
22
22
  identity: Identity | null;
23
23
  title: string;
24
24
  onToggle: () => void;
25
25
  onLogout: () => void;
26
26
  menu?: MenuItem[];
27
+ legacyHashRouting?: boolean;
27
28
  } = $props();
28
29
 
29
30
  const resources = getResources();
@@ -205,7 +206,7 @@
205
206
  {#each group.items as item}
206
207
  {@const active = isActive(item.path)}
207
208
  <a
208
- href={`#${item.path}`}
209
+ href={legacyHashRouting ? `#${item.path}` : item.path}
209
210
  class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-all duration-200
210
211
  {active
211
212
  ? 'bg-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active'
@@ -228,7 +229,7 @@
228
229
  {#snippet child({ props }: { props: Record<string, unknown> })}
229
230
  <a
230
231
  {...props}
231
- href={`#${item.path}`}
232
+ href={legacyHashRouting ? `#${item.path}` : item.path}
232
233
  class="flex items-center justify-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200
233
234
  {active
234
235
  ? 'bg-sidebar-accent text-sidebar-accent-foreground'
@@ -244,7 +245,7 @@
244
245
  </Tooltip.Root>
245
246
  {:else}
246
247
  <a
247
- href={`#${item.path}`}
248
+ href={legacyHashRouting ? `#${item.path}` : item.path}
248
249
  class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200
249
250
  {active
250
251
  ? 'bg-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active'
@@ -284,7 +285,7 @@
284
285
  {#each colorThemes as ct}
285
286
  <button
286
287
  class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground transition-colors"
287
- onclick={() => { setColorTheme(ct.id); colorPickerOpen = false; }}
288
+ onclick={() => { setColorTheme(ct.id as typeof ct.id & import('@svadmin/core').ColorTheme); colorPickerOpen = false; }}
288
289
  >
289
290
  <span
290
291
  class="h-3.5 w-3.5 rounded-full {getColorTheme() === ct.id ? 'ring-2 ring-offset-1 scale-110' : 'opacity-70'}"
@@ -1,23 +1,9 @@
1
- <script context="module">
2
- export type TaskStatus = 'pending' | 'running' | 'completed' | 'failed';
3
-
4
- export interface BackgroundTask {
5
- id: string;
6
- title: string;
7
- status: TaskStatus;
8
- progress?: number; // 0-100
9
- message?: string;
10
- createdAt: string | Date;
11
- downloadUrl?: string;
12
- [key: string]: unknown;
13
- }
14
- </script>
15
-
16
1
  <script lang="ts">
17
2
  import type { Snippet } from 'svelte';
18
3
  import { Loader2, CheckCircle2, XCircle, Clock, Download, X, ListTodo } from 'lucide-svelte';
19
4
  import { Button } from './ui/button/index.js';
20
5
  import { Badge } from './ui/badge/index.js';
6
+ import type { BackgroundTask, TaskStatus } from '../types.js';
21
7
 
22
8
  let {
23
9
  open = $bindable(false),
@@ -1,17 +1,9 @@
1
- <script context="module">
2
- export interface Tenant {
3
- id: string;
4
- name: string;
5
- logo?: string;
6
- [key: string]: unknown;
7
- }
8
- </script>
9
-
10
1
  <script lang="ts">
11
2
  import type { Snippet } from 'svelte';
12
3
  import { ChevronsUpDown, Building2, Check } from 'lucide-svelte';
13
4
  import { Button } from './ui/button/index.js';
14
5
  import * as DropdownMenu from './ui/dropdown-menu/index.js';
6
+ import type { Tenant } from '../types.js';
15
7
 
16
8
  let {
17
9
  tenants = [],
package/src/index.ts CHANGED
@@ -64,15 +64,15 @@ export { default as SettingsPage } from "./components/SettingsPage.svelte";
64
64
  export { default as AppearanceSettings } from "./components/AppearanceSettings.svelte";
65
65
  export { default as AboutSettings } from "./components/AboutSettings.svelte";
66
66
  export { default as PermissionMatrix } from "./components/PermissionMatrix.svelte";
67
- export type { RoleInfo, ResourceInfo, ActionInfo } from "./components/PermissionMatrix.svelte";
67
+ export type { RoleInfo, ResourceInfo, ActionInfo } from "./types.js";
68
68
  export { default as RolesSettings } from "./components/RolesSettings.svelte";
69
69
  export { default as AuditLogViewer } from "./components/AuditLogViewer.svelte";
70
70
  export { default as TenantSwitcher } from "./components/TenantSwitcher.svelte";
71
- export type { Tenant } from "./components/TenantSwitcher.svelte";
71
+ export type { Tenant } from "./types.js";
72
72
  export { default as TaskQueueDrawer } from "./components/TaskQueueDrawer.svelte";
73
- export type { BackgroundTask, TaskStatus } from "./components/TaskQueueDrawer.svelte";
73
+ export type { BackgroundTask, TaskStatus } from "./types.js";
74
74
  export { default as DraggableGrid } from "./components/DraggableGrid.svelte";
75
- export type { GridModule } from "./components/DraggableGrid.svelte";
75
+ export type { GridModule } from "./types.js";
76
76
 
77
77
  // Dashboard Charts (zero-dependency SVG)
78
78
  export { BarChart, LineChart, PieChart } from './components/charts/index.js';
package/src/types.ts ADDED
@@ -0,0 +1,33 @@
1
+ export type RoleInfo = { code: string; name: string; [key: string]: any };
2
+ export type ResourceInfo = { code: string; name: string; section?: string; [key: string]: any };
3
+ export type ActionInfo = { code: string; name: string; [key: string]: any };
4
+
5
+ export interface Tenant {
6
+ id: string;
7
+ name: string;
8
+ logo?: string;
9
+ [key: string]: unknown;
10
+ }
11
+
12
+ export type TaskStatus = 'pending' | 'running' | 'completed' | 'failed';
13
+
14
+ export interface BackgroundTask {
15
+ id: string;
16
+ title: string;
17
+ status: TaskStatus;
18
+ progress?: number; // 0-100
19
+ message?: string;
20
+ createdAt: string | Date;
21
+ downloadUrl?: string;
22
+ [key: string]: unknown;
23
+ }
24
+
25
+ export interface GridModule {
26
+ id: string;
27
+ w: number;
28
+ h: number;
29
+ x: number;
30
+ y: number;
31
+ title?: string;
32
+ componentProps?: any; // Component specific props
33
+ }