@svadmin/core 0.31.0 → 0.32.0

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.
@@ -202,25 +202,6 @@ function resolvePreset(preset: ColorPreset | string): ColorPreset | undefined {
202
202
  return preset;
203
203
  }
204
204
 
205
- /** Apply a color preset's CSS variables based on current resolved theme. */
206
- let activePresetVars: string[] = [];
207
-
208
- function applyColorPreset(preset: ColorPreset): void {
209
- if (typeof document === 'undefined') return;
210
- const resolved = mode === 'system' ? getSystemPreference() : mode;
211
- const vars = resolved === 'dark' ? preset.dark : preset.light;
212
- const root = document.documentElement;
213
- for (const key of activePresetVars) {
214
- root.style.removeProperty(key);
215
- }
216
- activePresetVars = [];
217
- for (const [key, value] of Object.entries(vars)) {
218
- const cssVar = key.startsWith('--') ? key : `--${key}`;
219
- root.style.setProperty(cssVar, value);
220
- activePresetVars.push(cssVar);
221
- }
222
- }
223
-
224
205
  /** Get all available color presets (built-in + registered). */
225
206
  export function getColorPresets(): ColorPreset[] {
226
207
  return Object.values(builtinPresets);
@@ -239,136 +220,224 @@ export interface ThemeConfig {
239
220
  layoutPreset?: 'default' | 'clean-flat';
240
221
  }
241
222
 
223
+ /** Explicit document-level values contributed by one mounted AdminApp. */
224
+ export interface ThemeOwnerOptions {
225
+ themeConfig?: ThemeConfig;
226
+ defaultTheme?: ThemeMode;
227
+ }
228
+
229
+ /** Opaque identity used to update or unregister a mounted theme owner. */
230
+ export type ThemeOwnerToken = symbol;
231
+
242
232
  const STORAGE_KEY = 'svadmin-theme';
243
233
  const COLOR_STORAGE_KEY = 'svadmin-color-theme';
244
234
 
245
- // ── Theme configuration ──────────────────────────────────
235
+ interface ThemeOwnerEntry {
236
+ token: ThemeOwnerToken;
237
+ options: ThemeOwnerOptions;
238
+ }
239
+
240
+ let legacyThemeConfig: ThemeConfig = {};
246
241
  let themeConfig: ThemeConfig = {};
242
+ let themeOwners: ThemeOwnerEntry[] = [];
243
+ let activePresetVars: string[] = [];
244
+ let activeCssOverrideVars: string[] = [];
245
+ let colorSelectionOverridesConfig = false;
246
+
247
+ function cloneColorPreset(preset: ColorPreset): ColorPreset {
248
+ return {
249
+ ...preset,
250
+ light: { ...preset.light },
251
+ dark: { ...preset.dark },
252
+ };
253
+ }
247
254
 
248
- /**
249
- * Configure the theme system. Must be called before setTheme() or
250
- * automatically via AdminApp's themeConfig prop.
251
- */
252
- export function configureTheme(config: ThemeConfig): void {
253
- themeConfig = { ...themeConfig, ...config };
254
- applyTheme(mode);
255
- if (config.colorPreset) {
256
- const preset = resolvePreset(config.colorPreset);
257
- if (preset) {
258
- colorTheme = preset.name as ColorTheme;
259
- if (typeof window !== 'undefined') {
260
- try { localStorage.setItem(COLOR_STORAGE_KEY, preset.name); } catch { /* intentional */}
261
- }
262
- applyColorTheme(preset.name as ColorTheme);
263
- applyColorPreset(preset);
264
- }
265
- }
266
- if (config.cssOverrides) {
267
- applyCssOverrides(config.cssOverrides);
268
- }
255
+ function cloneThemeConfig(config: ThemeConfig = {}): ThemeConfig {
256
+ const cloned = { ...config };
257
+ if (config.cssOverrides) cloned.cssOverrides = { ...config.cssOverrides };
258
+ if (typeof config.colorPreset === 'object') cloned.colorPreset = cloneColorPreset(config.colorPreset);
259
+ return cloned;
269
260
  }
270
261
 
271
- /** Get current theme configuration */
272
- export function getThemeConfig(): ThemeConfig {
273
- return { ...themeConfig };
262
+ function cloneOwnerOptions(options: ThemeOwnerOptions): ThemeOwnerOptions {
263
+ return {
264
+ defaultTheme: options.defaultTheme,
265
+ themeConfig: options.themeConfig === undefined
266
+ ? undefined
267
+ : cloneThemeConfig(options.themeConfig),
268
+ };
274
269
  }
275
270
 
276
- function applyCssOverrides(overrides: Record<string, string>): void {
277
- if (typeof document === 'undefined') return;
278
- const root = document.documentElement;
279
- for (const [key, value] of Object.entries(overrides)) {
280
- const cssVar = key.startsWith('--') ? key : `--${key}`;
281
- root.style.setProperty(cssVar, value);
282
- }
271
+ function isExplicitOwner(options: ThemeOwnerOptions): boolean {
272
+ return options.themeConfig !== undefined || options.defaultTheme !== undefined;
283
273
  }
284
274
 
285
- /** Remove previously applied CSS overrides */
286
- export function clearCssOverrides(keys?: string[]): void {
287
- if (typeof document === 'undefined') return;
288
- const root = document.documentElement;
289
- if (keys) {
290
- for (const key of keys) {
291
- const cssVar = key.startsWith('--') ? key : `--${key}`;
292
- root.style.removeProperty(cssVar);
293
- }
294
- } else if (themeConfig.cssOverrides) {
295
- for (const key of Object.keys(themeConfig.cssOverrides)) {
296
- const cssVar = key.startsWith('--') ? key : `--${key}`;
297
- root.style.removeProperty(cssVar);
298
- }
275
+ function getActiveOwner(): ThemeOwnerEntry | undefined {
276
+ for (let index = themeOwners.length - 1; index >= 0; index -= 1) {
277
+ const owner = themeOwners[index];
278
+ if (isExplicitOwner(owner.options)) return owner;
299
279
  }
280
+ return undefined;
300
281
  }
301
282
 
302
- // ── Color themes (display metadata, derived from presets) ──
303
- const _colorThemesCache: { id: ColorTheme | string; label: string; color: string }[] = $derived(Object.values(builtinPresets).map(p => ({
304
- id: p.name as ColorTheme,
305
- label: p.label,
306
- color: p.color,
307
- })));
283
+ function getStorage(): Storage | undefined {
284
+ if (typeof window === 'undefined') return undefined;
285
+ try {
286
+ return window.localStorage;
287
+ } catch {
288
+ return undefined;
289
+ }
290
+ }
308
291
 
309
- export function getColorThemes() {
310
- return _colorThemesCache;
292
+ function readStoredTheme(): ThemeMode | undefined {
293
+ const stored = getStorage()?.getItem(STORAGE_KEY);
294
+ return stored === 'light' || stored === 'dark' || stored === 'system' ? stored : undefined;
311
295
  }
312
296
 
313
- // ── Dark/Light mode ──────────────────────────────────────
297
+ function readStoredColorTheme(): ColorTheme {
298
+ const stored = getStorage()?.getItem(COLOR_STORAGE_KEY);
299
+ return stored && builtinPresets[stored] ? stored as ColorTheme : 'blue';
300
+ }
314
301
 
315
- function getStoredTheme(): ThemeMode {
316
- if (typeof window === 'undefined') return 'system';
317
- try { return (localStorage.getItem(STORAGE_KEY) as ThemeMode) ?? 'system'; } catch { return 'system'; }
302
+ function persistSelection(key: string, value: string): void {
303
+ const storage = getStorage();
304
+ if (!storage || storage.getItem(key) === value) return;
305
+ storage.setItem(key, value);
318
306
  }
319
307
 
308
+ let selectedMode = readStoredTheme();
309
+ let mode = $state<ThemeMode>(selectedMode ?? 'system');
310
+ const initialColorTheme = readStoredColorTheme();
311
+ let selectedColorTheme = $state<ColorTheme>(initialColorTheme);
312
+ let colorTheme = $state<ColorTheme>(initialColorTheme);
313
+
320
314
  function getSystemPreference(): 'light' | 'dark' {
321
- if (typeof window === 'undefined') return 'light';
315
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return 'light';
322
316
  return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
323
317
  }
324
318
 
325
- let mode = $state<ThemeMode>(getStoredTheme());
319
+ function removeCssProperties(keys: string[]): void {
320
+ if (typeof document === 'undefined') return;
321
+ const root = document.documentElement;
322
+ for (const key of keys) root.style.removeProperty(key);
323
+ }
326
324
 
327
- function applyTheme(m: ThemeMode): void {
328
- const resolved = m === 'system' ? getSystemPreference() : m;
325
+ function clearAppliedArtifacts(): void {
329
326
  if (typeof document === 'undefined') return;
327
+ const root = document.documentElement;
330
328
 
331
- const strategy = themeConfig.strategy ?? 'standard';
332
- const el = document.documentElement;
333
-
334
- // Apply clean-flat layout preset class to html
335
- if (themeConfig.layoutPreset === 'clean-flat') {
336
- el.classList.add('layout-clean-flat');
337
- } else {
338
- el.classList.remove('layout-clean-flat');
339
- }
329
+ removeCssProperties(activeCssOverrideVars);
330
+ removeCssProperties(activePresetVars);
331
+ activeCssOverrideVars = [];
332
+ activePresetVars = [];
333
+ root.classList.remove('layout-clean-flat', 'light', 'dark');
334
+ root.style.removeProperty('color-scheme');
335
+ root.removeAttribute('data-theme');
336
+ }
337
+
338
+ function applyThemeClasses(themeMode: ThemeMode, config: ThemeConfig): void {
339
+ if (typeof document === 'undefined') return;
340
+ const resolved = themeMode === 'system' ? getSystemPreference() : themeMode;
341
+ const strategy = config.strategy ?? 'standard';
342
+ const root = document.documentElement;
340
343
 
344
+ root.classList.toggle('layout-clean-flat', config.layoutPreset === 'clean-flat');
341
345
  if (strategy === 'dark-first') {
342
- // Dark-first: default is dark, add 'light' class for light mode
343
- el.classList.toggle('light', resolved === 'light');
344
- el.classList.remove('dark'); // ensure no conflict
346
+ root.classList.toggle('light', resolved === 'light');
347
+ root.classList.remove('dark');
345
348
  } else {
346
- // Standard: default is light, add 'dark' class for dark mode
347
- el.classList.toggle('dark', resolved === 'dark');
348
- el.classList.remove('light'); // ensure no conflict
349
+ root.classList.toggle('dark', resolved === 'dark');
350
+ root.classList.remove('light');
351
+ }
352
+
353
+ if (!config.disableColorScheme) root.style.colorScheme = resolved;
354
+ }
355
+
356
+ function applyColorPreset(preset: ColorPreset, themeMode: ThemeMode): void {
357
+ if (typeof document === 'undefined') return;
358
+ const resolved = themeMode === 'system' ? getSystemPreference() : themeMode;
359
+ const variables = resolved === 'dark' ? preset.dark : preset.light;
360
+ const root = document.documentElement;
361
+
362
+ for (const [key, value] of Object.entries(variables)) {
363
+ const cssVar = key.startsWith('--') ? key : '--' + key;
364
+ root.style.setProperty(cssVar, value);
365
+ activePresetVars.push(cssVar);
349
366
  }
367
+ }
368
+
369
+ function applyCssOverrides(overrides: Record<string, string>): void {
370
+ if (typeof document === 'undefined') return;
371
+ const root = document.documentElement;
350
372
 
351
- // Set color-scheme attribute unless disabled
352
- if (!themeConfig.disableColorScheme) {
353
- el.style.colorScheme = resolved;
373
+ for (const [key, value] of Object.entries(overrides)) {
374
+ const cssVar = key.startsWith('--') ? key : '--' + key;
375
+ root.style.setProperty(cssVar, value);
376
+ activeCssOverrideVars.push(cssVar);
354
377
  }
355
378
  }
356
379
 
357
- // Apply on init (browser only)
358
- if (typeof window !== 'undefined') {
359
- applyTheme(getStoredTheme());
380
+ function applyEffectiveTheme(): void {
381
+ const owner = getActiveOwner();
382
+ const nextConfig = owner
383
+ ? cloneThemeConfig(owner.options.themeConfig)
384
+ : cloneThemeConfig(legacyThemeConfig);
385
+ const nextMode = selectedMode ?? owner?.options.defaultTheme ?? 'system';
386
+ const configuredPreset = !colorSelectionOverridesConfig && nextConfig.colorPreset
387
+ ? resolvePreset(nextConfig.colorPreset)
388
+ : undefined;
389
+ const preset = configuredPreset ?? builtinPresets[selectedColorTheme];
390
+
391
+ themeConfig = nextConfig;
392
+ mode = nextMode;
393
+ colorTheme = (preset?.name ?? selectedColorTheme) as ColorTheme;
394
+
395
+ if (typeof document === 'undefined') return;
396
+ clearAppliedArtifacts();
397
+ applyThemeClasses(mode, themeConfig);
398
+ document.documentElement.setAttribute('data-theme', colorTheme);
399
+ if (preset) applyColorPreset(preset, mode);
400
+ if (themeConfig.cssOverrides) applyCssOverrides(themeConfig.cssOverrides);
401
+ }
402
+
403
+ /**
404
+ * Configure the legacy document owner. Configuration is replaced as a whole;
405
+ * mounted explicit owners continue to take precedence until they unregister.
406
+ */
407
+ export function configureTheme(config: ThemeConfig): void {
408
+ legacyThemeConfig = cloneThemeConfig(config);
409
+ colorSelectionOverridesConfig = false;
410
+ if (!getActiveOwner()) applyEffectiveTheme();
360
411
  }
361
412
 
362
- // Listen for system preference changes
363
- if (typeof window !== 'undefined') {
364
- window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
365
- if (mode === 'system') {
366
- applyTheme('system');
367
- // Re-apply color preset for new resolved mode (light/dark values differ)
368
- const preset = builtinPresets[colorTheme];
369
- if (preset) applyColorPreset(preset);
413
+ /** Get current effective theme configuration. */
414
+ export function getThemeConfig(): ThemeConfig {
415
+ return cloneThemeConfig(themeConfig);
416
+ }
417
+
418
+ /** Remove previously applied CSS overrides. */
419
+ export function clearCssOverrides(keys?: string[]): void {
420
+ if (typeof document === 'undefined') return;
421
+ const root = document.documentElement;
422
+
423
+ if (keys) {
424
+ for (const key of keys) {
425
+ const cssVar = key.startsWith('--') ? key : '--' + key;
426
+ root.style.removeProperty(cssVar);
427
+ activeCssOverrideVars = activeCssOverrideVars.filter((activeKey) => activeKey !== cssVar);
370
428
  }
371
- });
429
+ } else {
430
+ removeCssProperties(activeCssOverrideVars);
431
+ activeCssOverrideVars = [];
432
+ }
433
+ }
434
+
435
+ export function getColorThemes() {
436
+ return Object.values(builtinPresets).map((preset) => ({
437
+ id: preset.name as ColorTheme,
438
+ label: preset.label,
439
+ color: preset.color,
440
+ }));
372
441
  }
373
442
 
374
443
  export function getTheme(): ThemeMode {
@@ -376,14 +445,9 @@ export function getTheme(): ThemeMode {
376
445
  }
377
446
 
378
447
  export function setTheme(newMode: ThemeMode): void {
379
- mode = newMode;
380
- if (typeof window !== 'undefined') {
381
- try { localStorage.setItem(STORAGE_KEY, newMode); } catch { /* intentional */}
382
- }
383
- applyTheme(newMode);
384
- // Re-apply color preset — light/dark CSS variable overrides differ
385
- const preset = builtinPresets[colorTheme];
386
- if (preset) applyColorPreset(preset);
448
+ selectedMode = newMode;
449
+ persistSelection(STORAGE_KEY, newMode);
450
+ applyEffectiveTheme();
387
451
  }
388
452
 
389
453
  export function toggleTheme(): void {
@@ -391,59 +455,89 @@ export function toggleTheme(): void {
391
455
  setTheme(resolved === 'dark' ? 'light' : 'dark');
392
456
  }
393
457
 
394
- /** Resolved theme (always 'light' or 'dark', never 'system') */
458
+ /** Resolved theme (always 'light' or 'dark', never 'system'). */
395
459
  export function getResolvedTheme(): 'light' | 'dark' {
396
460
  return mode === 'system' ? getSystemPreference() : mode;
397
461
  }
398
462
 
399
- // ── Color theme ──────────────────────────────────────────
463
+ export function getColorTheme(): ColorTheme {
464
+ return colorTheme;
465
+ }
400
466
 
401
- function getStoredColorTheme(): ColorTheme {
402
- if (typeof window === 'undefined') return 'blue';
403
- try { return (localStorage.getItem(COLOR_STORAGE_KEY) as ColorTheme) ?? 'blue'; } catch { return 'blue'; }
467
+ export function setColorTheme(nextColorTheme: ColorTheme): void {
468
+ selectedColorTheme = nextColorTheme;
469
+ colorSelectionOverridesConfig = true;
470
+ persistSelection(COLOR_STORAGE_KEY, nextColorTheme);
471
+ applyEffectiveTheme();
404
472
  }
405
473
 
406
- let colorTheme = $state<ColorTheme>(getStoredColorTheme());
474
+ /**
475
+ * Register a browser-only document owner. Owners with omitted values remain
476
+ * inert, allowing a mounted AdminApp to activate the same token after prop updates.
477
+ */
478
+ export function registerThemeOwner(options: ThemeOwnerOptions): ThemeOwnerToken | undefined {
479
+ if (typeof window === 'undefined' || typeof document === 'undefined') return undefined;
480
+
481
+ const token = Symbol('svadmin-theme-owner');
482
+ const entry = { token, options: cloneOwnerOptions(options) };
483
+ themeOwners.push(entry);
407
484
 
408
- function applyColorTheme(ct: ColorTheme): void {
409
- if (typeof document !== 'undefined') {
410
- document.documentElement.setAttribute('data-theme', ct);
485
+ if (isExplicitOwner(entry.options)) {
486
+ colorSelectionOverridesConfig = false;
487
+ applyEffectiveTheme();
411
488
  }
412
- }
413
489
 
414
- // Apply on init (browser only)
415
- if (typeof window !== 'undefined') {
416
- applyColorTheme(getStoredColorTheme());
417
- const initPreset = builtinPresets[getStoredColorTheme()];
418
- if (initPreset) applyColorPreset(initPreset);
490
+ return token;
419
491
  }
420
492
 
421
- export function getColorTheme(): ColorTheme {
422
- return colorTheme;
493
+ /** Update one mounted owner without changing its stack order. */
494
+ export function updateThemeOwner(
495
+ token: ThemeOwnerToken | undefined,
496
+ options: ThemeOwnerOptions,
497
+ ): void {
498
+ if (!token || typeof window === 'undefined' || typeof document === 'undefined') return;
499
+ const owner = themeOwners.find((entry) => entry.token === token);
500
+ if (!owner) return;
501
+
502
+ const wasExplicit = isExplicitOwner(owner.options);
503
+ owner.options = cloneOwnerOptions(options);
504
+ if (wasExplicit || isExplicitOwner(owner.options)) {
505
+ colorSelectionOverridesConfig = false;
506
+ applyEffectiveTheme();
507
+ }
423
508
  }
424
509
 
425
- export function setColorTheme(ct: ColorTheme): void {
426
- colorTheme = ct;
427
- if (typeof window !== 'undefined') {
428
- try { localStorage.setItem(COLOR_STORAGE_KEY, ct); } catch { /* intentional */}
510
+ /** Remove an owner safely regardless of its position in the stack. */
511
+ export function unregisterThemeOwner(token: ThemeOwnerToken | undefined): void {
512
+ if (!token || typeof window === 'undefined' || typeof document === 'undefined') return;
513
+ const index = themeOwners.findIndex((entry) => entry.token === token);
514
+ if (index === -1) return;
515
+
516
+ const [removed] = themeOwners.splice(index, 1);
517
+ if (isExplicitOwner(removed.options)) {
518
+ colorSelectionOverridesConfig = false;
519
+ applyEffectiveTheme();
429
520
  }
430
- applyColorTheme(ct);
431
- const preset = builtinPresets[ct];
432
- if (preset) applyColorPreset(preset);
433
521
  }
434
522
 
435
523
  export function resetTheme(): void {
436
- mode = getStoredTheme();
437
- colorTheme = getStoredColorTheme();
524
+ selectedMode = readStoredTheme();
525
+ selectedColorTheme = readStoredColorTheme();
526
+ mode = selectedMode ?? 'system';
527
+ colorTheme = selectedColorTheme;
528
+ legacyThemeConfig = {};
438
529
  themeConfig = {};
439
- if (typeof document !== 'undefined') {
440
- for (const key of activePresetVars) {
441
- document.documentElement.style.removeProperty(key);
442
- }
530
+ themeOwners = [];
531
+ colorSelectionOverridesConfig = false;
532
+ applyEffectiveTheme();
533
+ }
534
+
535
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
536
+ applyEffectiveTheme();
537
+
538
+ if (typeof window.matchMedia === 'function') {
539
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
540
+ if (mode === 'system') applyEffectiveTheme();
541
+ });
443
542
  }
444
- activePresetVars = [];
445
- applyTheme(mode);
446
- applyColorTheme(colorTheme);
447
- const preset = builtinPresets[colorTheme];
448
- if (preset) applyColorPreset(preset);
449
543
  }
@@ -1,4 +1,3 @@
1
- /* eslint-disable svelte/prefer-svelte-reactivity */
2
1
  export type ToastType = 'success' | 'error' | 'info' | 'warning' | 'undoable';
3
2
 
4
3
  export interface ToastItem {
@@ -18,7 +17,7 @@ let queue = $state<ToastItem[]>([]);
18
17
  export function getToastQueue() { return queue; }
19
18
  export function consumeToastQueue() { queue = []; }
20
19
 
21
- let promiseQueue = $state<{ id: number; promise: Promise<any>; opts: { loading: string; success: string; error: string } }[]>([]);
20
+ let promiseQueue = $state<{ id: number; promise: Promise<unknown>; opts: { loading: string; success: string; error: string } }[]>([]);
22
21
  export function getPromiseQueue() { return promiseQueue; }
23
22
  export function consumePromiseQueue() { promiseQueue = []; }
24
23
 
package/src/types.ts CHANGED
@@ -1,18 +1,40 @@
1
1
  // Core type definitions — DataProvider + AuthProvider + Providers
2
2
 
3
+ import type { Component } from 'svelte';
4
+
3
5
  // ─── HttpError ─────────────────────────────────────────────────
4
6
 
5
7
  export type ValidationErrors = Record<string, string | string[]>;
6
8
 
9
+ export interface HttpErrorOptions {
10
+ code?: string;
11
+ details?: unknown;
12
+ body?: unknown;
13
+ cause?: unknown;
14
+ }
15
+
7
16
  export class HttpError extends Error {
8
17
  statusCode: number;
9
18
  errors?: ValidationErrors;
10
-
11
- constructor(message: string, statusCode: number, errors?: ValidationErrors) {
12
- super(message);
19
+ code?: string;
20
+ details?: unknown;
21
+ body?: unknown;
22
+ override cause?: unknown;
23
+
24
+ constructor(
25
+ message: string,
26
+ statusCode: number,
27
+ errors?: ValidationErrors,
28
+ options: HttpErrorOptions = {},
29
+ ) {
30
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
13
31
  this.name = 'HttpError';
14
32
  this.statusCode = statusCode;
15
33
  this.errors = errors;
34
+ this.code = options.code;
35
+ this.details = options.details;
36
+ this.body = options.body;
37
+ this.cause = options.cause;
16
38
  }
17
39
  }
18
40
 
@@ -385,7 +407,7 @@ export interface MenuItem {
385
407
  /** Display label. Falls back to `t('menu.${name}')` if omitted */
386
408
  label?: string;
387
409
  /** Icon name string (maps to lucide icon set) or Svelte component */
388
- icon?: string | any;
410
+ icon?: string | Component;
389
411
  /** Navigation path — omit for parent-only menu nodes */
390
412
  href?: string;
391
413
  /** Open in new tab (useful for external links) */
@@ -1,8 +1,7 @@
1
- /* eslint-disable svelte/prefer-svelte-reactivity */
2
1
  // UnsavedChangesNotifier — guards against accidental navigation with dirty forms
3
2
  // Supports: browser tab close, hash-based SPA routing, and framework router interception
4
3
 
5
- import { t } from './i18n.svelte';
4
+ import { useTranslation } from './i18n.svelte';
6
5
  import { syncGlobalPath } from './useParsed.svelte';
7
6
 
8
7
  let dirty = $state(false);
@@ -15,46 +14,12 @@ export function resetUnsavedChanges(): void {
15
14
  dirty = false;
16
15
  _unsavedChangesCleanup?.();
17
16
  _unsavedChangesCleanup = null;
18
- __navigationGuard = null;
19
17
  }
20
18
 
21
19
  export function getUnsavedChanges(): boolean {
22
20
  return dirty;
23
21
  }
24
22
 
25
- /**
26
- * Navigation interceptor callback type.
27
- * Frameworks like SvelteKit can register a cancel function here.
28
- * Return `true` to allow navigation, `false` to block it.
29
- */
30
- interface NavigationGuard {
31
- (opts: { cancel: () => void }): void;
32
- }
33
-
34
- let __navigationGuard: NavigationGuard | null = null;
35
-
36
- /**
37
- * Register a framework-level navigation guard.
38
- *
39
- * In SvelteKit, call this from your root layout:
40
- * ```ts
41
- * import { beforeNavigate } from '$app/navigation';
42
- * import { registerNavigationGuard } from '@svadmin/core';
43
- *
44
- * registerNavigationGuard(({ cancel }) => {
45
- * beforeNavigate(({ cancel: kitCancel }) => {
46
- * cancel = kitCancel;
47
- * });
48
- * });
49
- * ```
50
- *
51
- * Or more idiomatically, use the built-in `initUnsavedChangesNotifier`
52
- * and pass a `beforeNavigate` callback.
53
- */
54
- export function registerNavigationGuard(guard: NavigationGuard): void {
55
- __navigationGuard = guard;
56
- }
57
-
58
23
  /**
59
24
  * Initialize the UnsavedChangesNotifier.
60
25
  *
@@ -82,6 +47,7 @@ export function initUnsavedChangesNotifier(options?: {
82
47
  beforeNavigate?: (callback: (nav: { cancel: () => void }) => void) => void;
83
48
  }) {
84
49
  if (typeof window === 'undefined') return;
50
+ const i18n = useTranslation();
85
51
  _unsavedChangesCleanup?.();
86
52
 
87
53
  const onBeforeUnload = (e: BeforeUnloadEvent) => {
@@ -94,7 +60,7 @@ export function initUnsavedChangesNotifier(options?: {
94
60
  if (options?.beforeNavigate) {
95
61
  options.beforeNavigate(({ cancel }) => {
96
62
  if (dirty) {
97
- if (!confirm(t('common.unsavedChanges'))) {
63
+ if (!confirm(i18n.t('common.unsavedChanges'))) {
98
64
  cancel();
99
65
  } else {
100
66
  dirty = false;
@@ -105,7 +71,7 @@ export function initUnsavedChangesNotifier(options?: {
105
71
 
106
72
  const onHashChange = (e: HashChangeEvent) => {
107
73
  if (!dirty) return;
108
- if (!confirm(t('common.unsavedChanges'))) {
74
+ if (!confirm(i18n.t('common.unsavedChanges'))) {
109
75
  if (e.oldURL) {
110
76
  history.replaceState(null, '', new URL(e.oldURL).hash || '#');
111
77
  syncGlobalPath();