@svadmin/core 0.31.0 → 0.32.1
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 +3 -3
- package/src/auth-hooks.svelte.ts +42 -25
- package/src/context.svelte.ts +185 -46
- package/src/data-transfer.svelte.ts +9 -62
- package/src/export-format.ts +35 -31
- package/src/form-hooks.svelte.ts +322 -58
- package/src/helpers.ts +11 -7
- package/src/hook-utils.svelte.ts +13 -12
- package/src/hooks.svelte.ts +29 -23
- package/src/http.ts +114 -33
- package/src/i18n.svelte.ts +181 -35
- package/src/index.ts +7 -6
- package/src/mutation-hooks.svelte.ts +27 -16
- package/src/query-hooks.svelte.ts +16 -13
- package/src/router.ts +28 -8
- package/src/routing-hooks.svelte.ts +13 -18
- package/src/table-hooks.svelte.ts +5 -3
- package/src/table-state.svelte.ts +6 -3
- package/src/task-hooks.svelte.ts +27 -14
- package/src/theme.svelte.ts +248 -154
- package/src/toast.svelte.ts +1 -2
- package/src/types.ts +26 -4
- package/src/unsaved-changes.svelte.ts +4 -38
- package/src/url-sync.ts +47 -8
- package/src/useParsed.svelte.ts +16 -11
- package/src/utility-hooks.svelte.ts +8 -7
package/src/theme.svelte.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
277
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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
|
-
|
|
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
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
el.classList.remove('dark'); // ensure no conflict
|
|
346
|
+
root.classList.toggle('light', resolved === 'light');
|
|
347
|
+
root.classList.remove('dark');
|
|
345
348
|
} else {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
463
|
+
export function getColorTheme(): ColorTheme {
|
|
464
|
+
return colorTheme;
|
|
465
|
+
}
|
|
400
466
|
|
|
401
|
-
function
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
485
|
+
if (isExplicitOwner(entry.options)) {
|
|
486
|
+
colorSelectionOverridesConfig = false;
|
|
487
|
+
applyEffectiveTheme();
|
|
411
488
|
}
|
|
412
|
-
}
|
|
413
489
|
|
|
414
|
-
|
|
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
|
-
|
|
422
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
if (typeof window
|
|
428
|
-
|
|
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
|
-
|
|
437
|
-
|
|
524
|
+
selectedMode = readStoredTheme();
|
|
525
|
+
selectedColorTheme = readStoredColorTheme();
|
|
526
|
+
mode = selectedMode ?? 'system';
|
|
527
|
+
colorTheme = selectedColorTheme;
|
|
528
|
+
legacyThemeConfig = {};
|
|
438
529
|
themeConfig = {};
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
}
|
package/src/toast.svelte.ts
CHANGED
|
@@ -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<
|
|
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
|
-
|
|
12
|
-
|
|
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 |
|
|
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 {
|
|
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();
|