@svadmin/core 0.30.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.
- package/package.json +5 -5
- package/src/auth-hooks.svelte.ts +58 -24
- package/src/context.svelte.ts +185 -46
- package/src/data-transfer.svelte.ts +51 -41
- package/src/export-format.ts +115 -0
- 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 +423 -21
- package/src/index.ts +11 -8
- package/src/mutation-hooks.svelte.ts +27 -16
- package/src/permissions.svelte.ts +4 -3
- package/src/query-hooks.svelte.ts +35 -15
- package/src/router.ts +28 -8
- package/src/routing-hooks.svelte.ts +22 -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 +29 -4
- package/src/unsaved-changes.svelte.ts +4 -38
- package/src/url-sync.ts +47 -8
- package/src/useCan.ts +3 -2
- package/src/useParsed.svelte.ts +39 -15
- package/src/utility-hooks.svelte.ts +119 -7
package/src/task-hooks.svelte.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createMutation, createQuery, useQueryClient } from '@tanstack/svelte-query';
|
|
2
2
|
import { getAdminOptions } from './options.svelte';
|
|
3
|
-
import {
|
|
3
|
+
import { captureAdminContext } from './context.svelte';
|
|
4
|
+
import type { AdminContextAccessor } from './context.svelte';
|
|
4
5
|
import { checkError, createOvertimeTracker, fireErrorNotification, fireSuccessNotification } from './hook-utils.svelte';
|
|
5
6
|
import type { NotificationConfig, OvertimeOptions } from './hook-utils.svelte';
|
|
6
|
-
import {
|
|
7
|
+
import { useTranslation } from './i18n.svelte';
|
|
7
8
|
import type {
|
|
8
9
|
HttpError,
|
|
9
10
|
SubmitTaskOptions,
|
|
@@ -15,8 +16,13 @@ import type {
|
|
|
15
16
|
|
|
16
17
|
function normalizeTaskProvider<TTask extends TaskRecord = TaskRecord>(
|
|
17
18
|
provider?: TaskProvider<TTask>,
|
|
19
|
+
adminContext: AdminContextAccessor = captureAdminContext(),
|
|
18
20
|
): TaskProvider<TTask> {
|
|
19
|
-
|
|
21
|
+
const activeProvider = provider ?? adminContext.taskProvider;
|
|
22
|
+
if (!activeProvider) {
|
|
23
|
+
throw new Error('TaskProvider not found. Did you call setTaskProvider in App.svelte?');
|
|
24
|
+
}
|
|
25
|
+
return activeProvider as TaskProvider<TTask>;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
export interface UseSubmitTaskOptions {
|
|
@@ -39,19 +45,21 @@ export function useSubmitTask<
|
|
|
39
45
|
TTask extends TaskRecord = TaskRecord,
|
|
40
46
|
TError = HttpError,
|
|
41
47
|
>(options: UseSubmitTaskOptions = {}) {
|
|
48
|
+
const adminContext = captureAdminContext();
|
|
49
|
+
const i18n = useTranslation();
|
|
42
50
|
const adminOptions = getAdminOptions();
|
|
43
51
|
const queryClient = useQueryClient();
|
|
44
52
|
const overtime = createOvertimeTracker(() => mutation.isPending, options.overtimeOptions ?? adminOptions.overtime);
|
|
45
53
|
|
|
46
54
|
const mutation = createMutation<TaskHandle<TTask>, TError, UseSubmitTaskMutateParams<TTask>>(() => ({
|
|
47
55
|
mutationFn: async (params) => {
|
|
48
|
-
const provider = normalizeTaskProvider<TTask>(params.taskProvider);
|
|
56
|
+
const provider = normalizeTaskProvider<TTask>(params.taskProvider, adminContext);
|
|
49
57
|
return provider.submit(params.taskName, params.options);
|
|
50
58
|
},
|
|
51
59
|
onSuccess: (data, params, context) => {
|
|
52
60
|
fireSuccessNotification(
|
|
53
61
|
params.successNotification,
|
|
54
|
-
t('task.submitSuccess'),
|
|
62
|
+
i18n.t('task.submitSuccess'),
|
|
55
63
|
data,
|
|
56
64
|
params.options,
|
|
57
65
|
params.taskName,
|
|
@@ -60,10 +68,10 @@ export function useSubmitTask<
|
|
|
60
68
|
options.mutationOptions?.onSuccess?.(data, params, context);
|
|
61
69
|
},
|
|
62
70
|
onError: (error, params, context) => {
|
|
63
|
-
checkError(error);
|
|
71
|
+
checkError(error, adminContext);
|
|
64
72
|
fireErrorNotification(
|
|
65
73
|
params.errorNotification,
|
|
66
|
-
t('task.submitFailed'),
|
|
74
|
+
i18n.t('task.submitFailed'),
|
|
67
75
|
error,
|
|
68
76
|
params.taskName,
|
|
69
77
|
);
|
|
@@ -93,10 +101,12 @@ export interface UseTaskOptions<TTask extends TaskRecord = TaskRecord, _TError =
|
|
|
93
101
|
export function useTask<TTask extends TaskRecord = TaskRecord, TError = HttpError>(
|
|
94
102
|
options: UseTaskOptions<TTask, TError> = {},
|
|
95
103
|
) {
|
|
104
|
+
const adminContext = captureAdminContext();
|
|
105
|
+
const i18n = useTranslation();
|
|
96
106
|
const adminOptions = getAdminOptions();
|
|
97
107
|
|
|
98
108
|
const query = createQuery<TTask, TError>(() => {
|
|
99
|
-
const provider = normalizeTaskProvider<TTask>(options.taskProvider);
|
|
109
|
+
const provider = normalizeTaskProvider<TTask>(options.taskProvider, adminContext);
|
|
100
110
|
const queryOptions = options.queryOptions;
|
|
101
111
|
return {
|
|
102
112
|
queryKey: ['task', options.taskId],
|
|
@@ -125,8 +135,8 @@ export function useTask<TTask extends TaskRecord = TaskRecord, TError = HttpErro
|
|
|
125
135
|
}
|
|
126
136
|
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
127
137
|
lastErrorAt = query.errorUpdatedAt;
|
|
128
|
-
checkError(query.error);
|
|
129
|
-
fireErrorNotification(options.errorNotification, t('task.fetchFailed'), query.error, options.taskId);
|
|
138
|
+
checkError(query.error, adminContext);
|
|
139
|
+
fireErrorNotification(options.errorNotification, i18n.t('task.fetchFailed'), query.error, options.taskId);
|
|
130
140
|
}
|
|
131
141
|
});
|
|
132
142
|
|
|
@@ -158,10 +168,12 @@ export interface UseTaskListOptions<TTask extends TaskRecord = TaskRecord, _TErr
|
|
|
158
168
|
export function useTaskList<TTask extends TaskRecord = TaskRecord, TError = HttpError>(
|
|
159
169
|
options: UseTaskListOptions<TTask, TError> = {},
|
|
160
170
|
) {
|
|
171
|
+
const adminContext = captureAdminContext();
|
|
172
|
+
const i18n = useTranslation();
|
|
161
173
|
const adminOptions = getAdminOptions();
|
|
162
174
|
|
|
163
175
|
const query = createQuery<TaskListResult<TTask>, TError>(() => {
|
|
164
|
-
const provider = normalizeTaskProvider<TTask>(options.taskProvider);
|
|
176
|
+
const provider = normalizeTaskProvider<TTask>(options.taskProvider, adminContext);
|
|
165
177
|
const queryOptions = options.queryOptions;
|
|
166
178
|
return {
|
|
167
179
|
queryKey: ['taskList', options.dlq ? 'dlq' : 'default', options.params],
|
|
@@ -194,8 +206,8 @@ export function useTaskList<TTask extends TaskRecord = TaskRecord, TError = Http
|
|
|
194
206
|
}
|
|
195
207
|
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
196
208
|
lastErrorAt = query.errorUpdatedAt;
|
|
197
|
-
checkError(query.error);
|
|
198
|
-
fireErrorNotification(options.errorNotification, t('task.fetchListFailed'), query.error, options.dlq ? 'taskDlq' : 'tasks');
|
|
209
|
+
checkError(query.error, adminContext);
|
|
210
|
+
fireErrorNotification(options.errorNotification, i18n.t('task.fetchListFailed'), query.error, options.dlq ? 'taskDlq' : 'tasks');
|
|
199
211
|
}
|
|
200
212
|
});
|
|
201
213
|
|
|
@@ -217,11 +229,12 @@ export interface UseTaskSubscriptionOptions<TTask extends TaskRecord = TaskRecor
|
|
|
217
229
|
export function useTaskSubscription<TTask extends TaskRecord = TaskRecord>(
|
|
218
230
|
options: UseTaskSubscriptionOptions<TTask>,
|
|
219
231
|
): void {
|
|
232
|
+
const adminContext = captureAdminContext();
|
|
220
233
|
$effect(() => {
|
|
221
234
|
const enabled = options.enabled ?? true;
|
|
222
235
|
if (!enabled) return;
|
|
223
236
|
|
|
224
|
-
const provider = normalizeTaskProvider<TTask>(options.taskProvider);
|
|
237
|
+
const provider = normalizeTaskProvider<TTask>(options.taskProvider, adminContext);
|
|
225
238
|
if (!provider.subscribe) {
|
|
226
239
|
throw new Error('TaskProvider does not implement subscribe');
|
|
227
240
|
}
|
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
|
|