@adea-ai/ui 0.128.1 → 0.128.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/accent-roles.js +94 -9
- package/dist/lib/accent-roles.js.map +1 -1
- package/dist/lib/font-zoom.js +4 -2
- package/dist/lib/font-zoom.js.map +1 -1
- package/dist/lib/overlay.js +3 -1
- package/dist/lib/overlay.js.map +1 -1
- package/dist/lib/themes.js +4 -2
- package/dist/lib/themes.js.map +1 -1
- package/dist/lib/tooltip-focus-gate.js +7 -1
- package/dist/lib/tooltip-focus-gate.js.map +1 -1
- package/dist/lib/variants.js +2 -2
- package/package.json +3 -2
package/dist/lib/accent-roles.js
CHANGED
|
@@ -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":";;;AAqDA,IAAM,MAAM;;AAGZ,SAAgB,WAAW,OAAqC;CAE9D,MAAM,SADQ,IAAI,KAAK,MAAM,KAAK,CACnB,CAAA,GAAQ;CACvB,IAAI,CAAC,QAAQ,OAAO,KAAA;CACpB,IAAI,OAAO,WAAW,KAAK,OAAO,WAAW,GAAG;EAE9C,MAAM,CAAC,GAAG,GAAG,GAAG,KAAK,CAAC,GAAG,MAAM,CAAC,CAAC,KAAK,UAAU,OAAO,SAAS,QAAQ,OAAO,EAAE,CAAC;EAClF,OAAO;GAAK;GAAO;GAAO;GAAI,GAAG,KAAK;EAAI;CAC5C;CACA,IAAI,OAAO,WAAW,KAAK,OAAO,WAAW,GAAG;EAC9C,MAAM,QAAQ,UAAkB,OAAO,SAAS,OAAO,MAAM,OAAO,QAAQ,CAAC,GAAG,EAAE;EAClF,OAAO;GAAE,GAAG,KAAK,CAAC;GAAG,GAAG,KAAK,CAAC;GAAG,GAAG,KAAK,CAAC;GAAG,GAAG,OAAO,WAAW,IAAI,KAAK,CAAC,IAAI;EAAI;CACtF;AAEF;AAEA,SAAS,UAAU,OAAuB;CACxC,OAAO,MAAM,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG;AAC3C;;AAGA,SAAgB,WAAW,OAAyB;CAClD,MAAM,SAAS,IAAI,UAAU,MAAM,CAAC,IAAI,UAAU,MAAM,CAAC,IAAI,UAAU,MAAM,CAAC;CAC9E,OAAO,MAAM,MAAM,MAAM,SAAS,GAAG,SAAS,UAAU,MAAM,CAAC;AACjE;AAEA,SAAS,UAAU,YAAsB,YAAgC;CACvE,MAAM,QAAQ,WAAW,IAAI;CAC7B,MAAM,SAAS,OAAe,SAAiB,KAAK,MAAM,QAAQ,QAAQ,QAAQ,IAAI,MAAM;CAC5F,OAAO;EACL,GAAG,MAAM,WAAW,GAAG,WAAW,CAAC;EACnC,GAAG,MAAM,WAAW,GAAG,WAAW,CAAC;EACnC,GAAG,MAAM,WAAW,GAAG,WAAW,CAAC;EACnC,GAAG;CACL;AACF;AAEA,SAAS,iBAAiB,OAAoC;CAC5D,OAAOA,aAAoB,WAAW;EAAE,GAAG;EAAO,GAAG;CAAI,CAAC,CAAC;AAC7D;;AAGA,SAAgB,cACd,YACA,YACQ;CACR,MAAM,QAAQ,OAAO,eAAe,WAAW,WAAW,UAAU,IAAI;CACxE,MAAM,OAAO,OAAO,eAAe,WAAW,WAAW,UAAU,IAAI;CACvE,IAAI,CAAC,SAAS,CAAC,MAAM,OAAO;CAE5B,MAAM,kBAAkB,iBADJ,MAAM,MAAM,MAAM,QAAQ,UAAU,OAAO,IAAI,CACf;CACpD,MAAM,kBAAkB,iBAAiB,IAAI;CAC7C,IAAI,CAAC,mBAAmB,CAAC,iBAAiB,OAAO;CACjD,OAAOC,gBAAuB,iBAAiB,eAAe;AAChE;AAEA,IAAM,QAAkB;CAAE,GAAG;CAAK,GAAG;CAAK,GAAG;CAAK,GAAG;AAAI;AACzD,IAAM,cAAqB;CAAE,GAAG;CAAG,GAAG;CAAG,GAAG;AAAE;;;;;;;;;;AAW9C,SAAS,YAAY,QAAe,QAAe,SAAyB;CAC1E,KAAK,IAAI,SAAS,GAAG,UAAU,KAAM,SAAS,QAAQ,SAAS,KAAA,CAAO,QAAQ,CAAC,CAAC,GAAG;EACjF,MAAM,WAAW,eAAe,QAAQ,QAAQ,UAAU,MAAM;EAChE,IAAI,CAAC,SAAS,WAAW;EACzB,MAAM,MAAM,WAAW,SAAS,KAAK;EACrC,MAAM,UAAUD,aAAoB,GAAG;EACvC,IAAI,WAAWC,gBAAuB,SAAS,MAAM,KAAK,SAAS,OAAO;CAC5E;CACA,OAAOA,gBAAuB,aAAa,MAAM,KAAK,UAAU,YAAY;AAC9E;;AAGA,SAAgB,eAAe,OAAiB,YAAsB,SAA2B;CAC/F,IAAI,cAAc,OAAO,UAAU,KAAK,SAAS,OAAO;CAGxD,MAAM,SAAS,iBADO,MAAM,MAAM,MAAM,QAAQ,UAAU,OAAO,UAAU,CAC9B;CAC7C,MAAM,SAAS,iBAAiB,UAAU;CAC1C,IAAI,CAAC,UAAU,CAAC,QAAQ,OAAO;CAC/B,OAAO,WAAW,YAAY,QAAQ,QAAQ,OAAO,CAAC;AACxD;;AAGA,IAAa,gCAAgC;;;;;;;AAQ7C,SAAgB,qBACd,OACA,YACA,UAAA,GACoB;CACpB,MAAM,SAAS,WAAW,KAAK;CAC/B,IAAI,CAAC,QAAQ,OAAO,KAAA;CAEpB,OAAO,WAAW,eAAe,QADhB,WAAW,UAAU,KAAK,OACQ,OAAO,CAAC;AAC7D;;;;;;;;;;AAWA,SAAgB,sBAAsB,OAAoC;CACxE,OAAO,OAAO,UAAU,YAAY,oBAAoB,KAAK,KAAK,IAC9D,MAAM,YAAY,IAClB,KAAA;AACN;;AAGA,SAAgB,eAAe,OAAyB;CACtD,OAAO,sBAAsB,KAAK,MAAM,KAAA;AAC1C;AA6BA,SAAS,aAAa,QAAiD;CACrE,OAAO,OAAO,WAAW,WAAW,SAAS,OAAO;AACtD;;;;;;;;AASA,SAAS,UAAU,OAAe,YAAgC,OAAuB;CACvF,MAAM,SAASD,aAAoB,KAAK;CACxC,MAAM,SAAS,aAAaA,aAAoB,UAAU,IAAI,KAAA;CAC9D,IAAI,CAAC,UAAU,CAAC,QAAQ,OAAO;CAC/B,IAAIC,gBAAuB,QAAQ,MAAM,KAAK,OAAO,OAAO;CAC5D,OAAO,YAAY,QAAQ,QAAQ,KAAK;AAC1C;;;;;;;;;AAUA,SAAgB,qBACd,QACA,SACyB;CACzB,MAAM,aAAa,aAAa,QAAQ,MAAM;CAC9C,MAAM,YAAY,YAAY,QAAQ,UAAU;CAChD,IAAI,CAACD,aAAoB,SAAS,GAAG,OAAO,KAAA;CAE5C,MAAM,QAAQ,QAAQ;CACtB,IAAI,UAAU,KAAA,GAAW,OAAO,YAAY,QAAQ,QAAQ,MAAM;CAElE,MAAM,QAAQ,UAAU,WAAW,QAAQ,YAAY,KAAK;CAC5D,IAAI,UAAU,WAAW,OAAO,YAAY,QAAQ,QAAQ,MAAM;CAKlE,OAAO;EAAE,GAHK,YAAY;GAAE,GAAG;GAAQ,OAAO;GAAO,MAAM;EAAM,GAAG,QAAQ,MAGhE;EAAO,eAAe,iBAAiB,QAAQ,QAAQ,aAAa,CAAC,KAAK,CAAC;CAAE;AAC3F;;;;;;;;;AAUA,SAAgB,mBAAmB,QAAgB,SAAgD;CACjG,IAAI,WAAW,SAAS,OAAO,KAAA;CAE/B,MAAM,SAAS,sBAAsB,MAAM;CAC3C,IAAI,QAAQ;EACV,MAAM,QAAQ,QAAQ,SAAA;EACtB,OAAO,qBACL;GAAE,IAAI;GAAU,OAAO;GAAU,aAAa;GAAI,OAAO;GAAQ,MAAM;EAAO,GAC9E;GAAE,GAAG;GAAS;EAAM,CACtB;CACF;CAEA,MAAM,SACJ,QAAQ,MAAM,cAAc,UAAU,OAAO,MAAM,KACnD,QAAQ,cAAc,MAAM,cAAc,UAAU,OAAO,MAAM;CACnE,OAAO,SAAS,qBAAqB,QAAQ,OAAO,IAAI,KAAA;AAC1D;;;;;;;;;AAUA,SAAgB,wBAAwB,OAA4C;CAClF,OAAO;EACL,aAAa,MAAM;EACnB,wBAAwB,MAAM;EAC9B,mBAAmB,MAAM;EACzB,UAAU,MAAM;CAClB;AACF;;AAGA,IAAa,0BAA0B;CACrC;CACA;CACA;CACA;CACA;AACF;;;;;;;;;;;;;AAcA,SAAgB,iBACd,SACA,OACM;CACN,IAAI,CAAC,OAAO;EACV,KAAK,MAAM,YAAY,yBAAyB,QAAQ,MAAM,eAAe,QAAQ;EACrF;CACF;CACA,QAAQ,MAAM,YAAY,aAAa,MAAM,OAAO;CACpD,QAAQ,MAAM,YAAY,wBAAwB,MAAM,iBAAiB;CACzE,QAAQ,MAAM,YAAY,mBAAmB,MAAM,YAAY;CAC/D,QAAQ,MAAM,YAAY,oBAAoB,MAAM,aAAa;CACjE,QAAQ,MAAM,YAAY,UAAU,MAAM,IAAI;AAChD"}
|
package/dist/lib/font-zoom.js
CHANGED
|
@@ -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"}
|
package/dist/lib/overlay.js
CHANGED
|
@@ -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
|
package/dist/lib/overlay.js.map
CHANGED
|
@@ -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;;;;;;;;;;;;;
|
|
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"}
|
package/dist/lib/themes.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { accentOverrideVariables, resolveAccentRoles } from "./accent-roles.js";
|
|
2
2
|
import { isThemeAccentId } from "./theme-accents.js";
|
|
3
|
-
import { accentForeground, chartSeries, contrastRatio
|
|
3
|
+
import { ACCENTS, accentForeground, chartSeries, contrastRatio, editorRolesHex, getAccent, getTheme, hasTheme, primaryHover, primarySubtleCss, shadcnDestructiveProjection, statusForeground, syntaxRoles, themeAccentPresets, themes, themesByAppearance, tint, validateCatalogue } from "@adea-ai/themes";
|
|
4
4
|
import { shadcnVariables } from "@adea-ai/themes/adapters/shadcn";
|
|
5
5
|
import { oklchToHex, parseColor } from "@adea-ai/themes/oklch";
|
|
6
6
|
//#region src/lib/themes.ts
|
|
@@ -251,6 +251,8 @@ function themeFamilies() {
|
|
|
251
251
|
}
|
|
252
252
|
return [...families.values()];
|
|
253
253
|
}
|
|
254
|
+
/** The accent presets, in the order a picker should show them. */
|
|
255
|
+
var accents = ACCENTS;
|
|
254
256
|
/**
|
|
255
257
|
* The canonical catalogue records, in catalogue order.
|
|
256
258
|
*
|
|
@@ -618,6 +620,6 @@ function validateThemeRegistry(themes$1 = builtinThemes) {
|
|
|
618
620
|
var defaultLightThemeId = "adea-light";
|
|
619
621
|
var defaultDarkThemeId = "adea-dark";
|
|
620
622
|
//#endregion
|
|
621
|
-
export { accentRolesForSelection, accentVariables, appearancePreviewThemes, builtinThemes, contrastRatio
|
|
623
|
+
export { accentRolesForSelection, accentVariables, accents, appearancePreviewThemes, builtinThemes, contrastRatio, defaultDarkThemeId, defaultLightThemeId, editorPaletteFor, floorEditorPalette, isCatalogueVariant, isThemeAccentId, primarySubtleFor, resolveAccentPreset, resolveVariant, terminalCssVariables, themeAccentsFor, themeById, themeCssVariables, themeFamilies, themeRecordById, themeRecords, themesForAppearance, tokensAsHex, validateTheme, validateThemeRegistry, variantFromRecord, withAccent };
|
|
622
624
|
|
|
623
625
|
//# sourceMappingURL=themes.js.map
|
package/dist/lib/themes.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"themes.js","names":["catalogue","themes"],"sources":["../../src/lib/themes.ts"],"sourcesContent":["/**\n * The theme registry, now a bridge.\n *\n * The catalogue itself lives in `@adea-ai/themes`. This module is what is left in\n * the design system: the translation between the catalogue's **canonical** role\n * names and the **shadcn** vocabulary every component here is written against, plus\n * the handful of custom properties this system declares that are presentations of\n * data the catalogue already holds — the `--terminal-*` ramp, the `--editor-*` ramp,\n * and `--chrome`.\n *\n * ## Why it is a bridge and not a copy\n *\n * This file used to hold thirteen themes as hand-written hex literal tables, with its\n * own contrast validator, its own terminal and editor palettes, and its own\n * provenance records. That is a lot of surface to maintain for one consumer of a\n * palette, and it meant every product that wanted the same themes — Adea, Cortana —\n * either imported this package or wrote its own. The catalogue moved out so that\n * there is one answer to \"what is Catppuccin Mocha\", and this file is the part that\n * genuinely belongs to the design system.\n *\n * It is deliberately thin. If you are looking for a colour, it is not here: see\n * `@adea-ai/themes`, whose README explains how the values are produced and whose\n * `NOTICE` records where every palette came from.\n *\n * ## The mapping, and the entry that is not a rename\n *\n * The package-owned `shadcnVariables` adapter maps canonical roles onto this\n * system's vocabulary. One entry will trip a reader: the catalogue's `accent`\n * becomes this system's `primary`, because in the shadcn vocabulary `--primary` is\n * the action colour — the fill of a primary button, the thing a focus ring is drawn\n * in — while `--accent` is a low-contrast hover wash. The catalogue's `accent` is\n * the former, so it goes to `--primary`, and `--accent` is filled from\n * `surfaceHover`, which is what shadcn components actually use it for.\n */\n\nimport {\n ACCENTS,\n accentForeground,\n chartSeries,\n editorRolesHex,\n getAccent,\n getTheme,\n hasTheme,\n shadcnDestructiveProjection,\n statusForeground,\n tint,\n primaryHover,\n primarySubtleCss,\n syntaxRoles,\n themes as catalogue,\n themesByAppearance,\n themeAccentPresets,\n validateCatalogue,\n type AccentPreset,\n type AdeaTheme,\n type AdeaThemeRecord,\n type ContrastFinding,\n} from '@adea-ai/themes'\nimport { shadcnVariables } from '@adea-ai/themes/adapters/shadcn'\nimport { oklchToHex, parseColor } from '@adea-ai/themes/oklch'\nimport { accentOverrideVariables, resolveAccentRoles, type AccentRoles } from './accent-roles'\nimport { isThemeAccentId } from './theme-accents'\n\nexport { isThemeAccentId } from './theme-accents'\n\nexport type ThemeAppearance = 'light' | 'dark'\n\n/**\n * The surface and text roles, in shadcn's vocabulary.\n *\n * Named to match the components below, which is why they differ from the\n * catalogue's: these are this system's variable names, and renaming them would mean\n * touching every component in the library for no gain.\n */\nexport type ThemeColors = {\n background: string\n foreground: string\n card: string\n cardForeground: string\n popover: string\n popoverForeground: string\n primary: string\n primaryForeground: string\n secondary: string\n secondaryForeground: string\n muted: string\n mutedForeground: string\n accent: string\n accentForeground: string\n destructive: string\n destructiveForeground: string\n destructiveSubtle: string\n /** Contrast-safe solid-action presentation, derived from the canonical error role. */\n destructiveAction: string\n destructiveActionForeground: string\n success: string\n successForeground: string\n successSubtle: string\n warning: string\n warningForeground: string\n warningSubtle: string\n info: string\n infoForeground: string\n infoSubtle: string\n border: string\n input: string\n ring: string\n sidebar: string\n sidebarForeground: string\n sidebarAccent: string\n sidebarBorder: string\n sidebarMutedForeground: string\n}\n\n/** The sixteen ANSI colours a terminal needs, plus its cursor and selection. */\nexport type ThemeTerminalPalette = {\n background: string\n foreground: string\n cursor: string\n selection: string\n black: string\n red: string\n green: string\n yellow: string\n blue: string\n magenta: string\n cyan: string\n white: string\n brightBlack: string\n brightRed: string\n brightGreen: string\n brightYellow: string\n brightBlue: string\n brightMagenta: string\n brightCyan: string\n brightWhite: string\n}\n\n/** The syntax roles a code view needs. */\nexport type ThemeEditorPalette = {\n keyword: string\n string: string\n number: string\n comment: string\n function: string\n variable: string\n type: string\n tag: string\n attribute: string\n operator: string\n heading: string\n link: string\n diffAdd: string\n diffDelete: string\n diffHunk: string\n searchMatch: string\n}\n\n/** Where a palette came from, and the terms it is used under. */\nexport type ThemeProvenance = {\n /** The project or person the palette belongs to. */\n source: string\n url: string\n /** An SPDX identifier. Every palette in this catalogue is permissive. */\n license: string\n}\n\nexport type ThemeVariant = {\n /** The `data-theme` value and the id a preference stores. */\n id: string\n /** Groups the variants that came from one project. */\n family: string\n familyLabel: string\n label: string\n appearance: ThemeAppearance\n /** One line, shown in a picker. */\n description: string\n colors: ThemeColors\n terminal: ThemeTerminalPalette\n editor: ThemeEditorPalette\n /** The categorical series, in order. */\n chart: readonly string[]\n provenance: ThemeProvenance\n /**\n * Custom properties written verbatim over {@link themeCssVariables}, last.\n *\n * For a variant a host built from data it does not control the derivation of —\n * an imported theme whose painted colours must stay exactly what the user\n * imported. The catalogue's own variants never carry it, so the derived roles\n * stay the only source of truth for them.\n */\n tokens?: Readonly<Record<string, string>>\n}\n\n/**\n * The shadcn-vocabulary roles of one catalogue entry.\n *\n * The expensive half of a variant: `shadcnVariables` projects the whole palette\n * (about 4 ms a theme), which is why {@link lazyVariant} defers it.\n */\nfunction colorsOf(theme: AdeaThemeRecord): ThemeColors {\n const projected = shadcnVariables(theme)\n const token = (name: string): string => {\n const value = projected[`--${name}`]\n if (!value) throw new Error(`${theme.id} has no shared shadcn projection for --${name}`)\n return value\n }\n const destructiveAction = shadcnDestructiveProjection(theme)\n return {\n background: token('background'),\n foreground: token('foreground'),\n card: token('card'),\n cardForeground: token('card-foreground'),\n popover: token('popover'),\n popoverForeground: token('popover-foreground'),\n primary: token('primary'),\n primaryForeground: token('primary-foreground'),\n secondary: token('secondary'),\n secondaryForeground: token('secondary-foreground'),\n muted: token('muted'),\n mutedForeground: token('muted-foreground'),\n accent: token('accent'),\n accentForeground: token('accent-foreground'),\n // Preserve the UI's semantic error token. The Themes projection uses its\n // contrast-safe solid-action fill for --destructive; UI keeps that fill in\n // the separate destructiveAction pair below.\n destructive: theme.colors.error,\n destructiveForeground: statusForeground(theme, 'error'),\n destructiveSubtle: tint(theme.colors.error, theme.colors.background),\n destructiveAction: destructiveAction.fill,\n destructiveActionForeground: destructiveAction.foreground,\n success: token('success'),\n successForeground: statusForeground(theme, 'success'),\n successSubtle: tint(theme.colors.success, theme.colors.background),\n warning: token('warning'),\n warningForeground: statusForeground(theme, 'warning'),\n warningSubtle: tint(theme.colors.warning, theme.colors.background),\n info: token('info'),\n infoForeground: statusForeground(theme, 'info'),\n infoSubtle: tint(theme.colors.info, theme.colors.background),\n border: token('border'),\n input: token('input'),\n ring: token('ring'),\n sidebar: token('sidebar'),\n sidebarForeground: token('sidebar-foreground'),\n sidebarAccent: token('sidebar-accent'),\n sidebarBorder: token('sidebar-border'),\n sidebarMutedForeground: token('sidebar-muted-foreground'),\n }\n}\n\n// The embedded terminal uses the theme's own canvas rather than a palette of its\n// own. The ANSI colours came from the palette that produced the canvas, so they\n// were chosen against it; a terminal on a different background would be the one\n// surface in the application not lit by the active theme.\nfunction terminalOf(theme: AdeaThemeRecord): ThemeTerminalPalette {\n return {\n background: theme.colors.background,\n foreground: theme.colors.foreground,\n cursor: theme.cursor,\n selection: theme.selection,\n ...theme.ansi,\n }\n}\n\nfunction editorOf(theme: AdeaThemeRecord): ThemeEditorPalette {\n const syntax = syntaxRoles(theme)\n return {\n keyword: syntax.keyword,\n string: syntax.string,\n number: syntax.number,\n comment: syntax.comment,\n function: syntax.function,\n variable: syntax.variable,\n type: syntax.type,\n tag: syntax.tag,\n attribute: syntax.attribute,\n operator: syntax.operator,\n heading: syntax.heading,\n link: syntax.link,\n diffAdd: syntax.diffAdd,\n diffDelete: syntax.diffDelete,\n diffHunk: syntax.diffHunk,\n searchMatch: syntax.searchMatch,\n }\n}\n\nfunction provenanceOf(theme: AdeaThemeRecord): ThemeProvenance {\n return {\n source: theme.provenance.project,\n url: theme.provenance.url,\n license: theme.provenance.license,\n }\n}\n\n/**\n * The design system's view of one catalogue record, built in full and at once.\n *\n * For a host that holds a record the catalogue does not — an imported theme, a\n * reconstruction from stored data — and wants a variant it can hand to\n * `ThemeProvider`'s `themes`. It throws for a record the shadcn projection cannot\n * map, here and now, rather than on first paint. The catalogue itself goes through\n * {@link lazyVariant} instead.\n */\nexport function variantFromRecord(theme: AdeaThemeRecord): ThemeVariant {\n return {\n id: theme.id,\n family: theme.family,\n familyLabel: theme.familyLabel,\n label: theme.label,\n appearance: theme.appearance,\n description: theme.description,\n colors: colorsOf(theme),\n terminal: terminalOf(theme),\n editor: editorOf(theme),\n chart: chartSeries(theme),\n provenance: provenanceOf(theme),\n }\n}\n\n/** Runs `build` on first call and returns that value from then on. A throw is not cached. */\nfunction once<T>(build: () => T): () => T {\n let built = false\n let value: T\n return () => {\n if (!built) {\n value = build()\n built = true\n }\n return value\n }\n}\n\nconst lazy = <T>(build: () => T): PropertyDescriptor => ({\n get: once(build),\n enumerable: true,\n})\n\n/**\n * {@link variantFromRecord} with the four derived palettes built on first read.\n *\n * Evaluating this module used to project all 34 catalogue themes up front, about\n * 155 ms before a single one was needed, on the hydration path of every consumer.\n * A page paints one variant, and a picker's labels and appearance are plain\n * fields, so only a variant whose `colors`, `terminal`, `editor` or `chart` is read\n * pays for them, once. The properties stay enumerable data-shaped accessors, so\n * spreading, `Object.entries` and JSON see the same object they always did.\n */\nfunction lazyVariant(theme: AdeaThemeRecord): ThemeVariant {\n return Object.defineProperties(\n {\n id: theme.id,\n family: theme.family,\n familyLabel: theme.familyLabel,\n label: theme.label,\n appearance: theme.appearance,\n description: theme.description,\n provenance: provenanceOf(theme),\n },\n {\n colors: lazy(() => colorsOf(theme)),\n terminal: lazy(() => terminalOf(theme)),\n editor: lazy(() => editorOf(theme)),\n chart: lazy(() => chartSeries(theme)),\n }\n ) as ThemeVariant\n}\n\n/**\n * A variant's `--primary-subtle`: the tint over `var(--primary)` at the strength its\n * catalogue record measures (`primarySubtleAlpha`). A variant that is not in the\n * catalogue has nothing to measure, and gets its appearance's ceiling.\n */\nexport function primarySubtleFor(theme: Pick<ThemeVariant, 'id' | 'appearance'>): string {\n const record = getTheme(theme.id)\n return record ? primarySubtleCss(record) : primarySubtleCss(theme.appearance)\n}\n\n/** The catalogue, in catalogue order. */\nexport const builtinThemes: readonly ThemeVariant[] = Object.freeze(catalogue.map(lazyVariant))\n\nexport function themeById(id: string): ThemeVariant | undefined {\n if (!hasTheme(id)) return undefined\n return builtinThemes.find((theme) => theme.id === id)\n}\n\n/**\n * The variant a stored id means, or the default of the same appearance.\n *\n * An id the registry does not hold — an imported theme that was deleted, a\n * catalogue entry that was renamed — must still paint *something of the appearance\n * being shown*. Falling back to whichever variant happens to be first in the\n * registry painted a dark palette under a light `color-scheme` for a light id, so\n * the fallback is the appearance's own default, then any variant of that\n * appearance, then the registry's first entry only when it holds nothing else.\n *\n * `registry` is the builtin catalogue plus whatever extras the host supplies; the\n * stored id is never rewritten, only resolved, so a host that keeps an unknown id\n * keeps resolving it if the theme comes back.\n *\n * The fallback order is translated from Zeron's `ThemeRegistry::resolve` (MIT,\n * Copyright (c) 2026 Wing); see NOTICE.\n */\nexport function resolveVariant(\n registry: readonly ThemeVariant[],\n id: string,\n appearance: ThemeAppearance\n): ThemeVariant {\n const found = registry.find((variant) => variant.id === id)\n if (found) return found\n const defaultId = appearance === 'dark' ? defaultDarkThemeId : defaultLightThemeId\n return (\n registry.find((variant) => variant.id === defaultId) ??\n registry.find((variant) => variant.appearance === appearance) ??\n registry[0]!\n )\n}\n\n/** The variants of one appearance, which is what a picker for that mode shows. */\nexport function themesForAppearance(appearance: ThemeAppearance): readonly ThemeVariant[] {\n const ids = new Set(themesByAppearance(appearance).map((theme) => theme.id))\n return builtinThemes.filter((theme) => ids.has(theme.id))\n}\n\n/** The families in the catalogue, in catalogue order. */\nexport function themeFamilies(): { family: string; label: string; themes: ThemeVariant[] }[] {\n const families = new Map<string, { family: string; label: string; themes: ThemeVariant[] }>()\n for (const theme of builtinThemes) {\n const entry = families.get(theme.family) ?? {\n family: theme.family,\n label: theme.familyLabel,\n themes: [],\n }\n entry.themes.push(theme)\n families.set(theme.family, entry)\n }\n return [...families.values()]\n}\n\n/** The accent presets, in the order a picker should show them. */\nexport const accents = ACCENTS\n\n/**\n * The canonical catalogue records, in catalogue order.\n *\n * {@link builtinThemes} is this system's projection of the catalogue onto the\n * shadcn vocabulary, which is what a document is painted with. A *preview* of a\n * theme — the miniatures `AppearanceEditor` and `ThemeMiniature` draw — reads the\n * catalogue's own roles instead (`surfaceActive`, `borderMuted`, `textSubtle`…),\n * several of which the projection folds together. These are those records, so a\n * consumer hands the editor exactly what it takes rather than reconstructing it\n * from the projection.\n */\nexport const themeRecords: readonly AdeaThemeRecord[] = catalogue\n\nexport function themeRecordById(id: string): AdeaThemeRecord | undefined {\n return getTheme(id)\n}\n\n/** A light/dark theme pair by id, with the defaults standing in for an unknown id. */\nfunction recordPair(lightThemeId: string, darkThemeId: string) {\n const light = getTheme(lightThemeId) ?? getTheme(defaultLightThemeId)!\n const dark = getTheme(darkThemeId) ?? getTheme(defaultDarkThemeId)!\n return { light, dark }\n}\n\n/**\n * The accents the selected light/dark pair carries — offered beside the presets.\n *\n * Computed per pair rather than per theme because a theme accent is a *pair* of\n * values, like a preset: a slot is offered only when both themes can offer it.\n */\nexport function themeAccentsFor(\n lightThemeId: string,\n darkThemeId: string\n): readonly AccentPreset[] {\n const { light, dark } = recordPair(lightThemeId, darkThemeId)\n return themeAccentPresets(light, dark)\n}\n\n/**\n * An accent id resolved against a theme pair: a preset, a theme accent the pair\n * offers, or `undefined` for `theme`, an unknown id, or a slot this pair cannot\n * offer — each of which means \"use the theme's own primary\".\n *\n * Theme accent ids are role-shaped (`ansi-blue`), so a stored one survives a theme\n * switch and resolves to the new theme's blue; when the new pair cannot offer the\n * slot it resolves to nothing, the same fallback a stale preset id gets.\n */\nexport function resolveAccentPreset(\n accentId: string,\n light: AdeaTheme,\n dark: AdeaTheme\n): AccentPreset | undefined {\n if (accentId === 'theme') return undefined\n const preset = getAccent(accentId)\n if (preset) return preset\n if (!isThemeAccentId(accentId)) return undefined\n return themeAccentPresets(light, dark).find((option) => option.id === accentId)\n}\n\n/**\n * A theme with an accent laid over it, in the catalogue's own shape.\n *\n * The accent replaces `accent` and its measured label, which is all an accent\n * changes about a theme; `undefined` returns the theme untouched. This is the\n * overlay a host needs for `AppearanceEditor`'s `lightTheme`/`darkTheme`.\n */\nexport function withAccent<T extends AdeaTheme>(theme: T, accent: AccentPreset | undefined): T {\n if (!accent) return theme\n const value = theme.appearance === 'dark' ? accent.dark : accent.light\n return {\n ...theme,\n colors: { ...theme.colors, accent: value, accentForeground: accentForeground(value) },\n }\n}\n\n/**\n * The two preview themes an appearance selection resolves to, accent applied.\n *\n * The adapter between a stored selection and `AppearanceEditor`: it takes ids —\n * what a preference holds — and returns the canonical records the editor's\n * `lightTheme` and `darkTheme` props take, so a host does not map roles by hand.\n * Unknown ids fall back to the defaults and an accent the pair cannot offer falls\n * back to the theme's own, exactly as `ThemeProvider` resolves them.\n */\nexport function appearancePreviewThemes(selection: {\n lightThemeId: string\n darkThemeId: string\n accent: string\n}): { lightTheme: AdeaThemeRecord; darkTheme: AdeaThemeRecord } {\n const { light, dark } = recordPair(selection.lightThemeId, selection.darkThemeId)\n const accent = resolveAccentPreset(selection.accent, light, dark)\n return { lightTheme: withAccent(light, accent), darkTheme: withAccent(dark, accent) }\n}\n\n/**\n * The properties an accent selection overrides, as CSS custom properties.\n *\n * ## Why this exists rather than relying on the `[data-accent]` blocks\n *\n * `theme.css` carries a `[data-accent='…']` block per preset, and those blocks are\n * correct — but they are unreachable at runtime. `ThemeProvider` writes a theme's\n * whole role set onto `<html>` as *inline* custom properties, and an inline\n * declaration outranks any stylesheet selector regardless of specificity. The\n * attribute was set, the rules existed, the values were right, and every one of the\n * seven accents rendered as the same blue.\n *\n * The blocks are kept, because they are what styles the window between the document\n * arriving and the provider mounting — which is the whole reason `themeScript`\n * exists. But the authoritative value has to be applied by the same code that\n * applies the theme, from the same source, or the two can drift again. So both the\n * provider and the CSS generator call `accentRoles` out of `@adea-ai/themes`, and\n * `tests/appearance-axes.test.ts` asserts the two agree value for value.\n *\n * A theme accent (`ansi-blue`) has no block: its value depends on the theme pair,\n * so it is resolved against `pair` — the selection's light and dark theme ids —\n * and only ever applied here. Without a pair it resolves against the defaults.\n *\n * Returns `undefined` for `theme` — the variant's own primary — for an id the\n * catalogue does not have, and for a theme accent the pair does not offer, so a\n * stale stored preference falls back to the theme rather than blanking the primary.\n */\nexport function accentVariables(\n accentId: string,\n appearance: ThemeAppearance,\n pair: { lightThemeId: string; darkThemeId: string } = {\n lightThemeId: defaultLightThemeId,\n darkThemeId: defaultDarkThemeId,\n },\n options: AccentVariableOptions = {}\n): Record<string, string> | undefined {\n const roles = accentRolesForSelection(accentId, appearance, pair, options)\n return roles ? accentOverrideVariables(roles) : undefined\n}\n\n/** What an accent is held to when it is resolved against a surface. */\nexport type AccentVariableOptions = {\n /** The surface the accent must read on: the variant's `colors.background`. */\n background?: string\n /**\n * The contrast floor against `background`. Absent, a preset is painted verbatim\n * and only a custom `#rrggbb` is held to the default floor.\n */\n floor?: number\n /** The variant the accent is applied to, so the tint takes its measured strength. */\n variantId?: string\n}\n\n/**\n * An accent selection resolved to the full {@link AccentRoles}, against a theme\n * pair. {@link accentVariables} is the document half of this; an element-scoped\n * paint (`paintAccentRoles`) wants all of it, including the tint.\n */\nexport function accentRolesForSelection(\n accentId: string,\n appearance: ThemeAppearance,\n pair: { lightThemeId: string; darkThemeId: string } = {\n lightThemeId: defaultLightThemeId,\n darkThemeId: defaultDarkThemeId,\n },\n options: AccentVariableOptions = {}\n): AccentRoles | undefined {\n if (accentId === 'theme') return undefined\n const record = options.variantId ? getTheme(options.variantId) : undefined\n return resolveAccentRoles(accentId, {\n target: record ?? appearance,\n background: options.background,\n floor: options.floor,\n // Only an `ansi-*` id needs the pair's offering, and measuring it is not free.\n themeAccents: isThemeAccentId(accentId)\n ? themeAccentsFor(pair.lightThemeId, pair.darkThemeId)\n : undefined,\n })\n}\n\n/**\n * Which projection fills the `--editor-*` ramp.\n *\n * - `syntax` (the default) is the catalogue's quiet syntax roles, as `ThemeVariant.editor`\n * holds them: a comment may sit below body-text contrast, because in a code view\n * it is meant to recede.\n * - `floor` holds every editor role to 4.5:1 on the canvas (`editorRolesHex`), for a\n * host that renders code as content rather than as decoration. It is hex, where\n * `syntax` is `oklch()`.\n */\nexport type EditorRolesMode = 'syntax' | 'floor'\n\n/** Options for {@link themeCssVariables}. */\nexport type ThemeCssVariableOptions = {\n /** The `--editor-*` projection. Default `syntax`: absent, the output is unchanged. */\n editor?: EditorRolesMode\n}\n\n/**\n * True for the catalogue's own variant, not a host's copy that merely shares its id.\n *\n * Identity rather than id, because a host's imported theme may reuse a catalogue id\n * with different colours, and projecting the catalogue's record for it would paint\n * the wrong palette.\n */\nexport function isCatalogueVariant(variant: ThemeVariant): boolean {\n return themeById(variant.id) === variant\n}\n\n/**\n * The editor palette held to 4.5:1, or `fallback` for a record that cannot reach it.\n *\n * Some palettes (an imported one, most often) cannot be repaired to the floor from\n * their own ramp within the catalogue's lightness budget, and `editorRolesHex` says\n * so by throwing. A palette that paints nothing is worse than one that paints its\n * quiet roles, so the throw is the cue to fall back rather than to fail. `project` is\n * a parameter so that path can be exercised: no catalogue theme takes it.\n */\nexport function floorEditorPalette(\n record: AdeaTheme,\n fallback: ThemeEditorPalette,\n project: (theme: AdeaTheme) => Record<keyof ThemeEditorPalette, string> = editorRolesHex\n): ThemeEditorPalette {\n try {\n const roles = project(record)\n return {\n keyword: roles.keyword,\n string: roles.string,\n number: roles.number,\n comment: roles.comment,\n function: roles.function,\n variable: roles.variable,\n type: roles.type,\n tag: roles.tag,\n attribute: roles.attribute,\n operator: roles.operator,\n heading: roles.heading,\n link: roles.link,\n diffAdd: roles.diffAdd,\n diffDelete: roles.diffDelete,\n diffHunk: roles.diffHunk,\n searchMatch: roles.searchMatch,\n }\n } catch {\n return fallback\n }\n}\n\nconst floorEditorCache = new Map<string, ThemeEditorPalette>()\n\n/**\n * The editor palette a variant paints under a projection.\n *\n * `floor` applies to the catalogue's own variants, whose record the catalogue holds.\n * A variant it does not hold (a host's imported theme) has no record to project and\n * keeps its own `editor`; such a theme normally carries its roles in `tokens`, which\n * win over either projection anyway. Computed once per theme: the repair is a search.\n */\nexport function editorPaletteFor(\n theme: ThemeVariant,\n mode: EditorRolesMode = 'syntax'\n): ThemeEditorPalette {\n if (mode !== 'floor' || !isCatalogueVariant(theme)) return theme.editor\n const cached = floorEditorCache.get(theme.id)\n if (cached) return cached\n const record = getTheme(theme.id)\n const palette = record ? floorEditorPalette(record, theme.editor) : theme.editor\n floorEditorCache.set(theme.id, palette)\n return palette\n}\n\n/**\n * The roles a variant sets, as CSS custom properties.\n *\n * The one place that knows the mapping from a theme's roles to this system's variable\n * names. A provider applies this; nothing else repeats it. The `--terminal-*` and\n * `--editor-*` ramps are presentations of the catalogue's ANSI and syntax data rather\n * than separate roles, which is why they are built here and not in the catalogue.\n *\n * `options.editor` picks the `--editor-*` projection ({@link EditorRolesMode}).\n * Absent it, the result is what it has always been; the generated `theme-variants.css`\n * and the design-system artifact are built without it.\n */\nexport function themeCssVariables(\n theme: ThemeVariant,\n options: ThemeCssVariableOptions = {}\n): Record<string, string> {\n const c = theme.colors\n const t = theme.terminal\n const e = editorPaletteFor(theme, options.editor)\n const derived = {\n '--background': c.background,\n '--foreground': c.foreground,\n '--card': c.card,\n '--card-foreground': c.cardForeground,\n '--popover': c.popover,\n '--popover-foreground': c.popoverForeground,\n '--primary': c.primary,\n '--primary-foreground': c.primaryForeground,\n /*\n * The two tokens derived from the primary, and they are here rather than left to\n * `theme.css` because they must follow `--primary` — which this function writes\n * for *any* theme in the catalogue, not just the two defaults `theme.css` carries.\n *\n * `--primary-hover` is a resolved colour, not a `color-mix()`: a uniform hover\n * step cannot be expressed as a mix (see `primaryHover`). Left out, a theme switch\n * would keep the previous theme's hover on the new theme's primary.\n *\n * `--primary-subtle` is a mix over `--primary`, so it follows an accent on its\n * own, but its *strength* is per theme: `primarySubtleCss(record)` lowers the\n * appearance ceiling only as far as this theme needs for body text to clear 4.5:1\n * on the tint, over every surface, for every accent it offers. It has to be\n * written per theme for that reason — the appearance-wide value is not legible on\n * every palette.\n */\n '--primary-hover': primaryHover(c.primary, theme.appearance),\n '--primary-subtle': primarySubtleFor(theme),\n '--secondary': c.secondary,\n '--secondary-foreground': c.secondaryForeground,\n '--muted': c.muted,\n '--muted-foreground': c.mutedForeground,\n '--accent': c.accent,\n '--accent-foreground': c.accentForeground,\n '--destructive': c.destructive,\n '--destructive-foreground': c.destructiveForeground,\n '--destructive-subtle': c.destructiveSubtle,\n '--destructive-action': c.destructiveAction,\n '--destructive-action-foreground': c.destructiveActionForeground,\n '--success': c.success,\n '--success-foreground': c.successForeground,\n '--success-subtle': c.successSubtle,\n '--warning': c.warning,\n '--warning-foreground': c.warningForeground,\n '--warning-subtle': c.warningSubtle,\n '--info': c.info,\n '--info-foreground': c.infoForeground,\n '--info-subtle': c.infoSubtle,\n '--border': c.border,\n '--input': c.input,\n '--ring': c.ring,\n '--sidebar': c.sidebar,\n '--sidebar-foreground': c.sidebarForeground,\n '--sidebar-accent': c.sidebarAccent,\n '--sidebar-border': c.sidebarBorder,\n '--sidebar-primary': c.primary,\n '--sidebar-primary-foreground': c.primaryForeground,\n '--sidebar-ring': c.ring,\n '--sidebar-muted-foreground': c.sidebarMutedForeground,\n '--surface-sunken': c.muted,\n '--surface-hover': c.accent,\n '--surface-active': c.border,\n '--chrome': c.background,\n '--terminal-background': t.background,\n '--terminal-foreground': t.foreground,\n '--terminal-cursor': t.cursor,\n '--terminal-selection': t.selection,\n '--terminal-ansi-black': t.black,\n '--terminal-ansi-red': t.red,\n '--terminal-ansi-green': t.green,\n '--terminal-ansi-yellow': t.yellow,\n '--terminal-ansi-blue': t.blue,\n '--terminal-ansi-magenta': t.magenta,\n '--terminal-ansi-cyan': t.cyan,\n '--terminal-ansi-white': t.white,\n '--terminal-ansi-bright-black': t.brightBlack,\n '--terminal-ansi-bright-red': t.brightRed,\n '--terminal-ansi-bright-green': t.brightGreen,\n '--terminal-ansi-bright-yellow': t.brightYellow,\n '--terminal-ansi-bright-blue': t.brightBlue,\n '--terminal-ansi-bright-magenta': t.brightMagenta,\n '--terminal-ansi-bright-cyan': t.brightCyan,\n '--terminal-ansi-bright-white': t.brightWhite,\n '--editor-keyword': e.keyword,\n '--editor-string': e.string,\n '--editor-number': e.number,\n '--editor-comment': e.comment,\n '--editor-function': e.function,\n '--editor-variable': e.variable,\n '--editor-type': e.type,\n '--editor-tag': e.tag,\n '--editor-attribute': e.attribute,\n '--editor-operator': e.operator,\n '--editor-heading': e.heading,\n '--editor-link': e.link,\n '--editor-diff-add': e.diffAdd,\n '--editor-diff-delete': e.diffDelete,\n '--editor-diff-hunk': e.diffHunk,\n '--editor-search-match': e.searchMatch,\n '--chart-1': theme.chart[0] ?? c.primary,\n '--chart-2': theme.chart[1] ?? c.info,\n '--chart-3': theme.chart[2] ?? c.success,\n '--chart-4': theme.chart[3] ?? c.warning,\n '--chart-5': theme.chart[4] ?? c.destructive,\n '--chart-6': theme.chart[5] ?? c.mutedForeground,\n }\n return theme.tokens ? { ...derived, ...theme.tokens } : derived\n}\n\n/**\n * The `--terminal-*` properties of a variant: the palette a terminal paints.\n *\n * What a pinned terminal theme writes over the interface variant's own. Taken from\n * {@link themeCssVariables} rather than rebuilt, so the names, and a custom\n * variant's verbatim `tokens`, can never drift from what the interface variant\n * would have written for the same palette.\n */\nexport function terminalCssVariables(theme: ThemeVariant): Record<string, string> {\n return Object.fromEntries(\n Object.entries(themeCssVariables(theme)).filter(([name]) => name.startsWith('--terminal-'))\n )\n}\n\n/**\n * Token values with every `oklch()` colour written as `#rrggbb`.\n *\n * For a host whose stylesheet, tests or native shell speak hex: the catalogue's\n * roles are `oklch()` strings, and this is the one conversion, gamut-mapped by the\n * catalogue's own `oklchToHex`. A value that is already hex, carries alpha\n * (`rgba()`), or is not a plain colour (`color-mix()`, `var()`) is left byte for\n * byte, because converting would either lose the alpha or be a guess. Suitable as\n * `ThemeProvider`'s `transformTokens`.\n */\nexport function tokensAsHex(tokens: Readonly<Record<string, string>>): Record<string, string> {\n const converted: Record<string, string> = {}\n for (const [name, value] of Object.entries(tokens)) {\n const trimmed = value.trim()\n // `parseOklch` discards alpha, so a translucent oklch() stays as written.\n const opaque = trimmed.startsWith('oklch(') && !trimmed.includes('/')\n const parsed = opaque ? parseColor(trimmed) : undefined\n converted[name] = parsed ? oklchToHex(parsed) : value\n }\n return converted\n}\n\n/* ---------------------------------------------------------------------------\n * Validation\n *\n * Delegated. The floors are the catalogue's, because they are what makes the\n * catalogue able to hold other people's palettes at all, and a second set of floors\n * here would be a second answer to the same question — and the one that drifts.\n * ------------------------------------------------------------------------- */\n\nexport type ThemeFinding = {\n themeId: string\n role: string\n message: string\n}\n\nfunction toFinding(finding: ContrastFinding): ThemeFinding {\n return { themeId: finding.themeId, role: finding.role, message: finding.message }\n}\n\n/** Validates one variant. The floors, and the reasoning behind them, live in the catalogue. */\nexport function validateTheme(theme: ThemeVariant): ThemeFinding[] {\n const record = catalogue.find((entry) => entry.id === theme.id)\n if (!record) {\n return [{ themeId: theme.id, role: 'id', message: `${theme.id} is not in the catalogue` }]\n }\n return validateCatalogue([record]).map(toFinding)\n}\n\n/** Validates the whole registry, including the catalogue-level invariants. */\nexport function validateThemeRegistry(\n themes: readonly ThemeVariant[] = builtinThemes\n): ThemeFinding[] {\n const records = themes\n .map((theme) => catalogue.find((entry) => entry.id === theme.id))\n .filter((record): record is AdeaThemeRecord => !!record)\n\n const orphaned = themes\n .filter((theme) => !hasTheme(theme.id))\n .map((theme) => ({\n themeId: theme.id,\n role: 'id',\n message: `${theme.id} is in the registry but not in the catalogue`,\n }))\n\n return [...orphaned, ...validateCatalogue(records).map(toFinding)]\n}\n\nexport const defaultLightThemeId = 'adea-light'\nexport const defaultDarkThemeId = 'adea-dark'\n\n/** Re-exported so a consumer measuring a colour does not need a second import. */\nexport { contrastRatio } from '@adea-ai/themes'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"themes.js","names":["catalogue","themes"],"sources":["../../src/lib/themes.ts"],"sourcesContent":["/**\n * The theme registry, now a bridge.\n *\n * The catalogue itself lives in `@adea-ai/themes`. This module is what is left in\n * the design system: the translation between the catalogue's **canonical** role\n * names and the **shadcn** vocabulary every component here is written against, plus\n * the handful of custom properties this system declares that are presentations of\n * data the catalogue already holds — the `--terminal-*` ramp, the `--editor-*` ramp,\n * and `--chrome`.\n *\n * ## Why it is a bridge and not a copy\n *\n * This file used to hold thirteen themes as hand-written hex literal tables, with its\n * own contrast validator, its own terminal and editor palettes, and its own\n * provenance records. That is a lot of surface to maintain for one consumer of a\n * palette, and it meant every product that wanted the same themes — Adea, Cortana —\n * either imported this package or wrote its own. The catalogue moved out so that\n * there is one answer to \"what is Catppuccin Mocha\", and this file is the part that\n * genuinely belongs to the design system.\n *\n * It is deliberately thin. If you are looking for a colour, it is not here: see\n * `@adea-ai/themes`, whose README explains how the values are produced and whose\n * `NOTICE` records where every palette came from.\n *\n * ## The mapping, and the entry that is not a rename\n *\n * The package-owned `shadcnVariables` adapter maps canonical roles onto this\n * system's vocabulary. One entry will trip a reader: the catalogue's `accent`\n * becomes this system's `primary`, because in the shadcn vocabulary `--primary` is\n * the action colour — the fill of a primary button, the thing a focus ring is drawn\n * in — while `--accent` is a low-contrast hover wash. The catalogue's `accent` is\n * the former, so it goes to `--primary`, and `--accent` is filled from\n * `surfaceHover`, which is what shadcn components actually use it for.\n */\n\nimport {\n ACCENTS,\n accentForeground,\n chartSeries,\n editorRolesHex,\n getAccent,\n getTheme,\n hasTheme,\n shadcnDestructiveProjection,\n statusForeground,\n tint,\n primaryHover,\n primarySubtleCss,\n syntaxRoles,\n themes as catalogue,\n themesByAppearance,\n themeAccentPresets,\n validateCatalogue,\n type AccentPreset,\n type AdeaTheme,\n type AdeaThemeRecord,\n type ContrastFinding,\n} from '@adea-ai/themes'\nimport { shadcnVariables } from '@adea-ai/themes/adapters/shadcn'\nimport { oklchToHex, parseColor } from '@adea-ai/themes/oklch'\nimport { accentOverrideVariables, resolveAccentRoles, type AccentRoles } from './accent-roles'\nimport { isThemeAccentId } from './theme-accents'\n\nexport { isThemeAccentId } from './theme-accents'\n\nexport type ThemeAppearance = 'light' | 'dark'\n\n/**\n * The surface and text roles, in shadcn's vocabulary.\n *\n * Named to match the components below, which is why they differ from the\n * catalogue's: these are this system's variable names, and renaming them would mean\n * touching every component in the library for no gain.\n */\nexport type ThemeColors = {\n background: string\n foreground: string\n card: string\n cardForeground: string\n popover: string\n popoverForeground: string\n primary: string\n primaryForeground: string\n secondary: string\n secondaryForeground: string\n muted: string\n mutedForeground: string\n accent: string\n accentForeground: string\n destructive: string\n destructiveForeground: string\n destructiveSubtle: string\n /** Contrast-safe solid-action presentation, derived from the canonical error role. */\n destructiveAction: string\n destructiveActionForeground: string\n success: string\n successForeground: string\n successSubtle: string\n warning: string\n warningForeground: string\n warningSubtle: string\n info: string\n infoForeground: string\n infoSubtle: string\n border: string\n input: string\n ring: string\n sidebar: string\n sidebarForeground: string\n sidebarAccent: string\n sidebarBorder: string\n sidebarMutedForeground: string\n}\n\n/** The sixteen ANSI colours a terminal needs, plus its cursor and selection. */\nexport type ThemeTerminalPalette = {\n background: string\n foreground: string\n cursor: string\n selection: string\n black: string\n red: string\n green: string\n yellow: string\n blue: string\n magenta: string\n cyan: string\n white: string\n brightBlack: string\n brightRed: string\n brightGreen: string\n brightYellow: string\n brightBlue: string\n brightMagenta: string\n brightCyan: string\n brightWhite: string\n}\n\n/** The syntax roles a code view needs. */\nexport type ThemeEditorPalette = {\n keyword: string\n string: string\n number: string\n comment: string\n function: string\n variable: string\n type: string\n tag: string\n attribute: string\n operator: string\n heading: string\n link: string\n diffAdd: string\n diffDelete: string\n diffHunk: string\n searchMatch: string\n}\n\n/** Where a palette came from, and the terms it is used under. */\nexport type ThemeProvenance = {\n /** The project or person the palette belongs to. */\n source: string\n url: string\n /** An SPDX identifier. Every palette in this catalogue is permissive. */\n license: string\n}\n\nexport type ThemeVariant = {\n /** The `data-theme` value and the id a preference stores. */\n id: string\n /** Groups the variants that came from one project. */\n family: string\n familyLabel: string\n label: string\n appearance: ThemeAppearance\n /** One line, shown in a picker. */\n description: string\n colors: ThemeColors\n terminal: ThemeTerminalPalette\n editor: ThemeEditorPalette\n /** The categorical series, in order. */\n chart: readonly string[]\n provenance: ThemeProvenance\n /**\n * Custom properties written verbatim over {@link themeCssVariables}, last.\n *\n * For a variant a host built from data it does not control the derivation of —\n * an imported theme whose painted colours must stay exactly what the user\n * imported. The catalogue's own variants never carry it, so the derived roles\n * stay the only source of truth for them.\n */\n tokens?: Readonly<Record<string, string>>\n}\n\n/**\n * The shadcn-vocabulary roles of one catalogue entry.\n *\n * The expensive half of a variant: `shadcnVariables` projects the whole palette\n * (about 4 ms a theme), which is why {@link lazyVariant} defers it.\n */\nfunction colorsOf(theme: AdeaThemeRecord): ThemeColors {\n const projected = shadcnVariables(theme)\n const token = (name: string): string => {\n const value = projected[`--${name}`]\n if (!value) throw new Error(`${theme.id} has no shared shadcn projection for --${name}`)\n return value\n }\n const destructiveAction = shadcnDestructiveProjection(theme)\n return {\n background: token('background'),\n foreground: token('foreground'),\n card: token('card'),\n cardForeground: token('card-foreground'),\n popover: token('popover'),\n popoverForeground: token('popover-foreground'),\n primary: token('primary'),\n primaryForeground: token('primary-foreground'),\n secondary: token('secondary'),\n secondaryForeground: token('secondary-foreground'),\n muted: token('muted'),\n mutedForeground: token('muted-foreground'),\n accent: token('accent'),\n accentForeground: token('accent-foreground'),\n // Preserve the UI's semantic error token. The Themes projection uses its\n // contrast-safe solid-action fill for --destructive; UI keeps that fill in\n // the separate destructiveAction pair below.\n destructive: theme.colors.error,\n destructiveForeground: statusForeground(theme, 'error'),\n destructiveSubtle: tint(theme.colors.error, theme.colors.background),\n destructiveAction: destructiveAction.fill,\n destructiveActionForeground: destructiveAction.foreground,\n success: token('success'),\n successForeground: statusForeground(theme, 'success'),\n successSubtle: tint(theme.colors.success, theme.colors.background),\n warning: token('warning'),\n warningForeground: statusForeground(theme, 'warning'),\n warningSubtle: tint(theme.colors.warning, theme.colors.background),\n info: token('info'),\n infoForeground: statusForeground(theme, 'info'),\n infoSubtle: tint(theme.colors.info, theme.colors.background),\n border: token('border'),\n input: token('input'),\n ring: token('ring'),\n sidebar: token('sidebar'),\n sidebarForeground: token('sidebar-foreground'),\n sidebarAccent: token('sidebar-accent'),\n sidebarBorder: token('sidebar-border'),\n sidebarMutedForeground: token('sidebar-muted-foreground'),\n }\n}\n\n// The embedded terminal uses the theme's own canvas rather than a palette of its\n// own. The ANSI colours came from the palette that produced the canvas, so they\n// were chosen against it; a terminal on a different background would be the one\n// surface in the application not lit by the active theme.\nfunction terminalOf(theme: AdeaThemeRecord): ThemeTerminalPalette {\n return {\n background: theme.colors.background,\n foreground: theme.colors.foreground,\n cursor: theme.cursor,\n selection: theme.selection,\n ...theme.ansi,\n }\n}\n\nfunction editorOf(theme: AdeaThemeRecord): ThemeEditorPalette {\n const syntax = syntaxRoles(theme)\n return {\n keyword: syntax.keyword,\n string: syntax.string,\n number: syntax.number,\n comment: syntax.comment,\n function: syntax.function,\n variable: syntax.variable,\n type: syntax.type,\n tag: syntax.tag,\n attribute: syntax.attribute,\n operator: syntax.operator,\n heading: syntax.heading,\n link: syntax.link,\n diffAdd: syntax.diffAdd,\n diffDelete: syntax.diffDelete,\n diffHunk: syntax.diffHunk,\n searchMatch: syntax.searchMatch,\n }\n}\n\nfunction provenanceOf(theme: AdeaThemeRecord): ThemeProvenance {\n return {\n source: theme.provenance.project,\n url: theme.provenance.url,\n license: theme.provenance.license,\n }\n}\n\n/**\n * The design system's view of one catalogue record, built in full and at once.\n *\n * For a host that holds a record the catalogue does not — an imported theme, a\n * reconstruction from stored data — and wants a variant it can hand to\n * `ThemeProvider`'s `themes`. It throws for a record the shadcn projection cannot\n * map, here and now, rather than on first paint. The catalogue itself goes through\n * {@link lazyVariant} instead.\n */\nexport function variantFromRecord(theme: AdeaThemeRecord): ThemeVariant {\n return {\n id: theme.id,\n family: theme.family,\n familyLabel: theme.familyLabel,\n label: theme.label,\n appearance: theme.appearance,\n description: theme.description,\n colors: colorsOf(theme),\n terminal: terminalOf(theme),\n editor: editorOf(theme),\n chart: chartSeries(theme),\n provenance: provenanceOf(theme),\n }\n}\n\n/** Runs `build` on first call and returns that value from then on. A throw is not cached. */\nfunction once<T>(build: () => T): () => T {\n let built = false\n let value: T\n return () => {\n if (!built) {\n value = build()\n built = true\n }\n return value\n }\n}\n\nconst lazy = <T>(build: () => T): PropertyDescriptor => ({\n get: once(build),\n enumerable: true,\n})\n\n/**\n * {@link variantFromRecord} with the four derived palettes built on first read.\n *\n * Evaluating this module used to project all 34 catalogue themes up front, about\n * 155 ms before a single one was needed, on the hydration path of every consumer.\n * A page paints one variant, and a picker's labels and appearance are plain\n * fields, so only a variant whose `colors`, `terminal`, `editor` or `chart` is read\n * pays for them, once. The properties stay enumerable data-shaped accessors, so\n * spreading, `Object.entries` and JSON see the same object they always did.\n */\nfunction lazyVariant(theme: AdeaThemeRecord): ThemeVariant {\n return Object.defineProperties(\n {\n id: theme.id,\n family: theme.family,\n familyLabel: theme.familyLabel,\n label: theme.label,\n appearance: theme.appearance,\n description: theme.description,\n provenance: provenanceOf(theme),\n },\n {\n colors: lazy(() => colorsOf(theme)),\n terminal: lazy(() => terminalOf(theme)),\n editor: lazy(() => editorOf(theme)),\n chart: lazy(() => chartSeries(theme)),\n }\n ) as ThemeVariant\n}\n\n/**\n * A variant's `--primary-subtle`: the tint over `var(--primary)` at the strength its\n * catalogue record measures (`primarySubtleAlpha`). A variant that is not in the\n * catalogue has nothing to measure, and gets its appearance's ceiling.\n */\nexport function primarySubtleFor(theme: Pick<ThemeVariant, 'id' | 'appearance'>): string {\n const record = getTheme(theme.id)\n return record ? primarySubtleCss(record) : primarySubtleCss(theme.appearance)\n}\n\n/** The catalogue, in catalogue order. */\nexport const builtinThemes: readonly ThemeVariant[] = Object.freeze(catalogue.map(lazyVariant))\n\nexport function themeById(id: string): ThemeVariant | undefined {\n if (!hasTheme(id)) return undefined\n return builtinThemes.find((theme) => theme.id === id)\n}\n\n/**\n * The variant a stored id means, or the default of the same appearance.\n *\n * An id the registry does not hold — an imported theme that was deleted, a\n * catalogue entry that was renamed — must still paint *something of the appearance\n * being shown*. Falling back to whichever variant happens to be first in the\n * registry painted a dark palette under a light `color-scheme` for a light id, so\n * the fallback is the appearance's own default, then any variant of that\n * appearance, then the registry's first entry only when it holds nothing else.\n *\n * `registry` is the builtin catalogue plus whatever extras the host supplies; the\n * stored id is never rewritten, only resolved, so a host that keeps an unknown id\n * keeps resolving it if the theme comes back.\n *\n * The fallback order is translated from Zeron's `ThemeRegistry::resolve` (MIT,\n * Copyright (c) 2026 Wing); see NOTICE.\n */\nexport function resolveVariant(\n registry: readonly ThemeVariant[],\n id: string,\n appearance: ThemeAppearance\n): ThemeVariant {\n const found = registry.find((variant) => variant.id === id)\n if (found) return found\n const defaultId = appearance === 'dark' ? defaultDarkThemeId : defaultLightThemeId\n return (\n registry.find((variant) => variant.id === defaultId) ??\n registry.find((variant) => variant.appearance === appearance) ??\n registry[0]!\n )\n}\n\n/** The variants of one appearance, which is what a picker for that mode shows. */\nexport function themesForAppearance(appearance: ThemeAppearance): readonly ThemeVariant[] {\n const ids = new Set(themesByAppearance(appearance).map((theme) => theme.id))\n return builtinThemes.filter((theme) => ids.has(theme.id))\n}\n\n/** The families in the catalogue, in catalogue order. */\nexport function themeFamilies(): { family: string; label: string; themes: ThemeVariant[] }[] {\n const families = new Map<string, { family: string; label: string; themes: ThemeVariant[] }>()\n for (const theme of builtinThemes) {\n const entry = families.get(theme.family) ?? {\n family: theme.family,\n label: theme.familyLabel,\n themes: [],\n }\n entry.themes.push(theme)\n families.set(theme.family, entry)\n }\n return [...families.values()]\n}\n\n/** The accent presets, in the order a picker should show them. */\nexport const accents = ACCENTS\n\n/**\n * The canonical catalogue records, in catalogue order.\n *\n * {@link builtinThemes} is this system's projection of the catalogue onto the\n * shadcn vocabulary, which is what a document is painted with. A *preview* of a\n * theme — the miniatures `AppearanceEditor` and `ThemeMiniature` draw — reads the\n * catalogue's own roles instead (`surfaceActive`, `borderMuted`, `textSubtle`…),\n * several of which the projection folds together. These are those records, so a\n * consumer hands the editor exactly what it takes rather than reconstructing it\n * from the projection.\n */\nexport const themeRecords: readonly AdeaThemeRecord[] = catalogue\n\nexport function themeRecordById(id: string): AdeaThemeRecord | undefined {\n return getTheme(id)\n}\n\n/** A light/dark theme pair by id, with the defaults standing in for an unknown id. */\nfunction recordPair(lightThemeId: string, darkThemeId: string) {\n const light = getTheme(lightThemeId) ?? getTheme(defaultLightThemeId)!\n const dark = getTheme(darkThemeId) ?? getTheme(defaultDarkThemeId)!\n return { light, dark }\n}\n\n/**\n * The accents the selected light/dark pair carries — offered beside the presets.\n *\n * Computed per pair rather than per theme because a theme accent is a *pair* of\n * values, like a preset: a slot is offered only when both themes can offer it.\n */\nexport function themeAccentsFor(\n lightThemeId: string,\n darkThemeId: string\n): readonly AccentPreset[] {\n const { light, dark } = recordPair(lightThemeId, darkThemeId)\n return themeAccentPresets(light, dark)\n}\n\n/**\n * An accent id resolved against a theme pair: a preset, a theme accent the pair\n * offers, or `undefined` for `theme`, an unknown id, or a slot this pair cannot\n * offer — each of which means \"use the theme's own primary\".\n *\n * Theme accent ids are role-shaped (`ansi-blue`), so a stored one survives a theme\n * switch and resolves to the new theme's blue; when the new pair cannot offer the\n * slot it resolves to nothing, the same fallback a stale preset id gets.\n */\nexport function resolveAccentPreset(\n accentId: string,\n light: AdeaTheme,\n dark: AdeaTheme\n): AccentPreset | undefined {\n if (accentId === 'theme') return undefined\n const preset = getAccent(accentId)\n if (preset) return preset\n if (!isThemeAccentId(accentId)) return undefined\n return themeAccentPresets(light, dark).find((option) => option.id === accentId)\n}\n\n/**\n * A theme with an accent laid over it, in the catalogue's own shape.\n *\n * The accent replaces `accent` and its measured label, which is all an accent\n * changes about a theme; `undefined` returns the theme untouched. This is the\n * overlay a host needs for `AppearanceEditor`'s `lightTheme`/`darkTheme`.\n */\nexport function withAccent<T extends AdeaTheme>(theme: T, accent: AccentPreset | undefined): T {\n if (!accent) return theme\n const value = theme.appearance === 'dark' ? accent.dark : accent.light\n return {\n ...theme,\n colors: { ...theme.colors, accent: value, accentForeground: accentForeground(value) },\n }\n}\n\n/**\n * The two preview themes an appearance selection resolves to, accent applied.\n *\n * The adapter between a stored selection and `AppearanceEditor`: it takes ids —\n * what a preference holds — and returns the canonical records the editor's\n * `lightTheme` and `darkTheme` props take, so a host does not map roles by hand.\n * Unknown ids fall back to the defaults and an accent the pair cannot offer falls\n * back to the theme's own, exactly as `ThemeProvider` resolves them.\n */\nexport function appearancePreviewThemes(selection: {\n lightThemeId: string\n darkThemeId: string\n accent: string\n}): { lightTheme: AdeaThemeRecord; darkTheme: AdeaThemeRecord } {\n const { light, dark } = recordPair(selection.lightThemeId, selection.darkThemeId)\n const accent = resolveAccentPreset(selection.accent, light, dark)\n return { lightTheme: withAccent(light, accent), darkTheme: withAccent(dark, accent) }\n}\n\n/**\n * The properties an accent selection overrides, as CSS custom properties.\n *\n * ## Why this exists rather than relying on the `[data-accent]` blocks\n *\n * `theme.css` carries a `[data-accent='…']` block per preset, and those blocks are\n * correct — but they are unreachable at runtime. `ThemeProvider` writes a theme's\n * whole role set onto `<html>` as *inline* custom properties, and an inline\n * declaration outranks any stylesheet selector regardless of specificity. The\n * attribute was set, the rules existed, the values were right, and every one of the\n * seven accents rendered as the same blue.\n *\n * The blocks are kept, because they are what styles the window between the document\n * arriving and the provider mounting — which is the whole reason `themeScript`\n * exists. But the authoritative value has to be applied by the same code that\n * applies the theme, from the same source, or the two can drift again. So both the\n * provider and the CSS generator call `accentRoles` out of `@adea-ai/themes`, and\n * `tests/appearance-axes.test.ts` asserts the two agree value for value.\n *\n * A theme accent (`ansi-blue`) has no block: its value depends on the theme pair,\n * so it is resolved against `pair` — the selection's light and dark theme ids —\n * and only ever applied here. Without a pair it resolves against the defaults.\n *\n * Returns `undefined` for `theme` — the variant's own primary — for an id the\n * catalogue does not have, and for a theme accent the pair does not offer, so a\n * stale stored preference falls back to the theme rather than blanking the primary.\n */\nexport function accentVariables(\n accentId: string,\n appearance: ThemeAppearance,\n pair: { lightThemeId: string; darkThemeId: string } = {\n lightThemeId: defaultLightThemeId,\n darkThemeId: defaultDarkThemeId,\n },\n options: AccentVariableOptions = {}\n): Record<string, string> | undefined {\n const roles = accentRolesForSelection(accentId, appearance, pair, options)\n return roles ? accentOverrideVariables(roles) : undefined\n}\n\n/** What an accent is held to when it is resolved against a surface. */\nexport type AccentVariableOptions = {\n /** The surface the accent must read on: the variant's `colors.background`. */\n background?: string\n /**\n * The contrast floor against `background`. Absent, a preset is painted verbatim\n * and only a custom `#rrggbb` is held to the default floor.\n */\n floor?: number\n /** The variant the accent is applied to, so the tint takes its measured strength. */\n variantId?: string\n}\n\n/**\n * An accent selection resolved to the full {@link AccentRoles}, against a theme\n * pair. {@link accentVariables} is the document half of this; an element-scoped\n * paint (`paintAccentRoles`) wants all of it, including the tint.\n */\nexport function accentRolesForSelection(\n accentId: string,\n appearance: ThemeAppearance,\n pair: { lightThemeId: string; darkThemeId: string } = {\n lightThemeId: defaultLightThemeId,\n darkThemeId: defaultDarkThemeId,\n },\n options: AccentVariableOptions = {}\n): AccentRoles | undefined {\n if (accentId === 'theme') return undefined\n const record = options.variantId ? getTheme(options.variantId) : undefined\n return resolveAccentRoles(accentId, {\n target: record ?? appearance,\n background: options.background,\n floor: options.floor,\n // Only an `ansi-*` id needs the pair's offering, and measuring it is not free.\n themeAccents: isThemeAccentId(accentId)\n ? themeAccentsFor(pair.lightThemeId, pair.darkThemeId)\n : undefined,\n })\n}\n\n/**\n * Which projection fills the `--editor-*` ramp.\n *\n * - `syntax` (the default) is the catalogue's quiet syntax roles, as `ThemeVariant.editor`\n * holds them: a comment may sit below body-text contrast, because in a code view\n * it is meant to recede.\n * - `floor` holds every editor role to 4.5:1 on the canvas (`editorRolesHex`), for a\n * host that renders code as content rather than as decoration. It is hex, where\n * `syntax` is `oklch()`.\n */\nexport type EditorRolesMode = 'syntax' | 'floor'\n\n/** Options for {@link themeCssVariables}. */\nexport type ThemeCssVariableOptions = {\n /** The `--editor-*` projection. Default `syntax`: absent, the output is unchanged. */\n editor?: EditorRolesMode\n}\n\n/**\n * True for the catalogue's own variant, not a host's copy that merely shares its id.\n *\n * Identity rather than id, because a host's imported theme may reuse a catalogue id\n * with different colours, and projecting the catalogue's record for it would paint\n * the wrong palette.\n */\nexport function isCatalogueVariant(variant: ThemeVariant): boolean {\n return themeById(variant.id) === variant\n}\n\n/**\n * The editor palette held to 4.5:1, or `fallback` for a record that cannot reach it.\n *\n * Some palettes (an imported one, most often) cannot be repaired to the floor from\n * their own ramp within the catalogue's lightness budget, and `editorRolesHex` says\n * so by throwing. A palette that paints nothing is worse than one that paints its\n * quiet roles, so the throw is the cue to fall back rather than to fail. `project` is\n * a parameter so that path can be exercised: no catalogue theme takes it.\n */\nexport function floorEditorPalette(\n record: AdeaTheme,\n fallback: ThemeEditorPalette,\n project: (theme: AdeaTheme) => Record<keyof ThemeEditorPalette, string> = editorRolesHex\n): ThemeEditorPalette {\n try {\n const roles = project(record)\n return {\n keyword: roles.keyword,\n string: roles.string,\n number: roles.number,\n comment: roles.comment,\n function: roles.function,\n variable: roles.variable,\n type: roles.type,\n tag: roles.tag,\n attribute: roles.attribute,\n operator: roles.operator,\n heading: roles.heading,\n link: roles.link,\n diffAdd: roles.diffAdd,\n diffDelete: roles.diffDelete,\n diffHunk: roles.diffHunk,\n searchMatch: roles.searchMatch,\n }\n } catch {\n return fallback\n }\n}\n\nconst floorEditorCache = new Map<string, ThemeEditorPalette>()\n\n/**\n * The editor palette a variant paints under a projection.\n *\n * `floor` applies to the catalogue's own variants, whose record the catalogue holds.\n * A variant it does not hold (a host's imported theme) has no record to project and\n * keeps its own `editor`; such a theme normally carries its roles in `tokens`, which\n * win over either projection anyway. Computed once per theme: the repair is a search.\n */\nexport function editorPaletteFor(\n theme: ThemeVariant,\n mode: EditorRolesMode = 'syntax'\n): ThemeEditorPalette {\n if (mode !== 'floor' || !isCatalogueVariant(theme)) return theme.editor\n const cached = floorEditorCache.get(theme.id)\n if (cached) return cached\n const record = getTheme(theme.id)\n const palette = record ? floorEditorPalette(record, theme.editor) : theme.editor\n floorEditorCache.set(theme.id, palette)\n return palette\n}\n\n/**\n * The roles a variant sets, as CSS custom properties.\n *\n * The one place that knows the mapping from a theme's roles to this system's variable\n * names. A provider applies this; nothing else repeats it. The `--terminal-*` and\n * `--editor-*` ramps are presentations of the catalogue's ANSI and syntax data rather\n * than separate roles, which is why they are built here and not in the catalogue.\n *\n * `options.editor` picks the `--editor-*` projection ({@link EditorRolesMode}).\n * Absent it, the result is what it has always been; the generated `theme-variants.css`\n * and the design-system artifact are built without it.\n */\nexport function themeCssVariables(\n theme: ThemeVariant,\n options: ThemeCssVariableOptions = {}\n): Record<string, string> {\n const c = theme.colors\n const t = theme.terminal\n const e = editorPaletteFor(theme, options.editor)\n const derived = {\n '--background': c.background,\n '--foreground': c.foreground,\n '--card': c.card,\n '--card-foreground': c.cardForeground,\n '--popover': c.popover,\n '--popover-foreground': c.popoverForeground,\n '--primary': c.primary,\n '--primary-foreground': c.primaryForeground,\n /*\n * The two tokens derived from the primary, and they are here rather than left to\n * `theme.css` because they must follow `--primary` — which this function writes\n * for *any* theme in the catalogue, not just the two defaults `theme.css` carries.\n *\n * `--primary-hover` is a resolved colour, not a `color-mix()`: a uniform hover\n * step cannot be expressed as a mix (see `primaryHover`). Left out, a theme switch\n * would keep the previous theme's hover on the new theme's primary.\n *\n * `--primary-subtle` is a mix over `--primary`, so it follows an accent on its\n * own, but its *strength* is per theme: `primarySubtleCss(record)` lowers the\n * appearance ceiling only as far as this theme needs for body text to clear 4.5:1\n * on the tint, over every surface, for every accent it offers. It has to be\n * written per theme for that reason — the appearance-wide value is not legible on\n * every palette.\n */\n '--primary-hover': primaryHover(c.primary, theme.appearance),\n '--primary-subtle': primarySubtleFor(theme),\n '--secondary': c.secondary,\n '--secondary-foreground': c.secondaryForeground,\n '--muted': c.muted,\n '--muted-foreground': c.mutedForeground,\n '--accent': c.accent,\n '--accent-foreground': c.accentForeground,\n '--destructive': c.destructive,\n '--destructive-foreground': c.destructiveForeground,\n '--destructive-subtle': c.destructiveSubtle,\n '--destructive-action': c.destructiveAction,\n '--destructive-action-foreground': c.destructiveActionForeground,\n '--success': c.success,\n '--success-foreground': c.successForeground,\n '--success-subtle': c.successSubtle,\n '--warning': c.warning,\n '--warning-foreground': c.warningForeground,\n '--warning-subtle': c.warningSubtle,\n '--info': c.info,\n '--info-foreground': c.infoForeground,\n '--info-subtle': c.infoSubtle,\n '--border': c.border,\n '--input': c.input,\n '--ring': c.ring,\n '--sidebar': c.sidebar,\n '--sidebar-foreground': c.sidebarForeground,\n '--sidebar-accent': c.sidebarAccent,\n '--sidebar-border': c.sidebarBorder,\n '--sidebar-primary': c.primary,\n '--sidebar-primary-foreground': c.primaryForeground,\n '--sidebar-ring': c.ring,\n '--sidebar-muted-foreground': c.sidebarMutedForeground,\n '--surface-sunken': c.muted,\n '--surface-hover': c.accent,\n '--surface-active': c.border,\n '--chrome': c.background,\n '--terminal-background': t.background,\n '--terminal-foreground': t.foreground,\n '--terminal-cursor': t.cursor,\n '--terminal-selection': t.selection,\n '--terminal-ansi-black': t.black,\n '--terminal-ansi-red': t.red,\n '--terminal-ansi-green': t.green,\n '--terminal-ansi-yellow': t.yellow,\n '--terminal-ansi-blue': t.blue,\n '--terminal-ansi-magenta': t.magenta,\n '--terminal-ansi-cyan': t.cyan,\n '--terminal-ansi-white': t.white,\n '--terminal-ansi-bright-black': t.brightBlack,\n '--terminal-ansi-bright-red': t.brightRed,\n '--terminal-ansi-bright-green': t.brightGreen,\n '--terminal-ansi-bright-yellow': t.brightYellow,\n '--terminal-ansi-bright-blue': t.brightBlue,\n '--terminal-ansi-bright-magenta': t.brightMagenta,\n '--terminal-ansi-bright-cyan': t.brightCyan,\n '--terminal-ansi-bright-white': t.brightWhite,\n '--editor-keyword': e.keyword,\n '--editor-string': e.string,\n '--editor-number': e.number,\n '--editor-comment': e.comment,\n '--editor-function': e.function,\n '--editor-variable': e.variable,\n '--editor-type': e.type,\n '--editor-tag': e.tag,\n '--editor-attribute': e.attribute,\n '--editor-operator': e.operator,\n '--editor-heading': e.heading,\n '--editor-link': e.link,\n '--editor-diff-add': e.diffAdd,\n '--editor-diff-delete': e.diffDelete,\n '--editor-diff-hunk': e.diffHunk,\n '--editor-search-match': e.searchMatch,\n '--chart-1': theme.chart[0] ?? c.primary,\n '--chart-2': theme.chart[1] ?? c.info,\n '--chart-3': theme.chart[2] ?? c.success,\n '--chart-4': theme.chart[3] ?? c.warning,\n '--chart-5': theme.chart[4] ?? c.destructive,\n '--chart-6': theme.chart[5] ?? c.mutedForeground,\n }\n return theme.tokens ? { ...derived, ...theme.tokens } : derived\n}\n\n/**\n * The `--terminal-*` properties of a variant: the palette a terminal paints.\n *\n * What a pinned terminal theme writes over the interface variant's own. Taken from\n * {@link themeCssVariables} rather than rebuilt, so the names, and a custom\n * variant's verbatim `tokens`, can never drift from what the interface variant\n * would have written for the same palette.\n */\nexport function terminalCssVariables(theme: ThemeVariant): Record<string, string> {\n return Object.fromEntries(\n Object.entries(themeCssVariables(theme)).filter(([name]) => name.startsWith('--terminal-'))\n )\n}\n\n/**\n * Token values with every `oklch()` colour written as `#rrggbb`.\n *\n * For a host whose stylesheet, tests or native shell speak hex: the catalogue's\n * roles are `oklch()` strings, and this is the one conversion, gamut-mapped by the\n * catalogue's own `oklchToHex`. A value that is already hex, carries alpha\n * (`rgba()`), or is not a plain colour (`color-mix()`, `var()`) is left byte for\n * byte, because converting would either lose the alpha or be a guess. Suitable as\n * `ThemeProvider`'s `transformTokens`.\n */\nexport function tokensAsHex(tokens: Readonly<Record<string, string>>): Record<string, string> {\n const converted: Record<string, string> = {}\n for (const [name, value] of Object.entries(tokens)) {\n const trimmed = value.trim()\n // `parseOklch` discards alpha, so a translucent oklch() stays as written.\n const opaque = trimmed.startsWith('oklch(') && !trimmed.includes('/')\n const parsed = opaque ? parseColor(trimmed) : undefined\n converted[name] = parsed ? oklchToHex(parsed) : value\n }\n return converted\n}\n\n/* ---------------------------------------------------------------------------\n * Validation\n *\n * Delegated. The floors are the catalogue's, because they are what makes the\n * catalogue able to hold other people's palettes at all, and a second set of floors\n * here would be a second answer to the same question — and the one that drifts.\n * ------------------------------------------------------------------------- */\n\nexport type ThemeFinding = {\n themeId: string\n role: string\n message: string\n}\n\nfunction toFinding(finding: ContrastFinding): ThemeFinding {\n return { themeId: finding.themeId, role: finding.role, message: finding.message }\n}\n\n/** Validates one variant. The floors, and the reasoning behind them, live in the catalogue. */\nexport function validateTheme(theme: ThemeVariant): ThemeFinding[] {\n const record = catalogue.find((entry) => entry.id === theme.id)\n if (!record) {\n return [{ themeId: theme.id, role: 'id', message: `${theme.id} is not in the catalogue` }]\n }\n return validateCatalogue([record]).map(toFinding)\n}\n\n/** Validates the whole registry, including the catalogue-level invariants. */\nexport function validateThemeRegistry(\n themes: readonly ThemeVariant[] = builtinThemes\n): ThemeFinding[] {\n const records = themes\n .map((theme) => catalogue.find((entry) => entry.id === theme.id))\n .filter((record): record is AdeaThemeRecord => !!record)\n\n const orphaned = themes\n .filter((theme) => !hasTheme(theme.id))\n .map((theme) => ({\n themeId: theme.id,\n role: 'id',\n message: `${theme.id} is in the registry but not in the catalogue`,\n }))\n\n return [...orphaned, ...validateCatalogue(records).map(toFinding)]\n}\n\nexport const defaultLightThemeId = 'adea-light'\nexport const defaultDarkThemeId = 'adea-dark'\n\n/** Re-exported so a consumer measuring a colour does not need a second import. */\nexport { contrastRatio } from '@adea-ai/themes'\n"],"mappings":"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"}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
//#region src/lib/tooltip-focus-gate.ts
|
|
2
|
+
/** How long after a Tab key press a resulting focus event still counts as
|
|
3
|
+
* keyboard navigation. The native focus move follows the key press
|
|
4
|
+
* immediately; the window only has to bridge that dispatch, not a later
|
|
5
|
+
* script-driven focus. */
|
|
6
|
+
var TOOLTIP_FOCUS_INTENT_WINDOW_MS = 250;
|
|
1
7
|
/** Elements a tooltip-bearing control is rendered as. Everything else keeps
|
|
2
8
|
* its native focus behaviour, and so do resize grips (see below). */
|
|
3
9
|
var BUTTON_LIKE_SELECTOR = "button, a[href], [role=\"button\"], [role=\"link\"]";
|
|
@@ -129,6 +135,6 @@ function releaseTooltipFocusGate(targetDoc) {
|
|
|
129
135
|
gateHolders.set(doc, holders - 1);
|
|
130
136
|
}
|
|
131
137
|
//#endregion
|
|
132
|
-
export { acquireTooltipFocusGate, installTooltipFocusGate, isButtonLikeFocusTarget, releaseTooltipFocusGate, shouldSuppressTooltipFocus };
|
|
138
|
+
export { TOOLTIP_FOCUS_INTENT_WINDOW_MS, acquireTooltipFocusGate, installTooltipFocusGate, isButtonLikeFocusTarget, releaseTooltipFocusGate, shouldSuppressTooltipFocus };
|
|
133
139
|
|
|
134
140
|
//# sourceMappingURL=tooltip-focus-gate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tooltip-focus-gate.js","names":[],"sources":["../../src/lib/tooltip-focus-gate.ts"],"sourcesContent":["/*\n * Tooltips open on active hover over the control, or on keyboard focus that\n * follows a Tab key press. They must never open from programmatic focus: the\n * autofocus a dialog or sheet gives its first control, and the focus a closing\n * overlay restores to its opener, both land on buttons that carry tooltips —\n * and the tooltip primitive opens immediately on focus and pins the tooltip\n * until blur. That is the \"tooltip auto-displays when a sheet opens\" report,\n * and the pinned tooltip is the \"tooltip gets stuck and never disappears\"\n * report: the pointer is nowhere near the control, so no pointer leave ever\n * closes it.\n *\n * The trigger listens for the native `focus` event on the trigger element, and\n * composites build tooltips internally (ActionButton renders one from its\n * `tooltip` prop), so the single interception point that covers every tooltip\n * is a capture-phase `focus` listener on `document`. When focus lands without a\n * keyboard-intent signal on a button-like element, the event is stopped before\n * any element listener runs: the tooltip never learns about the focus. Hover\n * opens are untouched (they ride pointer events), and the separate `focusin`\n * event still flows, so dialog focus-cycle tracking, `:focus-visible` styling,\n * and focus restoration all keep working. Focus still lands — only the event\n * is stopped — so `document.activeElement` and scripted `.blur()` behave.\n *\n * The keyboard-intent signal is a Tab key press immediately before the focus —\n * the only key that moves focus natively. Enter and Space open overlays too,\n * so any \"recent key\" window wide enough for those would let an overlay's own\n * autofocus back through; Tab-only keeps autofocus suppressed no matter how\n * the overlay was opened, while normal Tab navigation still announces each\n * control's tooltip (hover descriptions stay reachable from the keyboard).\n *\n * Suppression is scoped to button-like targets (`button`, `a[href]`,\n * `[role=\"button\"]`, `[role=\"link\"]`). Composite widgets that move focus\n * programmatically — menu items, listbox options, grid cells — are other\n * element types whose roving-focus highlighting must keep receiving focus\n * events, and text controls keep their own focus behaviour (select on focus,\n * caret placement).\n *\n * Suppressed focus is real as far as the DOM is concerned (`activeElement`\n * moves; only the event was stopped), and the tooltip interaction model treats\n * a trigger that is `activeElement` as keyboard-held: while a tooltip on it is\n * open, pointer movement away only *requests* a close and the request is\n * refused for as long as the pointer keeps moving. Left alone, a later real\n * hover on that control would open the tip and never let it leave — the\n * residual \"stuck\" case. The gate therefore treats a suppressed focus as a\n * phantom and releases it: the first pointer activity after the focus (a\n * pointer session taking over from a script) blurs the element. Keyboard\n * sessions never release it and never need to — keyboard focus moves by Tab,\n * which is never suppressed, so nothing pins; and the release only runs for\n * elements still holding `activeElement`, so a Tab already taken from the\n * phantom is left alone.\n *\n * Focus that arrives while a pointer button is held belongs to a pointer\n * gesture, not to a tooltip: the pixel resize handle focuses its handle when a\n * drag starts and cancels the drag on blur, so a gesture focus must pass\n * through untouched (no suppression, no phantom, no release). A gesture's own\n * click behaviour already keeps tooltips closed, and the autofocus cases this\n * gate exists for all happen with the pointer up.\n *\n * Independently of the gesture window, resize grips render as\n * `<button role=\"separator\">` and carry no tooltip, so they are excluded from\n * the button-like scope entirely (see `isButtonLikeFocusTarget`).\n *\n * The gate is installed by the `Tooltip` root for the lifetime of the first\n * tooltip in a document and removed when the last one unmounts, so every\n * consumer of the primitive — including composites that build their own\n * tooltips — inherits the contract without wiring of its own.\n */\n\n/** How long after a Tab key press a resulting focus event still counts as\n * keyboard navigation. The native focus move follows the key press\n * immediately; the window only has to bridge that dispatch, not a later\n * script-driven focus. */\nexport const TOOLTIP_FOCUS_INTENT_WINDOW_MS = 250\n\n/** Elements a tooltip-bearing control is rendered as. Everything else keeps\n * its native focus behaviour, and so do resize grips (see below). */\nconst BUTTON_LIKE_SELECTOR = 'button, a[href], [role=\"button\"], [role=\"link\"]'\n\n/**\n * Whether a focus event landing on this target could open a tooltip on a\n * button-like control. Accepts any Element (an SVG glyph inside a control,\n * for instance) and duck-types so the decision is testable without a DOM.\n */\nexport function isButtonLikeFocusTarget(target: unknown): boolean {\n const element = target as Element | null | undefined\n if (typeof element?.closest !== 'function') return false\n const control = element.closest(BUTTON_LIKE_SELECTOR)\n if (control === null) return false\n // Resize grips render as `<button role=\"separator\">` and focus themselves\n // when a pointer drag starts. They carry no tooltip, and the blur that\n // releases a suppressed focus would end the drag on its first move.\n return control.getAttribute?.('role') !== 'separator'\n}\n\n/**\n * The suppression decision: a button-like focus target hides a tooltip only\n * when no keyboard navigation is in flight. Anything else — a Tab-driven\n * focus, or focus on a non-button element — passes through untouched.\n */\nexport function shouldSuppressTooltipFocus(input: {\n isButtonLike: boolean\n keyboardIntentActive: boolean\n}): boolean {\n return input.isButtonLike && !input.keyboardIntentActive\n}\n\nconst installedDocuments = new WeakSet<Document>()\n\n/**\n * Install the gate on a document. Idempotent per document; returns a disposer\n * that removes the listeners (used by tests and by the tooltip root, which\n * releases the gate when its document's last tooltip unmounts).\n */\nexport function installTooltipFocusGate(doc: Document = document): () => void {\n if (installedDocuments.has(doc)) return () => {}\n\n let lastTabKeyDownAt = 0\n /** True between a pointerdown and its pointerup/pointercancel: focus events\n * inside that window belong to a pointer gesture (drag bootstrap, press)\n * and must reach their element untouched. */\n let pointerGestureActive = false\n /** The element holding a suppressed programmatic focus, if it still holds\n * `activeElement`. One slot: a newer suppressed focus replaces an older\n * one, and anything that already lost focus needs no release. */\n let phantomFocusTarget: Element | undefined\n /** The phantom-release `pointermove` listener exists only while a phantom\n * does: a gate that parks a permanent pointer listener on every document\n * with a tooltip in it costs every page a standing listener for a state\n * that is live for a few hundred milliseconds at most. */\n let phantomReleaseAttached = false\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Tab') lastTabKeyDownAt = Date.now()\n }\n const onPointerDown = () => {\n pointerGestureActive = true\n releasePhantomFocus()\n }\n const onPointerGestureEnd = () => {\n pointerGestureActive = false\n }\n const dropPhantom = () => {\n phantomFocusTarget = undefined\n if (phantomReleaseAttached) {\n phantomReleaseAttached = false\n doc.removeEventListener('pointermove', onPointerMove, true)\n }\n }\n const onFocus = (event: FocusEvent) => {\n // Focus that moves on retires the phantom: there is nothing left to\n // release, so the parked pointer listener goes with it.\n if (phantomFocusTarget && event.target !== phantomFocusTarget) dropPhantom()\n if (pointerGestureActive) return\n if (!isButtonLikeFocusTarget(event.target)) return\n const keyboardIntentActive = Date.now() - lastTabKeyDownAt <= TOOLTIP_FOCUS_INTENT_WINDOW_MS\n if (!shouldSuppressTooltipFocus({ isButtonLike: true, keyboardIntentActive })) return\n event.stopPropagation()\n phantomFocusTarget = event.target as Element\n if (!phantomReleaseAttached) {\n phantomReleaseAttached = true\n doc.addEventListener('pointermove', onPointerMove, true)\n }\n }\n const releasePhantomFocus = () => {\n if (pointerGestureActive) return\n if (!phantomFocusTarget) return\n const phantom = phantomFocusTarget\n dropPhantom()\n if ((doc as Document).activeElement !== phantom) return\n if (typeof (phantom as HTMLElement).blur === 'function') (phantom as HTMLElement).blur()\n }\n const onPointerMove = () => releasePhantomFocus()\n\n doc.addEventListener('keydown', onKeyDown, true)\n doc.addEventListener('focus', onFocus, true)\n doc.addEventListener('pointerdown', onPointerDown, true)\n doc.addEventListener('pointerup', onPointerGestureEnd, true)\n doc.addEventListener('pointercancel', onPointerGestureEnd, true)\n installedDocuments.add(doc)\n return () => {\n doc.removeEventListener('keydown', onKeyDown, true)\n doc.removeEventListener('focus', onFocus, true)\n doc.removeEventListener('pointerdown', onPointerDown, true)\n doc.removeEventListener('pointerup', onPointerGestureEnd, true)\n doc.removeEventListener('pointercancel', onPointerGestureEnd, true)\n doc.removeEventListener('pointermove', onPointerMove, true)\n installedDocuments.delete(doc)\n }\n}\n\n/**\n * Reference-counted install for the tooltip root: the first tooltip in a\n * document acquires the gate, the last one to unmount releases it. The document\n * parameter exists so tests can drive the counter without a global DOM; the\n * production call site guards on the server render before calling.\n */\nconst gateHolders = new WeakMap<Document, number>()\nconst gateDisposers = new WeakMap<Document, () => void>()\n\nexport function acquireTooltipFocusGate(targetDoc?: Document): void {\n if (typeof document === 'undefined' && !targetDoc) return\n const doc = targetDoc ?? document\n const holders = gateHolders.get(doc) ?? 0\n gateHolders.set(doc, holders + 1)\n if (holders === 0) gateDisposers.set(doc, installTooltipFocusGate(doc))\n}\n\nexport function releaseTooltipFocusGate(targetDoc?: Document): void {\n if (typeof document === 'undefined' && !targetDoc) return\n const doc = targetDoc ?? document\n const holders = gateHolders.get(doc) ?? 0\n if (holders === 0) return\n if (holders === 1) {\n gateHolders.delete(doc)\n gateDisposers.get(doc)?.()\n gateDisposers.delete(doc)\n return\n }\n gateHolders.set(doc, holders - 1)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tooltip-focus-gate.js","names":[],"sources":["../../src/lib/tooltip-focus-gate.ts"],"sourcesContent":["/*\n * Tooltips open on active hover over the control, or on keyboard focus that\n * follows a Tab key press. They must never open from programmatic focus: the\n * autofocus a dialog or sheet gives its first control, and the focus a closing\n * overlay restores to its opener, both land on buttons that carry tooltips —\n * and the tooltip primitive opens immediately on focus and pins the tooltip\n * until blur. That is the \"tooltip auto-displays when a sheet opens\" report,\n * and the pinned tooltip is the \"tooltip gets stuck and never disappears\"\n * report: the pointer is nowhere near the control, so no pointer leave ever\n * closes it.\n *\n * The trigger listens for the native `focus` event on the trigger element, and\n * composites build tooltips internally (ActionButton renders one from its\n * `tooltip` prop), so the single interception point that covers every tooltip\n * is a capture-phase `focus` listener on `document`. When focus lands without a\n * keyboard-intent signal on a button-like element, the event is stopped before\n * any element listener runs: the tooltip never learns about the focus. Hover\n * opens are untouched (they ride pointer events), and the separate `focusin`\n * event still flows, so dialog focus-cycle tracking, `:focus-visible` styling,\n * and focus restoration all keep working. Focus still lands — only the event\n * is stopped — so `document.activeElement` and scripted `.blur()` behave.\n *\n * The keyboard-intent signal is a Tab key press immediately before the focus —\n * the only key that moves focus natively. Enter and Space open overlays too,\n * so any \"recent key\" window wide enough for those would let an overlay's own\n * autofocus back through; Tab-only keeps autofocus suppressed no matter how\n * the overlay was opened, while normal Tab navigation still announces each\n * control's tooltip (hover descriptions stay reachable from the keyboard).\n *\n * Suppression is scoped to button-like targets (`button`, `a[href]`,\n * `[role=\"button\"]`, `[role=\"link\"]`). Composite widgets that move focus\n * programmatically — menu items, listbox options, grid cells — are other\n * element types whose roving-focus highlighting must keep receiving focus\n * events, and text controls keep their own focus behaviour (select on focus,\n * caret placement).\n *\n * Suppressed focus is real as far as the DOM is concerned (`activeElement`\n * moves; only the event was stopped), and the tooltip interaction model treats\n * a trigger that is `activeElement` as keyboard-held: while a tooltip on it is\n * open, pointer movement away only *requests* a close and the request is\n * refused for as long as the pointer keeps moving. Left alone, a later real\n * hover on that control would open the tip and never let it leave — the\n * residual \"stuck\" case. The gate therefore treats a suppressed focus as a\n * phantom and releases it: the first pointer activity after the focus (a\n * pointer session taking over from a script) blurs the element. Keyboard\n * sessions never release it and never need to — keyboard focus moves by Tab,\n * which is never suppressed, so nothing pins; and the release only runs for\n * elements still holding `activeElement`, so a Tab already taken from the\n * phantom is left alone.\n *\n * Focus that arrives while a pointer button is held belongs to a pointer\n * gesture, not to a tooltip: the pixel resize handle focuses its handle when a\n * drag starts and cancels the drag on blur, so a gesture focus must pass\n * through untouched (no suppression, no phantom, no release). A gesture's own\n * click behaviour already keeps tooltips closed, and the autofocus cases this\n * gate exists for all happen with the pointer up.\n *\n * Independently of the gesture window, resize grips render as\n * `<button role=\"separator\">` and carry no tooltip, so they are excluded from\n * the button-like scope entirely (see `isButtonLikeFocusTarget`).\n *\n * The gate is installed by the `Tooltip` root for the lifetime of the first\n * tooltip in a document and removed when the last one unmounts, so every\n * consumer of the primitive — including composites that build their own\n * tooltips — inherits the contract without wiring of its own.\n */\n\n/** How long after a Tab key press a resulting focus event still counts as\n * keyboard navigation. The native focus move follows the key press\n * immediately; the window only has to bridge that dispatch, not a later\n * script-driven focus. */\nexport const TOOLTIP_FOCUS_INTENT_WINDOW_MS = 250\n\n/** Elements a tooltip-bearing control is rendered as. Everything else keeps\n * its native focus behaviour, and so do resize grips (see below). */\nconst BUTTON_LIKE_SELECTOR = 'button, a[href], [role=\"button\"], [role=\"link\"]'\n\n/**\n * Whether a focus event landing on this target could open a tooltip on a\n * button-like control. Accepts any Element (an SVG glyph inside a control,\n * for instance) and duck-types so the decision is testable without a DOM.\n */\nexport function isButtonLikeFocusTarget(target: unknown): boolean {\n const element = target as Element | null | undefined\n if (typeof element?.closest !== 'function') return false\n const control = element.closest(BUTTON_LIKE_SELECTOR)\n if (control === null) return false\n // Resize grips render as `<button role=\"separator\">` and focus themselves\n // when a pointer drag starts. They carry no tooltip, and the blur that\n // releases a suppressed focus would end the drag on its first move.\n return control.getAttribute?.('role') !== 'separator'\n}\n\n/**\n * The suppression decision: a button-like focus target hides a tooltip only\n * when no keyboard navigation is in flight. Anything else — a Tab-driven\n * focus, or focus on a non-button element — passes through untouched.\n */\nexport function shouldSuppressTooltipFocus(input: {\n isButtonLike: boolean\n keyboardIntentActive: boolean\n}): boolean {\n return input.isButtonLike && !input.keyboardIntentActive\n}\n\nconst installedDocuments = new WeakSet<Document>()\n\n/**\n * Install the gate on a document. Idempotent per document; returns a disposer\n * that removes the listeners (used by tests and by the tooltip root, which\n * releases the gate when its document's last tooltip unmounts).\n */\nexport function installTooltipFocusGate(doc: Document = document): () => void {\n if (installedDocuments.has(doc)) return () => {}\n\n let lastTabKeyDownAt = 0\n /** True between a pointerdown and its pointerup/pointercancel: focus events\n * inside that window belong to a pointer gesture (drag bootstrap, press)\n * and must reach their element untouched. */\n let pointerGestureActive = false\n /** The element holding a suppressed programmatic focus, if it still holds\n * `activeElement`. One slot: a newer suppressed focus replaces an older\n * one, and anything that already lost focus needs no release. */\n let phantomFocusTarget: Element | undefined\n /** The phantom-release `pointermove` listener exists only while a phantom\n * does: a gate that parks a permanent pointer listener on every document\n * with a tooltip in it costs every page a standing listener for a state\n * that is live for a few hundred milliseconds at most. */\n let phantomReleaseAttached = false\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Tab') lastTabKeyDownAt = Date.now()\n }\n const onPointerDown = () => {\n pointerGestureActive = true\n releasePhantomFocus()\n }\n const onPointerGestureEnd = () => {\n pointerGestureActive = false\n }\n const dropPhantom = () => {\n phantomFocusTarget = undefined\n if (phantomReleaseAttached) {\n phantomReleaseAttached = false\n doc.removeEventListener('pointermove', onPointerMove, true)\n }\n }\n const onFocus = (event: FocusEvent) => {\n // Focus that moves on retires the phantom: there is nothing left to\n // release, so the parked pointer listener goes with it.\n if (phantomFocusTarget && event.target !== phantomFocusTarget) dropPhantom()\n if (pointerGestureActive) return\n if (!isButtonLikeFocusTarget(event.target)) return\n const keyboardIntentActive = Date.now() - lastTabKeyDownAt <= TOOLTIP_FOCUS_INTENT_WINDOW_MS\n if (!shouldSuppressTooltipFocus({ isButtonLike: true, keyboardIntentActive })) return\n event.stopPropagation()\n phantomFocusTarget = event.target as Element\n if (!phantomReleaseAttached) {\n phantomReleaseAttached = true\n doc.addEventListener('pointermove', onPointerMove, true)\n }\n }\n const releasePhantomFocus = () => {\n if (pointerGestureActive) return\n if (!phantomFocusTarget) return\n const phantom = phantomFocusTarget\n dropPhantom()\n if ((doc as Document).activeElement !== phantom) return\n if (typeof (phantom as HTMLElement).blur === 'function') (phantom as HTMLElement).blur()\n }\n const onPointerMove = () => releasePhantomFocus()\n\n doc.addEventListener('keydown', onKeyDown, true)\n doc.addEventListener('focus', onFocus, true)\n doc.addEventListener('pointerdown', onPointerDown, true)\n doc.addEventListener('pointerup', onPointerGestureEnd, true)\n doc.addEventListener('pointercancel', onPointerGestureEnd, true)\n installedDocuments.add(doc)\n return () => {\n doc.removeEventListener('keydown', onKeyDown, true)\n doc.removeEventListener('focus', onFocus, true)\n doc.removeEventListener('pointerdown', onPointerDown, true)\n doc.removeEventListener('pointerup', onPointerGestureEnd, true)\n doc.removeEventListener('pointercancel', onPointerGestureEnd, true)\n doc.removeEventListener('pointermove', onPointerMove, true)\n installedDocuments.delete(doc)\n }\n}\n\n/**\n * Reference-counted install for the tooltip root: the first tooltip in a\n * document acquires the gate, the last one to unmount releases it. The document\n * parameter exists so tests can drive the counter without a global DOM; the\n * production call site guards on the server render before calling.\n */\nconst gateHolders = new WeakMap<Document, number>()\nconst gateDisposers = new WeakMap<Document, () => void>()\n\nexport function acquireTooltipFocusGate(targetDoc?: Document): void {\n if (typeof document === 'undefined' && !targetDoc) return\n const doc = targetDoc ?? document\n const holders = gateHolders.get(doc) ?? 0\n gateHolders.set(doc, holders + 1)\n if (holders === 0) gateDisposers.set(doc, installTooltipFocusGate(doc))\n}\n\nexport function releaseTooltipFocusGate(targetDoc?: Document): void {\n if (typeof document === 'undefined' && !targetDoc) return\n const doc = targetDoc ?? document\n const holders = gateHolders.get(doc) ?? 0\n if (holders === 0) return\n if (holders === 1) {\n gateHolders.delete(doc)\n gateDisposers.get(doc)?.()\n gateDisposers.delete(doc)\n return\n }\n gateHolders.set(doc, holders - 1)\n}\n"],"mappings":"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"}
|
package/dist/lib/variants.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cva
|
|
1
|
+
import { cva } from "class-variance-authority";
|
|
2
2
|
//#region src/lib/variants.ts
|
|
3
3
|
/**
|
|
4
4
|
* The shared control-size ladder, in Tailwind height utilities.
|
|
@@ -48,6 +48,6 @@ var controlInteractive = "transition-colors ease-out outline-none focus-visible:
|
|
|
48
48
|
/** The shared focus treatment for surfaces that are not focusable controls. */
|
|
49
49
|
var surfaceInteractive = "transition-colors ease-out outline-none";
|
|
50
50
|
//#endregion
|
|
51
|
-
export { controlInteractive, controlSize, controlSizeIcon, controlSizes, cva
|
|
51
|
+
export { controlInteractive, controlSize, controlSizeIcon, controlSizes, cva, surfaceInteractive };
|
|
52
52
|
|
|
53
53
|
//# sourceMappingURL=variants.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adea-ai/ui",
|
|
3
|
-
"version": "0.128.
|
|
3
|
+
"version": "0.128.2",
|
|
4
4
|
"description": "The Adea design system: Kobalte-backed Solid components, semantic OKLCH tokens, and the app-shell layout primitives shared by Adea and Cortana.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"adea",
|
|
@@ -109,7 +109,8 @@
|
|
|
109
109
|
"cn:check": "bun scripts/check-cn-tables.ts",
|
|
110
110
|
"registry:build": "bun run cn:build && bun scripts/build-registry.ts",
|
|
111
111
|
"registry:validate": "bun run cn:check && bun scripts/validate-registry.ts",
|
|
112
|
-
"pack:check": "bun scripts/check-pack.ts",
|
|
112
|
+
"pack:check": "bun scripts/check-pack.ts && bun scripts/check-lib-exports.ts",
|
|
113
|
+
"check:lib-exports": "bun scripts/check-lib-exports.ts",
|
|
113
114
|
"check:tree-shaking": "bun scripts/check-tree-shaking.ts",
|
|
114
115
|
"theme:build": "bun scripts/build-default-theme.ts",
|
|
115
116
|
"theme:check": "bun scripts/build-default-theme.ts --check",
|