@svadmin/core 0.49.1 → 0.52.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.
- package/README.md +32 -0
- package/package.json +21 -4
- package/src/access-control-contract.ts +250 -0
- package/src/adapters/casbin.ts +20 -14
- package/src/adapters/casl.ts +22 -19
- package/src/admin-tool.ts +7 -7
- package/src/audit-contract.ts +105 -0
- package/src/audit.ts +88 -128
- package/src/auth-hooks.svelte.ts +593 -261
- package/src/auth-mutation-contract.ts +143 -0
- package/src/auth-query-contract.ts +97 -0
- package/src/captured-mutation.svelte.ts +45 -0
- package/src/chatProvider.svelte.ts +1 -1
- package/src/command-contract.ts +195 -0
- package/src/context.svelte.ts +217 -205
- package/src/contract-context.svelte.ts +37 -0
- package/src/contract-form.svelte.ts +566 -0
- package/src/create-contract.ts +56 -0
- package/src/create-hooks.svelte.ts +347 -0
- package/src/create-many-contract.ts +90 -0
- package/src/create-many-hooks.svelte.ts +396 -0
- package/src/data-transfer.svelte.ts +1 -215
- package/src/defined-options.ts +50 -0
- package/src/delete-contract.ts +50 -0
- package/src/delete-hooks.svelte.ts +403 -0
- package/src/delete-many-contract.ts +60 -0
- package/src/delete-many-hooks.svelte.ts +402 -0
- package/src/enterprise.ts +4 -4
- package/src/export-format.ts +16 -14
- package/src/export-hooks.svelte.ts +191 -0
- package/src/feature-gate-contract.ts +53 -0
- package/src/hook-utils.svelte.ts +53 -105
- package/src/hooks.svelte.ts +19 -774
- package/src/http.ts +88 -87
- package/src/i18n.svelte.ts +56 -2
- package/src/import-contract.ts +106 -0
- package/src/import-hooks.svelte.ts +310 -0
- package/src/index.ts +77 -28
- package/src/inferencer-graphql.ts +7 -6
- package/src/infinite-query.svelte.ts +228 -0
- package/src/invalidation-contract.ts +26 -0
- package/src/live-sse.ts +91 -127
- package/src/live-subscription.svelte.ts +130 -0
- package/src/live-transport.ts +127 -0
- package/src/live-websocket.ts +85 -118
- package/src/live.svelte.ts +66 -68
- package/src/notification.svelte.ts +17 -15
- package/src/permission-catalog.ts +121 -0
- package/src/permission-hints-contract.ts +48 -0
- package/src/permissions.svelte.ts +112 -59
- package/src/plain-data.ts +61 -0
- package/src/provider-bundle.ts +85 -79
- package/src/provider-response.ts +35 -0
- package/src/query-hooks.svelte.ts +241 -286
- package/src/query-invalidation.ts +172 -0
- package/src/query-keys.ts +221 -110
- package/src/query-session.svelte.ts +216 -0
- package/src/query-snapshot.ts +144 -0
- package/src/reactive-projection.ts +53 -0
- package/src/record-decoder.ts +63 -0
- package/src/resource-binding.svelte.ts +19 -0
- package/src/resource-contract.ts +595 -0
- package/src/resource-mutation-types.ts +30 -0
- package/src/resource-schemas.ts +371 -0
- package/src/resource-transport-config.ts +156 -0
- package/src/route-parsing.ts +11 -11
- package/src/router-provider.ts +64 -59
- package/src/router.ts +8 -3
- package/src/routing-hooks.svelte.ts +90 -62
- package/src/schema-validation.ts +92 -0
- package/src/schema.ts +13 -0
- package/src/select-options.ts +95 -0
- package/src/select-query.svelte.ts +191 -0
- package/src/session-query.svelte.ts +88 -0
- package/src/strict-command-hooks.svelte.ts +333 -0
- package/src/strict-hooks.svelte.ts +198 -0
- package/src/table-hooks.svelte.ts +180 -174
- package/src/table-state.svelte.ts +117 -117
- package/src/task-contract.ts +87 -0
- package/src/task-hooks.svelte.ts +193 -149
- package/src/task-provider.ts +115 -0
- package/src/task-subscription.ts +132 -0
- package/src/theme.svelte.ts +155 -154
- package/src/toast.svelte.ts +38 -37
- package/src/types.ts +55 -103
- package/src/unsafe.ts +7 -0
- package/src/update-contract.ts +65 -0
- package/src/update-hooks.svelte.ts +347 -0
- package/src/update-many-contract.ts +79 -0
- package/src/update-many-hooks.svelte.ts +373 -0
- package/src/url-sync.ts +11 -10
- package/src/useCan.ts +96 -31
- package/src/useParsed.svelte.ts +18 -14
- package/src/useStepsForm.svelte.ts +123 -63
- package/src/utility-hooks.svelte.ts +101 -158
- package/src/form-hooks.svelte.ts +0 -889
- package/src/mutation-hooks.svelte.ts +0 -561
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { TaskError } from './task-contract';
|
|
2
|
+
|
|
3
|
+
export function taskClientField(value: unknown, key: string): unknown {
|
|
4
|
+
try {
|
|
5
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
6
|
+
const field: unknown = Reflect.get(value, key);
|
|
7
|
+
return field;
|
|
8
|
+
}
|
|
9
|
+
} catch {
|
|
10
|
+
// Configuration accessors must not leak SDK credentials.
|
|
11
|
+
}
|
|
12
|
+
throw new TaskError('INVALID_TASK_INPUT');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function taskClientMethod(value: unknown, key: string, optional = false): ((...args: unknown[]) => unknown) | undefined {
|
|
16
|
+
const callable = taskClientField(value, key);
|
|
17
|
+
if (callable === undefined && optional) return undefined;
|
|
18
|
+
if (typeof callable !== 'function') throw new TaskError('INVALID_TASK_INPUT');
|
|
19
|
+
return (...args) => {
|
|
20
|
+
const result: unknown = Reflect.apply(callable, value, args);
|
|
21
|
+
return result;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function requiredTaskClientMethod(value: unknown, key: string): (...args: unknown[]) => unknown {
|
|
26
|
+
const method = taskClientMethod(value, key);
|
|
27
|
+
if (!method) throw new TaskError('INVALID_TASK_INPUT');
|
|
28
|
+
return method;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function reportTaskError(error: TaskError, onError?: (error: TaskError) => void): void {
|
|
32
|
+
if (!onError) { console.error(error); return; }
|
|
33
|
+
try {
|
|
34
|
+
void Promise.resolve(onError(error)).catch(() => console.error(new TaskError('TASK_CALLBACK_FAILED')));
|
|
35
|
+
}
|
|
36
|
+
catch { console.error(new TaskError('TASK_CALLBACK_FAILED')); }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function taskHandleField(value: unknown, key: string, write = false): unknown {
|
|
40
|
+
try {
|
|
41
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
42
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
43
|
+
if (!descriptor) return undefined;
|
|
44
|
+
if ('value' in descriptor) {
|
|
45
|
+
const field: unknown = descriptor.value;
|
|
46
|
+
if (field === undefined) throw new TaskError('INVALID_TASK_RESPONSE', write);
|
|
47
|
+
return field;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// Handle accessors and reflection errors cannot carry trusted protocol values.
|
|
52
|
+
}
|
|
53
|
+
throw new TaskError('INVALID_TASK_RESPONSE', write);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function taskHandleMethod(value: unknown, key: string, write = false): (...args: unknown[]) => unknown {
|
|
57
|
+
const callable = taskHandleField(value, key, write);
|
|
58
|
+
if (typeof callable !== 'function') throw new TaskError('INVALID_TASK_RESPONSE', write);
|
|
59
|
+
return (...args) => {
|
|
60
|
+
const result: unknown = Reflect.apply(callable, value, args);
|
|
61
|
+
return result;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Validate synchronous events before publishing, but wait for a valid cleanup handle. */
|
|
66
|
+
export function validatedTaskSubscription<T>(
|
|
67
|
+
start: (onUpdate: (value: unknown) => void, onFailure: (error: unknown) => void) => unknown,
|
|
68
|
+
decode: (value: unknown) => T,
|
|
69
|
+
callback: (task: T) => void,
|
|
70
|
+
onError?: (error: TaskError) => void,
|
|
71
|
+
): () => void {
|
|
72
|
+
if (typeof callback !== 'function' || onError !== undefined && typeof onError !== 'function') {
|
|
73
|
+
throw new TaskError('INVALID_TASK_INPUT');
|
|
74
|
+
}
|
|
75
|
+
let stopped = false;
|
|
76
|
+
let ready = false;
|
|
77
|
+
let cleanup: (() => unknown) | undefined;
|
|
78
|
+
const buffered: T[] = [];
|
|
79
|
+
const release = () => {
|
|
80
|
+
try {
|
|
81
|
+
void Promise.resolve(cleanup?.()).catch(() => reportTaskError(new TaskError('TASK_SUBSCRIPTION_FAILED'), onError));
|
|
82
|
+
} catch { reportTaskError(new TaskError('TASK_SUBSCRIPTION_FAILED'), onError); }
|
|
83
|
+
};
|
|
84
|
+
const stop = () => {
|
|
85
|
+
if (stopped) return;
|
|
86
|
+
stopped = true;
|
|
87
|
+
buffered.length = 0;
|
|
88
|
+
release();
|
|
89
|
+
};
|
|
90
|
+
const emit = (task: T) => {
|
|
91
|
+
if (stopped) return;
|
|
92
|
+
try {
|
|
93
|
+
void Promise.resolve(callback(task)).catch(() => reportTaskError(new TaskError('TASK_CALLBACK_FAILED'), onError));
|
|
94
|
+
}
|
|
95
|
+
catch { reportTaskError(new TaskError('TASK_CALLBACK_FAILED'), onError); }
|
|
96
|
+
};
|
|
97
|
+
const fail = (error: TaskError) => {
|
|
98
|
+
if (stopped) return;
|
|
99
|
+
stop();
|
|
100
|
+
reportTaskError(error, onError);
|
|
101
|
+
};
|
|
102
|
+
try {
|
|
103
|
+
const subscription = start(value => {
|
|
104
|
+
if (stopped) return;
|
|
105
|
+
let task: T;
|
|
106
|
+
try { task = decode(value); }
|
|
107
|
+
catch (error) {
|
|
108
|
+
fail(new TaskError(error instanceof TaskError ? error.code : 'INVALID_TASK_RESPONSE'));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (ready) emit(task);
|
|
112
|
+
else buffered.push(task);
|
|
113
|
+
}, () => fail(new TaskError('TASK_SUBSCRIPTION_FAILED')));
|
|
114
|
+
if (subscription instanceof Promise) {
|
|
115
|
+
void Promise.resolve<unknown>(subscription).catch(() => {});
|
|
116
|
+
throw new TaskError('TASK_SUBSCRIPTION_FAILED');
|
|
117
|
+
}
|
|
118
|
+
cleanup = typeof subscription === 'function'
|
|
119
|
+
? () => { const result: unknown = Reflect.apply(subscription, undefined, []); return result; }
|
|
120
|
+
: taskHandleMethod(subscription, 'unsubscribe');
|
|
121
|
+
if (stopped) release();
|
|
122
|
+
else {
|
|
123
|
+
ready = true;
|
|
124
|
+
for (const task of buffered) emit(task);
|
|
125
|
+
buffered.length = 0;
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
stop();
|
|
129
|
+
throw new TaskError('TASK_SUBSCRIPTION_FAILED');
|
|
130
|
+
}
|
|
131
|
+
return stop;
|
|
132
|
+
}
|
package/src/theme.svelte.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
import { definedOptions } from './defined-options';
|
|
1
2
|
// Theme — dark/light/system mode + color theme management (Svelte 5 runes)
|
|
2
3
|
//
|
|
3
4
|
// Supports two class strategies:
|
|
4
5
|
// - 'standard' (default): adds 'dark' class for dark mode (light-first)
|
|
5
6
|
// - 'dark-first': adds 'light' class for light mode (dark-first)
|
|
6
7
|
|
|
7
|
-
export type ThemeMode
|
|
8
|
-
export type ColorTheme
|
|
8
|
+
export type ThemeMode='light'|'dark'|'system';
|
|
9
|
+
export type ColorTheme='blue'|'green'|'rose'|'orange'|'violet'|'neutral';
|
|
9
10
|
|
|
10
11
|
/** Controls how the theme class is applied to <html> */
|
|
11
|
-
export type ThemeStrategy
|
|
12
|
+
export type ThemeStrategy='standard'|'dark-first';
|
|
12
13
|
|
|
13
14
|
// ── Color Preset System ──────────────────────────────────
|
|
14
15
|
|
|
@@ -21,13 +22,13 @@ export interface ColorPreset {
|
|
|
21
22
|
/** Preview swatch color (hex) for UI pickers */
|
|
22
23
|
color: string;
|
|
23
24
|
/** CSS variable overrides for light mode */
|
|
24
|
-
light: Record<string,
|
|
25
|
+
light: Record<string,string>;
|
|
25
26
|
/** CSS variable overrides for dark mode */
|
|
26
|
-
dark: Record<string,
|
|
27
|
+
dark: Record<string,string>;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/** Built-in color presets — consumers can extend via registerColorPreset(). */
|
|
30
|
-
export const builtinPresets: Record<string,
|
|
31
|
+
export const builtinPresets: Record<string,ColorPreset>=$state({
|
|
31
32
|
neutral: {
|
|
32
33
|
name: 'neutral',
|
|
33
34
|
label: 'Neutral',
|
|
@@ -193,12 +194,12 @@ export const builtinPresets: Record<string, ColorPreset> = $state({
|
|
|
193
194
|
|
|
194
195
|
/** Register a custom color preset. Overwrites any built-in preset with the same name. */
|
|
195
196
|
export function registerColorPreset(preset: ColorPreset): void {
|
|
196
|
-
builtinPresets[preset.name]
|
|
197
|
+
builtinPresets[preset.name]=preset;
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
/** Resolve a preset by name or return the preset object directly. */
|
|
200
|
-
function resolvePreset(preset: ColorPreset
|
|
201
|
-
if
|
|
201
|
+
function resolvePreset(preset: ColorPreset|string): ColorPreset|undefined {
|
|
202
|
+
if(typeof preset==='string') return builtinPresets[preset];
|
|
202
203
|
return preset;
|
|
203
204
|
}
|
|
204
205
|
|
|
@@ -211,13 +212,13 @@ export interface ThemeConfig {
|
|
|
211
212
|
/** Class strategy: 'standard' toggles '.dark', 'dark-first' toggles '.light' */
|
|
212
213
|
strategy?: ThemeStrategy;
|
|
213
214
|
/** Custom CSS variables to inject as overrides on <html> */
|
|
214
|
-
cssOverrides?: Record<string,
|
|
215
|
+
cssOverrides?: Record<string,string>;
|
|
215
216
|
/** Whether to disable the built-in color-scheme attribute */
|
|
216
217
|
disableColorScheme?: boolean;
|
|
217
218
|
/** Built-in color preset name (e.g. 'indigo', 'blue') or a custom ColorPreset object */
|
|
218
|
-
colorPreset?: ColorPreset
|
|
219
|
+
colorPreset?: ColorPreset|string;
|
|
219
220
|
/** Layout style option: 'default' or 'clean-flat' for high-contrast flat panel design */
|
|
220
|
-
layoutPreset?: 'default'
|
|
221
|
+
layoutPreset?: 'default'|'clean-flat';
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
/** Explicit document-level values contributed by one mounted AdminApp. */
|
|
@@ -227,22 +228,22 @@ export interface ThemeOwnerOptions {
|
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
/** Opaque identity used to update or unregister a mounted theme owner. */
|
|
230
|
-
export type ThemeOwnerToken
|
|
231
|
+
export type ThemeOwnerToken=symbol;
|
|
231
232
|
|
|
232
|
-
const STORAGE_KEY
|
|
233
|
-
const COLOR_STORAGE_KEY
|
|
233
|
+
const STORAGE_KEY='svadmin-theme';
|
|
234
|
+
const COLOR_STORAGE_KEY='svadmin-color-theme';
|
|
234
235
|
|
|
235
236
|
interface ThemeOwnerEntry {
|
|
236
237
|
token: ThemeOwnerToken;
|
|
237
238
|
options: ThemeOwnerOptions;
|
|
238
239
|
}
|
|
239
240
|
|
|
240
|
-
let legacyThemeConfig: ThemeConfig
|
|
241
|
-
let themeConfig: ThemeConfig
|
|
242
|
-
let themeOwners: ThemeOwnerEntry[]
|
|
243
|
-
let activePresetVars: string[]
|
|
244
|
-
let activeCssOverrideVars: string[]
|
|
245
|
-
let colorSelectionOverridesConfig
|
|
241
|
+
let legacyThemeConfig: ThemeConfig={};
|
|
242
|
+
let themeConfig: ThemeConfig={};
|
|
243
|
+
let themeOwners: ThemeOwnerEntry[]=[];
|
|
244
|
+
let activePresetVars: string[]=[];
|
|
245
|
+
let activeCssOverrideVars: string[]=[];
|
|
246
|
+
let colorSelectionOverridesConfig=false;
|
|
246
247
|
|
|
247
248
|
function cloneColorPreset(preset: ColorPreset): ColorPreset {
|
|
248
249
|
return {
|
|
@@ -252,36 +253,36 @@ function cloneColorPreset(preset: ColorPreset): ColorPreset {
|
|
|
252
253
|
};
|
|
253
254
|
}
|
|
254
255
|
|
|
255
|
-
function cloneThemeConfig(config: ThemeConfig
|
|
256
|
-
const cloned
|
|
257
|
-
if
|
|
258
|
-
if
|
|
256
|
+
function cloneThemeConfig(config: ThemeConfig={}): ThemeConfig {
|
|
257
|
+
const cloned={ ...config };
|
|
258
|
+
if(config.cssOverrides) cloned.cssOverrides={ ...config.cssOverrides };
|
|
259
|
+
if(typeof config.colorPreset==='object') cloned.colorPreset=cloneColorPreset(config.colorPreset);
|
|
259
260
|
return cloned;
|
|
260
261
|
}
|
|
261
262
|
|
|
262
263
|
function cloneOwnerOptions(options: ThemeOwnerOptions): ThemeOwnerOptions {
|
|
263
|
-
return {
|
|
264
|
+
return definedOptions({
|
|
264
265
|
defaultTheme: options.defaultTheme,
|
|
265
|
-
themeConfig: options.themeConfig
|
|
266
|
+
themeConfig: options.themeConfig===undefined
|
|
266
267
|
? undefined
|
|
267
|
-
:
|
|
268
|
-
};
|
|
268
|
+
:cloneThemeConfig(options.themeConfig),
|
|
269
|
+
});
|
|
269
270
|
}
|
|
270
271
|
|
|
271
272
|
function isExplicitOwner(options: ThemeOwnerOptions): boolean {
|
|
272
|
-
return options.themeConfig
|
|
273
|
+
return options.themeConfig!==undefined||options.defaultTheme!==undefined;
|
|
273
274
|
}
|
|
274
275
|
|
|
275
|
-
function getActiveOwner(): ThemeOwnerEntry
|
|
276
|
-
for
|
|
277
|
-
const owner
|
|
278
|
-
if
|
|
276
|
+
function getActiveOwner(): ThemeOwnerEntry|undefined {
|
|
277
|
+
for(let index=themeOwners.length-1;index>=0;index-=1) {
|
|
278
|
+
const owner=themeOwners[index];
|
|
279
|
+
if(owner&&isExplicitOwner(owner.options)) return owner;
|
|
279
280
|
}
|
|
280
281
|
return undefined;
|
|
281
282
|
}
|
|
282
283
|
|
|
283
|
-
function getStorage(): Storage
|
|
284
|
-
if
|
|
284
|
+
function getStorage(): Storage|undefined {
|
|
285
|
+
if(typeof window==='undefined') return undefined;
|
|
285
286
|
try {
|
|
286
287
|
return window.localStorage;
|
|
287
288
|
} catch {
|
|
@@ -289,115 +290,115 @@ function getStorage(): Storage | undefined {
|
|
|
289
290
|
}
|
|
290
291
|
}
|
|
291
292
|
|
|
292
|
-
function readStoredTheme(): ThemeMode
|
|
293
|
-
const stored
|
|
294
|
-
return stored
|
|
293
|
+
function readStoredTheme(): ThemeMode|undefined {
|
|
294
|
+
const stored=getStorage()?.getItem(STORAGE_KEY);
|
|
295
|
+
return stored==='light'||stored==='dark'||stored==='system'? stored:undefined;
|
|
295
296
|
}
|
|
296
297
|
|
|
297
298
|
function readStoredColorTheme(): ColorTheme {
|
|
298
|
-
const stored
|
|
299
|
-
return stored
|
|
299
|
+
const stored=getStorage()?.getItem(COLOR_STORAGE_KEY);
|
|
300
|
+
return stored&&builtinPresets[stored]? stored as ColorTheme:'blue';
|
|
300
301
|
}
|
|
301
302
|
|
|
302
|
-
function persistSelection(key: string,
|
|
303
|
-
const storage
|
|
304
|
-
if
|
|
305
|
-
storage.setItem(key,
|
|
303
|
+
function persistSelection(key: string,value: string): void {
|
|
304
|
+
const storage=getStorage();
|
|
305
|
+
if(!storage||storage.getItem(key)===value) return;
|
|
306
|
+
storage.setItem(key,value);
|
|
306
307
|
}
|
|
307
308
|
|
|
308
|
-
let selectedMode
|
|
309
|
-
let mode
|
|
310
|
-
const initialColorTheme
|
|
311
|
-
let selectedColorTheme
|
|
312
|
-
let colorTheme
|
|
309
|
+
let selectedMode=readStoredTheme();
|
|
310
|
+
let mode=$state<ThemeMode>(selectedMode??'system');
|
|
311
|
+
const initialColorTheme=readStoredColorTheme();
|
|
312
|
+
let selectedColorTheme=$state<ColorTheme>(initialColorTheme);
|
|
313
|
+
let colorTheme=$state<ColorTheme>(initialColorTheme);
|
|
313
314
|
|
|
314
|
-
function getSystemPreference(): 'light'
|
|
315
|
-
if
|
|
316
|
-
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
315
|
+
function getSystemPreference(): 'light'|'dark' {
|
|
316
|
+
if(typeof window==='undefined'||typeof window.matchMedia!=='function') return 'light';
|
|
317
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches? 'dark':'light';
|
|
317
318
|
}
|
|
318
319
|
|
|
319
320
|
function removeCssProperties(keys: string[]): void {
|
|
320
|
-
if
|
|
321
|
-
const root
|
|
322
|
-
for
|
|
321
|
+
if(typeof document==='undefined') return;
|
|
322
|
+
const root=document.documentElement;
|
|
323
|
+
for(const key of keys) root.style.removeProperty(key);
|
|
323
324
|
}
|
|
324
325
|
|
|
325
326
|
function clearAppliedArtifacts(): void {
|
|
326
|
-
if
|
|
327
|
-
const root
|
|
327
|
+
if(typeof document==='undefined') return;
|
|
328
|
+
const root=document.documentElement;
|
|
328
329
|
|
|
329
330
|
removeCssProperties(activeCssOverrideVars);
|
|
330
331
|
removeCssProperties(activePresetVars);
|
|
331
|
-
activeCssOverrideVars
|
|
332
|
-
activePresetVars
|
|
333
|
-
root.classList.remove('layout-clean-flat',
|
|
332
|
+
activeCssOverrideVars=[];
|
|
333
|
+
activePresetVars=[];
|
|
334
|
+
root.classList.remove('layout-clean-flat','light','dark');
|
|
334
335
|
root.style.removeProperty('color-scheme');
|
|
335
336
|
root.removeAttribute('data-theme');
|
|
336
337
|
}
|
|
337
338
|
|
|
338
|
-
function applyThemeClasses(themeMode: ThemeMode,
|
|
339
|
-
if
|
|
340
|
-
const resolved
|
|
341
|
-
const strategy
|
|
342
|
-
const root
|
|
339
|
+
function applyThemeClasses(themeMode: ThemeMode,config: ThemeConfig): void {
|
|
340
|
+
if(typeof document==='undefined') return;
|
|
341
|
+
const resolved=themeMode==='system'? getSystemPreference():themeMode;
|
|
342
|
+
const strategy=config.strategy??'standard';
|
|
343
|
+
const root=document.documentElement;
|
|
343
344
|
|
|
344
|
-
root.classList.toggle('layout-clean-flat',
|
|
345
|
-
if
|
|
346
|
-
root.classList.toggle('light',
|
|
345
|
+
root.classList.toggle('layout-clean-flat',config.layoutPreset==='clean-flat');
|
|
346
|
+
if(strategy==='dark-first') {
|
|
347
|
+
root.classList.toggle('light',resolved==='light');
|
|
347
348
|
root.classList.remove('dark');
|
|
348
349
|
} else {
|
|
349
|
-
root.classList.toggle('dark',
|
|
350
|
+
root.classList.toggle('dark',resolved==='dark');
|
|
350
351
|
root.classList.remove('light');
|
|
351
352
|
}
|
|
352
353
|
|
|
353
|
-
if
|
|
354
|
+
if(!config.disableColorScheme) root.style.colorScheme=resolved;
|
|
354
355
|
}
|
|
355
356
|
|
|
356
|
-
function applyColorPreset(preset: ColorPreset,
|
|
357
|
-
if
|
|
358
|
-
const resolved
|
|
359
|
-
const variables
|
|
360
|
-
const root
|
|
357
|
+
function applyColorPreset(preset: ColorPreset,themeMode: ThemeMode): void {
|
|
358
|
+
if(typeof document==='undefined') return;
|
|
359
|
+
const resolved=themeMode==='system'? getSystemPreference():themeMode;
|
|
360
|
+
const variables=resolved==='dark'? preset.dark:preset.light;
|
|
361
|
+
const root=document.documentElement;
|
|
361
362
|
|
|
362
|
-
for
|
|
363
|
-
const cssVar
|
|
364
|
-
root.style.setProperty(cssVar,
|
|
363
|
+
for(const [key,value] of Object.entries(variables)) {
|
|
364
|
+
const cssVar=key.startsWith('--')? key:'--'+key;
|
|
365
|
+
root.style.setProperty(cssVar,value);
|
|
365
366
|
activePresetVars.push(cssVar);
|
|
366
367
|
}
|
|
367
368
|
}
|
|
368
369
|
|
|
369
|
-
function applyCssOverrides(overrides: Record<string,
|
|
370
|
-
if
|
|
371
|
-
const root
|
|
370
|
+
function applyCssOverrides(overrides: Record<string,string>): void {
|
|
371
|
+
if(typeof document==='undefined') return;
|
|
372
|
+
const root=document.documentElement;
|
|
372
373
|
|
|
373
|
-
for
|
|
374
|
-
const cssVar
|
|
375
|
-
root.style.setProperty(cssVar,
|
|
374
|
+
for(const [key,value] of Object.entries(overrides)) {
|
|
375
|
+
const cssVar=key.startsWith('--')? key:'--'+key;
|
|
376
|
+
root.style.setProperty(cssVar,value);
|
|
376
377
|
activeCssOverrideVars.push(cssVar);
|
|
377
378
|
}
|
|
378
379
|
}
|
|
379
380
|
|
|
380
381
|
function applyEffectiveTheme(): void {
|
|
381
|
-
const owner
|
|
382
|
-
const nextConfig
|
|
382
|
+
const owner=getActiveOwner();
|
|
383
|
+
const nextConfig=owner
|
|
383
384
|
? cloneThemeConfig(owner.options.themeConfig)
|
|
384
|
-
:
|
|
385
|
-
const nextMode
|
|
386
|
-
const configuredPreset
|
|
385
|
+
:cloneThemeConfig(legacyThemeConfig);
|
|
386
|
+
const nextMode=selectedMode??owner?.options.defaultTheme??'system';
|
|
387
|
+
const configuredPreset=!colorSelectionOverridesConfig&&nextConfig.colorPreset
|
|
387
388
|
? resolvePreset(nextConfig.colorPreset)
|
|
388
|
-
:
|
|
389
|
-
const preset
|
|
389
|
+
:undefined;
|
|
390
|
+
const preset=configuredPreset??builtinPresets[selectedColorTheme];
|
|
390
391
|
|
|
391
|
-
themeConfig
|
|
392
|
-
mode
|
|
393
|
-
colorTheme
|
|
392
|
+
themeConfig=nextConfig;
|
|
393
|
+
mode=nextMode;
|
|
394
|
+
colorTheme=(preset?.name??selectedColorTheme) as ColorTheme;
|
|
394
395
|
|
|
395
|
-
if
|
|
396
|
+
if(typeof document==='undefined') return;
|
|
396
397
|
clearAppliedArtifacts();
|
|
397
|
-
applyThemeClasses(mode,
|
|
398
|
-
document.documentElement.setAttribute('data-theme',
|
|
399
|
-
if
|
|
400
|
-
if
|
|
398
|
+
applyThemeClasses(mode,themeConfig);
|
|
399
|
+
document.documentElement.setAttribute('data-theme',colorTheme);
|
|
400
|
+
if(preset) applyColorPreset(preset,mode);
|
|
401
|
+
if(themeConfig.cssOverrides) applyCssOverrides(themeConfig.cssOverrides);
|
|
401
402
|
}
|
|
402
403
|
|
|
403
404
|
/**
|
|
@@ -405,9 +406,9 @@ function applyEffectiveTheme(): void {
|
|
|
405
406
|
* mounted explicit owners continue to take precedence until they unregister.
|
|
406
407
|
*/
|
|
407
408
|
export function configureTheme(config: ThemeConfig): void {
|
|
408
|
-
legacyThemeConfig
|
|
409
|
-
colorSelectionOverridesConfig
|
|
410
|
-
if
|
|
409
|
+
legacyThemeConfig=cloneThemeConfig(config);
|
|
410
|
+
colorSelectionOverridesConfig=false;
|
|
411
|
+
if(!getActiveOwner()) applyEffectiveTheme();
|
|
411
412
|
}
|
|
412
413
|
|
|
413
414
|
/** Get current effective theme configuration. */
|
|
@@ -417,18 +418,18 @@ export function getThemeConfig(): ThemeConfig {
|
|
|
417
418
|
|
|
418
419
|
/** Remove previously applied CSS overrides. */
|
|
419
420
|
export function clearCssOverrides(keys?: string[]): void {
|
|
420
|
-
if
|
|
421
|
-
const root
|
|
421
|
+
if(typeof document==='undefined') return;
|
|
422
|
+
const root=document.documentElement;
|
|
422
423
|
|
|
423
|
-
if
|
|
424
|
-
for
|
|
425
|
-
const cssVar
|
|
424
|
+
if(keys) {
|
|
425
|
+
for(const key of keys) {
|
|
426
|
+
const cssVar=key.startsWith('--')? key:'--'+key;
|
|
426
427
|
root.style.removeProperty(cssVar);
|
|
427
|
-
activeCssOverrideVars
|
|
428
|
+
activeCssOverrideVars=activeCssOverrideVars.filter((activeKey) => activeKey!==cssVar);
|
|
428
429
|
}
|
|
429
430
|
} else {
|
|
430
431
|
removeCssProperties(activeCssOverrideVars);
|
|
431
|
-
activeCssOverrideVars
|
|
432
|
+
activeCssOverrideVars=[];
|
|
432
433
|
}
|
|
433
434
|
}
|
|
434
435
|
|
|
@@ -445,19 +446,19 @@ export function getTheme(): ThemeMode {
|
|
|
445
446
|
}
|
|
446
447
|
|
|
447
448
|
export function setTheme(newMode: ThemeMode): void {
|
|
448
|
-
selectedMode
|
|
449
|
-
persistSelection(STORAGE_KEY,
|
|
449
|
+
selectedMode=newMode;
|
|
450
|
+
persistSelection(STORAGE_KEY,newMode);
|
|
450
451
|
applyEffectiveTheme();
|
|
451
452
|
}
|
|
452
453
|
|
|
453
454
|
export function toggleTheme(): void {
|
|
454
|
-
const resolved
|
|
455
|
-
setTheme(resolved
|
|
455
|
+
const resolved=mode==='system'? getSystemPreference():mode;
|
|
456
|
+
setTheme(resolved==='dark'? 'light':'dark');
|
|
456
457
|
}
|
|
457
458
|
|
|
458
459
|
/** Resolved theme (always 'light' or 'dark', never 'system'). */
|
|
459
|
-
export function getResolvedTheme(): 'light'
|
|
460
|
-
return mode
|
|
460
|
+
export function getResolvedTheme(): 'light'|'dark' {
|
|
461
|
+
return mode==='system'? getSystemPreference():mode;
|
|
461
462
|
}
|
|
462
463
|
|
|
463
464
|
export function getColorTheme(): ColorTheme {
|
|
@@ -465,9 +466,9 @@ export function getColorTheme(): ColorTheme {
|
|
|
465
466
|
}
|
|
466
467
|
|
|
467
468
|
export function setColorTheme(nextColorTheme: ColorTheme): void {
|
|
468
|
-
selectedColorTheme
|
|
469
|
-
colorSelectionOverridesConfig
|
|
470
|
-
persistSelection(COLOR_STORAGE_KEY,
|
|
469
|
+
selectedColorTheme=nextColorTheme;
|
|
470
|
+
colorSelectionOverridesConfig=true;
|
|
471
|
+
persistSelection(COLOR_STORAGE_KEY,nextColorTheme);
|
|
471
472
|
applyEffectiveTheme();
|
|
472
473
|
}
|
|
473
474
|
|
|
@@ -475,15 +476,15 @@ export function setColorTheme(nextColorTheme: ColorTheme): void {
|
|
|
475
476
|
* Register a browser-only document owner. Owners with omitted values remain
|
|
476
477
|
* inert, allowing a mounted AdminApp to activate the same token after prop updates.
|
|
477
478
|
*/
|
|
478
|
-
export function registerThemeOwner(options: ThemeOwnerOptions): ThemeOwnerToken
|
|
479
|
-
if
|
|
479
|
+
export function registerThemeOwner(options: ThemeOwnerOptions): ThemeOwnerToken|undefined {
|
|
480
|
+
if(typeof window==='undefined'||typeof document==='undefined') return undefined;
|
|
480
481
|
|
|
481
|
-
const token
|
|
482
|
-
const entry
|
|
482
|
+
const token=Symbol('svadmin-theme-owner');
|
|
483
|
+
const entry={ token,options: cloneOwnerOptions(options) };
|
|
483
484
|
themeOwners.push(entry);
|
|
484
485
|
|
|
485
|
-
if
|
|
486
|
-
colorSelectionOverridesConfig
|
|
486
|
+
if(isExplicitOwner(entry.options)) {
|
|
487
|
+
colorSelectionOverridesConfig=false;
|
|
487
488
|
applyEffectiveTheme();
|
|
488
489
|
}
|
|
489
490
|
|
|
@@ -492,52 +493,52 @@ export function registerThemeOwner(options: ThemeOwnerOptions): ThemeOwnerToken
|
|
|
492
493
|
|
|
493
494
|
/** Update one mounted owner without changing its stack order. */
|
|
494
495
|
export function updateThemeOwner(
|
|
495
|
-
token: ThemeOwnerToken
|
|
496
|
+
token: ThemeOwnerToken|undefined,
|
|
496
497
|
options: ThemeOwnerOptions,
|
|
497
498
|
): void {
|
|
498
|
-
if
|
|
499
|
-
const owner
|
|
500
|
-
if
|
|
501
|
-
|
|
502
|
-
const wasExplicit
|
|
503
|
-
owner.options
|
|
504
|
-
if
|
|
505
|
-
colorSelectionOverridesConfig
|
|
499
|
+
if(!token||typeof window==='undefined'||typeof document==='undefined') return;
|
|
500
|
+
const owner=themeOwners.find((entry) => entry.token===token);
|
|
501
|
+
if(!owner) return;
|
|
502
|
+
|
|
503
|
+
const wasExplicit=isExplicitOwner(owner.options);
|
|
504
|
+
owner.options=cloneOwnerOptions(options);
|
|
505
|
+
if(wasExplicit||isExplicitOwner(owner.options)) {
|
|
506
|
+
colorSelectionOverridesConfig=false;
|
|
506
507
|
applyEffectiveTheme();
|
|
507
508
|
}
|
|
508
509
|
}
|
|
509
510
|
|
|
510
511
|
/** Remove an owner safely regardless of its position in the stack. */
|
|
511
|
-
export function unregisterThemeOwner(token: ThemeOwnerToken
|
|
512
|
-
if
|
|
513
|
-
const index
|
|
514
|
-
if
|
|
515
|
-
|
|
516
|
-
const [removed]
|
|
517
|
-
if
|
|
518
|
-
colorSelectionOverridesConfig
|
|
512
|
+
export function unregisterThemeOwner(token: ThemeOwnerToken|undefined): void {
|
|
513
|
+
if(!token||typeof window==='undefined'||typeof document==='undefined') return;
|
|
514
|
+
const index=themeOwners.findIndex((entry) => entry.token===token);
|
|
515
|
+
if(index===-1) return;
|
|
516
|
+
|
|
517
|
+
const [removed]=themeOwners.splice(index,1);
|
|
518
|
+
if(removed&&isExplicitOwner(removed.options)) {
|
|
519
|
+
colorSelectionOverridesConfig=false;
|
|
519
520
|
applyEffectiveTheme();
|
|
520
521
|
}
|
|
521
522
|
}
|
|
522
523
|
|
|
523
524
|
export function resetTheme(): void {
|
|
524
|
-
selectedMode
|
|
525
|
-
selectedColorTheme
|
|
526
|
-
mode
|
|
527
|
-
colorTheme
|
|
528
|
-
legacyThemeConfig
|
|
529
|
-
themeConfig
|
|
530
|
-
themeOwners
|
|
531
|
-
colorSelectionOverridesConfig
|
|
525
|
+
selectedMode=readStoredTheme();
|
|
526
|
+
selectedColorTheme=readStoredColorTheme();
|
|
527
|
+
mode=selectedMode??'system';
|
|
528
|
+
colorTheme=selectedColorTheme;
|
|
529
|
+
legacyThemeConfig={};
|
|
530
|
+
themeConfig={};
|
|
531
|
+
themeOwners=[];
|
|
532
|
+
colorSelectionOverridesConfig=false;
|
|
532
533
|
applyEffectiveTheme();
|
|
533
534
|
}
|
|
534
535
|
|
|
535
|
-
if
|
|
536
|
+
if(typeof window!=='undefined'&&typeof document!=='undefined') {
|
|
536
537
|
applyEffectiveTheme();
|
|
537
538
|
|
|
538
|
-
if
|
|
539
|
-
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change',
|
|
540
|
-
if
|
|
539
|
+
if(typeof window.matchMedia==='function') {
|
|
540
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change',() => {
|
|
541
|
+
if(mode==='system') applyEffectiveTheme();
|
|
541
542
|
});
|
|
542
543
|
}
|
|
543
544
|
}
|