@birdycom/omniacss-migrate 0.11.0 → 0.11.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/LICENSE +21 -21
- package/README.md +129 -129
- package/bin/omniacss-migrate.mjs +8 -8
- package/package.json +61 -61
- package/scripts/build-color-map.mjs +31 -31
- package/scripts/extract-palette.mjs +13 -13
- package/scripts/tailwind-palette.json +290 -290
- package/src/bootstrap/compat.mjs +72 -72
- package/src/bootstrap/convert.mjs +102 -102
- package/src/bootstrap/migrate.mjs +259 -259
- package/src/bootstrap/rules.mjs +212 -212
- package/src/cli.mjs +139 -139
- package/src/codemod.mjs +82 -82
- package/src/core.mjs +67 -67
- package/src/data/colors.json +290 -290
- package/src/extract.mjs +91 -91
- package/src/index.mjs +12 -12
- package/src/native.mjs +72 -72
- package/src/postcss.mjs +308 -308
- package/src/preflight.mjs +52 -52
- package/src/project.mjs +194 -194
- package/src/resolve.mjs +220 -220
- package/src/scan.mjs +106 -106
- package/src/setup.mjs +205 -205
- package/src/utilities.mjs +713 -713
- package/src/values.mjs +184 -184
- package/src/variants.mjs +189 -189
- package/src/vite.mjs +81 -81
package/src/values.mjs
CHANGED
|
@@ -1,184 +1,184 @@
|
|
|
1
|
-
// File: packages/migrate/src/values.mjs
|
|
2
|
-
// Value helpers for the compat resolver: arbitrary values, spacing scale, colors (Omnia tokens only), theme lookup.
|
|
3
|
-
// Author: Serge Cools | Framework: OmniaCSS
|
|
4
|
-
|
|
5
|
-
import { createRequire } from "node:module";
|
|
6
|
-
import fs from "node:fs";
|
|
7
|
-
import path from "node:path";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
|
|
10
|
-
const require = createRequire(import.meta.url);
|
|
11
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
export const COLOR_MAP = JSON.parse(fs.readFileSync(path.join(here, "data/colors.json"), "utf8"));
|
|
13
|
-
|
|
14
|
-
const fmt = (n) => String(+n.toFixed(5));
|
|
15
|
-
export const isNum = (s) => /^-?(\d+\.?\d*|\.\d+)$/.test(s);
|
|
16
|
-
export const rem = (n) => (n === 0 ? "0px" : `${fmt(n * 0.25)}rem`);
|
|
17
|
-
|
|
18
|
-
export const TYPE_HINTS = "length|color|percentage|number|url|string|angle|family-name|image|position|size|line-width|absolute-size|relative-size|bg-size|integer|ratio|vector|shadow|any";
|
|
19
|
-
|
|
20
|
-
/** `[37px]`, `[length:var(--x)]`, `(--x)`, `(length:--x)` -> { type, value } or null. */
|
|
21
|
-
export function arb(v) {
|
|
22
|
-
if (typeof v !== "string") return null;
|
|
23
|
-
let m = v.match(/^\[(?:([a-z-]+):(?!:))?([\s\S]*)\]$/);
|
|
24
|
-
if (m && (!m[1] || new RegExp(`^(${TYPE_HINTS})$`).test(m[1]))) return { type: m[1] || null, value: normalizeArb(m[2]) };
|
|
25
|
-
if (m && m[1]) return { type: null, value: normalizeArb(m[1] + ":" + m[2]) };
|
|
26
|
-
m = v.match(/^\((?:([a-z-]+):)?(--[^)]+)\)$/);
|
|
27
|
-
if (m) return { type: m[1] || null, value: `var(${m[2]})` };
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Underscores are spaces inside arbitrary values (except url() and escaped `\_`). */
|
|
32
|
-
export function normalizeArb(s) {
|
|
33
|
-
if (/^url\(/.test(s)) return s;
|
|
34
|
-
return s.replace(/\\_/g, "\u0000").replace(/_/g, " ").replace(/\u0000/g, "_");
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function inferType(val) {
|
|
38
|
-
if (/^(#[0-9a-f]{3,8}$|rgba?\(|hsla?\(|oklch\(|oklab\(|lab\(|lch\(|color\(|color-mix\(|hwb\()/i.test(val)) return "color";
|
|
39
|
-
if (/^(url\(|(repeating-)?(linear|radial|conic)-gradient\()/.test(val)) return "image";
|
|
40
|
-
if (/^-?(\d+\.?\d*|\.\d+)(px|rem|em|%|vw|vh|vmin|vmax|svh|dvh|lvh|ch|ex|cm|mm|in|pt|pc|fr|cqw|cqh)$/.test(val) || /^(calc|clamp|min|max)\(/.test(val)) return "length";
|
|
41
|
-
if (isNum(val)) return "number";
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Negate a CSS length. */
|
|
46
|
-
export function negate(val) {
|
|
47
|
-
if (val === "0px" || val === "0") return val;
|
|
48
|
-
if (/^[\d.]/.test(val)) return "-" + val;
|
|
49
|
-
if (val.startsWith("-") && /^-[\d.]/.test(val)) return val.slice(1);
|
|
50
|
-
return `calc(${val} * -1)`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Spacing value (margin, padding, gap, inset, sizing …).
|
|
55
|
-
* @param {string} v token value ("4", "px", "1/2", "[37px]", "full", "auto" …)
|
|
56
|
-
* @param {object} ctx resolver context
|
|
57
|
-
* @param {{ neg?: boolean, keywords?: Record<string,string>, fractions?: boolean }} [o]
|
|
58
|
-
*/
|
|
59
|
-
export function spacing(v, ctx, o = {}) {
|
|
60
|
-
let out = null;
|
|
61
|
-
if (o.keywords && Object.hasOwn(o.keywords, v)) out = o.keywords[v];
|
|
62
|
-
else if (arb(v)) out = arb(v).value;
|
|
63
|
-
else if (ctx.hasTheme(`--spacing-${v}`)) out = `var(--spacing-${v})`;
|
|
64
|
-
else if (v === "px") out = "1px";
|
|
65
|
-
else if (isNum(v) && +v >= 0) out = rem(+v);
|
|
66
|
-
else if (o.fractions !== false && /^(\d+)\/(\d+)$/.test(v)) { const [a, b] = v.split("/").map(Number); out = b ? `${fmt((a / b) * 100)}%` : null; }
|
|
67
|
-
if (out == null) return null;
|
|
68
|
-
return o.neg ? negate(out) : out;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// --- colors -----------------------------------------------------------------------------------------------
|
|
72
|
-
|
|
73
|
-
let paletteCache;
|
|
74
|
-
/** Omnia palette (from @birdycom/omniacss dist/oc-colors.css) as OKLab points, for approximating arbitrary colors. */
|
|
75
|
-
function palette(ctx) {
|
|
76
|
-
if (paletteCache) return paletteCache;
|
|
77
|
-
const files = [ctx.tokensFile, path.resolve(here, "../../core/dist/oc-colors.css")].filter(Boolean);
|
|
78
|
-
try { files.unshift(require.resolve("@birdycom/omniacss/tokens")); } catch { /* monorepo fallback */ }
|
|
79
|
-
let css = "";
|
|
80
|
-
for (const f of files) { try { css = fs.readFileSync(f, "utf8"); break; } catch { /* next */ } }
|
|
81
|
-
const FAMILIES = new Set("red pink orange yellow violet magenta purple indigo lime green teal cyan turquoise blue shallowblue duskblue twilightblue stormblue frostblue mistblue vividblue skycyan wheat peru brown white snow azure beige ivory linen silver gray black".split(" "));
|
|
82
|
-
paletteCache = [];
|
|
83
|
-
for (const m of css.matchAll(/--color-([a-z]+)-(\d+): oklch\(([\d.]+)% ([\d.]+) (none|[\d.]+)(?:deg)?\)/g)) {
|
|
84
|
-
if (!FAMILIES.has(m[1]) || m[2] === "100") continue;
|
|
85
|
-
const L = +m[3] / 100, C = +m[4], H = m[5] === "none" ? 0 : (+m[5] * Math.PI) / 180;
|
|
86
|
-
paletteCache.push({ token: `--color-${m[1]}-${m[2]}`, lab: [L, C * Math.cos(H), C * Math.sin(H)] });
|
|
87
|
-
}
|
|
88
|
-
return paletteCache;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function rgbToOklab(r, g, b) {
|
|
92
|
-
const lin = (v) => { v /= 255; return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; };
|
|
93
|
-
[r, g, b] = [lin(r), lin(g), lin(b)];
|
|
94
|
-
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
95
|
-
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
96
|
-
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
97
|
-
return [0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s, 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s, 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s];
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Parse #hex / rgb() / hsl() into [r,g,b,alpha] or null. */
|
|
101
|
-
export function parseColor(str) {
|
|
102
|
-
str = str.trim();
|
|
103
|
-
let m = str.match(/^#([0-9a-f]{3,8})$/i);
|
|
104
|
-
if (m) {
|
|
105
|
-
let h = m[1];
|
|
106
|
-
if (h.length <= 4) h = [...h].map((c) => c + c).join("");
|
|
107
|
-
if (h.length !== 6 && h.length !== 8) return null;
|
|
108
|
-
return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16)).concat(h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1);
|
|
109
|
-
}
|
|
110
|
-
m = str.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/i);
|
|
111
|
-
if (m) return [+m[1], +m[2], +m[3], m[4] ? (m[4].endsWith("%") ? parseFloat(m[4]) / 100 : +m[4]) : 1];
|
|
112
|
-
m = str.match(/^hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/i);
|
|
113
|
-
if (m) {
|
|
114
|
-
const h = +m[1] / 360, s = +m[2] / 100, l = +m[3] / 100;
|
|
115
|
-
const a = s * Math.min(l, 1 - l);
|
|
116
|
-
const f = (n) => { const k = (n + h * 12) % 12; return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)); };
|
|
117
|
-
return [f(0) * 255, f(8) * 255, f(4) * 255, m[4] ? (m[4].endsWith("%") ? parseFloat(m[4]) / 100 : +m[4]) : 1];
|
|
118
|
-
}
|
|
119
|
-
m = str.match(/^oklch\(\s*([\d.]+)(%?)\s+([\d.]+)\s+(none|[\d.]+)(?:deg)?(?:\s*\/\s*([\d.]+%?))?\s*\)$/i);
|
|
120
|
-
if (m) {
|
|
121
|
-
const l = m[2] ? +m[1] / 100 : +m[1], C = +m[3], H = m[4] === "none" ? 0 : (+m[4] * Math.PI) / 180;
|
|
122
|
-
const a = C * Math.cos(H), b = C * Math.sin(H);
|
|
123
|
-
const l_ = (l + 0.3963377774 * a + 0.2158037573 * b) ** 3, m_ = (l - 0.1055613458 * a - 0.0638541728 * b) ** 3, s_ = (l - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
124
|
-
const lin = [4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_, -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_, -0.0041960863 * l_ - 0.7034186147 * m_ + 1.707614701 * s_];
|
|
125
|
-
const enc = (v) => { v = Math.min(1, Math.max(0, v)); return (v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055) * 255; };
|
|
126
|
-
return [...lin.map(enc), m[5] ? (m[5].endsWith("%") ? parseFloat(m[5]) / 100 : +m[5]) : 1];
|
|
127
|
-
}
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Nearest Omnia token (`--color-x-y`) for a color string; null if it cannot be parsed. */
|
|
132
|
-
export function nearestToken(str, ctx) {
|
|
133
|
-
const c = parseColor(str);
|
|
134
|
-
if (!c) return null;
|
|
135
|
-
const [L, A, B] = rgbToOklab(c[0], c[1], c[2]);
|
|
136
|
-
let best = null;
|
|
137
|
-
for (const p of palette(ctx)) {
|
|
138
|
-
const d = (L - p.lab[0]) ** 2 * 1.4 + (A - p.lab[1]) ** 2 + (B - p.lab[2]) ** 2;
|
|
139
|
-
if (!best || d < best.d) best = { d, token: p.token };
|
|
140
|
-
}
|
|
141
|
-
return best ? { token: best.token, alpha: c[3] } : null;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Wrap an Omnia-token color with an alpha (0..100 %). */
|
|
145
|
-
export const withAlpha = (color, pct) => `color-mix(in oklab, ${color} ${fmt(pct)}%, transparent)`;
|
|
146
|
-
|
|
147
|
-
/** Color for an arbitrary/literal value, mapped to the Omnia palette unless ctx.arbitraryColors === "keep". */
|
|
148
|
-
function literalColor(val, ctx) {
|
|
149
|
-
if (ctx.arbitraryColors === "keep") return val;
|
|
150
|
-
const n = nearestToken(val, ctx);
|
|
151
|
-
if (!n) return val;
|
|
152
|
-
const base = `var(${n.token})`;
|
|
153
|
-
return n.alpha < 1 ? withAlpha(base, n.alpha * 100) : base;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Resolve a color name/value ("red-500", "white", "primary", "[#fff]", "(--x)") with an optional opacity modifier.
|
|
158
|
-
* @returns {string|null}
|
|
159
|
-
*/
|
|
160
|
-
export function color(v, mod, ctx, { hinted = false } = {}) {
|
|
161
|
-
let out = null;
|
|
162
|
-
const a = arb(v);
|
|
163
|
-
if (a) {
|
|
164
|
-
if (a.type && a.type !== "color") return null;
|
|
165
|
-
if (!a.type && !hinted && inferType(a.value) !== "color" && !/^var\(/.test(a.value)) return null;
|
|
166
|
-
out = /^var\(/.test(a.value) ? a.value : literalColor(a.value, ctx);
|
|
167
|
-
} else if (v === "transparent" || v === "current" || v === "inherit") out = v === "current" ? "currentColor" : v;
|
|
168
|
-
else if (ctx.hasTheme(`--color-${v}`)) out = `var(--color-${v})`;
|
|
169
|
-
else if (ctx.customColors?.has(v)) out = ctx.customColors.get(v);
|
|
170
|
-
else if (COLOR_MAP[v]) out = `var(${COLOR_MAP[v]})`;
|
|
171
|
-
if (out == null) return null;
|
|
172
|
-
if (mod != null && mod !== "") {
|
|
173
|
-
const ma = arb(mod);
|
|
174
|
-
let pct = ma ? (+ma.value <= 1 ? +ma.value * 100 : +ma.value) : +mod;
|
|
175
|
-
if (!Number.isFinite(pct)) return out;
|
|
176
|
-
if (out === "transparent") return out;
|
|
177
|
-
out = withAlpha(out, pct);
|
|
178
|
-
}
|
|
179
|
-
return out;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** Shared by utilities: 0-100 style number -> fraction (opacity-50 -> 0.5). */
|
|
183
|
-
export const frac = (n) => fmt(+n / 100);
|
|
184
|
-
export { fmt };
|
|
1
|
+
// File: packages/migrate/src/values.mjs
|
|
2
|
+
// Value helpers for the compat resolver: arbitrary values, spacing scale, colors (Omnia tokens only), theme lookup.
|
|
3
|
+
// Author: Serge Cools | Framework: OmniaCSS
|
|
4
|
+
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
export const COLOR_MAP = JSON.parse(fs.readFileSync(path.join(here, "data/colors.json"), "utf8"));
|
|
13
|
+
|
|
14
|
+
const fmt = (n) => String(+n.toFixed(5));
|
|
15
|
+
export const isNum = (s) => /^-?(\d+\.?\d*|\.\d+)$/.test(s);
|
|
16
|
+
export const rem = (n) => (n === 0 ? "0px" : `${fmt(n * 0.25)}rem`);
|
|
17
|
+
|
|
18
|
+
export const TYPE_HINTS = "length|color|percentage|number|url|string|angle|family-name|image|position|size|line-width|absolute-size|relative-size|bg-size|integer|ratio|vector|shadow|any";
|
|
19
|
+
|
|
20
|
+
/** `[37px]`, `[length:var(--x)]`, `(--x)`, `(length:--x)` -> { type, value } or null. */
|
|
21
|
+
export function arb(v) {
|
|
22
|
+
if (typeof v !== "string") return null;
|
|
23
|
+
let m = v.match(/^\[(?:([a-z-]+):(?!:))?([\s\S]*)\]$/);
|
|
24
|
+
if (m && (!m[1] || new RegExp(`^(${TYPE_HINTS})$`).test(m[1]))) return { type: m[1] || null, value: normalizeArb(m[2]) };
|
|
25
|
+
if (m && m[1]) return { type: null, value: normalizeArb(m[1] + ":" + m[2]) };
|
|
26
|
+
m = v.match(/^\((?:([a-z-]+):)?(--[^)]+)\)$/);
|
|
27
|
+
if (m) return { type: m[1] || null, value: `var(${m[2]})` };
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Underscores are spaces inside arbitrary values (except url() and escaped `\_`). */
|
|
32
|
+
export function normalizeArb(s) {
|
|
33
|
+
if (/^url\(/.test(s)) return s;
|
|
34
|
+
return s.replace(/\\_/g, "\u0000").replace(/_/g, " ").replace(/\u0000/g, "_");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function inferType(val) {
|
|
38
|
+
if (/^(#[0-9a-f]{3,8}$|rgba?\(|hsla?\(|oklch\(|oklab\(|lab\(|lch\(|color\(|color-mix\(|hwb\()/i.test(val)) return "color";
|
|
39
|
+
if (/^(url\(|(repeating-)?(linear|radial|conic)-gradient\()/.test(val)) return "image";
|
|
40
|
+
if (/^-?(\d+\.?\d*|\.\d+)(px|rem|em|%|vw|vh|vmin|vmax|svh|dvh|lvh|ch|ex|cm|mm|in|pt|pc|fr|cqw|cqh)$/.test(val) || /^(calc|clamp|min|max)\(/.test(val)) return "length";
|
|
41
|
+
if (isNum(val)) return "number";
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Negate a CSS length. */
|
|
46
|
+
export function negate(val) {
|
|
47
|
+
if (val === "0px" || val === "0") return val;
|
|
48
|
+
if (/^[\d.]/.test(val)) return "-" + val;
|
|
49
|
+
if (val.startsWith("-") && /^-[\d.]/.test(val)) return val.slice(1);
|
|
50
|
+
return `calc(${val} * -1)`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Spacing value (margin, padding, gap, inset, sizing …).
|
|
55
|
+
* @param {string} v token value ("4", "px", "1/2", "[37px]", "full", "auto" …)
|
|
56
|
+
* @param {object} ctx resolver context
|
|
57
|
+
* @param {{ neg?: boolean, keywords?: Record<string,string>, fractions?: boolean }} [o]
|
|
58
|
+
*/
|
|
59
|
+
export function spacing(v, ctx, o = {}) {
|
|
60
|
+
let out = null;
|
|
61
|
+
if (o.keywords && Object.hasOwn(o.keywords, v)) out = o.keywords[v];
|
|
62
|
+
else if (arb(v)) out = arb(v).value;
|
|
63
|
+
else if (ctx.hasTheme(`--spacing-${v}`)) out = `var(--spacing-${v})`;
|
|
64
|
+
else if (v === "px") out = "1px";
|
|
65
|
+
else if (isNum(v) && +v >= 0) out = rem(+v);
|
|
66
|
+
else if (o.fractions !== false && /^(\d+)\/(\d+)$/.test(v)) { const [a, b] = v.split("/").map(Number); out = b ? `${fmt((a / b) * 100)}%` : null; }
|
|
67
|
+
if (out == null) return null;
|
|
68
|
+
return o.neg ? negate(out) : out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- colors -----------------------------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
let paletteCache;
|
|
74
|
+
/** Omnia palette (from @birdycom/omniacss dist/oc-colors.css) as OKLab points, for approximating arbitrary colors. */
|
|
75
|
+
function palette(ctx) {
|
|
76
|
+
if (paletteCache) return paletteCache;
|
|
77
|
+
const files = [ctx.tokensFile, path.resolve(here, "../../core/dist/oc-colors.css")].filter(Boolean);
|
|
78
|
+
try { files.unshift(require.resolve("@birdycom/omniacss/tokens")); } catch { /* monorepo fallback */ }
|
|
79
|
+
let css = "";
|
|
80
|
+
for (const f of files) { try { css = fs.readFileSync(f, "utf8"); break; } catch { /* next */ } }
|
|
81
|
+
const FAMILIES = new Set("red pink orange yellow violet magenta purple indigo lime green teal cyan turquoise blue shallowblue duskblue twilightblue stormblue frostblue mistblue vividblue skycyan wheat peru brown white snow azure beige ivory linen silver gray black".split(" "));
|
|
82
|
+
paletteCache = [];
|
|
83
|
+
for (const m of css.matchAll(/--color-([a-z]+)-(\d+): oklch\(([\d.]+)% ([\d.]+) (none|[\d.]+)(?:deg)?\)/g)) {
|
|
84
|
+
if (!FAMILIES.has(m[1]) || m[2] === "100") continue;
|
|
85
|
+
const L = +m[3] / 100, C = +m[4], H = m[5] === "none" ? 0 : (+m[5] * Math.PI) / 180;
|
|
86
|
+
paletteCache.push({ token: `--color-${m[1]}-${m[2]}`, lab: [L, C * Math.cos(H), C * Math.sin(H)] });
|
|
87
|
+
}
|
|
88
|
+
return paletteCache;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function rgbToOklab(r, g, b) {
|
|
92
|
+
const lin = (v) => { v /= 255; return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; };
|
|
93
|
+
[r, g, b] = [lin(r), lin(g), lin(b)];
|
|
94
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
95
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
96
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
97
|
+
return [0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s, 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s, 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Parse #hex / rgb() / hsl() into [r,g,b,alpha] or null. */
|
|
101
|
+
export function parseColor(str) {
|
|
102
|
+
str = str.trim();
|
|
103
|
+
let m = str.match(/^#([0-9a-f]{3,8})$/i);
|
|
104
|
+
if (m) {
|
|
105
|
+
let h = m[1];
|
|
106
|
+
if (h.length <= 4) h = [...h].map((c) => c + c).join("");
|
|
107
|
+
if (h.length !== 6 && h.length !== 8) return null;
|
|
108
|
+
return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16)).concat(h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1);
|
|
109
|
+
}
|
|
110
|
+
m = str.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/i);
|
|
111
|
+
if (m) return [+m[1], +m[2], +m[3], m[4] ? (m[4].endsWith("%") ? parseFloat(m[4]) / 100 : +m[4]) : 1];
|
|
112
|
+
m = str.match(/^hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/i);
|
|
113
|
+
if (m) {
|
|
114
|
+
const h = +m[1] / 360, s = +m[2] / 100, l = +m[3] / 100;
|
|
115
|
+
const a = s * Math.min(l, 1 - l);
|
|
116
|
+
const f = (n) => { const k = (n + h * 12) % 12; return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)); };
|
|
117
|
+
return [f(0) * 255, f(8) * 255, f(4) * 255, m[4] ? (m[4].endsWith("%") ? parseFloat(m[4]) / 100 : +m[4]) : 1];
|
|
118
|
+
}
|
|
119
|
+
m = str.match(/^oklch\(\s*([\d.]+)(%?)\s+([\d.]+)\s+(none|[\d.]+)(?:deg)?(?:\s*\/\s*([\d.]+%?))?\s*\)$/i);
|
|
120
|
+
if (m) {
|
|
121
|
+
const l = m[2] ? +m[1] / 100 : +m[1], C = +m[3], H = m[4] === "none" ? 0 : (+m[4] * Math.PI) / 180;
|
|
122
|
+
const a = C * Math.cos(H), b = C * Math.sin(H);
|
|
123
|
+
const l_ = (l + 0.3963377774 * a + 0.2158037573 * b) ** 3, m_ = (l - 0.1055613458 * a - 0.0638541728 * b) ** 3, s_ = (l - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
124
|
+
const lin = [4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_, -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_, -0.0041960863 * l_ - 0.7034186147 * m_ + 1.707614701 * s_];
|
|
125
|
+
const enc = (v) => { v = Math.min(1, Math.max(0, v)); return (v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055) * 255; };
|
|
126
|
+
return [...lin.map(enc), m[5] ? (m[5].endsWith("%") ? parseFloat(m[5]) / 100 : +m[5]) : 1];
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Nearest Omnia token (`--color-x-y`) for a color string; null if it cannot be parsed. */
|
|
132
|
+
export function nearestToken(str, ctx) {
|
|
133
|
+
const c = parseColor(str);
|
|
134
|
+
if (!c) return null;
|
|
135
|
+
const [L, A, B] = rgbToOklab(c[0], c[1], c[2]);
|
|
136
|
+
let best = null;
|
|
137
|
+
for (const p of palette(ctx)) {
|
|
138
|
+
const d = (L - p.lab[0]) ** 2 * 1.4 + (A - p.lab[1]) ** 2 + (B - p.lab[2]) ** 2;
|
|
139
|
+
if (!best || d < best.d) best = { d, token: p.token };
|
|
140
|
+
}
|
|
141
|
+
return best ? { token: best.token, alpha: c[3] } : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Wrap an Omnia-token color with an alpha (0..100 %). */
|
|
145
|
+
export const withAlpha = (color, pct) => `color-mix(in oklab, ${color} ${fmt(pct)}%, transparent)`;
|
|
146
|
+
|
|
147
|
+
/** Color for an arbitrary/literal value, mapped to the Omnia palette unless ctx.arbitraryColors === "keep". */
|
|
148
|
+
function literalColor(val, ctx) {
|
|
149
|
+
if (ctx.arbitraryColors === "keep") return val;
|
|
150
|
+
const n = nearestToken(val, ctx);
|
|
151
|
+
if (!n) return val;
|
|
152
|
+
const base = `var(${n.token})`;
|
|
153
|
+
return n.alpha < 1 ? withAlpha(base, n.alpha * 100) : base;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Resolve a color name/value ("red-500", "white", "primary", "[#fff]", "(--x)") with an optional opacity modifier.
|
|
158
|
+
* @returns {string|null}
|
|
159
|
+
*/
|
|
160
|
+
export function color(v, mod, ctx, { hinted = false } = {}) {
|
|
161
|
+
let out = null;
|
|
162
|
+
const a = arb(v);
|
|
163
|
+
if (a) {
|
|
164
|
+
if (a.type && a.type !== "color") return null;
|
|
165
|
+
if (!a.type && !hinted && inferType(a.value) !== "color" && !/^var\(/.test(a.value)) return null;
|
|
166
|
+
out = /^var\(/.test(a.value) ? a.value : literalColor(a.value, ctx);
|
|
167
|
+
} else if (v === "transparent" || v === "current" || v === "inherit") out = v === "current" ? "currentColor" : v;
|
|
168
|
+
else if (ctx.hasTheme(`--color-${v}`)) out = `var(--color-${v})`;
|
|
169
|
+
else if (ctx.customColors?.has(v)) out = ctx.customColors.get(v);
|
|
170
|
+
else if (COLOR_MAP[v]) out = `var(${COLOR_MAP[v]})`;
|
|
171
|
+
if (out == null) return null;
|
|
172
|
+
if (mod != null && mod !== "") {
|
|
173
|
+
const ma = arb(mod);
|
|
174
|
+
let pct = ma ? (+ma.value <= 1 ? +ma.value * 100 : +ma.value) : +mod;
|
|
175
|
+
if (!Number.isFinite(pct)) return out;
|
|
176
|
+
if (out === "transparent") return out;
|
|
177
|
+
out = withAlpha(out, pct);
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Shared by utilities: 0-100 style number -> fraction (opacity-50 -> 0.5). */
|
|
183
|
+
export const frac = (n) => fmt(+n / 100);
|
|
184
|
+
export { fmt };
|