@svadmin/ui 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/app.css +4 -48
- package/src/components/AdminApp.svelte +20 -26
- package/src/components/AutoForm.svelte +5 -5
- package/src/components/AutoTable.svelte +12 -12
- package/src/components/Layout.svelte +2 -2
- package/src/components/ShowPage.svelte +3 -3
- package/src/components/Sidebar.svelte +43 -7
- package/src/router-state.svelte.ts +37 -0
package/package.json
CHANGED
package/src/app.css
CHANGED
|
@@ -1,40 +1,7 @@
|
|
|
1
|
-
@
|
|
2
|
-
@import "
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@theme inline {
|
|
7
|
-
--color-background: var(--background);
|
|
8
|
-
--color-foreground: var(--foreground);
|
|
9
|
-
--color-card: var(--card);
|
|
10
|
-
--color-card-foreground: var(--card-foreground);
|
|
11
|
-
--color-popover: var(--popover);
|
|
12
|
-
--color-popover-foreground: var(--popover-foreground);
|
|
13
|
-
--color-primary: var(--primary);
|
|
14
|
-
--color-primary-foreground: var(--primary-foreground);
|
|
15
|
-
--color-secondary: var(--secondary);
|
|
16
|
-
--color-secondary-foreground: var(--secondary-foreground);
|
|
17
|
-
--color-muted: var(--muted);
|
|
18
|
-
--color-muted-foreground: var(--muted-foreground);
|
|
19
|
-
--color-accent: var(--accent);
|
|
20
|
-
--color-accent-foreground: var(--accent-foreground);
|
|
21
|
-
--color-destructive: var(--destructive);
|
|
22
|
-
--color-border: var(--border);
|
|
23
|
-
--color-input: var(--input);
|
|
24
|
-
--color-ring: var(--ring);
|
|
25
|
-
--color-sidebar: var(--sidebar);
|
|
26
|
-
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
27
|
-
--color-sidebar-primary: var(--sidebar-primary);
|
|
28
|
-
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
29
|
-
--color-sidebar-accent: var(--sidebar-accent);
|
|
30
|
-
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
31
|
-
--color-sidebar-border: var(--sidebar-border);
|
|
32
|
-
--color-sidebar-ring: var(--sidebar-ring);
|
|
33
|
-
--radius-sm: calc(var(--radius) - 4px);
|
|
34
|
-
--radius-md: calc(var(--radius) - 2px);
|
|
35
|
-
--radius-lg: var(--radius);
|
|
36
|
-
--radius-xl: calc(var(--radius) + 4px);
|
|
37
|
-
}
|
|
1
|
+
/* @svadmin/ui — Design tokens (CSS custom properties)
|
|
2
|
+
* Import this in your app's CSS AFTER @import "tailwindcss"
|
|
3
|
+
* or import it standalone for the color variables.
|
|
4
|
+
*/
|
|
38
5
|
|
|
39
6
|
:root {
|
|
40
7
|
--radius: 0.625rem;
|
|
@@ -95,17 +62,6 @@
|
|
|
95
62
|
--sidebar-ring: oklch(0.556 0 0);
|
|
96
63
|
}
|
|
97
64
|
|
|
98
|
-
@layer base {
|
|
99
|
-
* {
|
|
100
|
-
@apply border-border;
|
|
101
|
-
}
|
|
102
|
-
body {
|
|
103
|
-
@apply bg-background text-foreground m-0;
|
|
104
|
-
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
105
|
-
-webkit-font-smoothing: antialiased;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
65
|
/* Scrollbar styling */
|
|
110
66
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
111
67
|
::-webkit-scrollbar-track { background: transparent; }
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode } from '@svadmin/core';
|
|
4
4
|
import { setDataProvider, setAuthProvider, setResources, setLocale, setTheme } from '@svadmin/core';
|
|
5
|
-
import {
|
|
5
|
+
import { navigate } from '@svadmin/core/router';
|
|
6
6
|
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
|
|
7
7
|
import Layout from './Layout.svelte';
|
|
8
8
|
import AutoTable from './AutoTable.svelte';
|
|
9
9
|
import AutoForm from './AutoForm.svelte';
|
|
10
10
|
import ShowPage from './ShowPage.svelte';
|
|
11
11
|
import Toast from './Toast.svelte';
|
|
12
|
+
import { initRouter, getRoute, getParams } from '../router-state.svelte.js';
|
|
12
13
|
|
|
13
14
|
interface Props {
|
|
14
15
|
dataProvider: DataProvider;
|
|
@@ -45,27 +46,12 @@
|
|
|
45
46
|
},
|
|
46
47
|
});
|
|
47
48
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
'/login',
|
|
51
|
-
'/',
|
|
52
|
-
'/:resource',
|
|
53
|
-
'/:resource/create',
|
|
54
|
-
'/:resource/edit/:id',
|
|
55
|
-
'/:resource/show/:id',
|
|
56
|
-
];
|
|
49
|
+
// Initialize hash router
|
|
50
|
+
initRouter();
|
|
57
51
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
$
|
|
61
|
-
const handler = () => { hash = window.location.hash; };
|
|
62
|
-
window.addEventListener('hashchange', handler);
|
|
63
|
-
return () => window.removeEventListener('hashchange', handler);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
const match = $derived(matchRoute(hash, routes));
|
|
67
|
-
const route = $derived(match?.route ?? '/');
|
|
68
|
-
const params = $derived(match?.params ?? {});
|
|
52
|
+
// Reactive getters for route state
|
|
53
|
+
const route = $derived(getRoute());
|
|
54
|
+
const params = $derived(getParams());
|
|
69
55
|
|
|
70
56
|
// Auth check
|
|
71
57
|
let isAuthenticated = $state(!authProvider);
|
|
@@ -104,18 +90,26 @@
|
|
|
104
90
|
{@render dashboard()}
|
|
105
91
|
{:else}
|
|
106
92
|
<div class="space-y-4">
|
|
107
|
-
<h1 class="text-2xl font-bold text-gray-900">Welcome to {title}</h1>
|
|
93
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Welcome to {title}</h1>
|
|
108
94
|
<p class="text-gray-500">Select a resource from the sidebar to get started.</p>
|
|
109
95
|
</div>
|
|
110
96
|
{/if}
|
|
111
97
|
{:else if route === '/:resource'}
|
|
112
|
-
|
|
98
|
+
{#key params.resource}
|
|
99
|
+
<AutoTable resourceName={params.resource} />
|
|
100
|
+
{/key}
|
|
113
101
|
{:else if route === '/:resource/create'}
|
|
114
|
-
|
|
102
|
+
{#key params.resource}
|
|
103
|
+
<AutoForm resourceName={params.resource} mode="create" />
|
|
104
|
+
{/key}
|
|
115
105
|
{:else if route === '/:resource/edit/:id'}
|
|
116
|
-
|
|
106
|
+
{#key `${params.resource}-${params.id}`}
|
|
107
|
+
<AutoForm resourceName={params.resource} mode="edit" id={params.id} />
|
|
108
|
+
{/key}
|
|
117
109
|
{:else if route === '/:resource/show/:id'}
|
|
118
|
-
|
|
110
|
+
{#key `${params.resource}-${params.id}`}
|
|
111
|
+
<ShowPage resourceName={params.resource} id={params.id} />
|
|
112
|
+
{/key}
|
|
119
113
|
{/if}
|
|
120
114
|
</Layout>
|
|
121
115
|
{:else}
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
}
|
|
62
62
|
formData = defaults;
|
|
63
63
|
initialized = true;
|
|
64
|
-
} else if (existingQuery &&
|
|
65
|
-
formData = {
|
|
64
|
+
} else if (existingQuery && existingQuery?.data) {
|
|
65
|
+
formData = { ...existingQuery.data as Record<string, unknown> };
|
|
66
66
|
initialized = true;
|
|
67
67
|
}
|
|
68
68
|
});
|
|
@@ -98,9 +98,9 @@
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
if (mode === 'create') {
|
|
101
|
-
await
|
|
101
|
+
await createMut.mutateAsync(cleanData);
|
|
102
102
|
} else if (id != null) {
|
|
103
|
-
await
|
|
103
|
+
await updateMut.mutateAsync({ id, variables: cleanData });
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
isDirty = false;
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
isDirty = true;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
const isLoading = $derived(mode === 'edit' && existingQuery ?
|
|
120
|
+
const isLoading = $derived(mode === 'edit' && existingQuery ? existingQuery?.isLoading : false);
|
|
121
121
|
|
|
122
122
|
const pageTitle = $derived(
|
|
123
123
|
mode === 'create'
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
selectedIds = new Set();
|
|
103
103
|
selectAll = false;
|
|
104
104
|
} else {
|
|
105
|
-
const allIds = (
|
|
105
|
+
const allIds = (query.data?.data ?? []).map(r => r[primaryKey] as string | number);
|
|
106
106
|
selectedIds = new Set(allIds);
|
|
107
107
|
selectAll = true;
|
|
108
108
|
}
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
function confirmDelete(id: string | number) {
|
|
112
112
|
confirmMessage = t('common.deleteConfirm');
|
|
113
113
|
confirmAction = async () => {
|
|
114
|
-
await
|
|
114
|
+
await deleteMutation.mutateAsync(id);
|
|
115
115
|
confirmOpen = false;
|
|
116
116
|
};
|
|
117
117
|
confirmOpen = true;
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
confirmMessage = t('common.batchDeleteConfirm', { count: selectedIds.size });
|
|
122
122
|
confirmAction = async () => {
|
|
123
123
|
for (const id of selectedIds) {
|
|
124
|
-
await
|
|
124
|
+
await deleteMutation.mutateAsync(id);
|
|
125
125
|
}
|
|
126
126
|
selectedIds = new Set();
|
|
127
127
|
selectAll = false;
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
|
|
133
133
|
// CSV Export
|
|
134
134
|
function exportCSV() {
|
|
135
|
-
const data =
|
|
135
|
+
const data = query.data?.data ?? [];
|
|
136
136
|
if (data.length === 0) return;
|
|
137
137
|
|
|
138
138
|
const headers = listFields.map(f => f.label);
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
const canDelete = canAccess(resourceName, 'delete').can && resource.canDelete !== false;
|
|
174
174
|
const canExport = canAccess(resourceName, 'export').can;
|
|
175
175
|
|
|
176
|
-
const totalPages = $derived(Math.ceil((
|
|
176
|
+
const totalPages = $derived(Math.ceil((query.data?.total ?? 0) / (pagination.pageSize ?? 10)));
|
|
177
177
|
</script>
|
|
178
178
|
|
|
179
179
|
<div class="space-y-4">
|
|
@@ -217,14 +217,14 @@
|
|
|
217
217
|
{/if}
|
|
218
218
|
|
|
219
219
|
<!-- Table -->
|
|
220
|
-
<div class="rounded-xl border border-border bg-card shadow-
|
|
221
|
-
{#if
|
|
220
|
+
<div class="rounded-xl border border-border/60 bg-card shadow-md overflow-hidden">
|
|
221
|
+
{#if query.isLoading}
|
|
222
222
|
<div class="flex h-64 items-center justify-center">
|
|
223
223
|
<Loader2 class="h-6 w-6 animate-spin text-primary" />
|
|
224
224
|
</div>
|
|
225
|
-
{:else if
|
|
225
|
+
{:else if query.error}
|
|
226
226
|
<div class="flex h-64 items-center justify-center text-destructive text-sm">
|
|
227
|
-
{t('common.loadFailed', { message: (
|
|
227
|
+
{t('common.loadFailed', { message: (query.error as Error).message })}
|
|
228
228
|
</div>
|
|
229
229
|
{:else}
|
|
230
230
|
<Table.Root>
|
|
@@ -247,9 +247,9 @@
|
|
|
247
247
|
</Table.Row>
|
|
248
248
|
</Table.Header>
|
|
249
249
|
<Table.Body>
|
|
250
|
-
{#each
|
|
250
|
+
{#each query.data?.data ?? [] as record}
|
|
251
251
|
{@const id = record[primaryKey] as string | number}
|
|
252
|
-
<Table.Row class={selectedIds.has(id) ? 'bg-accent' : ''}>
|
|
252
|
+
<Table.Row class="transition-colors {selectedIds.has(id) ? 'bg-accent' : ''}">
|
|
253
253
|
{#if canDelete}
|
|
254
254
|
<Table.Cell>
|
|
255
255
|
<Checkbox checked={selectedIds.has(id)} onCheckedChange={() => toggleSelect(id)} />
|
|
@@ -313,7 +313,7 @@
|
|
|
313
313
|
|
|
314
314
|
<!-- Pagination -->
|
|
315
315
|
<div class="flex items-center justify-between text-sm text-muted-foreground">
|
|
316
|
-
<span>{t('common.total', { total:
|
|
316
|
+
<span>{t('common.total', { total: query.data?.total ?? 0 })}</span>
|
|
317
317
|
<div class="flex items-center gap-2">
|
|
318
318
|
<Button
|
|
319
319
|
variant="outline" size="icon-sm"
|
|
@@ -51,10 +51,10 @@
|
|
|
51
51
|
<Loader2 class="h-8 w-8 animate-spin text-primary" />
|
|
52
52
|
</div>
|
|
53
53
|
{:else}
|
|
54
|
-
<div class="flex h-screen">
|
|
54
|
+
<div class="flex h-screen bg-gradient-to-br from-background via-background to-muted/30">
|
|
55
55
|
<Sidebar {collapsed} {identity} {title} onToggle={() => collapsed = !collapsed} onLogout={handleLogout} />
|
|
56
56
|
<main
|
|
57
|
-
class="flex-1 overflow-y-auto p-
|
|
57
|
+
class="flex-1 overflow-y-auto p-8 transition-all duration-300"
|
|
58
58
|
class:ml-64={!collapsed}
|
|
59
59
|
class:ml-16={collapsed}
|
|
60
60
|
>
|
|
@@ -43,15 +43,15 @@
|
|
|
43
43
|
{/if}
|
|
44
44
|
</div>
|
|
45
45
|
|
|
46
|
-
{#if
|
|
46
|
+
{#if query.isLoading}
|
|
47
47
|
<div class="flex h-64 items-center justify-center">
|
|
48
48
|
<Loader2 class="h-6 w-6 animate-spin text-primary" />
|
|
49
49
|
</div>
|
|
50
|
-
{:else if
|
|
50
|
+
{:else if query.data}
|
|
51
51
|
<Card.Root>
|
|
52
52
|
<Card.Content class="divide-y divide-border p-0">
|
|
53
53
|
{#each showFields as field}
|
|
54
|
-
{@const value = (
|
|
54
|
+
{@const value = (query.data as Record<string, unknown>)[field.key]}
|
|
55
55
|
<div class="flex px-6 py-4">
|
|
56
56
|
<div class="w-1/3 text-sm font-medium text-muted-foreground">{field.label}</div>
|
|
57
57
|
<div class="w-2/3 text-sm text-foreground">
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
import { getResources } from '@svadmin/core';
|
|
3
3
|
import type { Identity } from '@svadmin/core';
|
|
4
4
|
import { currentPath, navigate } from '@svadmin/core/router';
|
|
5
|
-
import { t } from '@svadmin/core/i18n';
|
|
6
|
-
import { toggleTheme, getResolvedTheme } from '@svadmin/core';
|
|
5
|
+
import { t, getLocale, setLocale, getAvailableLocales } from '@svadmin/core/i18n';
|
|
6
|
+
import { toggleTheme, getResolvedTheme, colorThemes, getColorTheme, setColorTheme } from '@svadmin/core';
|
|
7
7
|
import { Button } from './ui/button/index.js';
|
|
8
8
|
import * as Tooltip from './ui/tooltip/index.js';
|
|
9
9
|
import { Separator } from './ui/separator/index.js';
|
|
10
10
|
import {
|
|
11
11
|
LayoutDashboard, FileText, Users, Settings, Home,
|
|
12
|
-
ChevronLeft, ChevronRight, LogOut, Sun, Moon
|
|
12
|
+
ChevronLeft, ChevronRight, LogOut, Sun, Moon, Languages
|
|
13
13
|
} from 'lucide-svelte';
|
|
14
14
|
|
|
15
15
|
let { collapsed, identity, title, onToggle, onLogout } = $props<{
|
|
@@ -36,14 +36,27 @@
|
|
|
36
36
|
Icon: typeof LayoutDashboard;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
// Use $derived so navItems rebuild when locale changes
|
|
40
|
+
const navItems: NavItem[] = $derived([
|
|
40
41
|
{ path: '/', label: t('common.home'), Icon: LayoutDashboard },
|
|
41
42
|
...resources.map(r => ({
|
|
42
43
|
path: `/${r.name}`,
|
|
43
44
|
label: r.label,
|
|
44
45
|
Icon: iconMap[r.name] ?? Settings,
|
|
45
46
|
})),
|
|
46
|
-
];
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
/** Toggle between available locales */
|
|
50
|
+
function toggleLocale() {
|
|
51
|
+
const locales = getAvailableLocales();
|
|
52
|
+
const current = getLocale();
|
|
53
|
+
const idx = locales.indexOf(current);
|
|
54
|
+
const next = locales[(idx + 1) % locales.length];
|
|
55
|
+
setLocale(next);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Short display label for current locale */
|
|
59
|
+
const localeLabel = $derived(getLocale() === 'zh-CN' ? '中' : 'EN');
|
|
47
60
|
|
|
48
61
|
// Track current hash for active state
|
|
49
62
|
let path = $state(currentPath());
|
|
@@ -60,7 +73,7 @@
|
|
|
60
73
|
</script>
|
|
61
74
|
|
|
62
75
|
<aside
|
|
63
|
-
class="fixed inset-y-0 left-0 z-30 flex flex-col bg-sidebar border-r border-sidebar-border transition-all duration-300"
|
|
76
|
+
class="fixed inset-y-0 left-0 z-30 flex flex-col bg-sidebar/80 backdrop-blur-xl border-r border-sidebar-border/50 shadow-xl transition-all duration-300"
|
|
64
77
|
class:w-64={!collapsed}
|
|
65
78
|
class:w-16={collapsed}
|
|
66
79
|
>
|
|
@@ -123,7 +136,21 @@
|
|
|
123
136
|
|
|
124
137
|
<!-- Footer -->
|
|
125
138
|
<Separator class="bg-sidebar-border" />
|
|
126
|
-
<div class="p-3 space-y-
|
|
139
|
+
<div class="p-3 space-y-2">
|
|
140
|
+
<!-- Color theme picker -->
|
|
141
|
+
{#if !collapsed}
|
|
142
|
+
<div class="flex items-center justify-center gap-1.5 px-2 py-1">
|
|
143
|
+
{#each colorThemes as ct}
|
|
144
|
+
<button
|
|
145
|
+
class="h-5 w-5 rounded-full transition-all duration-200 hover:scale-110 {getColorTheme() === ct.id ? 'ring-2 ring-offset-2 ring-offset-sidebar scale-110' : 'opacity-70 hover:opacity-100'}"
|
|
146
|
+
style="background-color: {ct.color}; {getColorTheme() === ct.id ? `--tw-ring-color: ${ct.color}` : ''}"
|
|
147
|
+
title={ct.label}
|
|
148
|
+
onclick={() => setColorTheme(ct.id)}
|
|
149
|
+
></button>
|
|
150
|
+
{/each}
|
|
151
|
+
</div>
|
|
152
|
+
{/if}
|
|
153
|
+
|
|
127
154
|
{#if !collapsed && identity}
|
|
128
155
|
<div class="flex items-center gap-3 rounded-lg px-2 py-2">
|
|
129
156
|
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-sidebar-accent text-sm font-medium text-sidebar-accent-foreground">
|
|
@@ -132,6 +159,9 @@
|
|
|
132
159
|
<div class="flex-1 min-w-0">
|
|
133
160
|
<p class="truncate text-sm font-medium text-sidebar-foreground">{identity.name}</p>
|
|
134
161
|
</div>
|
|
162
|
+
<Button variant="ghost" size="icon-sm" onclick={toggleLocale} class="text-sidebar-foreground" title="Switch language">
|
|
163
|
+
<span class="text-xs font-bold">{localeLabel}</span>
|
|
164
|
+
</Button>
|
|
135
165
|
<Button variant="ghost" size="icon-sm" onclick={toggleTheme} class="text-sidebar-foreground" title={t('common.toggleTheme')}>
|
|
136
166
|
{#if getResolvedTheme() === 'dark'}
|
|
137
167
|
<Sun class="h-4 w-4" />
|
|
@@ -144,6 +174,9 @@
|
|
|
144
174
|
</Button>
|
|
145
175
|
</div>
|
|
146
176
|
{:else if collapsed}
|
|
177
|
+
<Button variant="ghost" size="icon" onclick={toggleLocale} class="w-full text-sidebar-foreground" title="Switch language">
|
|
178
|
+
<span class="text-xs font-bold">{localeLabel}</span>
|
|
179
|
+
</Button>
|
|
147
180
|
<Button variant="ghost" size="icon" onclick={toggleTheme} class="w-full text-sidebar-foreground" title={t('common.toggleTheme')}>
|
|
148
181
|
{#if getResolvedTheme() === 'dark'}
|
|
149
182
|
<Sun class="h-5 w-5" />
|
|
@@ -156,6 +189,9 @@
|
|
|
156
189
|
</Button>
|
|
157
190
|
{:else}
|
|
158
191
|
<div class="flex gap-1">
|
|
192
|
+
<Button variant="ghost" size="icon" onclick={toggleLocale} class="flex-1 text-sidebar-foreground" title="Switch language">
|
|
193
|
+
<span class="text-xs font-bold">{localeLabel}</span>
|
|
194
|
+
</Button>
|
|
159
195
|
<Button variant="ghost" size="icon" onclick={toggleTheme} class="flex-1 text-sidebar-foreground" title={t('common.toggleTheme')}>
|
|
160
196
|
{#if getResolvedTheme() === 'dark'}
|
|
161
197
|
<Sun class="h-5 w-5" />
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Shared reactive hash router state — .svelte.ts enables $state at module level
|
|
2
|
+
import { matchRoute } from '@svadmin/core/router';
|
|
3
|
+
|
|
4
|
+
let _route = $state('/');
|
|
5
|
+
let _params: Record<string, string> = $state({});
|
|
6
|
+
let _initialized = false;
|
|
7
|
+
|
|
8
|
+
const ROUTES = [
|
|
9
|
+
'/login',
|
|
10
|
+
'/',
|
|
11
|
+
'/:resource',
|
|
12
|
+
'/:resource/create',
|
|
13
|
+
'/:resource/edit/:id',
|
|
14
|
+
'/:resource/show/:id',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function sync() {
|
|
18
|
+
const hash = window.location.hash;
|
|
19
|
+
const m = matchRoute(hash, ROUTES);
|
|
20
|
+
_route = m?.route ?? '/';
|
|
21
|
+
_params = m?.params ?? {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function initRouter() {
|
|
25
|
+
if (_initialized) return;
|
|
26
|
+
_initialized = true;
|
|
27
|
+
sync();
|
|
28
|
+
window.addEventListener('hashchange', sync);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getRoute(): string {
|
|
32
|
+
return _route;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getParams(): Record<string, string> {
|
|
36
|
+
return _params;
|
|
37
|
+
}
|