@adea-ai/ui 0.128.1 → 0.128.3

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.
Files changed (141) hide show
  1. package/dist/components/theme/appearance-panel.js +1 -1
  2. package/dist/components/theme/index.js +1 -1
  3. package/dist/components/theme/lazy.d.ts +15 -0
  4. package/dist/components/theme/lazy.d.ts.map +1 -0
  5. package/dist/components/theme/lazy.js +9 -0
  6. package/dist/components/theme/theme-catalogue-source.d.ts +15 -0
  7. package/dist/components/theme/theme-catalogue-source.d.ts.map +1 -0
  8. package/dist/components/theme/theme-catalogue-source.js +51 -0
  9. package/dist/components/theme/theme-catalogue-source.js.map +1 -0
  10. package/dist/components/theme/theme-document-core.d.ts +123 -0
  11. package/dist/components/theme/theme-document-core.d.ts.map +1 -0
  12. package/dist/components/theme/{theme-document.js → theme-document-core.js} +12 -12
  13. package/dist/components/theme/theme-document-core.js.map +1 -0
  14. package/dist/components/theme/theme-document.d.ts +8 -107
  15. package/dist/components/theme/theme-document.d.ts.map +1 -1
  16. package/dist/components/theme/theme-persistence.d.ts +1 -1
  17. package/dist/components/theme/theme-persistence.d.ts.map +1 -1
  18. package/dist/components/theme/theme-persistence.js +9 -6
  19. package/dist/components/theme/theme-persistence.js.map +1 -1
  20. package/dist/components/theme/theme-provider-core.d.ts +19 -0
  21. package/dist/components/theme/theme-provider-core.d.ts.map +1 -0
  22. package/dist/components/theme/theme-provider-core.js +28 -0
  23. package/dist/components/theme/theme-provider-core.js.map +1 -0
  24. package/dist/components/theme/theme-provider-lazy.d.ts +28 -0
  25. package/dist/components/theme/theme-provider-lazy.d.ts.map +1 -0
  26. package/dist/components/theme/theme-provider-lazy.js +36 -0
  27. package/dist/components/theme/theme-provider-lazy.js.map +1 -0
  28. package/dist/components/theme/theme-provider-value.d.ts +175 -0
  29. package/dist/components/theme/theme-provider-value.d.ts.map +1 -0
  30. package/dist/components/theme/theme-provider-value.js +200 -0
  31. package/dist/components/theme/theme-provider-value.js.map +1 -0
  32. package/dist/components/theme/theme-provider.d.ts +11 -136
  33. package/dist/components/theme/theme-provider.d.ts.map +1 -1
  34. package/dist/components/theme/theme-provider.js +27 -149
  35. package/dist/components/theme/theme-provider.js.map +1 -1
  36. package/dist/components/theme/theme-script.js +4 -3
  37. package/dist/components/theme/theme-script.js.map +1 -1
  38. package/dist/components/theme/theme-state-core.d.ts +248 -0
  39. package/dist/components/theme/theme-state-core.d.ts.map +1 -0
  40. package/dist/components/theme/{theme-state.js → theme-state-core.js} +8 -7
  41. package/dist/components/theme/theme-state-core.js.map +1 -0
  42. package/dist/components/theme/theme-state.d.ts +8 -214
  43. package/dist/components/theme/theme-state.d.ts.map +1 -1
  44. package/dist/components/theme/theme-toggle.js.map +1 -1
  45. package/dist/index.js +7 -4
  46. package/dist/lib/accent-presets.d.ts +26 -0
  47. package/dist/lib/accent-presets.d.ts.map +1 -0
  48. package/dist/lib/accent-presets.js +11 -0
  49. package/dist/lib/accent-presets.js.map +1 -0
  50. package/dist/lib/accent-roles.js +94 -9
  51. package/dist/lib/accent-roles.js.map +1 -1
  52. package/dist/lib/font-zoom.js +4 -2
  53. package/dist/lib/font-zoom.js.map +1 -1
  54. package/dist/lib/overlay.js +3 -1
  55. package/dist/lib/overlay.js.map +1 -1
  56. package/dist/lib/theme-catalogue.d.ts +3 -0
  57. package/dist/lib/theme-catalogue.d.ts.map +1 -0
  58. package/dist/lib/theme-catalogue.js +22 -0
  59. package/dist/lib/theme-catalogue.js.map +1 -0
  60. package/dist/lib/theme-css.d.ts +43 -0
  61. package/dist/lib/theme-css.d.ts.map +1 -0
  62. package/dist/lib/theme-css.js +167 -0
  63. package/dist/lib/theme-css.js.map +1 -0
  64. package/dist/lib/theme-defaults.d.ts +46 -0
  65. package/dist/lib/theme-defaults.d.ts.map +1 -0
  66. package/dist/lib/theme-defaults.generated.d.ts +13 -0
  67. package/dist/lib/theme-defaults.generated.d.ts.map +1 -0
  68. package/dist/lib/theme-defaults.generated.js +318 -0
  69. package/dist/lib/theme-defaults.generated.js.map +1 -0
  70. package/dist/lib/theme-defaults.js +47 -0
  71. package/dist/lib/theme-defaults.js.map +1 -0
  72. package/dist/lib/theme-engine-light.d.ts +3 -0
  73. package/dist/lib/theme-engine-light.d.ts.map +1 -0
  74. package/dist/lib/theme-engine-light.js +75 -0
  75. package/dist/lib/theme-engine-light.js.map +1 -0
  76. package/dist/lib/theme-engine.d.ts +69 -0
  77. package/dist/lib/theme-engine.d.ts.map +1 -0
  78. package/dist/lib/theme-engine.js +33 -0
  79. package/dist/lib/theme-engine.js.map +1 -0
  80. package/dist/lib/themes.d.ts +14 -48
  81. package/dist/lib/themes.d.ts.map +1 -1
  82. package/dist/lib/themes.js +24 -164
  83. package/dist/lib/themes.js.map +1 -1
  84. package/dist/lib/tokens.d.ts +1 -25
  85. package/dist/lib/tokens.d.ts.map +1 -1
  86. package/dist/lib/tokens.js +1 -24
  87. package/dist/lib/tokens.js.map +1 -1
  88. package/dist/lib/tooltip-focus-gate.js +7 -1
  89. package/dist/lib/tooltip-focus-gate.js.map +1 -1
  90. package/dist/lib/variants.js +2 -2
  91. package/dist/r/lib.json +35 -0
  92. package/dist/r/registry.json +70 -0
  93. package/dist/r/src/components/theme/lazy.ts +35 -0
  94. package/dist/r/src/components/theme/theme-catalogue-source.ts +59 -0
  95. package/dist/r/src/components/theme/theme-document-core.ts +262 -0
  96. package/dist/r/src/components/theme/theme-document.ts +23 -240
  97. package/dist/r/src/components/theme/theme-persistence.ts +14 -6
  98. package/dist/r/src/components/theme/theme-provider-core.tsx +40 -0
  99. package/dist/r/src/components/theme/theme-provider-lazy.tsx +34 -0
  100. package/dist/r/src/components/theme/theme-provider-value.ts +445 -0
  101. package/dist/r/src/components/theme/theme-provider.tsx +23 -346
  102. package/dist/r/src/components/theme/theme-script.ts +3 -3
  103. package/dist/r/src/components/theme/theme-state-core.ts +364 -0
  104. package/dist/r/src/components/theme/theme-state.ts +10 -329
  105. package/dist/r/src/components/theme/theme-toggle.tsx +1 -1
  106. package/dist/r/src/lib/accent-presets.ts +43 -0
  107. package/dist/r/src/lib/theme-catalogue.ts +26 -0
  108. package/dist/r/src/lib/theme-css.ts +201 -0
  109. package/dist/r/src/lib/theme-defaults.generated.ts +319 -0
  110. package/dist/r/src/lib/theme-defaults.ts +79 -0
  111. package/dist/r/src/lib/theme-engine-light.ts +117 -0
  112. package/dist/r/src/lib/theme-engine.ts +111 -0
  113. package/dist/r/src/lib/themes.ts +27 -191
  114. package/dist/r/src/lib/tokens.ts +1 -35
  115. package/dist/r/theming.json +35 -0
  116. package/package.json +12 -3
  117. package/registry.json +70 -0
  118. package/src/components/theme/lazy.ts +35 -0
  119. package/src/components/theme/theme-catalogue-source.ts +59 -0
  120. package/src/components/theme/theme-document-core.ts +262 -0
  121. package/src/components/theme/theme-document.ts +23 -240
  122. package/src/components/theme/theme-persistence.ts +14 -6
  123. package/src/components/theme/theme-provider-core.tsx +40 -0
  124. package/src/components/theme/theme-provider-lazy.tsx +34 -0
  125. package/src/components/theme/theme-provider-value.ts +445 -0
  126. package/src/components/theme/theme-provider.tsx +23 -346
  127. package/src/components/theme/theme-script.ts +3 -3
  128. package/src/components/theme/theme-state-core.ts +364 -0
  129. package/src/components/theme/theme-state.ts +10 -329
  130. package/src/components/theme/theme-toggle.tsx +1 -1
  131. package/src/lib/accent-presets.ts +43 -0
  132. package/src/lib/theme-catalogue.ts +26 -0
  133. package/src/lib/theme-css.ts +201 -0
  134. package/src/lib/theme-defaults.generated.ts +319 -0
  135. package/src/lib/theme-defaults.ts +79 -0
  136. package/src/lib/theme-engine-light.ts +117 -0
  137. package/src/lib/theme-engine.ts +111 -0
  138. package/src/lib/themes.ts +27 -191
  139. package/src/lib/tokens.ts +1 -35
  140. package/dist/components/theme/theme-document.js.map +0 -1
  141. package/dist/components/theme/theme-state.js.map +0 -1
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The accent axis.
3
+ *
4
+ * adea's accent presets, as a selection rather than a constant. `theme` is the
5
+ * default and means "the variant's own primary" — monochrome in adea's palette —
6
+ * and each named preset overrides the interactive roles through the
7
+ * `[data-accent]` blocks in `theme.css`.
8
+ *
9
+ * These are the same six presets `accentPresets` exports in adea's own
10
+ * `appearance.ts`, and the values match. A consumer's appearance picker and the
11
+ * workshop's theme toolbar read this one list, so the two cannot disagree about
12
+ * what a preset is called or what colour it is.
13
+ */
14
+ export type AccentPreset = {
15
+ /** The `data-accent` value. `theme` is the variant's own primary. */
16
+ id: string;
17
+ label: string;
18
+ /** What the preset is for, in the gallery and in a picker. */
19
+ description: string;
20
+ /** The accent as it appears in the light theme. Absent for `theme`. */
21
+ light?: string;
22
+ /** The accent as it appears in the dark theme. Absent for `theme`. */
23
+ dark?: string;
24
+ };
25
+ export declare const accentPresets: readonly AccentPreset[];
26
+ //# sourceMappingURL=accent-presets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accent-presets.d.ts","sourceRoot":"","sources":["../../src/lib/accent-presets.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,qEAAqE;IACrE,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,WAAW,EAAE,MAAM,CAAA;IACnB,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED,eAAO,MAAM,aAAa,EAAE,SAAS,YAAY,EAQ/C,CAAA"}
@@ -0,0 +1,11 @@
1
+ import { ACCENTS } from "@adea-ai/themes";
2
+ //#region src/lib/accent-presets.ts
3
+ var accentPresets = Object.freeze([{
4
+ id: "theme",
5
+ label: "Theme",
6
+ description: "The variant's own primary. Neutral in adea's palette, so the interface stays monochrome."
7
+ }, ...ACCENTS]);
8
+ //#endregion
9
+ export { accentPresets };
10
+
11
+ //# sourceMappingURL=accent-presets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accent-presets.js","names":[],"sources":["../../src/lib/accent-presets.ts"],"sourcesContent":["import { ACCENTS } from '@adea-ai/themes'\n\n/*\n * Its own module so the theme provider's start-up path (the stored-preference reader\n * and the boot script both need the preset ids) does not import the token manifest in\n * `tokens.ts`, which is 20 KB of documentation data a provider never reads.\n * `tokens.ts` re-exports both names, so every existing import keeps working.\n */\n\n/**\n * The accent axis.\n *\n * adea's accent presets, as a selection rather than a constant. `theme` is the\n * default and means \"the variant's own primary\" — monochrome in adea's palette —\n * and each named preset overrides the interactive roles through the\n * `[data-accent]` blocks in `theme.css`.\n *\n * These are the same six presets `accentPresets` exports in adea's own\n * `appearance.ts`, and the values match. A consumer's appearance picker and the\n * workshop's theme toolbar read this one list, so the two cannot disagree about\n * what a preset is called or what colour it is.\n */\nexport type AccentPreset = {\n /** The `data-accent` value. `theme` is the variant's own primary. */\n id: string\n label: string\n /** What the preset is for, in the gallery and in a picker. */\n description: string\n /** The accent as it appears in the light theme. Absent for `theme`. */\n light?: string\n /** The accent as it appears in the dark theme. Absent for `theme`. */\n dark?: string\n}\n\nexport const accentPresets: readonly AccentPreset[] = Object.freeze([\n {\n id: 'theme',\n label: 'Theme',\n description:\n \"The variant's own primary. Neutral in adea's palette, so the interface stays monochrome.\",\n },\n ...ACCENTS,\n])\n"],"mappings":";;AAkCA,IAAa,gBAAyC,OAAO,OAAO,CAClE;CACE,IAAI;CACJ,OAAO;CACP,aACE;AACJ,GACA,GAAG,OACL,CAAC"}
@@ -1,6 +1,70 @@
1
1
  import { ACCENTS, accentRoles, accentValue, primarySubtleCss } from "@adea-ai/themes";
2
- import { contrastRatio, oklchToHex, parseColor, repairContrast } from "@adea-ai/themes/oklch";
2
+ import { contrastRatio as contrastRatio$1, oklchToHex, parseColor as parseColor$1, repairContrast } from "@adea-ai/themes/oklch";
3
3
  //#region src/lib/accent-roles.ts
4
+ var HEX = /^#([0-9a-fA-F]+)$/;
5
+ /** Parse a hex colour (`#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`). `undefined` for anything else. */
6
+ function parseColor(value) {
7
+ const digits = HEX.exec(value.trim())?.[1];
8
+ if (!digits) return void 0;
9
+ if (digits.length === 3 || digits.length === 4) {
10
+ const [r, g, b, a] = [...digits].map((digit) => Number.parseInt(digit + digit, 16));
11
+ return {
12
+ r,
13
+ g,
14
+ b,
15
+ a: a ?? 255
16
+ };
17
+ }
18
+ if (digits.length === 6 || digits.length === 8) {
19
+ const byte = (index) => Number.parseInt(digits.slice(index, index + 2), 16);
20
+ return {
21
+ r: byte(0),
22
+ g: byte(2),
23
+ b: byte(4),
24
+ a: digits.length === 8 ? byte(6) : 255
25
+ };
26
+ }
27
+ }
28
+ function toHexByte(value) {
29
+ return value.toString(16).padStart(2, "0");
30
+ }
31
+ /** `#rrggbb`, or `#rrggbbaa` when the colour is not opaque. */
32
+ function colorToHex(color) {
33
+ const opaque = `#${toHexByte(color.r)}${toHexByte(color.g)}${toHexByte(color.b)}`;
34
+ return color.a === 255 ? opaque : `${opaque}${toHexByte(color.a)}`;
35
+ }
36
+ function blendOver(foreground, background) {
37
+ const alpha = foreground.a / 255;
38
+ const blend = (front, back) => Math.round(front * alpha + back * (1 - alpha));
39
+ return {
40
+ r: blend(foreground.r, background.r),
41
+ g: blend(foreground.g, background.g),
42
+ b: blend(foreground.b, background.b),
43
+ a: 255
44
+ };
45
+ }
46
+ function toCanonicalColor(color) {
47
+ return parseColor$1(colorToHex({
48
+ ...color,
49
+ a: 255
50
+ }));
51
+ }
52
+ /** WCAG contrast ratio between two colours. Alpha composites over the background first. */
53
+ function contrastRatio(foreground, background) {
54
+ const front = typeof foreground === "string" ? parseColor(foreground) : foreground;
55
+ const back = typeof background === "string" ? parseColor(background) : background;
56
+ if (!front || !back) return 0;
57
+ const foregroundColor = toCanonicalColor(front.a === 255 ? front : blendOver(front, back));
58
+ const backgroundColor = toCanonicalColor(back);
59
+ if (!foregroundColor || !backgroundColor) return 0;
60
+ return contrastRatio$1(foregroundColor, backgroundColor);
61
+ }
62
+ var WHITE = {
63
+ r: 255,
64
+ g: 255,
65
+ b: 255,
66
+ a: 255
67
+ };
4
68
  var WHITE_OKLCH = {
5
69
  l: 1,
6
70
  c: 0,
@@ -20,10 +84,31 @@ function repairedHex(source, canvas, minimum) {
20
84
  const repaired = repairContrast(source, canvas, minimum + margin);
21
85
  if (!repaired.satisfied) continue;
22
86
  const hex = oklchToHex(repaired.color);
23
- const rounded = parseColor(hex);
24
- if (rounded && contrastRatio(rounded, canvas) >= minimum) return hex;
87
+ const rounded = parseColor$1(hex);
88
+ if (rounded && contrastRatio$1(rounded, canvas) >= minimum) return hex;
25
89
  }
26
- return contrastRatio(WHITE_OKLCH, canvas) >= minimum ? "#ffffff" : "#000000";
90
+ return contrastRatio$1(WHITE_OKLCH, canvas) >= minimum ? "#ffffff" : "#000000";
91
+ }
92
+ /** Preserve the host hex shape while delegating contrast repair to the catalogue. */
93
+ function ensureContrast(color, background, minimum) {
94
+ if (contrastRatio(color, background) >= minimum) return color;
95
+ const source = toCanonicalColor(color.a === 255 ? color : blendOver(color, background));
96
+ const canvas = toCanonicalColor(background);
97
+ if (!source || !canvas) return color;
98
+ return parseColor(repairedHex(source, canvas, minimum));
99
+ }
100
+ /** The contrast an accent is held to against its surface unless the host sets another. */
101
+ var DEFAULT_ACCENT_CONTRAST_FLOOR = 3;
102
+ /**
103
+ * Normalise a custom accent against a surface: unparseable input is rejected
104
+ * (`undefined`), and any accepted colour is raised to `minimum` so an accent can
105
+ * never ship unreadable. Accepts every CSS hex length; for the stored grammar use
106
+ * {@link normalizeCustomAccent}.
107
+ */
108
+ function normalizeAccentValue(value, background, minimum = 3) {
109
+ const parsed = parseColor(value);
110
+ if (!parsed) return void 0;
111
+ return colorToHex(ensureContrast(parsed, parseColor(background) ?? WHITE, minimum));
27
112
  }
28
113
  /**
29
114
  * The stored form of a custom accent: `#rrggbb`, six digits, opaque, lower case.
@@ -52,10 +137,10 @@ function appearanceOf(target) {
52
137
  * that already reads: the caller compares and keeps the catalogue's own roles.
53
138
  */
54
139
  function gateColor(value, background, floor) {
55
- const source = parseColor(value);
56
- const canvas = background ? parseColor(background) : void 0;
140
+ const source = parseColor$1(value);
141
+ const canvas = background ? parseColor$1(background) : void 0;
57
142
  if (!source || !canvas) return value;
58
- if (contrastRatio(source, canvas) >= floor) return value;
143
+ if (contrastRatio$1(source, canvas) >= floor) return value;
59
144
  return repairedHex(source, canvas, floor);
60
145
  }
61
146
  /**
@@ -69,7 +154,7 @@ function gateColor(value, background, floor) {
69
154
  function accentRolesForPreset(preset, context) {
70
155
  const appearance = appearanceOf(context.target);
71
156
  const requested = accentValue(preset, appearance);
72
- if (!parseColor(requested)) return void 0;
157
+ if (!parseColor$1(requested)) return void 0;
73
158
  const floor = context.floor;
74
159
  if (floor === void 0) return accentRoles(preset, context.target);
75
160
  const gated = gateColor(requested, context.background, floor);
@@ -158,6 +243,6 @@ function paintAccentRoles(element, roles) {
158
243
  element.style.setProperty("--ring", roles.ring);
159
244
  }
160
245
  //#endregion
161
- export { ACCENT_PAINT_PROPERTIES, accentOverrideVariables, accentRolesForPreset, isCustomAccent, normalizeCustomAccent, paintAccentRoles, resolveAccentRoles };
246
+ export { ACCENT_PAINT_PROPERTIES, DEFAULT_ACCENT_CONTRAST_FLOOR, accentOverrideVariables, accentRolesForPreset, colorToHex, contrastRatio, ensureContrast, isCustomAccent, normalizeAccentValue, normalizeCustomAccent, paintAccentRoles, parseColor, resolveAccentRoles };
162
247
 
163
248
  //# sourceMappingURL=accent-roles.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"accent-roles.js","names":["parseCanonicalColor","canonicalContrastRatio"],"sources":["../../src/lib/accent-roles.ts"],"sourcesContent":["/*\n * Copyright (c) 2026 Wing\n * SPDX-License-Identifier: MIT\n *\n * The colour helpers, the contrast gate and the custom-accent grammar are\n * substantially translated from Zeron's `Color` and `AccentRoles`\n * (https://github.com/zeronsh/zeron), as Adea's accepted port of them in\n * `packages/ui/src/components/appearance.ts` expresses it. Extracted here so the\n * shared provider and Adea run one implementation; `@adea-ai/themes` owns the\n * colour engine (parsing, contrast, repair, the preset values and their roles).\n * See NOTICE.\n */\n\nimport {\n ACCENTS,\n accentRoles,\n accentValue,\n primarySubtleCss,\n type AccentPreset,\n type AccentRoles as CatalogueAccentRoles,\n} from '@adea-ai/themes'\nimport {\n contrastRatio as canonicalContrastRatio,\n oklchToHex,\n parseColor as parseCanonicalColor,\n repairContrast,\n type Oklch,\n} from '@adea-ai/themes/oklch'\nimport type { ThemeAppearance } from './themes'\n\n/**\n * Accent colour maths and the roles an accent paints.\n *\n * Three questions live here, and none of them touches the document or a signal:\n *\n * - **What colour is this?** `parseColor`, `colorToHex`, `contrastRatio` and\n * `ensureContrast` are the hex helpers a host needs around an accent (an\n * imported theme's colours, a colour field). Hex is the host preference format;\n * the catalogue's colour engine does the maths.\n * - **Is this accent readable on this surface?** The *contrast floor*. A preset is\n * curated, so the catalogue already measured it against its own themes and the\n * floor is the identity for it; a custom `#rrggbb` is anyone's choice, so it is\n * raised until it clears the floor and can never ship unreadable. A host that\n * imports themes (whose backgrounds the catalogue never saw) turns the floor on\n * for presets too.\n * - **Which custom properties does an accent own?** `AccentRoles`, written on\n * `<html>` by the provider and onto one element by {@link paintAccentRoles}.\n */\n\n/* Hex is the host preference format; the shared Themes package owns colour maths. */\n\nexport type RgbColor = Readonly<{ r: number; g: number; b: number; a: number }>\n\nconst HEX = /^#([0-9a-fA-F]+)$/\n\n/** Parse a hex colour (`#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`). `undefined` for anything else. */\nexport function parseColor(value: string): RgbColor | undefined {\n const match = HEX.exec(value.trim())\n const digits = match?.[1]\n if (!digits) return undefined\n if (digits.length === 3 || digits.length === 4) {\n // Each nibble is doubled: `#abc` is `#aabbcc`.\n const [r, g, b, a] = [...digits].map((digit) => Number.parseInt(digit + digit, 16))\n return { r: r!, g: g!, b: b!, a: a ?? 255 }\n }\n if (digits.length === 6 || digits.length === 8) {\n const byte = (index: number) => Number.parseInt(digits.slice(index, index + 2), 16)\n return { r: byte(0), g: byte(2), b: byte(4), a: digits.length === 8 ? byte(6) : 255 }\n }\n return undefined\n}\n\nfunction toHexByte(value: number): string {\n return value.toString(16).padStart(2, '0')\n}\n\n/** `#rrggbb`, or `#rrggbbaa` when the colour is not opaque. */\nexport function colorToHex(color: RgbColor): string {\n const opaque = `#${toHexByte(color.r)}${toHexByte(color.g)}${toHexByte(color.b)}`\n return color.a === 255 ? opaque : `${opaque}${toHexByte(color.a)}`\n}\n\nfunction blendOver(foreground: RgbColor, background: RgbColor): RgbColor {\n const alpha = foreground.a / 255\n const blend = (front: number, back: number) => Math.round(front * alpha + back * (1 - alpha))\n return {\n r: blend(foreground.r, background.r),\n g: blend(foreground.g, background.g),\n b: blend(foreground.b, background.b),\n a: 255,\n }\n}\n\nfunction toCanonicalColor(color: RgbColor): Oklch | undefined {\n return parseCanonicalColor(colorToHex({ ...color, a: 255 }))\n}\n\n/** WCAG contrast ratio between two colours. Alpha composites over the background first. */\nexport function contrastRatio(\n foreground: RgbColor | string,\n background: RgbColor | string\n): number {\n const front = typeof foreground === 'string' ? parseColor(foreground) : foreground\n const back = typeof background === 'string' ? parseColor(background) : background\n if (!front || !back) return 0\n const opaqueFront = front.a === 255 ? front : blendOver(front, back)\n const foregroundColor = toCanonicalColor(opaqueFront)\n const backgroundColor = toCanonicalColor(back)\n if (!foregroundColor || !backgroundColor) return 0\n return canonicalContrastRatio(foregroundColor, backgroundColor)\n}\n\nconst WHITE: RgbColor = { r: 255, g: 255, b: 255, a: 255 }\nconst WHITE_OKLCH: Oklch = { l: 1, c: 0, h: 0 }\n\n/**\n * The nearest colour to `source` that reaches `minimum` against `canvas`, as hex.\n *\n * The catalogue's `repairContrast` is asked for a little more than the floor, in\n * 0.001 steps up to 0.05, because the result is rounded to 8-bit hex and the\n * rounded colour is what is measured: asking for exactly the floor can land a hair\n * under it after rounding. If nothing in that range survives the rounding, white or\n * black stands in, whichever clears the floor.\n */\nfunction repairedHex(source: Oklch, canvas: Oklch, minimum: number): string {\n for (let margin = 0; margin <= 0.05; margin = Number((margin + 0.001).toFixed(3))) {\n const repaired = repairContrast(source, canvas, minimum + margin)\n if (!repaired.satisfied) continue\n const hex = oklchToHex(repaired.color)\n const rounded = parseCanonicalColor(hex)\n if (rounded && canonicalContrastRatio(rounded, canvas) >= minimum) return hex\n }\n return canonicalContrastRatio(WHITE_OKLCH, canvas) >= minimum ? '#ffffff' : '#000000'\n}\n\n/** Preserve the host hex shape while delegating contrast repair to the catalogue. */\nexport function ensureContrast(color: RgbColor, background: RgbColor, minimum: number): RgbColor {\n if (contrastRatio(color, background) >= minimum) return color\n\n const renderedColor = color.a === 255 ? color : blendOver(color, background)\n const source = toCanonicalColor(renderedColor)\n const canvas = toCanonicalColor(background)\n if (!source || !canvas) return color\n return parseColor(repairedHex(source, canvas, minimum))!\n}\n\n/** The contrast an accent is held to against its surface unless the host sets another. */\nexport const DEFAULT_ACCENT_CONTRAST_FLOOR = 3\n\n/**\n * Normalise a custom accent against a surface: unparseable input is rejected\n * (`undefined`), and any accepted colour is raised to `minimum` so an accent can\n * never ship unreadable. Accepts every CSS hex length; for the stored grammar use\n * {@link normalizeCustomAccent}.\n */\nexport function normalizeAccentValue(\n value: string,\n background: string,\n minimum: number = DEFAULT_ACCENT_CONTRAST_FLOOR\n): string | undefined {\n const parsed = parseColor(value)\n if (!parsed) return undefined\n const backdrop = parseColor(background) ?? WHITE\n return colorToHex(ensureContrast(parsed, backdrop, minimum))\n}\n\n/**\n * The stored form of a custom accent: `#rrggbb`, six digits, opaque, lower case.\n *\n * Deliberately narrower than {@link parseColor}. A stored accent is compared,\n * deduplicated and used as an attribute, so it has one spelling; a host that\n * accepts `#abc` in a field expands it before storing. Anything else — a name, an\n * `rgb()`, a short or alpha form, a non-string — is not an accent and returns\n * `undefined`.\n */\nexport function normalizeCustomAccent(value: unknown): string | undefined {\n return typeof value === 'string' && /^#[0-9a-fA-F]{6}$/.test(value)\n ? value.toLowerCase()\n : undefined\n}\n\n/** Whether a selection's accent is the custom `#rrggbb` form (any letter case). */\nexport function isCustomAccent(value: unknown): boolean {\n return normalizeCustomAccent(value) !== undefined\n}\n\n/**\n * The roles an accent paints, as CSS values: the catalogue's `AccentRoles`, so the\n * provider, the generated stylesheet and a host's own paint cannot disagree about\n * what a role is. `primarySubtle` is an expression over `var(--primary)`; the\n * document writer never sets it (see {@link accentOverrideVariables}), an element\n * paint must.\n */\nexport type AccentRoles = CatalogueAccentRoles\n\n/** What an accent resolves against: the surface it sits on and the theme it sits in. */\nexport type AccentTarget = {\n /** The theme, or only its appearance. A theme gives the tint its measured strength. */\n target: Parameters<typeof accentRoles>[1]\n /** The surface the accent must read on. Without it nothing can be measured or repaired. */\n background: string | undefined\n /**\n * The contrast to hold the accent to against `background`. Absent, a preset is\n * painted verbatim and a custom colour is held to {@link DEFAULT_ACCENT_CONTRAST_FLOOR}.\n */\n floor?: number\n /**\n * The accents the selected theme pair carries (`ansi-blue`…), for an id that is\n * neither a preset nor a colour.\n */\n themeAccents?: readonly AccentPreset[]\n}\n\nfunction appearanceOf(target: AccentTarget['target']): ThemeAppearance {\n return typeof target === 'string' ? target : target.appearance\n}\n\n/**\n * `value` raised to `floor` against `background` as hex, or `value` itself when it\n * already clears the floor, is not a colour, or there is no surface to measure it on.\n *\n * Returning the argument itself is what makes the floor the identity for a preset\n * that already reads: the caller compares and keeps the catalogue's own roles.\n */\nfunction gateColor(value: string, background: string | undefined, floor: number): string {\n const source = parseCanonicalColor(value)\n const canvas = background ? parseCanonicalColor(background) : undefined\n if (!source || !canvas) return value\n if (canonicalContrastRatio(source, canvas) >= floor) return value\n return repairedHex(source, canvas, floor)\n}\n\n/**\n * The roles for a preset (or a custom colour made into one), held to the floor.\n *\n * A preset that clears the floor keeps the catalogue's roles untouched. One that\n * does not is repaired to the nearest passing colour and the whole role set is\n * rebuilt from the repaired value, so the label, hover and ring follow it instead\n * of describing a colour that is no longer on screen.\n */\nexport function accentRolesForPreset(\n preset: AccentPreset,\n context: AccentTarget\n): AccentRoles | undefined {\n const appearance = appearanceOf(context.target)\n const requested = accentValue(preset, appearance)\n if (!parseCanonicalColor(requested)) return undefined\n\n const floor = context.floor\n if (floor === undefined) return accentRoles(preset, context.target)\n\n const gated = gateColor(requested, context.background, floor)\n if (gated === requested) return accentRoles(preset, context.target)\n\n const roles = accentRoles({ ...preset, light: gated, dark: gated }, context.target)\n // The tint's strength is measured against the primary it will sit over, and the\n // repaired primary is not one the theme's own measurement saw.\n return { ...roles, primarySubtle: primarySubtleCss(context.target, '--primary', [gated]) }\n}\n\n/**\n * Resolve an accent selection to its roles, or `undefined` when it overrides nothing.\n *\n * `undefined` is \"use the theme's own primary\": the literal `theme`, an id that is\n * neither a preset, an offered theme accent nor a colour, and a colour that does not\n * parse. A host that wants one of those to mean something else decides before\n * asking (Adea's codec maps a stored `theme` to its default preset when it reads).\n */\nexport function resolveAccentRoles(accent: string, context: AccentTarget): AccentRoles | undefined {\n if (accent === 'theme') return undefined\n\n const custom = normalizeCustomAccent(accent)\n if (custom) {\n const floor = context.floor ?? DEFAULT_ACCENT_CONTRAST_FLOOR\n return accentRolesForPreset(\n { id: 'custom', label: 'Custom', description: '', light: custom, dark: custom },\n { ...context, floor }\n )\n }\n\n const preset =\n ACCENTS.find((candidate) => candidate.id === accent) ??\n context.themeAccents?.find((candidate) => candidate.id === accent)\n return preset ? accentRolesForPreset(preset, context) : undefined\n}\n\n/**\n * The custom properties an accent override owns on `<html>`.\n *\n * `--primary-subtle` is deliberately not among them. It is a `color-mix()` over\n * `var(--primary)`, so on the same element it follows an accent by itself, and its\n * strength is the theme's, measured to keep body text at 4.5:1 on the tint.\n * Writing one here would replace that measurement with the argument's.\n */\nexport function accentOverrideVariables(roles: AccentRoles): Record<string, string> {\n return {\n '--primary': roles.primary,\n '--primary-foreground': roles.primaryForeground,\n '--primary-hover': roles.primaryHover,\n '--ring': roles.ring,\n }\n}\n\n/** The custom properties {@link paintAccentRoles} writes and clears. */\nexport const ACCENT_PAINT_PROPERTIES = [\n '--primary',\n '--primary-foreground',\n '--primary-hover',\n '--primary-subtle',\n '--ring',\n] as const\n\n/**\n * Scope an accent to one element and its descendants.\n *\n * Unlike the document, an element must write `--primary-subtle` too. A custom\n * property that uses `var()` is resolved where it is declared, so the tint inherited\n * from `<html>` is already a mix over *`<html>`'s* primary and an element that only\n * overrides `--primary` would keep the old tint. Declared here, the same expression\n * resolves against this element's primary.\n *\n * `undefined` clears every property, and the element inherits the document's accent\n * again.\n */\nexport function paintAccentRoles(\n element: { style: Pick<CSSStyleDeclaration, 'setProperty' | 'removeProperty'> },\n roles: AccentRoles | undefined\n): void {\n if (!roles) {\n for (const property of ACCENT_PAINT_PROPERTIES) element.style.removeProperty(property)\n return\n }\n element.style.setProperty('--primary', roles.primary)\n element.style.setProperty('--primary-foreground', roles.primaryForeground)\n element.style.setProperty('--primary-hover', roles.primaryHover)\n element.style.setProperty('--primary-subtle', roles.primarySubtle)\n element.style.setProperty('--ring', roles.ring)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"accent-roles.js","names":["parseCanonicalColor","canonicalContrastRatio"],"sources":["../../src/lib/accent-roles.ts"],"sourcesContent":["/*\n * Copyright (c) 2026 Wing\n * SPDX-License-Identifier: MIT\n *\n * The colour helpers, the contrast gate and the custom-accent grammar are\n * substantially translated from Zeron's `Color` and `AccentRoles`\n * (https://github.com/zeronsh/zeron), as Adea's accepted port of them in\n * `packages/ui/src/components/appearance.ts` expresses it. Extracted here so the\n * shared provider and Adea run one implementation; `@adea-ai/themes` owns the\n * colour engine (parsing, contrast, repair, the preset values and their roles).\n * See NOTICE.\n */\n\nimport {\n ACCENTS,\n accentRoles,\n accentValue,\n primarySubtleCss,\n type AccentPreset,\n type AccentRoles as CatalogueAccentRoles,\n} from '@adea-ai/themes'\nimport {\n contrastRatio as canonicalContrastRatio,\n oklchToHex,\n parseColor as parseCanonicalColor,\n repairContrast,\n type Oklch,\n} from '@adea-ai/themes/oklch'\nimport type { ThemeAppearance } from './themes'\n\n/**\n * Accent colour maths and the roles an accent paints.\n *\n * Three questions live here, and none of them touches the document or a signal:\n *\n * - **What colour is this?** `parseColor`, `colorToHex`, `contrastRatio` and\n * `ensureContrast` are the hex helpers a host needs around an accent (an\n * imported theme's colours, a colour field). Hex is the host preference format;\n * the catalogue's colour engine does the maths.\n * - **Is this accent readable on this surface?** The *contrast floor*. A preset is\n * curated, so the catalogue already measured it against its own themes and the\n * floor is the identity for it; a custom `#rrggbb` is anyone's choice, so it is\n * raised until it clears the floor and can never ship unreadable. A host that\n * imports themes (whose backgrounds the catalogue never saw) turns the floor on\n * for presets too.\n * - **Which custom properties does an accent own?** `AccentRoles`, written on\n * `<html>` by the provider and onto one element by {@link paintAccentRoles}.\n */\n\n/* Hex is the host preference format; the shared Themes package owns colour maths. */\n\nexport type RgbColor = Readonly<{ r: number; g: number; b: number; a: number }>\n\nconst HEX = /^#([0-9a-fA-F]+)$/\n\n/** Parse a hex colour (`#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`). `undefined` for anything else. */\nexport function parseColor(value: string): RgbColor | undefined {\n const match = HEX.exec(value.trim())\n const digits = match?.[1]\n if (!digits) return undefined\n if (digits.length === 3 || digits.length === 4) {\n // Each nibble is doubled: `#abc` is `#aabbcc`.\n const [r, g, b, a] = [...digits].map((digit) => Number.parseInt(digit + digit, 16))\n return { r: r!, g: g!, b: b!, a: a ?? 255 }\n }\n if (digits.length === 6 || digits.length === 8) {\n const byte = (index: number) => Number.parseInt(digits.slice(index, index + 2), 16)\n return { r: byte(0), g: byte(2), b: byte(4), a: digits.length === 8 ? byte(6) : 255 }\n }\n return undefined\n}\n\nfunction toHexByte(value: number): string {\n return value.toString(16).padStart(2, '0')\n}\n\n/** `#rrggbb`, or `#rrggbbaa` when the colour is not opaque. */\nexport function colorToHex(color: RgbColor): string {\n const opaque = `#${toHexByte(color.r)}${toHexByte(color.g)}${toHexByte(color.b)}`\n return color.a === 255 ? opaque : `${opaque}${toHexByte(color.a)}`\n}\n\nfunction blendOver(foreground: RgbColor, background: RgbColor): RgbColor {\n const alpha = foreground.a / 255\n const blend = (front: number, back: number) => Math.round(front * alpha + back * (1 - alpha))\n return {\n r: blend(foreground.r, background.r),\n g: blend(foreground.g, background.g),\n b: blend(foreground.b, background.b),\n a: 255,\n }\n}\n\nfunction toCanonicalColor(color: RgbColor): Oklch | undefined {\n return parseCanonicalColor(colorToHex({ ...color, a: 255 }))\n}\n\n/** WCAG contrast ratio between two colours. Alpha composites over the background first. */\nexport function contrastRatio(\n foreground: RgbColor | string,\n background: RgbColor | string\n): number {\n const front = typeof foreground === 'string' ? parseColor(foreground) : foreground\n const back = typeof background === 'string' ? parseColor(background) : background\n if (!front || !back) return 0\n const opaqueFront = front.a === 255 ? front : blendOver(front, back)\n const foregroundColor = toCanonicalColor(opaqueFront)\n const backgroundColor = toCanonicalColor(back)\n if (!foregroundColor || !backgroundColor) return 0\n return canonicalContrastRatio(foregroundColor, backgroundColor)\n}\n\nconst WHITE: RgbColor = { r: 255, g: 255, b: 255, a: 255 }\nconst WHITE_OKLCH: Oklch = { l: 1, c: 0, h: 0 }\n\n/**\n * The nearest colour to `source` that reaches `minimum` against `canvas`, as hex.\n *\n * The catalogue's `repairContrast` is asked for a little more than the floor, in\n * 0.001 steps up to 0.05, because the result is rounded to 8-bit hex and the\n * rounded colour is what is measured: asking for exactly the floor can land a hair\n * under it after rounding. If nothing in that range survives the rounding, white or\n * black stands in, whichever clears the floor.\n */\nfunction repairedHex(source: Oklch, canvas: Oklch, minimum: number): string {\n for (let margin = 0; margin <= 0.05; margin = Number((margin + 0.001).toFixed(3))) {\n const repaired = repairContrast(source, canvas, minimum + margin)\n if (!repaired.satisfied) continue\n const hex = oklchToHex(repaired.color)\n const rounded = parseCanonicalColor(hex)\n if (rounded && canonicalContrastRatio(rounded, canvas) >= minimum) return hex\n }\n return canonicalContrastRatio(WHITE_OKLCH, canvas) >= minimum ? '#ffffff' : '#000000'\n}\n\n/** Preserve the host hex shape while delegating contrast repair to the catalogue. */\nexport function ensureContrast(color: RgbColor, background: RgbColor, minimum: number): RgbColor {\n if (contrastRatio(color, background) >= minimum) return color\n\n const renderedColor = color.a === 255 ? color : blendOver(color, background)\n const source = toCanonicalColor(renderedColor)\n const canvas = toCanonicalColor(background)\n if (!source || !canvas) return color\n return parseColor(repairedHex(source, canvas, minimum))!\n}\n\n/** The contrast an accent is held to against its surface unless the host sets another. */\nexport const DEFAULT_ACCENT_CONTRAST_FLOOR = 3\n\n/**\n * Normalise a custom accent against a surface: unparseable input is rejected\n * (`undefined`), and any accepted colour is raised to `minimum` so an accent can\n * never ship unreadable. Accepts every CSS hex length; for the stored grammar use\n * {@link normalizeCustomAccent}.\n */\nexport function normalizeAccentValue(\n value: string,\n background: string,\n minimum: number = DEFAULT_ACCENT_CONTRAST_FLOOR\n): string | undefined {\n const parsed = parseColor(value)\n if (!parsed) return undefined\n const backdrop = parseColor(background) ?? WHITE\n return colorToHex(ensureContrast(parsed, backdrop, minimum))\n}\n\n/**\n * The stored form of a custom accent: `#rrggbb`, six digits, opaque, lower case.\n *\n * Deliberately narrower than {@link parseColor}. A stored accent is compared,\n * deduplicated and used as an attribute, so it has one spelling; a host that\n * accepts `#abc` in a field expands it before storing. Anything else — a name, an\n * `rgb()`, a short or alpha form, a non-string — is not an accent and returns\n * `undefined`.\n */\nexport function normalizeCustomAccent(value: unknown): string | undefined {\n return typeof value === 'string' && /^#[0-9a-fA-F]{6}$/.test(value)\n ? value.toLowerCase()\n : undefined\n}\n\n/** Whether a selection's accent is the custom `#rrggbb` form (any letter case). */\nexport function isCustomAccent(value: unknown): boolean {\n return normalizeCustomAccent(value) !== undefined\n}\n\n/**\n * The roles an accent paints, as CSS values: the catalogue's `AccentRoles`, so the\n * provider, the generated stylesheet and a host's own paint cannot disagree about\n * what a role is. `primarySubtle` is an expression over `var(--primary)`; the\n * document writer never sets it (see {@link accentOverrideVariables}), an element\n * paint must.\n */\nexport type AccentRoles = CatalogueAccentRoles\n\n/** What an accent resolves against: the surface it sits on and the theme it sits in. */\nexport type AccentTarget = {\n /** The theme, or only its appearance. A theme gives the tint its measured strength. */\n target: Parameters<typeof accentRoles>[1]\n /** The surface the accent must read on. Without it nothing can be measured or repaired. */\n background: string | undefined\n /**\n * The contrast to hold the accent to against `background`. Absent, a preset is\n * painted verbatim and a custom colour is held to {@link DEFAULT_ACCENT_CONTRAST_FLOOR}.\n */\n floor?: number\n /**\n * The accents the selected theme pair carries (`ansi-blue`…), for an id that is\n * neither a preset nor a colour.\n */\n themeAccents?: readonly AccentPreset[]\n}\n\nfunction appearanceOf(target: AccentTarget['target']): ThemeAppearance {\n return typeof target === 'string' ? target : target.appearance\n}\n\n/**\n * `value` raised to `floor` against `background` as hex, or `value` itself when it\n * already clears the floor, is not a colour, or there is no surface to measure it on.\n *\n * Returning the argument itself is what makes the floor the identity for a preset\n * that already reads: the caller compares and keeps the catalogue's own roles.\n */\nfunction gateColor(value: string, background: string | undefined, floor: number): string {\n const source = parseCanonicalColor(value)\n const canvas = background ? parseCanonicalColor(background) : undefined\n if (!source || !canvas) return value\n if (canonicalContrastRatio(source, canvas) >= floor) return value\n return repairedHex(source, canvas, floor)\n}\n\n/**\n * The roles for a preset (or a custom colour made into one), held to the floor.\n *\n * A preset that clears the floor keeps the catalogue's roles untouched. One that\n * does not is repaired to the nearest passing colour and the whole role set is\n * rebuilt from the repaired value, so the label, hover and ring follow it instead\n * of describing a colour that is no longer on screen.\n */\nexport function accentRolesForPreset(\n preset: AccentPreset,\n context: AccentTarget\n): AccentRoles | undefined {\n const appearance = appearanceOf(context.target)\n const requested = accentValue(preset, appearance)\n if (!parseCanonicalColor(requested)) return undefined\n\n const floor = context.floor\n if (floor === undefined) return accentRoles(preset, context.target)\n\n const gated = gateColor(requested, context.background, floor)\n if (gated === requested) return accentRoles(preset, context.target)\n\n const roles = accentRoles({ ...preset, light: gated, dark: gated }, context.target)\n // The tint's strength is measured against the primary it will sit over, and the\n // repaired primary is not one the theme's own measurement saw.\n return { ...roles, primarySubtle: primarySubtleCss(context.target, '--primary', [gated]) }\n}\n\n/**\n * Resolve an accent selection to its roles, or `undefined` when it overrides nothing.\n *\n * `undefined` is \"use the theme's own primary\": the literal `theme`, an id that is\n * neither a preset, an offered theme accent nor a colour, and a colour that does not\n * parse. A host that wants one of those to mean something else decides before\n * asking (Adea's codec maps a stored `theme` to its default preset when it reads).\n */\nexport function resolveAccentRoles(accent: string, context: AccentTarget): AccentRoles | undefined {\n if (accent === 'theme') return undefined\n\n const custom = normalizeCustomAccent(accent)\n if (custom) {\n const floor = context.floor ?? DEFAULT_ACCENT_CONTRAST_FLOOR\n return accentRolesForPreset(\n { id: 'custom', label: 'Custom', description: '', light: custom, dark: custom },\n { ...context, floor }\n )\n }\n\n const preset =\n ACCENTS.find((candidate) => candidate.id === accent) ??\n context.themeAccents?.find((candidate) => candidate.id === accent)\n return preset ? accentRolesForPreset(preset, context) : undefined\n}\n\n/**\n * The custom properties an accent override owns on `<html>`.\n *\n * `--primary-subtle` is deliberately not among them. It is a `color-mix()` over\n * `var(--primary)`, so on the same element it follows an accent by itself, and its\n * strength is the theme's, measured to keep body text at 4.5:1 on the tint.\n * Writing one here would replace that measurement with the argument's.\n */\nexport function accentOverrideVariables(roles: AccentRoles): Record<string, string> {\n return {\n '--primary': roles.primary,\n '--primary-foreground': roles.primaryForeground,\n '--primary-hover': roles.primaryHover,\n '--ring': roles.ring,\n }\n}\n\n/** The custom properties {@link paintAccentRoles} writes and clears. */\nexport const ACCENT_PAINT_PROPERTIES = [\n '--primary',\n '--primary-foreground',\n '--primary-hover',\n '--primary-subtle',\n '--ring',\n] as const\n\n/**\n * Scope an accent to one element and its descendants.\n *\n * Unlike the document, an element must write `--primary-subtle` too. A custom\n * property that uses `var()` is resolved where it is declared, so the tint inherited\n * from `<html>` is already a mix over *`<html>`'s* primary and an element that only\n * overrides `--primary` would keep the old tint. Declared here, the same expression\n * resolves against this element's primary.\n *\n * `undefined` clears every property, and the element inherits the document's accent\n * again.\n */\nexport function paintAccentRoles(\n element: { style: Pick<CSSStyleDeclaration, 'setProperty' | 'removeProperty'> },\n roles: AccentRoles | undefined\n): void {\n if (!roles) {\n for (const property of ACCENT_PAINT_PROPERTIES) element.style.removeProperty(property)\n return\n }\n element.style.setProperty('--primary', roles.primary)\n element.style.setProperty('--primary-foreground', roles.primaryForeground)\n element.style.setProperty('--primary-hover', roles.primaryHover)\n element.style.setProperty('--primary-subtle', roles.primarySubtle)\n element.style.setProperty('--ring', roles.ring)\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { normalizeAppearanceEditorFontSettings } from "./appearance-font-settings.js";
1
+ import { APPEARANCE_EDITOR_FONT_AXES, normalizeAppearanceEditorFontSettings } from "./appearance-font-settings.js";
2
2
  //#region src/lib/font-zoom.ts
3
3
  /**
4
4
  * Classify the platform zoom chord: Cmd (macOS) or Ctrl (other platforms)
@@ -39,7 +39,9 @@ function stepAppearanceFontSizes(fonts, delta) {
39
39
  }
40
40
  };
41
41
  }
42
+ /** Every axis name, for callers that want to report or test the stepping. */
43
+ var FONT_ZOOM_AXES = APPEARANCE_EDITOR_FONT_AXES;
42
44
  //#endregion
43
- export { fontZoomShortcut, stepAppearanceFontSizes };
45
+ export { FONT_ZOOM_AXES, fontZoomShortcut, stepAppearanceFontSizes };
44
46
 
45
47
  //# sourceMappingURL=font-zoom.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"font-zoom.js","names":[],"sources":["../../src/lib/font-zoom.ts"],"sourcesContent":["/*\n * Copyright (c) 2026 Wing\n * Licensed under the MIT License.\n *\n * The platform zoom chords, mapped onto Adea's three text roles.\n *\n * macOS (and Windows/Linux) zoom shortcuts — Cmd/Ctrl with `=`, `+`, `-`, or\n * `0` — normally scale the browser's page zoom. In Adea every text element is\n * already tied to one of the three appearance font tiers (the UI tier drives\n * the whole `text-*` scale through `--font-ui-scale`; content and code roles\n * scale through their own tokens), so stepping those tiers reproduces zoom\n * while keeping the app inside its tokenized, persisted type system: the page\n * layout, menus, sheets, and terminals reflow live from the same projection\n * the appearance editor writes.\n *\n * Translated from Adea's accepted port (`packages/ui/src/components/font-zoom.ts`) so\n * the shared provider and Adea run one implementation. See NOTICE.\n */\nimport {\n APPEARANCE_EDITOR_FONT_AXES,\n APPEARANCE_EDITOR_FONT_SIZE_MAX,\n APPEARANCE_EDITOR_FONT_SIZE_MIN,\n normalizeAppearanceEditorFontSettings,\n type AppearanceEditorFontSettings,\n} from '#lib/appearance-font-settings'\n\n/** What the user's zoom chord asked for. */\nexport type FontZoomIntent = 'in' | 'out' | 'reset'\n\n/**\n * Classify the platform zoom chord: Cmd (macOS) or Ctrl (other platforms)\n * with `=`/`+` zooming in, `-` zooming out, and `0` resetting to the System\n * defaults. Unmodified keys, Alt-modified chords, and IME composition never\n * count, so typing and text editing are untouched.\n */\nexport function fontZoomShortcut(event: {\n key: string\n metaKey: boolean\n ctrlKey: boolean\n altKey: boolean\n isComposing?: boolean\n}): FontZoomIntent | undefined {\n if (event.isComposing || event.altKey) return undefined\n if (!(event.metaKey || event.ctrlKey)) return undefined\n if (event.key === '=' || event.key === '+') return 'in'\n if (event.key === '-') return 'out'\n if (event.key === '0') return 'reset'\n return undefined\n}\n\n/** Clamp a pixel size to the shared range the appearance editor enforces. */\nfunction clampFontSize(size: number): number {\n return Math.min(APPEARANCE_EDITOR_FONT_SIZE_MAX, Math.max(APPEARANCE_EDITOR_FONT_SIZE_MIN, size))\n}\n\n/**\n * Step every text tier by `delta` pixels, clamped to that range. The axes move\n * together — that is what makes the chord read as zoom — and each keeps its\n * own family choice.\n */\nexport function stepAppearanceFontSizes(\n fonts: unknown,\n delta: 1 | -1\n): AppearanceEditorFontSettings {\n const current = normalizeAppearanceEditorFontSettings(fonts).settings\n return {\n ui: { ...current.ui, size: clampFontSize(current.ui.size + delta) },\n content: { ...current.content, size: clampFontSize(current.content.size + delta) },\n code: { ...current.code, size: clampFontSize(current.code.size + delta) },\n }\n}\n\n/** Every axis name, for callers that want to report or test the stepping. */\nexport const FONT_ZOOM_AXES = APPEARANCE_EDITOR_FONT_AXES\n"],"mappings":";;;;;;;;AAmCA,SAAgB,iBAAiB,OAMF;CAC7B,IAAI,MAAM,eAAe,MAAM,QAAQ,OAAO,KAAA;CAC9C,IAAI,EAAE,MAAM,WAAW,MAAM,UAAU,OAAO,KAAA;CAC9C,IAAI,MAAM,QAAQ,OAAO,MAAM,QAAQ,KAAK,OAAO;CACnD,IAAI,MAAM,QAAQ,KAAK,OAAO;CAC9B,IAAI,MAAM,QAAQ,KAAK,OAAO;AAEhC;;AAGA,SAAS,cAAc,MAAsB;CAC3C,OAAO,KAAK,IAAA,IAAqC,KAAK,IAAA,IAAqC,IAAI,CAAC;AAClG;;;;;;AAOA,SAAgB,wBACd,OACA,OAC8B;CAC9B,MAAM,UAAU,sCAAsC,KAAK,CAAC,CAAC;CAC7D,OAAO;EACL,IAAI;GAAE,GAAG,QAAQ;GAAI,MAAM,cAAc,QAAQ,GAAG,OAAO,KAAK;EAAE;EAClE,SAAS;GAAE,GAAG,QAAQ;GAAS,MAAM,cAAc,QAAQ,QAAQ,OAAO,KAAK;EAAE;EACjF,MAAM;GAAE,GAAG,QAAQ;GAAM,MAAM,cAAc,QAAQ,KAAK,OAAO,KAAK;EAAE;CAC1E;AACF"}
1
+ {"version":3,"file":"font-zoom.js","names":[],"sources":["../../src/lib/font-zoom.ts"],"sourcesContent":["/*\n * Copyright (c) 2026 Wing\n * Licensed under the MIT License.\n *\n * The platform zoom chords, mapped onto Adea's three text roles.\n *\n * macOS (and Windows/Linux) zoom shortcuts — Cmd/Ctrl with `=`, `+`, `-`, or\n * `0` — normally scale the browser's page zoom. In Adea every text element is\n * already tied to one of the three appearance font tiers (the UI tier drives\n * the whole `text-*` scale through `--font-ui-scale`; content and code roles\n * scale through their own tokens), so stepping those tiers reproduces zoom\n * while keeping the app inside its tokenized, persisted type system: the page\n * layout, menus, sheets, and terminals reflow live from the same projection\n * the appearance editor writes.\n *\n * Translated from Adea's accepted port (`packages/ui/src/components/font-zoom.ts`) so\n * the shared provider and Adea run one implementation. See NOTICE.\n */\nimport {\n APPEARANCE_EDITOR_FONT_AXES,\n APPEARANCE_EDITOR_FONT_SIZE_MAX,\n APPEARANCE_EDITOR_FONT_SIZE_MIN,\n normalizeAppearanceEditorFontSettings,\n type AppearanceEditorFontSettings,\n} from '#lib/appearance-font-settings'\n\n/** What the user's zoom chord asked for. */\nexport type FontZoomIntent = 'in' | 'out' | 'reset'\n\n/**\n * Classify the platform zoom chord: Cmd (macOS) or Ctrl (other platforms)\n * with `=`/`+` zooming in, `-` zooming out, and `0` resetting to the System\n * defaults. Unmodified keys, Alt-modified chords, and IME composition never\n * count, so typing and text editing are untouched.\n */\nexport function fontZoomShortcut(event: {\n key: string\n metaKey: boolean\n ctrlKey: boolean\n altKey: boolean\n isComposing?: boolean\n}): FontZoomIntent | undefined {\n if (event.isComposing || event.altKey) return undefined\n if (!(event.metaKey || event.ctrlKey)) return undefined\n if (event.key === '=' || event.key === '+') return 'in'\n if (event.key === '-') return 'out'\n if (event.key === '0') return 'reset'\n return undefined\n}\n\n/** Clamp a pixel size to the shared range the appearance editor enforces. */\nfunction clampFontSize(size: number): number {\n return Math.min(APPEARANCE_EDITOR_FONT_SIZE_MAX, Math.max(APPEARANCE_EDITOR_FONT_SIZE_MIN, size))\n}\n\n/**\n * Step every text tier by `delta` pixels, clamped to that range. The axes move\n * together — that is what makes the chord read as zoom — and each keeps its\n * own family choice.\n */\nexport function stepAppearanceFontSizes(\n fonts: unknown,\n delta: 1 | -1\n): AppearanceEditorFontSettings {\n const current = normalizeAppearanceEditorFontSettings(fonts).settings\n return {\n ui: { ...current.ui, size: clampFontSize(current.ui.size + delta) },\n content: { ...current.content, size: clampFontSize(current.content.size + delta) },\n code: { ...current.code, size: clampFontSize(current.code.size + delta) },\n }\n}\n\n/** Every axis name, for callers that want to report or test the stepping. */\nexport const FONT_ZOOM_AXES = APPEARANCE_EDITOR_FONT_AXES\n"],"mappings":";;;;;;;;AAmCA,SAAgB,iBAAiB,OAMF;CAC7B,IAAI,MAAM,eAAe,MAAM,QAAQ,OAAO,KAAA;CAC9C,IAAI,EAAE,MAAM,WAAW,MAAM,UAAU,OAAO,KAAA;CAC9C,IAAI,MAAM,QAAQ,OAAO,MAAM,QAAQ,KAAK,OAAO;CACnD,IAAI,MAAM,QAAQ,KAAK,OAAO;CAC9B,IAAI,MAAM,QAAQ,KAAK,OAAO;AAEhC;;AAGA,SAAS,cAAc,MAAsB;CAC3C,OAAO,KAAK,IAAA,IAAqC,KAAK,IAAA,IAAqC,IAAI,CAAC;AAClG;;;;;;AAOA,SAAgB,wBACd,OACA,OAC8B;CAC9B,MAAM,UAAU,sCAAsC,KAAK,CAAC,CAAC;CAC7D,OAAO;EACL,IAAI;GAAE,GAAG,QAAQ;GAAI,MAAM,cAAc,QAAQ,GAAG,OAAO,KAAK;EAAE;EAClE,SAAS;GAAE,GAAG,QAAQ;GAAS,MAAM,cAAc,QAAQ,QAAQ,OAAO,KAAK;EAAE;EACjF,MAAM;GAAE,GAAG,QAAQ;GAAM,MAAM,cAAc,QAAQ,KAAK,OAAO,KAAK;EAAE;CAC1E;AACF;;AAGA,IAAa,iBAAiB"}
@@ -107,6 +107,8 @@ var menuSwatchTone = cva("size-2 shrink-0 rounded-full", {
107
107
  } },
108
108
  defaultVariants: { tone: "neutral" }
109
109
  });
110
+ /** Padding shared by every menu-list surface. */
111
+ var menuContentPadding = "p-1";
110
112
  /**
111
113
  * Marks a floating layer — a menu, a listbox, a popover — as part of the top
112
114
  * layer.
@@ -121,6 +123,6 @@ var menuSwatchTone = cva("size-2 shrink-0 rounded-full", {
121
123
  */
122
124
  var topLayerProps = { "data-kb-top-layer": "" };
123
125
  //#endregion
124
- export { destructiveMenuItem, dialogSurface, menuItem, menuLabel, menuSeparator, menuSwatchTone, overlayMotion, overlayPositioner, overlayScrim, overlaySurface, overlayTitle, popoverArrow, popoverMotion, tooltipTip, topLayerProps };
126
+ export { destructiveMenuItem, dialogSurface, menuContentPadding, menuItem, menuLabel, menuSeparator, menuSwatchTone, overlayMotion, overlayPositioner, overlayScrim, overlaySurface, overlayTitle, popoverArrow, popoverMotion, tooltipTip, topLayerProps };
125
127
 
126
128
  //# sourceMappingURL=overlay.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"overlay.js","names":[],"sources":["../../src/lib/overlay.ts"],"sourcesContent":["/**\n * Shared strings for the overlay layer: dialogs, sheets, menus, popovers,\n * tooltips and the command palette.\n *\n * These live here rather than in each component because the overlay family is\n * where consistency is most visible and least likely to survive duplication.\n * A dialog, a sheet and a dropdown that disagree about their border, radius,\n * motion or shadow read as three different apps — and an app that adopts this\n * library would inherit all three. Declaring the surface once is what makes\n * \"an overlay looks like this\" a fact rather than a convention.\n */\n\nimport { cva, type VariantProps } from '#lib/variants'\n\n/** The panel itself: fill, hairline edge, radius, elevation. Menus, popovers\n * and the command palette float on the elevated overlay rung (`--popover`). */\nexport const overlaySurface =\n 'bg-popover text-popover-foreground rounded-xl border border-border shadow-lg'\n\n/**\n * The modal panel: the same hairline edge, radius and elevation, painted on the\n * *neutral* theme surface instead of the elevated overlay rung.\n *\n * `--popover` is the catalogue's `surfaceElevated`, whose derivation amplifies\n * the canvas hue's chroma on the elevated step. On the dark default's violet\n * canvas that makes the dialog the most chromatic surface in the ladder —\n * `oklch(0.2406 0.0088 300.91)`, red above green in sRGB — and on a full dialog\n * panel that cast reads as a warm tint the canvas never has. shadcn's own\n * convention already splits the two: a Dialog paints `bg-background`, menus and\n * popovers paint `bg-popover`. The veil, the hairline and the elevation shadow\n * carry the separation the lighter fill no longer provides.\n */\nexport const dialogSurface =\n 'bg-background text-foreground rounded-xl border border-border shadow-lg'\n\n/**\n * Overlay titles have one fixed typography role. Keep this small recipe shared\n * without loading the configurable Heading component into every dialog.\n */\nexport const overlayTitle = 'font-semibold text-base tracking-tight leading-none'\n\n/** The span of the portal wrapper that centres a dialog-like overlay. */\nexport const overlayPositioner = 'fixed inset-0 z-(--z-dialog) grid place-items-center p-4'\n\n/** The dimming layer behind a modal overlay. */\nexport const overlayScrim =\n 'fixed inset-0 z-(--z-dialog) bg-scrim/50 supports-[backdrop-filter]:backdrop-blur-xs'\n\n/**\n * The tip: the card-toned bubble an explanation shows in. The side rail draws\n * it flush to a collapsed row and every floating tooltip composes the same\n * fill, edge, and type, so a control never explains itself in two visual\n * languages.\n */\nexport const tooltipTip =\n 'bg-card text-card-foreground rounded-md border border-border text-sm font-medium shadow-lg'\n\n/**\n * Entrance and exit motion, expressed as data-state classes.\n *\n * `data-expanded` / `data-closed` are Kobalte's; `data-side-*` come from its\n * popper positioning and let a menu slide in from the edge it is anchored to,\n * which is what makes a dropdown feel attached to its trigger rather than\n * dropped on the screen. Exit motion is shorter than entry, because a\n * dismissal should feel immediate while an arrival benefits from being seen.\n */\nexport const overlayMotion = [\n 'data-expanded:animate-in data-expanded:fade-in-0 data-expanded:zoom-in-95',\n 'data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',\n 'data-expanded:duration-200 data-closed:duration-150',\n].join(' ')\n\n/** Popover-style motion: no zoom, so a panel anchored to a control does not\n * appear to breathe away from it. */\nexport const popoverMotion = [\n 'data-expanded:animate-in data-expanded:fade-in-0 data-closed:animate-out data-closed:fade-out-0',\n 'data-expanded:duration-150 data-closed:duration-100',\n].join(' ')\n\n/** The popper arrow. Sized in the component; coloured here. */\nexport const popoverArrow = 'fill-popover stroke-border'\n\n/**\n * A menu row: the shared height, radius, hover fill and focus treatment for\n * every item inside a menu, listbox or command list.\n *\n * The row sizes and dims the icons a caller puts in it, but not the glyph of\n * its selection indicator (any `[data-slot$=-indicator]`): a descendant `svg`\n * rule outranks a single utility on the glyph, so without the exclusion a\n * radio item's `size-2` dot drew as a 16px muted disc and every check lost the\n * indicator's accent.\n */\nexport const menuItem = [\n 'relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none',\n 'transition-colors ease-out',\n 'data-[highlighted]:bg-surface-hover data-[highlighted]:text-foreground',\n 'data-[disabled]:pointer-events-none data-[disabled]:text-muted-foreground',\n '[&_svg]:pointer-events-none [&_svg]:shrink-0',\n '[&_svg:not([data-slot$=-indicator]_*)]:size-4',\n '[&_svg:not([class*=text-]):not([data-slot$=-indicator]_*)]:text-muted-foreground',\n].join(' ')\n\n/** A destructive action keeps readable text and marks its row with a status edge. */\nexport const destructiveMenuItem =\n 'border-s-2 border-destructive ps-1.5 text-foreground data-[highlighted]:bg-destructive-subtle'\n\n/** The label above a group of menu rows. */\nexport const menuLabel =\n 'px-2 py-1.5 text-2xs font-medium tracking-wide text-muted-foreground uppercase'\n\n/** The rule between menu groups. */\nexport const menuSeparator = 'bg-border -mx-1 my-1 h-px'\n\n/**\n * A leading colour swatch on a menu or select row: the sanctioned way to carry\n * a card-tone colour into a list of options. The tones are the system's accent\n * and status roles — the vocabulary a Badge already speaks — so the colour an\n * option shows in a menu is the colour its badge carries on a card, and a\n * caller never passes a raw colour into a list. The dot is `aria-hidden`; the\n * label carries the meaning, so a tone is never the only signal.\n */\nexport const menuSwatchTone = cva('size-2 shrink-0 rounded-full', {\n variants: {\n tone: {\n neutral: 'bg-muted-foreground',\n primary: 'bg-primary',\n success: 'bg-success',\n warning: 'bg-warning',\n destructive: 'bg-destructive',\n info: 'bg-info',\n },\n },\n defaultVariants: { tone: 'neutral' },\n})\n\n/** The tones a menu row's swatch can take. */\nexport type MenuSwatchTone = NonNullable<VariantProps<typeof menuSwatchTone>['tone']>\n\n/** Padding shared by every menu-list surface. */\nexport const menuContentPadding = 'p-1'\n\n/**\n * Marks a floating layer — a menu, a listbox, a popover — as part of the top\n * layer.\n *\n * A modal dialog hides everything outside itself from assistive technology\n * (`aria-hidden` on its siblings) and keeps watching `<body>` for new nodes.\n * A menu or select opened from inside the dialog portals to `<body>`, so the\n * dialog hid it too: the list was on screen, focused, and invisible to a\n * screen reader, and an outside-click check could treat a click on it as a\n * click away from the dialog. Kobalte exempts nodes carrying this attribute\n * from both, so every floating layer carries it.\n */\nexport const topLayerProps = { 'data-kb-top-layer': '' } as const\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,IAAa,iBACX;;;;;;;;;;;;;;AAeF,IAAa,gBACX;;;;;AAMF,IAAa,eAAe;;AAG5B,IAAa,oBAAoB;;AAGjC,IAAa,eACX;;;;;;;AAQF,IAAa,aACX;;;;;;;;;;AAWF,IAAa,gBAAgB;CAC3B;CACA;CACA;AACF,CAAC,CAAC,KAAK,GAAG;;;AAIV,IAAa,gBAAgB,CAC3B,mGACA,qDACF,CAAC,CAAC,KAAK,GAAG;;AAGV,IAAa,eAAe;;;;;;;;;;;AAY5B,IAAa,WAAW;CACtB;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,GAAG;;AAGV,IAAa,sBACX;;AAGF,IAAa,YACX;;AAGF,IAAa,gBAAgB;;;;;;;;;AAU7B,IAAa,iBAAiB,IAAI,gCAAgC;CAChE,UAAU,EACR,MAAM;EACJ,SAAS;EACT,SAAS;EACT,SAAS;EACT,SAAS;EACT,aAAa;EACb,MAAM;CACR,EACF;CACA,iBAAiB,EAAE,MAAM,UAAU;AACrC,CAAC;;;;;;;;;;;;;AAoBD,IAAa,gBAAgB,EAAE,qBAAqB,GAAG"}
1
+ {"version":3,"file":"overlay.js","names":[],"sources":["../../src/lib/overlay.ts"],"sourcesContent":["/**\n * Shared strings for the overlay layer: dialogs, sheets, menus, popovers,\n * tooltips and the command palette.\n *\n * These live here rather than in each component because the overlay family is\n * where consistency is most visible and least likely to survive duplication.\n * A dialog, a sheet and a dropdown that disagree about their border, radius,\n * motion or shadow read as three different apps — and an app that adopts this\n * library would inherit all three. Declaring the surface once is what makes\n * \"an overlay looks like this\" a fact rather than a convention.\n */\n\nimport { cva, type VariantProps } from '#lib/variants'\n\n/** The panel itself: fill, hairline edge, radius, elevation. Menus, popovers\n * and the command palette float on the elevated overlay rung (`--popover`). */\nexport const overlaySurface =\n 'bg-popover text-popover-foreground rounded-xl border border-border shadow-lg'\n\n/**\n * The modal panel: the same hairline edge, radius and elevation, painted on the\n * *neutral* theme surface instead of the elevated overlay rung.\n *\n * `--popover` is the catalogue's `surfaceElevated`, whose derivation amplifies\n * the canvas hue's chroma on the elevated step. On the dark default's violet\n * canvas that makes the dialog the most chromatic surface in the ladder —\n * `oklch(0.2406 0.0088 300.91)`, red above green in sRGB — and on a full dialog\n * panel that cast reads as a warm tint the canvas never has. shadcn's own\n * convention already splits the two: a Dialog paints `bg-background`, menus and\n * popovers paint `bg-popover`. The veil, the hairline and the elevation shadow\n * carry the separation the lighter fill no longer provides.\n */\nexport const dialogSurface =\n 'bg-background text-foreground rounded-xl border border-border shadow-lg'\n\n/**\n * Overlay titles have one fixed typography role. Keep this small recipe shared\n * without loading the configurable Heading component into every dialog.\n */\nexport const overlayTitle = 'font-semibold text-base tracking-tight leading-none'\n\n/** The span of the portal wrapper that centres a dialog-like overlay. */\nexport const overlayPositioner = 'fixed inset-0 z-(--z-dialog) grid place-items-center p-4'\n\n/** The dimming layer behind a modal overlay. */\nexport const overlayScrim =\n 'fixed inset-0 z-(--z-dialog) bg-scrim/50 supports-[backdrop-filter]:backdrop-blur-xs'\n\n/**\n * The tip: the card-toned bubble an explanation shows in. The side rail draws\n * it flush to a collapsed row and every floating tooltip composes the same\n * fill, edge, and type, so a control never explains itself in two visual\n * languages.\n */\nexport const tooltipTip =\n 'bg-card text-card-foreground rounded-md border border-border text-sm font-medium shadow-lg'\n\n/**\n * Entrance and exit motion, expressed as data-state classes.\n *\n * `data-expanded` / `data-closed` are Kobalte's; `data-side-*` come from its\n * popper positioning and let a menu slide in from the edge it is anchored to,\n * which is what makes a dropdown feel attached to its trigger rather than\n * dropped on the screen. Exit motion is shorter than entry, because a\n * dismissal should feel immediate while an arrival benefits from being seen.\n */\nexport const overlayMotion = [\n 'data-expanded:animate-in data-expanded:fade-in-0 data-expanded:zoom-in-95',\n 'data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',\n 'data-expanded:duration-200 data-closed:duration-150',\n].join(' ')\n\n/** Popover-style motion: no zoom, so a panel anchored to a control does not\n * appear to breathe away from it. */\nexport const popoverMotion = [\n 'data-expanded:animate-in data-expanded:fade-in-0 data-closed:animate-out data-closed:fade-out-0',\n 'data-expanded:duration-150 data-closed:duration-100',\n].join(' ')\n\n/** The popper arrow. Sized in the component; coloured here. */\nexport const popoverArrow = 'fill-popover stroke-border'\n\n/**\n * A menu row: the shared height, radius, hover fill and focus treatment for\n * every item inside a menu, listbox or command list.\n *\n * The row sizes and dims the icons a caller puts in it, but not the glyph of\n * its selection indicator (any `[data-slot$=-indicator]`): a descendant `svg`\n * rule outranks a single utility on the glyph, so without the exclusion a\n * radio item's `size-2` dot drew as a 16px muted disc and every check lost the\n * indicator's accent.\n */\nexport const menuItem = [\n 'relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none',\n 'transition-colors ease-out',\n 'data-[highlighted]:bg-surface-hover data-[highlighted]:text-foreground',\n 'data-[disabled]:pointer-events-none data-[disabled]:text-muted-foreground',\n '[&_svg]:pointer-events-none [&_svg]:shrink-0',\n '[&_svg:not([data-slot$=-indicator]_*)]:size-4',\n '[&_svg:not([class*=text-]):not([data-slot$=-indicator]_*)]:text-muted-foreground',\n].join(' ')\n\n/** A destructive action keeps readable text and marks its row with a status edge. */\nexport const destructiveMenuItem =\n 'border-s-2 border-destructive ps-1.5 text-foreground data-[highlighted]:bg-destructive-subtle'\n\n/** The label above a group of menu rows. */\nexport const menuLabel =\n 'px-2 py-1.5 text-2xs font-medium tracking-wide text-muted-foreground uppercase'\n\n/** The rule between menu groups. */\nexport const menuSeparator = 'bg-border -mx-1 my-1 h-px'\n\n/**\n * A leading colour swatch on a menu or select row: the sanctioned way to carry\n * a card-tone colour into a list of options. The tones are the system's accent\n * and status roles — the vocabulary a Badge already speaks — so the colour an\n * option shows in a menu is the colour its badge carries on a card, and a\n * caller never passes a raw colour into a list. The dot is `aria-hidden`; the\n * label carries the meaning, so a tone is never the only signal.\n */\nexport const menuSwatchTone = cva('size-2 shrink-0 rounded-full', {\n variants: {\n tone: {\n neutral: 'bg-muted-foreground',\n primary: 'bg-primary',\n success: 'bg-success',\n warning: 'bg-warning',\n destructive: 'bg-destructive',\n info: 'bg-info',\n },\n },\n defaultVariants: { tone: 'neutral' },\n})\n\n/** The tones a menu row's swatch can take. */\nexport type MenuSwatchTone = NonNullable<VariantProps<typeof menuSwatchTone>['tone']>\n\n/** Padding shared by every menu-list surface. */\nexport const menuContentPadding = 'p-1'\n\n/**\n * Marks a floating layer — a menu, a listbox, a popover — as part of the top\n * layer.\n *\n * A modal dialog hides everything outside itself from assistive technology\n * (`aria-hidden` on its siblings) and keeps watching `<body>` for new nodes.\n * A menu or select opened from inside the dialog portals to `<body>`, so the\n * dialog hid it too: the list was on screen, focused, and invisible to a\n * screen reader, and an outside-click check could treat a click on it as a\n * click away from the dialog. Kobalte exempts nodes carrying this attribute\n * from both, so every floating layer carries it.\n */\nexport const topLayerProps = { 'data-kb-top-layer': '' } as const\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,IAAa,iBACX;;;;;;;;;;;;;;AAeF,IAAa,gBACX;;;;;AAMF,IAAa,eAAe;;AAG5B,IAAa,oBAAoB;;AAGjC,IAAa,eACX;;;;;;;AAQF,IAAa,aACX;;;;;;;;;;AAWF,IAAa,gBAAgB;CAC3B;CACA;CACA;AACF,CAAC,CAAC,KAAK,GAAG;;;AAIV,IAAa,gBAAgB,CAC3B,mGACA,qDACF,CAAC,CAAC,KAAK,GAAG;;AAGV,IAAa,eAAe;;;;;;;;;;;AAY5B,IAAa,WAAW;CACtB;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,GAAG;;AAGV,IAAa,sBACX;;AAGF,IAAa,YACX;;AAGF,IAAa,gBAAgB;;;;;;;;;AAU7B,IAAa,iBAAiB,IAAI,gCAAgC;CAChE,UAAU,EACR,MAAM;EACJ,SAAS;EACT,SAAS;EACT,SAAS;EACT,SAAS;EACT,aAAa;EACb,MAAM;CACR,EACF;CACA,iBAAiB,EAAE,MAAM,UAAU;AACrC,CAAC;;AAMD,IAAa,qBAAqB;;;;;;;;;;;;;AAclC,IAAa,gBAAgB,EAAE,qBAAqB,GAAG"}
@@ -0,0 +1,3 @@
1
+ import type { ThemeEngine } from './theme-engine';
2
+ export declare const catalogueThemeEngine: ThemeEngine;
3
+ //# sourceMappingURL=theme-catalogue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-catalogue.d.ts","sourceRoot":"","sources":["../../src/lib/theme-catalogue.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAEjD,eAAO,MAAM,oBAAoB,EAAE,WAOlC,CAAA"}
@@ -0,0 +1,22 @@
1
+ import { accentRolesForSelection, accentVariables, builtinThemes, isCatalogueVariant, themeAccentsFor, themeCssVariables } from "./themes.js";
2
+ //#region src/lib/theme-catalogue.ts
3
+ /**
4
+ * The full theme engine, backed by the whole catalogue.
5
+ *
6
+ * This is the module a provider loads when it needs more than the default pair — and,
7
+ * for `ThemeProvider`, always. It imports `lib/themes` (which imports the catalogue)
8
+ * and nothing imports it statically except the eager provider, so a bundler can put
9
+ * it, and the catalogue behind it, in a chunk of its own for the lazy provider.
10
+ */
11
+ var catalogueThemeEngine = {
12
+ builtinThemes,
13
+ isCatalogueVariant,
14
+ themeCssVariables,
15
+ accentVariables,
16
+ accentRolesForSelection,
17
+ themeAccentsFor
18
+ };
19
+ //#endregion
20
+ export { catalogueThemeEngine };
21
+
22
+ //# sourceMappingURL=theme-catalogue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-catalogue.js","names":[],"sources":["../../src/lib/theme-catalogue.ts"],"sourcesContent":["/**\n * The full theme engine, backed by the whole catalogue.\n *\n * This is the module a provider loads when it needs more than the default pair — and,\n * for `ThemeProvider`, always. It imports `lib/themes` (which imports the catalogue)\n * and nothing imports it statically except the eager provider, so a bundler can put\n * it, and the catalogue behind it, in a chunk of its own for the lazy provider.\n */\nimport {\n accentRolesForSelection,\n accentVariables,\n builtinThemes,\n isCatalogueVariant,\n themeAccentsFor,\n themeCssVariables,\n} from './themes'\nimport type { ThemeEngine } from './theme-engine'\n\nexport const catalogueThemeEngine: ThemeEngine = {\n builtinThemes,\n isCatalogueVariant,\n themeCssVariables,\n accentVariables,\n accentRolesForSelection,\n themeAccentsFor,\n}\n"],"mappings":";;;;;;;;;;AAkBA,IAAa,uBAAoC;CAC/C;CACA;CACA;CACA;CACA;CACA;AACF"}
@@ -0,0 +1,43 @@
1
+ import type { EditorRolesMode, ThemeCssVariableOptions, ThemeEditorPalette, ThemeVariant } from './themes';
2
+ /** What a variant's custom properties need that depends on the catalogue. */
3
+ export type ThemeCssHooks = {
4
+ /** The `--primary-subtle` expression of a variant. */
5
+ primarySubtle(theme: Pick<ThemeVariant, 'id' | 'appearance'>): string;
6
+ /** The editor ramp of a variant under a projection. */
7
+ editor(theme: ThemeVariant, mode: EditorRolesMode): ThemeEditorPalette;
8
+ };
9
+ /**
10
+ * The roles a variant sets, as CSS custom properties.
11
+ *
12
+ * The one place that knows the mapping from a theme's roles to this system's variable
13
+ * names. A provider applies this; nothing else repeats it. The `--terminal-*` and
14
+ * `--editor-*` ramps are presentations of the catalogue's ANSI and syntax data rather
15
+ * than separate roles, which is why they are built here and not in the catalogue.
16
+ *
17
+ * `options.editor` picks the `--editor-*` projection ({@link EditorRolesMode}).
18
+ * Absent it, the result is what it has always been; the generated `theme-variants.css`
19
+ * and the design-system artifact are built without it.
20
+ */
21
+ export declare function themeCssVariablesWith(hooks: ThemeCssHooks, theme: ThemeVariant, options?: ThemeCssVariableOptions): Record<string, string>;
22
+ /**
23
+ * The `--terminal-*` properties of a variant: the palette a terminal paints.
24
+ *
25
+ * What a pinned terminal theme writes over the interface variant's own. The names
26
+ * are the ones {@link themeCssVariablesWith} writes, and a custom variant's verbatim
27
+ * `tokens` still win, so they can never drift from what the interface variant would
28
+ * have written for the same palette. Built from the terminal palette alone: it needs
29
+ * neither the catalogue nor the rest of the variant's roles.
30
+ */
31
+ export declare function terminalCssVariables(theme: ThemeVariant): Record<string, string>;
32
+ /**
33
+ * Token values with every `oklch()` colour written as `#rrggbb`.
34
+ *
35
+ * For a host whose stylesheet, tests or native shell speak hex: the catalogue's
36
+ * roles are `oklch()` strings, and this is the one conversion, gamut-mapped by the
37
+ * catalogue's own `oklchToHex`. A value that is already hex, carries alpha
38
+ * (`rgba()`), or is not a plain colour (`color-mix()`, `var()`) is left byte for
39
+ * byte, because converting would either lose the alpha or be a guess. Suitable as
40
+ * `ThemeProvider`'s `transformTokens`.
41
+ */
42
+ export declare function tokensAsHex(tokens: Readonly<Record<string, string>>): Record<string, string>;
43
+ //# sourceMappingURL=theme-css.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-css.d.ts","sourceRoot":"","sources":["../../src/lib/theme-css.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACV,eAAe,EACf,uBAAuB,EACvB,kBAAkB,EAClB,YAAY,EACb,MAAM,UAAU,CAAA;AAEjB,6EAA6E;AAC7E,MAAM,MAAM,aAAa,GAAG;IAC1B,sDAAsD;IACtD,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,GAAG,YAAY,CAAC,GAAG,MAAM,CAAA;IACrE,uDAAuD;IACvD,MAAM,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,eAAe,GAAG,kBAAkB,CAAA;CACvE,CAAA;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,aAAa,EACpB,KAAK,EAAE,YAAY,EACnB,OAAO,GAAE,uBAA4B,GACpC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA2FxB;AA2BD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOhF;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAU5F"}
@@ -0,0 +1,167 @@
1
+ import { primaryHover } from "@adea-ai/themes";
2
+ import { oklchToHex, parseColor } from "@adea-ai/themes/oklch";
3
+ //#region src/lib/theme-css.ts
4
+ /**
5
+ * The mapping from a variant's roles to this system's custom properties, without the
6
+ * catalogue.
7
+ *
8
+ * `themeCssVariables` needs two things only the catalogue can measure for a theme:
9
+ * the strength of its `--primary-subtle` tint and its floor-projected editor ramp.
10
+ * Those are the `hooks`, so this module imports no catalogue data and a host that
11
+ * never loads the catalogue can still paint a variant it already holds. `lib/themes`
12
+ * binds the hooks to the catalogue and keeps exporting the same
13
+ * `themeCssVariables`; `lib/theme-engine` binds them to the precomputed default pair.
14
+ */
15
+ /**
16
+ * The roles a variant sets, as CSS custom properties.
17
+ *
18
+ * The one place that knows the mapping from a theme's roles to this system's variable
19
+ * names. A provider applies this; nothing else repeats it. The `--terminal-*` and
20
+ * `--editor-*` ramps are presentations of the catalogue's ANSI and syntax data rather
21
+ * than separate roles, which is why they are built here and not in the catalogue.
22
+ *
23
+ * `options.editor` picks the `--editor-*` projection ({@link EditorRolesMode}).
24
+ * Absent it, the result is what it has always been; the generated `theme-variants.css`
25
+ * and the design-system artifact are built without it.
26
+ */
27
+ function themeCssVariablesWith(hooks, theme, options = {}) {
28
+ const c = theme.colors;
29
+ const t = theme.terminal;
30
+ const e = hooks.editor(theme, options.editor ?? "syntax");
31
+ const derived = {
32
+ "--background": c.background,
33
+ "--foreground": c.foreground,
34
+ "--card": c.card,
35
+ "--card-foreground": c.cardForeground,
36
+ "--popover": c.popover,
37
+ "--popover-foreground": c.popoverForeground,
38
+ "--primary": c.primary,
39
+ "--primary-foreground": c.primaryForeground,
40
+ "--primary-hover": primaryHover(c.primary, theme.appearance),
41
+ "--primary-subtle": hooks.primarySubtle(theme),
42
+ "--secondary": c.secondary,
43
+ "--secondary-foreground": c.secondaryForeground,
44
+ "--muted": c.muted,
45
+ "--muted-foreground": c.mutedForeground,
46
+ "--accent": c.accent,
47
+ "--accent-foreground": c.accentForeground,
48
+ "--destructive": c.destructive,
49
+ "--destructive-foreground": c.destructiveForeground,
50
+ "--destructive-subtle": c.destructiveSubtle,
51
+ "--destructive-action": c.destructiveAction,
52
+ "--destructive-action-foreground": c.destructiveActionForeground,
53
+ "--success": c.success,
54
+ "--success-foreground": c.successForeground,
55
+ "--success-subtle": c.successSubtle,
56
+ "--warning": c.warning,
57
+ "--warning-foreground": c.warningForeground,
58
+ "--warning-subtle": c.warningSubtle,
59
+ "--info": c.info,
60
+ "--info-foreground": c.infoForeground,
61
+ "--info-subtle": c.infoSubtle,
62
+ "--border": c.border,
63
+ "--input": c.input,
64
+ "--ring": c.ring,
65
+ "--sidebar": c.sidebar,
66
+ "--sidebar-foreground": c.sidebarForeground,
67
+ "--sidebar-accent": c.sidebarAccent,
68
+ "--sidebar-border": c.sidebarBorder,
69
+ "--sidebar-primary": c.primary,
70
+ "--sidebar-primary-foreground": c.primaryForeground,
71
+ "--sidebar-ring": c.ring,
72
+ "--sidebar-muted-foreground": c.sidebarMutedForeground,
73
+ "--surface-sunken": c.muted,
74
+ "--surface-hover": c.accent,
75
+ "--surface-active": c.border,
76
+ "--chrome": c.background,
77
+ ...terminalVariables(t),
78
+ "--editor-keyword": e.keyword,
79
+ "--editor-string": e.string,
80
+ "--editor-number": e.number,
81
+ "--editor-comment": e.comment,
82
+ "--editor-function": e.function,
83
+ "--editor-variable": e.variable,
84
+ "--editor-type": e.type,
85
+ "--editor-tag": e.tag,
86
+ "--editor-attribute": e.attribute,
87
+ "--editor-operator": e.operator,
88
+ "--editor-heading": e.heading,
89
+ "--editor-link": e.link,
90
+ "--editor-diff-add": e.diffAdd,
91
+ "--editor-diff-delete": e.diffDelete,
92
+ "--editor-diff-hunk": e.diffHunk,
93
+ "--editor-search-match": e.searchMatch,
94
+ "--chart-1": theme.chart[0] ?? c.primary,
95
+ "--chart-2": theme.chart[1] ?? c.info,
96
+ "--chart-3": theme.chart[2] ?? c.success,
97
+ "--chart-4": theme.chart[3] ?? c.warning,
98
+ "--chart-5": theme.chart[4] ?? c.destructive,
99
+ "--chart-6": theme.chart[5] ?? c.mutedForeground
100
+ };
101
+ return theme.tokens ? {
102
+ ...derived,
103
+ ...theme.tokens
104
+ } : derived;
105
+ }
106
+ function terminalVariables(t) {
107
+ return {
108
+ "--terminal-background": t.background,
109
+ "--terminal-foreground": t.foreground,
110
+ "--terminal-cursor": t.cursor,
111
+ "--terminal-selection": t.selection,
112
+ "--terminal-ansi-black": t.black,
113
+ "--terminal-ansi-red": t.red,
114
+ "--terminal-ansi-green": t.green,
115
+ "--terminal-ansi-yellow": t.yellow,
116
+ "--terminal-ansi-blue": t.blue,
117
+ "--terminal-ansi-magenta": t.magenta,
118
+ "--terminal-ansi-cyan": t.cyan,
119
+ "--terminal-ansi-white": t.white,
120
+ "--terminal-ansi-bright-black": t.brightBlack,
121
+ "--terminal-ansi-bright-red": t.brightRed,
122
+ "--terminal-ansi-bright-green": t.brightGreen,
123
+ "--terminal-ansi-bright-yellow": t.brightYellow,
124
+ "--terminal-ansi-bright-blue": t.brightBlue,
125
+ "--terminal-ansi-bright-magenta": t.brightMagenta,
126
+ "--terminal-ansi-bright-cyan": t.brightCyan,
127
+ "--terminal-ansi-bright-white": t.brightWhite
128
+ };
129
+ }
130
+ /**
131
+ * The `--terminal-*` properties of a variant: the palette a terminal paints.
132
+ *
133
+ * What a pinned terminal theme writes over the interface variant's own. The names
134
+ * are the ones {@link themeCssVariablesWith} writes, and a custom variant's verbatim
135
+ * `tokens` still win, so they can never drift from what the interface variant would
136
+ * have written for the same palette. Built from the terminal palette alone: it needs
137
+ * neither the catalogue nor the rest of the variant's roles.
138
+ */
139
+ function terminalCssVariables(theme) {
140
+ const properties = terminalVariables(theme.terminal);
141
+ if (!theme.tokens) return properties;
142
+ for (const [name, value] of Object.entries(theme.tokens)) if (name.startsWith("--terminal-")) properties[name] = value;
143
+ return properties;
144
+ }
145
+ /**
146
+ * Token values with every `oklch()` colour written as `#rrggbb`.
147
+ *
148
+ * For a host whose stylesheet, tests or native shell speak hex: the catalogue's
149
+ * roles are `oklch()` strings, and this is the one conversion, gamut-mapped by the
150
+ * catalogue's own `oklchToHex`. A value that is already hex, carries alpha
151
+ * (`rgba()`), or is not a plain colour (`color-mix()`, `var()`) is left byte for
152
+ * byte, because converting would either lose the alpha or be a guess. Suitable as
153
+ * `ThemeProvider`'s `transformTokens`.
154
+ */
155
+ function tokensAsHex(tokens) {
156
+ const converted = {};
157
+ for (const [name, value] of Object.entries(tokens)) {
158
+ const trimmed = value.trim();
159
+ const parsed = trimmed.startsWith("oklch(") && !trimmed.includes("/") ? parseColor(trimmed) : void 0;
160
+ converted[name] = parsed ? oklchToHex(parsed) : value;
161
+ }
162
+ return converted;
163
+ }
164
+ //#endregion
165
+ export { terminalCssVariables, themeCssVariablesWith, tokensAsHex };
166
+
167
+ //# sourceMappingURL=theme-css.js.map