@svadmin/ui 0.30.0 → 0.30.2

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.
@@ -0,0 +1,4 @@
1
+ declare module '*.css' {
2
+ const classes: { [key: string]: string };
3
+ export default classes;
4
+ }
@@ -118,21 +118,35 @@
118
118
  let authChecked = $state(false);
119
119
 
120
120
  $effect(() => {
121
+ let cancelled = false;
121
122
  // Explicitly track route so auth is re-checked on navigation
122
123
  const _route = route;
123
124
 
124
125
  if (!authProvider) {
125
- isAuthenticated = true;
126
- authChecked = true;
126
+ if (!cancelled) {
127
+ isAuthenticated = true;
128
+ authChecked = true;
129
+ }
127
130
  return;
128
131
  }
129
132
  authProvider.check().then(result => {
133
+ if (cancelled) return;
130
134
  isAuthenticated = result.authenticated;
131
135
  authChecked = true;
132
136
  if (!result.authenticated && _route !== '/login' && _route !== '/register' && _route !== '/forgot-password' && _route !== '/update-password') {
133
137
  navigate(result.redirectTo ?? '/login');
134
138
  }
139
+ }).catch(err => {
140
+ if (cancelled) return;
141
+ console.warn('Auth check failed:', err);
142
+ isAuthenticated = false;
143
+ authChecked = true;
144
+ if (_route !== '/login' && _route !== '/register' && _route !== '/forgot-password' && _route !== '/update-password') {
145
+ navigate('/login');
146
+ }
135
147
  });
148
+
149
+ return () => { cancelled = true; };
136
150
  });
137
151
  </script>
138
152
 
@@ -160,7 +174,7 @@
160
174
  <Layout {title} {menu} {siteUrl} routeMode={resolvedRouteMode}>
161
175
  {#key route + (params.resource ?? '') + (params.id ?? '')}
162
176
  <div class="svadmin-page-enter">
163
- {#if currentPath().startsWith('/settings')}
177
+ {#if route.startsWith('/settings')}
164
178
  <SettingsPage />
165
179
  {:else if route === '/' || route === ''}
166
180
  {#if dashboard}
@@ -171,26 +185,31 @@
171
185
  <p class="text-muted-foreground">{t('common.dashboardHint')}</p>
172
186
  </div>
173
187
  {/if}
174
- {:else if route === '/:resource'}
188
+ {:else if route === '/:resource' || route === '/:parent/:parentId/:resource'}
175
189
  {#key params.resource}
176
190
  {@const Comp = getComponentRegistry().AutoTable}
177
191
  <Comp resourceName={params.resource} />
178
192
  {/key}
179
- {:else if route === '/:resource/create'}
193
+ {:else if route === '/:resource/create' || route === '/:parent/:parentId/:resource/create'}
180
194
  {#key params.resource}
181
195
  {@const Comp = getComponentRegistry().AutoForm}
182
196
  <Comp resourceName={params.resource} mode="create" />
183
197
  {/key}
184
- {:else if route === '/:resource/edit/:id'}
198
+ {:else if route === '/:resource/edit/:id' || route === '/:parent/:parentId/:resource/edit/:id'}
185
199
  {#key `${params.resource}-${params.id}`}
186
200
  {@const Comp = getComponentRegistry().AutoForm}
187
201
  <Comp resourceName={params.resource} mode="edit" id={params.id} />
188
202
  {/key}
189
- {:else if route === '/:resource/show/:id'}
203
+ {:else if route === '/:resource/show/:id' || route === '/:parent/:parentId/:resource/show/:id'}
190
204
  {#key `${params.resource}-${params.id}`}
191
205
  {@const Comp = getComponentRegistry().ShowPage}
192
206
  <Comp resourceName={params.resource} id={params.id} />
193
207
  {/key}
208
+ {:else if route === '/:resource/clone/:id' || route === '/:parent/:parentId/:resource/clone/:id'}
209
+ {#key `${params.resource}-clone-${params.id}`}
210
+ {@const Comp = getComponentRegistry().AutoForm}
211
+ <Comp resourceName={params.resource} mode="clone" id={params.id} />
212
+ {/key}
194
213
  {/if}
195
214
  </div>
196
215
  {/key}
@@ -22,24 +22,27 @@
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') {
31
+ localStorage.setItem(DENSITY_KEY, d);
32
+ window.dispatchEvent(new CustomEvent('svadmin-density-change', { detail: d }));
33
+ }
31
34
  }
32
35
 
33
36
  // Page size
34
37
  const PAGE_SIZE_KEY = 'svadmin-default-page-size';
35
38
  const pageSizeOptions = [10, 20, 50];
36
39
  let pageSize = $state<number>(
37
- typeof localStorage !== 'undefined' ? parseInt(localStorage.getItem(PAGE_SIZE_KEY) ?? '10', 10) : 10
40
+ typeof window !== 'undefined' ? parseInt(localStorage.getItem(PAGE_SIZE_KEY) ?? '10', 10) : 10
38
41
  );
39
42
 
40
43
  function setPageSize(size: number) {
41
44
  pageSize = size;
42
- if (typeof localStorage !== 'undefined') localStorage.setItem(PAGE_SIZE_KEY, String(size));
45
+ if (typeof window !== 'undefined') localStorage.setItem(PAGE_SIZE_KEY, String(size));
43
46
  }
44
47
 
45
48
  // Locale display names
@@ -9,7 +9,7 @@
9
9
  import { Input } from './ui/input/index.js';
10
10
  import { Loader2, ChevronLeft, ChevronRight, Search, FileSearch, Eye, X } from '@lucide/svelte';
11
11
 
12
- const authProvider = getAuthProvider();
12
+ const authProvider = getAuthProvider({ optional: true });
13
13
 
14
14
  let logs = $state<AuditLog[]>([]);
15
15
  let total = $state(0);
@@ -42,6 +42,8 @@
42
42
  }
43
43
 
44
44
  $effect(() => {
45
+ // Track `page` to re-fetch on pagination — this is the single trigger
46
+ void page;
45
47
  loadLogs();
46
48
  });
47
49
 
@@ -162,7 +164,7 @@
162
164
  <Button
163
165
  variant="outline" size="sm"
164
166
  disabled={page <= 1}
165
- onclick={() => { page--; loadLogs(); }}
167
+ onclick={() => { page--; }}
166
168
  >
167
169
  <ChevronLeft class="h-4 w-4" />
168
170
  </Button>
@@ -170,7 +172,7 @@
170
172
  <Button
171
173
  variant="outline" size="sm"
172
174
  disabled={page >= totalPages}
173
- onclick={() => { page++; loadLogs(); }}
175
+ onclick={() => { page++; }}
174
176
  >
175
177
  <ChevronRight class="h-4 w-4" />
176
178
  </Button>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { useIsAuthenticated } from '@svadmin/core';
2
+ import { useIsAuthenticated, getRouterProvider } from '@svadmin/core';
3
3
  import type { Snippet } from 'svelte';
4
4
  import { navigate } from '@svadmin/core';
5
5
 
@@ -23,8 +23,15 @@
23
23
  if (fallback) return;
24
24
  let url = redirectTo;
25
25
  if (appendCurrentPathToQuery && typeof window !== 'undefined') {
26
- const current = encodeURIComponent(window.location.pathname + window.location.search);
27
- url += `?to=${current}`;
26
+ const rp = getRouterProvider();
27
+ let currentString = window.location.pathname + window.location.search;
28
+ if (rp) {
29
+ const parsed = rp.parse();
30
+ const qs = new URLSearchParams(parsed.params).toString();
31
+ currentString = parsed.pathname + (qs ? `?${qs}` : '');
32
+ }
33
+ const current = encodeURIComponent(currentString);
34
+ url += url.includes('?') ? `&to=${current}` : `?to=${current}`;
28
35
  }
29
36
  navigate(url);
30
37
  }
@@ -17,7 +17,7 @@
17
17
  interface Props {
18
18
  resourceName: string;
19
19
  id?: string | number;
20
- mode?: 'create' | 'edit';
20
+ mode?: 'create' | 'edit' | 'clone';
21
21
  fieldRenderer?: Snippet<[{ field: FieldDefinition; value: unknown; onchange: (v: unknown) => void }]>;
22
22
  formActions?: Snippet<[{ isLoading: boolean; onSubmit: () => void }]>;
23
23
  headerContent?: Snippet;
@@ -3,7 +3,7 @@ import type { Snippet } from 'svelte';
3
3
  interface Props {
4
4
  resourceName: string;
5
5
  id?: string | number;
6
- mode?: 'create' | 'edit';
6
+ mode?: 'create' | 'edit' | 'clone';
7
7
  fieldRenderer?: Snippet<[{
8
8
  field: FieldDefinition;
9
9
  value: unknown;
@@ -6,10 +6,10 @@
6
6
  type ColumnDef,
7
7
  type SortingState,
8
8
  type RowSelectionState,
9
- type VisibilityState,
9
+ type ColumnVisibilityState,
10
10
  type ExpandedState,
11
11
  } from '@tanstack/svelte-table';
12
- import { getCoreRowModel } from '@tanstack/table-core';
12
+ import { createCoreRowModel } from '@tanstack/table-core';
13
13
 
14
14
  import { useList, useDelete, getResource } from '@svadmin/core';
15
15
  import type { Pagination as PaginationState, Sort, Filter, FieldDefinition } from '@svadmin/core';
@@ -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
 
@@ -101,6 +101,19 @@
101
101
  let filters = $state<Filter[]>([]);
102
102
  let searchText = $state(urlState.search ?? '');
103
103
 
104
+ // Sync external controlled state
105
+ $effect(() => {
106
+ if (externalPagination) {
107
+ pagination = externalPagination;
108
+ }
109
+ });
110
+
111
+ $effect(() => {
112
+ if (externalSorters) {
113
+ sorters = externalSorters;
114
+ }
115
+ });
116
+
104
117
  // URL sync
105
118
  $effect(() => {
106
119
  writeURLState({
@@ -168,17 +181,17 @@
168
181
  let sorting = $state<SortingState>(untrack(() =>
169
182
  sorters.map(s => ({ id: s.field, desc: s.order === 'desc' }))
170
183
  ));
171
- let columnVisibility = $state<VisibilityState>((() => {
184
+ let columnVisibility = $state<ColumnVisibilityState>((() => {
172
185
  // Try to restore from localStorage first
173
186
  const storageKey = `svadmin-columns-${resourceName}`;
174
- if (typeof localStorage !== 'undefined') {
187
+ if (typeof window !== 'undefined') {
175
188
  try {
176
189
  const stored = localStorage.getItem(storageKey);
177
190
  if (stored) return JSON.parse(stored);
178
191
  } catch { /* ignore */ }
179
192
  }
180
193
  // Fall back to resource field definitions
181
- const vis: VisibilityState = {};
194
+ const vis: ColumnVisibilityState = {};
182
195
  for (const f of resource.fields) {
183
196
  if (f.showInList === false) vis[f.key] = false;
184
197
  }
@@ -191,7 +204,7 @@
191
204
  // Persist column visibility to localStorage
192
205
  $effect(() => {
193
206
  const storageKey = `svadmin-columns-${resourceName}`;
194
- if (typeof localStorage !== 'undefined') {
207
+ if (typeof window !== 'undefined') {
195
208
  try {
196
209
  localStorage.setItem(storageKey, JSON.stringify(columnVisibility));
197
210
  } catch { /* ignore quota errors */ }
@@ -214,7 +227,7 @@
214
227
  resource.fields.filter(f => f.showInList !== false)
215
228
  );
216
229
 
217
- const columns = $derived<ColumnDef<Record<string, unknown>>[]>([
230
+ const columns = $derived<ColumnDef<any, Record<string, unknown>>[]>([
218
231
  // Selection column
219
232
  ...(selectable && canDelete ? [{
220
233
  id: '_select',
@@ -222,7 +235,7 @@
222
235
  cell: () => '',
223
236
  size: 40,
224
237
  enableSorting: false,
225
- } satisfies ColumnDef<Record<string, unknown>>] : []),
238
+ } satisfies ColumnDef<any, Record<string, unknown>>] : []),
226
239
  // Expand column
227
240
  ...(expandedRowRender ? [{
228
241
  id: '_expand',
@@ -230,9 +243,9 @@
230
243
  cell: () => '',
231
244
  size: 40,
232
245
  enableSorting: false,
233
- } satisfies ColumnDef<Record<string, unknown>>] : []),
246
+ } satisfies ColumnDef<any, Record<string, unknown>>] : []),
234
247
  // Data columns
235
- ...visibleFields.map((field): ColumnDef<Record<string, unknown>> => ({
248
+ ...visibleFields.map((field): ColumnDef<any, Record<string, unknown>> => ({
236
249
  id: field.key,
237
250
  accessorKey: field.key,
238
251
  header: () => field.label,
@@ -252,10 +265,10 @@
252
265
  const tbl = createTable({
253
266
  get data() { return query.data?.data ?? []; },
254
267
  get columns() { return columns; },
255
- getCoreRowModel: getCoreRowModel(),
268
+ getCoreRowModel: createCoreRowModel(),
256
269
  manualPagination: true,
257
270
  manualSorting: true,
258
- getRowId: (row) => String(row[primaryKey]),
271
+ getRowId: (row: any) => String(row[primaryKey]),
259
272
  state: {
260
273
  get sorting() { return sorting; },
261
274
  get columnVisibility() { return columnVisibility; },
@@ -263,19 +276,19 @@
263
276
  get expanded() { return expanded; },
264
277
  get columnOrder() { return columnOrder; },
265
278
  },
266
- onSortingChange: (updater) => {
279
+ onSortingChange: (updater: any) => {
267
280
  sorting = typeof updater === 'function' ? updater(sorting) : updater;
268
281
  },
269
- onColumnVisibilityChange: (updater) => {
282
+ onColumnVisibilityChange: (updater: any) => {
270
283
  columnVisibility = typeof updater === 'function' ? updater(columnVisibility) : updater;
271
284
  },
272
- onRowSelectionChange: (updater) => {
285
+ onRowSelectionChange: (updater: any) => {
273
286
  rowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
274
287
  },
275
- onExpandedChange: (updater) => {
288
+ onExpandedChange: (updater: any) => {
276
289
  expanded = typeof updater === 'function' ? updater(expanded) : updater;
277
290
  },
278
- onColumnOrderChange: (updater) => {
291
+ onColumnOrderChange: (updater: any) => {
279
292
  columnOrder = typeof updater === 'function' ? updater(columnOrder) : updater;
280
293
  },
281
294
  get enableRowSelection() { return selectable && canDelete; },
@@ -390,7 +403,7 @@
390
403
  {/snippet}
391
404
  </DropdownMenu.Trigger>
392
405
  <DropdownMenu.Content align="end" class="w-48">
393
- {#each tbl.getAllLeafColumns().filter(c => !c.id.startsWith('_')) as column}
406
+ {#each tbl.getAllLeafColumns().filter((c: any) => !c.id.startsWith('_')) as column}
394
407
  <DropdownMenu.CheckboxItem
395
408
  checked={column.getIsVisible()}
396
409
  onCheckedChange={(v) => column.toggleVisibility(!!v)}
@@ -515,9 +528,9 @@
515
528
  <Table.Header>
516
529
  {#each tbl.getHeaderGroups() as headerGroup}
517
530
  <DraggableHeader
518
- columns={headerGroup.headers.map(h => ({ id: h.column.id, header: h }))}
531
+ columns={headerGroup.headers.map((h: any) => ({ id: h.column.id, header: h }))}
519
532
  resourceName={resourceName}
520
- onReorder={(newOrder) => { columnOrder = newOrder.map(c => c.id); }}
533
+ onReorder={(newOrder) => { columnOrder = newOrder.map((c: any) => c.id); }}
521
534
  >
522
535
  {#snippet header(col, index, dragProps)}
523
536
  {@const header = col.header as typeof headerGroup.headers[0]}
@@ -1,41 +1,51 @@
1
1
  <script lang="ts">
2
2
  import { getResources } from '@svadmin/core';
3
- import { currentPath } from '@svadmin/core/router';
3
+ import { getPath } from '../router-state.svelte.js';
4
+ import { formatLink } from '@svadmin/core/router';
4
5
  import { t } from '@svadmin/core/i18n';
5
6
  import * as Breadcrumb from './ui/breadcrumb/index.js';
6
7
 
7
8
  const resources = getResources();
8
- let path = $state(currentPath());
9
-
10
- function onHashChange() { path = currentPath(); }
11
9
 
12
10
  interface Crumb { label: string; href: string; }
13
11
 
14
12
  const crumbs = $derived.by(() => {
15
- const result: Crumb[] = [{ label: t('common.home'), href: '#/' }];
16
- if (path === '/') return result;
13
+ const result: Crumb[] = [{ label: t('common.home') ?? 'Home', href: formatLink('/') }];
14
+ const path = getPath().replace(/^\//, '');
15
+ if (path === '') return result;
17
16
 
18
17
  const segments = path.split('/').filter(Boolean);
19
- const resourceName = segments[0];
20
- const res = resources.find(r => r.name === resourceName);
21
-
22
- if (res) {
23
- result.push({ label: res.label, href: `#/${res.name}` });
24
-
25
- if (segments[1] === 'create') {
26
- result.push({ label: t('common.create'), href: `#/${res.name}/create` });
27
- } else if (segments[1] === 'edit' && segments[2]) {
28
- result.push({ label: `${t('common.edit')} #${segments[2]}`, href: `#/${res.name}/edit/${segments[2]}` });
29
- } else if (segments[1] === 'show' && segments[2]) {
30
- result.push({ label: `${t('common.detail')} #${segments[2]}`, href: `#/${res.name}/show/${segments[2]}` });
31
- }
18
+ const resourceNames = resources.map(r => r.name);
19
+
20
+ let currentPath = '';
21
+
22
+ for (let i = 0; i < segments.length; i++) {
23
+ const seg = segments[i];
24
+ currentPath += `/${seg}`;
25
+
26
+ if (resourceNames.includes(seg)) {
27
+ const res = resources.find(r => r.name === seg)!;
28
+ result.push({ label: res.label, href: formatLink(currentPath) });
29
+ } else if (i > 0 && resourceNames.includes(segments[i-1])) {
30
+ // This is a parent ID (e.g. /teams/123/users)
31
+ result.push({ label: `#${seg}`, href: formatLink(currentPath) });
32
+ } else if (seg === 'create') {
33
+ result.push({ label: t('common.create') ?? 'Create', href: formatLink(currentPath) });
34
+ } else if (['edit', 'show', 'clone'].includes(seg) && segments[i+1]) {
35
+ const actionLabel = seg === 'edit' ? (t('common.edit') ?? 'Edit') :
36
+ seg === 'show' ? (t('common.detail') ?? 'Details') :
37
+ (t('common.clone') ?? 'Clone');
38
+ const id = segments[i+1];
39
+ currentPath += `/${id}`; // advance path by id
40
+ result.push({ label: `${actionLabel} #${id}`, href: formatLink(currentPath) });
41
+ i++; // skip next segment since we consumed the id
42
+ }
32
43
  }
44
+
33
45
  return result;
34
46
  });
35
47
  </script>
36
48
 
37
- <svelte:window onhashchange={onHashChange} />
38
-
39
49
  {#if crumbs.length > 1}
40
50
  <Breadcrumb.Root class="mb-4">
41
51
  <Breadcrumb.List>
@@ -94,6 +94,8 @@
94
94
  }
95
95
  }
96
96
  }, 300);
97
+
98
+ return () => { if (persistTimer) clearTimeout(persistTimer); };
97
99
  });
98
100
 
99
101
  function genId(): string {
@@ -29,10 +29,13 @@
29
29
  let dragIndex = $state<number | null>(null);
30
30
  let dropIndex = $state<number | null>(null);
31
31
 
32
- // Restore saved order from localStorage
32
+ // Restore saved order from localStorage (once on mount)
33
+ let _restored = false;
33
34
  $effect(() => {
35
+ if (_restored) return;
34
36
  const storageKey = `svadmin-colorder-${resourceName}`;
35
- if (typeof localStorage === 'undefined') return;
37
+ if (typeof window === 'undefined') return;
38
+ _restored = true;
36
39
  try {
37
40
  const saved = localStorage.getItem(storageKey);
38
41
  if (saved) {
@@ -79,7 +82,7 @@
79
82
 
80
83
  // Persist to localStorage
81
84
  const storageKey = `svadmin-colorder-${resourceName}`;
82
- if (typeof localStorage !== 'undefined') {
85
+ if (typeof window !== 'undefined') {
83
86
  try {
84
87
  localStorage.setItem(storageKey, JSON.stringify(newColumns.map(c => c.id)));
85
88
  } catch { /* ignore */ }
@@ -35,22 +35,20 @@
35
35
  let identity = $state<Identity | null>(null);
36
36
 
37
37
  $effect(() => {
38
+ let cancelled = false;
38
39
  if (!auth) {
39
- loading = false;
40
+ if (!cancelled) loading = false;
40
41
  return;
41
42
  }
42
- (async () => {
43
- try {
44
- const { authenticated, redirectTo } = await auth.check();
45
- if (!authenticated) {
46
- navigate(redirectTo ?? '/login');
47
- return;
48
- }
49
- identity = await auth.getIdentity();
50
- } finally {
43
+ auth.getIdentity().then(id => {
44
+ if (!cancelled) {
45
+ identity = id;
51
46
  loading = false;
52
47
  }
53
- })();
48
+ }).catch(() => {
49
+ if (!cancelled) loading = false;
50
+ });
51
+ return () => { cancelled = true; };
54
52
  });
55
53
 
56
54
  async function handleLogout() {
@@ -2,6 +2,7 @@
2
2
  import { Shield, AlertCircle, Check } from '@lucide/svelte';
3
3
  import type { Snippet } from 'svelte';
4
4
  import type { RoleInfo, ResourceInfo, ActionInfo } from '../types.js';
5
+ import { t } from '@svadmin/core/i18n';
5
6
 
6
7
  let {
7
8
  // Basic metadata lists
@@ -51,9 +52,9 @@
51
52
  <div>
52
53
  <h1 class="text-2xl font-bold text-foreground flex items-center gap-2">
53
54
  <Shield class="w-6 h-6 text-primary" />
54
- 统一操作权限矩阵
55
+ {t('permissions.title') || 'Permission Matrix'}
55
56
  </h1>
56
- <p class="text-muted-foreground mt-1 text-sm">直观配置系统模块与角色的鉴权规则网络。</p>
57
+ <p class="text-muted-foreground mt-1 text-sm">{t('permissions.description') || 'Configure authorization rules for system modules and roles.'}</p>
57
58
  </div>
58
59
 
59
60
  <!-- Toast Feedback -->
@@ -72,7 +73,7 @@
72
73
  <div class="flex flex-col md:flex-row gap-6 flex-1 min-h-[500px]">
73
74
  <!-- Left: Role Selector Sidebar -->
74
75
  <div class="w-full md:w-64 flex-shrink-0 bg-card rounded-xl shadow-sm border border-border p-4 flex flex-col">
75
- <h2 class="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-4 px-2">业务受众角色</h2>
76
+ <h2 class="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-4 px-2">{t('permissions.roles') || 'Roles'}</h2>
76
77
  <div class="space-y-1 flex-1 overflow-y-auto">
77
78
  {#each roles as role}
78
79
  <button
@@ -84,7 +85,7 @@
84
85
  </button>
85
86
  {/each}
86
87
  {#if roles.length === 0}
87
- <p class="text-center text-muted-foreground text-sm p-4">暂无角色</p>
88
+ <p class="text-center text-muted-foreground text-sm p-4">{t('permissions.noRoles') || 'No roles available'}</p>
88
89
  {/if}
89
90
  </div>
90
91
 
@@ -102,10 +103,10 @@
102
103
  <div class="p-5 border-b border-border bg-muted/30 flex items-center justify-between">
103
104
  <div class="flex items-center gap-4">
104
105
  <div>
105
- <h2 class="text-lg font-bold text-foreground">当前视角:
106
- <span class="text-primary">{roles.find(r => r.code === selectedRole)?.name || '未选择'}</span>
106
+ <h2 class="text-lg font-bold text-foreground">{t('permissions.currentRole') || 'Current role'}:
107
+ <span class="text-primary">{roles.find(r => r.code === selectedRole)?.name || (t('permissions.noneSelected') || 'None selected')}</span>
107
108
  </h2>
108
- <p class="text-xs text-muted-foreground mt-1">勾选以即时挂载/收回操作权限</p>
109
+ <p class="text-xs text-muted-foreground mt-1">{t('permissions.toggleHint') || 'Toggle checkboxes to grant or revoke permissions'}</p>
109
110
  </div>
110
111
 
111
112
  <!-- Slot for header extra logic -->
@@ -116,7 +117,7 @@
116
117
 
117
118
  {#if conflictCount > 0}
118
119
  <div class="px-3 py-1.5 bg-destructive/10 text-destructive rounded-lg text-xs font-medium ring-1 ring-destructive/20 flex items-center gap-1.5">
119
- <AlertCircle class="w-4 h-4" /> {conflictCount} 重叠/冲突规则
120
+ <AlertCircle class="w-4 h-4" /> {conflictCount} {t('permissions.conflicts') || 'conflicting rules'}
120
121
  </div>
121
122
  {/if}
122
123
  </div>
@@ -126,7 +127,7 @@
126
127
  <table class="min-w-full rounded-lg overflow-hidden shadow-sm ring-1 ring-border/10">
127
128
  <thead class="bg-muted/50">
128
129
  <tr>
129
- <th scope="col" class="py-3 pr-3 pl-4 text-left text-sm font-semibold text-foreground w-48">模块域与资源点</th>
130
+ <th scope="col" class="py-3 pr-3 pl-4 text-left text-sm font-semibold text-foreground w-48">{t('permissions.resources') || 'Resources'}</th>
130
131
  {#each actions as action}
131
132
  <th scope="col" class="px-2 py-3 text-center text-sm font-semibold text-foreground">
132
133
  <div class="flex flex-col items-center">
@@ -140,7 +141,7 @@
140
141
  <tbody class="bg-card">
141
142
  {#if resources.length === 0}
142
143
  <tr>
143
- <td colspan={actions.length + 1} class="py-10 text-center text-muted-foreground">暂无可用资源</td>
144
+ <td colspan={actions.length + 1} class="py-10 text-center text-muted-foreground">{t('permissions.noResources') || 'No resources available'}</td>
144
145
  </tr>
145
146
  {/if}
146
147
  {#each resources as resource, i}
@@ -7,7 +7,7 @@
7
7
  import { toast } from '@svadmin/core/toast';
8
8
  import { AlertCircle } from '@lucide/svelte';
9
9
 
10
- const authProvider = getAuthProvider();
10
+ const authProvider = getAuthProvider({ optional: true });
11
11
  const rawResources = getResources();
12
12
 
13
13
  let roles = $state<RoleInfo[]>([]);
@@ -8,6 +8,9 @@
8
8
  import AboutSettings from './AboutSettings.svelte';
9
9
  import RolesSettings from './RolesSettings.svelte';
10
10
  import AuditLogViewer from './AuditLogViewer.svelte';
11
+ import { getAuthProvider } from '@svadmin/core';
12
+
13
+ const authProvider = getAuthProvider({ optional: true });
11
14
 
12
15
  const sections = [
13
16
  {
@@ -20,26 +23,29 @@
20
23
  {
21
24
  group: 'settings.system',
22
25
  items: [
23
- { key: 'roles', path: '/settings/roles', icon: Shield, label: 'settings.rolesAndPermissions' },
24
- { key: 'audit', path: '/settings/audit', icon: FileSearch, label: 'settings.auditLogs' },
26
+ ...(authProvider ? [
27
+ { key: 'roles', path: '/settings/roles', icon: Shield, label: 'settings.rolesAndPermissions' },
28
+ { key: 'audit', path: '/settings/audit', icon: FileSearch, label: 'settings.auditLogs' },
29
+ ] : []),
25
30
  { key: 'about', path: '/settings/about', icon: Info, label: 'settings.about' },
26
31
  ],
27
32
  },
28
33
  ];
29
34
 
35
+ import { getParams, getRoute } from '../router-state.svelte.js';
36
+
30
37
  let activeKey = $derived.by(() => {
31
- const path = currentPath();
32
- if (path.includes('/settings/appearance')) return 'appearance';
33
- if (path.includes('/settings/roles')) return 'roles';
34
- if (path.includes('/settings/audit')) return 'audit';
35
- if (path.includes('/settings/about')) return 'about';
38
+ const tab = getParams().tab;
39
+ if (tab === 'appearance') return 'appearance';
40
+ if (tab === 'roles') return 'roles';
41
+ if (tab === 'audit') return 'audit';
42
+ if (tab === 'about') return 'about';
36
43
  return 'profile';
37
44
  });
38
45
 
39
46
  // Default redirect to profile
40
47
  $effect(() => {
41
- const path = currentPath();
42
- if (path === '/settings') {
48
+ if (getRoute() === '/settings') {
43
49
  navigate('/settings/profile');
44
50
  }
45
51
  });
@@ -11,6 +11,7 @@
11
11
  import ListButton from './buttons/ListButton.svelte';
12
12
  import EditButton from './buttons/EditButton.svelte';
13
13
  import DeleteButton from './buttons/DeleteButton.svelte';
14
+ import CloneButton from './buttons/CloneButton.svelte';
14
15
  import RefreshButton from './buttons/RefreshButton.svelte';
15
16
 
16
17
  let { resourceName, id } = $props<{ resourceName: string; id: string | number }>();
@@ -28,6 +29,9 @@
28
29
  {#if resource.canEdit !== false}
29
30
  <EditButton resource={resourceName} recordItemId={id} hideText />
30
31
  {/if}
32
+ {#if resource.canCreate !== false}
33
+ <CloneButton resource={resourceName} recordItemId={id} hideText />
34
+ {/if}
31
35
  {#if resource.canDelete !== false}
32
36
  <DeleteButton resource={resourceName} recordItemId={id} hideText onSuccess={() => navigate(`/${resourceName}`)} />
33
37
  {/if}
@@ -1,7 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { getResources, canAccessAsync } from '@svadmin/core';
3
3
  import type { Identity, MenuItem } from '@svadmin/core';
4
- import { currentPath, navigate } from '@svadmin/core/router';
4
+ import { navigate } from '@svadmin/core/router';
5
+ import { getPath } from '../router-state.svelte.js';
5
6
  import { t, getLocale, setLocale, getAvailableLocales } from '@svadmin/core/i18n';
6
7
  import { toggleTheme, getResolvedTheme, colorThemes, getColorTheme, setColorTheme } from '@svadmin/core';
7
8
  import { Button } from './ui/button/index.js';
@@ -118,9 +119,7 @@
118
119
  const localeLabel = $derived(getLocale() === 'zh-CN' ? '中' : 'EN');
119
120
 
120
121
  // Track current hash for active state
121
- let path = $state(currentPath());
122
-
123
- function onHashChange() { path = currentPath(); }
122
+ const path = $derived(getPath());
124
123
 
125
124
  function isActive(itemPath: string): boolean {
126
125
  if (itemPath === '/') return path === '/';
@@ -159,6 +158,26 @@
159
158
  let colorPickerRef = $state<HTMLDivElement | null>(null);
160
159
  let colorPickerOpenedAt = 0;
161
160
 
161
+ // Density state
162
+ let density = $state<'compact' | 'standard'>('standard');
163
+
164
+ $effect(() => {
165
+ if (typeof window !== 'undefined') {
166
+ const stored = localStorage.getItem('svadmin-sidebar-density');
167
+ if (stored === 'compact' || stored === 'standard') density = stored;
168
+
169
+ const onDensityChange = (e: Event) => {
170
+ const customEvent = e as CustomEvent<'compact' | 'standard'>;
171
+ density = customEvent.detail;
172
+ };
173
+ window.addEventListener('svadmin-density-change', onDensityChange);
174
+ return () => window.removeEventListener('svadmin-density-change', onDensityChange);
175
+ }
176
+ });
177
+
178
+ const pyClass = $derived(density === 'compact' ? 'py-1.5' : 'py-2.5');
179
+ const pyClassGroupItem = $derived(density === 'compact' ? 'py-1.5' : 'py-2');
180
+
162
181
  // Click-outside to close color picker
163
182
  $effect(() => {
164
183
  if (!colorPickerOpen) return;
@@ -176,7 +195,7 @@
176
195
  });
177
196
  </script>
178
197
 
179
- <svelte:window onhashchange={onHashChange} />
198
+
180
199
 
181
200
  <aside
182
201
  aria-label="Sidebar navigation"
@@ -235,7 +254,7 @@
235
254
  {@const active = isActive(item.path)}
236
255
  <a
237
256
  href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
238
- class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-all duration-200
257
+ class="flex items-center gap-3 rounded-lg px-3 {pyClassGroupItem} text-sm font-medium transition-all duration-200
239
258
  {active
240
259
  ? 'bg-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active'
241
260
  : 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
@@ -258,7 +277,7 @@
258
277
  <a
259
278
  {...props}
260
279
  href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
261
- class="flex items-center justify-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200
280
+ class="flex items-center justify-center gap-3 rounded-lg px-3 {pyClass} text-sm font-medium transition-all duration-200
262
281
  {active
263
282
  ? 'bg-sidebar-accent text-sidebar-accent-foreground'
264
283
  : 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
@@ -274,7 +293,7 @@
274
293
  {:else}
275
294
  <a
276
295
  href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
277
- class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200
296
+ class="flex items-center gap-3 rounded-lg px-3 {pyClass} text-sm font-medium transition-all duration-200
278
297
  {active
279
298
  ? 'bg-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active'
280
299
  : 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
@@ -9,6 +9,8 @@
9
9
  ChevronDown, Folder, ExternalLink, type Icon as LucideIcon,
10
10
  } from '@lucide/svelte';
11
11
 
12
+ import { formatLink } from '@svadmin/core/router';
13
+
12
14
  let { item, currentPath, collapsed = false, depth = 0 }: {
13
15
  item: MenuItem;
14
16
  currentPath: string;
@@ -37,6 +39,7 @@
37
39
  function isActive(itemHref: string | undefined): boolean {
38
40
  if (!itemHref) return false;
39
41
  if (itemHref === '/') return currentPath === '/';
42
+ // Remove hash/leading slash for comparison if necessary, but currentPath usually starts with /
40
43
  return currentPath.startsWith(itemHref);
41
44
  }
42
45
 
@@ -49,7 +52,9 @@
49
52
  const hasChildren = $derived(item.children && item.children.length > 0);
50
53
  const active = $derived(isActive(item.href));
51
54
  const childActive = $derived(hasActiveChild(item));
52
- const isExternal = $derived(item.target === '_blank');
55
+ const isExternal = $derived(item.target === '_blank' || item.href?.startsWith('http'));
56
+
57
+ const finalHref = $derived(isExternal ? item.href : (item.href ? formatLink(item.href) : undefined));
53
58
 
54
59
  // Track open state — auto-open if a child is active
55
60
  let isOpen = $state(false);
@@ -92,7 +97,7 @@
92
97
  {#snippet child({ props }: { props: Record<string, unknown> })}
93
98
  <a
94
99
  {...props}
95
- href={item.href ? `#${item.href}` : undefined}
100
+ href={finalHref}
96
101
  target={isExternal ? '_blank' : undefined}
97
102
  rel={isExternal ? 'noopener noreferrer' : undefined}
98
103
  class="flex items-center justify-center rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200
@@ -111,7 +116,7 @@
111
116
  {:else}
112
117
  <!-- Leaf node: render as a link -->
113
118
  <a
114
- href={item.href ? `#${item.href}` : undefined}
119
+ href={finalHref}
115
120
  target={isExternal ? '_blank' : undefined}
116
121
  rel={isExternal ? 'noopener noreferrer' : undefined}
117
122
  class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-all duration-200
@@ -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';
@@ -1,5 +1,6 @@
1
1
  import type { RouterProvider } from '@svadmin/core';
2
2
  export declare function initRouter(provider?: RouterProvider): void;
3
3
  export declare function getRoute(): string;
4
+ export declare function getPath(): string;
4
5
  export declare function getParams(): Record<string, string>;
5
6
  export declare function getRouterProviderInstance(): RouterProvider | undefined;
@@ -2,6 +2,7 @@
2
2
  import { matchRoute, setActiveRouterProvider } from '@svadmin/core/router';
3
3
  let _route = $state('/');
4
4
  let _params = $state({});
5
+ let _path = $state('/');
5
6
  let _initialized = false;
6
7
  let _provider;
7
8
  const ROUTES = [
@@ -9,15 +10,19 @@ const ROUTES = [
9
10
  '/register',
10
11
  '/forgot-password',
11
12
  '/update-password',
13
+ '/settings',
14
+ '/settings/:tab',
12
15
  '/',
13
16
  '/:resource',
14
17
  '/:resource/create',
15
18
  '/:resource/edit/:id',
16
19
  '/:resource/show/:id',
20
+ '/:resource/clone/:id',
17
21
  '/:parent/:parentId/:resource',
18
22
  '/:parent/:parentId/:resource/create',
19
23
  '/:parent/:parentId/:resource/edit/:id',
20
24
  '/:parent/:parentId/:resource/show/:id',
25
+ '/:parent/:parentId/:resource/clone/:id',
21
26
  ];
22
27
  function sync() {
23
28
  let path;
@@ -26,12 +31,14 @@ function sync() {
26
31
  path = parsed.pathname || '/';
27
32
  const m = matchRoute(path, ROUTES);
28
33
  _route = m?.route ?? '/';
34
+ _path = path;
29
35
  _params = { ...(m?.params ?? {}), ...parsed.params };
30
36
  }
31
37
  else {
32
38
  path = typeof window !== 'undefined' ? window.location.hash.replace(/^#/, '') || '/' : '/';
33
39
  const m = matchRoute(path, ROUTES);
34
40
  _route = m?.route ?? '/';
41
+ _path = path;
35
42
  _params = m?.params ?? {};
36
43
  }
37
44
  }
@@ -50,6 +57,9 @@ export function initRouter(provider) {
50
57
  export function getRoute() {
51
58
  return _route;
52
59
  }
60
+ export function getPath() {
61
+ return _path;
62
+ }
53
63
  export function getParams() {
54
64
  return _params;
55
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/ui",
3
- "version": "0.30.0",
3
+ "version": "0.30.2",
4
4
  "description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -9,23 +9,22 @@
9
9
  "files": [
10
10
  "dist"
11
11
  ],
12
- "main": "dist/index.js",
13
- "types": "dist/index.d.d.ts",
12
+ "main": "src/index.ts",
13
+ "types": "src/index.ts",
14
14
  "exports": {
15
15
  ".": {
16
- "types": "./dist/index.d.ts",
17
- "default": "./dist/index.js",
18
- "svelte": "./dist/index.js"
16
+ "types": "./src/index.ts",
17
+ "svelte": "./src/index.ts",
18
+ "default": "./src/index.ts"
19
19
  },
20
20
  "./app.css": {
21
- "style": "./dist/app.css",
22
- "default": "./dist/app.css",
23
- "svelte": "./dist/app.css"
21
+ "style": "./src/app.css",
22
+ "default": "./src/app.css"
24
23
  },
25
24
  "./components/*": "./dist/components/*"
26
25
  },
27
26
  "peerDependencies": {
28
- "@svadmin/core": "^0.21.0",
27
+ "@svadmin/core": "^0.21.2",
29
28
  "@tanstack/svelte-query": "^6.0.0",
30
29
  "highlight.js": "^11.11.1",
31
30
  "isomorphic-dompurify": "^3.7.1",
@@ -52,7 +51,8 @@
52
51
  },
53
52
  "dependencies": {
54
53
  "@lucide/svelte": "^1.7.0",
55
- "@tanstack/svelte-table": "^9.0.0-alpha.10",
54
+ "@tanstack/svelte-table": "9.0.0-alpha.32",
55
+ "@tanstack/table-core": "9.0.0-alpha.32",
56
56
  "bits-ui": "^2.17.1",
57
57
  "clsx": "^2.1.1",
58
58
  "cmdk-sv": "^0.0.19",
@@ -71,9 +71,7 @@
71
71
  "bugs": {
72
72
  "url": "https://github.com/zuohuadong/svadmin/issues"
73
73
  },
74
- "devDependencies": {
75
- "@tanstack/table-core": "8.21.3"
76
- },
74
+ "devDependencies": {},
77
75
  "scripts": {
78
76
  "build": "svelte-package -i src -o dist"
79
77
  }
package/src/index.ts ADDED
@@ -0,0 +1,155 @@
1
+ // @svadmin/ui — Pre-built admin UI components
2
+
3
+ // Entry component
4
+ export { default as AdminApp } from './components/AdminApp.svelte';
5
+
6
+ // Admin components
7
+ export { default as AutoTable } from './components/AutoTable.svelte';
8
+ export { default as AutoForm } from './components/AutoForm.svelte';
9
+ export { default as ShowPage } from './components/ShowPage.svelte';
10
+ export { default as ConfirmDialog } from './components/ConfirmDialog.svelte';
11
+ export { default as Sidebar } from './components/Sidebar.svelte';
12
+ export { default as Layout } from './components/Layout.svelte';
13
+ export { default as Header } from './components/Header.svelte';
14
+ export { default as ErrorBoundary } from './components/ErrorBoundary.svelte';
15
+ export { default as Toast } from './components/Toast.svelte';
16
+ export { default as Breadcrumbs } from './components/Breadcrumbs.svelte';
17
+ export { default as FieldRenderer } from './components/FieldRenderer.svelte';
18
+ export { default as EmptyState } from './components/EmptyState.svelte';
19
+ export { default as StatsCard } from './components/StatsCard.svelte';
20
+ export { default as PageHeader } from './components/PageHeader.svelte';
21
+ export { default as LoginPage } from './components/LoginPage.svelte';
22
+ export { default as RegisterPage } from './components/RegisterPage.svelte';
23
+ export { default as ForgotPasswordPage } from './components/ForgotPasswordPage.svelte';
24
+ export { default as CanAccess } from './components/CanAccess.svelte';
25
+ export { default as Can } from './components/CanAccess.svelte';
26
+ export { default as UndoableNotification } from './components/UndoableNotification.svelte';
27
+ export { default as ModalForm } from './components/ModalForm.svelte';
28
+ export { default as DrawerForm } from './components/DrawerForm.svelte';
29
+ export { default as DevTools } from './components/DevTools.svelte';
30
+ export { default as Authenticated } from './components/Authenticated.svelte';
31
+ export { default as UpdatePasswordPage } from './components/UpdatePasswordPage.svelte';
32
+ export { default as ProfilePage } from './components/ProfilePage.svelte';
33
+ export { default as ConfigErrorScreen } from './components/ConfigErrorScreen.svelte';
34
+ export { default as InferencerPanel } from './components/InferencerPanel.svelte';
35
+ export { default as LiveIndicator } from './components/LiveIndicator.svelte';
36
+ export { default as ListPage } from './components/ListPage.svelte';
37
+ export { default as CreatePage } from './components/CreatePage.svelte';
38
+ export { default as EditPage } from './components/EditPage.svelte';
39
+ export { default as AutoSaveIndicator } from './components/AutoSaveIndicator.svelte';
40
+ export { default as ErrorComponent } from './components/ErrorComponent.svelte';
41
+ export { default as PageSkeleton } from './components/PageSkeleton.svelte';
42
+ export { default as NavigateToResource } from './components/NavigateToResource.svelte';
43
+ export { default as CatchAllNavigate } from './components/CatchAllNavigate.svelte';
44
+ export { default as ThemedTitle } from './components/ThemedTitle.svelte';
45
+ export { default as CommandPalette } from './components/CommandPalette.svelte';
46
+ export { default as StepsForm } from './components/StepsForm.svelte';
47
+ export { default as InfiniteList } from './components/InfiniteList.svelte';
48
+ export { default as ComboboxField } from './components/ComboboxField.svelte';
49
+ export { default as PasswordInput } from './components/PasswordInput.svelte';
50
+ export { default as ChatDialog } from "./components/ChatDialog.svelte";
51
+ export { default as MarkdownRenderer } from "./components/MarkdownRenderer.svelte";
52
+ export { default as SmartSuggest } from "./components/SmartSuggest.svelte";
53
+ export { default as AICommandBar } from "./components/AICommandBar.svelte";
54
+ export { default as CopilotPanel } from "./components/CopilotPanel.svelte";
55
+ export { default as InsightCard } from "./components/InsightCard.svelte";
56
+ export { default as AnomalyBadge } from "./components/AnomalyBadge.svelte";
57
+ export { default as VoiceInput } from "./components/VoiceInput.svelte";
58
+ export { default as AnimatedCounter } from "./components/AnimatedCounter.svelte";
59
+ export { default as KeyboardShortcuts } from "./components/KeyboardShortcuts.svelte";
60
+ export { default as InlineEdit } from "./components/InlineEdit.svelte";
61
+ export { default as VirtualTable } from "./components/VirtualTable.svelte";
62
+ export { default as DraggableHeader } from "./components/DraggableHeader.svelte";
63
+ export { default as SettingsPage } from "./components/SettingsPage.svelte";
64
+
65
+ // Editor Registry
66
+ export { setRichTextEditor, getRichTextEditor } from "./editor-config.svelte.js";
67
+
68
+ // Settings & Preferences
69
+ export { default as AppearanceSettings } from "./components/AppearanceSettings.svelte";
70
+ export { default as AboutSettings } from "./components/AboutSettings.svelte";
71
+ export { default as PermissionMatrix } from "./components/PermissionMatrix.svelte";
72
+ export type { RoleInfo, ResourceInfo, ActionInfo } from "./types.js";
73
+ export { default as RolesSettings } from "./components/RolesSettings.svelte";
74
+ export { default as AuditLogViewer } from "./components/AuditLogViewer.svelte";
75
+ export { default as TenantSwitcher } from "./components/TenantSwitcher.svelte";
76
+ export type { Tenant } from "./types.js";
77
+ export { default as TaskQueueDrawer } from "./components/TaskQueueDrawer.svelte";
78
+ export type { BackgroundTask, TaskStatus } from "./types.js";
79
+ export { default as DraggableGrid } from "./components/DraggableGrid.svelte";
80
+ export type { GridModule } from "./types.js";
81
+
82
+ // Dashboard Charts (zero-dependency SVG)
83
+ export { BarChart, LineChart, PieChart } from './components/charts/index.js';
84
+ // Field display components
85
+ export { default as NumberField } from './components/fields/NumberField.svelte';
86
+ export { default as DateField } from './components/fields/DateField.svelte';
87
+ export { default as EmailField } from './components/fields/EmailField.svelte';
88
+ export { default as UrlField } from './components/fields/UrlField.svelte';
89
+ export { default as BooleanField } from './components/fields/BooleanField.svelte';
90
+ export { default as TagField } from './components/fields/TagField.svelte';
91
+ export { default as FileField } from './components/fields/FileField.svelte';
92
+ export { default as MarkdownField } from './components/fields/MarkdownField.svelte';
93
+ export { default as TextField } from './components/fields/TextField.svelte';
94
+ export { default as ImageField } from './components/fields/ImageField.svelte';
95
+ export { default as SelectField } from './components/fields/SelectField.svelte';
96
+ export { default as MultiSelectField } from './components/fields/MultiSelectField.svelte';
97
+ export { default as RelationField } from './components/fields/RelationField.svelte';
98
+ export { default as JsonField } from './components/fields/JsonField.svelte';
99
+ export { default as RichTextField } from './components/fields/RichTextField.svelte';
100
+
101
+ // Rich text editor (requires @svadmin/editor) as a direct import in consumer app
102
+
103
+ // CRUD Buttons
104
+ export {
105
+ CreateButton, EditButton, DeleteButton, ShowButton, ListButton,
106
+ RefreshButton, ExportButton, ImportButton, SaveButton, CloneButton,
107
+ } from './components/buttons/index.js';
108
+
109
+ // Base UI components (shadcn-svelte)
110
+ export { Button, buttonVariants } from './components/ui/button/index.js';
111
+ export { Input } from './components/ui/input/index.js';
112
+ export { Textarea } from './components/ui/textarea/index.js';
113
+ export { Select } from './components/ui/select/index.js';
114
+ export { Switch } from './components/ui/switch/index.js';
115
+ export { Checkbox } from './components/ui/checkbox/index.js';
116
+ export { Badge } from './components/ui/badge/index.js';
117
+ export { Separator } from './components/ui/separator/index.js';
118
+ export { Avatar } from './components/ui/avatar/index.js';
119
+ export { Skeleton } from './components/ui/skeleton/index.js';
120
+ export * as Sheet from './components/ui/sheet/index.js';
121
+ export * as Alert from './components/ui/alert/index.js';
122
+ export * as Card from './components/ui/card/index.js';
123
+ export * as Dialog from './components/ui/dialog/index.js';
124
+ export * as Table from './components/ui/table/index.js';
125
+ export * as Tabs from './components/ui/tabs/index.js';
126
+ export * as Tooltip from './components/ui/tooltip/index.js';
127
+ export * as DropdownMenu from './components/ui/dropdown-menu/index.js';
128
+
129
+ // Additional shadcn components
130
+ export * as Breadcrumb from './components/ui/breadcrumb/index.js';
131
+ export * as Pagination from './components/ui/pagination/index.js';
132
+ export * as ContextMenu from './components/ui/context-menu/index.js';
133
+
134
+ export * as Collapsible from './components/ui/collapsible/index.js';
135
+ export { Label } from './components/ui/label/index.js';
136
+ export { Command } from './components/ui/command/index.js';
137
+
138
+ // Utils
139
+ export { cn } from './utils';
140
+
141
+ // Component Registry (DI)
142
+ export {
143
+ setComponentRegistry, getComponentRegistry, useComponent,
144
+ type ComponentRegistry,
145
+ } from './component-registry.svelte';
146
+
147
+ // Field component registry
148
+ export {
149
+ builtinDisplayComponents, registerDisplayComponent,
150
+ getDisplayComponent, hasDisplayComponent,
151
+ } from './components/fieldComponentMap';
152
+ export type { FieldComponentMap } from './components/fieldComponentMap';
153
+
154
+ // Svelte Actions
155
+ export { clickOutside, shortcut, intersect, copyOnClick } from './actions';