@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/core",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Core SDK — hooks, types, context, i18n, permissions, router",
5
5
  "type": "module",
6
6
  "sideEffects": false,
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';
@@ -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
- // ── Color themes (display metadata) ──────────────────────
10
- export const colorThemes: { id: ColorTheme; label: string; color: string }[] = [
11
- { id: 'blue', label: 'Blue', color: '#3b82f6' },
12
- { id: 'green', label: 'Green', color: '#22c55e' },
13
- { id: 'rose', label: 'Rose', color: '#f43f5e' },
14
- { id: 'orange', label: 'Orange', color: '#f97316' },
15
- { id: 'violet', label: 'Violet', color: '#8b5cf6' },
16
- { id: 'zinc', label: 'Zinc', color: '#71717a' },
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 !== 'undefined') {
36
- document.documentElement.classList.toggle('dark', resolved === 'dark');
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