@svadmin/ui 0.5.10 → 0.5.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/ui",
3
- "version": "0.5.10",
3
+ "version": "0.5.12",
4
4
  "description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "peerDependencies": {
23
23
  "svelte": "^5.0.0",
24
- "@svadmin/core": "^0.4.1",
24
+ "@svadmin/core": "^0.5.0",
25
25
  "@tanstack/svelte-query": "^5.0.0",
26
26
  "bits-ui": "^2.0.0",
27
27
  "tailwind-variants": "^3.0.0",
package/src/app.css CHANGED
@@ -126,7 +126,7 @@ body {
126
126
  * This enables bg-primary, text-muted-foreground, border-input, etc.
127
127
  * Without this, Tailwind 4 utility classes referencing these tokens are no-ops.
128
128
  */
129
- @theme inline {
129
+ @theme {
130
130
  --color-background: var(--background);
131
131
  --color-foreground: var(--foreground);
132
132
  --color-card: var(--card);
@@ -45,15 +45,17 @@
45
45
  }: Props = $props();
46
46
 
47
47
  // Resolve router provider (default to hash)
48
- const resolvedRouter = routerProvider ?? createHashRouterProvider();
48
+ const resolvedRouter = $derived(routerProvider ?? createHashRouterProvider());
49
49
 
50
- // Set up context
51
- setDataProvider(dataProvider);
52
- if (authProvider) setAuthProvider(authProvider);
53
- setResources(resources);
54
- setRouterProvider(resolvedRouter);
55
- if (locale) setLocale(locale);
56
- if (defaultTheme) setTheme(defaultTheme);
50
+ // Set up context — use $effect so prop changes are tracked
51
+ $effect.pre(() => {
52
+ setDataProvider(dataProvider);
53
+ if (authProvider) setAuthProvider(authProvider);
54
+ setResources(resources);
55
+ setRouterProvider(resolvedRouter);
56
+ if (locale) setLocale(locale);
57
+ if (defaultTheme) setTheme(defaultTheme);
58
+ });
57
59
 
58
60
  const queryClient = new QueryClient({
59
61
  defaultOptions: {
@@ -62,7 +64,9 @@
62
64
  });
63
65
 
64
66
  // Initialize router with provider
65
- initRouter(resolvedRouter);
67
+ $effect.pre(() => {
68
+ initRouter(resolvedRouter);
69
+ });
66
70
 
67
71
  // Reactive getters for route state
68
72
  const route = $derived(getRoute());
@@ -107,7 +111,7 @@
107
111
  {:else if route === '/update-password' && authProvider?.updatePassword}
108
112
  <UpdatePasswordPage {title} />
109
113
  {:else if route === '/login' || route === '/register' || route === '/forgot-password' || route === '/update-password'}
110
- <ConfigErrorScreen title="{title} — Configuration Required" />
114
+ <ConfigErrorScreen title="{title} — {t('common.configRequired')}" />
111
115
  {:else if isAuthenticated || !authProvider}
112
116
  <Layout {title}>
113
117
  {#if route === '/'}
@@ -37,34 +37,34 @@
37
37
  headerContent,
38
38
  }: Props = $props();
39
39
 
40
- const resource = getResource(resourceName);
41
- const primaryKey = resource.primaryKey ?? 'id';
40
+ const resource = $derived(getResource(resourceName));
41
+ const primaryKey = $derived(resource.primaryKey ?? 'id');
42
42
 
43
- const formFields = resource.fields.filter(f => {
43
+ const formFields = $derived(resource.fields.filter(f => {
44
44
  if (f.key === primaryKey) return false;
45
45
  if (f.showInForm === false) return false;
46
46
  if (mode === 'create' && f.showInCreate === false) return false;
47
47
  if (mode === 'edit' && f.showInEdit === false) return false;
48
48
  return true;
49
- });
49
+ }));
50
50
 
51
- const hasGroups = formFields.some(f => f.group);
52
- const groups = (() => {
51
+ const hasGroups = $derived(formFields.some(f => f.group));
52
+ const groups = $derived((() => {
53
53
  if (!hasGroups) return [];
54
54
  const order: string[] = [];
55
- const map = new Map<string, typeof formFields>();
55
+ const map = new Map<string, FieldDefinition[]>();
56
56
  for (const f of formFields) {
57
57
  const g = f.group ?? '';
58
58
  if (!map.has(g)) { order.push(g); map.set(g, []); }
59
59
  map.get(g)!.push(f);
60
60
  }
61
61
  return order.map(g => ({ name: g, fields: map.get(g)! }));
62
- })();
62
+ })());
63
63
 
64
64
  // Load existing data for edit mode
65
- const existingQuery = mode === 'edit' && id != null
66
- ? useOne({ resource: resourceName, id })
67
- : null;
65
+ const existingQuery = $derived.by(() => mode === 'edit' && id != null
66
+ ? useOne({ get resource() { return resourceName; }, get id() { return id; } })
67
+ : null);
68
68
 
69
69
  // Form state
70
70
  let formData = $state<Record<string, unknown>>({});
@@ -116,8 +116,8 @@
116
116
  };
117
117
  });
118
118
 
119
- const createMut = useCreate({ resource: resourceName });
120
- const updateMut = useUpdate({ resource: resourceName });
119
+ const createMut = useCreate({ get resource() { return resourceName; } });
120
+ const updateMut = useUpdate({ get resource() { return resourceName; } });
121
121
 
122
122
  function validateFields(): boolean {
123
123
  const errors: Record<string, string> = {};
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { untrack } from 'svelte';
2
3
  import {
3
4
  createTable,
4
5
  getCoreRowModel,
@@ -33,6 +34,7 @@
33
34
  Eye, Copy
34
35
  } from 'lucide-svelte';
35
36
  import ConfirmDialog from './ConfirmDialog.svelte';
37
+ import TooltipButton from './TooltipButton.svelte';
36
38
  import type { Snippet } from 'svelte';
37
39
 
38
40
  // ─── Props with Snippet composability ─────────────────────────
@@ -67,21 +69,25 @@
67
69
  sorters: externalSorters,
68
70
  }: Props = $props();
69
71
 
70
- const resource = getResource(resourceName);
72
+ const resource = $derived(getResource(resourceName));
71
73
  const primaryKey = $derived(resource.primaryKey ?? 'id');
72
74
 
73
75
  // ─── URL state + server-side state ────────────────────────────
74
76
  const urlState = readURLState();
75
77
 
76
- let pagination = $state<{ current: number; pageSize: number }>(externalPagination ?? {
78
+ // Snapshot resource values for initial state (untrack to avoid reactive tracking)
79
+ const initPageSize = untrack(() => resource.pageSize ?? 10);
80
+ const initDefaultSort = untrack(() => resource.defaultSort);
81
+
82
+ let pagination = $state<{ current: number; pageSize: number }>(untrack(() => externalPagination ?? {
77
83
  current: urlState.page ?? 1,
78
- pageSize: urlState.pageSize ?? resource.pageSize ?? 10,
79
- });
80
- let sorters = $state<Sort[]>(
84
+ pageSize: urlState.pageSize ?? initPageSize,
85
+ }));
86
+ let sorters = $state<Sort[]>(untrack(() =>
81
87
  externalSorters ??
82
88
  (urlState.sortField
83
89
  ? [{ field: urlState.sortField, order: urlState.sortOrder ?? 'asc' }]
84
- : resource.defaultSort ? [resource.defaultSort] : [])
90
+ : initDefaultSort ? [initDefaultSort] : []))
85
91
  );
86
92
  let filters = $state<Filter[]>([]);
87
93
  let searchText = $state(urlState.search ?? '');
@@ -98,8 +104,8 @@
98
104
  });
99
105
 
100
106
  // ─── Build active filters with search ─────────────────────────
101
- const searchableFields = resource.fields.filter(f => f.searchable);
102
- const filterableFields = resource.fields.filter(f => f.filterable);
107
+ const searchableFields = $derived(resource.fields.filter(f => f.searchable));
108
+ const filterableFields = $derived(resource.fields.filter(f => f.filterable));
103
109
  let filterValues = $state<Record<string, string>>({});
104
110
  const activeFilterCount = $derived(Object.values(filterValues).filter(v => v.trim()).length);
105
111
  const activeFilters = $derived.by(() => {
@@ -117,21 +123,26 @@
117
123
  });
118
124
 
119
125
  // ─── Data fetching ────────────────────────────────────────────
120
- const listResult = useList({ resource: resourceName, pagination, sorters, filters: activeFilters });
126
+ const listResult = useList({
127
+ get resource() { return resourceName; },
128
+ get pagination() { return pagination; },
129
+ get sorters() { return sorters; },
130
+ get filters() { return activeFilters; },
131
+ });
121
132
  const query = listResult.query;
122
- const deleteResult = useDelete({ resource: resourceName });
133
+ const deleteResult = useDelete({ get resource() { return resourceName; } });
123
134
  const deleteMutation = deleteResult.mutation;
124
135
 
125
136
  // ─── Permissions ──────────────────────────────────────────────
126
- const canCreate = canAccess(resourceName, 'create').can && resource.canCreate !== false;
127
- const canEdit = canAccess(resourceName, 'edit').can && resource.canEdit !== false;
128
- const canDelete = canAccess(resourceName, 'delete').can && resource.canDelete !== false;
129
- const canExport = canAccess(resourceName, 'export').can;
137
+ const canCreate = $derived(canAccess(resourceName, 'create').can && resource.canCreate !== false);
138
+ const canEdit = $derived(canAccess(resourceName, 'edit').can && resource.canEdit !== false);
139
+ const canDelete = $derived(canAccess(resourceName, 'delete').can && resource.canDelete !== false);
140
+ const canExport = $derived(canAccess(resourceName, 'export').can);
130
141
 
131
142
  // ─── TanStack Table state ────────────────────────────────────
132
- let sorting = $state<SortingState>(
143
+ let sorting = $state<SortingState>(untrack(() =>
133
144
  sorters.map(s => ({ id: s.field, desc: s.order === 'desc' }))
134
- );
145
+ ));
135
146
  let columnVisibility = $state<VisibilityState>((() => {
136
147
  const vis: VisibilityState = {};
137
148
  for (const f of resource.fields) {
@@ -195,7 +206,7 @@
195
206
  // ─── Create TanStack Table ────────────────────────────────────
196
207
  const tbl = createTable({
197
208
  get data() { return query.data?.data ?? []; },
198
- columns,
209
+ get columns() { return columns; },
199
210
  getCoreRowModel: getCoreRowModel(),
200
211
  manualPagination: true,
201
212
  manualSorting: true,
@@ -218,8 +229,8 @@
218
229
  onExpandedChange: (updater) => {
219
230
  expanded = typeof updater === 'function' ? updater(expanded) : updater;
220
231
  },
221
- enableRowSelection: selectable && canDelete,
222
- enableExpanding: !!expandedRowRender,
232
+ get enableRowSelection() { return selectable && canDelete; },
233
+ get enableExpanding() { return !!expandedRowRender; },
223
234
  });
224
235
 
225
236
  const selectedCount = $derived(Object.keys(rowSelection).length);
@@ -481,27 +492,27 @@
481
492
  onCheckedChange={() => row.toggleSelected()}
482
493
  />
483
494
  {:else if cell.column.id === '_expand'}
484
- <Button variant="ghost" size="icon" class="h-7 w-7" onclick={() => row.toggleExpanded()}>
495
+ <TooltipButton tooltip={row.getIsExpanded() ? t('common.collapse') : t('common.expand')} variant="ghost" size="icon" class="h-7 w-7" onclick={() => row.toggleExpanded()}>
485
496
  {#if row.getIsExpanded()}
486
497
  <ChevronUp class="h-4 w-4" />
487
498
  {:else}
488
499
  <ChevronDown class="h-4 w-4" />
489
500
  {/if}
490
- </Button>
501
+ </TooltipButton>
491
502
  {:else if cell.column.id === '_actions'}
492
503
  <div class="flex items-center justify-end gap-1">
493
504
  {#if rowActions}
494
505
  {@render rowActions({ record, id })}
495
506
  {:else}
496
507
  {#if canEdit}
497
- <Button variant="ghost" size="icon-sm" onclick={() => navigate(`/${resourceName}/edit/${id}`)} title={t('common.edit')}>
508
+ <TooltipButton tooltip={t('common.edit')} variant="ghost" size="icon-sm" onclick={() => navigate(`/${resourceName}/edit/${id}`)}>
498
509
  <Pencil class="h-4 w-4" />
499
- </Button>
510
+ </TooltipButton>
500
511
  {/if}
501
512
  {#if canDelete}
502
- <Button variant="ghost" size="icon-sm" onclick={() => confirmDelete(id)} title={t('common.delete')} class="hover:text-destructive">
513
+ <TooltipButton tooltip={t('common.delete')} variant="ghost" size="icon-sm" onclick={() => confirmDelete(id)} class="hover:text-destructive">
503
514
  <Trash2 class="h-4 w-4" />
504
- </Button>
515
+ </TooltipButton>
505
516
  {/if}
506
517
  {/if}
507
518
  </div>
@@ -32,10 +32,10 @@
32
32
  }: Props = $props();
33
33
 
34
34
  const { options, query, onSearchChange } = useSelect({
35
- resource,
36
- optionLabel,
37
- optionValue,
38
- onSearch: onSearch ?? (searchable ? (v: string) => v ? [{ field: optionLabel, operator: 'contains' as const, value: v }] : [] : undefined),
35
+ get resource() { return resource; },
36
+ get optionLabel() { return optionLabel; },
37
+ get optionValue() { return optionValue; },
38
+ get onSearch() { return onSearch ?? (searchable ? (v: string) => v ? [{ field: optionLabel, operator: 'contains' as const, value: v }] : [] : undefined); },
39
39
  });
40
40
 
41
41
  let open = $state(false);
@@ -23,8 +23,8 @@
23
23
  class: className = '',
24
24
  }: Props = $props();
25
25
 
26
- const resource = getResource(resourceName);
27
- const pageTitle = title ?? `${t('common.create')}${resource.label}`;
26
+ const resource = $derived(getResource(resourceName));
27
+ const pageTitle = $derived(title ?? `${t('common.create')}${resource.label}`);
28
28
  </script>
29
29
 
30
30
  <div class="space-y-6 {className}">
@@ -136,14 +136,14 @@
136
136
  {/if}
137
137
  </div>
138
138
  {:else}
139
- <Button
139
+ <TooltipButton
140
+ tooltip={t('devtools.title')}
140
141
  variant="default"
141
142
  size="icon"
142
143
  class="fixed bottom-4 right-4 z-[9999] h-9 w-9 rounded-full shadow-lg opacity-60 hover:opacity-100 hover:scale-110 transition-all"
143
144
  onclick={toggle}
144
- title={t('devtools.title')}
145
145
  >
146
146
  <Bug class="h-4 w-4" />
147
- </Button>
147
+ </TooltipButton>
148
148
  {/if}
149
149
  {/if}
@@ -28,11 +28,11 @@
28
28
  class: className = '',
29
29
  }: Props = $props();
30
30
 
31
- const resource = getResource(resourceName);
32
- const pageTitle = title ?? `${t('common.edit')} ${resource.label} #${id}`;
33
- const showDelete = canDelete ?? resource.canDelete !== false;
31
+ const resource = $derived(getResource(resourceName));
32
+ const pageTitle = $derived(title ?? `${t('common.edit')} ${resource.label} #${id}`);
33
+ const showDelete = $derived(canDelete ?? resource.canDelete !== false);
34
34
 
35
- const deleteResult = useDelete({ resource: resourceName });
35
+ const deleteResult = useDelete({ get resource() { return resourceName; } });
36
36
  const deleteMutation = deleteResult.mutation;
37
37
 
38
38
  let confirmOpen = $state(false);
@@ -13,6 +13,7 @@
13
13
  import { Select } from './ui/select/index.js';
14
14
  import { Checkbox } from './ui/checkbox/index.js';
15
15
  import { Button } from './ui/button/index.js';
16
+ import TooltipButton from './TooltipButton.svelte';
16
17
  import ComboboxField from './ComboboxField.svelte';
17
18
  import { Plus, X } from 'lucide-svelte';
18
19
  import type { Snippet } from 'svelte';
@@ -283,9 +284,9 @@
283
284
  {#if url}
284
285
  <img src={url} alt="preview" class="h-9 w-9 rounded object-cover border" />
285
286
  {/if}
286
- <Button variant="ghost" size="icon" class="h-8 w-8 shrink-0" onclick={() => removeImage(i)}>
287
+ <TooltipButton tooltip={t('common.removeImage')} variant="ghost" size="icon" class="h-8 w-8 shrink-0" onclick={() => removeImage(i)}>
287
288
  <X class="h-3.5 w-3.5" />
288
- </Button>
289
+ </TooltipButton>
289
290
  </div>
290
291
  {/each}
291
292
  <Button variant="outline" size="sm" type="button" onclick={addImage}>
@@ -33,10 +33,10 @@
33
33
  }: Props = $props();
34
34
 
35
35
  const { query } = useInfiniteList({
36
- resource,
37
- pagination: { pageSize },
38
- sorters,
39
- filters,
36
+ get resource() { return resource; },
37
+ get pagination() { return { pageSize }; },
38
+ get sorters() { return sorters; },
39
+ get filters() { return filters; },
40
40
  });
41
41
 
42
42
  const allItems = $derived(
@@ -41,9 +41,9 @@
41
41
  class: className = '',
42
42
  }: Props = $props();
43
43
 
44
- const resource = getResource(resourceName);
45
- const pageTitle = title ?? resource.label;
46
- const showCreate = canCreate ?? resource.canCreate !== false;
44
+ const resource = $derived(getResource(resourceName));
45
+ const pageTitle = $derived(title ?? resource.label);
46
+ const showCreate = $derived(canCreate ?? resource.canCreate !== false);
47
47
  </script>
48
48
 
49
49
  <div class="space-y-6 {className}">
@@ -5,6 +5,7 @@
5
5
  import { Label } from './ui/label/index.js';
6
6
  import { Progress } from './ui/progress/index.js';
7
7
  import { Lock, Eye, EyeOff } from 'lucide-svelte';
8
+ import { t } from '@svadmin/core/i18n';
8
9
 
9
10
  interface Props {
10
11
  id: string;
@@ -66,7 +67,7 @@
66
67
  autocomplete={autocomplete as any}
67
68
  />
68
69
  <TooltipButton
69
- tooltip={showPassword ? 'Hide password' : 'Show password'}
70
+ tooltip={showPassword ? t('common.hidePassword') : t('common.showPassword')}
70
71
  variant="ghost"
71
72
  size="icon"
72
73
  type="button"
@@ -7,23 +7,24 @@
7
7
  import * as Card from './ui/card/index.js';
8
8
  import { Skeleton } from './ui/skeleton/index.js';
9
9
  import PageHeader from './PageHeader.svelte';
10
+ import TooltipButton from './TooltipButton.svelte';
10
11
  import { getDisplayComponent } from './fieldComponentMap';
11
- import { Pencil } from 'lucide-svelte';
12
+ import { ArrowLeft, Pencil } from 'lucide-svelte';
12
13
 
13
14
  let { resourceName, id } = $props<{ resourceName: string; id: string | number }>();
14
15
 
15
- const resource = getResource(resourceName);
16
- const showFields = resource.fields.filter(f => f.showInShow !== false);
16
+ const resource = $derived(getResource(resourceName));
17
+ const showFields = $derived(resource.fields.filter(f => f.showInShow !== false));
17
18
 
18
- const query = useShow({ resource: resourceName, id });
19
+ const query = useShow({ get resource() { return resourceName; }, get id() { return id; } });
19
20
  </script>
20
21
 
21
22
  <div class="space-y-6">
22
23
  <PageHeader title="{resource.label} {t('common.detail')}">
23
24
  {#snippet actions()}
24
- <Button variant="ghost" size="icon" onclick={() => navigate(`/${resourceName}`)}>
25
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>
26
- </Button>
25
+ <TooltipButton tooltip={t('common.back')} onclick={() => navigate(`/${resourceName}`)}>
26
+ <ArrowLeft class="h-5 w-5" />
27
+ </TooltipButton>
27
28
  {#if resource.canEdit !== false}
28
29
  <Button onclick={() => navigate(`/${resourceName}/edit/${id}`)}>
29
30
  <Pencil class="h-4 w-4" data-icon="inline-start" /> {t('common.edit')}
@@ -5,6 +5,7 @@
5
5
  import { t, getLocale, setLocale, getAvailableLocales } from '@svadmin/core/i18n';
6
6
  import { toggleTheme, getResolvedTheme, colorThemes, getColorTheme, setColorTheme, canAccessAsync } from '@svadmin/core';
7
7
  import { Button } from './ui/button/index.js';
8
+ import TooltipButton from './TooltipButton.svelte';
8
9
  import * as Tooltip from './ui/tooltip/index.js';
9
10
  import * as DropdownMenu from './ui/dropdown-menu/index.js';
10
11
  import { Separator } from './ui/separator/index.js';
@@ -134,13 +135,13 @@
134
135
  {:else}
135
136
  <span class="text-lg font-bold text-sidebar-primary">{title.charAt(0)}</span>
136
137
  {/if}
137
- <Button variant="ghost" size="icon-sm" onclick={onToggle}>
138
+ <TooltipButton tooltip={t('common.toggleSidebar')} variant="ghost" size="icon-sm" onclick={onToggle}>
138
139
  {#if collapsed}
139
140
  <ChevronRight class="h-4 w-4" />
140
141
  {:else}
141
142
  <ChevronLeft class="h-4 w-4" />
142
143
  {/if}
143
- </Button>
144
+ </TooltipButton>
144
145
  </div>
145
146
 
146
147
  <ScrollArea class="flex-1">
@@ -269,25 +270,25 @@
269
270
  <div class="flex-1 min-w-0">
270
271
  <p class="truncate text-sm font-medium text-sidebar-foreground">{identity.name}</p>
271
272
  </div>
272
- <Button variant="ghost" size="icon-sm" onclick={toggleLocale} class="text-sidebar-foreground" title={t('common.switchLanguage')}>
273
+ <TooltipButton tooltip={t('common.switchLanguage')} variant="ghost" size="icon-sm" onclick={toggleLocale} class="text-sidebar-foreground">
273
274
  <span class="text-xs font-bold">{localeLabel}</span>
274
- </Button>
275
- <Button variant="ghost" size="icon-sm" onclick={toggleTheme} class="text-sidebar-foreground" title={t('common.toggleTheme')}>
275
+ </TooltipButton>
276
+ <TooltipButton tooltip={t('common.toggleTheme')} variant="ghost" size="icon-sm" onclick={toggleTheme} class="text-sidebar-foreground">
276
277
  {#if getResolvedTheme() === 'dark'}
277
278
  <Sun class="h-4 w-4" />
278
279
  {:else}
279
280
  <Moon class="h-4 w-4" />
280
281
  {/if}
281
- </Button>
282
- <Button variant="ghost" size="icon-sm" onclick={onLogout} class="text-sidebar-foreground hover:text-destructive" title={t('common.logout')}>
282
+ </TooltipButton>
283
+ <TooltipButton tooltip={t('common.logout')} variant="ghost" size="icon-sm" onclick={onLogout} class="text-sidebar-foreground hover:text-destructive">
283
284
  <LogOut class="h-4 w-4" />
284
- </Button>
285
+ </TooltipButton>
285
286
  </div>
286
287
  {:else if collapsed}
287
288
  <Tooltip.Root>
288
289
  <Tooltip.Trigger>
289
290
  {#snippet child({ props })}
290
- <Button {...props} variant="ghost" size="icon" onclick={toggleLocale} class="w-full text-sidebar-foreground" title={t('common.switchLanguage')}>
291
+ <Button {...props} variant="ghost" size="icon" onclick={toggleLocale} class="w-full text-sidebar-foreground">
291
292
  <span class="text-xs font-bold">{localeLabel}</span>
292
293
  </Button>
293
294
  {/snippet}
@@ -297,7 +298,7 @@
297
298
  <Tooltip.Root>
298
299
  <Tooltip.Trigger>
299
300
  {#snippet child({ props })}
300
- <Button {...props} variant="ghost" size="icon" onclick={toggleTheme} class="w-full text-sidebar-foreground" title={t('common.toggleTheme')}>
301
+ <Button {...props} variant="ghost" size="icon" onclick={toggleTheme} class="w-full text-sidebar-foreground">
301
302
  {#if getResolvedTheme() === 'dark'}
302
303
  <Sun class="h-5 w-5" />
303
304
  {:else}
@@ -311,7 +312,7 @@
311
312
  <Tooltip.Root>
312
313
  <Tooltip.Trigger>
313
314
  {#snippet child({ props })}
314
- <Button {...props} variant="ghost" size="icon" onclick={onLogout} class="w-full text-sidebar-foreground hover:text-destructive" title={t('common.logout')}>
315
+ <Button {...props} variant="ghost" size="icon" onclick={onLogout} class="w-full text-sidebar-foreground hover:text-destructive">
315
316
  <LogOut class="h-5 w-5" />
316
317
  </Button>
317
318
  {/snippet}
@@ -320,16 +321,16 @@
320
321
  </Tooltip.Root>
321
322
  {:else}
322
323
  <div class="flex gap-1">
323
- <Button variant="ghost" size="icon" onclick={toggleLocale} class="flex-1 text-sidebar-foreground" title={t('common.switchLanguage')}>
324
+ <TooltipButton tooltip={t('common.switchLanguage')} variant="ghost" size="icon" onclick={toggleLocale} class="flex-1 text-sidebar-foreground">
324
325
  <span class="text-xs font-bold">{localeLabel}</span>
325
- </Button>
326
- <Button variant="ghost" size="icon" onclick={toggleTheme} class="flex-1 text-sidebar-foreground" title={t('common.toggleTheme')}>
326
+ </TooltipButton>
327
+ <TooltipButton tooltip={t('common.toggleTheme')} variant="ghost" size="icon" onclick={toggleTheme} class="flex-1 text-sidebar-foreground">
327
328
  {#if getResolvedTheme() === 'dark'}
328
329
  <Sun class="h-5 w-5" />
329
330
  {:else}
330
331
  <Moon class="h-5 w-5" />
331
332
  {/if}
332
- </Button>
333
+ </TooltipButton>
333
334
  </div>
334
335
  {/if}
335
336
  </div>
@@ -27,18 +27,18 @@
27
27
 
28
28
  let { resourceName, id, mode = 'create', steps }: Props = $props();
29
29
 
30
- const resource = getResource(resourceName);
31
- const primaryKey = resource.primaryKey ?? 'id';
30
+ const resource = $derived(getResource(resourceName));
31
+ const primaryKey = $derived(resource.primaryKey ?? 'id');
32
32
 
33
33
  const stepsForm = useStepsForm({
34
- resource: resourceName,
35
- action: mode,
36
- id,
34
+ get resource() { return resourceName; },
35
+ get action() { return mode; },
36
+ get id() { return id; },
37
37
  });
38
38
 
39
39
  const { currentStep, next, prev, gotoStep } = stepsForm;
40
40
 
41
- const totalSteps = steps.length;
41
+ const totalSteps = $derived(steps.length);
42
42
  const progressValue = $derived(((currentStep + 1) / totalSteps) * 100);
43
43
  const isLastStep = $derived(currentStep >= totalSteps - 1);
44
44
 
@@ -49,13 +49,13 @@
49
49
  .filter((f): f is FieldDefinition => f != null && f.key !== primaryKey) ?? []
50
50
  );
51
51
 
52
- const flatStepsFields = steps.flatMap(s => s.fields)
52
+ const flatStepsFields = $derived(steps.flatMap(s => s.fields)
53
53
  .map(key => resource.fields.find(f => f.key === key))
54
- .filter((f): f is FieldDefinition => f != null);
54
+ .filter((f): f is FieldDefinition => f != null));
55
55
 
56
- const existingQuery = mode === 'edit' && id != null
56
+ const existingQuery = $derived(mode === 'edit' && id != null
57
57
  ? stepsForm.query as { data?: { data: Record<string, unknown> }, isLoading: boolean }
58
- : null;
58
+ : null);
59
59
 
60
60
  let formData = $state<Record<string, unknown>>({});
61
61
  let fieldErrors = $state<Record<string, string>>({});
@@ -12,9 +12,14 @@
12
12
  onTimeout: () => void;
13
13
  }>();
14
14
 
15
- let remaining = $state(duration);
15
+ let remaining = $state(0);
16
16
  let dismissed = $state(false);
17
17
 
18
+ // Initialize remaining from prop
19
+ $effect.pre(() => {
20
+ remaining = duration;
21
+ });
22
+
18
23
  $effect(() => {
19
24
  const interval = setInterval(() => {
20
25
  remaining -= 100;
@@ -12,7 +12,7 @@
12
12
  }>();
13
13
 
14
14
  const nav = useNavigation();
15
- const can = accessControl?.enabled ? useCan(resource, 'create') : null;
15
+ const can = $derived(accessControl?.enabled ? useCan(resource, 'create') : null);
16
16
  const hidden = $derived(accessControl?.hideIfUnauthorized && can && !can.allowed);
17
17
  </script>
18
18
 
@@ -11,7 +11,7 @@
11
11
  }>();
12
12
 
13
13
  const nav = useNavigation();
14
- const can = accessControl?.enabled ? useCan(resource, 'create') : null;
14
+ const can = $derived(accessControl?.enabled ? useCan(resource, 'create') : null);
15
15
  const hidden = $derived(accessControl?.hideIfUnauthorized && can && !can.allowed);
16
16
  </script>
17
17
 
@@ -16,8 +16,8 @@
16
16
  class?: string;
17
17
  }>();
18
18
 
19
- const deleteMut = useDelete({ resource, mutationMode: undoable ? 'undoable' : 'pessimistic' });
20
- const can = accessControl?.enabled ? useCan(resource, 'delete') : null;
19
+ const deleteMut = useDelete({ get resource() { return resource; }, get mutationMode() { return undoable ? 'undoable' as const : 'pessimistic' as const; } });
20
+ const can = $derived(accessControl?.enabled ? useCan(resource, 'delete') : null);
21
21
  const hidden = $derived(accessControl?.hideIfUnauthorized && can && !can.allowed);
22
22
  let confirming = $state(false);
23
23
 
@@ -12,7 +12,7 @@
12
12
  }>();
13
13
 
14
14
  const nav = useNavigation();
15
- const can = accessControl?.enabled ? useCan(resource, 'edit') : null;
15
+ const can = $derived(accessControl?.enabled ? useCan(resource, 'edit') : null);
16
16
  const hidden = $derived(accessControl?.hideIfUnauthorized && can && !can.allowed);
17
17
  </script>
18
18
 
@@ -9,7 +9,7 @@
9
9
  class?: string;
10
10
  }>();
11
11
 
12
- const { triggerExport, isLoading } = useExport({ resource });
12
+ const { triggerExport, isLoading } = useExport({ get resource() { return resource; } });
13
13
  </script>
14
14
 
15
15
  <Button