@svadmin/ui 0.38.1 → 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 +10 -5
  2. package/dist/app.theme.css +1374 -0
  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 +18 -12
@@ -1,14 +1,15 @@
1
1
  <script lang="ts">
2
- import { getResources, getChatProvider } from '@svadmin/core';
3
- import { navigate } from '@svadmin/core/router';
4
- import { t } from '@svadmin/core/i18n';
5
- import { toggleTheme } from '@svadmin/core';
6
- import { Command } from 'cmdk-sv';
2
+ import { captureAdminContext, getResources, getChatProvider, toggleTheme } from '@svadmin/core';
3
+ import { useTranslation } from '@svadmin/core/i18n';
4
+ import { Command } from './ui/command/index.js';
7
5
  import * as Dialog from './ui/dialog/index.js';
8
6
  import { Search, LayoutDashboard, Plus, Sun, FileText, Sparkles, Loader2 } from '@lucide/svelte';
9
7
  import MarkdownRenderer from './MarkdownRenderer.svelte';
10
8
 
9
+ const i18n = useTranslation();
10
+
11
11
  let { open = $bindable(false) } = $props<{ open?: boolean }>();
12
+ const adminContext = captureAdminContext();
12
13
  let searchValue = $state('');
13
14
 
14
15
  const resources = getResources();
@@ -80,14 +81,14 @@
80
81
  }
81
82
  }
82
83
 
83
- const itemClass = 'relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50';
84
+ const itemClass = 'relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[selected]:bg-accent data-[selected]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50';
84
85
  </script>
85
86
 
86
87
  <Dialog.Dialog bind:open onOpenChange={(v: boolean) => !v && close()}>
87
88
  <Dialog.DialogContent class="overflow-hidden p-0 sm:max-w-[600px] border-none shadow-2xl">
88
89
  <Command.Root
89
90
  shouldFilter={!aiMode}
90
- class="flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground [&_[data-cmdk-group-heading]]:text-muted-foreground [&_[data-cmdk-group-heading]]:px-2 [&_[data-cmdk-group-heading]]:py-1.5 [&_[data-cmdk-group-heading]]:text-xs [&_[data-cmdk-group-heading]]:font-semibold [&_[data-cmdk-group-heading]]:uppercase [&_[data-cmdk-group-heading]]:tracking-wider"
91
+ class="flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground [&_[data-command-group-heading]]:text-muted-foreground [&_[data-command-group-heading]]:px-2 [&_[data-command-group-heading]]:py-1.5 [&_[data-command-group-heading]]:text-xs [&_[data-command-group-heading]]:font-semibold [&_[data-command-group-heading]]:uppercase [&_[data-command-group-heading]]:tracking-wider"
91
92
  >
92
93
  <div class="flex items-center border-b px-3 h-12 relative" data-cmdk-input-wrapper="">
93
94
  {#if aiMode}
@@ -119,13 +120,13 @@
119
120
  </Command.Empty>
120
121
 
121
122
  <!-- Navigation -->
122
- <Command.Group heading={t('common.home')}>
123
- <Command.Item value="home" onSelect={() => act(() => navigate('/'))} class={itemClass}>
123
+ <Command.Group heading={i18n.t('common.home')}>
124
+ <Command.Item value="home" onSelect={() => act(() => adminContext.navigate('/'))} class={itemClass}>
124
125
  <LayoutDashboard class="h-4 w-4 text-muted-foreground" />
125
- {t('common.home')}
126
+ {i18n.t('common.home')}
126
127
  </Command.Item>
127
128
  {#each resources as r, _i (_i)}
128
- <Command.Item value={r.name} onSelect={() => act(() => navigate(`/${r.name}`))} class={itemClass}>
129
+ <Command.Item value={r.name} onSelect={() => act(() => adminContext.navigate(`/${r.name}`))} class={itemClass}>
129
130
  <FileText class="h-4 w-4 text-muted-foreground" />
130
131
  {r.label}
131
132
  </Command.Item>
@@ -135,16 +136,16 @@
135
136
  <Command.Separator class="mx-1 my-1 h-px bg-border" />
136
137
 
137
138
  <!-- Actions -->
138
- <Command.Group heading={t('common.actions')}>
139
+ <Command.Group heading={i18n.t('common.actions')}>
139
140
  {#each resources as r, _i (_i)}
140
- <Command.Item value={`create-${r.name}`} onSelect={() => act(() => navigate(`/${r.name}/create`))} class={itemClass}>
141
+ <Command.Item value={`create-${r.name}`} onSelect={() => act(() => adminContext.navigate(`/${r.name}/create`))} class={itemClass}>
141
142
  <Plus class="h-4 w-4 text-muted-foreground" />
142
- {t('common.create')} {r.label}
143
+ {i18n.t('common.create')} {r.label}
143
144
  </Command.Item>
144
145
  {/each}
145
146
  <Command.Item value="toggle-theme" onSelect={() => act(() => toggleTheme())} class={itemClass}>
146
147
  <Sun class="h-4 w-4 text-muted-foreground" />
147
- {t('common.toggleTheme')}
148
+ {i18n.t('common.toggleTheme')}
148
149
  </Command.Item>
149
150
  </Command.Group>
150
151
  </Command.List>
@@ -1,12 +1,13 @@
1
1
  <script lang="ts">
2
- import { t } from '@svadmin/core/i18n';
2
+ import { useTranslation } from '@svadmin/core/i18n';
3
3
  import { getResources, getDataProviderNames } from '@svadmin/core';
4
- import { getLocale } from '@svadmin/core/i18n';
5
4
  import { getResolvedTheme, getColorTheme } from '@svadmin/core';
6
5
  import * as Card from './ui/card/index.js';
7
6
  import { Badge } from './ui/badge/index.js';
8
7
  import { Info, Database, Globe, Palette, Layers } from '@lucide/svelte';
9
8
 
9
+ const i18n = useTranslation();
10
+
10
11
  const resources = getResources();
11
12
  const providerNames = getDataProviderNames();
12
13
 
@@ -15,7 +16,7 @@
15
16
 
16
17
  <div class="space-y-6">
17
18
  <div>
18
- <h2 class="text-xl font-semibold text-foreground">{t('settings.about')}</h2>
19
+ <h2 class="text-xl font-semibold text-foreground">{i18n.t('settings.about')}</h2>
19
20
  </div>
20
21
 
21
22
  <!-- Version -->
@@ -29,7 +30,7 @@
29
30
  <Card.CardContent>
30
31
  <div class="grid gap-4 text-sm">
31
32
  <div class="flex items-center justify-between">
32
- <span class="text-muted-foreground">{t('settings.version')}</span>
33
+ <span class="text-muted-foreground">{i18n.t('settings.version')}</span>
33
34
  <Badge variant="secondary" class="font-mono text-xs">{version}</Badge>
34
35
  </div>
35
36
  </div>
@@ -41,7 +42,7 @@
41
42
  <Card.CardHeader class="pb-3">
42
43
  <Card.CardTitle class="text-base flex items-center gap-2">
43
44
  <Layers class="h-4 w-4 text-muted-foreground" />
44
- {t('settings.registeredResources')}
45
+ {i18n.t('settings.registeredResources')}
45
46
  </Card.CardTitle>
46
47
  </Card.CardHeader>
47
48
  <Card.CardContent>
@@ -61,7 +62,7 @@
61
62
  <Card.CardHeader class="pb-3">
62
63
  <Card.CardTitle class="text-base flex items-center gap-2">
63
64
  <Database class="h-4 w-4 text-muted-foreground" />
64
- {t('settings.dataProvider')}
65
+ {i18n.t('settings.dataProvider')}
65
66
  </Card.CardTitle>
66
67
  </Card.CardHeader>
67
68
  <Card.CardContent>
@@ -78,25 +79,25 @@
78
79
  <Card.CardHeader class="pb-3">
79
80
  <Card.CardTitle class="text-base flex items-center gap-2">
80
81
  <Globe class="h-4 w-4 text-muted-foreground" />
81
- {t('settings.currentEnvironment')}
82
+ {i18n.t('settings.currentEnvironment')}
82
83
  </Card.CardTitle>
83
84
  </Card.CardHeader>
84
85
  <Card.CardContent>
85
86
  <div class="grid gap-3 text-sm">
86
87
  <div class="flex items-center justify-between">
87
- <span class="text-muted-foreground">{t('settings.language')}</span>
88
- <span class="font-mono text-xs">{getLocale()}</span>
88
+ <span class="text-muted-foreground">{i18n.t('settings.language')}</span>
89
+ <span class="font-mono text-xs">{i18n.locale}</span>
89
90
  </div>
90
91
  <div class="h-px bg-border"></div>
91
92
  <div class="flex items-center justify-between">
92
- <span class="text-muted-foreground">{t('settings.themeMode')}</span>
93
+ <span class="text-muted-foreground">{i18n.t('settings.themeMode')}</span>
93
94
  <span class="font-mono text-xs">{getResolvedTheme()}</span>
94
95
  </div>
95
96
  <div class="h-px bg-border"></div>
96
97
  <div class="flex items-center justify-between">
97
98
  <span class="text-muted-foreground flex items-center gap-1.5">
98
99
  <Palette class="h-3.5 w-3.5" />
99
- {t('settings.colorAccent')}
100
+ {i18n.t('settings.colorAccent')}
100
101
  </span>
101
102
  <span class="font-mono text-xs">{getColorTheme()}</span>
102
103
  </div>
@@ -1,11 +1,35 @@
1
+ <script module lang="ts">
2
+ const loadPublicProfilePage = () => import('./profile/PublicProfilePage.svelte');
3
+ const loadGetStartedPage = () => import('./account/GetStartedPage.svelte');
4
+ const loadCompanyProfilePage = () => import('./account/CompanyProfilePage.svelte');
5
+ const loadSettingsPlainPage = () => import('./account/SettingsPlainPage.svelte');
6
+ const loadSettingsSidebarPage = () => import('./account/SettingsSidebarPage.svelte');
7
+ const loadSettingsEnterprisePage = () => import('./account/SettingsEnterprisePage.svelte');
8
+ const loadImportMembersPage = () => import('./account/ImportMembersPage.svelte');
9
+ const loadMembersStarterPage = () => import('./account/MembersStarterPage.svelte');
10
+ const loadTeamMembersPage = () => import('./account/TeamMembersPage.svelte');
11
+ const loadSecurityLogPage = () => import('./account/SecurityLogPage.svelte');
12
+ const loadUserCardsNFTPage = () => import('./network/UserCardsNFTPage.svelte');
13
+ const loadTeamCrewTablePage = () => import('./network/TeamCrewTablePage.svelte');
14
+ const loadTwoFactorAuthPage = () => import('./TwoFactorAuthPage.svelte');
15
+ </script>
16
+
1
17
  <script lang="ts">
2
18
  // Auto-inject design tokens so consumers don't need to manually import
3
19
  import '../app.css';
4
- import type { Component, Snippet } from 'svelte';
5
- import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode, RouterProvider, ThemeConfig, MenuItem, TaskProvider } from '@svadmin/core';
6
- import { setDataProvider, setAuthProvider, setResources, setLocale, setTheme, setRouterProvider, setTaskProvider, createHashRouterProvider, configureTheme } from '@svadmin/core';
7
- import { t } from '@svadmin/core/i18n';
8
- import { navigate } from '@svadmin/core/router';
20
+ import { onDestroy, onMount, untrack, type Component, type Snippet } from 'svelte';
21
+ import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode, RouterProvider, ThemeConfig, MenuItem, TaskProvider, I18nProvider } from '@svadmin/core';
22
+ import {
23
+ captureAdminContext,
24
+ createHashRouterProvider,
25
+ createI18nScope,
26
+ provideAdminContext,
27
+ provideI18nScope,
28
+ registerThemeOwner,
29
+ unregisterThemeOwner,
30
+ updateThemeOwner,
31
+ } from '@svadmin/core';
32
+ import { useTranslation } from '@svadmin/core/i18n';
9
33
  import { QueryClient, QueryClientProvider, type DefaultOptions } from '@tanstack/svelte-query';
10
34
  import { setComponentRegistry, type ComponentRegistry } from '../component-registry.svelte.js';
11
35
  import Layout from './Layout.svelte';
@@ -24,24 +48,12 @@
24
48
  import TaskQueueDrawer from './TaskQueueDrawer.svelte';
25
49
  import SettingsPage from './SettingsPage.svelte';
26
50
  import ErrorPage from './ErrorPage.svelte';
27
- import PublicProfilePage from './profile/PublicProfilePage.svelte';
28
- import GetStartedPage from './account/GetStartedPage.svelte';
29
- import CompanyProfilePage from './account/CompanyProfilePage.svelte';
30
- import SettingsPlainPage from './account/SettingsPlainPage.svelte';
31
- import SettingsSidebarPage from './account/SettingsSidebarPage.svelte';
32
- import SettingsEnterprisePage from './account/SettingsEnterprisePage.svelte';
33
- import ImportMembersPage from './account/ImportMembersPage.svelte';
34
- import MembersStarterPage from './account/MembersStarterPage.svelte';
35
- import TeamMembersPage from './account/TeamMembersPage.svelte';
36
- import SecurityLogPage from './account/SecurityLogPage.svelte';
37
- import UserCardsNFTPage from './network/UserCardsNFTPage.svelte';
38
- import TeamCrewTablePage from './network/TeamCrewTablePage.svelte';
39
- import TwoFactorAuthPage from './TwoFactorAuthPage.svelte';
51
+ import LazyPage from './LazyPage.svelte';
40
52
  import { Button } from './ui/button/index.js';
41
53
  import { Input } from './ui/input/index.js';
42
54
  import { Badge } from './ui/badge/index.js';
43
55
  import { Skeleton } from './ui/skeleton/index.js';
44
- import { initRouter, getRoute, getParams } from '../router-state.svelte.js';
56
+ import { provideRouterState } from '../router-state.svelte.js';
45
57
 
46
58
  interface Props {
47
59
  dataProvider: DataProvider;
@@ -49,7 +61,10 @@
49
61
  taskProvider?: TaskProvider;
50
62
  routerProvider?: RouterProvider;
51
63
  resources: ResourceDefinition[];
64
+ /** Bindable tree-local locale; defaults to the provider locale or per-tree browser detection (English during SSR). */
52
65
  locale?: string;
66
+ /** Optional tree-local translation provider. */
67
+ i18nProvider?: I18nProvider;
53
68
  title?: string;
54
69
  defaultTheme?: ThemeMode;
55
70
  /** Theme configuration for dark-first mode, CSS overrides, etc. */
@@ -90,7 +105,8 @@
90
105
  taskProvider,
91
106
  routerProvider,
92
107
  resources,
93
- locale,
108
+ locale = $bindable(),
109
+ i18nProvider,
94
110
  title = 'Admin',
95
111
  defaultTheme,
96
112
  themeConfig: userThemeConfig,
@@ -122,28 +138,89 @@
122
138
  const resolvedRouter = $derived(routerProvider ?? createHashRouterProvider());
123
139
  const resolvedRouteMode = $derived(routeMode ?? (routerProvider ? 'auto' : 'hash'));
124
140
 
141
+ const routerState = provideRouterState(untrack(() => resolvedRouter));
142
+ const scopedRouterProvider: RouterProvider = {
143
+ go(options) {
144
+ resolvedRouter.go(options);
145
+ routerState.sync();
146
+ },
147
+ back() {
148
+ resolvedRouter.back();
149
+ routerState.sync();
150
+ },
151
+ parse() {
152
+ return resolvedRouter.parse();
153
+ },
154
+ formatLink(path) {
155
+ return resolvedRouter.formatLink?.(path) ?? `#${path.replace(/^#/, '')}`;
156
+ },
157
+ };
158
+
159
+ $effect.pre(() => routerState.init(resolvedRouter));
160
+ onDestroy(() => routerState.destroy());
161
+
162
+ function updateBoundLocale(nextLocale: string) {
163
+ locale = nextLocale;
164
+ }
165
+
166
+ const i18nScope = createI18nScope({
167
+ locale: untrack(() => locale),
168
+ provider: untrack(() => i18nProvider),
169
+ onLocaleChange: updateBoundLocale,
170
+ });
171
+ provideI18nScope(i18nScope);
172
+ const translation = useTranslation();
173
+
174
+ $effect.pre(() => {
175
+ const ownerProvider = i18nProvider;
176
+ const ownerLocale = locale;
177
+ untrack(() => i18nScope.updateOwner({
178
+ locale: ownerLocale,
179
+ provider: ownerProvider,
180
+ onLocaleChange: updateBoundLocale,
181
+ }));
182
+ });
183
+
184
+ let themeOwner = $state.raw<ReturnType<typeof registerThemeOwner>>(undefined);
185
+
186
+ onMount(() => {
187
+ themeOwner = registerThemeOwner({
188
+ defaultTheme,
189
+ themeConfig: userThemeConfig,
190
+ });
191
+
192
+ return () => {
193
+ unregisterThemeOwner(themeOwner);
194
+ themeOwner = undefined;
195
+ };
196
+ });
197
+
198
+ $effect(() => {
199
+ const owner = themeOwner;
200
+ const ownerOptions = {
201
+ defaultTheme,
202
+ themeConfig: userThemeConfig,
203
+ };
204
+ untrack(() => updateThemeOwner(owner, ownerOptions));
205
+ });
206
+
125
207
  function syncComponentRegistry() {
126
208
  setComponentRegistry(mergedComponents);
127
209
  }
128
210
 
129
- function syncAppContext() {
130
- setDataProvider(dataProvider);
131
- if (authProvider) setAuthProvider(authProvider);
132
- if (taskProvider) setTaskProvider(taskProvider);
133
- setResources(resources);
134
- setRouterProvider(resolvedRouter);
135
- if (locale) setLocale(locale);
136
- if (userThemeConfig) configureTheme(userThemeConfig);
137
- if (defaultTheme) setTheme(defaultTheme);
138
- }
211
+ provideAdminContext({
212
+ get dataProvider() { return dataProvider; },
213
+ get authProvider() { return authProvider ?? null; },
214
+ get taskProvider() { return taskProvider; },
215
+ get resources() { return resources; },
216
+ get routerProvider() { return scopedRouterProvider; },
217
+ });
218
+ const adminContext = captureAdminContext();
139
219
 
140
- // Set up initial context synchronously so children can access resources immediately during first render.
220
+ // Context must be available synchronously to descendants during their first render.
141
221
  syncComponentRegistry();
142
- syncAppContext();
143
222
 
144
- // Set up context — use $effect so prop changes are tracked
145
223
  $effect.pre(syncComponentRegistry);
146
- $effect.pre(syncAppContext);
147
224
 
148
225
  const queryClient = $derived(providedQueryClient ?? new QueryClient({
149
226
  defaultOptions: {
@@ -152,24 +229,20 @@
152
229
  },
153
230
  }));
154
231
 
155
- function syncRouter() {
156
- initRouter(resolvedRouter);
157
- }
158
-
159
- // Initialize router with provider synchronously
160
- syncRouter();
161
- $effect.pre(syncRouter);
162
-
163
232
  // Reactive getters for route state
164
- const route = $derived(getRoute());
165
- const params = $derived(getParams());
233
+ const route = $derived(routerState.route);
234
+ const params = $derived(routerState.params);
166
235
  const resourceNames = $derived(new Set(resources.map((resource) => resource.name)));
167
236
  const hasRouteResource = $derived(!params.resource || resourceNames.has(params.resource));
168
237
  const currentResourcePages = $derived(params.resource ? resourcePages?.[params.resource] : undefined);
169
238
 
170
239
  // Auth check
171
- let isAuthenticated = $state(false);
172
- let authChecked = $state(false);
240
+ let isAuthenticated = $state(untrack(() => !authProvider));
241
+ let authChecked = $state(untrack(() => !authProvider));
242
+
243
+ async function navigateWithinApp(path: string) {
244
+ await adminContext.navigate(path);
245
+ }
173
246
 
174
247
  $effect(() => {
175
248
  let cancelled = false;
@@ -183,12 +256,13 @@
183
256
  }
184
257
  return;
185
258
  }
259
+ authChecked = false;
186
260
  authProvider.check().then(result => {
187
261
  if (cancelled) return;
188
262
  isAuthenticated = result.authenticated;
189
263
  authChecked = true;
190
264
  if (!result.authenticated && _route !== '/login' && _route !== '/register' && _route !== '/forgot-password' && _route !== '/update-password') {
191
- navigate(result.redirectTo ?? '/login');
265
+ void navigateWithinApp(result.redirectTo ?? '/login');
192
266
  }
193
267
  }).catch(err => {
194
268
  if (cancelled) return;
@@ -196,7 +270,7 @@
196
270
  isAuthenticated = false;
197
271
  authChecked = true;
198
272
  if (_route !== '/login' && _route !== '/register' && _route !== '/forgot-password' && _route !== '/update-password') {
199
- navigate('/login');
273
+ void navigateWithinApp('/login');
200
274
  }
201
275
  });
202
276
 
@@ -220,7 +294,7 @@
220
294
  defaultIdentifier={loginDefaults?.identifier}
221
295
  defaultPassword={loginDefaults?.password}
222
296
  loginHint={loginDefaults?.hint}
223
- onSuccess={() => { isAuthenticated = true; navigate('/'); }}
297
+ onSuccess={() => { isAuthenticated = true; void navigateWithinApp('/'); }}
224
298
  />
225
299
  {:else if route === '/register' && authProvider?.register}
226
300
  <RegisterPage {title} />
@@ -229,49 +303,49 @@
229
303
  {:else if route === '/update-password' && authProvider?.updatePassword}
230
304
  <UpdatePasswordPage {title} />
231
305
  {:else if route === '/login' || route === '/register' || route === '/forgot-password' || route === '/update-password'}
232
- <ConfigErrorScreen title="{title} — {t('common.configRequired')}" />
306
+ <ConfigErrorScreen title="{title} — {translation.t('common.configRequired')}" />
233
307
  {:else if isAuthenticated || !authProvider}
234
308
  <Layout {title} {menu} {siteUrl} routeMode={resolvedRouteMode}>
235
309
  {#key route + (params.resource ?? '') + (params.id ?? '') + (params.variant ?? '') + (params.columns ?? '')}
236
310
  <div class="svadmin-page-enter">
237
311
  {#if route === '/2fa' || route === '/authentication/branded/2fa'}
238
- <TwoFactorAuthPage />
312
+ <LazyPage loader={loadTwoFactorAuthPage} props={{}} />
239
313
  {:else if route === '/public-profile'}
240
- <PublicProfilePage variant="default" initialTab="projects" />
314
+ <LazyPage loader={loadPublicProfilePage} props={{ variant: 'default', initialTab: 'projects' }} />
241
315
  {:else if route === '/public-profile/projects/:columns'}
242
- <PublicProfilePage variant="default" initialTab="projects" columns={params.columns?.includes('3') ? 3 : 2} />
316
+ <LazyPage loader={loadPublicProfilePage} props={{ variant: 'default', initialTab: 'projects', columns: params.columns?.includes('3') ? 3 : 2 }} />
243
317
  {:else if route === '/public-profile/activity'}
244
- <PublicProfilePage variant="default" initialTab="activity" />
318
+ <LazyPage loader={loadPublicProfilePage} props={{ variant: 'default', initialTab: 'activity' }} />
245
319
  {:else if route === '/public-profile/teams'}
246
- <PublicProfilePage variant="default" initialTab="teams" />
320
+ <LazyPage loader={loadPublicProfilePage} props={{ variant: 'default', initialTab: 'teams' }} />
247
321
  {:else if route === '/public-profile/profiles/:variant'}
248
- <PublicProfilePage variant={(params.variant === 'company' || params.variant === 'gamer' || params.variant === 'default' ? params.variant : 'default') as 'company' | 'gamer' | 'default'} />
322
+ <LazyPage loader={loadPublicProfilePage} props={{ variant: (params.variant === 'company' || params.variant === 'gamer' || params.variant === 'default' ? params.variant : 'default') as 'company' | 'gamer' | 'default' }} />
249
323
  {:else if route === '/account/get-started' || route === '/account/home/get-started'}
250
- <GetStartedPage />
324
+ <LazyPage loader={loadGetStartedPage} props={{}} />
251
325
  {:else if route === '/account/home/user-profile'}
252
326
  <SettingsPage />
253
327
  {:else if route === '/account/company-profile' || route === '/account/home/company-profile'}
254
- <CompanyProfilePage />
328
+ <LazyPage loader={loadCompanyProfilePage} props={{}} />
255
329
  {:else if route === '/account/settings-plain' || route === '/account/home/settings-plain'}
256
- <SettingsPlainPage />
330
+ <LazyPage loader={loadSettingsPlainPage} props={{}} />
257
331
  {:else if route === '/account/settings-sidebar' || route === '/account/home/settings-sidebar'}
258
- <SettingsSidebarPage />
332
+ <LazyPage loader={loadSettingsSidebarPage} props={{}} />
259
333
  {:else if route === '/account/settings-enterprise' || route === '/account/home/settings-enterprise'}
260
- <SettingsEnterprisePage />
334
+ <LazyPage loader={loadSettingsEnterprisePage} props={{}} />
261
335
  {:else if route === '/account/:tab'}
262
336
  <SettingsPage />
263
337
  {:else if route === '/account/import-members' || route === '/account/members/import-members'}
264
- <ImportMembersPage />
338
+ <LazyPage loader={loadImportMembersPage} props={{}} />
265
339
  {:else if route === '/account/members-starter' || route === '/account/members/members-starter'}
266
- <MembersStarterPage />
340
+ <LazyPage loader={loadMembersStarterPage} props={{}} />
267
341
  {:else if route === '/account/team-members' || route === '/account/members/team-members'}
268
- <TeamMembersPage />
342
+ <LazyPage loader={loadTeamMembersPage} props={{}} />
269
343
  {:else if route === '/account/security-log' || route === '/account/security/security-log'}
270
- <SecurityLogPage />
344
+ <LazyPage loader={loadSecurityLogPage} props={{}} />
271
345
  {:else if route === '/network/user-cards' || route === '/network/user-cards/nft'}
272
- <UserCardsNFTPage />
346
+ <LazyPage loader={loadUserCardsNFTPage} props={{}} />
273
347
  {:else if route === '/network/team-crew' || route === '/network/user-table/team-crew'}
274
- <TeamCrewTablePage />
348
+ <LazyPage loader={loadTeamCrewTablePage} props={{}} />
275
349
  {:else if route.startsWith('/settings')}
276
350
  <SettingsPage />
277
351
  {:else if route === '/500' || (route === '/:resource' && params.resource === '500')}
@@ -282,8 +356,8 @@
282
356
  {@render dashboard()}
283
357
  {:else}
284
358
  <div class="space-y-4">
285
- <h1 class="text-xl font-semibold text-foreground">{t('common.welcome', { title })}</h1>
286
- <p class="text-muted-foreground">{t('common.dashboardHint')}</p>
359
+ <h1 class="text-xl font-semibold text-foreground">{translation.t('common.welcome', { title })}</h1>
360
+ <p class="text-muted-foreground">{translation.t('common.dashboardHint')}</p>
287
361
  </div>
288
362
  {/if}
289
363
  {:else if (route === '/:resource' || route === '/:parent/:parentId/:resource') && hasRouteResource}
@@ -320,7 +394,7 @@
320
394
  </Layout>
321
395
  {:else}
322
396
  <div class="flex h-screen items-center justify-center">
323
- <p class="text-muted-foreground">{t('common.redirecting')}</p>
397
+ <p class="text-muted-foreground">{translation.t('common.redirecting')}</p>
324
398
  </div>
325
399
  {/if}
326
400
  <Toast />
@@ -1,6 +1,6 @@
1
1
  import '../app.css';
2
- import type { Component, Snippet } from 'svelte';
3
- import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode, RouterProvider, ThemeConfig, MenuItem, TaskProvider } from '@svadmin/core';
2
+ import { type Component, type Snippet } from 'svelte';
3
+ import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode, RouterProvider, ThemeConfig, MenuItem, TaskProvider, I18nProvider } from '@svadmin/core';
4
4
  import { QueryClient, type DefaultOptions } from '@tanstack/svelte-query';
5
5
  import { type ComponentRegistry } from '../component-registry.svelte.js';
6
6
  interface Props {
@@ -9,7 +9,10 @@ interface Props {
9
9
  taskProvider?: TaskProvider;
10
10
  routerProvider?: RouterProvider;
11
11
  resources: ResourceDefinition[];
12
+ /** Bindable tree-local locale; defaults to the provider locale or per-tree browser detection (English during SSR). */
12
13
  locale?: string;
14
+ /** Optional tree-local translation provider. */
15
+ i18nProvider?: I18nProvider;
13
16
  title?: string;
14
17
  defaultTheme?: ThemeMode;
15
18
  /** Theme configuration for dark-first mode, CSS overrides, etc. */
@@ -59,6 +62,6 @@ interface Props {
59
62
  /** Optional TanStack Query default options override when QueryClient is not supplied. */
60
63
  queryClientDefaultOptions?: DefaultOptions;
61
64
  }
62
- declare const AdminApp: Component<Props, {}, "">;
65
+ declare const AdminApp: Component<Props, {}, "locale">;
63
66
  type AdminApp = ReturnType<typeof AdminApp>;
64
67
  export default AdminApp;
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
- import { t } from '@svadmin/core/i18n';
2
+ import { useTranslation } from '@svadmin/core/i18n';
3
+
3
4
  import { AlertCircle, Calendar, Check, Copy, Key, Plus, ShieldCheck, Trash2 } from '@lucide/svelte';
4
5
  import * as Card from './ui/card/index.js';
5
6
  import { Badge } from './ui/badge/index.js';
@@ -8,6 +9,8 @@
8
9
  import { Input } from './ui/input/index.js';
9
10
  import { Label } from './ui/label/index.js';
10
11
 
12
+ const i18n = useTranslation();
13
+
11
14
  interface ApiKey {
12
15
  id: string;
13
16
  name: string;
@@ -76,8 +79,8 @@
76
79
 
77
80
  <div class="space-y-6">
78
81
  <div>
79
- <h2 class="text-xl font-semibold text-foreground">{t('settings.api')}</h2>
80
- <p class="mt-1 text-sm text-muted-foreground">{t('settings.apiDescription')}</p>
82
+ <h2 class="text-xl font-semibold text-foreground">{i18n.t('settings.api')}</h2>
83
+ <p class="mt-1 text-sm text-muted-foreground">{i18n.t('settings.apiDescription')}</p>
81
84
  </div>
82
85
 
83
86
  <div class="grid gap-6 lg:grid-cols-3">
@@ -85,19 +88,19 @@
85
88
  <Card.CardHeader>
86
89
  <Card.CardTitle class="flex items-center gap-2 text-base">
87
90
  <Plus class="h-4 w-4" />
88
- {t('api.generateTitle')}
91
+ {i18n.t('api.generateTitle')}
89
92
  </Card.CardTitle>
90
- <Card.CardDescription>{t('api.generateDesc')}</Card.CardDescription>
93
+ <Card.CardDescription>{i18n.t('api.generateDesc')}</Card.CardDescription>
91
94
  </Card.CardHeader>
92
95
  <Card.CardContent>
93
96
  <form onsubmit={handleGenerateKey} class="space-y-4">
94
97
  <div class="space-y-2">
95
- <Label for="api-key-name">{t('api.keyName')}</Label>
98
+ <Label for="api-key-name">{i18n.t('api.keyName')}</Label>
96
99
  <Input id="api-key-name" placeholder="CI deployment token" bind:value={newKeyName} required />
97
100
  </div>
98
101
 
99
102
  <div class="space-y-3">
100
- <Label>{t('api.permissions')}</Label>
103
+ <Label>{i18n.t('api.permissions')}</Label>
101
104
  {#each Object.keys(newKeyPermissions) as permission (permission)}
102
105
  <div class="flex items-center gap-2">
103
106
  <Checkbox id={`perm-${permission}`} bind:checked={newKeyPermissions[permission as keyof typeof newKeyPermissions]} />
@@ -106,7 +109,7 @@
106
109
  {/each}
107
110
  </div>
108
111
 
109
- <Button type="submit" class="w-full">{t('api.generateButton')}</Button>
112
+ <Button type="submit" class="w-full">{i18n.t('api.generateButton')}</Button>
110
113
  </form>
111
114
  </Card.CardContent>
112
115
  </Card.Card>
@@ -115,9 +118,9 @@
115
118
  <Card.CardHeader>
116
119
  <Card.CardTitle class="flex items-center gap-2 text-base">
117
120
  <Key class="h-4 w-4 text-muted-foreground" />
118
- {t('api.activeKeysTitle')}
121
+ {i18n.t('api.activeKeysTitle')}
119
122
  </Card.CardTitle>
120
- <Card.CardDescription>{t('api.activeKeysDesc')}</Card.CardDescription>
123
+ <Card.CardDescription>{i18n.t('api.activeKeysDesc')}</Card.CardDescription>
121
124
  </Card.CardHeader>
122
125
  <Card.CardContent>
123
126
  {#if showCreatedToken}
@@ -125,8 +128,8 @@
125
128
  <div class="flex items-start gap-2 text-sm text-green-700 dark:text-green-300">
126
129
  <ShieldCheck class="mt-0.5 h-5 w-5 shrink-0" />
127
130
  <div>
128
- <h4 class="font-semibold">{t('api.successTitle')}</h4>
129
- <p class="mt-0.5 text-xs">{t('api.successHint')}</p>
131
+ <h4 class="font-semibold">{i18n.t('api.successTitle')}</h4>
132
+ <p class="mt-0.5 text-xs">{i18n.t('api.successHint')}</p>
130
133
  </div>
131
134
  </div>
132
135
  <div class="flex items-center gap-2">
@@ -140,7 +143,7 @@
140
143
  </Button>
141
144
  </div>
142
145
  <div class="flex justify-end">
143
- <Button variant="ghost" size="sm" onclick={() => { showCreatedToken = false; generatedToken = ''; }}>{t('common.close')}</Button>
146
+ <Button variant="ghost" size="sm" onclick={() => { showCreatedToken = false; generatedToken = ''; }}>{i18n.t('common.close')}</Button>
144
147
  </div>
145
148
  </div>
146
149
  {/if}
@@ -160,14 +163,14 @@
160
163
  </div>
161
164
  </div>
162
165
  <div class="flex shrink-0 items-center gap-1">
163
- <Button variant="ghost" size="icon-sm" onclick={() => copyToken(key.maskedToken, key.id)} title={t('common.copy')}>
166
+ <Button variant="ghost" size="icon-sm" onclick={() => copyToken(key.maskedToken, key.id)} title={i18n.t('common.copy')}>
164
167
  {#if copiedKeyId === key.id}
165
168
  <Check class="h-4 w-4 text-green-500" />
166
169
  {:else}
167
170
  <Copy class="h-4 w-4" />
168
171
  {/if}
169
172
  </Button>
170
- <Button variant="ghost" size="icon-sm" onclick={() => deleteKey(key.id)} title={t('common.delete')}>
173
+ <Button variant="ghost" size="icon-sm" onclick={() => deleteKey(key.id)} title={i18n.t('common.delete')}>
171
174
  <Trash2 class="h-4 w-4" />
172
175
  </Button>
173
176
  </div>
@@ -181,7 +184,7 @@
181
184
  {:else}
182
185
  <div class="flex flex-col items-center gap-2 py-8 text-center text-sm text-muted-foreground">
183
186
  <AlertCircle class="h-8 w-8 opacity-60" />
184
- <span>{t('api.noKeys')}</span>
187
+ <span>{i18n.t('api.noKeys')}</span>
185
188
  </div>
186
189
  {/each}
187
190
  </div>