@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.
@@ -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 localStorage === 'undefined') return 'system';
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
- applyTheme(getStoredTheme());
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 localStorage !== 'undefined') {
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 localStorage === 'undefined') return 'blue';
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
- applyColorTheme(getStoredColorTheme());
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 localStorage !== 'undefined') {
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 hash = window.location.hash;
17
- const queryIdx = hash.indexOf('?');
18
- if (queryIdx === -1) return {};
15
+ const rp = getRouterProvider();
16
+ if (!rp) return {};
19
17
 
20
- const params = new URLSearchParams(hash.slice(queryIdx + 1));
18
+ const { params } = rp.parse();
21
19
  const state: URLState = {};
22
20
 
23
- const page = params.get('page');
21
+ const page = params['page'];
24
22
  if (page) state.page = parseInt(page, 10);
25
23
 
26
- const pageSize = params.get('pageSize');
24
+ const pageSize = params['pageSize'];
27
25
  if (pageSize) state.pageSize = parseInt(pageSize, 10);
28
26
 
29
- const sortField = params.get('sort');
27
+ const sortField = params['sort'];
30
28
  if (sortField) state.sortField = sortField;
31
29
 
32
- const sortOrder = params.get('order');
30
+ const sortOrder = params['order'];
33
31
  if (sortOrder === 'asc' || sortOrder === 'desc') state.sortOrder = sortOrder;
34
32
 
35
- const search = params.get('q');
33
+ const search = params['q'];
36
34
  if (search) state.search = search;
37
35
 
38
- const filtersRaw = params.get('filters');
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 hash = window.location.hash;
51
- const pathIdx = hash.indexOf('?');
52
- const basePath = pathIdx === -1 ? hash : hash.slice(0, pathIdx);
53
-
54
- const params = new URLSearchParams();
55
- if (state.page && state.page > 1) params.set('page', String(state.page));
56
- if (state.pageSize && state.pageSize !== 10) params.set('pageSize', String(state.pageSize));
57
- if (state.sortField) params.set('sort', state.sortField);
58
- if (state.sortOrder) params.set('order', state.sortOrder);
59
- if (state.search) params.set('q', state.search);
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.set('filters', JSON.stringify(state.filters));
70
+ params['filters'] = JSON.stringify(state.filters);
71
+ } else {
72
+ delete params['filters'];
62
73
  }
63
74
 
64
- const qs = params.toString();
65
- const newHash = qs ? `${basePath}?${qs}` : basePath;
66
-
67
- if (window.location.hash !== newHash) {
68
- history.replaceState(null, '', newHash);
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
  }
@@ -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 = path;
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) formState.setId(id);
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