@svadmin/core 0.21.0 → 0.21.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/auth-hooks.svelte.ts +17 -7
- package/src/context.svelte.ts +4 -2
- package/src/data-transfer.svelte.ts +3 -3
- package/src/form-hooks.svelte.ts +67 -39
- package/src/hook-utils.svelte.ts +12 -14
- package/src/hooks.svelte.ts +41 -25
- package/src/i18n.svelte.ts +3 -1
- package/src/index.ts +1 -0
- package/src/live-sse.ts +7 -2
- package/src/live-websocket.ts +1 -4
- package/src/live.svelte.ts +2 -2
- package/src/mutation-hooks.svelte.ts +50 -12
- package/src/query-hooks.svelte.ts +104 -58
- package/src/router-provider.ts +24 -3
- package/src/router.ts +18 -5
- package/src/routing-hooks.svelte.ts +3 -2
- package/src/table-hooks.svelte.ts +8 -8
- package/src/table-state.svelte.ts +7 -6
- package/src/theme.svelte.ts +15 -13
- package/src/unsaved-changes.svelte.ts +0 -1
- package/src/url-sync.ts +44 -28
- package/src/useParsed.svelte.ts +17 -3
- package/src/utility-hooks.svelte.ts +7 -1
package/src/theme.svelte.ts
CHANGED
|
@@ -301,8 +301,8 @@ export const colorThemes: { id: ColorTheme | string; label: string; color: strin
|
|
|
301
301
|
// ── Dark/Light mode ──────────────────────────────────────
|
|
302
302
|
|
|
303
303
|
function getStoredTheme(): ThemeMode {
|
|
304
|
-
if (typeof
|
|
305
|
-
return (localStorage.getItem(STORAGE_KEY) as ThemeMode) ?? 'system';
|
|
304
|
+
if (typeof window === 'undefined') return 'system';
|
|
305
|
+
try { return (localStorage.getItem(STORAGE_KEY) as ThemeMode) ?? 'system'; } catch { return 'system'; }
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
function getSystemPreference(): 'light' | 'dark' {
|
|
@@ -335,8 +335,10 @@ function applyTheme(m: ThemeMode): void {
|
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
// Apply on init
|
|
339
|
-
|
|
338
|
+
// Apply on init (browser only)
|
|
339
|
+
if (typeof window !== 'undefined') {
|
|
340
|
+
applyTheme(getStoredTheme());
|
|
341
|
+
}
|
|
340
342
|
|
|
341
343
|
// Listen for system preference changes
|
|
342
344
|
if (typeof window !== 'undefined') {
|
|
@@ -356,8 +358,8 @@ export function getTheme(): ThemeMode {
|
|
|
356
358
|
|
|
357
359
|
export function setTheme(newMode: ThemeMode): void {
|
|
358
360
|
mode = newMode;
|
|
359
|
-
if (typeof
|
|
360
|
-
localStorage.setItem(STORAGE_KEY, newMode);
|
|
361
|
+
if (typeof window !== 'undefined') {
|
|
362
|
+
try { localStorage.setItem(STORAGE_KEY, newMode); } catch {}
|
|
361
363
|
}
|
|
362
364
|
applyTheme(newMode);
|
|
363
365
|
// Re-apply color preset — light/dark CSS variable overrides differ
|
|
@@ -378,8 +380,8 @@ export function getResolvedTheme(): 'light' | 'dark' {
|
|
|
378
380
|
// ── Color theme ──────────────────────────────────────────
|
|
379
381
|
|
|
380
382
|
function getStoredColorTheme(): ColorTheme {
|
|
381
|
-
if (typeof
|
|
382
|
-
return (localStorage.getItem(COLOR_STORAGE_KEY) as ColorTheme) ?? 'blue';
|
|
383
|
+
if (typeof window === 'undefined') return 'blue';
|
|
384
|
+
try { return (localStorage.getItem(COLOR_STORAGE_KEY) as ColorTheme) ?? 'blue'; } catch { return 'blue'; }
|
|
383
385
|
}
|
|
384
386
|
|
|
385
387
|
let colorTheme = $state<ColorTheme>(getStoredColorTheme());
|
|
@@ -390,9 +392,9 @@ function applyColorTheme(ct: ColorTheme): void {
|
|
|
390
392
|
}
|
|
391
393
|
}
|
|
392
394
|
|
|
393
|
-
// Apply on init
|
|
394
|
-
|
|
395
|
-
|
|
395
|
+
// Apply on init (browser only)
|
|
396
|
+
if (typeof window !== 'undefined') {
|
|
397
|
+
applyColorTheme(getStoredColorTheme());
|
|
396
398
|
const initPreset = builtinPresets[getStoredColorTheme()];
|
|
397
399
|
if (initPreset) applyColorPreset(initPreset);
|
|
398
400
|
}
|
|
@@ -403,8 +405,8 @@ export function getColorTheme(): ColorTheme {
|
|
|
403
405
|
|
|
404
406
|
export function setColorTheme(ct: ColorTheme): void {
|
|
405
407
|
colorTheme = ct;
|
|
406
|
-
if (typeof
|
|
407
|
-
localStorage.setItem(COLOR_STORAGE_KEY, ct);
|
|
408
|
+
if (typeof window !== 'undefined') {
|
|
409
|
+
try { localStorage.setItem(COLOR_STORAGE_KEY, ct); } catch {}
|
|
408
410
|
}
|
|
409
411
|
applyColorTheme(ct);
|
|
410
412
|
// Apply the preset's CSS variable overrides for the current resolved theme
|
|
@@ -98,7 +98,6 @@ export function initUnsavedChangesNotifier(options?: {
|
|
|
98
98
|
const onHashChange = (e: HashChangeEvent) => {
|
|
99
99
|
if (!dirty) return;
|
|
100
100
|
if (!confirm(t('common.unsavedChanges'))) {
|
|
101
|
-
e.preventDefault();
|
|
102
101
|
if (e.oldURL) {
|
|
103
102
|
history.replaceState(null, '', new URL(e.oldURL).hash || '#');
|
|
104
103
|
}
|
package/src/url-sync.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
// URL state sync — sync pagination/sort/filters with hash params
|
|
2
|
-
|
|
3
1
|
import type { Filter } from './types';
|
|
2
|
+
import { getRouterProvider } from './context.svelte';
|
|
4
3
|
|
|
5
4
|
export interface URLState {
|
|
6
5
|
page?: number;
|
|
@@ -13,29 +12,28 @@ export interface URLState {
|
|
|
13
12
|
|
|
14
13
|
export function readURLState(): URLState {
|
|
15
14
|
if (typeof window === 'undefined') return {};
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
if (queryIdx === -1) return {};
|
|
15
|
+
const rp = getRouterProvider();
|
|
16
|
+
if (!rp) return {};
|
|
19
17
|
|
|
20
|
-
const params =
|
|
18
|
+
const { params } = rp.parse();
|
|
21
19
|
const state: URLState = {};
|
|
22
20
|
|
|
23
|
-
const page = params
|
|
21
|
+
const page = params['page'];
|
|
24
22
|
if (page) state.page = parseInt(page, 10);
|
|
25
23
|
|
|
26
|
-
const pageSize = params
|
|
24
|
+
const pageSize = params['pageSize'];
|
|
27
25
|
if (pageSize) state.pageSize = parseInt(pageSize, 10);
|
|
28
26
|
|
|
29
|
-
const sortField = params
|
|
27
|
+
const sortField = params['sort'];
|
|
30
28
|
if (sortField) state.sortField = sortField;
|
|
31
29
|
|
|
32
|
-
const sortOrder = params
|
|
30
|
+
const sortOrder = params['order'];
|
|
33
31
|
if (sortOrder === 'asc' || sortOrder === 'desc') state.sortOrder = sortOrder;
|
|
34
32
|
|
|
35
|
-
const search = params
|
|
33
|
+
const search = params['q'];
|
|
36
34
|
if (search) state.search = search;
|
|
37
35
|
|
|
38
|
-
const filtersRaw = params
|
|
36
|
+
const filtersRaw = params['filters'];
|
|
39
37
|
if (filtersRaw) {
|
|
40
38
|
try {
|
|
41
39
|
state.filters = JSON.parse(filtersRaw);
|
|
@@ -47,24 +45,42 @@ export function readURLState(): URLState {
|
|
|
47
45
|
|
|
48
46
|
export function writeURLState(state: URLState): void {
|
|
49
47
|
if (typeof window === 'undefined') return;
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const params =
|
|
55
|
-
|
|
56
|
-
if (state.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (state.
|
|
48
|
+
const rp = getRouterProvider();
|
|
49
|
+
if (!rp) return;
|
|
50
|
+
|
|
51
|
+
const current = rp.parse();
|
|
52
|
+
const params: Record<string, string> = { ...current.params };
|
|
53
|
+
|
|
54
|
+
if (state.page && state.page > 1) params['page'] = String(state.page);
|
|
55
|
+
else delete params['page'];
|
|
56
|
+
|
|
57
|
+
if (state.pageSize && state.pageSize !== 10) params['pageSize'] = String(state.pageSize);
|
|
58
|
+
else delete params['pageSize'];
|
|
59
|
+
|
|
60
|
+
if (state.sortField) params['sort'] = state.sortField;
|
|
61
|
+
else delete params['sort'];
|
|
62
|
+
|
|
63
|
+
if (state.sortOrder) params['order'] = state.sortOrder;
|
|
64
|
+
else delete params['order'];
|
|
65
|
+
|
|
66
|
+
if (state.search) params['q'] = state.search;
|
|
67
|
+
else delete params['q'];
|
|
68
|
+
|
|
60
69
|
if (state.filters && state.filters.length > 0) {
|
|
61
|
-
params
|
|
70
|
+
params['filters'] = JSON.stringify(state.filters);
|
|
71
|
+
} else {
|
|
72
|
+
delete params['filters'];
|
|
62
73
|
}
|
|
63
74
|
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
// Prevent redundant navigation if nothing actually changed
|
|
76
|
+
const qsOld = new URLSearchParams(current.params).toString();
|
|
77
|
+
const qsNew = new URLSearchParams(params).toString();
|
|
78
|
+
|
|
79
|
+
if (qsOld !== qsNew) {
|
|
80
|
+
rp.go({
|
|
81
|
+
to: current.pathname,
|
|
82
|
+
query: params,
|
|
83
|
+
type: 'replace'
|
|
84
|
+
});
|
|
69
85
|
}
|
|
70
86
|
}
|
package/src/useParsed.svelte.ts
CHANGED
|
@@ -5,11 +5,20 @@ import { getResources } from './context.svelte';
|
|
|
5
5
|
|
|
6
6
|
interface ParsedRoute {
|
|
7
7
|
resource?: string;
|
|
8
|
-
action?: 'list' | 'create' | 'edit' | 'show';
|
|
8
|
+
action?: 'list' | 'create' | 'edit' | 'show' | 'clone';
|
|
9
9
|
id?: string;
|
|
10
10
|
params: Record<string, string>;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
let globalPath = $state('/');
|
|
14
|
+
if (typeof window !== 'undefined') {
|
|
15
|
+
// Initialize
|
|
16
|
+
globalPath = currentPath();
|
|
17
|
+
// Listen to navigation events
|
|
18
|
+
window.addEventListener('hashchange', () => { globalPath = currentPath(); });
|
|
19
|
+
window.addEventListener('popstate', () => { globalPath = currentPath(); });
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
/**
|
|
14
23
|
* Parse the current hash URL into a structured route object.
|
|
15
24
|
* Returns reactive properties that update when the URL changes.
|
|
@@ -21,11 +30,10 @@ interface ParsedRoute {
|
|
|
21
30
|
* // parsed.id === '123'
|
|
22
31
|
*/
|
|
23
32
|
export function useParsed(): ParsedRoute {
|
|
24
|
-
const path = $derived(currentPath());
|
|
25
33
|
const resources = $derived((() => { try { return getResources(); } catch { return []; } })());
|
|
26
34
|
|
|
27
35
|
const parsed = $derived.by(() => {
|
|
28
|
-
const p =
|
|
36
|
+
const p = globalPath;
|
|
29
37
|
const result: ParsedRoute = { params: {} };
|
|
30
38
|
|
|
31
39
|
// Parse query params from hash
|
|
@@ -73,6 +81,9 @@ export function useParsed(): ParsedRoute {
|
|
|
73
81
|
} else if (restSegments[0] === 'show' && restSegments[1]) {
|
|
74
82
|
result.action = 'show';
|
|
75
83
|
result.id = restSegments[1];
|
|
84
|
+
} else if (restSegments[0] === 'clone' && restSegments[1]) {
|
|
85
|
+
result.action = 'clone';
|
|
86
|
+
result.id = restSegments[1];
|
|
76
87
|
} else if (restSegments[0]) {
|
|
77
88
|
// Legacy: /:resource/:id
|
|
78
89
|
result.action = 'show';
|
|
@@ -92,6 +103,9 @@ export function useParsed(): ParsedRoute {
|
|
|
92
103
|
} else if (segments[1] === 'show' && segments[2]) {
|
|
93
104
|
result.action = 'show';
|
|
94
105
|
result.id = segments[2];
|
|
106
|
+
} else if (segments[1] === 'clone' && segments[2]) {
|
|
107
|
+
result.action = 'clone';
|
|
108
|
+
result.id = segments[2];
|
|
95
109
|
} else if (segments[1]) {
|
|
96
110
|
result.action = 'show';
|
|
97
111
|
result.id = segments[1];
|
|
@@ -42,7 +42,13 @@ export function useModalForm<
|
|
|
42
42
|
let visible = $state(options.defaultVisible ?? false);
|
|
43
43
|
|
|
44
44
|
function show(id?: string | number) {
|
|
45
|
-
if (id !== undefined)
|
|
45
|
+
if (id !== undefined) {
|
|
46
|
+
formState.setAction('edit');
|
|
47
|
+
formState.setId(id);
|
|
48
|
+
} else {
|
|
49
|
+
formState.setAction('create');
|
|
50
|
+
formState.setId(undefined);
|
|
51
|
+
}
|
|
46
52
|
visible = true;
|
|
47
53
|
}
|
|
48
54
|
|