@birdycom/omniacss-migrate 0.0.0-stage → 0.11.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.
package/src/values.mjs ADDED
@@ -0,0 +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 };
@@ -0,0 +1,189 @@
1
+ // File: packages/migrate/src/variants.mjs
2
+ // Variant handling: hover:, md:, dark:, group-hover:, data-[state=open]:, [&>svg]:, has-[...]: …
3
+ // A variant transforms a "state" ({ sels, media, prelude, rank }) around the utility's class selector.
4
+ // Author: Serge Cools | Framework: OmniaCSS
5
+
6
+ import { arb, normalizeArb } from "./values.mjs";
7
+
8
+ /** Escape a string for use in a CSS class selector. */
9
+ export function esc(s) {
10
+ return s.replace(/^\d/, (c) => `\\3${c} `).replace(/[^A-Za-z0-9_ -￿-]/g, (c) => `\\${c}`).replace(/^-(\d)/, "-\\3$1 ");
11
+ }
12
+
13
+ /** Split on `sep` outside of [] and (). */
14
+ export function splitTop(str, sep) {
15
+ const out = [];
16
+ let depth = 0, cur = "";
17
+ for (let i = 0; i < str.length; i++) {
18
+ const ch = str[i];
19
+ if (ch === "\\") { cur += ch + (str[++i] ?? ""); continue; }
20
+ if (ch === "[" || ch === "(") depth++;
21
+ else if (ch === "]" || ch === ")") depth--;
22
+ if (ch === sep && depth === 0) { out.push(cur); cur = ""; } else cur += ch;
23
+ }
24
+ out.push(cur);
25
+ return out;
26
+ }
27
+
28
+ // Rank: static pseudo-classes in cascade order (hover < focus < active < disabled …), then group/peer, dark, media, screens.
29
+ const PSEUDO_ORDER = ["first-letter", "first-line", "marker", "selection", "file", "placeholder", "backdrop", "before", "after", "first", "last", "only", "odd", "even", "first-of-type", "last-of-type", "only-of-type",
30
+ "visited", "target", "open", "default", "checked", "indeterminate", "placeholder-shown", "autofill", "optional", "required", "valid", "invalid", "user-valid", "user-invalid", "in-range", "out-of-range", "read-only", "empty",
31
+ "focus-within", "hover", "focus", "focus-visible", "active", "enabled", "disabled"];
32
+
33
+ const PSEUDO_CLASS = {
34
+ first: ":first-child", last: ":last-child", only: ":only-child", odd: ":nth-child(odd)", even: ":nth-child(even)", "first-of-type": ":first-of-type", "last-of-type": ":last-of-type", "only-of-type": ":only-of-type",
35
+ visited: ":visited", target: ":target", open: ":is([open], :popover-open, :open)", default: ":default", checked: ":checked", indeterminate: ":indeterminate", "placeholder-shown": ":placeholder-shown", autofill: ":autofill",
36
+ optional: ":optional", required: ":required", valid: ":valid", invalid: ":invalid", "user-valid": ":user-valid", "user-invalid": ":user-invalid", "in-range": ":in-range", "out-of-range": ":out-of-range", "read-only": ":read-only",
37
+ empty: ":empty", "focus-within": ":focus-within", hover: ":hover", focus: ":focus", "focus-visible": ":focus-visible", active: ":active", enabled: ":enabled", disabled: ":disabled", inert: ":is([inert], [inert] *)",
38
+ };
39
+ const PSEUDO_ELEMENT = {
40
+ before: "::before", after: "::after", "first-letter": "::first-letter", "first-line": "::first-line", marker: "::marker", placeholder: "::placeholder", backdrop: "::backdrop", file: "::file-selector-button",
41
+ };
42
+ const MEDIA = {
43
+ print: "print", "motion-safe": "(prefers-reduced-motion: no-preference)", "motion-reduce": "(prefers-reduced-motion: reduce)", portrait: "(orientation: portrait)", landscape: "(orientation: landscape)",
44
+ "contrast-more": "(prefers-contrast: more)", "contrast-less": "(prefers-contrast: less)", "forced-colors": "(forced-colors: active)", "pointer-fine": "(pointer: fine)", "pointer-coarse": "(pointer: coarse)",
45
+ "pointer-none": "(pointer: none)", "any-pointer-fine": "(any-pointer: fine)", "any-pointer-coarse": "(any-pointer: coarse)", "noscript": "(scripting: none)",
46
+ };
47
+ const ARIA = ["busy", "checked", "disabled", "expanded", "hidden", "pressed", "readonly", "required", "selected"];
48
+
49
+ const quoteAttr = (s) => {
50
+ const m = s.match(/^([^=~|^$*]+)([~|^$*]?=)(.*)$/);
51
+ if (!m) return s;
52
+ let v = m[3];
53
+ if (!/^["']/.test(v)) v = `"${v}"`;
54
+ return `${m[1]}${m[2]}${v}`;
55
+ };
56
+
57
+ /** Selector pattern ("&:hover", "&[data-x]") that a pseudo-ish variant name applies to the element, or null. */
58
+ function innerPattern(name, ctx) {
59
+ const st = { sels: ["&"], media: [], prelude: [], rank: 0 };
60
+ const ok = applyVariant(name, st, ctx, { inner: true });
61
+ return ok && !st.media.length ? st.sels[0] : null;
62
+ }
63
+
64
+ /**
65
+ * Apply one variant to the state.
66
+ * @returns {boolean} false when the variant is unknown
67
+ */
68
+ export function applyVariant(name, st, ctx, { inner = false } = {}) {
69
+ const mapSels = (fn) => { st.sels = st.sels.map(fn); };
70
+ const rankOf = (n) => { const i = PSEUDO_ORDER.indexOf(n); return (i < 0 ? 40 : i) + 1; };
71
+
72
+ if (PSEUDO_CLASS[name]) { mapSels((s) => addSimple(s, PSEUDO_CLASS[name])); st.rank += rankOf(name) * 10; return true; }
73
+ if (name === "selection") { st.sels = st.sels.flatMap((s) => [`${s} ::selection`, `${s}::selection`]); st.rank += 40; return true; }
74
+ if (PSEUDO_ELEMENT[name]) {
75
+ mapSels((s) => s + PSEUDO_ELEMENT[name]);
76
+ if (name === "before" || name === "after") st.prelude.push(["content", "var(--oc-content, \"\")"]);
77
+ st.rank += rankOf(name) * 10; return true;
78
+ }
79
+ if (name === "dark") {
80
+ if (ctx.darkMode === "media") st.media.unshift("@media (prefers-color-scheme: dark)");
81
+ else mapSels((s) => `${ctx.darkSelector} ${s}`);
82
+ st.rank += 5000; return true;
83
+ }
84
+ if (name === "ltr" || name === "rtl") { mapSels((s) => `[dir="${name}"] ${s}`); st.rank += 4000; return true; }
85
+ if (MEDIA[name]) { st.media.unshift(`@media ${MEDIA[name]}`); st.rank += 6000; return true; }
86
+ if (name === "starting") { st.media.unshift("@starting-style"); st.rank += 6500; return true; }
87
+ if (name === "*") { mapSels((s) => `${s} > *`); st.rank += 3000; return true; }
88
+ if (name === "**") { mapSels((s) => `${s} *`); st.rank += 3000; return true; }
89
+
90
+ // screens
91
+ if (ctx.screens[name]) { st.media.unshift(`@media (min-width: ${ctx.screens[name]})`); st.rank += 10000 + ctx.screenRank(name); return true; }
92
+ let m = name.match(/^max-(.+)$/);
93
+ if (m && ctx.screens[m[1]]) { st.media.unshift(`@media (width < ${ctx.screens[m[1]]})`); st.rank += 9000 - ctx.screenRank(m[1]); return true; }
94
+ m = name.match(/^(min|max)-(\[.+\])$/);
95
+ if (m) { const a = arb(m[2]); st.media.unshift(`@media (${m[1]}-width: ${a.value})`); st.rank += m[1] === "min" ? 10050 : 8999; return true; }
96
+ // container queries
97
+ m = name.match(/^@(?:(min|max)-)?(.+?)(?:\/([\w-]+))?$/);
98
+ if (m) {
99
+ const [, kind, size, cname] = m;
100
+ const sizes = { "3xs": 16, "2xs": 18, xs: 20, sm: 24, md: 28, lg: 32, xl: 36, "2xl": 42, "3xl": 48, "4xl": 56, "5xl": 64, "6xl": 72, "7xl": 80 };
101
+ const a = arb(size);
102
+ const width = a ? a.value : sizes[size] ? `${sizes[size]}rem` : null;
103
+ if (width) {
104
+ const cond = kind === "max" ? `(width < ${width})` : `(min-width: ${width})`;
105
+ st.media.unshift(`@container ${cname ? cname + " " : ""}${cond}`);
106
+ st.rank += 20000 + (sizes[size] || 0); return true;
107
+ }
108
+ }
109
+ // supports
110
+ m = name.match(/^(not-)?supports-(.+)$/);
111
+ if (m) {
112
+ const a = arb(m[2]);
113
+ const cond = a ? (/^[\w-]+:/.test(a.value) ? `(${a.value})` : a.value) : `(${m[2]}: var(--oc-x))`;
114
+ st.media.unshift(`@supports ${m[1] ? "not " : ""}${cond}`); st.rank += 6200; return true;
115
+ }
116
+ // data- / aria-
117
+ m = name.match(/^data-(.+)$/);
118
+ if (m) {
119
+ const a = arb(m[1]);
120
+ const attr = a ? `[data-${quoteAttr(a.value)}]` : `[data-${m[1]}]`;
121
+ mapSels((s) => addSimple(s, attr)); st.rank += 3500; return true;
122
+ }
123
+ m = name.match(/^aria-(.+)$/);
124
+ if (m) {
125
+ const a = arb(m[1]);
126
+ const attr = a ? `[aria-${quoteAttr(a.value)}]` : `[aria-${m[1]}="true"]`;
127
+ mapSels((s) => addSimple(s, attr)); st.rank += 3500; return true;
128
+ }
129
+ // group / peer
130
+ m = name.match(/^(group|peer)(?:-(.+?))?(?:\/([\w-]+))?$/);
131
+ if (m && m[2] !== undefined) {
132
+ const [, kind, innerName, gname] = m;
133
+ const base = `.${kind}${gname ? "\\/" + esc(gname) : ""}`;
134
+ let pattern;
135
+ const a = arb(innerName);
136
+ if (a) pattern = a.value.includes("&") ? a.value : `&${a.value}`;
137
+ else pattern = innerPattern(innerName, ctx);
138
+ if (!pattern) return false;
139
+ const rel = pattern.replace(/&/g, base);
140
+ mapSels((s) => (kind === "group" ? `${rel} ${s}` : `${rel} ~ ${s}`));
141
+ st.rank += 2000; return true;
142
+ }
143
+ // has / not / in / nth
144
+ m = name.match(/^(has|not|in)-(.+)$/);
145
+ if (m) {
146
+ const [, kind, innerName] = m;
147
+ const a = arb(innerName);
148
+ let pattern;
149
+ if (a) pattern = a.value.includes("&") ? a.value : `&${a.value}`;
150
+ else pattern = innerPattern(innerName, ctx);
151
+ if (kind === "not" && !pattern) {
152
+ const med = MEDIA[innerName];
153
+ if (med) { st.media.unshift(`@media not ${med}`); st.rank += 6000; return true; }
154
+ if (ctx.screens[innerName]) { st.media.unshift(`@media (width < ${ctx.screens[innerName]})`); st.rank += 9000; return true; }
155
+ }
156
+ if (!pattern) return false;
157
+ const rest = pattern.startsWith("&") ? pattern.slice(1) : pattern;
158
+ if (kind === "has") mapSels((s) => addSimple(s, `:has(${rest})`));
159
+ else if (kind === "not") mapSels((s) => addSimple(s, `:not(${rest})`));
160
+ else mapSels((s) => `:where(${rest || "*"}) ${s}`);
161
+ st.rank += 3200; return true;
162
+ }
163
+ m = name.match(/^nth-(last-)?(of-type-)?(.+)$/);
164
+ if (m) {
165
+ const a = arb(m[3]);
166
+ const arg = a ? a.value : m[3];
167
+ mapSels((s) => `${s}:nth-${m[1] ? "last-" : ""}${m[2] ? "of-type" : "child"}(${arg})`); st.rank += 450; return true;
168
+ }
169
+ if (ctx.customVariants && Object.hasOwn(ctx.customVariants, name)) {
170
+ const p = ctx.customVariants[name].trim();
171
+ if (p.startsWith("@")) st.media.unshift(p); else mapSels((s) => p.replace(/&/g, s));
172
+ st.rank += 3000; return true;
173
+ }
174
+ // arbitrary variant: [&>*]:, [&_svg]:, [@media(min-width:900px)]:
175
+ m = name.match(/^\[([\s\S]+)\]$/);
176
+ if (m) {
177
+ const body = normalizeArb(m[1]);
178
+ if (body.startsWith("@")) { st.media.unshift(body.replace(/^@(media|supports|container)\s*/, "@$1 ")); st.rank += 6100; return true; }
179
+ if (!body.includes("&")) return false;
180
+ mapSels((s) => body.replace(/&/g, s)); st.rank += 3000; return true;
181
+ }
182
+ return false;
183
+ }
184
+
185
+ /** Append a simple selector (pseudo-class, attribute …) in front of a trailing pseudo-element: `.a::after` + `:hover` -> `.a:hover::after`. */
186
+ function addSimple(sel, suffix) {
187
+ const m = sel.match(/^(.*?)(::[a-z-]+)$/s);
188
+ return m ? m[1] + suffix + m[2] : sel + suffix;
189
+ }
package/src/vite.mjs ADDED
@@ -0,0 +1,81 @@
1
+ // File: packages/migrate/src/vite.mjs
2
+ // Vite plugin: drop-in replacement for the Tailwind Vite plugin. It puts the "keep your classes" PostCSS plugin
3
+ // in front of your existing PostCSS plugins (autoprefixer …) and removes any Tailwind PostCSS plugin.
4
+ //
5
+ // // vite.config.js
6
+ // import omniaMigrate from "@birdycom/omniacss-migrate/vite";
7
+ // export default defineConfig({ plugins: [laravel({ ... }), omniaMigrate()] });
8
+ //
9
+ // Author: Serge Cools | Framework: OmniaCSS
10
+
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import { createRequire } from "node:module";
14
+ import { pathToFileURL } from "node:url";
15
+ import postcss from "postcss";
16
+ import omniaMigratePostcss from "./postcss.mjs";
17
+
18
+ // every PostCSS entry that belongs to the Tailwind ecosystem is dropped (@tailwindcss/postcss, tailwindcss, @iconify/tailwind4 …)
19
+ const isTailwind = (name) => /tailwind/i.test(name);
20
+
21
+ /** Load the project's postcss.config.* into a plugin array (Tailwind removed). */
22
+ async function loadExistingPlugins(root) {
23
+ const file = ["postcss.config.js", "postcss.config.mjs", "postcss.config.cjs"].map((f) => path.join(root, f)).find((f) => fs.existsSync(f));
24
+ if (!file) return [];
25
+ let cfg;
26
+ try { const mod = await import(pathToFileURL(file).href); cfg = mod.default ?? mod; } catch { return []; }
27
+ if (typeof cfg === "function") cfg = cfg({ env: process.env.NODE_ENV || "development" });
28
+ const plugins = cfg?.plugins;
29
+ if (!plugins) return [];
30
+ const req = createRequire(path.join(root, "package.json"));
31
+ const out = [];
32
+ if (Array.isArray(plugins)) {
33
+ for (const p of plugins) {
34
+ if (typeof p === "string") { if (isTailwind(p)) continue; try { const m = req(p); out.push((m.default ?? m)()); } catch { /* skip */ } }
35
+ else if (p && (p.postcssPlugin === "tailwindcss" || p.postcssPlugin === "@tailwindcss/postcss" || p.postcssPlugin === "internal:tailwind")) continue;
36
+ else if (typeof p === "function" && (p.name === "tailwindcss" || p.postcssPlugin === "tailwindcss")) continue;
37
+ else out.push(p);
38
+ }
39
+ } else {
40
+ for (const [name, opts] of Object.entries(plugins)) {
41
+ if (isTailwind(name) || opts === false) continue;
42
+ try {
43
+ const m = await import(pathToFileURL(req.resolve(name)).href).catch(() => ({ default: req(name) }));
44
+ const factory = m.default ?? m;
45
+ out.push(typeof factory === "function" ? factory(opts) : factory);
46
+ } catch { /* plugin not installed: skip */ }
47
+ }
48
+ }
49
+ return out;
50
+ }
51
+
52
+ /** @param {Parameters<typeof omniaMigratePostcss>[0]} [options] */
53
+ export default function omniaMigrate(options = {}) {
54
+ let root = process.cwd();
55
+ return {
56
+ name: "omniacss-migrate",
57
+ enforce: "pre",
58
+ // Vite inlines `@import` before PostCSS plugins run, and `@import "tailwindcss"` no longer resolves once the package is gone:
59
+ // so the directive entry files are compiled here, before Vite looks at them. @apply in imported files is handled by the PostCSS plugin below.
60
+ async transform(code, id) {
61
+ const file = id.split("?")[0];
62
+ if (!/\.(css|pcss|postcss)$/.test(file)) return null;
63
+ if (!/@import\s+["']tailwindcss|@tailwind\s+\w+|@apply\s|@theme\b/.test(code)) return null;
64
+ const result = await postcss([omniaMigratePostcss({ cwd: root, ...options })]).process(code, { from: file, map: false });
65
+ for (const m of result.messages) if (m.type === "dependency" && m.file) this.addWatchFile(m.file);
66
+ return { code: result.css, map: null };
67
+ },
68
+ configResolved(config) {
69
+ root = config.root;
70
+ },
71
+ async config(config) {
72
+ const root = path.resolve(config.root || process.cwd());
73
+ config.css ||= {};
74
+ const existing = config.css.postcss;
75
+ if (typeof existing === "string") return; // custom config dir: use the PostCSS plugin there instead
76
+ const others = existing && typeof existing === "object" && existing.plugins ? existing.plugins : await loadExistingPlugins(root);
77
+ const keep = others.filter((p) => !(p && (p.postcssPlugin === "tailwindcss" || p.postcssPlugin === "@tailwindcss/postcss" || p.name === "tailwindcss")) && p !== omniaMigratePostcss);
78
+ config.css.postcss = { ...(existing && typeof existing === "object" ? existing : {}), plugins: [omniaMigratePostcss({ cwd: root, ...options }), ...keep] };
79
+ },
80
+ };
81
+ }