@svadmin/ui 0.0.2 → 0.0.6

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 (29) hide show
  1. package/package.json +2 -2
  2. package/src/components/AdminApp.svelte +35 -15
  3. package/src/components/Authenticated.svelte +26 -0
  4. package/src/components/AutoForm.svelte +57 -5
  5. package/src/components/CanAccess.svelte +21 -0
  6. package/src/components/ConfigErrorScreen.svelte +224 -0
  7. package/src/components/DevTools.svelte +280 -0
  8. package/src/components/DrawerForm.svelte +26 -0
  9. package/src/components/ForgotPasswordPage.svelte +203 -0
  10. package/src/components/InferencerPanel.svelte +166 -0
  11. package/src/components/LoginPage.svelte +236 -0
  12. package/src/components/ModalForm.svelte +34 -0
  13. package/src/components/RegisterPage.svelte +241 -0
  14. package/src/components/StatsCard.svelte +23 -2
  15. package/src/components/UndoableNotification.svelte +132 -0
  16. package/src/components/UpdatePasswordPage.svelte +217 -0
  17. package/src/components/buttons/CloneButton.svelte +30 -0
  18. package/src/components/buttons/CreateButton.svelte +29 -0
  19. package/src/components/buttons/DeleteButton.svelte +60 -0
  20. package/src/components/buttons/EditButton.svelte +30 -0
  21. package/src/components/buttons/ExportButton.svelte +24 -0
  22. package/src/components/buttons/ImportButton.svelte +28 -0
  23. package/src/components/buttons/ListButton.svelte +23 -0
  24. package/src/components/buttons/RefreshButton.svelte +30 -0
  25. package/src/components/buttons/SaveButton.svelte +27 -0
  26. package/src/components/buttons/ShowButton.svelte +24 -0
  27. package/src/components/buttons/index.ts +10 -0
  28. package/src/index.ts +18 -0
  29. package/src/router-state.svelte.ts +26 -5
@@ -0,0 +1,217 @@
1
+ <script lang="ts">
2
+ import { useUpdatePassword } from '@svadmin/core';
3
+ import { t } from '@svadmin/core/i18n';
4
+ import { navigate } from '@svadmin/core/router';
5
+ import { Button } from './ui/button/index.js';
6
+ import { Input } from './ui/input/index.js';
7
+ import * as Card from './ui/card/index.js';
8
+ import { Lock, Eye, EyeOff, ShieldCheck } from 'lucide-svelte';
9
+
10
+ let { title = 'Admin' } = $props<{
11
+ title?: string;
12
+ }>();
13
+
14
+ const updatePw = useUpdatePassword();
15
+
16
+ let password = $state('');
17
+ let confirmPassword = $state('');
18
+ let showPassword = $state(false);
19
+ let error = $state('');
20
+
21
+ async function handleSubmit(e: SubmitEvent) {
22
+ e.preventDefault();
23
+ error = '';
24
+
25
+ if (!password) { error = t('auth.passwordRequired'); return; }
26
+ if (password !== confirmPassword) { error = t('auth.passwordMismatch'); return; }
27
+
28
+ const result = await updatePw.mutate({ password, confirmPassword });
29
+ if (!result.success) {
30
+ error = result.error?.message ?? t('common.operationFailed');
31
+ }
32
+ }
33
+ </script>
34
+
35
+ <div class="auth-page">
36
+ <div class="auth-container">
37
+ <Card.Card class="auth-card">
38
+ <Card.CardHeader class="auth-header">
39
+ <div class="auth-icon">
40
+ <ShieldCheck class="h-6 w-6" />
41
+ </div>
42
+ <Card.CardTitle class="text-2xl font-bold">{t('auth.resetPassword')}</Card.CardTitle>
43
+ <p class="text-sm text-muted-foreground">Enter your new password below.</p>
44
+ </Card.CardHeader>
45
+ <Card.CardContent>
46
+ <form onsubmit={handleSubmit} class="space-y-4">
47
+ {#if error}
48
+ <div class="error-alert">
49
+ <p>{error}</p>
50
+ </div>
51
+ {/if}
52
+
53
+ <div class="space-y-2">
54
+ <label for="new-password" class="text-sm font-medium text-foreground">{t('auth.password')}</label>
55
+ <div class="input-with-icon">
56
+ <Lock class="input-icon h-4 w-4" />
57
+ <Input
58
+ id="new-password"
59
+ type={showPassword ? 'text' : 'password'}
60
+ placeholder="••••••••"
61
+ bind:value={password}
62
+ class="pl-9 pr-9"
63
+ autocomplete="new-password"
64
+ />
65
+ <button
66
+ type="button"
67
+ class="password-toggle"
68
+ onclick={() => showPassword = !showPassword}
69
+ tabindex={-1}
70
+ >
71
+ {#if showPassword}
72
+ <EyeOff class="h-4 w-4" />
73
+ {:else}
74
+ <Eye class="h-4 w-4" />
75
+ {/if}
76
+ </button>
77
+ </div>
78
+ </div>
79
+
80
+ <div class="space-y-2">
81
+ <label for="confirm-password" class="text-sm font-medium text-foreground">{t('auth.confirmPassword')}</label>
82
+ <div class="input-with-icon">
83
+ <Lock class="input-icon h-4 w-4" />
84
+ <Input
85
+ id="confirm-password"
86
+ type={showPassword ? 'text' : 'password'}
87
+ placeholder="••••••••"
88
+ bind:value={confirmPassword}
89
+ class="pl-9"
90
+ autocomplete="new-password"
91
+ />
92
+ </div>
93
+ </div>
94
+
95
+ <Button type="submit" class="w-full" disabled={updatePw.isLoading}>
96
+ {#if updatePw.isLoading}
97
+ <span class="loading-spinner"></span>
98
+ {/if}
99
+ {t('auth.resetPassword')}
100
+ </Button>
101
+
102
+ <div class="auth-link">
103
+ <button
104
+ type="button"
105
+ class="text-sm text-primary hover:underline font-medium"
106
+ onclick={() => navigate('/login')}
107
+ >{t('auth.backToLogin')}</button>
108
+ </div>
109
+ </form>
110
+ </Card.CardContent>
111
+ </Card.Card>
112
+
113
+ <p class="text-xs text-muted-foreground mt-4 text-center opacity-60">
114
+ Powered by {title}
115
+ </p>
116
+ </div>
117
+ </div>
118
+
119
+ <style>
120
+ .auth-page {
121
+ min-height: 100vh;
122
+ display: flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ background: linear-gradient(135deg, hsl(var(--primary) / 0.08) 0%, hsl(var(--background)) 50%, hsl(var(--primary) / 0.04) 100%);
126
+ padding: 1rem;
127
+ }
128
+
129
+ .auth-container {
130
+ width: 100%;
131
+ max-width: 420px;
132
+ }
133
+
134
+ :global(.auth-card) {
135
+ backdrop-filter: blur(20px);
136
+ border: 1px solid hsl(var(--border) / 0.5);
137
+ box-shadow: 0 8px 32px hsl(var(--primary) / 0.08), 0 2px 8px hsl(0 0% 0% / 0.06);
138
+ }
139
+
140
+ :global(.auth-header) {
141
+ text-align: center;
142
+ padding-bottom: 0.5rem;
143
+ }
144
+
145
+ .auth-icon {
146
+ display: inline-flex;
147
+ align-items: center;
148
+ justify-content: center;
149
+ width: 48px;
150
+ height: 48px;
151
+ border-radius: 12px;
152
+ background: hsl(var(--primary) / 0.1);
153
+ color: hsl(var(--primary));
154
+ margin: 0 auto 0.75rem;
155
+ }
156
+
157
+ .input-with-icon {
158
+ position: relative;
159
+ }
160
+
161
+ :global(.input-icon) {
162
+ position: absolute;
163
+ left: 0.75rem;
164
+ top: 50%;
165
+ transform: translateY(-50%);
166
+ color: hsl(var(--muted-foreground));
167
+ pointer-events: none;
168
+ z-index: 1;
169
+ }
170
+
171
+ .password-toggle {
172
+ position: absolute;
173
+ right: 0.75rem;
174
+ top: 50%;
175
+ transform: translateY(-50%);
176
+ color: hsl(var(--muted-foreground));
177
+ background: none;
178
+ border: none;
179
+ cursor: pointer;
180
+ padding: 2px;
181
+ z-index: 1;
182
+ }
183
+ .password-toggle:hover {
184
+ color: hsl(var(--foreground));
185
+ }
186
+
187
+ .error-alert {
188
+ padding: 0.75rem;
189
+ border-radius: 0.5rem;
190
+ background: hsl(var(--destructive) / 0.1);
191
+ border: 1px solid hsl(var(--destructive) / 0.3);
192
+ color: hsl(var(--destructive));
193
+ font-size: 0.875rem;
194
+ }
195
+
196
+ .loading-spinner {
197
+ display: inline-block;
198
+ width: 16px;
199
+ height: 16px;
200
+ border: 2px solid transparent;
201
+ border-top-color: currentColor;
202
+ border-radius: 50%;
203
+ animation: spin 0.6s linear infinite;
204
+ margin-right: 0.5rem;
205
+ }
206
+
207
+ @keyframes spin {
208
+ to { transform: rotate(360deg); }
209
+ }
210
+
211
+ .auth-link {
212
+ display: flex;
213
+ align-items: center;
214
+ justify-content: center;
215
+ margin-top: 0.5rem;
216
+ }
217
+ </style>
@@ -0,0 +1,30 @@
1
+ <script lang="ts">
2
+ import { useNavigation, useCan, t } from '@svadmin/core';
3
+ import { Button } from '../ui/button/index.js';
4
+ import { Copy } from 'lucide-svelte';
5
+
6
+ let { resource, recordItemId, hideText = false, accessControl, class: className = '' } = $props<{
7
+ resource: string;
8
+ recordItemId: string | number;
9
+ hideText?: boolean;
10
+ accessControl?: { enabled?: boolean; hideIfUnauthorized?: boolean };
11
+ class?: string;
12
+ }>();
13
+
14
+ const nav = useNavigation();
15
+ const can = accessControl?.enabled ? useCan({ resource, action: 'create' }) : null;
16
+ const hidden = $derived(accessControl?.hideIfUnauthorized && can && !can.allowed);
17
+ </script>
18
+
19
+ {#if !hidden}
20
+ <Button
21
+ variant="outline"
22
+ size={hideText ? 'icon' : 'sm'}
23
+ class={className}
24
+ disabled={can ? !can.allowed : false}
25
+ onclick={() => nav.clone(resource, recordItemId)}
26
+ >
27
+ <Copy class="h-4 w-4" />
28
+ {#if !hideText}<span class="ml-1">{t('common.clone')}</span>{/if}
29
+ </Button>
30
+ {/if}
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ import { useNavigation, useCan, t } from '@svadmin/core';
3
+ import { Button } from '../ui/button/index.js';
4
+ import { Plus } from 'lucide-svelte';
5
+
6
+ let { resource, hideText = false, accessControl, class: className = '' } = $props<{
7
+ resource: string;
8
+ hideText?: boolean;
9
+ accessControl?: { enabled?: boolean; hideIfUnauthorized?: boolean };
10
+ class?: string;
11
+ }>();
12
+
13
+ const nav = useNavigation();
14
+ const can = accessControl?.enabled ? useCan({ resource, action: 'create' }) : null;
15
+ const hidden = $derived(accessControl?.hideIfUnauthorized && can && !can.allowed);
16
+ </script>
17
+
18
+ {#if !hidden}
19
+ <Button
20
+ variant="default"
21
+ size={hideText ? 'icon' : 'default'}
22
+ class={className}
23
+ disabled={can ? !can.allowed : false}
24
+ onclick={() => nav.create(resource)}
25
+ >
26
+ <Plus class="h-4 w-4" />
27
+ {#if !hideText}<span class="ml-1">{t('common.create')}</span>{/if}
28
+ </Button>
29
+ {/if}
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ import { useDelete, useCan, t } from '@svadmin/core';
3
+ import { Button } from '../ui/button/index.js';
4
+ import { Trash2 } from 'lucide-svelte';
5
+
6
+ let {
7
+ resource, recordItemId, hideText = false,
8
+ accessControl, onSuccess, undoable = false, class: className = '',
9
+ } = $props<{
10
+ resource: string;
11
+ recordItemId: string | number;
12
+ hideText?: boolean;
13
+ accessControl?: { enabled?: boolean; hideIfUnauthorized?: boolean };
14
+ onSuccess?: () => void;
15
+ undoable?: boolean;
16
+ class?: string;
17
+ }>();
18
+
19
+ const deleteMut = useDelete(resource, { mutationMode: undoable ? 'undoable' : 'pessimistic' });
20
+ const can = accessControl?.enabled ? useCan({ resource, action: 'delete' }) : null;
21
+ const hidden = $derived(accessControl?.hideIfUnauthorized && can && !can.allowed);
22
+ let confirming = $state(false);
23
+
24
+ async function handleDelete() {
25
+ if (!confirming) {
26
+ confirming = true;
27
+ return;
28
+ }
29
+ confirming = false;
30
+ // @ts-expect-error $ rune prefix
31
+ await $deleteMut.mutateAsync(recordItemId);
32
+ onSuccess?.();
33
+ }
34
+
35
+ function cancel() { confirming = false; }
36
+ </script>
37
+
38
+ {#if !hidden}
39
+ {#if confirming}
40
+ <div class="inline-flex items-center gap-1">
41
+ <Button variant="destructive" size="sm" onclick={handleDelete}>
42
+ {t('common.confirm')}
43
+ </Button>
44
+ <Button variant="ghost" size="sm" onclick={cancel}>
45
+ {t('common.cancel')}
46
+ </Button>
47
+ </div>
48
+ {:else}
49
+ <Button
50
+ variant="ghost"
51
+ size={hideText ? 'icon' : 'sm'}
52
+ class="text-destructive hover:text-destructive {className}"
53
+ disabled={can ? !can.allowed : false}
54
+ onclick={handleDelete}
55
+ >
56
+ <Trash2 class="h-4 w-4" />
57
+ {#if !hideText}<span class="ml-1">{t('common.delete')}</span>{/if}
58
+ </Button>
59
+ {/if}
60
+ {/if}
@@ -0,0 +1,30 @@
1
+ <script lang="ts">
2
+ import { useNavigation, useCan, t } from '@svadmin/core';
3
+ import { Button } from '../ui/button/index.js';
4
+ import { Pencil } from 'lucide-svelte';
5
+
6
+ let { resource, recordItemId, hideText = false, accessControl, class: className = '' } = $props<{
7
+ resource: string;
8
+ recordItemId: string | number;
9
+ hideText?: boolean;
10
+ accessControl?: { enabled?: boolean; hideIfUnauthorized?: boolean };
11
+ class?: string;
12
+ }>();
13
+
14
+ const nav = useNavigation();
15
+ const can = accessControl?.enabled ? useCan({ resource, action: 'edit' }) : null;
16
+ const hidden = $derived(accessControl?.hideIfUnauthorized && can && !can.allowed);
17
+ </script>
18
+
19
+ {#if !hidden}
20
+ <Button
21
+ variant="outline"
22
+ size={hideText ? 'icon' : 'sm'}
23
+ class={className}
24
+ disabled={can ? !can.allowed : false}
25
+ onclick={() => nav.edit(resource, recordItemId)}
26
+ >
27
+ <Pencil class="h-4 w-4" />
28
+ {#if !hideText}<span class="ml-1">{t('common.edit')}</span>{/if}
29
+ </Button>
30
+ {/if}
@@ -0,0 +1,24 @@
1
+ <script lang="ts">
2
+ import { useExport, t } from '@svadmin/core';
3
+ import { Button } from '../ui/button/index.js';
4
+ import { Download } from 'lucide-svelte';
5
+
6
+ let { resource, hideText = false, class: className = '' } = $props<{
7
+ resource: string;
8
+ hideText?: boolean;
9
+ class?: string;
10
+ }>();
11
+
12
+ const { triggerExport, isLoading } = useExport({ resource });
13
+ </script>
14
+
15
+ <Button
16
+ variant="outline"
17
+ size={hideText ? 'icon' : 'sm'}
18
+ class={className}
19
+ disabled={isLoading}
20
+ onclick={triggerExport}
21
+ >
22
+ <Download class="h-4 w-4" />
23
+ {#if !hideText}<span class="ml-1">{t('common.export')}</span>{/if}
24
+ </Button>
@@ -0,0 +1,28 @@
1
+ <script lang="ts">
2
+ import { useImport, t } from '@svadmin/core';
3
+ import { Button } from '../ui/button/index.js';
4
+ import { Upload } from 'lucide-svelte';
5
+
6
+ let { resource, hideText = false, onComplete, class: className = '' } = $props<{
7
+ resource: string;
8
+ hideText?: boolean;
9
+ onComplete?: (result: { success: number; failed: number }) => void;
10
+ class?: string;
11
+ }>();
12
+
13
+ const { triggerImport, isLoading } = useImport({
14
+ resource,
15
+ onComplete: (result) => onComplete?.(result),
16
+ });
17
+ </script>
18
+
19
+ <Button
20
+ variant="outline"
21
+ size={hideText ? 'icon' : 'sm'}
22
+ class={className}
23
+ disabled={isLoading}
24
+ onclick={triggerImport}
25
+ >
26
+ <Upload class="h-4 w-4" />
27
+ {#if !hideText}<span class="ml-1">{t('common.import')}</span>{/if}
28
+ </Button>
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ import { useNavigation, t } from '@svadmin/core';
3
+ import { Button } from '../ui/button/index.js';
4
+ import { List } from 'lucide-svelte';
5
+
6
+ let { resource, hideText = false, class: className = '' } = $props<{
7
+ resource: string;
8
+ hideText?: boolean;
9
+ class?: string;
10
+ }>();
11
+
12
+ const nav = useNavigation();
13
+ </script>
14
+
15
+ <Button
16
+ variant="outline"
17
+ size={hideText ? 'icon' : 'sm'}
18
+ class={className}
19
+ onclick={() => nav.list(resource)}
20
+ >
21
+ <List class="h-4 w-4" />
22
+ {#if !hideText}<span class="ml-1">{resource}</span>{/if}
23
+ </Button>
@@ -0,0 +1,30 @@
1
+ <script lang="ts">
2
+ import { useQueryClient } from '@tanstack/svelte-query';
3
+ import { Button } from '../ui/button/index.js';
4
+ import { RefreshCw } from 'lucide-svelte';
5
+
6
+ let { resource, hideText = false, class: className = '' } = $props<{
7
+ resource: string;
8
+ hideText?: boolean;
9
+ class?: string;
10
+ }>();
11
+
12
+ const queryClient = useQueryClient();
13
+ let spinning = $state(false);
14
+
15
+ function refresh() {
16
+ spinning = true;
17
+ queryClient.invalidateQueries({ queryKey: [resource] });
18
+ setTimeout(() => { spinning = false; }, 600);
19
+ }
20
+ </script>
21
+
22
+ <Button
23
+ variant="ghost"
24
+ size={hideText ? 'icon' : 'sm'}
25
+ class={className}
26
+ onclick={refresh}
27
+ >
28
+ <RefreshCw class="h-4 w-4 {spinning ? 'animate-spin' : ''}" />
29
+ {#if !hideText}<span class="ml-1">Refresh</span>{/if}
30
+ </Button>
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import { t } from '@svadmin/core';
3
+ import { Button } from '../ui/button/index.js';
4
+ import { Save, Loader2 } from 'lucide-svelte';
5
+
6
+ let { loading = false, hideText = false, type = 'submit', class: className = '' } = $props<{
7
+ loading?: boolean;
8
+ hideText?: boolean;
9
+ type?: 'submit' | 'button';
10
+ class?: string;
11
+ }>();
12
+ </script>
13
+
14
+ <Button
15
+ {type}
16
+ variant="default"
17
+ size={hideText ? 'icon' : 'default'}
18
+ class={className}
19
+ disabled={loading}
20
+ >
21
+ {#if loading}
22
+ <Loader2 class="h-4 w-4 animate-spin" />
23
+ {:else}
24
+ <Save class="h-4 w-4" />
25
+ {/if}
26
+ {#if !hideText}<span class="ml-1">{t('common.save')}</span>{/if}
27
+ </Button>
@@ -0,0 +1,24 @@
1
+ <script lang="ts">
2
+ import { useNavigation, t } from '@svadmin/core';
3
+ import { Button } from '../ui/button/index.js';
4
+ import { Eye } from 'lucide-svelte';
5
+
6
+ let { resource, recordItemId, hideText = false, class: className = '' } = $props<{
7
+ resource: string;
8
+ recordItemId: string | number;
9
+ hideText?: boolean;
10
+ class?: string;
11
+ }>();
12
+
13
+ const nav = useNavigation();
14
+ </script>
15
+
16
+ <Button
17
+ variant="ghost"
18
+ size={hideText ? 'icon' : 'sm'}
19
+ class={className}
20
+ onclick={() => nav.show(resource, recordItemId)}
21
+ >
22
+ <Eye class="h-4 w-4" />
23
+ {#if !hideText}<span class="ml-1">{t('common.detail')}</span>{/if}
24
+ </Button>
@@ -0,0 +1,10 @@
1
+ export { default as CreateButton } from './CreateButton.svelte';
2
+ export { default as EditButton } from './EditButton.svelte';
3
+ export { default as DeleteButton } from './DeleteButton.svelte';
4
+ export { default as ShowButton } from './ShowButton.svelte';
5
+ export { default as ListButton } from './ListButton.svelte';
6
+ export { default as RefreshButton } from './RefreshButton.svelte';
7
+ export { default as ExportButton } from './ExportButton.svelte';
8
+ export { default as ImportButton } from './ImportButton.svelte';
9
+ export { default as SaveButton } from './SaveButton.svelte';
10
+ export { default as CloneButton } from './CloneButton.svelte';
package/src/index.ts CHANGED
@@ -18,6 +18,24 @@ export { default as FieldRenderer } from './components/FieldRenderer.svelte';
18
18
  export { default as EmptyState } from './components/EmptyState.svelte';
19
19
  export { default as StatsCard } from './components/StatsCard.svelte';
20
20
  export { default as PageHeader } from './components/PageHeader.svelte';
21
+ export { default as LoginPage } from './components/LoginPage.svelte';
22
+ export { default as RegisterPage } from './components/RegisterPage.svelte';
23
+ export { default as ForgotPasswordPage } from './components/ForgotPasswordPage.svelte';
24
+ export { default as CanAccess } from './components/CanAccess.svelte';
25
+ export { default as UndoableNotification } from './components/UndoableNotification.svelte';
26
+ export { default as ModalForm } from './components/ModalForm.svelte';
27
+ export { default as DrawerForm } from './components/DrawerForm.svelte';
28
+ export { default as DevTools } from './components/DevTools.svelte';
29
+ export { default as Authenticated } from './components/Authenticated.svelte';
30
+ export { default as UpdatePasswordPage } from './components/UpdatePasswordPage.svelte';
31
+ export { default as ConfigErrorScreen } from './components/ConfigErrorScreen.svelte';
32
+ export { default as InferencerPanel } from './components/InferencerPanel.svelte';
33
+
34
+ // CRUD Buttons
35
+ export {
36
+ CreateButton, EditButton, DeleteButton, ShowButton, ListButton,
37
+ RefreshButton, ExportButton, ImportButton, SaveButton, CloneButton,
38
+ } from './components/buttons/index.js';
21
39
 
22
40
  // Base UI components (shadcn-svelte)
23
41
  export { Button, buttonVariants } from './components/ui/button/index.js';
@@ -1,12 +1,17 @@
1
1
  // Shared reactive hash router state — .svelte.ts enables $state at module level
2
2
  import { matchRoute } from '@svadmin/core/router';
3
+ import type { RouterProvider } from '@svadmin/core';
3
4
 
4
5
  let _route = $state('/');
5
6
  let _params: Record<string, string> = $state({});
6
7
  let _initialized = false;
8
+ let _provider: RouterProvider | undefined;
7
9
 
8
10
  const ROUTES = [
9
11
  '/login',
12
+ '/register',
13
+ '/forgot-password',
14
+ '/update-password',
10
15
  '/',
11
16
  '/:resource',
12
17
  '/:resource/create',
@@ -15,17 +20,29 @@ const ROUTES = [
15
20
  ];
16
21
 
17
22
  function sync() {
18
- const hash = window.location.hash;
19
- const m = matchRoute(hash, ROUTES);
20
- _route = m?.route ?? '/';
21
- _params = m?.params ?? {};
23
+ if (_provider) {
24
+ // Use RouterProvider for parsing
25
+ const parsed = _provider.parse();
26
+ const path = parsed.pathname || '/';
27
+ const m = matchRoute(path.startsWith('#') ? path : `#${path}`, ROUTES);
28
+ _route = m?.route ?? '/';
29
+ _params = { ...(m?.params ?? {}), ...parsed.params };
30
+ } else {
31
+ // Fallback: direct hash parsing
32
+ const hash = window.location.hash;
33
+ const m = matchRoute(hash, ROUTES);
34
+ _route = m?.route ?? '/';
35
+ _params = m?.params ?? {};
36
+ }
22
37
  }
23
38
 
24
- export function initRouter() {
39
+ export function initRouter(provider?: RouterProvider) {
25
40
  if (_initialized) return;
26
41
  _initialized = true;
42
+ _provider = provider;
27
43
  sync();
28
44
  window.addEventListener('hashchange', sync);
45
+ window.addEventListener('popstate', sync);
29
46
  }
30
47
 
31
48
  export function getRoute(): string {
@@ -35,3 +52,7 @@ export function getRoute(): string {
35
52
  export function getParams(): Record<string, string> {
36
53
  return _params;
37
54
  }
55
+
56
+ export function getRouterProviderInstance(): RouterProvider | undefined {
57
+ return _provider;
58
+ }