@adea-ai/ui 0.28.0 → 0.30.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/components/appearance.d.ts +245 -0
- package/dist/components/appearance.js +768 -0
- package/dist/components/theme-provider.d.ts +35 -10
- package/dist/components/theme-provider.js +86 -55
- package/dist/components/theme-swatch.d.ts +12 -0
- package/dist/components/theme-swatch.js +66 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -1
- package/package.json +2 -1
- package/src/components/appearance.ts +1055 -0
- package/src/components/theme-provider.tsx +150 -66
- package/src/components/theme-swatch.tsx +49 -0
- package/src/index.tsx +1 -0
- package/src/styles/conventional-workspace.css +63 -2
- package/src/styles/theme.css +122 -0
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
//#region src/components/appearance.ts
|
|
2
|
+
var APPEARANCE_STORAGE_KEY = "appearance";
|
|
3
|
+
/** The pre-#425 key. Migration reads it and never deletes it. */
|
|
4
|
+
var LEGACY_THEME_STORAGE_KEY = "theme";
|
|
5
|
+
var DARK_QUERY = "(prefers-color-scheme: dark)";
|
|
6
|
+
var REDUCED_TRANSPARENCY_QUERY = "(prefers-reduced-transparency: reduce)";
|
|
7
|
+
var defaultAppearancePreferences = Object.freeze({
|
|
8
|
+
version: 2,
|
|
9
|
+
mode: "system",
|
|
10
|
+
lightThemeId: "adea-light",
|
|
11
|
+
darkThemeId: "adea-dark",
|
|
12
|
+
accent: "theme",
|
|
13
|
+
surface: "opaque",
|
|
14
|
+
reduceTransparency: false
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Combine the user's choice with the OS state. A pinned mode ignores the OS;
|
|
18
|
+
* `system` follows it. (Zeron `resolve`.)
|
|
19
|
+
*/
|
|
20
|
+
function resolveAppearanceMode(mode, system) {
|
|
21
|
+
if (mode === "system") return system;
|
|
22
|
+
return mode;
|
|
23
|
+
}
|
|
24
|
+
var ColorParseError = class extends Error {
|
|
25
|
+
constructor() {
|
|
26
|
+
super("expected a CSS hex color (#rgb, #rgba, #rrggbb, or #rrggbbaa)");
|
|
27
|
+
this.name = "ColorParseError";
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var NIBBLES = {
|
|
31
|
+
"0": 0,
|
|
32
|
+
"1": 1,
|
|
33
|
+
"2": 2,
|
|
34
|
+
"3": 3,
|
|
35
|
+
"4": 4,
|
|
36
|
+
"5": 5,
|
|
37
|
+
"6": 6,
|
|
38
|
+
"7": 7,
|
|
39
|
+
"8": 8,
|
|
40
|
+
"9": 9,
|
|
41
|
+
a: 10,
|
|
42
|
+
b: 11,
|
|
43
|
+
c: 12,
|
|
44
|
+
d: 13,
|
|
45
|
+
e: 14,
|
|
46
|
+
f: 15,
|
|
47
|
+
A: 10,
|
|
48
|
+
B: 11,
|
|
49
|
+
C: 12,
|
|
50
|
+
D: 13,
|
|
51
|
+
E: 14,
|
|
52
|
+
F: 15
|
|
53
|
+
};
|
|
54
|
+
function expandNibble(nibble) {
|
|
55
|
+
return nibble << 4 | nibble;
|
|
56
|
+
}
|
|
57
|
+
function parseByte(high, low) {
|
|
58
|
+
return high << 4 | low;
|
|
59
|
+
}
|
|
60
|
+
/** Parse a hex color. Returns `undefined` for anything unsupported. */
|
|
61
|
+
function parseColor(value) {
|
|
62
|
+
const trimmed = value.trim();
|
|
63
|
+
if (!trimmed.startsWith("#")) return void 0;
|
|
64
|
+
const bytes = trimmed.slice(1);
|
|
65
|
+
if (bytes.length === 3 || bytes.length === 4) {
|
|
66
|
+
const nibbles = [];
|
|
67
|
+
for (const character of bytes) {
|
|
68
|
+
const nibble = NIBBLES[character];
|
|
69
|
+
if (nibble === void 0) return void 0;
|
|
70
|
+
nibbles.push(nibble);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
r: expandNibble(nibbles[0]),
|
|
74
|
+
g: expandNibble(nibbles[1]),
|
|
75
|
+
b: expandNibble(nibbles[2]),
|
|
76
|
+
a: nibbles.length === 4 ? expandNibble(nibbles[3]) : 255
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (bytes.length === 6 || bytes.length === 8) {
|
|
80
|
+
const nibbles = [];
|
|
81
|
+
for (const character of bytes) {
|
|
82
|
+
const nibble = NIBBLES[character];
|
|
83
|
+
if (nibble === void 0) return void 0;
|
|
84
|
+
nibbles.push(nibble);
|
|
85
|
+
}
|
|
86
|
+
const pair = (index) => parseByte(nibbles[index], nibbles[index + 1]);
|
|
87
|
+
return {
|
|
88
|
+
r: pair(0),
|
|
89
|
+
g: pair(2),
|
|
90
|
+
b: pair(4),
|
|
91
|
+
a: nibbles.length === 8 ? pair(6) : 255
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function toHexByte(value) {
|
|
96
|
+
return value.toString(16).padStart(2, "0");
|
|
97
|
+
}
|
|
98
|
+
function colorToHex(color) {
|
|
99
|
+
if (color.a === 255) return `#${toHexByte(color.r)}${toHexByte(color.g)}${toHexByte(color.b)}`;
|
|
100
|
+
return `#${toHexByte(color.r)}${toHexByte(color.g)}${toHexByte(color.b)}${toHexByte(color.a)}`;
|
|
101
|
+
}
|
|
102
|
+
function blendOver(foreground, background) {
|
|
103
|
+
const alpha = foreground.a / 255;
|
|
104
|
+
const blend = (front, back) => Math.round(front * alpha + back * (1 - alpha));
|
|
105
|
+
return {
|
|
106
|
+
r: blend(foreground.r, background.r),
|
|
107
|
+
g: blend(foreground.g, background.g),
|
|
108
|
+
b: blend(foreground.b, background.b),
|
|
109
|
+
a: 255
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function mixColors(left, right, amount) {
|
|
113
|
+
const clamped = Math.min(1, Math.max(0, amount));
|
|
114
|
+
const mix = (a, b) => Math.round(a + (b - a) * clamped);
|
|
115
|
+
return {
|
|
116
|
+
r: mix(left.r, right.r),
|
|
117
|
+
g: mix(left.g, right.g),
|
|
118
|
+
b: mix(left.b, right.b),
|
|
119
|
+
a: 255
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function linearChannel(channel) {
|
|
123
|
+
const value = channel / 255;
|
|
124
|
+
return value <= .04045 ? value / 12.92 : ((value + .055) / 1.055) ** 2.4;
|
|
125
|
+
}
|
|
126
|
+
function relativeLuminance(color) {
|
|
127
|
+
return .2126 * linearChannel(color.r) + .7152 * linearChannel(color.g) + .0722 * linearChannel(color.b);
|
|
128
|
+
}
|
|
129
|
+
/** WCAG contrast ratio between two colors. Alpha composites over the background first. */
|
|
130
|
+
function contrastRatio(foreground, background) {
|
|
131
|
+
const front = typeof foreground === "string" ? parseColor(foreground) : foreground;
|
|
132
|
+
const back = typeof background === "string" ? parseColor(background) : background;
|
|
133
|
+
if (!front || !back) return 0;
|
|
134
|
+
const a = relativeLuminance(front.a === 255 ? front : blendOver(front, back));
|
|
135
|
+
const b = relativeLuminance(back);
|
|
136
|
+
return (Math.max(a, b) + .05) / (Math.min(a, b) + .05);
|
|
137
|
+
}
|
|
138
|
+
function bestOnColor(color) {
|
|
139
|
+
return contrastRatio(WHITE, color) >= contrastRatio(BLACK, color) ? WHITE : BLACK;
|
|
140
|
+
}
|
|
141
|
+
var WHITE = {
|
|
142
|
+
r: 255,
|
|
143
|
+
g: 255,
|
|
144
|
+
b: 255,
|
|
145
|
+
a: 255
|
|
146
|
+
};
|
|
147
|
+
var BLACK = {
|
|
148
|
+
r: 0,
|
|
149
|
+
g: 0,
|
|
150
|
+
b: 0,
|
|
151
|
+
a: 255
|
|
152
|
+
};
|
|
153
|
+
/** Move toward black or white until the requested contrast is met. (Zeron `ensure_contrast`.) */
|
|
154
|
+
function ensureContrast(color, background, minimum) {
|
|
155
|
+
if (contrastRatio(color, background) >= minimum) return color;
|
|
156
|
+
const target = contrastRatio(BLACK, background) >= contrastRatio(WHITE, background) ? BLACK : WHITE;
|
|
157
|
+
for (let step = 1; step <= 20; step++) {
|
|
158
|
+
const candidate = mixColors(color, target, step / 20);
|
|
159
|
+
if (contrastRatio(candidate, background) >= minimum) return candidate;
|
|
160
|
+
}
|
|
161
|
+
return target;
|
|
162
|
+
}
|
|
163
|
+
var accentPresets = Object.freeze([
|
|
164
|
+
{
|
|
165
|
+
id: "violet",
|
|
166
|
+
label: "Violet",
|
|
167
|
+
dark: "#a78bfa",
|
|
168
|
+
light: "#6d28d9"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: "blue",
|
|
172
|
+
label: "Blue",
|
|
173
|
+
dark: "#60a5fa",
|
|
174
|
+
light: "#2563eb"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: "green",
|
|
178
|
+
label: "Green",
|
|
179
|
+
dark: "#4ade80",
|
|
180
|
+
light: "#15803d"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: "amber",
|
|
184
|
+
label: "Amber",
|
|
185
|
+
dark: "#fbbf24",
|
|
186
|
+
light: "#b45309"
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: "cyan",
|
|
190
|
+
label: "Cyan",
|
|
191
|
+
dark: "#22d3ee",
|
|
192
|
+
light: "#0e7490"
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: "pink",
|
|
196
|
+
label: "Pink",
|
|
197
|
+
dark: "#f472b6",
|
|
198
|
+
light: "#be185d"
|
|
199
|
+
}
|
|
200
|
+
]);
|
|
201
|
+
/**
|
|
202
|
+
* Normalize a custom accent color against a variant background: unparseable
|
|
203
|
+
* input is rejected (`undefined`), and any accepted color is raised to the
|
|
204
|
+
* 3:1 interaction minimum so an accent can never ship unreadable.
|
|
205
|
+
*/
|
|
206
|
+
function normalizeAccentValue(value, background) {
|
|
207
|
+
const parsed = parseColor(value);
|
|
208
|
+
if (!parsed) return void 0;
|
|
209
|
+
return colorToHex(ensureContrast(parsed, parseColor(background) ?? WHITE, 3));
|
|
210
|
+
}
|
|
211
|
+
function accentPresetById(id) {
|
|
212
|
+
return accentPresets.find((preset) => preset.id === id);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Derive the accent roles for a selection against a variant. `'theme'` keeps
|
|
216
|
+
* the variant's own accent; presets and custom colors are normalized to the
|
|
217
|
+
* 3:1 interaction minimum (Zeron `AccentRoles::derive`, thresholds preserved).
|
|
218
|
+
*/
|
|
219
|
+
function deriveAccentRoles(selection, variant) {
|
|
220
|
+
const background = variant.colors.background;
|
|
221
|
+
let primary = variant.colors.primary;
|
|
222
|
+
let overrides = false;
|
|
223
|
+
if (selection !== "theme") {
|
|
224
|
+
const preset = accentPresetById(selection);
|
|
225
|
+
const requested = preset ? parseColor(variant.appearance === "dark" ? preset.dark : preset.light) : parseColor(selection);
|
|
226
|
+
if (requested) {
|
|
227
|
+
primary = colorToHex(ensureContrast(requested, parseColor(background), 3));
|
|
228
|
+
overrides = true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const primaryRgb = parseColor(primary);
|
|
232
|
+
const onPrimary = colorToHex(bestOnColor(primaryRgb));
|
|
233
|
+
let strong = primary;
|
|
234
|
+
if (contrastRatio(onPrimary, strong) < 4.5) strong = colorToHex(ensureContrast(primaryRgb, parseColor(onPrimary), 4.5));
|
|
235
|
+
return {
|
|
236
|
+
primary,
|
|
237
|
+
onPrimary,
|
|
238
|
+
strong,
|
|
239
|
+
ring: strong,
|
|
240
|
+
overrides
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function resolveSurface(preference, environment) {
|
|
244
|
+
if (environment.osReducedTransparency || environment.userReducedTransparency) return "opaque";
|
|
245
|
+
if (preference === "translucent" && !environment.nativeTranslucency) return "frosted";
|
|
246
|
+
return preference;
|
|
247
|
+
}
|
|
248
|
+
function themeSelectionVariantId(selection, appearance) {
|
|
249
|
+
return appearance === "dark" ? selection.darkThemeId : selection.lightThemeId;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Registry validation, ported from Zeron `ThemeRegistry::validate`: core text
|
|
253
|
+
* and muted text must reach 4.5:1, the accent 3:1, on-accent 4.5:1, the
|
|
254
|
+
* terminal foreground 4.5:1, and every chromatic ANSI slot 3:1 (slots 0 and 8
|
|
255
|
+
* are structural black/dim colors). Provenance completeness is a Zeron
|
|
256
|
+
* library concept; M12 ships only built-ins, so the structural check here is
|
|
257
|
+
* id/non-empty-color integrity.
|
|
258
|
+
*/
|
|
259
|
+
function validateThemeRegistry(registry) {
|
|
260
|
+
const issues = [];
|
|
261
|
+
const seen = /* @__PURE__ */ new Set();
|
|
262
|
+
for (const variant of registry) {
|
|
263
|
+
if (variant.id.trim().length === 0 || seen.has(variant.id)) issues.push({
|
|
264
|
+
variantId: variant.id,
|
|
265
|
+
severity: "error",
|
|
266
|
+
message: "variant id must be unique"
|
|
267
|
+
});
|
|
268
|
+
seen.add(variant.id);
|
|
269
|
+
const check = (role, foreground, background, minimum) => {
|
|
270
|
+
const actual = contrastRatio(foreground, background);
|
|
271
|
+
if (actual < minimum) issues.push({
|
|
272
|
+
variantId: variant.id,
|
|
273
|
+
severity: "error",
|
|
274
|
+
message: `${role} contrast is ${actual.toFixed(2)}:1; expected ${minimum.toFixed(1)}:1`
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
check("text", variant.colors.foreground, variant.colors.background, 4.5);
|
|
278
|
+
check("muted text", variant.colors.mutedForeground, variant.colors.background, 4.5);
|
|
279
|
+
check("primary", variant.colors.primary, variant.colors.background, 3);
|
|
280
|
+
check("on-primary", variant.colors.primaryForeground, variant.colors.primary, 4.5);
|
|
281
|
+
check("terminal foreground", variant.terminal.foreground, variant.terminal.background, 4.5);
|
|
282
|
+
for (const [index, color] of variant.terminal.ansi.entries()) {
|
|
283
|
+
if (index % 8 === 0) continue;
|
|
284
|
+
if (contrastRatio(color, variant.terminal.background) < 3) issues.push({
|
|
285
|
+
variantId: variant.id,
|
|
286
|
+
severity: "warning",
|
|
287
|
+
message: `terminal ANSI slot ${index} is below 3:1`
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
check("editor comment", variant.editor.comment, variant.colors.background, 4.5);
|
|
291
|
+
check("diff delete", variant.editor.diffDelete, variant.colors.background, 3);
|
|
292
|
+
check("diff add", variant.editor.diffAdd, variant.colors.background, 3);
|
|
293
|
+
}
|
|
294
|
+
return issues;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Resolve the variant for a selection, falling back deterministically to the
|
|
298
|
+
* default variant of the same appearance when the stored id is missing or
|
|
299
|
+
* broken — a corrupt preference degrades the palette, never the UI.
|
|
300
|
+
* (Zeron `ThemeRegistry::resolve`.)
|
|
301
|
+
*/
|
|
302
|
+
function resolveThemeVariant(registry, selection, appearance) {
|
|
303
|
+
const wanted = themeSelectionVariantId(selection, appearance);
|
|
304
|
+
const found = registry.find((variant) => variant.id === wanted);
|
|
305
|
+
if (found) return found;
|
|
306
|
+
const fallbackId = appearance === "dark" ? defaultAppearancePreferences.darkThemeId : defaultAppearancePreferences.lightThemeId;
|
|
307
|
+
const fallback = registry.find((variant) => variant.id === fallbackId);
|
|
308
|
+
if (fallback) return fallback;
|
|
309
|
+
const sameAppearance = registry.find((variant) => variant.appearance === appearance);
|
|
310
|
+
if (sameAppearance) return sameAppearance;
|
|
311
|
+
return registry[0];
|
|
312
|
+
}
|
|
313
|
+
var lightTerminal = Object.freeze({
|
|
314
|
+
background: "#ffffff",
|
|
315
|
+
foreground: "#1b1f24",
|
|
316
|
+
cursor: "#24292f",
|
|
317
|
+
selection: "#b6c7ff",
|
|
318
|
+
ansi: Object.freeze([
|
|
319
|
+
"#1b1f24",
|
|
320
|
+
"#b91c1c",
|
|
321
|
+
"#116a2e",
|
|
322
|
+
"#8a5a1b",
|
|
323
|
+
"#0b57d0",
|
|
324
|
+
"#a0186f",
|
|
325
|
+
"#0e7490",
|
|
326
|
+
"#57606a",
|
|
327
|
+
"#57606a",
|
|
328
|
+
"#c94d4d",
|
|
329
|
+
"#1f9d4f",
|
|
330
|
+
"#a9752c",
|
|
331
|
+
"#3b82f6",
|
|
332
|
+
"#c04a92",
|
|
333
|
+
"#0891b2",
|
|
334
|
+
"#24292f"
|
|
335
|
+
])
|
|
336
|
+
});
|
|
337
|
+
var darkTerminal = Object.freeze({
|
|
338
|
+
background: "#0d1117",
|
|
339
|
+
foreground: "#e6edf3",
|
|
340
|
+
cursor: "#e6edf3",
|
|
341
|
+
selection: "#264f78",
|
|
342
|
+
ansi: Object.freeze([
|
|
343
|
+
"#2f3742",
|
|
344
|
+
"#ff8183",
|
|
345
|
+
"#56d364",
|
|
346
|
+
"#e3b341",
|
|
347
|
+
"#6ca4f8",
|
|
348
|
+
"#db61a2",
|
|
349
|
+
"#39c5cf",
|
|
350
|
+
"#d5dde5",
|
|
351
|
+
"#57606a",
|
|
352
|
+
"#ff9494",
|
|
353
|
+
"#79dd8a",
|
|
354
|
+
"#f0c264",
|
|
355
|
+
"#8db9ff",
|
|
356
|
+
"#e87cb4",
|
|
357
|
+
"#66d3dc",
|
|
358
|
+
"#eef2f6"
|
|
359
|
+
])
|
|
360
|
+
});
|
|
361
|
+
var lightEditor = Object.freeze({
|
|
362
|
+
keyword: "#0b57d0",
|
|
363
|
+
string: "#116a2e",
|
|
364
|
+
number: "#8a5a1b",
|
|
365
|
+
comment: "#57606a",
|
|
366
|
+
function: "#a0186f",
|
|
367
|
+
variable: "#1b1f24",
|
|
368
|
+
type: "#0e7490",
|
|
369
|
+
tag: "#b91c1c",
|
|
370
|
+
attribute: "#8a5a1b",
|
|
371
|
+
operator: "#1b1f24",
|
|
372
|
+
heading: "#1b1f24",
|
|
373
|
+
link: "#0b57d0",
|
|
374
|
+
diffAdd: "#116a2e",
|
|
375
|
+
diffDelete: "#b91c1c",
|
|
376
|
+
diffHunk: "#57606a",
|
|
377
|
+
searchMatch: "#8a5a1b"
|
|
378
|
+
});
|
|
379
|
+
var darkEditor = Object.freeze({
|
|
380
|
+
keyword: "#6ca4f8",
|
|
381
|
+
string: "#56d364",
|
|
382
|
+
number: "#e3b341",
|
|
383
|
+
comment: "#8b949e",
|
|
384
|
+
function: "#db61a2",
|
|
385
|
+
variable: "#e6edf3",
|
|
386
|
+
type: "#39c5cf",
|
|
387
|
+
tag: "#ff8183",
|
|
388
|
+
attribute: "#e3b341",
|
|
389
|
+
operator: "#e6edf3",
|
|
390
|
+
heading: "#e6edf3",
|
|
391
|
+
link: "#6ca4f8",
|
|
392
|
+
diffAdd: "#56d364",
|
|
393
|
+
diffDelete: "#ff8183",
|
|
394
|
+
diffHunk: "#8b949e",
|
|
395
|
+
searchMatch: "#e3b341"
|
|
396
|
+
});
|
|
397
|
+
var lightCharts = Object.freeze({
|
|
398
|
+
chart1: "#0b57d0",
|
|
399
|
+
chart2: "#116a2e",
|
|
400
|
+
chart3: "#8a5a1b",
|
|
401
|
+
chart4: "#a0186f",
|
|
402
|
+
chart5: "#0e7490",
|
|
403
|
+
chart6: "#57606a"
|
|
404
|
+
});
|
|
405
|
+
var darkCharts = Object.freeze({
|
|
406
|
+
chart1: "#6ca4f8",
|
|
407
|
+
chart2: "#56d364",
|
|
408
|
+
chart3: "#e3b341",
|
|
409
|
+
chart4: "#db61a2",
|
|
410
|
+
chart5: "#39c5cf",
|
|
411
|
+
chart6: "#8b949e"
|
|
412
|
+
});
|
|
413
|
+
function defineVariant(id, familyId, familyName, name, appearance, colors) {
|
|
414
|
+
const light = appearance === "light";
|
|
415
|
+
return {
|
|
416
|
+
id,
|
|
417
|
+
familyId,
|
|
418
|
+
familyName,
|
|
419
|
+
name,
|
|
420
|
+
appearance,
|
|
421
|
+
colors,
|
|
422
|
+
terminal: light ? lightTerminal : darkTerminal,
|
|
423
|
+
editor: light ? lightEditor : darkEditor,
|
|
424
|
+
charts: light ? lightCharts : darkCharts
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
/** The `adea-light`/`adea-dark` entries mirror `styles/theme.css`. */
|
|
428
|
+
var builtinThemeRegistry = Object.freeze([
|
|
429
|
+
defineVariant("adea-light", "adea", "Adea", "Adea Light", "light", {
|
|
430
|
+
background: "#ffffff",
|
|
431
|
+
foreground: "#252525",
|
|
432
|
+
card: "#ffffff",
|
|
433
|
+
cardForeground: "#252525",
|
|
434
|
+
popover: "#ffffff",
|
|
435
|
+
popoverForeground: "#252525",
|
|
436
|
+
primary: "#343434",
|
|
437
|
+
primaryForeground: "#fcfcfc",
|
|
438
|
+
secondary: "#f7f7f7",
|
|
439
|
+
secondaryForeground: "#343434",
|
|
440
|
+
muted: "#f7f7f7",
|
|
441
|
+
mutedForeground: "#6f6f6f",
|
|
442
|
+
accent: "#f7f7f7",
|
|
443
|
+
accentForeground: "#343434",
|
|
444
|
+
destructive: "#c53c2b",
|
|
445
|
+
success: "#1a7f37",
|
|
446
|
+
border: "#ebebeb",
|
|
447
|
+
input: "#ebebeb",
|
|
448
|
+
ring: "#a3a3a3"
|
|
449
|
+
}),
|
|
450
|
+
defineVariant("adea-dark", "adea", "Adea", "Adea Dark", "dark", {
|
|
451
|
+
background: "#252525",
|
|
452
|
+
foreground: "#fcfcfc",
|
|
453
|
+
card: "#343434",
|
|
454
|
+
cardForeground: "#fcfcfc",
|
|
455
|
+
popover: "#343434",
|
|
456
|
+
popoverForeground: "#fcfcfc",
|
|
457
|
+
primary: "#ebebeb",
|
|
458
|
+
primaryForeground: "#343434",
|
|
459
|
+
secondary: "#444444",
|
|
460
|
+
secondaryForeground: "#fcfcfc",
|
|
461
|
+
muted: "#444444",
|
|
462
|
+
mutedForeground: "#a3a3a3",
|
|
463
|
+
accent: "#444444",
|
|
464
|
+
accentForeground: "#fcfcfc",
|
|
465
|
+
destructive: "#e07060",
|
|
466
|
+
success: "#3fb950",
|
|
467
|
+
border: "rgba(255, 255, 255, 0.16)",
|
|
468
|
+
input: "rgba(255, 255, 255, 0.2)",
|
|
469
|
+
ring: "#7c7c7c"
|
|
470
|
+
}),
|
|
471
|
+
defineVariant("slate-light", "slate", "Slate", "Slate Light", "light", {
|
|
472
|
+
background: "#f8fafc",
|
|
473
|
+
foreground: "#0f172a",
|
|
474
|
+
card: "#ffffff",
|
|
475
|
+
cardForeground: "#0f172a",
|
|
476
|
+
popover: "#ffffff",
|
|
477
|
+
popoverForeground: "#0f172a",
|
|
478
|
+
primary: "#0f172a",
|
|
479
|
+
primaryForeground: "#f8fafc",
|
|
480
|
+
secondary: "#e2e8f0",
|
|
481
|
+
secondaryForeground: "#0f172a",
|
|
482
|
+
muted: "#e2e8f0",
|
|
483
|
+
mutedForeground: "#475569",
|
|
484
|
+
accent: "#e2e8f0",
|
|
485
|
+
accentForeground: "#0f172a",
|
|
486
|
+
destructive: "#b91c1c",
|
|
487
|
+
success: "#15803d",
|
|
488
|
+
border: "#cbd5e1",
|
|
489
|
+
input: "#cbd5e1",
|
|
490
|
+
ring: "#64748b"
|
|
491
|
+
}),
|
|
492
|
+
defineVariant("slate-dark", "slate", "Slate", "Slate Dark", "dark", {
|
|
493
|
+
background: "#0f172a",
|
|
494
|
+
foreground: "#f1f5f9",
|
|
495
|
+
card: "#1e293b",
|
|
496
|
+
cardForeground: "#f1f5f9",
|
|
497
|
+
popover: "#1e293b",
|
|
498
|
+
popoverForeground: "#f1f5f9",
|
|
499
|
+
primary: "#e2e8f0",
|
|
500
|
+
primaryForeground: "#0f172a",
|
|
501
|
+
secondary: "#334155",
|
|
502
|
+
secondaryForeground: "#f1f5f9",
|
|
503
|
+
muted: "#334155",
|
|
504
|
+
mutedForeground: "#94a3b8",
|
|
505
|
+
accent: "#334155",
|
|
506
|
+
accentForeground: "#f1f5f9",
|
|
507
|
+
destructive: "#f87171",
|
|
508
|
+
success: "#4ade80",
|
|
509
|
+
border: "rgba(148, 163, 184, 0.2)",
|
|
510
|
+
input: "rgba(148, 163, 184, 0.25)",
|
|
511
|
+
ring: "#64748b"
|
|
512
|
+
}),
|
|
513
|
+
defineVariant("contrast-light", "contrast", "High Contrast", "High Contrast Light", "light", {
|
|
514
|
+
background: "#ffffff",
|
|
515
|
+
foreground: "#000000",
|
|
516
|
+
card: "#ffffff",
|
|
517
|
+
cardForeground: "#000000",
|
|
518
|
+
popover: "#ffffff",
|
|
519
|
+
popoverForeground: "#000000",
|
|
520
|
+
primary: "#143d8f",
|
|
521
|
+
primaryForeground: "#ffffff",
|
|
522
|
+
secondary: "#f0f0f0",
|
|
523
|
+
secondaryForeground: "#000000",
|
|
524
|
+
muted: "#f0f0f0",
|
|
525
|
+
mutedForeground: "#333333",
|
|
526
|
+
accent: "#f0f0f0",
|
|
527
|
+
accentForeground: "#000000",
|
|
528
|
+
destructive: "#b91c1c",
|
|
529
|
+
success: "#14532d",
|
|
530
|
+
border: "#767676",
|
|
531
|
+
input: "#767676",
|
|
532
|
+
ring: "#000000"
|
|
533
|
+
}),
|
|
534
|
+
defineVariant("contrast-dark", "contrast", "High Contrast", "High Contrast Dark", "dark", {
|
|
535
|
+
background: "#000000",
|
|
536
|
+
foreground: "#ffffff",
|
|
537
|
+
card: "#0a0a0a",
|
|
538
|
+
cardForeground: "#ffffff",
|
|
539
|
+
popover: "#0a0a0a",
|
|
540
|
+
popoverForeground: "#ffffff",
|
|
541
|
+
primary: "#8ab4ff",
|
|
542
|
+
primaryForeground: "#000000",
|
|
543
|
+
secondary: "#1a1a1a",
|
|
544
|
+
secondaryForeground: "#ffffff",
|
|
545
|
+
muted: "#1a1a1a",
|
|
546
|
+
mutedForeground: "#e5e5e5",
|
|
547
|
+
accent: "#1a1a1a",
|
|
548
|
+
accentForeground: "#ffffff",
|
|
549
|
+
destructive: "#ff6b6b",
|
|
550
|
+
success: "#4ade80",
|
|
551
|
+
border: "#8f8f8f",
|
|
552
|
+
input: "#8f8f8f",
|
|
553
|
+
ring: "#ffffff"
|
|
554
|
+
})
|
|
555
|
+
]);
|
|
556
|
+
function normalizeMode(value) {
|
|
557
|
+
return value === "light" || value === "dark" || value === "system" ? value : "system";
|
|
558
|
+
}
|
|
559
|
+
function normalizeSurface(value) {
|
|
560
|
+
return value === "frosted" || value === "translucent" || value === "opaque" ? value : "opaque";
|
|
561
|
+
}
|
|
562
|
+
function normalizeThemeId(value, fallback) {
|
|
563
|
+
return typeof value === "string" && value.trim().length > 0 ? value : fallback;
|
|
564
|
+
}
|
|
565
|
+
function normalizeAccent(value) {
|
|
566
|
+
if (value === "theme") return "theme";
|
|
567
|
+
if (typeof value !== "string") return "theme";
|
|
568
|
+
if (accentPresetById(value)) return value;
|
|
569
|
+
const parsed = parseColor(value);
|
|
570
|
+
return parsed ? colorToHex(parsed) : "theme";
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Parse a stored appearance document. Anything that is not a strict version-2
|
|
574
|
+
* record falls back to the defaults and keeps the raw value for retention;
|
|
575
|
+
* individual unknown values inside a v2 record are corrected field by field.
|
|
576
|
+
*/
|
|
577
|
+
function normalizeAppearancePreferences(raw) {
|
|
578
|
+
if (typeof raw !== "object" || raw === null || raw.version !== 2) return {
|
|
579
|
+
value: defaultAppearancePreferences,
|
|
580
|
+
retainedRaw: raw
|
|
581
|
+
};
|
|
582
|
+
const record = raw;
|
|
583
|
+
return { value: {
|
|
584
|
+
version: 2,
|
|
585
|
+
mode: normalizeMode(record.mode),
|
|
586
|
+
lightThemeId: normalizeThemeId(record.lightThemeId, defaultAppearancePreferences.lightThemeId),
|
|
587
|
+
darkThemeId: normalizeThemeId(record.darkThemeId, defaultAppearancePreferences.darkThemeId),
|
|
588
|
+
accent: normalizeAccent(record.accent),
|
|
589
|
+
surface: normalizeSurface(record.surface),
|
|
590
|
+
reduceTransparency: record.reduceTransparency === true
|
|
591
|
+
} };
|
|
592
|
+
}
|
|
593
|
+
/** The legacy single-key preference, mapped into v2 during migration. */
|
|
594
|
+
function migrateLegacyThemeValue(stored) {
|
|
595
|
+
if (stored !== "light" && stored !== "dark" && stored !== "system") return void 0;
|
|
596
|
+
return {
|
|
597
|
+
...defaultAppearancePreferences,
|
|
598
|
+
mode: stored
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Read the appearance preferences: the v2 key first, then the legacy `theme`
|
|
603
|
+
* key, then defaults. Storage failures degrade to defaults like every other
|
|
604
|
+
* blocked-storage consumer.
|
|
605
|
+
*/
|
|
606
|
+
function readAppearancePreferences(storage) {
|
|
607
|
+
if (!storage) return defaultAppearancePreferences;
|
|
608
|
+
try {
|
|
609
|
+
const raw = storage.getItem(APPEARANCE_STORAGE_KEY);
|
|
610
|
+
if (raw !== null) {
|
|
611
|
+
let parsed;
|
|
612
|
+
try {
|
|
613
|
+
parsed = JSON.parse(raw);
|
|
614
|
+
} catch {
|
|
615
|
+
return defaultAppearancePreferences;
|
|
616
|
+
}
|
|
617
|
+
return normalizeAppearancePreferences(parsed).value;
|
|
618
|
+
}
|
|
619
|
+
return migrateLegacyThemeValue(storage.getItem("theme")) ?? defaultAppearancePreferences;
|
|
620
|
+
} catch {
|
|
621
|
+
return defaultAppearancePreferences;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function writeAppearancePreferences(storage, preferences) {
|
|
625
|
+
if (!storage) return;
|
|
626
|
+
try {
|
|
627
|
+
storage.setItem(APPEARANCE_STORAGE_KEY, JSON.stringify(preferences));
|
|
628
|
+
} catch {}
|
|
629
|
+
}
|
|
630
|
+
/** Resolve preferences against the environment into the applied document state. */
|
|
631
|
+
function resolveAppearanceState(preferences, environment, registry = builtinThemeRegistry) {
|
|
632
|
+
const resolvedMode = resolveAppearanceMode(preferences.mode, environment.systemAppearance);
|
|
633
|
+
const variant = resolveThemeVariant(registry, preferences, resolvedMode);
|
|
634
|
+
return {
|
|
635
|
+
resolvedMode,
|
|
636
|
+
variant,
|
|
637
|
+
accent: deriveAccentRoles(preferences.accent, variant),
|
|
638
|
+
effectiveSurface: resolveSurface(preferences.surface, {
|
|
639
|
+
osReducedTransparency: environment.osReducedTransparency,
|
|
640
|
+
userReducedTransparency: preferences.reduceTransparency,
|
|
641
|
+
nativeTranslucency: environment.nativeTranslucency
|
|
642
|
+
}),
|
|
643
|
+
reduceTransparencyActive: environment.osReducedTransparency || preferences.reduceTransparency
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
var SURFACE_BACKGROUND_ALPHA = {
|
|
647
|
+
opaque: "1",
|
|
648
|
+
frosted: "0.92",
|
|
649
|
+
translucent: "0.8"
|
|
650
|
+
};
|
|
651
|
+
/**
|
|
652
|
+
* The custom properties a non-default variant owns, as a flat map. This one
|
|
653
|
+
* mapping feeds both the live provider and the pre-paint no-flash script, so
|
|
654
|
+
* a restored first paint and a later live switch cannot disagree. Default
|
|
655
|
+
* variants return an empty map: `styles/theme.css` declares those tokens.
|
|
656
|
+
*/
|
|
657
|
+
function flatVariantTokens(variant) {
|
|
658
|
+
if (isDefaultVariant(variant)) return {};
|
|
659
|
+
const tokens = {};
|
|
660
|
+
for (const [role, value] of Object.entries(variant.colors)) tokens[`--${kebabCase(role)}`] = value;
|
|
661
|
+
const terminal = variant.terminal;
|
|
662
|
+
tokens["--terminal-background"] = terminal.background;
|
|
663
|
+
tokens["--terminal-foreground"] = terminal.foreground;
|
|
664
|
+
tokens["--terminal-cursor"] = terminal.cursor;
|
|
665
|
+
tokens["--terminal-selection"] = terminal.selection;
|
|
666
|
+
const ansiNames = [
|
|
667
|
+
"black",
|
|
668
|
+
"red",
|
|
669
|
+
"green",
|
|
670
|
+
"yellow",
|
|
671
|
+
"blue",
|
|
672
|
+
"magenta",
|
|
673
|
+
"cyan",
|
|
674
|
+
"white"
|
|
675
|
+
];
|
|
676
|
+
for (const [index, color] of terminal.ansi.entries()) {
|
|
677
|
+
const name = ansiNames[index % 8];
|
|
678
|
+
tokens[index < 8 ? `--terminal-ansi-${name}` : `--terminal-ansi-bright-${name}`] = color;
|
|
679
|
+
}
|
|
680
|
+
for (const [role, value] of Object.entries(variant.editor)) tokens[`--editor-${kebabCase(role)}`] = value;
|
|
681
|
+
for (const [index, value] of Object.values(variant.charts).entries()) tokens[`--chart-${index + 1}`] = value;
|
|
682
|
+
return tokens;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Apply a resolved state to a document: the `dark` class and color scheme for
|
|
686
|
+
* the palette, the diagnostic data attributes, the accent role overrides, and
|
|
687
|
+
* the resolved variant's semantic tokens (including terminal ANSI and editor
|
|
688
|
+
* roles). CSS custom properties cascade, so terminals and editors update live
|
|
689
|
+
* without remounting.
|
|
690
|
+
*
|
|
691
|
+
* The default `adea-light`/`adea-dark` variants skip the palette override —
|
|
692
|
+
* `styles/theme.css` declares those tokens already, and leaving them CSS-owned
|
|
693
|
+
* keeps first paint byte-identical to the pre-#425 app.
|
|
694
|
+
*/
|
|
695
|
+
function applyAppearanceToDocument(document, state) {
|
|
696
|
+
const root = document.documentElement;
|
|
697
|
+
const style = root.style;
|
|
698
|
+
root.classList.toggle("dark", state.resolvedMode === "dark");
|
|
699
|
+
style.colorScheme = state.resolvedMode;
|
|
700
|
+
root.dataset.theme = state.variant.id;
|
|
701
|
+
root.dataset.appearanceMode = state.resolvedMode;
|
|
702
|
+
root.dataset.surface = state.effectiveSurface;
|
|
703
|
+
root.dataset.accent = state.accent.overrides ? "custom" : "theme";
|
|
704
|
+
root.dataset.reduceTransparency = state.reduceTransparencyActive ? "true" : "false";
|
|
705
|
+
const setToken = (name, value) => style.setProperty(name, value);
|
|
706
|
+
setToken("--surface-alpha", SURFACE_BACKGROUND_ALPHA[state.effectiveSurface]);
|
|
707
|
+
if (state.accent.overrides) {
|
|
708
|
+
setToken("--primary", state.accent.primary);
|
|
709
|
+
setToken("--primary-foreground", state.accent.onPrimary);
|
|
710
|
+
setToken("--ring", state.accent.ring);
|
|
711
|
+
}
|
|
712
|
+
for (const [name, value] of Object.entries(flatVariantTokens(state.variant))) setToken(name, value);
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* The no-flash preload script rendered in the document head. It re-resolves
|
|
716
|
+
* the stored (or legacy) preference against the OS before first paint and
|
|
717
|
+
* applies the same palette state the provider would, so hydration never shows
|
|
718
|
+
* the wrong palette. Accent overrides land with the provider: they decorate
|
|
719
|
+
* the resolved palette and cannot produce a wrong-palette flash.
|
|
720
|
+
*/
|
|
721
|
+
function appearanceThemeScript() {
|
|
722
|
+
const preload = builtinThemeRegistry.map((variant) => ({
|
|
723
|
+
id: variant.id,
|
|
724
|
+
dark: variant.appearance === "dark",
|
|
725
|
+
tokens: flatVariantTokens(variant)
|
|
726
|
+
}));
|
|
727
|
+
const registry = JSON.stringify(preload);
|
|
728
|
+
const alpha = JSON.stringify(SURFACE_BACKGROUND_ALPHA);
|
|
729
|
+
return `(function(){try{
|
|
730
|
+
var prefs=null;var raw=null;
|
|
731
|
+
try{raw=localStorage.getItem('${APPEARANCE_STORAGE_KEY}')}catch(e){}
|
|
732
|
+
if(raw){try{var parsed=JSON.parse(raw);if(parsed&&parsed.version===2)prefs=parsed}catch(e){}}
|
|
733
|
+
var mode='system';
|
|
734
|
+
if(prefs){if(prefs.mode==='light'||prefs.mode==='dark')mode=prefs.mode}
|
|
735
|
+
else{try{var t=localStorage.getItem('${LEGACY_THEME_STORAGE_KEY}');if(t==='light'||t==='dark')mode=t}catch(e){}}
|
|
736
|
+
var dark=mode==='dark'||(mode==='system'&&window.matchMedia('${DARK_QUERY}').matches);
|
|
737
|
+
var registry=${registry};
|
|
738
|
+
var wanted=dark?(prefs&&prefs.darkThemeId)||'${defaultAppearancePreferences.darkThemeId}':(prefs&&prefs.lightThemeId)||'${defaultAppearancePreferences.lightThemeId}';
|
|
739
|
+
var variant=null;
|
|
740
|
+
for(var i=0;i<registry.length;i++){if(registry[i].id===wanted)variant=registry[i]}
|
|
741
|
+
if(!variant){for(var j=0;j<registry.length;j++){if(registry[j].dark===dark)variant=registry[j]}}
|
|
742
|
+
if(!variant)variant=registry[0];
|
|
743
|
+
var reduce=window.matchMedia('${REDUCED_TRANSPARENCY_QUERY}').matches||!!(prefs&&prefs.reduceTransparency);
|
|
744
|
+
/* Pre-hydration the host translucency capability is unknown, so the script
|
|
745
|
+
resolves every glass request to the conservative frosted tokens; the
|
|
746
|
+
provider re-resolves with the real capability once mounted. */
|
|
747
|
+
var surface=reduce?'opaque':(prefs&&prefs.surface==='frosted'||prefs&&prefs.surface==='translucent'?'frosted':'opaque');
|
|
748
|
+
if(surface!=='opaque'&&surface!=='frosted')surface='opaque';
|
|
749
|
+
var r=document.documentElement,s=r.style;
|
|
750
|
+
r.classList.toggle('dark',dark);
|
|
751
|
+
s.colorScheme=dark?'dark':'light';
|
|
752
|
+
r.dataset.theme=variant.id;
|
|
753
|
+
r.dataset.appearanceMode=mode;
|
|
754
|
+
r.dataset.surface=surface;
|
|
755
|
+
r.dataset.reduceTransparency=reduce?'true':'false';
|
|
756
|
+
s.setProperty('--surface-alpha',${alpha}[surface]||'1');
|
|
757
|
+
var tokens=variant.tokens||{};
|
|
758
|
+
for(var name in tokens){s.setProperty(name,tokens[name])}
|
|
759
|
+
}catch(e){}})();`;
|
|
760
|
+
}
|
|
761
|
+
function isDefaultVariant(variant) {
|
|
762
|
+
return variant.id === defaultAppearancePreferences.lightThemeId || variant.id === defaultAppearancePreferences.darkThemeId;
|
|
763
|
+
}
|
|
764
|
+
function kebabCase(value) {
|
|
765
|
+
return value.replaceAll(/[A-Z]/g, (character) => `-${character.toLocaleLowerCase()}`);
|
|
766
|
+
}
|
|
767
|
+
//#endregion
|
|
768
|
+
export { APPEARANCE_STORAGE_KEY, ColorParseError, DARK_QUERY, LEGACY_THEME_STORAGE_KEY, REDUCED_TRANSPARENCY_QUERY, accentPresetById, accentPresets, appearanceThemeScript, applyAppearanceToDocument, builtinThemeRegistry, colorToHex, contrastRatio, defaultAppearancePreferences, deriveAccentRoles, flatVariantTokens, migrateLegacyThemeValue, normalizeAccentValue, normalizeAppearancePreferences, parseColor, readAppearancePreferences, resolveAppearanceMode, resolveAppearanceState, resolveSurface, resolveThemeVariant, themeSelectionVariantId, validateThemeRegistry, writeAppearancePreferences };
|