@svadmin/core 0.8.0 → 0.10.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 +1 -1
- package/src/index.ts +2 -2
- package/src/theme.svelte.ts +309 -11
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -37,8 +37,8 @@ export { audit, setAuditHandler, setAuditLogProvider, getAuditLogProvider } from
|
|
|
37
37
|
export type { AuditLogProvider } from './audit';
|
|
38
38
|
export { setChatProvider, getChatProvider, setChatContext, getChatContext } from './chatProvider.svelte';
|
|
39
39
|
export type { ChatProvider, ChatMessage, ChatContext, ChatAction } from './chatProvider.svelte';
|
|
40
|
-
export { getTheme, setTheme, toggleTheme, getResolvedTheme, getColorTheme, setColorTheme, colorThemes } from './theme.svelte';
|
|
41
|
-
export type { ThemeMode, ColorTheme } from './theme.svelte';
|
|
40
|
+
export { getTheme, setTheme, toggleTheme, getResolvedTheme, getColorTheme, setColorTheme, colorThemes, configureTheme, getThemeConfig, clearCssOverrides, builtinPresets, registerColorPreset, getColorPresets } from './theme.svelte';
|
|
41
|
+
export type { ThemeMode, ColorTheme, ThemeStrategy, ThemeConfig, ColorPreset } from './theme.svelte';
|
|
42
42
|
export { setUnsavedChanges, getUnsavedChanges, initUnsavedChangesNotifier } from './unsaved-changes.svelte';
|
|
43
43
|
export { setAdminOptions, getAdminOptions, getTextTransformers } from './options';
|
|
44
44
|
export type { AdminOptions, TextTransformers, OvertimeConfig } from './options';
|
package/src/theme.svelte.ts
CHANGED
|
@@ -1,20 +1,302 @@
|
|
|
1
1
|
// Theme — dark/light/system mode + color theme management (Svelte 5 runes)
|
|
2
|
+
//
|
|
3
|
+
// Supports two class strategies:
|
|
4
|
+
// - 'standard' (default): adds 'dark' class for dark mode (light-first)
|
|
5
|
+
// - 'dark-first': adds 'light' class for light mode (dark-first)
|
|
2
6
|
|
|
3
7
|
export type ThemeMode = 'light' | 'dark' | 'system';
|
|
4
8
|
export type ColorTheme = 'blue' | 'green' | 'rose' | 'orange' | 'violet' | 'zinc';
|
|
5
9
|
|
|
10
|
+
/** Controls how the theme class is applied to <html> */
|
|
11
|
+
export type ThemeStrategy = 'standard' | 'dark-first';
|
|
12
|
+
|
|
13
|
+
// ── Color Preset System ──────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/** A color preset defines CSS variable overrides for light and dark modes. */
|
|
16
|
+
export interface ColorPreset {
|
|
17
|
+
/** Unique name (e.g. 'indigo', 'blue') */
|
|
18
|
+
name: string;
|
|
19
|
+
/** Display label (e.g. 'Indigo', 'Blue') */
|
|
20
|
+
label: string;
|
|
21
|
+
/** Preview swatch color (hex) for UI pickers */
|
|
22
|
+
color: string;
|
|
23
|
+
/** CSS variable overrides for light mode */
|
|
24
|
+
light: Record<string, string>;
|
|
25
|
+
/** CSS variable overrides for dark mode */
|
|
26
|
+
dark: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Built-in color presets — consumers can extend via registerColorPreset(). */
|
|
30
|
+
export const builtinPresets: Record<string, ColorPreset> = {
|
|
31
|
+
neutral: {
|
|
32
|
+
name: 'neutral',
|
|
33
|
+
label: 'Neutral',
|
|
34
|
+
color: '#71717a',
|
|
35
|
+
light: {
|
|
36
|
+
'--primary': 'oklch(0.205 0.006 286)',
|
|
37
|
+
'--primary-foreground': 'oklch(0.985 0 0)',
|
|
38
|
+
'--ring': 'oklch(0.205 0.006 286)',
|
|
39
|
+
'--sidebar-primary': 'oklch(0.205 0.006 286)',
|
|
40
|
+
'--sidebar-primary-foreground': 'oklch(0.985 0 0)',
|
|
41
|
+
'--sidebar-ring': 'oklch(0.205 0.006 286)',
|
|
42
|
+
'--chart-1': 'oklch(0.488 0.243 264.376)',
|
|
43
|
+
},
|
|
44
|
+
dark: {
|
|
45
|
+
'--primary': 'oklch(0.922 0.004 286)',
|
|
46
|
+
'--primary-foreground': 'oklch(0.205 0.006 286)',
|
|
47
|
+
'--ring': 'oklch(0.556 0.004 286)',
|
|
48
|
+
'--sidebar-primary': 'oklch(0.922 0.004 286)',
|
|
49
|
+
'--sidebar-primary-foreground': 'oklch(0.205 0.006 286)',
|
|
50
|
+
'--sidebar-ring': 'oklch(0.556 0.004 286)',
|
|
51
|
+
'--chart-1': 'oklch(0.585 0.233 264)',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
indigo: {
|
|
55
|
+
name: 'indigo',
|
|
56
|
+
label: 'Indigo',
|
|
57
|
+
color: '#4f46e5',
|
|
58
|
+
light: {
|
|
59
|
+
'--primary': 'oklch(0.488 0.243 264.376)',
|
|
60
|
+
'--primary-foreground': 'oklch(0.985 0 0)',
|
|
61
|
+
'--ring': 'oklch(0.488 0.243 264.376)',
|
|
62
|
+
'--sidebar-primary': 'oklch(0.488 0.243 264.376)',
|
|
63
|
+
'--sidebar-primary-foreground': 'oklch(0.985 0 0)',
|
|
64
|
+
'--sidebar-ring': 'oklch(0.488 0.243 264.376)',
|
|
65
|
+
'--chart-1': 'oklch(0.488 0.243 264.376)',
|
|
66
|
+
},
|
|
67
|
+
dark: {
|
|
68
|
+
'--primary': 'oklch(0.585 0.233 264)',
|
|
69
|
+
'--primary-foreground': 'oklch(0.15 0.04 265)',
|
|
70
|
+
'--ring': 'oklch(0.55 0.18 264)',
|
|
71
|
+
'--sidebar-primary': 'oklch(0.585 0.233 264)',
|
|
72
|
+
'--sidebar-primary-foreground': 'oklch(0.985 0 0)',
|
|
73
|
+
'--sidebar-ring': 'oklch(0.55 0.18 264)',
|
|
74
|
+
'--chart-1': 'oklch(0.585 0.233 264)',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
blue: {
|
|
78
|
+
name: 'blue',
|
|
79
|
+
label: 'Blue',
|
|
80
|
+
color: '#3b82f6',
|
|
81
|
+
light: {
|
|
82
|
+
'--primary': 'oklch(0.546 0.245 262.881)',
|
|
83
|
+
'--primary-foreground': 'oklch(0.985 0 0)',
|
|
84
|
+
'--ring': 'oklch(0.546 0.245 262.881)',
|
|
85
|
+
'--sidebar-primary': 'oklch(0.546 0.245 262.881)',
|
|
86
|
+
'--sidebar-primary-foreground': 'oklch(0.985 0 0)',
|
|
87
|
+
'--sidebar-ring': 'oklch(0.546 0.245 262.881)',
|
|
88
|
+
'--chart-1': 'oklch(0.546 0.245 262.881)',
|
|
89
|
+
},
|
|
90
|
+
dark: {
|
|
91
|
+
'--primary': 'oklch(0.623 0.214 259.815)',
|
|
92
|
+
'--primary-foreground': 'oklch(0.15 0.04 265)',
|
|
93
|
+
'--ring': 'oklch(0.546 0.245 262.881)',
|
|
94
|
+
'--sidebar-primary': 'oklch(0.623 0.214 259.815)',
|
|
95
|
+
'--sidebar-primary-foreground': 'oklch(0.985 0 0)',
|
|
96
|
+
'--sidebar-ring': 'oklch(0.546 0.245 262.881)',
|
|
97
|
+
'--chart-1': 'oklch(0.623 0.214 259.815)',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
green: {
|
|
101
|
+
name: 'green',
|
|
102
|
+
label: 'Green',
|
|
103
|
+
color: '#22c55e',
|
|
104
|
+
light: {
|
|
105
|
+
'--primary': 'oklch(0.527 0.185 150.069)',
|
|
106
|
+
'--primary-foreground': 'oklch(0.985 0 0)',
|
|
107
|
+
'--ring': 'oklch(0.527 0.185 150.069)',
|
|
108
|
+
'--sidebar-primary': 'oklch(0.527 0.185 150.069)',
|
|
109
|
+
'--sidebar-primary-foreground': 'oklch(0.985 0 0)',
|
|
110
|
+
'--sidebar-ring': 'oklch(0.527 0.185 150.069)',
|
|
111
|
+
'--chart-1': 'oklch(0.527 0.185 150.069)',
|
|
112
|
+
},
|
|
113
|
+
dark: {
|
|
114
|
+
'--primary': 'oklch(0.627 0.194 149.214)',
|
|
115
|
+
'--primary-foreground': 'oklch(0.15 0.05 150)',
|
|
116
|
+
'--ring': 'oklch(0.527 0.185 150.069)',
|
|
117
|
+
'--sidebar-primary': 'oklch(0.627 0.194 149.214)',
|
|
118
|
+
'--sidebar-primary-foreground': 'oklch(0.985 0 0)',
|
|
119
|
+
'--sidebar-ring': 'oklch(0.527 0.185 150.069)',
|
|
120
|
+
'--chart-1': 'oklch(0.627 0.194 149.214)',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
rose: {
|
|
124
|
+
name: 'rose',
|
|
125
|
+
label: 'Rose',
|
|
126
|
+
color: '#f43f5e',
|
|
127
|
+
light: {
|
|
128
|
+
'--primary': 'oklch(0.577 0.245 27.325)',
|
|
129
|
+
'--primary-foreground': 'oklch(0.985 0 0)',
|
|
130
|
+
'--ring': 'oklch(0.577 0.245 27.325)',
|
|
131
|
+
'--sidebar-primary': 'oklch(0.577 0.245 27.325)',
|
|
132
|
+
'--sidebar-primary-foreground': 'oklch(0.985 0 0)',
|
|
133
|
+
'--sidebar-ring': 'oklch(0.577 0.245 27.325)',
|
|
134
|
+
'--chart-1': 'oklch(0.577 0.245 27.325)',
|
|
135
|
+
},
|
|
136
|
+
dark: {
|
|
137
|
+
'--primary': 'oklch(0.577 0.245 27.325)',
|
|
138
|
+
'--primary-foreground': 'oklch(0.985 0 0)',
|
|
139
|
+
'--ring': 'oklch(0.577 0.245 27.325)',
|
|
140
|
+
'--sidebar-primary': 'oklch(0.577 0.245 27.325)',
|
|
141
|
+
'--sidebar-primary-foreground': 'oklch(0.985 0 0)',
|
|
142
|
+
'--sidebar-ring': 'oklch(0.577 0.245 27.325)',
|
|
143
|
+
'--chart-1': 'oklch(0.577 0.245 27.325)',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
orange: {
|
|
147
|
+
name: 'orange',
|
|
148
|
+
label: 'Orange',
|
|
149
|
+
color: '#f97316',
|
|
150
|
+
light: {
|
|
151
|
+
'--primary': 'oklch(0.646 0.222 41.116)',
|
|
152
|
+
'--primary-foreground': 'oklch(0.985 0 0)',
|
|
153
|
+
'--ring': 'oklch(0.646 0.222 41.116)',
|
|
154
|
+
'--sidebar-primary': 'oklch(0.646 0.222 41.116)',
|
|
155
|
+
'--sidebar-primary-foreground': 'oklch(0.985 0 0)',
|
|
156
|
+
'--sidebar-ring': 'oklch(0.646 0.222 41.116)',
|
|
157
|
+
'--chart-1': 'oklch(0.646 0.222 41.116)',
|
|
158
|
+
},
|
|
159
|
+
dark: {
|
|
160
|
+
'--primary': 'oklch(0.646 0.222 41.116)',
|
|
161
|
+
'--primary-foreground': 'oklch(0.985 0 0)',
|
|
162
|
+
'--ring': 'oklch(0.646 0.222 41.116)',
|
|
163
|
+
'--sidebar-primary': 'oklch(0.646 0.222 41.116)',
|
|
164
|
+
'--sidebar-primary-foreground': 'oklch(0.985 0 0)',
|
|
165
|
+
'--sidebar-ring': 'oklch(0.646 0.222 41.116)',
|
|
166
|
+
'--chart-1': 'oklch(0.646 0.222 41.116)',
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
violet: {
|
|
170
|
+
name: 'violet',
|
|
171
|
+
label: 'Violet',
|
|
172
|
+
color: '#8b5cf6',
|
|
173
|
+
light: {
|
|
174
|
+
'--primary': 'oklch(0.541 0.281 293.009)',
|
|
175
|
+
'--primary-foreground': 'oklch(0.985 0 0)',
|
|
176
|
+
'--ring': 'oklch(0.541 0.281 293.009)',
|
|
177
|
+
'--sidebar-primary': 'oklch(0.541 0.281 293.009)',
|
|
178
|
+
'--sidebar-primary-foreground': 'oklch(0.985 0 0)',
|
|
179
|
+
'--sidebar-ring': 'oklch(0.541 0.281 293.009)',
|
|
180
|
+
'--chart-1': 'oklch(0.541 0.281 293.009)',
|
|
181
|
+
},
|
|
182
|
+
dark: {
|
|
183
|
+
'--primary': 'oklch(0.541 0.281 293.009)',
|
|
184
|
+
'--primary-foreground': 'oklch(0.985 0 0)',
|
|
185
|
+
'--ring': 'oklch(0.541 0.281 293.009)',
|
|
186
|
+
'--sidebar-primary': 'oklch(0.541 0.281 293.009)',
|
|
187
|
+
'--sidebar-primary-foreground': 'oklch(0.985 0 0)',
|
|
188
|
+
'--sidebar-ring': 'oklch(0.541 0.281 293.009)',
|
|
189
|
+
'--chart-1': 'oklch(0.541 0.281 293.009)',
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/** Register a custom color preset. Overwrites any built-in preset with the same name. */
|
|
195
|
+
export function registerColorPreset(preset: ColorPreset): void {
|
|
196
|
+
builtinPresets[preset.name] = preset;
|
|
197
|
+
// Update colorThemes display list
|
|
198
|
+
const idx = colorThemes.findIndex(ct => ct.id === preset.name);
|
|
199
|
+
const entry = { id: preset.name as ColorTheme, label: preset.label, color: preset.color };
|
|
200
|
+
if (idx >= 0) colorThemes[idx] = entry;
|
|
201
|
+
else colorThemes.push(entry);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Resolve a preset by name or return the preset object directly. */
|
|
205
|
+
function resolvePreset(preset: ColorPreset | string): ColorPreset | undefined {
|
|
206
|
+
if (typeof preset === 'string') return builtinPresets[preset];
|
|
207
|
+
return preset;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Apply a color preset's CSS variables based on current resolved theme. */
|
|
211
|
+
function applyColorPreset(preset: ColorPreset): void {
|
|
212
|
+
if (typeof document === 'undefined') return;
|
|
213
|
+
const resolved = mode === 'system' ? getSystemPreference() : mode;
|
|
214
|
+
const vars = resolved === 'dark' ? preset.dark : preset.light;
|
|
215
|
+
const root = document.documentElement;
|
|
216
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
217
|
+
const cssVar = key.startsWith('--') ? key : `--${key}`;
|
|
218
|
+
root.style.setProperty(cssVar, value);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Get all available color presets (built-in + registered). */
|
|
223
|
+
export function getColorPresets(): ColorPreset[] {
|
|
224
|
+
return Object.values(builtinPresets);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface ThemeConfig {
|
|
228
|
+
/** Class strategy: 'standard' toggles '.dark', 'dark-first' toggles '.light' */
|
|
229
|
+
strategy?: ThemeStrategy;
|
|
230
|
+
/** Custom CSS variables to inject as overrides on <html> */
|
|
231
|
+
cssOverrides?: Record<string, string>;
|
|
232
|
+
/** Whether to disable the built-in color-scheme attribute */
|
|
233
|
+
disableColorScheme?: boolean;
|
|
234
|
+
/** Built-in color preset name (e.g. 'indigo', 'blue') or a custom ColorPreset object */
|
|
235
|
+
colorPreset?: ColorPreset | string;
|
|
236
|
+
}
|
|
237
|
+
|
|
6
238
|
const STORAGE_KEY = 'svadmin-theme';
|
|
7
239
|
const COLOR_STORAGE_KEY = 'svadmin-color-theme';
|
|
8
240
|
|
|
9
|
-
// ──
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
241
|
+
// ── Theme configuration ──────────────────────────────────
|
|
242
|
+
let themeConfig: ThemeConfig = {};
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Configure the theme system. Must be called before setTheme() or
|
|
246
|
+
* automatically via AdminApp's themeConfig prop.
|
|
247
|
+
*/
|
|
248
|
+
export function configureTheme(config: ThemeConfig): void {
|
|
249
|
+
themeConfig = { ...config };
|
|
250
|
+
// Re-apply the current theme with new strategy
|
|
251
|
+
applyTheme(mode);
|
|
252
|
+
// Apply color preset first (lower priority)
|
|
253
|
+
if (config.colorPreset) {
|
|
254
|
+
const preset = resolvePreset(config.colorPreset);
|
|
255
|
+
if (preset) applyColorPreset(preset);
|
|
256
|
+
}
|
|
257
|
+
// Then apply manual CSS overrides (higher priority)
|
|
258
|
+
if (config.cssOverrides) {
|
|
259
|
+
applyCssOverrides(config.cssOverrides);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Get current theme configuration */
|
|
264
|
+
export function getThemeConfig(): ThemeConfig {
|
|
265
|
+
return { ...themeConfig };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function applyCssOverrides(overrides: Record<string, string>): void {
|
|
269
|
+
if (typeof document === 'undefined') return;
|
|
270
|
+
const root = document.documentElement;
|
|
271
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
272
|
+
const cssVar = key.startsWith('--') ? key : `--${key}`;
|
|
273
|
+
root.style.setProperty(cssVar, value);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Remove previously applied CSS overrides */
|
|
278
|
+
export function clearCssOverrides(keys?: string[]): void {
|
|
279
|
+
if (typeof document === 'undefined') return;
|
|
280
|
+
const root = document.documentElement;
|
|
281
|
+
if (keys) {
|
|
282
|
+
for (const key of keys) {
|
|
283
|
+
const cssVar = key.startsWith('--') ? key : `--${key}`;
|
|
284
|
+
root.style.removeProperty(cssVar);
|
|
285
|
+
}
|
|
286
|
+
} else if (themeConfig.cssOverrides) {
|
|
287
|
+
for (const key of Object.keys(themeConfig.cssOverrides)) {
|
|
288
|
+
const cssVar = key.startsWith('--') ? key : `--${key}`;
|
|
289
|
+
root.style.removeProperty(cssVar);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── Color themes (display metadata, derived from presets) ──
|
|
295
|
+
export const colorThemes: { id: ColorTheme | string; label: string; color: string }[] = Object.values(builtinPresets).map(p => ({
|
|
296
|
+
id: p.name as ColorTheme,
|
|
297
|
+
label: p.label,
|
|
298
|
+
color: p.color,
|
|
299
|
+
}));
|
|
18
300
|
|
|
19
301
|
// ── Dark/Light mode ──────────────────────────────────────
|
|
20
302
|
|
|
@@ -32,8 +314,24 @@ let mode = $state<ThemeMode>(getStoredTheme());
|
|
|
32
314
|
|
|
33
315
|
function applyTheme(m: ThemeMode): void {
|
|
34
316
|
const resolved = m === 'system' ? getSystemPreference() : m;
|
|
35
|
-
if (typeof document
|
|
36
|
-
|
|
317
|
+
if (typeof document === 'undefined') return;
|
|
318
|
+
|
|
319
|
+
const strategy = themeConfig.strategy ?? 'standard';
|
|
320
|
+
const el = document.documentElement;
|
|
321
|
+
|
|
322
|
+
if (strategy === 'dark-first') {
|
|
323
|
+
// Dark-first: default is dark, add 'light' class for light mode
|
|
324
|
+
el.classList.toggle('light', resolved === 'light');
|
|
325
|
+
el.classList.remove('dark'); // ensure no conflict
|
|
326
|
+
} else {
|
|
327
|
+
// Standard: default is light, add 'dark' class for dark mode
|
|
328
|
+
el.classList.toggle('dark', resolved === 'dark');
|
|
329
|
+
el.classList.remove('light'); // ensure no conflict
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Set color-scheme attribute unless disabled
|
|
333
|
+
if (!themeConfig.disableColorScheme) {
|
|
334
|
+
el.style.colorScheme = resolved;
|
|
37
335
|
}
|
|
38
336
|
}
|
|
39
337
|
|