@labpics/colors 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,255 @@
1
+ // Effective background resolution — zero dependencies.
2
+ //
3
+ // `labcolors` resolves roles against a *solid* background. A real UI surface is
4
+ // often translucent (a panel at `rgba(…, .8)` over its parents) or has no
5
+ // background of its own (inheriting whatever is behind it). To resolve such a
6
+ // surface honestly you need the **effective** background: the opaque colour a
7
+ // viewer actually sees behind the element's own content.
8
+ //
9
+ // This module computes it by walking the ancestor chain and **alpha-compositing**
10
+ // each element's `background-color` (front-to-back) until the stack is opaque,
11
+ // over an opaque fallback (white by default). It is the bridge from the DOM's
12
+ // layered, translucent reality to the single solid hex the WASM core consumes.
13
+ //
14
+ // HONEST LIMIT: this composites solid/translucent `background-color` layers only.
15
+ // It does NOT sample `background-image`s, gradients, blurred backdrops, video, or
16
+ // content showing through — those have no single colour to read from computed
17
+ // style. For those, the caller supplies the effective background explicitly (the
18
+ // `background` option of `watchTheme`, or a sampled average). What it does cover —
19
+ // translucent panels over solid parents — is the common case and is composited
20
+ // *correctly* (true source-over alpha), not approximated.
21
+
22
+ /** @typedef {[number, number, number, number]} Rgba r,g,b in 0..255, a in 0..1 */
23
+
24
+ /**
25
+ * Parse a CSS colour string into `[r, g, b, a]`, or `null` if unrecognised.
26
+ *
27
+ * Handles the forms computed style actually yields (`rgb(r, g, b)`,
28
+ * `rgba(r, g, b, a)`, the modern `rgb(r g b / a)`) plus `#rgb`/`#rrggbb` and the
29
+ * `transparent` keyword. Unknown keywords return `null` (treated as "no layer").
30
+ *
31
+ * @param {string} css
32
+ * @returns {Rgba | null}
33
+ */
34
+ export function parseCssColor(css) {
35
+ if (typeof css !== "string") return null;
36
+ const s = css.trim().toLowerCase();
37
+ if (s === "transparent") return [0, 0, 0, 0];
38
+
39
+ if (s[0] === "#") {
40
+ const h = s.slice(1);
41
+ if (h.length === 3 || h.length === 4) {
42
+ const r = parseInt(h[0] + h[0], 16);
43
+ const g = parseInt(h[1] + h[1], 16);
44
+ const b = parseInt(h[2] + h[2], 16);
45
+ const a = h.length === 4 ? parseInt(h[3] + h[3], 16) / 255 : 1;
46
+ return [r, g, b, a].some(Number.isNaN) ? null : [r, g, b, a];
47
+ }
48
+ if (h.length === 6 || h.length === 8) {
49
+ const r = parseInt(h.slice(0, 2), 16);
50
+ const g = parseInt(h.slice(2, 4), 16);
51
+ const b = parseInt(h.slice(4, 6), 16);
52
+ const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1;
53
+ return [r, g, b, a].some(Number.isNaN) ? null : [r, g, b, a];
54
+ }
55
+ return null;
56
+ }
57
+
58
+ const m = s.match(/^rgba?\(([^)]+)\)$/);
59
+ if (!m) return null;
60
+ // Split on commas or whitespace and an optional "/" alpha separator.
61
+ const parts = m[1].split(/[,\s/]+/).filter((p) => p.length > 0);
62
+ if (parts.length < 3) return null;
63
+ const chan = (p) => (p.endsWith("%") ? (parseFloat(p) / 100) * 255 : parseFloat(p));
64
+ const r = chan(parts[0]);
65
+ const g = chan(parts[1]);
66
+ const b = chan(parts[2]);
67
+ const a = parts.length >= 4 ? (parts[3].endsWith("%") ? parseFloat(parts[3]) / 100 : parseFloat(parts[3])) : 1;
68
+ if ([r, g, b, a].some((v) => Number.isNaN(v))) return null;
69
+ return [clamp255(r), clamp255(g), clamp255(b), Math.min(1, Math.max(0, a))];
70
+ }
71
+
72
+ function clamp255(v) {
73
+ return Math.min(255, Math.max(0, v));
74
+ }
75
+
76
+ /**
77
+ * Source-over composite of `top` onto `bottom` (Porter-Duff "over").
78
+ *
79
+ * @param {Rgba} top
80
+ * @param {Rgba} bottom
81
+ * @returns {Rgba}
82
+ */
83
+ export function compositeOver(top, bottom) {
84
+ const at = top[3];
85
+ const ab = bottom[3];
86
+ const a = at + ab * (1 - at);
87
+ if (a === 0) return [0, 0, 0, 0];
88
+ const c = (i) => (top[i] * at + bottom[i] * ab * (1 - at)) / a;
89
+ return [c(0), c(1), c(2), a];
90
+ }
91
+
92
+ /**
93
+ * `[r, g, b]` (0..255) → `#RRGGBB`, channels rounded and clamped.
94
+ *
95
+ * @param {Rgba | [number, number, number]} rgb
96
+ * @returns {string}
97
+ */
98
+ export function toHex(rgb) {
99
+ // Coerce non-finite channels (NaN/±Infinity, e.g. from a malformed `Rgba`
100
+ // passed by a caller) to 0, so a bad input yields a valid `#RRGGBB` rather
101
+ // than an invalid CSS string like `"#NAN0000"`.
102
+ const h = (v) => {
103
+ const n = Math.round(clamp255(Number.isFinite(v) ? v : 0));
104
+ return n.toString(16).padStart(2, "0");
105
+ };
106
+ return `#${h(rgb[0])}${h(rgb[1])}${h(rgb[2])}`.toUpperCase();
107
+ }
108
+
109
+ // --- Perceptual interpolation (Oklab) -------------------------------------
110
+ //
111
+ // A crossfade should feel even: equal progress should be equal *perceived*
112
+ // change. A straight sRGB-channel blend is not that — it lingers in the brighter
113
+ // half (black→white at t=0.5 is #808080, but the perceived midpoint grey is
114
+ // ~#606060). Interpolating in Oklab — a perceptually-uniform space — fixes the
115
+ // timing and, for chromatic endpoints, avoids the muddy desaturated midpoint a
116
+ // raw RGB blend produces. Björn Ottosson's sRGB↔Oklab transform, self-contained.
117
+
118
+ /** sRGB gamma transfer (IEC 61966-2-1): encoded channel 0..1 → linear 0..1. */
119
+ function srgbToLinear(c) {
120
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
121
+ }
122
+
123
+ /** Inverse sRGB transfer: linear 0..1 → encoded 0..1. */
124
+ function linearToSrgb(c) {
125
+ return c <= 0.0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055;
126
+ }
127
+
128
+ /** Linear-light sRGB `[r,g,b]` (0..1) → Oklab `[L, a, b]`. */
129
+ function linearRgbToOklab(r, g, b) {
130
+ const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
131
+ const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
132
+ const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
133
+ const l_ = Math.cbrt(l);
134
+ const m_ = Math.cbrt(m);
135
+ const s_ = Math.cbrt(s);
136
+ return [
137
+ 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
138
+ 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
139
+ 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_,
140
+ ];
141
+ }
142
+
143
+ /** Oklab `[L, a, b]` → linear-light sRGB `[r,g,b]` (0..1, may be out of gamut). */
144
+ function oklabToLinearRgb(L, A, B) {
145
+ const l_ = L + 0.3963377774 * A + 0.2158037573 * B;
146
+ const m_ = L - 0.1055613458 * A - 0.0638541728 * B;
147
+ const s_ = L - 0.0894841775 * A - 1.291485548 * B;
148
+ const l = l_ * l_ * l_;
149
+ const m = m_ * m_ * m_;
150
+ const s = s_ * s_ * s_;
151
+ return [
152
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
153
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
154
+ -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
155
+ ];
156
+ }
157
+
158
+ /**
159
+ * Interpolate two `#RRGGBB` colours in Oklab at `t ∈ [0,1]`, returning `#RRGGBB`.
160
+ *
161
+ * Perceptually uniform: equal steps in `t` are equal steps in perceived
162
+ * lightness (and a straight, non-muddy path in hue/chroma), so a crossfade feels
163
+ * even rather than lingering bright. Endpoints are returned exactly (`t ≤ 0` →
164
+ * `from`, `t ≥ 1` → `to`, both re-normalised through `toHex`); out-of-gamut
165
+ * intermediates are clamped per channel. Unparseable input falls back to the
166
+ * nearer endpoint.
167
+ *
168
+ * @param {string} fromHex
169
+ * @param {string} toHex_
170
+ * @param {number} t
171
+ * @returns {string}
172
+ */
173
+ export function oklabLerp(fromHex, toHex_, t) {
174
+ const a = parseCssColor(fromHex);
175
+ const b = parseCssColor(toHex_);
176
+ if (!a || !b) return (b && t >= 0.5) || !a ? (b ? toHex(b) : "#000000") : toHex(a);
177
+ if (t <= 0) return toHex(a);
178
+ if (t >= 1) return toHex(b);
179
+ const la = linearRgbToOklab(srgbToLinear(a[0] / 255), srgbToLinear(a[1] / 255), srgbToLinear(a[2] / 255));
180
+ const lb = linearRgbToOklab(srgbToLinear(b[0] / 255), srgbToLinear(b[1] / 255), srgbToLinear(b[2] / 255));
181
+ const lin = oklabToLinearRgb(
182
+ la[0] + (lb[0] - la[0]) * t,
183
+ la[1] + (lb[1] - la[1]) * t,
184
+ la[2] + (lb[2] - la[2]) * t,
185
+ );
186
+ return toHex([linearToSrgb(lin[0]) * 255, linearToSrgb(lin[1]) * 255, linearToSrgb(lin[2]) * 255]);
187
+ }
188
+
189
+ /**
190
+ * Compose an ordered stack of colour layers (front-to-back) over an opaque base
191
+ * into a single opaque `#RRGGBB`. Pure — no DOM. Exposed for testing and for
192
+ * callers that sample their own layers.
193
+ *
194
+ * @param {Rgba[]} layersFrontToBack index 0 is the topmost layer
195
+ * @param {Rgba} opaqueBase must have alpha 1
196
+ * @returns {string}
197
+ */
198
+ export function compositeStackToHex(layersFrontToBack, opaqueBase) {
199
+ let result = opaqueBase;
200
+ // Apply from the back (closest to base) forward, so index 0 lands on top.
201
+ for (let i = layersFrontToBack.length - 1; i >= 0; i--) {
202
+ result = compositeOver(layersFrontToBack[i], result);
203
+ }
204
+ return toHex(result);
205
+ }
206
+
207
+ /**
208
+ * The opaque effective background `#RRGGBB` visible behind `element`'s own
209
+ * content, by walking ancestors and compositing their `background-color`s.
210
+ *
211
+ * Walks from `element` upward, collecting each `background-color` layer, and
212
+ * stops at the first fully-opaque layer (which becomes the base). If the chain
213
+ * reaches the root without an opaque layer, `fallback` (default white) is the
214
+ * base — matching how a browser shows the page's default canvas.
215
+ *
216
+ * Pure and injectable: pass `getStyle` and `parentOf` to test without a DOM; in
217
+ * the browser they default to `getComputedStyle` and `el.parentElement`.
218
+ *
219
+ * @param {*} element
220
+ * @param {object} [opts]
221
+ * @param {string} [opts.fallback="#FFFFFF"] base when the chain is fully translucent
222
+ * @param {(el: *) => { getPropertyValue: (p: string) => string }} [opts.getStyle]
223
+ * @param {(el: *) => *} [opts.parentOf]
224
+ * @param {number} [opts.maxDepth=64] guard against detached/cyclic chains
225
+ * @returns {string}
226
+ */
227
+ export function effectiveBackground(element, opts = {}) {
228
+ const fallback = opts.fallback ?? "#FFFFFF";
229
+ const getStyle =
230
+ opts.getStyle ?? ((el) => (typeof getComputedStyle === "function" ? getComputedStyle(el) : { getPropertyValue: () => "" }));
231
+ const parentOf = opts.parentOf ?? ((el) => el.parentElement);
232
+ const maxDepth = opts.maxDepth ?? 64;
233
+
234
+ /** @type {Rgba[]} */
235
+ const layers = [];
236
+ let el = element;
237
+ let depth = 0;
238
+ let base = parseCssColor(fallback) ?? [255, 255, 255, 1];
239
+
240
+ while (el && depth < maxDepth) {
241
+ const css = getStyle(el).getPropertyValue("background-color");
242
+ const c = parseCssColor(css);
243
+ if (c && c[3] > 0) {
244
+ if (c[3] >= 1) {
245
+ base = c; // first opaque layer is the base; nothing behind it shows
246
+ break;
247
+ }
248
+ layers.push(c);
249
+ }
250
+ el = parentOf(el);
251
+ depth++;
252
+ }
253
+
254
+ return compositeStackToHex(layers, base);
255
+ }
package/index.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ // Public types for @labpics/colors.
2
+ //
3
+ // Re-exports the wasm-bindgen-generated types (the rich `ResolvedTheme` /
4
+ // `RoleResult` union and the `LabColors` engine) and the vanilla `applyTheme`
5
+ // helper, so a consumer gets full typing from the package root.
6
+
7
+ export {
8
+ default,
9
+ default as init,
10
+ initSync,
11
+ LabColors,
12
+ } from "./pkg/labcolors.js";
13
+
14
+ export type {
15
+ ThemeName,
16
+ SolvedColor,
17
+ NoneRole,
18
+ UnreachableRole,
19
+ RoleResult,
20
+ ResolvedTheme,
21
+ } from "./pkg/labcolors.js";
22
+
23
+ export { applyTheme } from "./apply-theme.js";
24
+ export { watchTheme } from "./watch-theme.js";
25
+ export type { WatchThemeOptions, WatchController } from "./watch-theme.js";
26
+ export { adaptTheme } from "./adapt-theme.js";
27
+ export type { AdaptThemeOptions, AdaptController } from "./adapt-theme.js";
28
+ export {
29
+ effectiveBackground,
30
+ parseCssColor,
31
+ compositeOver,
32
+ compositeStackToHex,
33
+ toHex,
34
+ oklabLerp,
35
+ } from "./effective-bg.js";
36
+ export type { Rgba, EffectiveBackgroundOptions, StyleLike } from "./effective-bg.js";
package/index.js ADDED
@@ -0,0 +1,21 @@
1
+ // Public entry for @labpics/colors.
2
+ //
3
+ // Re-exports the wasm-bindgen surface (the default `init` loader, `initSync`,
4
+ // and the `LabColors` engine class) plus the vanilla DOM runtime helpers:
5
+ // `applyTheme` (one-shot apply), `watchTheme` (reactive sync), and the
6
+ // effective-background resolver. The wasm glue is the generated `pkg/` artifact
7
+ // (built by `npm run build`).
8
+
9
+ export { default, default as init, initSync, LabColors } from "./pkg/labcolors.js";
10
+
11
+ export { applyTheme } from "./apply-theme.js";
12
+ export { watchTheme } from "./watch-theme.js";
13
+ export { adaptTheme } from "./adapt-theme.js";
14
+ export {
15
+ effectiveBackground,
16
+ parseCssColor,
17
+ compositeOver,
18
+ compositeStackToHex,
19
+ toHex,
20
+ oklabLerp,
21
+ } from "./effective-bg.js";
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@labpics/colors",
3
+ "version": "0.1.0",
4
+ "description": "Framework-agnostic contrast engine: resolve accessible, perceptually-anchored colour roles for any background. WASM core, zero runtime dependencies.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "provenance": true
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/Labpics-Team/lab-colors",
14
+ "directory": "packages/colors"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./index.d.ts",
19
+ "default": "./index.js"
20
+ },
21
+ "./apply-theme": {
22
+ "types": "./apply-theme.d.ts",
23
+ "default": "./apply-theme.js"
24
+ },
25
+ "./watch-theme": {
26
+ "types": "./watch-theme.d.ts",
27
+ "default": "./watch-theme.js"
28
+ },
29
+ "./adapt-theme": {
30
+ "types": "./adapt-theme.d.ts",
31
+ "default": "./adapt-theme.js"
32
+ },
33
+ "./effective-bg": {
34
+ "types": "./effective-bg.d.ts",
35
+ "default": "./effective-bg.js"
36
+ }
37
+ },
38
+ "files": [
39
+ "index.js",
40
+ "index.d.ts",
41
+ "apply-theme.js",
42
+ "apply-theme.d.ts",
43
+ "watch-theme.js",
44
+ "watch-theme.d.ts",
45
+ "adapt-theme.js",
46
+ "adapt-theme.d.ts",
47
+ "effective-bg.js",
48
+ "effective-bg.d.ts",
49
+ "pkg/labcolors.js",
50
+ "pkg/labcolors.d.ts",
51
+ "pkg/labcolors_bg.wasm",
52
+ "pkg/labcolors_bg.wasm.d.ts"
53
+ ],
54
+ "scripts": {
55
+ "//build": "wasm-pack resolves --out-dir relative to the CRATE dir (crates/labcolors-wasm), not the cwd, so the ../../packages path is correct and unambiguous regardless of where npm runs it.",
56
+ "build": "wasm-pack build ../../crates/labcolors-wasm --release --target web --out-dir ../../packages/colors/pkg --out-name labcolors",
57
+ "typecheck": "tsc --noEmit -p tsconfig.json",
58
+ "test": "node --test"
59
+ },
60
+ "devDependencies": {
61
+ "typescript": "^5.5.0"
62
+ },
63
+ "types": "./index.d.ts",
64
+ "sideEffects": false
65
+ }
@@ -0,0 +1,239 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ /** The stable theme contract. `-ic` variants apply increased contrast; all four spellings are fully supported. */
5
+ export type ThemeName = "light" | "dark" | "light-ic" | "dark-ic";
6
+
7
+ /** A solved colour and the contrasts it actually achieves. */
8
+ export interface SolvedColor {
9
+ readonly kind: "color";
10
+ /** The CSS custom-property name for this role, e.g. "--lab-label-primary". */
11
+ readonly cssVar: string;
12
+ /** The resolved colour as #RRGGBB. */
13
+ readonly hex: string;
14
+ /** Signed perceptual contrast (Lc) against the background. */
15
+ readonly lc: number;
16
+ /** WCAG 2.1 ratio (1–21) against the background. */
17
+ readonly wcagRatio: number;
18
+ /** The legal floor squeezed this role onto the smallest step below its senior. */
19
+ readonly compressed: boolean;
20
+ /** The WCAG floor overrode the perceptual target. */
21
+ readonly floorOverride: boolean;
22
+ /**
23
+ * The minimum WCAG ratio this role is legally clamped to (4.5 for AA text,
24
+ * 3.0 for AA UI), or `null` for decorative / zero roles. A property of
25
+ * the role's contract, not of this solve: a runtime easing between themes
26
+ * uses it to hold the floor every frame of the transition.
27
+ */
28
+ readonly legalFloor: number | null;
29
+ }
30
+
31
+ /** The explicit zero token: no colour here, by design (not a failure). */
32
+ export interface NoneRole {
33
+ readonly kind: "none";
34
+ readonly cssVar: string;
35
+ }
36
+
37
+ /** No colour can satisfy this role on this background, with the reason. */
38
+ export interface UnreachableRole {
39
+ readonly kind: "unreachable";
40
+ readonly cssVar: string;
41
+ /** Stable machine code, e.g. "floor_unreachable". */
42
+ readonly code: string;
43
+ /** Human-readable explanation. */
44
+ readonly message: string;
45
+ }
46
+
47
+ /** A semi-transparent ladder / alpha-analog emission: the CSS carries rgba(), the browser composites it. */
48
+ export interface RgbaRole {
49
+ readonly kind: "rgba";
50
+ readonly cssVar: string;
51
+ /** The tint as #RRGGBB — the colour the rgba() carries. */
52
+ readonly tintHex: string;
53
+ /** The alpha of the emission, (0, 1]. */
54
+ readonly alpha: number;
55
+ /** The solid the tint composites to on the resolve background. */
56
+ readonly compositeHex: string;
57
+ /** Signed perceptual contrast (Lc) of the composite. */
58
+ readonly compositeLc: number;
59
+ /** WCAG 2.1 ratio of the composite. */
60
+ readonly compositeWcag: number;
61
+ /** Ready-to-serve CSS value: "rgb(R G B / A)". `vars` carries the same string. */
62
+ readonly css: string;
63
+ }
64
+
65
+ export type RoleResult = SolvedColor | RgbaRole | NoneRole | UnreachableRole;
66
+
67
+ /** Пер-темная четвёрка якорных hex (light / dark / light-ic / dark-ic). */
68
+ export interface ThemeAnchors {
69
+ readonly light: string;
70
+ readonly dark: string;
71
+ readonly light_ic: string;
72
+ readonly dark_ic: string;
73
+ }
74
+
75
+ /** Источник тинта лестницы/альфа-аналога. */
76
+ export type LadderSource =
77
+ | { kind: "brand" }
78
+ | { kind: "family"; key: string }
79
+ | { kind: "sentiment"; name: string }
80
+ | { kind: "neutral"; pick: "mid" | "edge" | "inverted" | "light" | "dark" };
81
+
82
+ /** Рецепт роли из физического меню движка. */
83
+ export type RoleRecipe =
84
+ | { kind: "text-anchor"; fraction: number; floor: "aa-text" | "aa-ui" | "none" }
85
+ | { kind: "dj-anchor"; light: number; dark: number }
86
+ | { kind: "decorative-lc"; magnitude: number }
87
+ | { kind: "ladder"; source: LadderSource; position: string }
88
+ | { kind: "alpha-analog"; of: LadderSource; alpha: number }
89
+ | { kind: "zero" };
90
+
91
+ /** Полный конфиг дизайн-системы клиента — вход loadConfig (JSON.stringify(config)). */
92
+ export interface ThemeConfig {
93
+ readonly brand: ThemeAnchors;
94
+ readonly neutral: {
95
+ readonly anchors: { light: string; mid: string; dark: string };
96
+ readonly tint: {
97
+ ratio: number;
98
+ target_mp: number;
99
+ hue_stiffness: number;
100
+ hue_override_deg?: number;
101
+ };
102
+ readonly edge?: ThemeAnchors;
103
+ readonly inverted?: ThemeAnchors;
104
+ };
105
+ readonly palette: ReadonlyArray<{ key: string; anchors: ThemeAnchors }>;
106
+ readonly sentiments: {
107
+ readonly categories: ReadonlyArray<{
108
+ name: string;
109
+ family: string;
110
+ hue_floor_deg?: number;
111
+ preferred_side?: -1 | 1;
112
+ }>;
113
+ readonly hardness: number;
114
+ readonly chroma_fraction: number;
115
+ };
116
+ readonly themes: ReadonlyArray<{ name: string; preset: "srgb" | "dim" | "srgb-ic" | "dim-ic" }>;
117
+ readonly roles: ReadonlyArray<{ name: string; recipe: RoleRecipe }>;
118
+ readonly aliases?: ReadonlyArray<{ alias: string; target: string }>;
119
+ }
120
+
121
+ /** The full result of resolving one background under one theme. */
122
+ export interface ResolvedTheme {
123
+ readonly theme: ThemeName;
124
+ readonly background: string;
125
+ /**
126
+ * Reachable roles only. Values are ready-to-serve CSS: "#RRGGBB" for solid
127
+ * roles, "rgb(R G B / A)" for semi-transparent ladder/alpha-analog roles —
128
+ * do not validate them as hex.
129
+ */
130
+ readonly vars: Record<string, string>;
131
+ /** Every role, keyed by its stable role key (without the --lab- prefix). */
132
+ readonly roles: Record<string, RoleResult>;
133
+ }
134
+
135
+
136
+
137
+ /**
138
+ * A configured contrast engine. Construct with [`LabColors::new`], then call
139
+ * [`resolve_theme`](LabColors::resolve_theme) many times; identical calls are
140
+ * served from the contract cache.
141
+ */
142
+ export class LabColors {
143
+ free(): void;
144
+ [Symbol.dispose](): void;
145
+ /**
146
+ * Calculate a relative confidence score for the [`muddiness`](Self::muddiness)
147
+ * call on an sRGB hex colour: `0` means the call is unreliable (near the
148
+ * decision boundary or the grey frontier), higher means more confident.
149
+ * The practical ceiling is an internal calibration detail, not a public
150
+ * contract — do not hardcode an upper bound against this value.
151
+ */
152
+ confidence(hex: string): number;
153
+ /**
154
+ * Загрузить конфиг дизайн-системы (JSON по типу `ThemeConfig` из `.d.ts`).
155
+ *
156
+ * Полный preflight движка: невалидный конфиг отклоняется структурной
157
+ * ошибкой `invalid_config: …` и НЕ меняет состояние. После успешной
158
+ * загрузки `resolveTheme` эмитит роли конфига (включая полупрозрачные
159
+ * `rgba`-роли лестницы). Возвращает отпечаток конфига — 16 hex-символов;
160
+ * разные конфиги дают разные отпечатки (и разные кэш-пространства).
161
+ */
162
+ loadConfig(json: string): string;
163
+ /**
164
+ * Calculate the muddiness score (0 to 1) of an sRGB hex colour.
165
+ */
166
+ muddiness(hex: string): number;
167
+ /**
168
+ * Create a zero-config engine on the default role table and the default
169
+ * per-theme viewing conditions.
170
+ *
171
+ * `v1` takes no config; the brand/anchor seam is left for a future
172
+ * version. Adding an optional config object later is additive — it does
173
+ * not change this signature.
174
+ */
175
+ constructor();
176
+ /**
177
+ * Recheck the contrasts `fgHexes` achieve against `bgHex` under `theme` —
178
+ * the cheap per-frame primitive a reactive runtime uses to decide whether
179
+ * already-resolved colours still pass against a changed background (re-solve
180
+ * only when they stably do not). No full solve: one perceptual-model forward
181
+ * for the background plus one per foreground.
182
+ *
183
+ * Returns a `Float64Array` of `[lc, wcagRatio]` pairs, interleaved and in the
184
+ * order of `fgHexes`: index `2*i` is foreground `i`'s signed `Lc`, `2*i+1`
185
+ * its WCAG ratio. Rejects (structured `{code, message}`) on an invalid hex or
186
+ * an unknown theme.
187
+ */
188
+ recheckContrast(bg_hex: string, fg_hexes: string[], theme: string): Float64Array;
189
+ /**
190
+ * Resolve every role for `bgHex` under `theme` (`"light" | "dark" |
191
+ * "light-ic" | "dark-ic"`).
192
+ *
193
+ * Returns a [`ResolvedTheme`] object. Per-role unreachability is part of a
194
+ * successful result (each role carries its own `kind`); only whole-call
195
+ * failures (invalid hex, unknown theme) reject — as a
196
+ * structured `{ code, message }` error, never an unwound panic.
197
+ */
198
+ resolveTheme(bg_hex: string, theme: string): ResolvedTheme;
199
+ }
200
+
201
+ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
202
+
203
+ export interface InitOutput {
204
+ readonly memory: WebAssembly.Memory;
205
+ readonly __wbg_labcolors_free: (a: number, b: number) => void;
206
+ readonly labcolors_confidence: (a: number, b: number, c: number, d: number) => void;
207
+ readonly labcolors_loadConfig: (a: number, b: number, c: number, d: number) => void;
208
+ readonly labcolors_muddiness: (a: number, b: number, c: number, d: number) => void;
209
+ readonly labcolors_new: () => number;
210
+ readonly labcolors_recheckContrast: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
211
+ readonly labcolors_resolveTheme: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
212
+ readonly __wbindgen_export: (a: number, b: number) => number;
213
+ readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
214
+ readonly __wbindgen_export3: (a: number) => void;
215
+ readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
216
+ readonly __wbindgen_export4: (a: number, b: number, c: number) => void;
217
+ }
218
+
219
+ export type SyncInitInput = BufferSource | WebAssembly.Module;
220
+
221
+ /**
222
+ * Instantiates the given `module`, which can either be bytes or
223
+ * a precompiled `WebAssembly.Module`.
224
+ *
225
+ * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
226
+ *
227
+ * @returns {InitOutput}
228
+ */
229
+ export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
230
+
231
+ /**
232
+ * If `module_or_path` is {RequestInfo} or {URL}, makes a request and
233
+ * for everything else, calls `WebAssembly.instantiate` directly.
234
+ *
235
+ * @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
236
+ *
237
+ * @returns {Promise<InitOutput>}
238
+ */
239
+ export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;