@vobs/ui 0.1.0 → 0.3.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/dist/app-shell-dom.d.ts +6 -0
- package/dist/app-shell-dom.js +373 -33
- package/dist/app-shell.d.ts +55 -19
- package/dist/app-shell.js +63 -18
- package/dist/base.css +1 -2
- package/dist/complex-inputs.js +7 -3
- package/dist/components.css +235 -47
- package/dist/data-display.d.ts +31 -0
- package/dist/data-display.js +72 -29
- package/dist/dialog.d.ts +26 -0
- package/dist/dialog.js +94 -0
- package/dist/forms.js +10 -6
- package/dist/index.d.ts +11 -2
- package/dist/index.js +16 -8
- package/dist/overlay.d.ts +4 -0
- package/dist/overlay.js +10 -5
- package/dist/page-state.d.ts +23 -0
- package/dist/page-state.js +96 -0
- package/dist/theme-data.d.ts +31 -0
- package/dist/theme-data.js +43 -0
- package/dist/theme.d.ts +19 -1
- package/dist/theme.js +72 -33
- package/dist/tokens.css +121 -0
- package/dist/tree.js +11 -5
- package/dist/virtual-list.js +28 -11
- package/package.json +17 -9
package/dist/theme-data.d.ts
CHANGED
|
@@ -66,11 +66,15 @@ export declare const primitiveTokenValues: {
|
|
|
66
66
|
readonly '--radius-10': "10px";
|
|
67
67
|
readonly '--radius-full': "9999px";
|
|
68
68
|
readonly '--spacer-0': "0px";
|
|
69
|
+
readonly '--spacer-2': "2px";
|
|
69
70
|
readonly '--spacer-4': "4px";
|
|
70
71
|
readonly '--spacer-6': "6px";
|
|
71
72
|
readonly '--spacer-8': "8px";
|
|
73
|
+
readonly '--spacer-10': "10px";
|
|
72
74
|
readonly '--spacer-12': "12px";
|
|
75
|
+
readonly '--spacer-14': "14px";
|
|
73
76
|
readonly '--spacer-16': "16px";
|
|
77
|
+
readonly '--spacer-20': "20px";
|
|
74
78
|
readonly '--spacer-24': "24px";
|
|
75
79
|
readonly '--spacer-32': "32px";
|
|
76
80
|
readonly '--spacer-40': "40px";
|
|
@@ -157,9 +161,11 @@ export declare const primitiveTokenValues: {
|
|
|
157
161
|
readonly '--code-terminal-font-weight': "450";
|
|
158
162
|
readonly '--code-terminal-line-height': "18px";
|
|
159
163
|
readonly '--font-weight-default': "400";
|
|
164
|
+
readonly '--font-weight-regular': "var(--font-weight-default)";
|
|
160
165
|
readonly '--font-weight-code': "450";
|
|
161
166
|
readonly '--font-weight-medium': "500";
|
|
162
167
|
readonly '--font-weight-strong': "600";
|
|
168
|
+
readonly '--font-weight-semibold': "var(--font-weight-strong)";
|
|
163
169
|
readonly '--z-index-dropdown': "2000";
|
|
164
170
|
readonly '--z-index-drawer': "2500";
|
|
165
171
|
readonly '--z-index-modal': "3000";
|
|
@@ -207,6 +213,9 @@ export declare const themeTokenValues: {
|
|
|
207
213
|
readonly '--color-warning': "var(--status-warning-default)";
|
|
208
214
|
readonly '--color-error': "var(--status-error-default)";
|
|
209
215
|
readonly '--color-error-soft': "var(--status-error-surface-l1)";
|
|
216
|
+
readonly '--color-foreground-muted': "var(--text-secondary)";
|
|
217
|
+
readonly '--accent-blue': "var(--status-primary-default)";
|
|
218
|
+
readonly '--code-string': "var(--code-text)";
|
|
210
219
|
readonly '--bg-base-default': "#1A1B1D";
|
|
211
220
|
readonly '--bg-base-secondary': "#222427";
|
|
212
221
|
readonly '--bg-base-tertiary': "#2A2D31";
|
|
@@ -321,25 +330,46 @@ export declare const themeTokenValues: {
|
|
|
321
330
|
readonly '--color-warning': "var(--status-warning-default)";
|
|
322
331
|
readonly '--color-error': "var(--status-error-default)";
|
|
323
332
|
readonly '--color-error-soft': "var(--status-error-surface-l1)";
|
|
333
|
+
readonly '--color-foreground-muted': "var(--text-secondary)";
|
|
334
|
+
readonly '--accent-blue': "var(--status-primary-default)";
|
|
335
|
+
readonly '--code-string': "var(--code-text)";
|
|
324
336
|
readonly '--bg-base-default': "#FFFFFF";
|
|
325
337
|
readonly '--bg-base-secondary': "#F5F6F8";
|
|
326
338
|
readonly '--bg-base-tertiary': "#EDF0F5";
|
|
339
|
+
readonly '--bg-tooltip': "#0F1117";
|
|
340
|
+
readonly '--bg-menu': "#FFFFFF";
|
|
341
|
+
readonly '--bg-invert': "#0F1117";
|
|
342
|
+
readonly '--bg-invert-hover': "#1F2937";
|
|
343
|
+
readonly '--bg-invert-active': "#374151";
|
|
344
|
+
readonly '--bg-invert-disabled': "rgba(15, 17, 23, 0.12)";
|
|
327
345
|
readonly '--bg-overlay-l1': "rgba(0, 0, 0, 0.04)";
|
|
328
346
|
readonly '--bg-overlay-l2': "rgba(0, 0, 0, 0.06)";
|
|
329
347
|
readonly '--bg-overlay-l3': "rgba(0, 0, 0, 0.08)";
|
|
330
348
|
readonly '--bg-overlay-l4': "rgba(0, 0, 0, 0.12)";
|
|
331
349
|
readonly '--text-default': "#0F1117";
|
|
350
|
+
readonly '--text-default-hover': "#000000";
|
|
351
|
+
readonly '--text-default-active': "#000000";
|
|
332
352
|
readonly '--text-secondary': "#4B5563";
|
|
353
|
+
readonly '--text-secondary-hover': "#1F2937";
|
|
354
|
+
readonly '--text-secondary-active': "#1F2937";
|
|
333
355
|
readonly '--text-tertiary': "#5B6370";
|
|
334
356
|
readonly '--text-disabled': "#525A68";
|
|
335
357
|
readonly '--text-onbrand': "#FFFFFF";
|
|
358
|
+
readonly '--text-onaccent': "#FFFFFF";
|
|
336
359
|
readonly '--icon-default': "#0F1117";
|
|
360
|
+
readonly '--icon-default-hover': "#000000";
|
|
361
|
+
readonly '--icon-default-active': "#000000";
|
|
337
362
|
readonly '--icon-secondary': "#4B5563";
|
|
363
|
+
readonly '--icon-secondary-hover': "#1F2937";
|
|
364
|
+
readonly '--icon-secondary-active': "#1F2937";
|
|
338
365
|
readonly '--icon-tertiary': "#5B6370";
|
|
339
366
|
readonly '--icon-disabled': "#525A68";
|
|
367
|
+
readonly '--icon-onbrand': "#FFFFFF";
|
|
368
|
+
readonly '--icon-onaccent': "#FFFFFF";
|
|
340
369
|
readonly '--border-neutral-l1': "rgba(0, 0, 0, 0.12)";
|
|
341
370
|
readonly '--border-neutral-l2': "rgba(0, 0, 0, 0.18)";
|
|
342
371
|
readonly '--border-neutral-l3': "rgba(0, 0, 0, 0.25)";
|
|
372
|
+
readonly '--border-contrast': "#0F1117";
|
|
343
373
|
readonly '--status-success-default': "#16A34A";
|
|
344
374
|
readonly '--status-warning-default': "#CA8A04";
|
|
345
375
|
readonly '--status-error-default': "#DC2626";
|
|
@@ -359,5 +389,6 @@ export interface BrandTokens {
|
|
|
359
389
|
}
|
|
360
390
|
export declare function resolveBrandColor(value?: BrandInput): BrandColor;
|
|
361
391
|
export declare function deriveBrandTokens(color: string): BrandTokens;
|
|
392
|
+
export declare function deriveBrandTextColor(color: string): string;
|
|
362
393
|
export declare function normalizeHexColor(value: string): string;
|
|
363
394
|
export declare function contrastRatio(left: string, right: string): number;
|
package/dist/theme-data.js
CHANGED
|
@@ -25,11 +25,15 @@ export const primitiveTokenValues = {
|
|
|
25
25
|
'--radius-10': '10px',
|
|
26
26
|
'--radius-full': '9999px',
|
|
27
27
|
'--spacer-0': '0px',
|
|
28
|
+
'--spacer-2': '2px',
|
|
28
29
|
'--spacer-4': '4px',
|
|
29
30
|
'--spacer-6': '6px',
|
|
30
31
|
'--spacer-8': '8px',
|
|
32
|
+
'--spacer-10': '10px',
|
|
31
33
|
'--spacer-12': '12px',
|
|
34
|
+
'--spacer-14': '14px',
|
|
32
35
|
'--spacer-16': '16px',
|
|
36
|
+
'--spacer-20': '20px',
|
|
33
37
|
'--spacer-24': '24px',
|
|
34
38
|
'--spacer-32': '32px',
|
|
35
39
|
'--spacer-40': '40px',
|
|
@@ -116,9 +120,11 @@ export const primitiveTokenValues = {
|
|
|
116
120
|
'--code-terminal-font-weight': '450',
|
|
117
121
|
'--code-terminal-line-height': '18px',
|
|
118
122
|
'--font-weight-default': '400',
|
|
123
|
+
'--font-weight-regular': 'var(--font-weight-default)',
|
|
119
124
|
'--font-weight-code': '450',
|
|
120
125
|
'--font-weight-medium': '500',
|
|
121
126
|
'--font-weight-strong': '600',
|
|
127
|
+
'--font-weight-semibold': 'var(--font-weight-strong)',
|
|
122
128
|
'--z-index-dropdown': '2000',
|
|
123
129
|
'--z-index-drawer': '2500',
|
|
124
130
|
'--z-index-modal': '3000',
|
|
@@ -165,6 +171,9 @@ const semanticAliases = {
|
|
|
165
171
|
'--color-warning': 'var(--status-warning-default)',
|
|
166
172
|
'--color-error': 'var(--status-error-default)',
|
|
167
173
|
'--color-error-soft': 'var(--status-error-surface-l1)',
|
|
174
|
+
'--color-foreground-muted': 'var(--text-secondary)',
|
|
175
|
+
'--accent-blue': 'var(--status-primary-default)',
|
|
176
|
+
'--code-string': 'var(--code-text)',
|
|
168
177
|
};
|
|
169
178
|
export const themeTokenValues = {
|
|
170
179
|
dark: {
|
|
@@ -254,22 +263,40 @@ export const themeTokenValues = {
|
|
|
254
263
|
'--bg-base-default': '#FFFFFF',
|
|
255
264
|
'--bg-base-secondary': '#F5F6F8',
|
|
256
265
|
'--bg-base-tertiary': '#EDF0F5',
|
|
266
|
+
'--bg-tooltip': '#0F1117',
|
|
267
|
+
'--bg-menu': '#FFFFFF',
|
|
268
|
+
'--bg-invert': '#0F1117',
|
|
269
|
+
'--bg-invert-hover': '#1F2937',
|
|
270
|
+
'--bg-invert-active': '#374151',
|
|
271
|
+
'--bg-invert-disabled': 'rgba(15, 17, 23, 0.12)',
|
|
257
272
|
'--bg-overlay-l1': 'rgba(0, 0, 0, 0.04)',
|
|
258
273
|
'--bg-overlay-l2': 'rgba(0, 0, 0, 0.06)',
|
|
259
274
|
'--bg-overlay-l3': 'rgba(0, 0, 0, 0.08)',
|
|
260
275
|
'--bg-overlay-l4': 'rgba(0, 0, 0, 0.12)',
|
|
261
276
|
'--text-default': '#0F1117',
|
|
277
|
+
'--text-default-hover': '#000000',
|
|
278
|
+
'--text-default-active': '#000000',
|
|
262
279
|
'--text-secondary': '#4B5563',
|
|
280
|
+
'--text-secondary-hover': '#1F2937',
|
|
281
|
+
'--text-secondary-active': '#1F2937',
|
|
263
282
|
'--text-tertiary': '#5B6370',
|
|
264
283
|
'--text-disabled': '#525A68',
|
|
265
284
|
'--text-onbrand': '#FFFFFF',
|
|
285
|
+
'--text-onaccent': '#FFFFFF',
|
|
266
286
|
'--icon-default': '#0F1117',
|
|
287
|
+
'--icon-default-hover': '#000000',
|
|
288
|
+
'--icon-default-active': '#000000',
|
|
267
289
|
'--icon-secondary': '#4B5563',
|
|
290
|
+
'--icon-secondary-hover': '#1F2937',
|
|
291
|
+
'--icon-secondary-active': '#1F2937',
|
|
268
292
|
'--icon-tertiary': '#5B6370',
|
|
269
293
|
'--icon-disabled': '#525A68',
|
|
294
|
+
'--icon-onbrand': '#FFFFFF',
|
|
295
|
+
'--icon-onaccent': '#FFFFFF',
|
|
270
296
|
'--border-neutral-l1': 'rgba(0, 0, 0, 0.12)',
|
|
271
297
|
'--border-neutral-l2': 'rgba(0, 0, 0, 0.18)',
|
|
272
298
|
'--border-neutral-l3': 'rgba(0, 0, 0, 0.25)',
|
|
299
|
+
'--border-contrast': '#0F1117',
|
|
273
300
|
'--status-success-default': '#16A34A',
|
|
274
301
|
'--status-warning-default': '#CA8A04',
|
|
275
302
|
'--status-error-default': '#DC2626',
|
|
@@ -319,6 +346,22 @@ export function deriveBrandTokens(color) {
|
|
|
319
346
|
: '#0C0C0D',
|
|
320
347
|
};
|
|
321
348
|
}
|
|
349
|
+
export function deriveBrandTextColor(color) {
|
|
350
|
+
const normalized = normalizeHexColor(color);
|
|
351
|
+
if (contrastRatio(normalized, '#FFFFFF') >= 4.5)
|
|
352
|
+
return normalized;
|
|
353
|
+
let low = 0;
|
|
354
|
+
let high = 1;
|
|
355
|
+
for (let index = 0; index < 24; index += 1) {
|
|
356
|
+
const amount = (low + high) / 2;
|
|
357
|
+
const candidate = mixHex(normalized, '#000000', amount);
|
|
358
|
+
if (contrastRatio(candidate, '#FFFFFF') >= 4.5)
|
|
359
|
+
high = amount;
|
|
360
|
+
else
|
|
361
|
+
low = amount;
|
|
362
|
+
}
|
|
363
|
+
return mixHex(normalized, '#000000', high);
|
|
364
|
+
}
|
|
322
365
|
export function normalizeHexColor(value) {
|
|
323
366
|
const input = value.trim();
|
|
324
367
|
const short = /^#?([0-9a-f]{3})$/iu.exec(input);
|
package/dist/theme.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @vobs/ui
|
|
4
4
|
*/
|
|
5
5
|
import { type BrandColor, type BrandInput, type ThemeName } from './theme-data.js';
|
|
6
|
-
export { brandPresets, contrastRatio, DEFAULT_BRAND_COLOR, DEFAULT_BRAND_ID, DEFAULT_THEME, deriveBrandTokens, normalizeHexColor, resolveBrandColor, type BrandColor, type BrandColorInput, type BrandInput, type BrandTokens, type ThemeName, } from './theme-data.js';
|
|
6
|
+
export { brandPresets, contrastRatio, DEFAULT_BRAND_COLOR, DEFAULT_BRAND_ID, DEFAULT_THEME, deriveBrandTokens, deriveBrandTextColor, normalizeHexColor, resolveBrandColor, type BrandColor, type BrandColorInput, type BrandInput, type BrandTokens, type ThemeName, } from './theme-data.js';
|
|
7
7
|
export interface ThemeStyleTarget {
|
|
8
8
|
setProperty(name: string, value: string): void;
|
|
9
9
|
removeProperty(name: string): string;
|
|
@@ -15,6 +15,7 @@ export interface ThemeTarget {
|
|
|
15
15
|
export interface ThemePersistence {
|
|
16
16
|
readTheme?(): unknown;
|
|
17
17
|
writeTheme?(theme: ThemeName): void;
|
|
18
|
+
writeSystemTheme?(): void;
|
|
18
19
|
readBrand?(): unknown;
|
|
19
20
|
writeBrand?(brand: BrandColor): void;
|
|
20
21
|
}
|
|
@@ -39,9 +40,24 @@ export interface ThemeState {
|
|
|
39
40
|
readonly theme: ThemeName;
|
|
40
41
|
readonly brand: BrandColor;
|
|
41
42
|
}
|
|
43
|
+
export type ThemePreference = ThemeName | 'system';
|
|
44
|
+
export declare const THEME_PREFERENCE_CHANGE_EVENT = "vobs:theme-preference-change";
|
|
45
|
+
export declare const THEME_STATE_CHANGE_EVENT = "vobs:theme-state-change";
|
|
46
|
+
export interface ThemePreferenceChangeDetail {
|
|
47
|
+
readonly shellId?: string;
|
|
48
|
+
readonly theme?: ThemePreference;
|
|
49
|
+
readonly brand?: BrandInput;
|
|
50
|
+
}
|
|
51
|
+
export interface ThemeStateChangeDetail {
|
|
52
|
+
readonly shellId: string;
|
|
53
|
+
readonly theme: ThemeName;
|
|
54
|
+
readonly preference: ThemePreference;
|
|
55
|
+
readonly brand: BrandColor;
|
|
56
|
+
}
|
|
42
57
|
export interface ThemeControllerOptions {
|
|
43
58
|
readonly target: ThemeTarget;
|
|
44
59
|
readonly initialTheme?: ThemeName;
|
|
60
|
+
readonly initialPreference?: ThemePreference;
|
|
45
61
|
readonly initialBrand?: BrandInput;
|
|
46
62
|
readonly persistence?: ThemePersistence;
|
|
47
63
|
readonly systemPreference?: SystemThemePreference;
|
|
@@ -49,9 +65,11 @@ export interface ThemeControllerOptions {
|
|
|
49
65
|
}
|
|
50
66
|
export interface ThemeController {
|
|
51
67
|
readonly theme: ThemeName;
|
|
68
|
+
readonly preference: ThemePreference;
|
|
52
69
|
readonly brand: BrandColor;
|
|
53
70
|
readonly state: ThemeState;
|
|
54
71
|
setTheme(theme: ThemeName): void;
|
|
72
|
+
setPreference(preference: ThemePreference): void;
|
|
55
73
|
toggleTheme(): ThemeName;
|
|
56
74
|
setBrand(brand: BrandInput): BrandColor;
|
|
57
75
|
subscribe(listener: (state: ThemeState) => void): () => void;
|
package/dist/theme.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Copyright (c) 2026 vobsjs
|
|
3
3
|
* @vobs/ui
|
|
4
4
|
*/
|
|
5
|
-
import { brandPresets, contrastRatio, DEFAULT_BRAND_ID, DEFAULT_THEME, deriveBrandTokens, resolveBrandColor, themeTokenValues, } from './theme-data.js';
|
|
6
|
-
export { brandPresets, contrastRatio, DEFAULT_BRAND_COLOR, DEFAULT_BRAND_ID, DEFAULT_THEME, deriveBrandTokens, normalizeHexColor, resolveBrandColor, } from './theme-data.js';
|
|
5
|
+
import { brandPresets, contrastRatio, DEFAULT_BRAND_ID, DEFAULT_THEME, deriveBrandTokens, deriveBrandTextColor, resolveBrandColor, themeTokenValues, } from './theme-data.js';
|
|
6
|
+
export { brandPresets, contrastRatio, DEFAULT_BRAND_COLOR, DEFAULT_BRAND_ID, DEFAULT_THEME, deriveBrandTokens, deriveBrandTextColor, normalizeHexColor, resolveBrandColor, } from './theme-data.js';
|
|
7
|
+
export const THEME_PREFERENCE_CHANGE_EVENT = 'vobs:theme-preference-change';
|
|
8
|
+
export const THEME_STATE_CHANGE_EVENT = 'vobs:theme-state-change';
|
|
7
9
|
const brandVariableNames = [
|
|
8
10
|
'--bg-brand',
|
|
9
11
|
'--bg-brand-hover',
|
|
@@ -26,24 +28,21 @@ const brandVariableNames = [
|
|
|
26
28
|
];
|
|
27
29
|
export function createThemeController(options) {
|
|
28
30
|
const listeners = new Set();
|
|
29
|
-
const
|
|
30
|
-
let
|
|
31
|
-
|
|
31
|
+
const persistedPreference = readPersistedTheme(options.persistence);
|
|
32
|
+
let preference = persistedPreference ??
|
|
33
|
+
options.initialPreference ??
|
|
34
|
+
(options.initialTheme === undefined && options.systemPreference !== undefined
|
|
35
|
+
? 'system'
|
|
36
|
+
: (options.initialTheme ?? DEFAULT_THEME));
|
|
37
|
+
let followsSystem = preference === 'system';
|
|
38
|
+
let theme = resolveTheme(preference, options);
|
|
32
39
|
let brand = readPersistedBrand(options.persistence, options.onWarning) ??
|
|
33
40
|
readBrand(options.initialBrand ?? DEFAULT_BRAND_ID, options.onWarning);
|
|
34
41
|
let stopSystemPreference;
|
|
35
42
|
let destroyed = false;
|
|
36
43
|
applyTheme(options.target, theme);
|
|
37
|
-
applyBrand(options.target, brand);
|
|
38
|
-
|
|
39
|
-
stopSystemPreference = options.systemPreference?.subscribe?.((nextTheme) => {
|
|
40
|
-
if (destroyed || !followsSystem)
|
|
41
|
-
return;
|
|
42
|
-
theme = nextTheme;
|
|
43
|
-
applyTheme(options.target, theme);
|
|
44
|
-
notify();
|
|
45
|
-
});
|
|
46
|
-
}
|
|
44
|
+
applyBrand(options.target, brand, theme);
|
|
45
|
+
subscribeToSystemPreference();
|
|
47
46
|
function state() {
|
|
48
47
|
return { theme, brand };
|
|
49
48
|
}
|
|
@@ -53,19 +52,43 @@ export function createThemeController(options) {
|
|
|
53
52
|
listener(value);
|
|
54
53
|
}
|
|
55
54
|
function setTheme(nextTheme) {
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
setPreference(nextTheme);
|
|
56
|
+
}
|
|
57
|
+
function setPreference(nextPreference) {
|
|
58
|
+
assertThemePreference(nextPreference);
|
|
59
|
+
followsSystem = nextPreference === 'system';
|
|
58
60
|
stopSystemPreference?.();
|
|
59
61
|
stopSystemPreference = undefined;
|
|
60
|
-
|
|
62
|
+
preference = nextPreference;
|
|
63
|
+
theme = resolveTheme(preference, options);
|
|
61
64
|
applyTheme(options.target, theme);
|
|
62
|
-
options.
|
|
65
|
+
applyBrand(options.target, brand, theme);
|
|
66
|
+
if (preference === 'system')
|
|
67
|
+
options.persistence?.writeSystemTheme?.();
|
|
68
|
+
else
|
|
69
|
+
options.persistence?.writeTheme?.(theme);
|
|
70
|
+
subscribeToSystemPreference();
|
|
63
71
|
notify();
|
|
64
72
|
}
|
|
73
|
+
function subscribeToSystemPreference() {
|
|
74
|
+
if (!followsSystem || options.systemPreference?.subscribe === undefined)
|
|
75
|
+
return;
|
|
76
|
+
stopSystemPreference = options.systemPreference.subscribe((nextTheme) => {
|
|
77
|
+
if (destroyed || !followsSystem)
|
|
78
|
+
return;
|
|
79
|
+
theme = nextTheme;
|
|
80
|
+
applyTheme(options.target, theme);
|
|
81
|
+
applyBrand(options.target, brand, theme);
|
|
82
|
+
notify();
|
|
83
|
+
});
|
|
84
|
+
}
|
|
65
85
|
return {
|
|
66
86
|
get theme() {
|
|
67
87
|
return theme;
|
|
68
88
|
},
|
|
89
|
+
get preference() {
|
|
90
|
+
return preference;
|
|
91
|
+
},
|
|
69
92
|
get brand() {
|
|
70
93
|
return brand;
|
|
71
94
|
},
|
|
@@ -73,6 +96,7 @@ export function createThemeController(options) {
|
|
|
73
96
|
return state();
|
|
74
97
|
},
|
|
75
98
|
setTheme,
|
|
99
|
+
setPreference,
|
|
76
100
|
toggleTheme() {
|
|
77
101
|
const nextTheme = theme === 'dark' ? 'light' : 'dark';
|
|
78
102
|
setTheme(nextTheme);
|
|
@@ -80,7 +104,7 @@ export function createThemeController(options) {
|
|
|
80
104
|
},
|
|
81
105
|
setBrand(nextBrand) {
|
|
82
106
|
brand = readBrand(nextBrand, options.onWarning);
|
|
83
|
-
applyBrand(options.target, brand);
|
|
107
|
+
applyBrand(options.target, brand, theme);
|
|
84
108
|
options.persistence?.writeBrand?.(brand);
|
|
85
109
|
notify();
|
|
86
110
|
return brand;
|
|
@@ -110,6 +134,9 @@ export function createStorageThemePersistence(storage, keys = {}) {
|
|
|
110
134
|
writeTheme(theme) {
|
|
111
135
|
storage.setItem(themeKey, theme);
|
|
112
136
|
},
|
|
137
|
+
writeSystemTheme() {
|
|
138
|
+
storage.setItem(themeKey, 'system');
|
|
139
|
+
},
|
|
113
140
|
readBrand() {
|
|
114
141
|
const stored = storage.getItem(brandKey);
|
|
115
142
|
if (stored === null)
|
|
@@ -148,18 +175,23 @@ export function createThemeAttributes(state) {
|
|
|
148
175
|
const attributes = {
|
|
149
176
|
'data-theme': state.theme,
|
|
150
177
|
'data-brand': state.brand.id,
|
|
151
|
-
...(isPresetBrand(state.brand) ? {} : { style: serializeBrandStyle(state.brand) }),
|
|
178
|
+
...(isPresetBrand(state.brand) ? {} : { style: serializeBrandStyle(state.brand, state.theme) }),
|
|
152
179
|
};
|
|
153
180
|
return attributes;
|
|
154
181
|
}
|
|
155
182
|
export function createThemeBootstrapScript(state) {
|
|
156
183
|
const attributes = createThemeAttributes(state);
|
|
157
|
-
const variables = isPresetBrand(state.brand) ? {} : brandCssVariables(state.brand);
|
|
184
|
+
const variables = isPresetBrand(state.brand) ? {} : brandCssVariables(state.brand, state.theme);
|
|
158
185
|
return `(function(){var d=document.documentElement;d.setAttribute("data-theme",${JSON.stringify(attributes['data-theme'])});d.setAttribute("data-brand",${JSON.stringify(attributes['data-brand'])});var v=${JSON.stringify(variables)};for(var k in v){d.style.setProperty(k,v[k])}})()`;
|
|
159
186
|
}
|
|
160
187
|
function readPersistedTheme(persistence) {
|
|
161
188
|
const value = persistence?.readTheme?.();
|
|
162
|
-
return value === 'dark' || value === 'light' ? value : undefined;
|
|
189
|
+
return value === 'dark' || value === 'light' || value === 'system' ? value : undefined;
|
|
190
|
+
}
|
|
191
|
+
function resolveTheme(preference, options) {
|
|
192
|
+
return preference === 'system'
|
|
193
|
+
? (options.systemPreference?.read() ?? options.initialTheme ?? DEFAULT_THEME)
|
|
194
|
+
: preference;
|
|
163
195
|
}
|
|
164
196
|
function readPersistedBrand(persistence, onWarning) {
|
|
165
197
|
const value = persistence?.readBrand?.();
|
|
@@ -191,30 +223,32 @@ function readBrand(value, onWarning) {
|
|
|
191
223
|
function applyTheme(target, theme) {
|
|
192
224
|
target.setAttribute('data-theme', theme);
|
|
193
225
|
}
|
|
194
|
-
function applyBrand(target, brand) {
|
|
226
|
+
function applyBrand(target, brand, theme) {
|
|
195
227
|
target.setAttribute('data-brand', brand.id);
|
|
196
228
|
if (isPresetBrand(brand)) {
|
|
197
229
|
for (const name of brandVariableNames)
|
|
198
230
|
target.style.removeProperty(name);
|
|
199
231
|
return;
|
|
200
232
|
}
|
|
201
|
-
for (const [name, value] of Object.entries(brandCssVariables(brand))) {
|
|
233
|
+
for (const [name, value] of Object.entries(brandCssVariables(brand, theme))) {
|
|
202
234
|
target.style.setProperty(name, value);
|
|
203
235
|
}
|
|
204
236
|
}
|
|
205
|
-
function brandCssVariables(brand) {
|
|
237
|
+
function brandCssVariables(brand, theme = DEFAULT_THEME) {
|
|
206
238
|
const tokens = deriveBrandTokens(brand.color);
|
|
239
|
+
const readable = theme === 'light' ? deriveBrandTextColor(brand.color) : tokens.background;
|
|
240
|
+
const readableHover = theme === 'light' ? deriveBrandTextColor(tokens.hover) : tokens.hover;
|
|
207
241
|
return {
|
|
208
242
|
'--bg-brand': tokens.background,
|
|
209
243
|
'--bg-brand-hover': tokens.hover,
|
|
210
244
|
'--bg-brand-disabled': tokens.disabled,
|
|
211
245
|
'--bg-brand-popup': tokens.soft,
|
|
212
246
|
'--bg-brand-overlay': tokens.soft,
|
|
213
|
-
'--text-brand':
|
|
214
|
-
'--text-brand-hover':
|
|
215
|
-
'--icon-brand':
|
|
216
|
-
'--icon-brand-hover':
|
|
217
|
-
'--border-brand':
|
|
247
|
+
'--text-brand': readable,
|
|
248
|
+
'--text-brand-hover': readableHover,
|
|
249
|
+
'--icon-brand': readable,
|
|
250
|
+
'--icon-brand-hover': readableHover,
|
|
251
|
+
'--border-brand': readable,
|
|
218
252
|
'--text-onbrand': tokens.onBrand,
|
|
219
253
|
'--icon-onbrand': tokens.onBrand,
|
|
220
254
|
'--color-primary': tokens.background,
|
|
@@ -225,8 +259,8 @@ function brandCssVariables(brand) {
|
|
|
225
259
|
'--color-on-primary': tokens.onBrand,
|
|
226
260
|
};
|
|
227
261
|
}
|
|
228
|
-
function serializeBrandStyle(brand) {
|
|
229
|
-
return Object.entries(brandCssVariables(brand))
|
|
262
|
+
function serializeBrandStyle(brand, theme) {
|
|
263
|
+
return Object.entries(brandCssVariables(brand, theme))
|
|
230
264
|
.map(([name, value]) => `${name}:${value}`)
|
|
231
265
|
.join(';');
|
|
232
266
|
}
|
|
@@ -245,6 +279,11 @@ function assertTheme(theme) {
|
|
|
245
279
|
if (theme !== 'dark' && theme !== 'light')
|
|
246
280
|
throw new TypeError(`Invalid theme: ${theme}`);
|
|
247
281
|
}
|
|
282
|
+
function assertThemePreference(preference) {
|
|
283
|
+
if (preference !== 'dark' && preference !== 'light' && preference !== 'system') {
|
|
284
|
+
throw new TypeError(`Invalid theme preference: ${preference}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
248
287
|
/**
|
|
249
288
|
* Reads a design-token value (CSS variable) for the active theme, for third-party adapters (contract ui-adapters.md §3.1).
|
|
250
289
|
* Data source: tokens.css variables on html[data-theme] (getComputedStyle reads the latest effective value).
|