@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,10 +1,13 @@
1
1
  <script lang="ts">
2
- import { t } from '@svadmin/core/i18n';
3
- import { navigate } from '@svadmin/core/router';
2
+ import { captureAdminContext } from '@svadmin/core';
3
+ import { useTranslation } from '@svadmin/core/i18n';
4
+
4
5
  import { AlertTriangle, ArrowLeft, FileQuestion, Home } from '@lucide/svelte';
5
6
  import { Button } from './ui/button/index.js';
6
7
  import * as Card from './ui/card/index.js';
7
8
 
9
+ const i18n = useTranslation();
10
+
8
11
  interface Props {
9
12
  status?: '404' | '500';
10
13
  title?: string;
@@ -12,17 +15,16 @@
12
15
  }
13
16
 
14
17
  let { status = '404', title, description }: Props = $props();
18
+ const adminContext = captureAdminContext();
15
19
 
16
20
  function goBack() {
17
- if (typeof window !== 'undefined') {
18
- window.history.back();
19
- }
21
+ adminContext.back();
20
22
  }
21
23
 
22
24
  const isNotFound = $derived(status === '404');
23
- const resolvedTitle = $derived(title ?? (isNotFound ? t('common.pageNotFound') : t('common.error')));
25
+ const resolvedTitle = $derived(title ?? (isNotFound ? i18n.t('common.pageNotFound') : i18n.t('common.error')));
24
26
  const resolvedDescription = $derived(
25
- description ?? (isNotFound ? t('error.pageNotFoundDescription') : t('error.internalServerErrorDescription')),
27
+ description ?? (isNotFound ? i18n.t('error.pageNotFoundDescription') : i18n.t('error.internalServerErrorDescription')),
26
28
  );
27
29
  </script>
28
30
 
@@ -46,11 +48,11 @@
46
48
  <div class="flex flex-col justify-center gap-2 sm:flex-row">
47
49
  <Button variant="outline" onclick={goBack}>
48
50
  <ArrowLeft class="h-4 w-4" />
49
- {t('common.back')}
51
+ {i18n.t('common.back')}
50
52
  </Button>
51
- <Button onclick={() => navigate('/')}>
53
+ <Button onclick={() => adminContext.navigate('/')}>
52
54
  <Home class="h-4 w-4" />
53
- {t('common.returnHome')}
55
+ {i18n.t('common.returnHome')}
54
56
  </Button>
55
57
  </div>
56
58
  </Card.CardContent>
@@ -5,7 +5,7 @@
5
5
 
6
6
  <script lang="ts">
7
7
  import type { FieldDefinition } from '@svadmin/core';
8
- import { t } from '@svadmin/core/i18n';
8
+ import { useTranslation } from '@svadmin/core/i18n';
9
9
  import { Input } from './ui/input/index.js';
10
10
  import { Textarea } from './ui/textarea/index.js';
11
11
  import { Switch } from './ui/switch/index.js';
@@ -21,6 +21,8 @@
21
21
  import type { Snippet } from 'svelte';
22
22
  import { getRichTextEditor } from '../editor-config.svelte.js';
23
23
 
24
+ const i18n = useTranslation();
25
+
24
26
  let { field, value, onchange, disabled, children } = $props<{
25
27
  field: FieldDefinition;
26
28
  value: unknown;
@@ -107,7 +109,7 @@
107
109
  oninput={(e) => onchange((e.target as HTMLInputElement).value)}
108
110
  required={field.required}
109
111
  {disabled}
110
- placeholder={t('field.enterValue', { label: field.label })}
112
+ placeholder={i18n.t('field.enterValue', { label: field.label })}
111
113
  />
112
114
 
113
115
  {:else if field.type === 'email'}
@@ -188,7 +190,7 @@
188
190
  <EditorComp
189
191
  id={field.key}
190
192
  value={strVal}
191
- placeholder={t('field.enterValue', { label: field.label })}
193
+ placeholder={i18n.t('field.enterValue', { label: field.label })}
192
194
  preset="full"
193
195
  onchange={(html: string) => onchange(html)}
194
196
  {disabled}
@@ -201,7 +203,7 @@
201
203
  required={field.required}
202
204
  {disabled}
203
205
  rows={10}
204
- placeholder={t('field.enterValue', { label: field.label })}
206
+ placeholder={i18n.t('field.enterValue', { label: field.label })}
205
207
  class="resize-y"
206
208
  />
207
209
  {/if}
@@ -214,7 +216,7 @@
214
216
  required={field.required}
215
217
  {disabled}
216
218
  rows={4}
217
- placeholder={t('field.enterValue', { label: field.label })}
219
+ placeholder={i18n.t('field.enterValue', { label: field.label })}
218
220
  class="resize-y"
219
221
  />
220
222
 
@@ -226,7 +228,7 @@
226
228
  onchange={(v) => onchange(v)}
227
229
  optionLabel={field.optionLabel ?? 'title'}
228
230
  optionValue={field.optionValue ?? 'id'}
229
- placeholder={t('field.selectPlaceholder')}
231
+ placeholder={i18n.t('field.selectPlaceholder')}
230
232
  {disabled}
231
233
  />
232
234
 
@@ -240,7 +242,7 @@
240
242
  required={field.required}
241
243
  disabled={disabled}
242
244
  >
243
- <option value="">{t('field.selectPlaceholder')}</option>
245
+ <option value="">{i18n.t('field.selectPlaceholder')}</option>
244
246
  {#each field.options ?? [] as opt, _i (_i)}
245
247
  <option value={opt.value}>{opt.label}</option>
246
248
  {/each}
@@ -252,7 +254,7 @@
252
254
  onchange={(e) => onchange((e.target as HTMLSelectElement).value)}
253
255
  required={field.required}
254
256
  {disabled}
255
- placeholder={t('field.selectPlaceholder')}
257
+ placeholder={i18n.t('field.selectPlaceholder')}
256
258
  >
257
259
  {#each field.options ?? [] as opt, _i (_i)}
258
260
  <option value={opt.value}>{opt.label}</option>
@@ -278,7 +280,7 @@
278
280
  </label>
279
281
  {/each}
280
282
  {#if !(field.options?.length)}
281
- <p class="text-xs text-muted-foreground">{t('field.noOptions')}</p>
283
+ <p class="text-xs text-muted-foreground">{i18n.t('field.noOptions')}</p>
282
284
  {/if}
283
285
  </div>
284
286
  {#if multiVal.length > 0}
@@ -287,7 +289,7 @@
287
289
  {@const label = field.options?.find((o: { label: string; value: string | number }) => o.value === v)?.label ?? String(v)}
288
290
  <Badge variant="secondary" class="gap-1">
289
291
  {label}
290
- <button type="button" onclick={() => toggleMulti(v)} class="ml-0.5 rounded-sm hover:text-destructive hover:bg-destructive/10 transition-colors" aria-label={t('common.clear')}>×</button>
292
+ <button type="button" onclick={() => toggleMulti(v)} class="ml-0.5 rounded-sm hover:text-destructive hover:bg-destructive/10 transition-colors" aria-label={i18n.t('common.clear')}>×</button>
291
293
  </Badge>
292
294
  {/each}
293
295
  </div>
@@ -301,7 +303,7 @@
301
303
  onCheckedChange={(v) => onchange(v)}
302
304
  disabled={disabled}
303
305
  />
304
- <span class="text-sm text-muted-foreground">{boolVal ? t('common.yes') : t('common.no')}</span>
306
+ <span class="text-sm text-muted-foreground">{boolVal ? i18n.t('common.yes') : i18n.t('common.no')}</span>
305
307
  </div>
306
308
 
307
309
  {:else if field.type === 'tags'}
@@ -314,7 +316,7 @@
314
316
  type="button"
315
317
  onclick={() => removeTag(i)}
316
318
  class="ml-0.5 rounded-sm hover:text-destructive hover:bg-destructive/10 transition-colors"
317
- aria-label={t('common.clear')}
319
+ aria-label={i18n.t('common.clear')}
318
320
  >×</button>
319
321
  </Badge>
320
322
  {/each}
@@ -322,7 +324,7 @@
322
324
  <Input
323
325
  id={field.key}
324
326
  type="text"
325
- placeholder={t('field.tagsPlaceholder')}
327
+ placeholder={i18n.t('field.tagsPlaceholder')}
326
328
  onkeydown={handleTagKeydown}
327
329
  {disabled}
328
330
  />
@@ -354,13 +356,13 @@
354
356
  {#if url}
355
357
  <img src={url} alt="preview" class="h-9 w-9 rounded object-cover border" />
356
358
  {/if}
357
- <TooltipButton tooltip={t('common.removeImage')} variant="ghost" size="icon" class="h-8 w-8 shrink-0" onclick={() => removeImage(i)}>
359
+ <TooltipButton tooltip={i18n.t('common.removeImage')} variant="ghost" size="icon" class="h-8 w-8 shrink-0" onclick={() => removeImage(i)}>
358
360
  <X class="h-3.5 w-3.5" />
359
361
  </TooltipButton>
360
362
  </div>
361
363
  {/each}
362
364
  <Button variant="outline" size="sm" type="button" onclick={addImage}>
363
- <Plus class="h-3.5 w-3.5 mr-1" /> {t('field.addImage')}
365
+ <Plus class="h-3.5 w-3.5 mr-1" /> {i18n.t('field.addImage')}
364
366
  </Button>
365
367
  </div>
366
368
 
@@ -1,17 +1,20 @@
1
1
  <script lang="ts">
2
- import { useForgotPassword } from '@svadmin/core';
3
- import { t } from '@svadmin/core/i18n';
4
- import { navigate } from '@svadmin/core/router';
2
+ import { captureAdminContext, useForgotPassword } from '@svadmin/core';
3
+ import { useTranslation } from '@svadmin/core/i18n';
4
+
5
5
  import { Button } from './ui/button/index.js';
6
6
  import { Input } from './ui/input/index.js';
7
7
  import { Label } from './ui/label/index.js';
8
8
  import * as Alert from './ui/alert/index.js';
9
9
  import { User, ArrowLeft, CheckCircle, Loader2, AlertCircle, Shield } from '@lucide/svelte';
10
10
 
11
+ const i18n = useTranslation();
12
+
11
13
  let { title = 'Admin' } = $props<{
12
14
  title?: string;
13
15
  }>();
14
16
 
17
+ const adminContext = captureAdminContext();
15
18
  const forgot = useForgotPassword();
16
19
 
17
20
  let identifier = $state('');
@@ -22,13 +25,13 @@
22
25
  e.preventDefault();
23
26
  error = '';
24
27
 
25
- if (!identifier) { error = t('auth.usernameOrEmailRequired'); return; }
28
+ if (!identifier) { error = i18n.t('auth.usernameOrEmailRequired'); return; }
26
29
 
27
30
  const result = await forgot.mutate({ email: identifier, username: identifier });
28
31
  if (result.success) {
29
32
  sent = true;
30
33
  } else {
31
- error = result.error?.message ?? t('common.operationFailed');
34
+ error = result.error?.message ?? i18n.t('common.operationFailed');
32
35
  }
33
36
  }
34
37
  </script>
@@ -54,10 +57,10 @@
54
57
 
55
58
  <div class="space-y-4">
56
59
  <h2 class="text-3xl xl:text-4xl font-bold text-white leading-tight">
57
- {t('auth.forgotPassword')}
60
+ {i18n.t('auth.forgotPassword')}
58
61
  </h2>
59
62
  <p class="text-base text-white/70 leading-relaxed max-w-[320px]">
60
- {t('auth.forgotPasswordDescription')}
63
+ {i18n.t('auth.forgotPasswordDescription')}
61
64
  </p>
62
65
  </div>
63
66
 
@@ -82,16 +85,16 @@
82
85
  <div class="space-y-1 mb-8">
83
86
  <h1 class="text-2xl font-semibold tracking-tight">
84
87
  {#if sent}
85
- {t('auth.resetLinkSent')}
88
+ {i18n.t('auth.resetLinkSent')}
86
89
  {:else}
87
- {t('auth.forgotPassword')}
90
+ {i18n.t('auth.forgotPassword')}
88
91
  {/if}
89
92
  </h1>
90
93
  <p class="text-sm text-muted-foreground">
91
94
  {#if sent}
92
- {t('auth.resetLinkSent')}
95
+ {i18n.t('auth.resetLinkSent')}
93
96
  {:else}
94
- {t('auth.forgotPasswordDescription')}
97
+ {i18n.t('auth.forgotPasswordDescription')}
95
98
  {/if}
96
99
  </p>
97
100
  </div>
@@ -100,11 +103,11 @@
100
103
  <div class="space-y-4">
101
104
  <div class="flex items-center gap-3 rounded-lg border bg-muted/50 p-4">
102
105
  <CheckCircle class="h-5 w-5 text-primary shrink-0" />
103
- <p class="text-sm text-muted-foreground">{t('auth.resetLinkSent')}</p>
106
+ <p class="text-sm text-muted-foreground">{i18n.t('auth.resetLinkSent')}</p>
104
107
  </div>
105
- <Button variant="outline" class="w-full" onclick={() => navigate('/login')}>
108
+ <Button variant="outline" class="w-full" onclick={() => adminContext.navigate('/login')}>
106
109
  <ArrowLeft class="h-4 w-4 mr-2" />
107
- {t('auth.backToLogin')}
110
+ {i18n.t('auth.backToLogin')}
108
111
  </Button>
109
112
  </div>
110
113
  {:else}
@@ -117,13 +120,13 @@
117
120
  {/if}
118
121
 
119
122
  <div class="space-y-2">
120
- <Label for="forgot-identifier">{t('auth.usernameOrEmail')}</Label>
123
+ <Label for="forgot-identifier">{i18n.t('auth.usernameOrEmail')}</Label>
121
124
  <div class="relative">
122
125
  <User class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none z-[1]" />
123
126
  <Input
124
127
  id="forgot-identifier"
125
128
  type="text"
126
- placeholder={t('auth.identifierPlaceholder')}
129
+ placeholder={i18n.t('auth.identifierPlaceholder')}
127
130
  bind:value={identifier}
128
131
  class="pl-9"
129
132
  autocomplete="username"
@@ -135,14 +138,14 @@
135
138
  {#if forgot.isLoading}
136
139
  <Loader2 class="h-4 w-4 animate-spin mr-2" />
137
140
  {/if}
138
- {t('auth.sendResetLink')}
141
+ {i18n.t('auth.sendResetLink')}
139
142
  </Button>
140
143
  </form>
141
144
 
142
145
  <div class="flex items-center justify-center mt-8 pt-6 border-t">
143
- <Button variant="link" class="text-sm h-auto p-0 font-medium inline-flex items-center gap-1" onclick={() => navigate('/login')}>
146
+ <Button variant="link" class="text-sm h-auto p-0 font-medium inline-flex items-center gap-1" onclick={() => adminContext.navigate('/login')}>
144
147
  <ArrowLeft class="h-3 w-3" />
145
- {t('auth.backToLogin')}
148
+ {i18n.t('auth.backToLogin')}
146
149
  </Button>
147
150
  </div>
148
151
  {/if}
@@ -5,9 +5,12 @@
5
5
  import TooltipButton from './TooltipButton.svelte';
6
6
  import { getResolvedTheme, toggleTheme } from '@svadmin/core';
7
7
  import Breadcrumbs from './Breadcrumbs.svelte';
8
- import { t } from '@svadmin/core/i18n';
8
+ import { useTranslation } from '@svadmin/core/i18n';
9
+
9
10
  import { getComponentRegistry } from '../component-registry.svelte.js';
10
11
 
12
+ const i18n = useTranslation();
13
+
11
14
  let {
12
15
  showThemeToggle = false,
13
16
  showBreadcrumbs = true,
@@ -54,7 +57,7 @@
54
57
  {/if}
55
58
 
56
59
  {#if siteUrl}
57
- <TooltipButton tooltip={t('common.goToSite') || 'Site'} href={siteUrl} target="_blank" rel="noopener noreferrer" class="rounded-full flex items-center justify-center">
60
+ <TooltipButton tooltip={i18n.t('common.goToSite') || 'Site'} href={siteUrl} target="_blank" rel="noopener noreferrer" class="rounded-full flex items-center justify-center">
58
61
  <MonitorUp class="h-4 w-4 text-muted-foreground transition-colors hover:text-foreground" />
59
62
  </TooltipButton>
60
63
  {/if}
@@ -66,7 +69,7 @@
66
69
  {#if showSearch && onSearchClick}
67
70
  <Button variant="outline" size="sm" onclick={onSearchClick} class="gap-2 text-muted-foreground h-9 px-3 rounded-lg border-border/60">
68
71
  <Search class="h-3.5 w-3.5" />
69
- <span class="hidden sm:inline text-xs">{t('common.search')}...</span>
72
+ <span class="hidden sm:inline text-xs">{i18n.t('common.search')}...</span>
70
73
  <kbd class="hidden sm:inline-flex items-center gap-0.5 rounded border bg-muted/60 px-1.5 py-0.5 text-[10px] font-medium font-mono text-muted-foreground">
71
74
  <span class="text-xs">⌘</span>K
72
75
  </kbd>
@@ -77,7 +80,7 @@
77
80
  {#if CustomThemeToggle}
78
81
  <CustomThemeToggle />
79
82
  {:else}
80
- <TooltipButton tooltip={t('common.toggleTheme')} onclick={() => toggleTheme()} class="rounded-full">
83
+ <TooltipButton tooltip={i18n.t('common.toggleTheme')} onclick={() => toggleTheme()} class="rounded-full">
81
84
  {#if getResolvedTheme() === 'dark'}
82
85
  <Moon class="h-4 w-4 transition-all" />
83
86
  {:else}
@@ -3,11 +3,14 @@
3
3
  import { useInfiniteList } from '@svadmin/core';
4
4
  import type { BaseRecord, Sort, Filter } from '@svadmin/core';
5
5
  import type { Snippet } from 'svelte';
6
- import { t } from '@svadmin/core/i18n';
6
+ import { useTranslation } from '@svadmin/core/i18n';
7
+
7
8
  import { Skeleton } from './ui/skeleton/index.js';
8
9
  import { Loader2 } from '@lucide/svelte';
9
10
  import { intersect } from '../actions.js';
10
11
 
12
+ const i18n = useTranslation();
13
+
11
14
  interface Props<T extends BaseRecord = BaseRecord> {
12
15
  resource: string;
13
16
  pageSize?: number;
@@ -67,7 +70,7 @@
67
70
  {#if empty}
68
71
  {@render empty()}
69
72
  {:else}
70
- <p class="text-center text-sm text-muted-foreground py-8">{t('empty.title')}</p>
73
+ <p class="text-center text-sm text-muted-foreground py-8">{i18n.t('empty.title')}</p>
71
74
  {/if}
72
75
  {:else}
73
76
  {#each allItems as item, index (index)}
@@ -86,7 +89,7 @@
86
89
  <!-- Sentinel for infinite scroll -->
87
90
  <div use:intersect={loadMore} class="h-1"></div>
88
91
  {:else}
89
- <p class="text-center text-xs text-muted-foreground py-2">{t('empty.noMore')}</p>
92
+ <p class="text-center text-xs text-muted-foreground py-2">{i18n.t('empty.noMore')}</p>
90
93
  {/if}
91
94
  {/if}
92
95
  </div>
@@ -1,10 +1,13 @@
1
1
  <script lang="ts">
2
- import { t } from '@svadmin/core/i18n';
2
+ import { useTranslation } from '@svadmin/core/i18n';
3
+
3
4
  import { AlertCircle, Cloud, FolderGit, MessageCircle, Plug } from '@lucide/svelte';
4
5
  import type { Component } from 'svelte';
5
6
  import * as Card from './ui/card/index.js';
6
7
  import { Switch } from './ui/switch/index.js';
7
8
 
9
+ const i18n = useTranslation();
10
+
8
11
  interface Integration {
9
12
  id: string;
10
13
  name: string;
@@ -28,8 +31,8 @@
28
31
 
29
32
  <div class="space-y-6">
30
33
  <div>
31
- <h2 class="text-xl font-semibold text-foreground">{t('settings.integrations')}</h2>
32
- <p class="mt-1 text-sm text-muted-foreground">{t('settings.integrationsDescription')}</p>
34
+ <h2 class="text-xl font-semibold text-foreground">{i18n.t('settings.integrations')}</h2>
35
+ <p class="mt-1 text-sm text-muted-foreground">{i18n.t('settings.integrationsDescription')}</p>
33
36
  </div>
34
37
 
35
38
  <div class="grid gap-4">
@@ -45,14 +48,14 @@
45
48
  <div class="flex flex-wrap items-center gap-2">
46
49
  <span class="font-semibold text-foreground">{integration.name}</span>
47
50
  <span class="rounded-full px-2 py-0.5 text-[10px] font-semibold {integration.connected ? 'bg-green-500/10 text-green-600' : 'bg-muted text-muted-foreground'}">
48
- {integration.connected ? t('integrations.statusConnected') : t('integrations.statusDisconnected')}
51
+ {integration.connected ? i18n.t('integrations.statusConnected') : i18n.t('integrations.statusDisconnected')}
49
52
  </span>
50
53
  </div>
51
54
  <p class="text-sm text-muted-foreground">{integration.description}</p>
52
55
  </div>
53
56
  </div>
54
57
  <div class="flex items-center gap-3">
55
- <span class="text-xs text-muted-foreground">{integration.connected ? t('integrations.enabled') : t('integrations.disabled')}</span>
58
+ <span class="text-xs text-muted-foreground">{integration.connected ? i18n.t('integrations.enabled') : i18n.t('integrations.disabled')}</span>
56
59
  <Switch checked={integration.connected} onCheckedChange={(checked) => toggleConnection(integration.id, checked)} />
57
60
  </div>
58
61
  </div>
@@ -64,7 +67,7 @@
64
67
  <Card.Card class="border-dashed bg-muted/20">
65
68
  <Card.CardContent class="flex items-center gap-3 p-5 text-sm text-muted-foreground">
66
69
  <AlertCircle class="h-4 w-4 shrink-0" />
67
- <span>{t('integrations.addMoreHint')}</span>
70
+ <span>{i18n.t('integrations.addMoreHint')}</span>
68
71
  </Card.CardContent>
69
72
  </Card.Card>
70
73
  </div>
@@ -1,7 +1,9 @@
1
1
  <script lang="ts">
2
- import { t } from '@svadmin/core/i18n';
2
+ import { useTranslation } from '@svadmin/core/i18n';
3
3
  import * as Dialog from './ui/dialog/index.js';
4
4
 
5
+ const i18n = useTranslation();
6
+
5
7
  let { open = $bindable(false) }: { open?: boolean } = $props();
6
8
 
7
9
  interface Shortcut {
@@ -36,7 +38,7 @@
36
38
  <Dialog.Root bind:open>
37
39
  <Dialog.Content class="max-w-md">
38
40
  <Dialog.Header>
39
- <Dialog.Title>{t('common.keyboardShortcuts') || 'Keyboard Shortcuts'}</Dialog.Title>
41
+ <Dialog.Title>{i18n.t('common.keyboardShortcuts') || 'Keyboard Shortcuts'}</Dialog.Title>
40
42
  </Dialog.Header>
41
43
  <div class="space-y-4 py-2">
42
44
  {#each shortcuts as group, _i (_i)}
@@ -7,10 +7,10 @@
7
7
  import CommandPalette from './CommandPalette.svelte';
8
8
  import KeyboardShortcuts from './KeyboardShortcuts.svelte';
9
9
  import ChatDialog from './ChatDialog.svelte';
10
- import { t } from '@svadmin/core/i18n';
11
- import { getAuthProvider, getTaskProvider } from '@svadmin/core';
10
+ import { useTranslation } from '@svadmin/core/i18n';
11
+
12
+ import { captureAdminContext, getAuthProvider, getTaskProvider } from '@svadmin/core';
12
13
  import type { Identity, MenuItem, TaskProvider, TaskRecord } from '@svadmin/core';
13
- import { navigate } from '@svadmin/core/router';
14
14
  import { getPath } from '../router-state.svelte.js';
15
15
  import { Skeleton } from './ui/skeleton/index.js';
16
16
  import * as Sheet from './ui/sheet/index.js';
@@ -18,24 +18,23 @@
18
18
  import { getComponentRegistry } from '../component-registry.svelte.js';
19
19
  import { Button } from './ui/button/index.js';
20
20
 
21
+ const i18n = useTranslation();
22
+
21
23
  let commandOpen = $state(false);
22
24
  let shortcutsOpen = $state(false);
23
25
  let mobileMenuOpen = $state(false);
24
26
 
25
27
  let { children, title = 'Admin', menu, siteUrl, routeMode = 'auto' }: { children: Snippet; title?: string; menu?: MenuItem[]; siteUrl?: string; routeMode?: 'hash' | 'path' | 'auto' } = $props();
28
+ const adminContext = captureAdminContext();
26
29
 
27
- let _hasAuth = true;
28
30
  let auth: ReturnType<typeof getAuthProvider> | null = null;
29
31
  try {
30
32
  auth = getAuthProvider();
31
- } catch {
32
- _hasAuth = false;
33
- }
33
+ } catch { /* Auth is optional for public layouts. */ }
34
34
  let loading = $state(true);
35
35
  let identity = $state<Identity | null>(null);
36
36
  const taskProvider = getTaskProvider({ optional: true }) as TaskProvider<TaskRecord> | undefined;
37
- const registry = getComponentRegistry() || {} as any;
38
- const TaskQueueComponent = registry.TaskQueueDrawer;
37
+ const TaskQueueComponent = getComponentRegistry()?.TaskQueueDrawer;
39
38
 
40
39
  $effect(() => {
41
40
  let cancelled = false;
@@ -59,10 +58,10 @@
59
58
  try {
60
59
  const result = await auth.logout();
61
60
  if (result.success) {
62
- navigate(result.redirectTo ?? '/login');
61
+ await adminContext.navigate(result.redirectTo ?? '/login');
63
62
  }
64
63
  } catch {
65
- navigate('/login');
64
+ await adminContext.navigate('/login');
66
65
  }
67
66
  }
68
67
 
@@ -156,11 +155,11 @@
156
155
  variant="outline"
157
156
  size="sm"
158
157
  class="md:hidden gap-1.5 px-2.5"
159
- aria-label={t('common.menu')}
158
+ aria-label={i18n.t('common.menu')}
160
159
  onclick={() => { mobileMenuOpen = true; }}
161
160
  >
162
161
  <Menu class="h-5 w-5" />
163
- <span class="text-xs">{t('common.menu')}</span>
162
+ <span class="text-xs">{i18n.t('common.menu')}</span>
164
163
  </Button>
165
164
  {/snippet}
166
165
  {#snippet rightActions()}
@@ -0,0 +1,47 @@
1
+ <script module lang="ts">
2
+ import type { Component } from 'svelte';
3
+
4
+ export type LazyPageModule = {
5
+ default: Component<never>;
6
+ };
7
+
8
+ export type LazyPageRenderable = Component<Record<string, unknown>>;
9
+ export type LazyPageLoader = () => Promise<LazyPageModule>;
10
+
11
+ const pagePromises = new WeakMap<object, Promise<unknown>>();
12
+
13
+ export function loadLazyPage(loader: LazyPageLoader): Promise<LazyPageModule> {
14
+ const cached = pagePromises.get(loader);
15
+ if (cached) return cached as Promise<LazyPageModule>;
16
+
17
+ const promise = loader();
18
+ pagePromises.set(loader, promise);
19
+ void promise.catch(() => pagePromises.delete(loader));
20
+ return promise;
21
+ }
22
+ </script>
23
+
24
+ <script lang="ts">
25
+ let {
26
+ loader,
27
+ props,
28
+ }: {
29
+ loader: LazyPageLoader;
30
+ props: Record<string, unknown>;
31
+ } = $props();
32
+
33
+ const pagePromise = $derived(loadLazyPage(loader));
34
+ </script>
35
+
36
+ {#await pagePromise}
37
+ <div class="flex min-h-40 items-center justify-center" role="status" aria-live="polite">
38
+ <span class="text-sm text-muted-foreground">Loading...</span>
39
+ </div>
40
+ {:then pageModule}
41
+ {@const Page = pageModule.default as unknown as LazyPageRenderable}
42
+ <Page {...props} />
43
+ {:catch}
44
+ <div class="flex min-h-40 items-center justify-center" role="alert">
45
+ <span class="text-sm text-destructive">Unable to load this page.</span>
46
+ </div>
47
+ {/await}
@@ -0,0 +1,14 @@
1
+ import type { Component } from 'svelte';
2
+ export type LazyPageModule = {
3
+ default: Component<never>;
4
+ };
5
+ export type LazyPageRenderable = Component<Record<string, unknown>>;
6
+ export type LazyPageLoader = () => Promise<LazyPageModule>;
7
+ export declare function loadLazyPage(loader: LazyPageLoader): Promise<LazyPageModule>;
8
+ type $$ComponentProps = {
9
+ loader: LazyPageLoader;
10
+ props: Record<string, unknown>;
11
+ };
12
+ declare const LazyPage: Component<$$ComponentProps, {}, "">;
13
+ type LazyPage = ReturnType<typeof LazyPage>;
14
+ export default LazyPage;
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
- import { getResource } from '@svadmin/core';
3
- import { navigate } from '@svadmin/core/router';
4
- import { t } from '@svadmin/core/i18n';
2
+ import { captureAdminContext, getResource } from '@svadmin/core';
3
+ import { useTranslation } from '@svadmin/core/i18n';
4
+
5
5
  import type { Snippet } from 'svelte';
6
6
  import type { FieldDefinition } from '@svadmin/core';
7
7
  import PageHeader from './PageHeader.svelte';
@@ -9,6 +9,8 @@
9
9
  import { Button } from './ui/button/index.js';
10
10
  import { Plus } from '@lucide/svelte';
11
11
 
12
+ const i18n = useTranslation();
13
+
12
14
  interface Props {
13
15
  resourceName: string;
14
16
  title?: string;
@@ -40,6 +42,7 @@
40
42
  expandedRowRender,
41
43
  class: className = '',
42
44
  }: Props = $props();
45
+ const adminContext = captureAdminContext();
43
46
 
44
47
  const resource = $derived(getResource(resourceName));
45
48
  const pageTitle = $derived(title ?? resource.label);
@@ -50,8 +53,8 @@
50
53
  <PageHeader title={pageTitle}>
51
54
  {#snippet actions()}
52
55
  {#if showCreate}
53
- <Button onclick={() => navigate(`/${resourceName}/create`)}>
54
- <Plus class="h-4 w-4" data-icon="inline-start" /> {t('common.create')}
56
+ <Button onclick={() => adminContext.navigate(`/${resourceName}/create`)}>
57
+ <Plus class="h-4 w-4" data-icon="inline-start" /> {i18n.t('common.create')}
55
58
  </Button>
56
59
  {/if}
57
60
  {#if headerActions}