@svadmin/ui 0.38.2 → 0.38.5

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 +85 -71
  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 -9
  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 +13 -11
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { ListTodo, Loader2, Plus, Search, RefreshCw } from '@lucide/svelte';
3
3
  import { getTaskProvider, useSubmitTask, useTaskList } from '@svadmin/core';
4
- import { t } from '@svadmin/core/i18n';
4
+ import { useTranslation } from '@svadmin/core/i18n';
5
5
  import type { TaskProvider, TaskRecord } from '@svadmin/core';
6
6
  import { Button } from './ui/button/index.js';
7
7
  import { Badge } from './ui/badge/index.js';
@@ -21,6 +21,8 @@
21
21
  resolveTaskTitle,
22
22
  } from './task-utils.js';
23
23
 
24
+ const i18n = useTranslation();
25
+
24
26
  type AutoRefreshValue = '0' | '5000' | '15000' | '30000';
25
27
  type TaskTab = 'tasks' | 'dlq';
26
28
 
@@ -44,14 +46,14 @@
44
46
  let autoRefresh = $state<AutoRefreshValue>('15000');
45
47
  let submitTaskName = $state('');
46
48
  let submitIdempotencyKey = $state('');
47
- let submitBodyText = $state(t('task.bodyPlaceholder'));
49
+ let submitBodyText = $state(i18n.t('task.bodyPlaceholder'));
48
50
  let submitError = $state<string | null>(null);
49
51
  let submitOpen = $state(false);
50
52
 
51
53
  const refreshInterval = $derived.by<number | false>(() =>
52
54
  autoRefresh === '0' ? false : Number(autoRefresh),
53
55
  );
54
- const resolvedTitle = $derived(title ?? t('task.drawerTitle'));
56
+ const resolvedTitle = $derived(title ?? i18n.t('task.drawerTitle'));
55
57
 
56
58
  const taskQuery = useTaskList({
57
59
  get taskProvider() {
@@ -158,14 +160,14 @@
158
160
  function resetSubmitForm() {
159
161
  submitTaskName = '';
160
162
  submitIdempotencyKey = '';
161
- submitBodyText = t('task.bodyPlaceholder');
163
+ submitBodyText = i18n.t('task.bodyPlaceholder');
162
164
  submitError = null;
163
165
  }
164
166
 
165
167
  async function handleSubmitTask() {
166
168
  submitError = null;
167
169
  if (!submitTaskName.trim()) {
168
- submitError = t('validation.required');
170
+ submitError = i18n.t('validation.required');
169
171
  return;
170
172
  }
171
173
 
@@ -174,7 +176,7 @@
174
176
  try {
175
177
  body = JSON.parse(submitBodyText) as Record<string, unknown>;
176
178
  } catch {
177
- submitError = t('task.invalidJson');
179
+ submitError = i18n.t('task.invalidJson');
178
180
  return;
179
181
  }
180
182
  }
@@ -197,7 +199,7 @@
197
199
  dlqQuery.refetch?.(),
198
200
  ]);
199
201
  } catch (error) {
200
- submitError = error instanceof Error ? error.message : t('task.submitFailed');
202
+ submitError = error instanceof Error ? error.message : i18n.t('task.submitFailed');
201
203
  }
202
204
  }
203
205
 
@@ -232,16 +234,16 @@
232
234
  <div class="flex flex-wrap items-center gap-2">
233
235
  <ListTodo class="h-5 w-5 text-muted-foreground" />
234
236
  <h2 class="text-sm font-semibold">{resolvedTitle}</h2>
235
- <Badge variant="secondary" class="text-xs">{t('task.activeCount', { count: runningCount })}</Badge>
237
+ <Badge variant="secondary" class="text-xs">{i18n.t('task.activeCount', { count: runningCount })}</Badge>
236
238
  {#if allTasks[0] && resolveTaskCreatedAt(allTasks[0])}
237
- <span class="text-xs text-muted-foreground">{t('task.lastQueuedAt', { time: formatTime(resolveTaskCreatedAt(allTasks[0])) })}</span>
239
+ <span class="text-xs text-muted-foreground">{i18n.t('task.lastQueuedAt', { time: formatTime(resolveTaskCreatedAt(allTasks[0])) })}</span>
238
240
  {/if}
239
241
  </div>
240
242
  </div>
241
243
 
242
244
  {#if !taskProvider}
243
245
  <div class="flex flex-1 items-center justify-center p-6 text-sm text-muted-foreground">
244
- {t('common.configRequired')}
246
+ {i18n.t('common.configRequired')}
245
247
  </div>
246
248
  {:else}
247
249
  <div class="grid min-h-0 flex-1 gap-0 lg:grid-cols-[minmax(0,1.2fr)_minmax(340px,0.8fr)]">
@@ -252,11 +254,11 @@
252
254
  <Tabs.Root value={activeTab} class="w-full">
253
255
  <Tabs.List class="inline-flex rounded-lg border border-border bg-muted/40 p-1">
254
256
  <Tabs.Trigger value="tasks" active={activeTab === 'tasks'} onclick={() => activeTab = 'tasks'}>
255
- {t('task.tabTasks')}
257
+ {i18n.t('task.tabTasks')}
256
258
  <Badge variant="outline" class="ml-1 text-[10px]">{taskQuery.data?.total ?? allTasks.length}</Badge>
257
259
  </Tabs.Trigger>
258
260
  <Tabs.Trigger value="dlq" active={activeTab === 'dlq'} onclick={() => activeTab = 'dlq'}>
259
- {t('task.tabDlq')}
261
+ {i18n.t('task.tabDlq')}
260
262
  <Badge variant="outline" class="ml-1 text-[10px]">{dlqQuery.data?.total ?? dlqTasks.length}</Badge>
261
263
  </Tabs.Trigger>
262
264
  </Tabs.List>
@@ -264,11 +266,11 @@
264
266
  <div class="ml-auto flex items-center gap-2">
265
267
  <Button variant="outline" size="sm" onclick={() => void refreshCurrentTab()}>
266
268
  <RefreshCw class="mr-1.5 h-3.5 w-3.5" />
267
- {t('common.refresh')}
269
+ {i18n.t('common.refresh')}
268
270
  </Button>
269
271
  <Button size="sm" onclick={() => submitOpen = !submitOpen}>
270
272
  <Plus class="mr-1.5 h-3.5 w-3.5" />
271
- {t('task.submitAction')}
273
+ {i18n.t('task.submitAction')}
272
274
  </Button>
273
275
  </div>
274
276
  </div>
@@ -278,21 +280,21 @@
278
280
  <Search class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
279
281
  <Input
280
282
  class="pl-9"
281
- placeholder={t('task.searchPlaceholder')}
283
+ placeholder={i18n.t('task.searchPlaceholder')}
282
284
  bind:value={searchQuery}
283
285
  />
284
286
  </div>
285
- <Select bind:value={statusFilter} aria-label={t('task.statusFilterLabel')}>
286
- <option value="all">{t('task.statusAll')}</option>
287
+ <Select bind:value={statusFilter} aria-label={i18n.t('task.statusFilterLabel')}>
288
+ <option value="all">{i18n.t('task.statusAll')}</option>
287
289
  {#each availableStatuses as status, _i (_i)}
288
290
  <option value={status}>{status}</option>
289
291
  {/each}
290
292
  </Select>
291
- <Select bind:value={autoRefresh} aria-label={t('task.autoRefreshLabel')}>
292
- <option value="0">{t('task.refreshOff')}</option>
293
- <option value="5000">{t('task.refresh5s')}</option>
294
- <option value="15000">{t('task.refresh15s')}</option>
295
- <option value="30000">{t('task.refresh30s')}</option>
293
+ <Select bind:value={autoRefresh} aria-label={i18n.t('task.autoRefreshLabel')}>
294
+ <option value="0">{i18n.t('task.refreshOff')}</option>
295
+ <option value="5000">{i18n.t('task.refresh5s')}</option>
296
+ <option value="15000">{i18n.t('task.refresh15s')}</option>
297
+ <option value="30000">{i18n.t('task.refresh30s')}</option>
296
298
  </Select>
297
299
  </div>
298
300
  </div>
@@ -300,20 +302,20 @@
300
302
  {#if submitOpen}
301
303
  <Card.Root class="border-border/60 shadow-none">
302
304
  <Card.Header class="pb-3">
303
- <Card.Title class="text-base">{t('task.submitTitle')}</Card.Title>
304
- <Card.Description>{t('task.submitDescription')}</Card.Description>
305
+ <Card.Title class="text-base">{i18n.t('task.submitTitle')}</Card.Title>
306
+ <Card.Description>{i18n.t('task.submitDescription')}</Card.Description>
305
307
  </Card.Header>
306
308
  <Card.Content class="space-y-4 pt-0">
307
309
  <div class="space-y-2">
308
- <Label for="task-name">{t('task.taskNameLabel')}</Label>
310
+ <Label for="task-name">{i18n.t('task.taskNameLabel')}</Label>
309
311
  <Input id="task-name" bind:value={submitTaskName} placeholder="image.generate" />
310
312
  </div>
311
313
  <div class="space-y-2">
312
- <Label for="task-idempotency-key">{t('task.idempotencyKeyLabel')}</Label>
314
+ <Label for="task-idempotency-key">{i18n.t('task.idempotencyKeyLabel')}</Label>
313
315
  <Input id="task-idempotency-key" bind:value={submitIdempotencyKey} placeholder="poster-2026-04-19" />
314
316
  </div>
315
317
  <div class="space-y-2">
316
- <Label for="task-body">{t('task.bodyLabel')}</Label>
318
+ <Label for="task-body">{i18n.t('task.bodyLabel')}</Label>
317
319
  <Textarea id="task-body" bind:value={submitBodyText} class="min-h-[140px] font-mono text-xs" />
318
320
  </div>
319
321
  {#if submitError}
@@ -321,13 +323,13 @@
321
323
  {/if}
322
324
  <div class="flex flex-wrap justify-end gap-2">
323
325
  <Button variant="ghost" onclick={() => { submitOpen = false; submitError = null; }}>
324
- {t('common.cancel')}
326
+ {i18n.t('common.cancel')}
325
327
  </Button>
326
328
  <Button onclick={() => void handleSubmitTask()} disabled={submitTask.mutation.isPending}>
327
329
  {#if submitTask.mutation.isPending}
328
330
  <Loader2 class="mr-1.5 h-3.5 w-3.5 animate-spin" />
329
331
  {/if}
330
- {t('task.submitAction')}
332
+ {i18n.t('task.submitAction')}
331
333
  </Button>
332
334
  </div>
333
335
  </Card.Content>
@@ -338,8 +340,8 @@
338
340
  tasks={filteredTasks}
339
341
  {taskProvider}
340
342
  dlq={activeTab === 'dlq'}
341
- title={activeTab === 'dlq' ? t('task.dlqTitle') : t('task.listTitle')}
342
- emptyText={activeTab === 'dlq' ? t('task.noDlq') : t('task.noTasks')}
343
+ title={activeTab === 'dlq' ? i18n.t('task.dlqTitle') : i18n.t('task.listTitle')}
344
+ emptyText={activeTab === 'dlq' ? i18n.t('task.noDlq') : i18n.t('task.noTasks')}
343
345
  onSelect={(task) => {
344
346
  selectedTaskId = task.id;
345
347
  }}
@@ -361,7 +363,7 @@
361
363
  />
362
364
  {:else}
363
365
  <div class="flex h-full min-h-[280px] items-center justify-center rounded-lg border border-dashed border-border/60 bg-background/70 text-center text-sm text-muted-foreground">
364
- {t('task.selectHint')}
366
+ {i18n.t('task.selectHint')}
365
367
  </div>
366
368
  {/if}
367
369
  </div>
@@ -1,9 +1,12 @@
1
1
  <script lang="ts">
2
- import { t } from '@svadmin/core/i18n';
2
+ import { useTranslation } from '@svadmin/core/i18n';
3
+
3
4
  import { Ban, Clock3, Loader2, CheckCircle2, AlertTriangle, CircleDashed } from '@lucide/svelte';
4
5
  import { Badge } from './ui/badge/index.js';
5
6
  import { normalizeTaskStatus } from './task-utils.js';
6
7
 
8
+ const i18n = useTranslation();
9
+
7
10
  let {
8
11
  status = 'pending',
9
12
  showIcon = true,
@@ -19,7 +22,7 @@
19
22
 
20
23
  function translateStatus(value: string) {
21
24
  const key = `task.status.${value}`;
22
- const translated = t(key);
25
+ const translated = i18n.t(key);
23
26
  if (translated !== key) return translated;
24
27
  return value.charAt(0).toUpperCase() + value.slice(1);
25
28
  }
@@ -1,11 +1,22 @@
1
1
  <script lang="ts">
2
- /* eslint-disable @typescript-eslint/no-explicit-any */
3
- import { Toaster, toast as sonner } from 'sonner-svelte';
2
+ import { onMount } from 'svelte';
3
+ import { Toaster, toast as sonner, type ToasterProps } from 'svelte-sonner';
4
4
  import { getResolvedTheme, getToastQueue, consumeToastQueue, getPromiseQueue, consumePromiseQueue } from '@svadmin/core';
5
+ import { registerToastHost, type ToastHostRegistration } from './toast-host.svelte.js';
5
6
 
7
+ let host = $state<ToastHostRegistration | null>(null);
8
+ const isActiveHost = $derived(host?.isActive() ?? false);
6
9
  const theme = $derived(getResolvedTheme() === 'dark' ? 'dark' : 'light');
7
10
 
11
+ onMount(() => {
12
+ const registration = registerToastHost();
13
+ host = registration;
14
+
15
+ return () => registration.unregister();
16
+ });
17
+
8
18
  $effect(() => {
19
+ if (!isActiveHost) return;
9
20
  const queue = getToastQueue();
10
21
  if (queue.length > 0) {
11
22
  for (const t of queue) {
@@ -21,6 +32,7 @@
21
32
  });
22
33
 
23
34
  $effect(() => {
35
+ if (!isActiveHost) return;
24
36
  const pQueue = getPromiseQueue();
25
37
  if (pQueue.length > 0) {
26
38
  for (const p of pQueue) {
@@ -41,7 +53,9 @@
41
53
  toast: 'font-sans',
42
54
  },
43
55
  }
44
- } as any);
56
+ } satisfies ToasterProps);
45
57
  </script>
46
58
 
47
- <Toaster {...toasterProps} />
59
+ {#if isActiveHost}
60
+ <Toaster {...toasterProps} />
61
+ {/if}
@@ -34,12 +34,34 @@
34
34
  children,
35
35
  ...restProps
36
36
  }: Props = $props();
37
+
38
+ let open = $state(false);
39
+
40
+ function handleClick(event: MouseEvent, primitiveOnclick?: (event: MouseEvent) => void) {
41
+ primitiveOnclick?.(event);
42
+ open = false;
43
+ onclick?.(event);
44
+ }
45
+
46
+ function mergeButtonProps(primitiveProps: Record<string, any>) {
47
+ const merged: Record<string, any> = { ...primitiveProps, ...restProps };
48
+ for (const [key, userHandler] of Object.entries(restProps)) {
49
+ const primitiveHandler = primitiveProps[key];
50
+ if (!key.startsWith('on') || typeof userHandler !== 'function' || typeof primitiveHandler !== 'function') continue;
51
+ merged[key] = (event: Event) => {
52
+ primitiveHandler(event);
53
+ userHandler(event);
54
+ };
55
+ }
56
+ return merged;
57
+ }
37
58
  </script>
38
59
 
39
- <Tooltip.Root>
40
- <Tooltip.Trigger>
60
+ <Tooltip.Root bind:open>
61
+ <Tooltip.Trigger aria-describedby={restProps['aria-describedby']}>
41
62
  {#snippet child({ props })}
42
- {@const buttonProps = { ...props, ...restProps, href, target, rel, variant, size, type, class: className, 'aria-label': restProps['aria-label'] ?? tooltip, onclick, disabled, tabindex } as any}
63
+ {@const primitiveOnclick = props.onclick as ((event: MouseEvent) => void) | undefined}
64
+ {@const buttonProps = { ...mergeButtonProps(props), href, target, rel, variant, size, type, class: className, 'aria-label': restProps['aria-label'] ?? tooltip, 'aria-describedby': props['aria-describedby'], onclick: (event: MouseEvent) => handleClick(event, primitiveOnclick), disabled, tabindex } as any}
43
65
  <Button {...buttonProps}>
44
66
  {@render children()}
45
67
  </Button>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { t } from '@svadmin/core/i18n';
2
+ import { useTranslation } from '@svadmin/core/i18n';
3
3
  import * as Card from './ui/card/index.js';
4
4
  import { Button } from './ui/button/index.js';
5
5
  import { Input } from './ui/input/index.js';
@@ -8,6 +8,8 @@
8
8
  import { Separator } from './ui/separator/index.js';
9
9
  import { Shield, Copy, CheckCircle2, AlertTriangle, ArrowRight, ArrowLeft } from '@lucide/svelte';
10
10
 
11
+ const i18n = useTranslation();
12
+
11
13
  type Step = 'intro' | 'scan' | 'verify' | 'recovery';
12
14
 
13
15
  let currentStep = $state<Step>('intro');
@@ -45,7 +47,7 @@
45
47
  function handleVerify() {
46
48
  verifyError = '';
47
49
  if (verificationCode.length !== 6) {
48
- verifyError = t('auth.twoFactorEnterCode');
50
+ verifyError = i18n.t('auth.twoFactorEnterCode');
49
51
  return;
50
52
  }
51
53
  currentStep = 'recovery';
@@ -89,34 +91,34 @@
89
91
  <Shield class="h-8 w-8" />
90
92
  </div>
91
93
  <div class="space-y-2">
92
- <h2 class="text-2xl font-semibold text-foreground">{t('auth.twoFactorSetup')}</h2>
93
- <p class="text-sm text-muted-foreground">{t('security.twoFactorDescription')}</p>
94
+ <h2 class="text-2xl font-semibold text-foreground">{i18n.t('auth.twoFactorSetup')}</h2>
95
+ <p class="text-sm text-muted-foreground">{i18n.t('security.twoFactorDescription')}</p>
94
96
  </div>
95
97
  <div class="space-y-3 text-left">
96
98
  <div class="flex items-start gap-3 rounded-xl border p-3">
97
99
  <div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-blue-500/10 text-blue-500 text-sm font-bold">1</div>
98
100
  <div>
99
- <p class="text-sm font-medium text-foreground">{t('auth.twoFactorScanQR')}</p>
101
+ <p class="text-sm font-medium text-foreground">{i18n.t('auth.twoFactorScanQR')}</p>
100
102
  <p class="text-xs text-muted-foreground">Google Authenticator, Authy, etc.</p>
101
103
  </div>
102
104
  </div>
103
105
  <div class="flex items-start gap-3 rounded-xl border p-3">
104
106
  <div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-blue-500/10 text-blue-500 text-sm font-bold">2</div>
105
107
  <div>
106
- <p class="text-sm font-medium text-foreground">{t('auth.twoFactorEnterCode')}</p>
108
+ <p class="text-sm font-medium text-foreground">{i18n.t('auth.twoFactorEnterCode')}</p>
107
109
  <p class="text-xs text-muted-foreground">6-digit code from your authenticator</p>
108
110
  </div>
109
111
  </div>
110
112
  <div class="flex items-start gap-3 rounded-xl border p-3">
111
113
  <div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-blue-500/10 text-blue-500 text-sm font-bold">3</div>
112
114
  <div>
113
- <p class="text-sm font-medium text-foreground">{t('auth.twoFactorSaveRecovery')}</p>
114
- <p class="text-xs text-muted-foreground">{t('auth.twoFactorRecoveryHint')}</p>
115
+ <p class="text-sm font-medium text-foreground">{i18n.t('auth.twoFactorSaveRecovery')}</p>
116
+ <p class="text-xs text-muted-foreground">{i18n.t('auth.twoFactorRecoveryHint')}</p>
115
117
  </div>
116
118
  </div>
117
119
  </div>
118
120
  <Button class="w-full" onclick={startSetup}>
119
- {t('auth.twoFactorSetup')} <ArrowRight class="h-4 w-4 ml-2" />
121
+ {i18n.t('auth.twoFactorSetup')} <ArrowRight class="h-4 w-4 ml-2" />
120
122
  </Button>
121
123
  </Card.CardContent>
122
124
  </Card.Card>
@@ -126,7 +128,7 @@
126
128
  <Card.Card class="border-border/60">
127
129
  <Card.CardContent class="space-y-6 p-8">
128
130
  <div class="text-center space-y-2">
129
- <h2 class="text-xl font-semibold text-foreground">{t('auth.twoFactorScanQR')}</h2>
131
+ <h2 class="text-xl font-semibold text-foreground">{i18n.t('auth.twoFactorScanQR')}</h2>
130
132
  <p class="text-sm text-muted-foreground">Use your authenticator app to scan the code below.</p>
131
133
  </div>
132
134
  <div class="mx-auto flex h-48 w-48 items-center justify-center rounded-xl border-2 border-dashed border-border/60 bg-muted/20">
@@ -138,10 +140,10 @@
138
140
  </div>
139
141
  <div class="flex gap-3">
140
142
  <Button variant="outline" class="flex-1" onclick={() => currentStep = 'intro'}>
141
- <ArrowLeft class="h-4 w-4 mr-2" />{t('common.back')}
143
+ <ArrowLeft class="h-4 w-4 mr-2" />{i18n.t('common.back')}
142
144
  </Button>
143
145
  <Button class="flex-1" onclick={proceedToVerify}>
144
- {t('common.next')} <ArrowRight class="h-4 w-4 ml-2" />
146
+ {i18n.t('common.next')} <ArrowRight class="h-4 w-4 ml-2" />
145
147
  </Button>
146
148
  </div>
147
149
  </Card.CardContent>
@@ -152,8 +154,8 @@
152
154
  <Card.Card class="border-border/60">
153
155
  <Card.CardContent class="space-y-6 p-8">
154
156
  <div class="text-center space-y-2">
155
- <h2 class="text-xl font-semibold text-foreground">{t('auth.twoFactorVerify')}</h2>
156
- <p class="text-sm text-muted-foreground">{t('auth.twoFactorEnterCode')}</p>
157
+ <h2 class="text-xl font-semibold text-foreground">{i18n.t('auth.twoFactorVerify')}</h2>
158
+ <p class="text-sm text-muted-foreground">{i18n.t('auth.twoFactorEnterCode')}</p>
157
159
  </div>
158
160
 
159
161
  {#if verifyError}
@@ -178,10 +180,10 @@
178
180
 
179
181
  <div class="flex gap-3">
180
182
  <Button variant="outline" class="flex-1" onclick={() => currentStep = 'scan'}>
181
- <ArrowLeft class="h-4 w-4 mr-2" />{t('common.back')}
183
+ <ArrowLeft class="h-4 w-4 mr-2" />{i18n.t('common.back')}
182
184
  </Button>
183
185
  <Button class="flex-1" onclick={handleVerify}>
184
- {t('common.confirm')} <ArrowRight class="h-4 w-4 ml-2" />
186
+ {i18n.t('common.confirm')} <ArrowRight class="h-4 w-4 ml-2" />
185
187
  </Button>
186
188
  </div>
187
189
  </Card.CardContent>
@@ -195,8 +197,8 @@
195
197
  <div class="mx-auto inline-flex h-12 w-12 items-center justify-center rounded-xl bg-green-500/10 text-green-500">
196
198
  <CheckCircle2 class="h-6 w-6" />
197
199
  </div>
198
- <h2 class="text-xl font-semibold text-foreground">{t('auth.twoFactorRecovery')}</h2>
199
- <p class="text-sm text-muted-foreground">{t('auth.twoFactorRecoveryHint')}</p>
200
+ <h2 class="text-xl font-semibold text-foreground">{i18n.t('auth.twoFactorRecovery')}</h2>
201
+ <p class="text-sm text-muted-foreground">{i18n.t('auth.twoFactorRecoveryHint')}</p>
200
202
  </div>
201
203
 
202
204
  <div class="rounded-xl border bg-muted/20 p-4">
@@ -210,13 +212,13 @@
210
212
  <div class="flex gap-3">
211
213
  <Button variant="outline" class="flex-1" onclick={copyRecoveryCodes}>
212
214
  {#if copiedRecovery}
213
- <CheckCircle2 class="h-4 w-4 mr-2" />{t('common.copied')}
215
+ <CheckCircle2 class="h-4 w-4 mr-2" />{i18n.t('common.copied')}
214
216
  {:else}
215
- <Copy class="h-4 w-4 mr-2" />{t('common.copyAll')}
217
+ <Copy class="h-4 w-4 mr-2" />{i18n.t('common.copyAll')}
216
218
  {/if}
217
219
  </Button>
218
220
  <Button class="flex-1" onclick={completeSetup}>
219
- {t('account.completeSetup')} <ArrowRight class="h-4 w-4 ml-2" />
221
+ {i18n.t('account.completeSetup')} <ArrowRight class="h-4 w-4 ml-2" />
220
222
  </Button>
221
223
  </div>
222
224
  </Card.CardContent>
@@ -230,15 +232,15 @@
230
232
  <Shield class="h-8 w-8" />
231
233
  </div>
232
234
  <div class="space-y-2">
233
- <h2 class="text-2xl font-semibold text-foreground">{t('auth.twoFactorVerifySuccess')}</h2>
234
- <p class="text-sm text-muted-foreground">{t('security.twoFactorActive')}</p>
235
+ <h2 class="text-2xl font-semibold text-foreground">{i18n.t('auth.twoFactorVerifySuccess')}</h2>
236
+ <p class="text-sm text-muted-foreground">{i18n.t('security.twoFactorActive')}</p>
235
237
  </div>
236
238
  <Badge variant="secondary" class="bg-green-500/10 text-green-600">
237
239
  <CheckCircle2 class="h-3 w-3 mr-1" />2FA Active
238
240
  </Badge>
239
241
  <Separator />
240
242
  <Button variant="destructive" onclick={disable2fa}>
241
- {t('auth.twoFactorDisable')}
243
+ {i18n.t('auth.twoFactorDisable')}
242
244
  </Button>
243
245
  </Card.CardContent>
244
246
  </Card.Card>
@@ -1,10 +1,13 @@
1
1
  <script lang="ts">
2
2
  import { Button } from './ui/button/index.js';
3
3
  import { Progress } from './ui/progress/index.js';
4
- import { t } from '@svadmin/core/i18n';
4
+ import { useTranslation } from '@svadmin/core/i18n';
5
+
5
6
  import { slide } from 'svelte/transition';
6
7
  import { X, Undo2 } from '@lucide/svelte';
7
8
 
9
+ const i18n = useTranslation();
10
+
8
11
  let { message, duration = 5000, onUndo, onTimeout } = $props<{
9
12
  message: string;
10
13
  duration?: number;
@@ -56,7 +59,7 @@
56
59
  <div class="flex items-center gap-1 flex-shrink-0">
57
60
  <Button variant="ghost" size="sm" onclick={handleUndo} class="font-semibold text-primary">
58
61
  <Undo2 class="h-3.5 w-3.5 mr-1" />
59
- {t('common.undo')}
62
+ {i18n.t('common.undo')}
60
63
  </Button>
61
64
  <Button variant="ghost" size="icon" class="h-7 w-7" onclick={handleDismiss}>
62
65
  <X class="h-3.5 w-3.5" />
@@ -1,16 +1,19 @@
1
1
  <script lang="ts">
2
- import { useUpdatePassword } from '@svadmin/core';
3
- import { t } from '@svadmin/core/i18n';
4
- import { navigate } from '@svadmin/core/router';
2
+ import { captureAdminContext, useUpdatePassword } from '@svadmin/core';
3
+ import { useTranslation } from '@svadmin/core/i18n';
4
+
5
5
  import { Button } from './ui/button/index.js';
6
6
  import * as Alert from './ui/alert/index.js';
7
7
  import PasswordInput from './PasswordInput.svelte';
8
8
  import { ShieldCheck, Loader2, AlertCircle, Shield } from '@lucide/svelte';
9
9
 
10
+ const i18n = useTranslation();
11
+
10
12
  let { title = 'Admin' } = $props<{
11
13
  title?: string;
12
14
  }>();
13
15
 
16
+ const adminContext = captureAdminContext();
14
17
  const updatePw = useUpdatePassword();
15
18
 
16
19
  let password = $state('');
@@ -21,12 +24,12 @@
21
24
  e.preventDefault();
22
25
  error = '';
23
26
 
24
- if (!password) { error = t('auth.passwordRequired'); return; }
25
- if (password !== confirmPassword) { error = t('auth.passwordMismatch'); return; }
27
+ if (!password) { error = i18n.t('auth.passwordRequired'); return; }
28
+ if (password !== confirmPassword) { error = i18n.t('auth.passwordMismatch'); return; }
26
29
 
27
30
  const result = await updatePw.mutate({ password, confirmPassword });
28
31
  if (!result.success) {
29
- error = result.error?.message ?? t('common.operationFailed');
32
+ error = result.error?.message ?? i18n.t('common.operationFailed');
30
33
  }
31
34
  }
32
35
  </script>
@@ -52,10 +55,10 @@
52
55
 
53
56
  <div class="space-y-4">
54
57
  <h2 class="text-3xl xl:text-4xl font-bold text-white leading-tight">
55
- {t('auth.resetPassword')}
58
+ {i18n.t('auth.resetPassword')}
56
59
  </h2>
57
60
  <p class="text-base text-white/70 leading-relaxed max-w-[320px]">
58
- {t('auth.resetPasswordDescription')}
61
+ {i18n.t('auth.resetPasswordDescription')}
59
62
  </p>
60
63
  </div>
61
64
 
@@ -78,8 +81,8 @@
78
81
  </div>
79
82
 
80
83
  <div class="space-y-1 mb-8">
81
- <h1 class="text-2xl font-semibold tracking-tight">{t('auth.resetPassword')}</h1>
82
- <p class="text-sm text-muted-foreground">{t('auth.resetPasswordDescription')}</p>
84
+ <h1 class="text-2xl font-semibold tracking-tight">{i18n.t('auth.resetPassword')}</h1>
85
+ <p class="text-sm text-muted-foreground">{i18n.t('auth.resetPasswordDescription')}</p>
83
86
  </div>
84
87
 
85
88
  <form onsubmit={handleSubmit} class="space-y-5">
@@ -92,7 +95,7 @@
92
95
 
93
96
  <PasswordInput
94
97
  id="new-password"
95
- label={t('auth.password')}
98
+ label={i18n.t('auth.password')}
96
99
  bind:value={password}
97
100
  autocomplete="new-password"
98
101
  showStrength
@@ -100,7 +103,7 @@
100
103
 
101
104
  <PasswordInput
102
105
  id="confirm-password"
103
- label={t('auth.confirmPassword')}
106
+ label={i18n.t('auth.confirmPassword')}
104
107
  bind:value={confirmPassword}
105
108
  autocomplete="new-password"
106
109
  />
@@ -109,12 +112,12 @@
109
112
  {#if updatePw.isLoading}
110
113
  <Loader2 class="h-4 w-4 animate-spin mr-2" />
111
114
  {/if}
112
- {t('auth.resetPassword')}
115
+ {i18n.t('auth.resetPassword')}
113
116
  </Button>
114
117
 
115
118
  <div class="flex items-center justify-center mt-2">
116
- <Button variant="link" class="text-sm h-auto p-0 font-medium" onclick={() => navigate('/login')}>
117
- {t('auth.backToLogin')}
119
+ <Button variant="link" class="text-sm h-auto p-0 font-medium" onclick={() => adminContext.navigate('/login')}>
120
+ {i18n.t('auth.backToLogin')}
118
121
  </Button>
119
122
  </div>
120
123
  </form>
@@ -35,9 +35,6 @@
35
35
  }: Props<unknown> = $props();
36
36
 
37
37
  let scrollTop = $state(0);
38
- let containerRef = $state<HTMLDivElement | null>(null);
39
-
40
- const _totalHeight = $derived(items.length * rowHeight);
41
38
 
42
39
  const visibleRange = $derived.by(() => {
43
40
  const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
@@ -61,7 +58,6 @@
61
58
  </script>
62
59
 
63
60
  <div
64
- bind:this={containerRef}
65
61
  class="overflow-auto {className}"
66
62
  style="height: {containerHeight}px;"
67
63
  onscroll={handleScroll}