@svadmin/ui 0.29.4 → 0.30.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.
@@ -47,6 +47,8 @@
47
47
  menu?: MenuItem[];
48
48
  /** External URL to the main application or workspace (renders a shortcut in the header) */
49
49
  siteUrl?: string;
50
+ /** Routing strategy for Sidebar links ('hash' | 'path' | 'auto') */
51
+ routeMode?: 'hash' | 'path' | 'auto';
50
52
  }
51
53
 
52
54
  let {
@@ -63,6 +65,7 @@
63
65
  components: userComponents,
64
66
  menu,
65
67
  siteUrl,
68
+ routeMode,
66
69
  }: Props = $props();
67
70
 
68
71
  // Default component registry
@@ -76,10 +79,13 @@
76
79
  };
77
80
 
78
81
  // Merge user overrides and set context
79
- setComponentRegistry({ ...defaultComponents, ...userComponents });
82
+ $effect(() => {
83
+ setComponentRegistry({ ...defaultComponents, ...userComponents });
84
+ });
80
85
 
81
86
  // Resolve router provider (default to hash)
82
87
  const resolvedRouter = $derived(routerProvider ?? createHashRouterProvider());
88
+ const resolvedRouteMode = $derived(routeMode ?? (routerProvider ? 'auto' : 'hash'));
83
89
 
84
90
  // Set up context — use $effect so prop changes are tracked
85
91
  $effect.pre(() => {
@@ -151,7 +157,7 @@
151
157
  {:else if route === '/login' || route === '/register' || route === '/forgot-password' || route === '/update-password'}
152
158
  <ConfigErrorScreen title="{title} — {t('common.configRequired')}" />
153
159
  {:else if isAuthenticated || !authProvider}
154
- <Layout {title} {menu} {siteUrl}>
160
+ <Layout {title} {menu} {siteUrl} routeMode={resolvedRouteMode}>
155
161
  {#key route + (params.resource ?? '') + (params.id ?? '')}
156
162
  <div class="svadmin-page-enter">
157
163
  {#if currentPath().startsWith('/settings')}
@@ -185,6 +191,11 @@
185
191
  {@const Comp = getComponentRegistry().ShowPage}
186
192
  <Comp resourceName={params.resource} id={params.id} />
187
193
  {/key}
194
+ {:else if route === '/:resource/clone/:id'}
195
+ {#key `${params.resource}-clone-${params.id}`}
196
+ {@const Comp = getComponentRegistry().AutoForm}
197
+ <Comp resourceName={params.resource} mode="create" id={params.id} />
198
+ {/key}
188
199
  {/if}
189
200
  </div>
190
201
  {/key}
@@ -20,6 +20,8 @@ interface Props {
20
20
  menu?: MenuItem[];
21
21
  /** External URL to the main application or workspace (renders a shortcut in the header) */
22
22
  siteUrl?: string;
23
+ /** Routing strategy for Sidebar links ('hash' | 'path' | 'auto') */
24
+ routeMode?: 'hash' | 'path' | 'auto';
23
25
  }
24
26
  declare const AdminApp: import("svelte").Component<Props, {}, "">;
25
27
  type AdminApp = ReturnType<typeof AdminApp>;
@@ -22,24 +22,24 @@
22
22
  // Sidebar density
23
23
  const DENSITY_KEY = 'svadmin-sidebar-density';
24
24
  let density = $state<'compact' | 'standard'>(
25
- (typeof localStorage !== 'undefined' ? localStorage.getItem(DENSITY_KEY) : null) as 'compact' | 'standard' ?? 'standard'
25
+ (typeof window !== 'undefined' ? localStorage.getItem(DENSITY_KEY) : null) as 'compact' | 'standard' ?? 'standard'
26
26
  );
27
27
 
28
28
  function setDensity(d: 'compact' | 'standard') {
29
29
  density = d;
30
- if (typeof localStorage !== 'undefined') localStorage.setItem(DENSITY_KEY, d);
30
+ if (typeof window !== 'undefined') localStorage.setItem(DENSITY_KEY, d);
31
31
  }
32
32
 
33
33
  // Page size
34
34
  const PAGE_SIZE_KEY = 'svadmin-default-page-size';
35
35
  const pageSizeOptions = [10, 20, 50];
36
36
  let pageSize = $state<number>(
37
- typeof localStorage !== 'undefined' ? parseInt(localStorage.getItem(PAGE_SIZE_KEY) ?? '10', 10) : 10
37
+ typeof window !== 'undefined' ? parseInt(localStorage.getItem(PAGE_SIZE_KEY) ?? '10', 10) : 10
38
38
  );
39
39
 
40
40
  function setPageSize(size: number) {
41
41
  pageSize = size;
42
- if (typeof localStorage !== 'undefined') localStorage.setItem(PAGE_SIZE_KEY, String(size));
42
+ if (typeof window !== 'undefined') localStorage.setItem(PAGE_SIZE_KEY, String(size));
43
43
  }
44
44
 
45
45
  // Locale display names
@@ -69,15 +69,17 @@
69
69
  return d;
70
70
  })());
71
71
 
72
+ const validator = $derived(deriveValidator(formFields));
73
+
72
74
  // ─── useForm: single source of truth for values, errors, tainted ──
73
75
  const form = useForm({
74
76
  get resource() { return resourceName; },
75
77
  get action() { return mode; },
76
78
  get id() { return id; },
77
- defaultValues: defaults,
79
+ get defaultValues() { return defaults; },
78
80
  redirect: 'list',
79
81
  warnWhenUnsavedChanges: true,
80
- validate: deriveValidator(formFields),
82
+ get validate() { return validator; },
81
83
  });
82
84
 
83
85
  // ─── Submission error (non-field, e.g. network error) ─────────────
@@ -84,7 +84,7 @@
84
84
  const urlState = readURLState();
85
85
 
86
86
  // Snapshot resource values for initial state (untrack to avoid reactive tracking)
87
- const storedPageSize = typeof localStorage !== 'undefined' ? parseInt(localStorage.getItem('svadmin-default-page-size') ?? '', 10) : NaN;
87
+ const storedPageSize = typeof window !== 'undefined' ? parseInt(localStorage.getItem('svadmin-default-page-size') ?? '', 10) : NaN;
88
88
  const initPageSize = untrack(() => resource.pageSize ?? (isNaN(storedPageSize) ? 10 : storedPageSize));
89
89
  const initDefaultSort = untrack(() => resource.defaultSort);
90
90
 
@@ -149,7 +149,7 @@
149
149
  get sorters() { return sorters; },
150
150
  get filters() { return activeFilters; },
151
151
  });
152
- const query = listResult.query;
152
+ const query = listResult;
153
153
  const deleteResult = useDelete({ get resource() { return resourceName; } });
154
154
  const deleteMutation = deleteResult.mutation;
155
155
 
@@ -171,7 +171,7 @@
171
171
  let columnVisibility = $state<VisibilityState>((() => {
172
172
  // Try to restore from localStorage first
173
173
  const storageKey = `svadmin-columns-${resourceName}`;
174
- if (typeof localStorage !== 'undefined') {
174
+ if (typeof window !== 'undefined') {
175
175
  try {
176
176
  const stored = localStorage.getItem(storageKey);
177
177
  if (stored) return JSON.parse(stored);
@@ -191,7 +191,7 @@
191
191
  // Persist column visibility to localStorage
192
192
  $effect(() => {
193
193
  const storageKey = `svadmin-columns-${resourceName}`;
194
- if (typeof localStorage !== 'undefined') {
194
+ if (typeof window !== 'undefined') {
195
195
  try {
196
196
  localStorage.setItem(storageKey, JSON.stringify(columnVisibility));
197
197
  } catch { /* ignore quota errors */ }
@@ -623,7 +623,7 @@
623
623
  recordId={id}
624
624
  {field}
625
625
  value={cell.getValue()}
626
- onSave={() => listResult.query.refetch()}
626
+ onSave={() => listResult.refetch()}
627
627
  />
628
628
  {:else if field?.key === primaryKey}
629
629
  <span class="font-mono text-xs">{cell.getValue() ?? '—'}</span>
@@ -32,7 +32,7 @@
32
32
  // Restore saved order from localStorage
33
33
  $effect(() => {
34
34
  const storageKey = `svadmin-colorder-${resourceName}`;
35
- if (typeof localStorage === 'undefined') return;
35
+ if (typeof window === 'undefined') return;
36
36
  try {
37
37
  const saved = localStorage.getItem(storageKey);
38
38
  if (saved) {
@@ -79,7 +79,7 @@
79
79
 
80
80
  // Persist to localStorage
81
81
  const storageKey = `svadmin-colorder-${resourceName}`;
82
- if (typeof localStorage !== 'undefined') {
82
+ if (typeof window !== 'undefined') {
83
83
  try {
84
84
  localStorage.setItem(storageKey, JSON.stringify(newColumns.map(c => c.id)));
85
85
  } catch { /* ignore */ }
@@ -22,7 +22,7 @@
22
22
  let shortcutsOpen = $state(false);
23
23
  let mobileMenuOpen = $state(false);
24
24
 
25
- let { children, title = 'Admin', menu, siteUrl }: { children: Snippet; title?: string; menu?: MenuItem[]; siteUrl?: string } = $props();
25
+ let { children, title = 'Admin', menu, siteUrl, routeMode = 'auto' }: { children: Snippet; title?: string; menu?: MenuItem[]; siteUrl?: string; routeMode?: 'hash' | 'path' | 'auto' } = $props();
26
26
 
27
27
  let hasAuth = true;
28
28
  let auth: ReturnType<typeof getAuthProvider> | null = null;
@@ -121,13 +121,13 @@
121
121
  <div class="flex h-screen bg-background" in:fade={{ duration: 200, delay: 50 }}>
122
122
  <!-- Desktop sidebar -->
123
123
  <div class="hidden md:block">
124
- <Sidebar {collapsed} {identity} {title} {menu} onToggle={() => collapsed = !collapsed} onLogout={handleLogout} />
124
+ <Sidebar {collapsed} {identity} {title} {menu} {routeMode} onToggle={() => collapsed = !collapsed} onLogout={handleLogout} />
125
125
  </div>
126
126
 
127
127
  <!-- Mobile sidebar via Sheet -->
128
128
  <Sheet.Root bind:open={mobileMenuOpen} side="left">
129
129
  <div class="md:hidden">
130
- <Sidebar collapsed={false} {identity} {title} {menu} onToggle={() => { mobileMenuOpen = false; }} onLogout={handleLogout} />
130
+ <Sidebar collapsed={false} {identity} {title} {menu} {routeMode} onToggle={() => { mobileMenuOpen = false; }} onLogout={handleLogout} />
131
131
  </div>
132
132
  </Sheet.Root>
133
133
 
@@ -5,6 +5,7 @@ type $$ComponentProps = {
5
5
  title?: string;
6
6
  menu?: MenuItem[];
7
7
  siteUrl?: string;
8
+ routeMode?: 'hash' | 'path' | 'auto';
8
9
  };
9
10
  declare const Layout: import("svelte").Component<$$ComponentProps, {}, "">;
10
11
  type Layout = ReturnType<typeof Layout>;
@@ -35,7 +35,7 @@
35
35
  {/snippet}
36
36
  </PageHeader>
37
37
 
38
- {#if query.query.isLoading}
38
+ {#if query.isLoading}
39
39
  <Card.Root class="overflow-hidden border-border/40 shadow-sm">
40
40
  <Card.Content class="p-0">
41
41
  {#each showFields.slice(0, 6) as _, i}
@@ -46,11 +46,11 @@
46
46
  {/each}
47
47
  </Card.Content>
48
48
  </Card.Root>
49
- {:else if query.query.data?.data}
49
+ {:else if query.data?.data}
50
50
  <Card.Root class="overflow-hidden border-border/40 shadow-sm">
51
51
  <Card.Content class="p-0">
52
52
  {#each showFields as field, i}
53
- {@const value = (query.query.data!.data as Record<string, unknown>)[field.key]}
53
+ {@const value = (query.data!.data as Record<string, unknown>)[field.key]}
54
54
  {@const DisplayComponent = getDisplayComponent(field.type)}
55
55
  <div class="flex flex-col sm:flex-row px-4 sm:px-6 py-3 sm:py-4 {i % 2 === 1 ? 'bg-muted/20' : ''}">
56
56
  <div class="sm:w-1/3 text-xs sm:text-sm font-medium text-muted-foreground mb-1 sm:mb-0">{field.label}</div>
@@ -53,15 +53,17 @@
53
53
  return d;
54
54
  })());
55
55
 
56
+ const validator = $derived(deriveValidator(flatStepsFields));
57
+
56
58
  // ─── useStepsForm: form state + step navigation ──────────────────
57
59
  const form = useStepsForm({
58
60
  get resource() { return resourceName; },
59
61
  get action() { return mode; },
60
62
  get id() { return id; },
61
- defaultValues: defaults,
63
+ get defaultValues() { return defaults; },
62
64
  redirect: 'list',
63
65
  warnWhenUnsavedChanges: true,
64
- validate: deriveValidator(flatStepsFields),
66
+ get validate() { return validator; },
65
67
  });
66
68
 
67
69
  const totalSteps = $derived(steps.length);
@@ -6,6 +6,9 @@
6
6
 
7
7
  interface Props extends Omit<HTMLButtonAttributes, 'class' | 'children'> {
8
8
  tooltip: string;
9
+ href?: string;
10
+ target?: string;
11
+ rel?: string;
9
12
  variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
10
13
  size?: 'default' | 'sm' | 'lg' | 'icon' | 'icon-sm';
11
14
  side?: 'top' | 'right' | 'bottom' | 'left';
@@ -24,6 +27,9 @@
24
27
  disabled = false,
25
28
  type = 'button',
26
29
  tabindex,
30
+ href,
31
+ target,
32
+ rel,
27
33
  children,
28
34
  ...restProps
29
35
  }: Props = $props();
@@ -32,7 +38,7 @@
32
38
  <Tooltip.Root>
33
39
  <Tooltip.Trigger>
34
40
  {#snippet child({ props })}
35
- {@const buttonProps = { ...props, ...restProps, variant, size, type, class: className, 'aria-label': restProps['aria-label'] ?? tooltip, onclick, disabled, tabindex } as any}
41
+ {@const buttonProps = { ...props, ...restProps, href, target, rel, variant, size, type, class: className, 'aria-label': restProps['aria-label'] ?? tooltip, onclick, disabled, tabindex } as any}
36
42
  <Button {...buttonProps}>
37
43
  {@render children()}
38
44
  </Button>
@@ -2,6 +2,9 @@ import type { Snippet } from 'svelte';
2
2
  import type { HTMLButtonAttributes } from 'svelte/elements';
3
3
  interface Props extends Omit<HTMLButtonAttributes, 'class' | 'children'> {
4
4
  tooltip: string;
5
+ href?: string;
6
+ target?: string;
7
+ rel?: string;
5
8
  variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
6
9
  size?: 'default' | 'sm' | 'lg' | 'icon' | 'icon-sm';
7
10
  side?: 'top' | 'right' | 'bottom' | 'left';
@@ -14,10 +14,12 @@ const ROUTES = [
14
14
  '/:resource/create',
15
15
  '/:resource/edit/:id',
16
16
  '/:resource/show/:id',
17
+ '/:resource/clone/:id',
17
18
  '/:parent/:parentId/:resource',
18
19
  '/:parent/:parentId/:resource/create',
19
20
  '/:parent/:parentId/:resource/edit/:id',
20
21
  '/:parent/:parentId/:resource/show/:id',
22
+ '/:parent/:parentId/:resource/clone/:id',
21
23
  ];
22
24
  function sync() {
23
25
  let path;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/ui",
3
- "version": "0.29.4",
3
+ "version": "0.30.1",
4
4
  "description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -25,7 +25,7 @@
25
25
  "./components/*": "./dist/components/*"
26
26
  },
27
27
  "peerDependencies": {
28
- "@svadmin/core": "^0.20.3",
28
+ "@svadmin/core": "^0.21.1",
29
29
  "@tanstack/svelte-query": "^6.0.0",
30
30
  "highlight.js": "^11.11.1",
31
31
  "isomorphic-dompurify": "^3.7.1",
@@ -53,6 +53,7 @@
53
53
  "dependencies": {
54
54
  "@lucide/svelte": "^1.7.0",
55
55
  "@tanstack/svelte-table": "^9.0.0-alpha.10",
56
+ "@tanstack/table-core": "^9.0.0-alpha.10",
56
57
  "bits-ui": "^2.17.1",
57
58
  "clsx": "^2.1.1",
58
59
  "cmdk-sv": "^0.0.19",
@@ -71,9 +72,7 @@
71
72
  "bugs": {
72
73
  "url": "https://github.com/zuohuadong/svadmin/issues"
73
74
  },
74
- "devDependencies": {
75
- "@tanstack/table-core": "8.21.3"
76
- },
75
+ "devDependencies": {},
77
76
  "scripts": {
78
77
  "build": "svelte-package -i src -o dist"
79
78
  }