@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.
@@ -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 };