@svadmin/ui 0.38.2 → 0.38.7

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.
Files changed (144) hide show
  1. package/dist/app.css +8 -4
  2. package/dist/app.theme.css +8 -4
  3. package/dist/components/AICommandBar.svelte +16 -15
  4. package/dist/components/AboutSettings.svelte +12 -11
  5. package/dist/components/AdminApp.svelte +144 -70
  6. package/dist/components/AdminApp.svelte.d.ts +6 -3
  7. package/dist/components/ApiSettings.svelte +19 -16
  8. package/dist/components/AppearanceSettings.svelte +18 -16
  9. package/dist/components/AuditLogDrawer.svelte +6 -3
  10. package/dist/components/AuditLogViewer.svelte +18 -15
  11. package/dist/components/Authenticated.svelte +4 -4
  12. package/dist/components/AutoForm.svelte +19 -16
  13. package/dist/components/AutoSaveIndicator.svelte +7 -5
  14. package/dist/components/AutoTable.svelte +185 -159
  15. package/dist/components/AutoTable.svelte.d.ts +1 -1
  16. package/dist/components/Breadcrumbs.svelte +17 -13
  17. package/dist/components/CancelTaskButton.svelte +5 -2
  18. package/dist/components/CatchAllNavigate.svelte +3 -3
  19. package/dist/components/ChatDialog.svelte +22 -19
  20. package/dist/components/ComboboxField.svelte +6 -4
  21. package/dist/components/CommandPalette.svelte +20 -20
  22. package/dist/components/ConfigErrorScreen.svelte +12 -10
  23. package/dist/components/ConfirmDialog.svelte +6 -4
  24. package/dist/components/CreatePage.svelte +4 -2
  25. package/dist/components/DevTools.svelte +11 -14
  26. package/dist/components/DrawerForm.svelte +5 -2
  27. package/dist/components/EditPage.svelte +8 -5
  28. package/dist/components/EmptyState.svelte +5 -3
  29. package/dist/components/ErrorBoundary.svelte +5 -3
  30. package/dist/components/ErrorComponent.svelte +6 -4
  31. package/dist/components/ErrorPage.svelte +12 -10
  32. package/dist/components/FieldRenderer.svelte +17 -15
  33. package/dist/components/ForgotPasswordPage.svelte +22 -19
  34. package/dist/components/Header.svelte +7 -4
  35. package/dist/components/InfiniteList.svelte +6 -3
  36. package/dist/components/IntegrationsSettings.svelte +9 -6
  37. package/dist/components/KeyboardShortcuts.svelte +4 -2
  38. package/dist/components/Layout.svelte +12 -13
  39. package/dist/components/LazyPage.svelte +47 -0
  40. package/dist/components/LazyPage.svelte.d.ts +14 -0
  41. package/dist/components/ListPage.svelte +8 -5
  42. package/dist/components/LoginPage.svelte +24 -22
  43. package/dist/components/MarkdownRenderer.svelte +89 -35
  44. package/dist/components/ModalForm.svelte +4 -2
  45. package/dist/components/NavigateToResource.svelte +3 -3
  46. package/dist/components/NotificationsSettings.svelte +26 -24
  47. package/dist/components/PasswordInput.svelte +4 -2
  48. package/dist/components/PermissionMatrix.svelte +15 -12
  49. package/dist/components/ProfilePage.svelte +23 -21
  50. package/dist/components/RegisterPage.svelte +21 -19
  51. package/dist/components/ResourceOperationsPage.svelte +3 -2
  52. package/dist/components/RetryTaskButton.svelte +5 -2
  53. package/dist/components/RolesSettings.svelte +10 -8
  54. package/dist/components/SecuritySettings.svelte +26 -23
  55. package/dist/components/SettingsPage.svelte +14 -11
  56. package/dist/components/ShowPage.svelte +9 -7
  57. package/dist/components/Sidebar.svelte +36 -28
  58. package/dist/components/SidebarItem.svelte +4 -4
  59. package/dist/components/SidebarItem.svelte.d.ts +1 -1
  60. package/dist/components/StepsForm.svelte +11 -8
  61. package/dist/components/TaskDetails.svelte +14 -11
  62. package/dist/components/TaskList.svelte +14 -11
  63. package/dist/components/TaskQueueDrawer.svelte +34 -32
  64. package/dist/components/TaskStatusBadge.svelte +5 -2
  65. package/dist/components/Toast.svelte +18 -4
  66. package/dist/components/TooltipButton.svelte +25 -3
  67. package/dist/components/TwoFactorAuthPage.svelte +26 -24
  68. package/dist/components/UndoableNotification.svelte +5 -2
  69. package/dist/components/UpdatePasswordPage.svelte +18 -15
  70. package/dist/components/VirtualTable.svelte +0 -4
  71. package/dist/components/account/CompanyProfilePage.svelte +13 -11
  72. package/dist/components/account/GetStartedPage.svelte +15 -13
  73. package/dist/components/account/ImportMembersPage.svelte +14 -12
  74. package/dist/components/account/MembersStarterPage.svelte +10 -8
  75. package/dist/components/account/SecurityLogPage.svelte +24 -22
  76. package/dist/components/account/SettingsEnterprisePage.svelte +6 -4
  77. package/dist/components/account/SettingsPlainPage.svelte +14 -12
  78. package/dist/components/account/SettingsSidebarPage.svelte +11 -9
  79. package/dist/components/account/TeamMembersPage.svelte +10 -8
  80. package/dist/components/buttons/CloneButton.svelte +4 -2
  81. package/dist/components/buttons/CreateButton.svelte +4 -2
  82. package/dist/components/buttons/DeleteButton.svelte +6 -4
  83. package/dist/components/buttons/EditButton.svelte +4 -2
  84. package/dist/components/buttons/ExportButton.svelte +4 -2
  85. package/dist/components/buttons/ImportButton.svelte +4 -2
  86. package/dist/components/buttons/RefreshButton.svelte +4 -2
  87. package/dist/components/buttons/SaveButton.svelte +4 -2
  88. package/dist/components/buttons/ShowButton.svelte +4 -2
  89. package/dist/components/fieldComponentMap.js +0 -3
  90. package/dist/components/fields/ArrayField.svelte +6 -4
  91. package/dist/components/fields/JsonField.svelte +5 -3
  92. package/dist/components/fields/RelationField.svelte +3 -3
  93. package/dist/components/fields/RichTextField.svelte +4 -2
  94. package/dist/components/network/TeamCrewTablePage.svelte +18 -16
  95. package/dist/components/network/UserCardsNFTPage.svelte +11 -9
  96. package/dist/components/profile/ActivityTimeline.svelte +10 -8
  97. package/dist/components/profile/ProfileCard.svelte +13 -11
  98. package/dist/components/profile/ProjectsGrid.svelte +7 -5
  99. package/dist/components/profile/PublicProfilePage.svelte +18 -16
  100. package/dist/components/profile/TeamsShowcase.svelte +7 -5
  101. package/dist/components/toast-host.svelte.d.ts +5 -0
  102. package/dist/components/toast-host.svelte.js +21 -0
  103. package/dist/components/ui/command/command-dialog.svelte +29 -13
  104. package/dist/components/ui/command/command-dialog.svelte.d.ts +4 -3
  105. package/dist/components/ui/command/command-empty.svelte +18 -0
  106. package/dist/components/ui/command/command-empty.svelte.d.ts +4 -0
  107. package/dist/components/ui/command/command-group.svelte +41 -0
  108. package/dist/components/ui/command/command-group.svelte.d.ts +8 -0
  109. package/dist/components/ui/command/command-input.svelte +23 -0
  110. package/dist/components/ui/command/command-input.svelte.d.ts +4 -0
  111. package/dist/components/ui/command/command-item.svelte +43 -0
  112. package/dist/components/ui/command/command-item.svelte.d.ts +11 -0
  113. package/dist/components/ui/command/command-list.svelte +18 -0
  114. package/dist/components/ui/command/command-list.svelte.d.ts +4 -0
  115. package/dist/components/ui/command/command-loading.svelte +12 -0
  116. package/dist/components/ui/command/command-loading.svelte.d.ts +4 -0
  117. package/dist/components/ui/command/command-separator.svelte +23 -0
  118. package/dist/components/ui/command/command-separator.svelte.d.ts +7 -0
  119. package/dist/components/ui/command/command.svelte +26 -0
  120. package/dist/components/ui/command/command.svelte.d.ts +4 -0
  121. package/dist/components/ui/command/index.d.ts +55 -2
  122. package/dist/components/ui/command/index.js +21 -2
  123. package/dist/components/ui/sheet/sheet-content.svelte +1 -4
  124. package/dist/components/ui/sheet/sheet-content.svelte.d.ts +1 -3
  125. package/dist/components/ui/tabs/tabs-content.svelte +6 -2
  126. package/dist/components/ui/tabs/tabs-context.svelte.d.ts +5 -0
  127. package/dist/components/ui/tabs/tabs-context.svelte.js +2 -0
  128. package/dist/components/ui/tabs/tabs-trigger.svelte +49 -4
  129. package/dist/components/ui/tabs/tabs.svelte +12 -1
  130. package/dist/components/ui/tooltip/index.d.ts +4 -1
  131. package/dist/components/ui/tooltip/index.js +3 -1
  132. package/dist/components/ui/tooltip/tooltip-content.svelte +308 -35
  133. package/dist/components/ui/tooltip/tooltip-content.svelte.d.ts +42 -3
  134. package/dist/components/ui/tooltip/tooltip-context.svelte.d.ts +37 -0
  135. package/dist/components/ui/tooltip/tooltip-context.svelte.js +220 -0
  136. package/dist/components/ui/tooltip/tooltip-layer-registry.svelte.d.ts +11 -0
  137. package/dist/components/ui/tooltip/tooltip-layer-registry.svelte.js +93 -0
  138. package/dist/components/ui/tooltip/tooltip-trigger.svelte +56 -3
  139. package/dist/components/ui/tooltip/tooltip-trigger.svelte.d.ts +23 -2
  140. package/dist/components/ui/tooltip/tooltip.svelte +63 -3
  141. package/dist/components/ui/tooltip/tooltip.svelte.d.ts +23 -2
  142. package/dist/router-state.svelte.d.ts +13 -0
  143. package/dist/router-state.svelte.js +72 -37
  144. package/package.json +14 -11
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { untrack } from 'svelte';
2
+ import { onDestroy, untrack } from 'svelte';
3
3
  import { cn } from '../utils.js';
4
4
  import {
5
5
  createTable,
@@ -10,33 +10,38 @@
10
10
  type ColumnVisibilityState,
11
11
  type ExpandedState,
12
12
  } from '@tanstack/svelte-table';
13
- import type { Updater } from '@tanstack/table-core';
13
+ import type { Column, Header, TableFeatures } from '@tanstack/table-core';
14
+ import { createAtom, useSelector } from '@tanstack/svelte-store';
14
15
  import {
15
16
  column_getCanSort,
16
17
  column_getIsSorted,
17
18
  column_toggleSorting,
18
- column_getIsVisible,
19
19
  column_toggleVisibility,
20
20
  header_getSize,
21
21
  table_getRowModel,
22
- table_getHeaderGroups,
23
22
  table_getAllLeafColumns,
24
23
  table_getIsAllRowsSelected,
25
24
  table_toggleAllRowsSelected,
26
25
  row_getIsSelected,
27
26
  row_toggleSelected,
28
27
  row_getVisibleCells,
29
- row_getIsExpanded,
30
28
  row_toggleExpanded,
31
29
  cell_getValue,
32
30
  } from '@tanstack/table-core/static-functions';
33
31
 
34
- import { useList, useDelete, useDeleteMany, getResource, notify } from '@svadmin/core';
35
- import type { Pagination as PaginationState, Sort, Filter, FieldDefinition } from '@svadmin/core';
36
- import { navigate } from '@svadmin/core/router';
32
+ import { captureAdminContext, useList, useDelete, useDeleteMany, getResource, notify } from '@svadmin/core';
33
+ import type {
34
+ BaseRecord,
35
+ FieldDefinition,
36
+ Filter,
37
+ LogicalFilter,
38
+ Pagination as PaginationState,
39
+ Sort,
40
+ } from '@svadmin/core';
37
41
  import { useCan, getAccessControlProvider } from '@svadmin/core';
38
42
  import { readURLState, writeURLState } from '@svadmin/core';
39
- import { t } from '@svadmin/core/i18n';
43
+ import { useTranslation } from '@svadmin/core/i18n';
44
+
40
45
  import { fade } from 'svelte/transition';
41
46
  import { Button } from './ui/button/index.js';
42
47
  import { Input } from './ui/input/index.js';
@@ -50,7 +55,7 @@
50
55
  import * as ContextMenu from './ui/context-menu/index.js';
51
56
  import {
52
57
  Plus, Pencil, Trash2,
53
- Search, Download, Upload, ChevronDown, ChevronUp, SlidersHorizontal, Filter as FilterIcon,
58
+ Search, Download, ChevronDown, ChevronUp, SlidersHorizontal, Filter as FilterIcon,
54
59
  Eye, Copy
55
60
  } from '@lucide/svelte';
56
61
  import ConfirmDialog from './ConfirmDialog.svelte';
@@ -59,6 +64,8 @@
59
64
  import DraggableHeader from './DraggableHeader.svelte';
60
65
  import type { Snippet } from 'svelte';
61
66
 
67
+ const i18n = useTranslation();
68
+
62
69
  // ─── Props with Snippet composability ─────────────────────────
63
70
  interface Props {
64
71
  resourceName: string;
@@ -96,12 +103,13 @@
96
103
  pagination: externalPagination,
97
104
  sorters: externalSorters,
98
105
  }: Props = $props();
106
+ const adminContext = captureAdminContext();
99
107
 
100
108
  const resource = $derived(getResource(resourceName));
101
109
  const primaryKey = $derived(resource.primaryKey ?? 'id');
102
110
 
103
111
  // ─── URL state + server-side state ────────────────────────────
104
- const urlState = readURLState();
112
+ const urlState = readURLState(adminContext);
105
113
 
106
114
  // Snapshot resource values for initial state (untrack to avoid reactive tracking)
107
115
  const storedPageSize = typeof window !== 'undefined' ? parseInt(localStorage.getItem('svadmin-default-page-size') ?? '', 10) : NaN;
@@ -120,6 +128,22 @@
120
128
  );
121
129
  let filters = $state<Filter[]>([]);
122
130
  let searchText = $state(urlState.search ?? '');
131
+ let appliedSearchText = $state(untrack(() => searchText));
132
+ let searchDebounceTimer: ReturnType<typeof setTimeout> | undefined;
133
+
134
+ function scheduleSearch(event: Event) {
135
+ searchText = (event.currentTarget as HTMLInputElement).value;
136
+ if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
137
+ searchDebounceTimer = setTimeout(() => {
138
+ appliedSearchText = searchText;
139
+ pagination = { ...pagination, current: 1 };
140
+ searchDebounceTimer = undefined;
141
+ }, 300);
142
+ }
143
+
144
+ onDestroy(() => {
145
+ if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
146
+ });
123
147
 
124
148
  // Sync external controlled state
125
149
  $effect(() => {
@@ -141,8 +165,8 @@
141
165
  pageSize: pagination.pageSize,
142
166
  sortField: sorters[0]?.field,
143
167
  sortOrder: sorters[0]?.order,
144
- search: searchText || undefined,
145
- });
168
+ search: appliedSearchText || undefined,
169
+ }, adminContext);
146
170
  });
147
171
 
148
172
  // ─── Build active filters with search ─────────────────────────
@@ -152,18 +176,19 @@
152
176
  const activeFilterCount = $derived(Object.values(filterValues).filter(v => v.trim()).length);
153
177
  const activeFilters = $derived.by(() => {
154
178
  const result: Filter[] = [...filters];
155
- if (searchText.trim() && searchableFields.length > 0) {
179
+ if (appliedSearchText.trim() && searchableFields.length > 0) {
156
180
  if (searchableFields.length === 1) {
157
- result.push({ field: searchableFields[0].key, operator: 'contains', value: searchText });
181
+ result.push({ field: searchableFields[0].key, operator: 'contains', value: appliedSearchText });
158
182
  } else {
159
- result.push({
183
+ const searchFilter: LogicalFilter = {
160
184
  operator: 'or',
161
185
  value: searchableFields.map(f => ({
162
186
  field: f.key,
163
187
  operator: 'contains',
164
- value: searchText
188
+ value: appliedSearchText
165
189
  }))
166
- } as any);
190
+ };
191
+ result.push(searchFilter);
167
192
  }
168
193
  }
169
194
  for (const [key, value] of Object.entries(filterValues)) {
@@ -240,10 +265,10 @@
240
265
  const canExport = $derived(canExportPerm.allowed);
241
266
 
242
267
  // ─── TanStack Table state ────────────────────────────────────
243
- let sorting = $state<SortingState>(untrack(() =>
268
+ const initialSorting = untrack<SortingState>(() =>
244
269
  sorters.map(s => ({ id: s.field, desc: s.order === 'desc' }))
245
- ));
246
- let columnVisibility = $state<ColumnVisibilityState>((() => {
270
+ );
271
+ const initialColumnVisibility = (() => {
247
272
  // Try to restore from localStorage first
248
273
  const storageKey = `svadmin-columns-${resourceName}`;
249
274
  if (typeof window !== 'undefined') {
@@ -258,79 +283,41 @@
258
283
  if (f.showInList === false) vis[f.key] = false;
259
284
  }
260
285
  return vis;
261
- })());
262
- let rowSelection = $state<RowSelectionState>({});
263
- let expanded = $state.raw<ExpandedState>({});
264
- let columnOrder = $state<string[]>([]);
265
-
266
- function applyTableUpdater<T>(current: T, updater: Updater<T>): T {
267
- return typeof updater === 'function' ? (updater as (old: T) => T)(current) : updater;
286
+ })();
287
+ const sortingAtom = createAtom(initialSorting);
288
+ const columnVisibilityAtom = createAtom(initialColumnVisibility);
289
+ const rowSelectionAtom = createAtom({} as RowSelectionState);
290
+ const expandedAtom = createAtom({} as ExpandedState);
291
+ const columnOrderAtom = createAtom([] as string[]);
292
+
293
+ const tableSorting = useSelector(sortingAtom);
294
+ const tableColumnVisibility = useSelector(columnVisibilityAtom);
295
+ const tableRowSelection = useSelector(rowSelectionAtom);
296
+ const tableExpanded = useSelector(expandedAtom);
297
+ const tableColumnOrder = useSelector(columnOrderAtom);
298
+
299
+ function rowIsExpanded(rowId: string): boolean {
300
+ const expanded = tableExpanded.current;
301
+ return expanded === true || expanded[rowId] === true;
268
302
  }
269
303
 
270
- const tableSorting = $derived<SortingState>(sorting.map(sorter => ({
271
- id: sorter.id,
272
- desc: sorter.desc,
273
- })));
274
- const tableColumnVisibility = $derived<ColumnVisibilityState>({ ...columnVisibility });
275
- const tableRowSelection = $derived<RowSelectionState>({ ...rowSelection });
276
- const tableExpanded = $derived<ExpandedState>(expanded === true ? true : { ...expanded });
277
- const tableColumnOrder = $derived<string[]>([...columnOrder]);
278
-
279
- const sortingAtom = {
280
- get: () => tableSorting,
281
- set: (updater: Updater<SortingState>) => {
282
- const nextSorting = applyTableUpdater(tableSorting, updater);
283
- sorting = nextSorting;
284
-
285
- const nextSorters: Sort[] = nextSorting.map((s) => ({
286
- field: s.id,
287
- order: s.desc ? 'desc' as const : 'asc' as const,
288
- }));
289
-
290
- if (JSON.stringify(nextSorters) !== JSON.stringify(sorters)) {
291
- sorters = nextSorters;
292
- }
293
-
294
- if ((pagination.current ?? 1) !== 1) {
295
- pagination = { ...pagination, current: 1 };
296
- }
297
- },
298
- };
299
-
300
- const columnVisibilityAtom = {
301
- get: () => tableColumnVisibility,
302
- set: (updater: Updater<ColumnVisibilityState>) => {
303
- columnVisibility = applyTableUpdater(tableColumnVisibility, updater);
304
- },
305
- };
306
-
307
- const rowSelectionAtom = {
308
- get: () => tableRowSelection,
309
- set: (updater: Updater<RowSelectionState>) => {
310
- rowSelection = applyTableUpdater(tableRowSelection, updater);
311
- },
312
- };
313
-
314
- const expandedAtom = {
315
- get: () => tableExpanded,
316
- set: (updater: Updater<ExpandedState>) => {
317
- expanded = applyTableUpdater(tableExpanded, updater);
318
- },
319
- };
304
+ $effect(() => {
305
+ const nextSorters: Sort[] = tableSorting.current.map((sorter) => ({
306
+ field: sorter.id,
307
+ order: sorter.desc ? 'desc' as const : 'asc' as const,
308
+ }));
320
309
 
321
- const columnOrderAtom = {
322
- get: () => tableColumnOrder,
323
- set: (updater: Updater<string[]>) => {
324
- columnOrder = applyTableUpdater(tableColumnOrder, updater);
325
- },
326
- };
310
+ if (JSON.stringify(nextSorters) !== JSON.stringify(sorters)) {
311
+ sorters = nextSorters;
312
+ }
313
+ });
327
314
 
328
315
  // Persist column visibility to localStorage
329
316
  $effect(() => {
330
317
  const storageKey = `svadmin-columns-${resourceName}`;
331
318
  if (typeof window !== 'undefined') {
332
319
  try {
333
- localStorage.setItem(storageKey, JSON.stringify(columnVisibility));
320
+ localStorage.setItem(storageKey, JSON.stringify(tableColumnVisibility.current));
334
321
  } catch { /* ignore quota errors */ }
335
322
  }
336
323
  });
@@ -342,8 +329,8 @@
342
329
  sorters = externalSorters;
343
330
  }
344
331
  const nextSorting = externalSorters.map(s => ({ id: s.field, desc: s.order === 'desc' }));
345
- if (JSON.stringify(nextSorting) !== JSON.stringify(sorting)) {
346
- sorting = nextSorting;
332
+ if (JSON.stringify(nextSorting) !== JSON.stringify(tableSorting.current)) {
333
+ sortingAtom.set(nextSorting);
347
334
  }
348
335
  });
349
336
 
@@ -353,7 +340,7 @@
353
340
  resource.fields.filter(f => f.showInList !== false)
354
341
  );
355
342
 
356
- const columns = $derived<ColumnDef<any, Record<string, unknown>>[]>([
343
+ const columns = $derived<ColumnDef<TableFeatures, BaseRecord, unknown>[]>([
357
344
  // Selection column
358
345
  ...(selectable && (canDelete || batchActions) ? [{
359
346
  id: '_select',
@@ -361,7 +348,7 @@
361
348
  cell: () => '',
362
349
  size: 40,
363
350
  enableSorting: false,
364
- } satisfies ColumnDef<any, Record<string, unknown>>] : []),
351
+ } satisfies ColumnDef<TableFeatures, BaseRecord, unknown>] : []),
365
352
  // Expand column
366
353
  ...(expandedRowRender ? [{
367
354
  id: '_expand',
@@ -369,9 +356,9 @@
369
356
  cell: () => '',
370
357
  size: 40,
371
358
  enableSorting: false,
372
- } satisfies ColumnDef<any, Record<string, unknown>>] : []),
359
+ } satisfies ColumnDef<TableFeatures, BaseRecord, unknown>] : []),
373
360
  // Data columns
374
- ...visibleFields.map((field): ColumnDef<any, Record<string, unknown>> => ({
361
+ ...visibleFields.map((field): ColumnDef<TableFeatures, BaseRecord, unknown> => ({
375
362
  id: field.key,
376
363
  accessorKey: field.key,
377
364
  header: () => field.label,
@@ -380,33 +367,72 @@
380
367
  // Actions column
381
368
  {
382
369
  id: '_actions',
383
- header: () => t('common.actions'),
370
+ header: () => i18n.t('common.actions'),
384
371
  cell: () => '',
385
372
  size: 100,
386
373
  enableSorting: false,
387
374
  },
388
375
  ]);
376
+ const orderedColumns = $derived.by(() => {
377
+ const columnOrder = tableColumnOrder.current;
378
+ if (!columnOrder.length) return columns;
379
+
380
+ const columnsById = new Map(columns.map((column) => [column.id, column]));
381
+ return [
382
+ ...columnOrder.flatMap((id) => {
383
+ const column = columnsById.get(id);
384
+ return column ? [column] : [];
385
+ }),
386
+ ...columns.filter((column) => !columnOrder.includes(column.id ?? '')),
387
+ ];
388
+ });
389
389
 
390
390
  // ─── Create TanStack Table ────────────────────────────────────
391
- const tbl = createTable({
392
- get data() { return query.data?.data ?? []; },
393
- get columns() { return columns; },
394
- getCoreRowModel: createCoreRowModel(),
395
- manualPagination: true,
396
- manualSorting: true,
397
- getRowId: (row: any) => String(row[primaryKey]),
398
- atoms: {
399
- sorting: sortingAtom,
400
- columnVisibility: columnVisibilityAtom,
401
- rowSelection: rowSelectionAtom,
402
- expanded: expandedAtom,
403
- columnOrder: columnOrderAtom,
391
+ const tbl = createTable(
392
+ {
393
+ get data() { return query.data?.data ?? []; },
394
+ get columns() { return orderedColumns; },
395
+ getCoreRowModel: createCoreRowModel(),
396
+ manualPagination: true,
397
+ manualSorting: true,
398
+ getRowId: (row: BaseRecord) => String(row[primaryKey]),
399
+ atoms: {
400
+ sorting: sortingAtom,
401
+ columnVisibility: columnVisibilityAtom,
402
+ rowSelection: rowSelectionAtom,
403
+ expanded: expandedAtom,
404
+ },
405
+ onSortingChange: sortingAtom.set,
406
+ onColumnVisibilityChange: columnVisibilityAtom.set,
407
+ onRowSelectionChange: rowSelectionAtom.set,
408
+ onExpandedChange: expandedAtom.set,
409
+ get enableRowSelection() { return selectable && (canDelete || batchActions); },
410
+ get enableExpanding() { return !!expandedRowRender; },
411
+ getRowCanExpand: () => !!expandedRowRender,
404
412
  },
405
- get enableRowSelection() { return selectable && (canDelete || batchActions); },
406
- get enableExpanding() { return !!expandedRowRender; },
413
+ // AutoTable reads focused atoms directly; selecting the full state triggers Svelte proxy equality warnings.
414
+ () => undefined,
415
+ );
416
+
417
+ $effect.pre(() => {
418
+ const nextColumns = orderedColumns;
419
+ untrack(() => {
420
+ tbl.setOptions({ ...tbl.options, columns: nextColumns });
421
+ });
407
422
  });
408
423
 
409
- const selectedCount = $derived(Object.keys(rowSelection).length);
424
+ const selectedCount = $derived(Object.keys(tableRowSelection.current).length);
425
+ const tableView = $derived.by(() => {
426
+ void tableSorting.current;
427
+ void tableColumnVisibility.current;
428
+ void tableRowSelection.current;
429
+ void tableExpanded.current;
430
+ void tableColumnOrder.current;
431
+ return {
432
+ headerGroups: tbl.getHeaderGroups(),
433
+ rows: table_getRowModel(tbl).rows,
434
+ };
435
+ });
410
436
  const totalPages = $derived(Math.ceil((query.data?.total ?? 0) / (pagination.pageSize ?? 10)));
411
437
 
412
438
  // ─── Pagination helpers ───────────────────────────────────────
@@ -433,7 +459,7 @@
433
459
  let confirmAction = $state<() => void>(() => {});
434
460
 
435
461
  function confirmDelete(id: string | number) {
436
- confirmMessage = t('common.deleteConfirm');
462
+ confirmMessage = i18n.t('common.deleteConfirm');
437
463
  confirmAction = async () => {
438
464
  await deleteMutation.mutateAsync({ id, resource: resourceName });
439
465
  confirmOpen = false;
@@ -442,16 +468,16 @@
442
468
  }
443
469
 
444
470
  function confirmBatchDelete() {
445
- const ids = Object.keys(rowSelection);
446
- confirmMessage = t('common.batchDeleteConfirm', { count: ids.length });
471
+ const ids = Object.keys(tableRowSelection.current);
472
+ confirmMessage = i18n.t('common.batchDeleteConfirm', { count: ids.length });
447
473
  confirmAction = async () => {
448
474
  try {
449
475
  await deleteManyMutation.mutateAsync({ ids, resource: resourceName });
450
- notify({ type: 'success', message: t('common.batchDeleteSuccess', { count: ids.length }) });
476
+ notify({ type: 'success', message: i18n.t('common.batchDeleteSuccess', { count: ids.length }) });
451
477
  } catch {
452
- notify({ type: 'error', message: t('common.batchDeletePartialFail', { failed: 1, total: ids.length }) });
478
+ notify({ type: 'error', message: i18n.t('common.batchDeletePartialFail', { failed: 1, total: ids.length }) });
453
479
  }
454
- rowSelection = {};
480
+ rowSelectionAtom.set({});
455
481
  confirmOpen = false;
456
482
  };
457
483
  confirmOpen = true;
@@ -491,7 +517,7 @@
491
517
  <div class="flex flex-wrap items-center gap-2">
492
518
  {#if canExport}
493
519
  <Button variant="outline" size="sm" onclick={exportCSV}>
494
- <Download class="h-4 w-4" data-icon="inline-start" /> {t('common.export')}
520
+ <Download class="h-4 w-4" data-icon="inline-start" /> {i18n.t('common.export')}
495
521
  </Button>
496
522
  {/if}
497
523
  {#if selectedCount > 0 && canDelete}
@@ -501,12 +527,12 @@
501
527
  class="h-9 whitespace-nowrap"
502
528
  onclick={confirmBatchDelete}
503
529
  >
504
- <Trash2 class="h-4 w-4" data-icon="inline-start" /> {t('common.batchDelete', { count: selectedCount })}
530
+ <Trash2 class="h-4 w-4" data-icon="inline-start" /> {i18n.t('common.batchDelete', { count: selectedCount })}
505
531
  </Button>
506
532
  {/if}
507
533
 
508
534
  {#if selectedCount > 0 && batchActions}
509
- {@render batchActions({ selectedIds: Object.keys(rowSelection) })}
535
+ {@render batchActions({ selectedIds: Object.keys(tableRowSelection.current) })}
510
536
  {/if}
511
537
 
512
538
  <!-- Column Visibility Picker (DropdownMenu) -->
@@ -514,14 +540,14 @@
514
540
  <DropdownMenu.Trigger>
515
541
  {#snippet child({ props })}
516
542
  <Button variant="outline" size="sm" {...props}>
517
- <SlidersHorizontal class="h-4 w-4" data-icon="inline-start" /> {t('common.columns') || 'Columns'}
543
+ <SlidersHorizontal class="h-4 w-4" data-icon="inline-start" /> {i18n.t('common.columns')}
518
544
  </Button>
519
545
  {/snippet}
520
546
  </DropdownMenu.Trigger>
521
547
  <DropdownMenu.Content align="end" class="w-48">
522
- {#each table_getAllLeafColumns(tbl).filter((c: any) => !c.id.startsWith('_')) as column, _i (_i)}
548
+ {#each table_getAllLeafColumns(tbl).filter((column: Column<TableFeatures, BaseRecord, unknown>) => !column.id.startsWith('_')) as column, _i (_i)}
523
549
  <DropdownMenu.CheckboxItem
524
- checked={column_getIsVisible(column)}
550
+ checked={tableColumnVisibility.current[column.id] ?? true}
525
551
  onCheckedChange={(v) => column_toggleVisibility(column, !!v)}
526
552
  >
527
553
  {visibleFields.find(f => f.key === column.id)?.label ?? column.id}
@@ -533,8 +559,8 @@
533
559
  {@render headerActions()}
534
560
  {/if}
535
561
  {#if canCreate}
536
- <Button onclick={() => navigate(`/${resourceName}/create`)}>
537
- <Plus class="h-4 w-4" data-icon="inline-start" /> {t('common.create')}
562
+ <Button onclick={() => adminContext.navigate(`/${resourceName}/create`)}>
563
+ <Plus class="h-4 w-4" data-icon="inline-start" /> {i18n.t('common.create')}
538
564
  </Button>
539
565
  {/if}
540
566
  </div>
@@ -547,9 +573,9 @@
547
573
  <Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
548
574
  <Input
549
575
  type="text"
550
- bind:value={searchText}
551
- oninput={() => { pagination = { ...pagination, current: 1 }; }}
552
- placeholder={t('common.search')}
576
+ value={searchText}
577
+ oninput={scheduleSearch}
578
+ placeholder={i18n.t('common.search')}
553
579
  class="pl-10 h-9"
554
580
  />
555
581
  </div>
@@ -561,7 +587,7 @@
561
587
  {#snippet child({ props })}
562
588
  <Button variant="outline" size="sm" class="h-9 px-3" {...props}>
563
589
  <FilterIcon class="h-4 w-4" data-icon="inline-start" />
564
- {t('common.filter')}
590
+ {i18n.t('common.filter')}
565
591
  {#if activeFilterCount > 0}
566
592
  <Badge variant="secondary" class="ml-1 h-5 min-w-5 px-1">{activeFilterCount}</Badge>
567
593
  {/if}
@@ -570,7 +596,7 @@
570
596
  </Popover.Trigger>
571
597
  <Popover.Content class="w-80">
572
598
  <div class="space-y-3">
573
- <h4 class="font-medium text-sm">{t('common.filter')}</h4>
599
+ <h4 class="font-medium text-sm">{i18n.t('common.filter')}</h4>
574
600
  {#each filterableFields as field, _i (_i)}
575
601
  <div class="space-y-1">
576
602
  <label class="text-xs text-muted-foreground" for="filter-{field.key}">{field.label}</label>
@@ -581,7 +607,7 @@
581
607
  value={filterValues[field.key] ?? ''}
582
608
  onchange={(e) => filterValues[field.key] = (e.currentTarget as HTMLSelectElement).value}
583
609
  >
584
- <option value="">{t('common.all')}</option>
610
+ <option value="">{i18n.t('common.all')}</option>
585
611
  {#each field.options as opt, _i (_i)}
586
612
  <option value={opt.value}>{opt.label}</option>
587
613
  {/each}
@@ -600,13 +626,13 @@
600
626
  {/each}
601
627
  <div class="flex gap-2 pt-2">
602
628
  <Button size="sm" class="flex-1" onclick={() => { pagination = { ...pagination, current: 1 }; }}>
603
- {t('common.confirm')}
629
+ {i18n.t('common.confirm')}
604
630
  </Button>
605
631
  <Button variant="outline" size="sm" onclick={() => {
606
632
  filterValues = {};
607
633
  pagination = { ...pagination, current: 1 };
608
634
  }}>
609
- {t('common.reset')}
635
+ {i18n.t('common.reset')}
610
636
  </Button>
611
637
  </div>
612
638
  </div>
@@ -616,7 +642,7 @@
616
642
  </div>
617
643
 
618
644
  <!-- Table (TanStack-powered) -->
619
- <div class="rounded-[24px] bg-card/80 backdrop-blur-3xl shadow-[0_8px_30px_rgb(0,0,0,0.04)] overflow-hidden ring-1 ring-border/30" role="region" aria-label="{resource.label} {t('common.list')}">
645
+ <div class="rounded-[24px] bg-card/80 backdrop-blur-3xl shadow-[0_8px_30px_rgb(0,0,0,0.04)] overflow-hidden ring-1 ring-border/30" role="region" aria-label="{resource.label} {i18n.t('common.list')}">
620
646
  {#if query.isLoading}
621
647
  <div class="p-4 space-y-3">
622
648
  <div class="flex gap-4 mb-2">
@@ -634,7 +660,7 @@
634
660
  </div>
635
661
  {:else if query.error}
636
662
  <div class="flex h-64 items-center justify-center text-destructive text-sm">
637
- {t('common.loadFailed', { message: (query.error as Error).message })}
663
+ {i18n.t('common.loadFailed', { message: (query.error as Error).message })}
638
664
  </div>
639
665
  {:else}
640
666
  <div in:fade={{ duration: 150 }}>
@@ -642,11 +668,11 @@
642
668
  <div class="hidden md:block">
643
669
  <Table.Root>
644
670
  <Table.Header>
645
- {#each table_getHeaderGroups(tbl) as headerGroup, _i (_i)}
671
+ {#each tableView.headerGroups as headerGroup, _i (_i)}
646
672
  <DraggableHeader
647
- columns={headerGroup.headers.map((h: any) => ({ id: h.column.id, header: h }))}
673
+ columns={headerGroup.headers.map((header: Header<TableFeatures, BaseRecord, unknown>) => ({ id: header.column.id, header }))}
648
674
  resourceName={resourceName}
649
- onReorder={(newOrder) => { columnOrder = newOrder.map((c: any) => c.id); }}
675
+ onReorder={(newOrder) => columnOrderAtom.set(newOrder.map((column) => column.id))}
650
676
  >
651
677
  {#snippet header(col, _index, dragProps)}
652
678
  {@const header = col.header as typeof headerGroup.headers[0]}
@@ -663,7 +689,7 @@
663
689
  {:else if header.id === '_expand'}
664
690
  <!-- empty -->
665
691
  {:else if header.id === '_actions'}
666
- <span class="text-right block">{t('common.actions')}</span>
692
+ <span class="text-right block">{i18n.t('common.actions')}</span>
667
693
  {:else if column_getCanSort(header.column)}
668
694
  <Button
669
695
  variant="ghost"
@@ -688,7 +714,7 @@
688
714
  {/each}
689
715
  </Table.Header>
690
716
  <Table.Body>
691
- {#each table_getRowModel(tbl).rows as row, _i (_i)}
717
+ {#each tableView.rows as row, _i (_i)}
692
718
  {@const record = row.original}
693
719
  {@const id = record[primaryKey] as string | number}
694
720
  <ContextMenu.Root>
@@ -703,8 +729,8 @@
703
729
  onCheckedChange={() => row_toggleSelected(row)}
704
730
  />
705
731
  {:else if cell.column.id === '_expand'}
706
- <TooltipButton tooltip={row_getIsExpanded(row) ? t('common.collapse') : t('common.expand')} variant="ghost" size="icon" class="h-7 w-7" onclick={() => row_toggleExpanded(row)}>
707
- {#if row_getIsExpanded(row)}
732
+ <TooltipButton tooltip={rowIsExpanded(row.id) ? i18n.t('common.collapse') : i18n.t('common.expand')} variant="ghost" size="icon" class="h-7 w-7" onclick={() => row_toggleExpanded(row)}>
733
+ {#if rowIsExpanded(row.id)}
708
734
  <ChevronUp class="h-4 w-4" />
709
735
  {:else}
710
736
  <ChevronDown class="h-4 w-4" />
@@ -716,12 +742,12 @@
716
742
  {@render rowActions({ record, id })}
717
743
  {:else}
718
744
  {#if canEdit}
719
- <TooltipButton tooltip={t('common.edit')} variant="ghost" size="icon-sm" onclick={() => navigate(`/${resourceName}/edit/${id}`)}>
745
+ <TooltipButton tooltip={i18n.t('common.edit')} variant="ghost" size="icon-sm" onclick={() => adminContext.navigate(`/${resourceName}/edit/${id}`)}>
720
746
  <Pencil class="h-4 w-4" />
721
747
  </TooltipButton>
722
748
  {/if}
723
749
  {#if canDelete}
724
- <TooltipButton tooltip={t('common.delete')} variant="ghost" size="icon-sm" onclick={() => confirmDelete(id)} class="hover:text-destructive">
750
+ <TooltipButton tooltip={i18n.t('common.delete')} variant="ghost" size="icon-sm" onclick={() => confirmDelete(id)} class="hover:text-destructive">
725
751
  <Trash2 class="h-4 w-4" />
726
752
  </TooltipButton>
727
753
  {/if}
@@ -767,25 +793,25 @@
767
793
  </ContextMenu.Trigger>
768
794
  <ContextMenu.Content class="w-48">
769
795
  {#if canEdit}
770
- <ContextMenu.Item onclick={() => navigate(`/${resourceName}/edit/${id}`)} class="gap-2">
771
- <Pencil class="h-4 w-4" /> {t('common.edit')}
796
+ <ContextMenu.Item onclick={() => adminContext.navigate(`/${resourceName}/edit/${id}`)} class="gap-2">
797
+ <Pencil class="h-4 w-4" /> {i18n.t('common.edit')}
772
798
  </ContextMenu.Item>
773
799
  {/if}
774
- <ContextMenu.Item onclick={() => navigate(`/${resourceName}/show/${id}`)} class="gap-2">
775
- <Eye class="h-4 w-4" /> {t('common.detail')}
800
+ <ContextMenu.Item onclick={() => adminContext.navigate(`/${resourceName}/show/${id}`)} class="gap-2">
801
+ <Eye class="h-4 w-4" /> {i18n.t('common.detail')}
776
802
  </ContextMenu.Item>
777
803
  <ContextMenu.Item onclick={() => navigator.clipboard?.writeText(String(id))} class="gap-2">
778
- <Copy class="h-4 w-4" /> {t('common.copyId')}
804
+ <Copy class="h-4 w-4" /> {i18n.t('common.copyId')}
779
805
  </ContextMenu.Item>
780
806
  {#if canDelete}
781
807
  <ContextMenu.Separator />
782
808
  <ContextMenu.Item onclick={() => confirmDelete(id)} class="gap-2 text-destructive">
783
- <Trash2 class="h-4 w-4" /> {t('common.delete')}
809
+ <Trash2 class="h-4 w-4" /> {i18n.t('common.delete')}
784
810
  </ContextMenu.Item>
785
811
  {/if}
786
812
  </ContextMenu.Content>
787
813
  </ContextMenu.Root>
788
- {#if expandedRowRender && row_getIsExpanded(row)}
814
+ {#if expandedRowRender && rowIsExpanded(row.id)}
789
815
  <Table.Row class="bg-muted/10 border-b border-border/10 transition-all">
790
816
  <Table.Cell colspan={row_getVisibleCells(row).length}>
791
817
  {@render expandedRowRender({ record })}
@@ -802,12 +828,12 @@
802
828
  <svg class="h-16 w-16 text-muted-foreground/30 mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor">
803
829
  <path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
804
830
  </svg>
805
- <p class="text-sm font-medium text-muted-foreground mb-1">{t('common.noData')}</p>
806
- <p class="text-xs text-muted-foreground/60 mb-4">{t('common.noDataHint') || 'Get started by creating your first record.'}</p>
831
+ <p class="text-sm font-medium text-muted-foreground mb-1">{i18n.t('common.noData')}</p>
832
+ <p class="text-xs text-muted-foreground/60 mb-4">{i18n.t('common.noDataHint')}</p>
807
833
  {#if canCreate}
808
- <Button variant="outline" size="sm" class="gap-2" onclick={() => navigate(`/${resourceName}/create`)}>
834
+ <Button variant="outline" size="sm" class="gap-2" onclick={() => adminContext.navigate(`/${resourceName}/create`)}>
809
835
  <Plus class="h-3.5 w-3.5" />
810
- {t('common.create')}
836
+ {i18n.t('common.create')}
811
837
  </Button>
812
838
  {/if}
813
839
  </div>
@@ -821,7 +847,7 @@
821
847
 
822
848
  <!-- Mobile Card View (visible only on small screens) -->
823
849
  <div class="md:hidden space-y-3 p-2">
824
- {#each table_getRowModel(tbl).rows as row, _i (_i)}
850
+ {#each tableView.rows as row, _i (_i)}
825
851
  {@const record = row.original}
826
852
  {@const id = record[primaryKey] as string | number}
827
853
  <div
@@ -843,15 +869,15 @@
843
869
  {@render rowActions({ record, id })}
844
870
  {:else}
845
871
  {#if canEdit}
846
- <TooltipButton tooltip={t('common.edit')} variant="ghost" size="icon-sm" onclick={() => navigate(`/${resourceName}/edit/${id}`)}>
872
+ <TooltipButton tooltip={i18n.t('common.edit')} variant="ghost" size="icon-sm" onclick={() => adminContext.navigate(`/${resourceName}/edit/${id}`)}>
847
873
  <Pencil class="h-4 w-4" />
848
874
  </TooltipButton>
849
875
  {/if}
850
- <TooltipButton tooltip={t('common.detail')} variant="ghost" size="icon-sm" onclick={() => navigate(`/${resourceName}/show/${id}`)}>
876
+ <TooltipButton tooltip={i18n.t('common.detail')} variant="ghost" size="icon-sm" onclick={() => adminContext.navigate(`/${resourceName}/show/${id}`)}>
851
877
  <Eye class="h-4 w-4" />
852
878
  </TooltipButton>
853
879
  {#if canDelete}
854
- <TooltipButton tooltip={t('common.delete')} variant="ghost" size="icon-sm" onclick={() => confirmDelete(id)} class="hover:text-destructive">
880
+ <TooltipButton tooltip={i18n.t('common.delete')} variant="ghost" size="icon-sm" onclick={() => confirmDelete(id)} class="hover:text-destructive">
855
881
  <Trash2 class="h-4 w-4" />
856
882
  </TooltipButton>
857
883
  {/if}
@@ -895,7 +921,7 @@
895
921
  {#if emptyState}
896
922
  {@render emptyState()}
897
923
  {:else}
898
- {t('common.noData')}
924
+ {i18n.t('common.noData')}
899
925
  {/if}
900
926
  </div>
901
927
  {/each}
@@ -908,7 +934,7 @@
908
934
  {#if totalPages > 0}
909
935
  <div class="flex flex-col sm:flex-row items-center justify-between gap-2 text-sm text-muted-foreground">
910
936
  <div class="flex items-center gap-2">
911
- <span>{t('common.total', { total: query.data?.total ?? 0 })}</span>
937
+ <span>{i18n.t('common.total', { total: query.data?.total ?? 0 })}</span>
912
938
  <select
913
939
  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"
914
940
  value={String(pagination.pageSize ?? 10)}
@@ -962,7 +988,7 @@
962
988
  <ConfirmDialog
963
989
  open={confirmOpen}
964
990
  message={confirmMessage}
965
- confirmText={t('common.delete')}
991
+ confirmText={i18n.t('common.delete')}
966
992
  onconfirm={confirmAction}
967
993
  oncancel={() => { confirmOpen = false; }}
968
994
  />