@vobs/ui 0.2.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.
@@ -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).
package/dist/tokens.css CHANGED
@@ -8,11 +8,15 @@
8
8
  --radius-10: 10px;
9
9
  --radius-full: 9999px;
10
10
  --spacer-0: 0px;
11
+ --spacer-2: 2px;
11
12
  --spacer-4: 4px;
12
13
  --spacer-6: 6px;
13
14
  --spacer-8: 8px;
15
+ --spacer-10: 10px;
14
16
  --spacer-12: 12px;
17
+ --spacer-14: 14px;
15
18
  --spacer-16: 16px;
19
+ --spacer-20: 20px;
16
20
  --spacer-24: 24px;
17
21
  --spacer-32: 32px;
18
22
  --spacer-40: 40px;
@@ -99,9 +103,11 @@
99
103
  --code-terminal-font-weight: 450;
100
104
  --code-terminal-line-height: 18px;
101
105
  --font-weight-default: 400;
106
+ --font-weight-regular: var(--font-weight-default);
102
107
  --font-weight-code: 450;
103
108
  --font-weight-medium: 500;
104
109
  --font-weight-strong: 600;
110
+ --font-weight-semibold: var(--font-weight-strong);
105
111
  --z-index-dropdown: 2000;
106
112
  --z-index-drawer: 2500;
107
113
  --z-index-modal: 3000;
@@ -231,6 +237,9 @@
231
237
  --color-warning: var(--status-warning-default);
232
238
  --color-error: var(--status-error-default);
233
239
  --color-error-soft: var(--status-error-surface-l1);
240
+ --color-foreground-muted: var(--text-secondary);
241
+ --accent-blue: var(--status-primary-default);
242
+ --code-string: var(--code-text);
234
243
  }
235
244
 
236
245
  [data-theme="light"] {
@@ -238,22 +247,40 @@
238
247
  --bg-base-default: #FFFFFF;
239
248
  --bg-base-secondary: #F5F6F8;
240
249
  --bg-base-tertiary: #EDF0F5;
250
+ --bg-tooltip: #0F1117;
251
+ --bg-menu: #FFFFFF;
252
+ --bg-invert: #0F1117;
253
+ --bg-invert-hover: #1F2937;
254
+ --bg-invert-active: #374151;
255
+ --bg-invert-disabled: rgba(15, 17, 23, 0.12);
241
256
  --bg-overlay-l1: rgba(0, 0, 0, 0.04);
242
257
  --bg-overlay-l2: rgba(0, 0, 0, 0.06);
243
258
  --bg-overlay-l3: rgba(0, 0, 0, 0.08);
244
259
  --bg-overlay-l4: rgba(0, 0, 0, 0.12);
245
260
  --text-default: #0F1117;
261
+ --text-default-hover: #000000;
262
+ --text-default-active: #000000;
246
263
  --text-secondary: #4B5563;
264
+ --text-secondary-hover: #1F2937;
265
+ --text-secondary-active: #1F2937;
247
266
  --text-tertiary: #5B6370;
248
267
  --text-disabled: #525A68;
249
268
  --text-onbrand: #FFFFFF;
269
+ --text-onaccent: #FFFFFF;
250
270
  --icon-default: #0F1117;
271
+ --icon-default-hover: #000000;
272
+ --icon-default-active: #000000;
251
273
  --icon-secondary: #4B5563;
274
+ --icon-secondary-hover: #1F2937;
275
+ --icon-secondary-active: #1F2937;
252
276
  --icon-tertiary: #5B6370;
253
277
  --icon-disabled: #525A68;
278
+ --icon-onbrand: #FFFFFF;
279
+ --icon-onaccent: #FFFFFF;
254
280
  --border-neutral-l1: rgba(0, 0, 0, 0.12);
255
281
  --border-neutral-l2: rgba(0, 0, 0, 0.18);
256
282
  --border-neutral-l3: rgba(0, 0, 0, 0.25);
283
+ --border-contrast: #0F1117;
257
284
  --status-success-default: #16A34A;
258
285
  --status-warning-default: #CA8A04;
259
286
  --status-error-default: #DC2626;
@@ -294,6 +321,9 @@
294
321
  --color-warning: var(--status-warning-default);
295
322
  --color-error: var(--status-error-default);
296
323
  --color-error-soft: var(--status-error-surface-l1);
324
+ --color-foreground-muted: var(--text-secondary);
325
+ --accent-blue: var(--status-primary-default);
326
+ --code-string: var(--code-text);
297
327
  }
298
328
 
299
329
  :root,
@@ -312,6 +342,16 @@
312
342
  --icon-onbrand: #0C0C0D;
313
343
  }
314
344
 
345
+ [data-theme="light"],
346
+ [data-theme="light"][data-brand="green"],
347
+ [data-theme="light"] [data-brand="green"] {
348
+ --text-brand: #1C874F;
349
+ --text-brand-hover: #09884A;
350
+ --icon-brand: #1C874F;
351
+ --icon-brand-hover: #09884A;
352
+ --border-brand: #1C874F;
353
+ }
354
+
315
355
  [data-brand="blue"] {
316
356
  --bg-brand: #80C1FF;
317
357
  --bg-brand-hover: #6EA6DB;
@@ -327,6 +367,15 @@
327
367
  --icon-onbrand: #0C0C0D;
328
368
  }
329
369
 
370
+ [data-theme="light"][data-brand="blue"],
371
+ [data-theme="light"] [data-brand="blue"] {
372
+ --text-brand: #517AA2;
373
+ --text-brand-hover: #517AA2;
374
+ --icon-brand: #517AA2;
375
+ --icon-brand-hover: #517AA2;
376
+ --border-brand: #517AA2;
377
+ }
378
+
330
379
  [data-brand="purple"] {
331
380
  --bg-brand: #8E80FF;
332
381
  --bg-brand-hover: #9E92FF;
@@ -342,6 +391,15 @@
342
391
  --icon-onbrand: #0C0C0D;
343
392
  }
344
393
 
394
+ [data-theme="light"][data-brand="purple"],
395
+ [data-theme="light"] [data-brand="purple"] {
396
+ --text-brand: #7468D0;
397
+ --text-brand-hover: #756CBE;
398
+ --icon-brand: #7468D0;
399
+ --icon-brand-hover: #756CBE;
400
+ --border-brand: #7468D0;
401
+ }
402
+
345
403
  [data-brand="yellow"] {
346
404
  --bg-brand: #FFD080;
347
405
  --bg-brand-hover: #DBB36E;
@@ -357,6 +415,15 @@
357
415
  --icon-onbrand: #0C0C0D;
358
416
  }
359
417
 
418
+ [data-theme="light"][data-brand="yellow"],
419
+ [data-theme="light"] [data-brand="yellow"] {
420
+ --text-brand: #8C7347;
421
+ --text-brand-hover: #8C7347;
422
+ --icon-brand: #8C7347;
423
+ --icon-brand-hover: #8C7347;
424
+ --border-brand: #8C7347;
425
+ }
426
+
360
427
  [data-brand="red"] {
361
428
  --bg-brand: #FF8080;
362
429
  --bg-brand-hover: #DB6E6E;
@@ -372,6 +439,15 @@
372
439
  --icon-onbrand: #0C0C0D;
373
440
  }
374
441
 
442
+ [data-theme="light"][data-brand="red"],
443
+ [data-theme="light"] [data-brand="red"] {
444
+ --text-brand: #B65B5B;
445
+ --text-brand-hover: #B65B5B;
446
+ --icon-brand: #B65B5B;
447
+ --icon-brand-hover: #B65B5B;
448
+ --border-brand: #B65B5B;
449
+ }
450
+
375
451
  [data-brand="teal"] {
376
452
  --bg-brand: #2DD288;
377
453
  --bg-brand-hover: #27B575;
@@ -387,6 +463,15 @@
387
463
  --icon-onbrand: #0C0C0D;
388
464
  }
389
465
 
466
+ [data-theme="light"][data-brand="teal"],
467
+ [data-theme="light"] [data-brand="teal"] {
468
+ --text-brand: #1D8758;
469
+ --text-brand-hover: #1D8758;
470
+ --icon-brand: #1D8758;
471
+ --icon-brand-hover: #1D8758;
472
+ --border-brand: #1D8758;
473
+ }
474
+
390
475
  [data-brand="cyan"] {
391
476
  --bg-brand: #04CBE5;
392
477
  --bg-brand-hover: #27D2E9;
@@ -402,6 +487,15 @@
402
487
  --icon-onbrand: #0C0C0D;
403
488
  }
404
489
 
490
+ [data-theme="light"][data-brand="cyan"],
491
+ [data-theme="light"] [data-brand="cyan"] {
492
+ --text-brand: #038293;
493
+ --text-brand-hover: #188291;
494
+ --icon-brand: #038293;
495
+ --icon-brand-hover: #188291;
496
+ --border-brand: #038293;
497
+ }
498
+
405
499
  [data-brand="violet"] {
406
500
  --bg-brand: #BFA5FF;
407
501
  --bg-brand-hover: #A48EDB;
@@ -417,6 +511,15 @@
417
511
  --icon-onbrand: #0C0C0D;
418
512
  }
419
513
 
514
+ [data-theme="light"][data-brand="violet"],
515
+ [data-theme="light"] [data-brand="violet"] {
516
+ --text-brand: #7F6DA9;
517
+ --text-brand-hover: #7E6DA9;
518
+ --icon-brand: #7F6DA9;
519
+ --icon-brand-hover: #7E6DA9;
520
+ --border-brand: #7F6DA9;
521
+ }
522
+
420
523
  [data-brand="slate"] {
421
524
  --bg-brand: #AFB9CF;
422
525
  --bg-brand-hover: #979FB2;
@@ -432,6 +535,15 @@
432
535
  --icon-onbrand: #0C0C0D;
433
536
  }
434
537
 
538
+ [data-theme="light"][data-brand="slate"],
539
+ [data-theme="light"] [data-brand="slate"] {
540
+ --text-brand: #707785;
541
+ --text-brand-hover: #717685;
542
+ --icon-brand: #707785;
543
+ --icon-brand-hover: #717685;
544
+ --border-brand: #707785;
545
+ }
546
+
435
547
  [data-brand="neutral"] {
436
548
  --bg-brand: #BABABA;
437
549
  --bg-brand-hover: #A0A0A0;
@@ -446,4 +558,13 @@
446
558
  --text-onbrand: #0C0C0D;
447
559
  --icon-onbrand: #0C0C0D;
448
560
  }
561
+
562
+ [data-theme="light"][data-brand="neutral"],
563
+ [data-theme="light"] [data-brand="neutral"] {
564
+ --text-brand: #767676;
565
+ --text-brand-hover: #767676;
566
+ --icon-brand: #767676;
567
+ --icon-brand-hover: #767676;
568
+ --border-brand: #767676;
569
+ }
449
570
  }
package/dist/tree.js CHANGED
@@ -102,13 +102,17 @@ export function createTreeLazyLoadStateContract(options) {
102
102
  const loadedIds = normalizeTreeIds(options.loadedIds, knownIds);
103
103
  const errorIds = normalizeTreeIds(options.errorIds, knownIds);
104
104
  const retryableIds = normalizeTreeIds(options.retryableIds, knownIds);
105
+ const loadingSet = new Set(loadingIds);
106
+ const loadedSet = new Set(loadedIds);
107
+ const errorSet = new Set(errorIds);
108
+ const retryableSet = new Set(retryableIds);
105
109
  const nodeContracts = options.nodes.map((node) => treeLazyLoadNodeContract(node, {
106
110
  disabled,
107
111
  error: options.errors?.[node.id],
108
- loading: loadingIds.includes(node.id),
109
- loaded: loadedIds.includes(node.id),
110
- errored: errorIds.includes(node.id),
111
- retryable: retryableIds.includes(node.id),
112
+ loading: loadingSet.has(node.id),
113
+ loaded: loadedSet.has(node.id),
114
+ errored: errorSet.has(node.id),
115
+ retryable: retryableSet.has(node.id),
112
116
  }));
113
117
  const nodes = options.nodes.map((node, index) => treeNodeWithLazyContract(node, nodeContracts[index]));
114
118
  const loadableCount = nodeContracts.filter((node) => node.lazy).length;
@@ -405,9 +409,11 @@ function normalizeTreeIds(ids, knownIds) {
405
409
  if (ids === undefined)
406
410
  return [];
407
411
  const result = [];
412
+ const seen = new Set();
408
413
  for (const id of ids) {
409
- if (!knownIds.has(id) || result.includes(id))
414
+ if (!knownIds.has(id) || seen.has(id))
410
415
  continue;
416
+ seen.add(id);
411
417
  result.push(id);
412
418
  }
413
419
  return result;