@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.
@@ -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;
@@ -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 persistedTheme = readPersistedTheme(options.persistence);
30
- let followsSystem = persistedTheme === undefined && options.systemPreference !== undefined;
31
- let theme = persistedTheme ?? options.systemPreference?.read() ?? options.initialTheme ?? DEFAULT_THEME;
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
- if (followsSystem) {
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
- assertTheme(nextTheme);
57
- followsSystem = false;
55
+ setPreference(nextTheme);
56
+ }
57
+ function setPreference(nextPreference) {
58
+ assertThemePreference(nextPreference);
59
+ followsSystem = nextPreference === 'system';
58
60
  stopSystemPreference?.();
59
61
  stopSystemPreference = undefined;
60
- theme = nextTheme;
62
+ preference = nextPreference;
63
+ theme = resolveTheme(preference, options);
61
64
  applyTheme(options.target, theme);
62
- options.persistence?.writeTheme?.(theme);
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': tokens.background,
214
- '--text-brand-hover': tokens.hover,
215
- '--icon-brand': tokens.background,
216
- '--icon-brand-hover': tokens.hover,
217
- '--border-brand': tokens.background,
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).