@birdycom/omniacss 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 +44 -44
- package/dist/oc-theme.css +1 -1
- package/dist/omnia.css +2 -1
- package/dist/omnia.min.css +1 -1
- package/dist/omnia.starter.css +2 -0
- package/dist/omnia.starter.min.css +1 -1
- package/dist/tokens/omnia.dtcg.json +2 -2
- package/dist/tokens/omnia.tokens.json +1 -1
- package/dist/tokens/omnia_tokens.py +2 -2
- package/package.json +63 -63
- package/scss/utilities/_anchor_positioning.scss +65 -65
- package/scss/utilities/_animation_properties.scss +143 -143
- package/scss/utilities/_borders_extended.scss +97 -97
- package/scss/utilities/_borders_outlines.scss +161 -161
- package/scss/utilities/_containment.scss +82 -82
- package/scss/utilities/_css_functions.scss +34 -34
- package/scss/utilities/_dark_mode.scss +117 -117
- package/scss/utilities/_drop_shadow_colors.scss +26 -26
- package/scss/utilities/_experimental_css.scss +149 -149
- package/scss/utilities/_flexbox.scss +317 -317
- package/scss/utilities/_font_variants.scss +139 -139
- package/scss/utilities/_gap.scss +103 -103
- package/scss/utilities/_gradient_stops.scss +48 -48
- package/scss/utilities/_grid.scss +672 -672
- package/scss/utilities/_layout_extras.scss +171 -171
- package/scss/utilities/_layout_flow.scss +210 -210
- package/scss/utilities/_mask_extras.scss +62 -62
- package/scss/utilities/_motion_transforms.scss +167 -167
- package/scss/utilities/_scroll_animations.scss +80 -80
- package/scss/utilities/_scrollbar.scss +2 -1
- package/scss/utilities/_shadow_tokens.scss +21 -21
- package/scss/utilities/_shapes.scss +72 -72
- package/scss/utilities/_sizing_logical.scss +99 -99
- package/scss/utilities/_starting_style.scss +53 -53
- package/scss/utilities/_state_variants.scss +196 -196
- package/scss/utilities/_svg.scss +100 -100
- package/scss/utilities/_table_advanced.scss +19 -19
- package/scss/utilities/_typography_advanced.scss +124 -124
- package/scss/utilities/_typography_modern.scss +83 -83
- package/scss/utilities/_view_transitions.scss +64 -64
- package/scss/utilities/_writing_modes.scss +72 -72
- package/scss/variables/_hulp.scss +107 -107
- package/scss/variables/_variables_effects.scss +108 -108
- package/src/postcss.mjs +43 -43
- package/src/tokens.mjs +377 -377
- package/src/variants.mjs +278 -278
- package/tokens/semantic.json +80 -80
package/src/variants.mjs
CHANGED
|
@@ -1,278 +1,278 @@
|
|
|
1
|
-
// File: packages/core/src/variants.mjs
|
|
2
|
-
// On-demand variants for the OmniaCSS engine: hover:, md:, group-hover:, has-[..]:, aria-*, data-*, dark:, cq-md: …
|
|
3
|
-
// A variant maps a list of "branches" ({ sel, at }) around the utility: `sel` is a selector template where `&` stands
|
|
4
|
-
// for the utility class, `at` the list of wrapping at-rules (outermost first). Most variants keep one branch; dark: makes two.
|
|
5
|
-
// Map: breakpoints -> maps -> parseCandidate -> applyVariant -> resolveVariants
|
|
6
|
-
// Author: Serge Cools | Framework: OmniaCSS
|
|
7
|
-
|
|
8
|
-
/** Viewport breakpoints (min-width). sm/md/lg match the native responsive utilities (_responsive.scss). */
|
|
9
|
-
export const BREAKPOINTS = { sm: "36rem", md: "50rem", lg: "64rem", xl: "80rem", "2xl": "96rem" };
|
|
10
|
-
|
|
11
|
-
/** Container-query sizes for cq-<size>: (xs..xl match _containment.scss). */
|
|
12
|
-
export const CONTAINER_SIZES = {
|
|
13
|
-
"3xs": "16rem", "2xs": "18rem", xs: "20rem", sm: "24rem", md: "28rem", lg: "32rem", xl: "36rem",
|
|
14
|
-
"2xl": "42rem", "3xl": "48rem", "4xl": "56rem", "5xl": "64rem", "6xl": "72rem", "7xl": "80rem",
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const PSEUDO_CLASS = {
|
|
18
|
-
hover: ":hover", focus: ":focus", "focus-visible": ":focus-visible", "focus-within": ":focus-within", active: ":active",
|
|
19
|
-
visited: ":visited", target: ":target", disabled: ":disabled", enabled: ":enabled", checked: ":checked", indeterminate: ":indeterminate",
|
|
20
|
-
default: ":default", required: ":required", optional: ":optional", valid: ":valid", invalid: ":invalid", "user-valid": ":user-valid",
|
|
21
|
-
"user-invalid": ":user-invalid", "in-range": ":in-range", "out-of-range": ":out-of-range", "read-only": ":read-only",
|
|
22
|
-
"placeholder-shown": ":placeholder-shown", autofill: ":autofill", empty: ":empty",
|
|
23
|
-
first: ":first-child", last: ":last-child", only: ":only-child", odd: ":nth-child(odd)", even: ":nth-child(even)",
|
|
24
|
-
"first-of-type": ":first-of-type", "last-of-type": ":last-of-type", "only-of-type": ":only-of-type",
|
|
25
|
-
open: ":is([open], :popover-open, :open)", "popover-open": ":popover-open", inert: ":is([inert], [inert] *)",
|
|
26
|
-
};
|
|
27
|
-
const STATE_ORDER = ["visited", "target", "open", "popover-open", "default", "checked", "indeterminate", "placeholder-shown", "autofill", "optional", "required", "valid", "invalid", "user-valid", "user-invalid", "in-range", "out-of-range", "read-only", "empty", "focus-within", "hover", "focus", "focus-visible", "active", "enabled", "disabled", "inert"];
|
|
28
|
-
const STRUCT_ORDER = ["first", "last", "only", "odd", "even", "first-of-type", "last-of-type", "only-of-type"];
|
|
29
|
-
|
|
30
|
-
const PSEUDO_ELEMENT = {
|
|
31
|
-
before: "::before", after: "::after", "first-letter": "::first-letter", "first-line": "::first-line", marker: "::marker",
|
|
32
|
-
placeholder: "::placeholder", backdrop: "::backdrop", file: "::file-selector-button", selection: "::selection", "details-content": "::details-content",
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const MEDIA = {
|
|
36
|
-
print: "print", "motion-safe": "(prefers-reduced-motion: no-preference)", "motion-reduce": "(prefers-reduced-motion: reduce)",
|
|
37
|
-
portrait: "(orientation: portrait)", landscape: "(orientation: landscape)", "contrast-more": "(prefers-contrast: more)",
|
|
38
|
-
"contrast-less": "(prefers-contrast: less)", "forced-colors": "(forced-colors: active)", "inverted-colors": "(inverted-colors: inverted)",
|
|
39
|
-
"pointer-fine": "(pointer: fine)", "pointer-coarse": "(pointer: coarse)", "pointer-none": "(pointer: none)",
|
|
40
|
-
"any-pointer-fine": "(any-pointer: fine)", "any-pointer-coarse": "(any-pointer: coarse)", "any-pointer-none": "(any-pointer: none)",
|
|
41
|
-
"hover-capable": "(hover: hover)", noscript: "(scripting: none)", "auto-dark": "(prefers-color-scheme: dark)",
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/** Scroll-state container queries (the element must sit inside a `container-scroll-state` ancestor). */
|
|
45
|
-
const SCROLL_STATE = {
|
|
46
|
-
stuck: "not scroll-state(stuck: none)", "stuck-top": "scroll-state(stuck: top)", "stuck-bottom": "scroll-state(stuck: bottom)",
|
|
47
|
-
"stuck-left": "scroll-state(stuck: left)", "stuck-right": "scroll-state(stuck: right)",
|
|
48
|
-
snapped: "not scroll-state(snapped: none)", "snapped-x": "scroll-state(snapped: x)", "snapped-y": "scroll-state(snapped: y)",
|
|
49
|
-
"scrollable-top": "scroll-state(scrollable: top)", "scrollable-bottom": "scroll-state(scrollable: bottom)",
|
|
50
|
-
"scrollable-left": "scroll-state(scrollable: left)", "scrollable-right": "scroll-state(scrollable: right)",
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const ARIA = ["busy", "checked", "disabled", "expanded", "hidden", "pressed", "readonly", "required", "selected"];
|
|
54
|
-
|
|
55
|
-
/** Fixed variant names per group (for editor tooling such as the OmniaCSS IntelliSense extension). */
|
|
56
|
-
export const VARIANT_GROUPS = {
|
|
57
|
-
pseudoClass: Object.keys(PSEUDO_CLASS), pseudoElement: Object.keys(PSEUDO_ELEMENT), media: Object.keys(MEDIA),
|
|
58
|
-
scrollState: Object.keys(SCROLL_STATE), aria: ARIA, other: ["dark", "rtl", "ltr", "starting", "*", "**"],
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const DARK_EXPLICIT = ':where(.dark, [data-theme="dark"], :root:has(.oc-theme-switch input[value="dark"]:checked))';
|
|
62
|
-
const DARK_SYSTEM = ':where(:root:not([data-theme="light"], .light, :has(.oc-theme-switch input[value="light"]:checked)))';
|
|
63
|
-
|
|
64
|
-
/** Escape a string for use in a class selector. */
|
|
65
|
-
export function esc(s) {
|
|
66
|
-
let out = "";
|
|
67
|
-
for (let i = 0; i < s.length; i++) {
|
|
68
|
-
const ch = s[i];
|
|
69
|
-
if (/[0-9]/.test(ch) && (i === 0 || (i === 1 && s[0] === "-"))) out += "\\3" + ch + " ";
|
|
70
|
-
else if (/[A-Za-z0-9_\-¡-]/.test(ch)) out += ch;
|
|
71
|
-
else out += "\\" + ch;
|
|
72
|
-
}
|
|
73
|
-
return out;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Split on `sep` outside of [] and (). */
|
|
77
|
-
export function splitTop(str, sep = ":") {
|
|
78
|
-
const out = [];
|
|
79
|
-
let depth = 0, cur = "";
|
|
80
|
-
for (let i = 0; i < str.length; i++) {
|
|
81
|
-
const ch = str[i];
|
|
82
|
-
if (ch === "\\") { cur += ch + (str[++i] ?? ""); continue; }
|
|
83
|
-
if (ch === "[" || ch === "(") depth++;
|
|
84
|
-
else if (ch === "]" || ch === ")") depth--;
|
|
85
|
-
if (ch === sep && depth === 0) { out.push(cur); cur = ""; } else cur += ch;
|
|
86
|
-
}
|
|
87
|
-
out.push(cur);
|
|
88
|
-
return out;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** `[a_b]` -> `a b` (underscore = space, `\_` stays an underscore). */
|
|
92
|
-
const arbitrary = (s) => {
|
|
93
|
-
const m = s.match(/^\[([\s\S]+)\]$/);
|
|
94
|
-
return m ? m[1].replace(/\\_/g, "\u0000").replace(/_/g, " ").replace(/\u0000/g, "_") : null;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const quoteAttr = (s) => {
|
|
98
|
-
const m = s.match(/^([^=~|^$*]+)([~|^$*]?=)(.*)$/);
|
|
99
|
-
if (!m) return s;
|
|
100
|
-
const v = /^["']/.test(m[3]) ? m[3] : `"${m[3]}"`;
|
|
101
|
-
return `${m[1]}${m[2]}${v}`;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
/** Append a simple selector in front of a trailing pseudo-element: `&::after` + `:hover` -> `&:hover::after`. */
|
|
105
|
-
function addSimple(sel, suffix) {
|
|
106
|
-
const m = sel.match(/^(.*?)(::[a-z-]+)$/s);
|
|
107
|
-
return m ? m[1] + suffix + m[2] : sel + suffix;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Parse a class token: `md:hover:!bg-x` -> { variants: ["md","hover"], base: "bg-x", important: true }.
|
|
112
|
-
* Returns null when there is nothing to do (no variants and no `!`).
|
|
113
|
-
*/
|
|
114
|
-
export function parseCandidate(token) {
|
|
115
|
-
if (token.length > 120) return null;
|
|
116
|
-
const parts = splitTop(token, ":");
|
|
117
|
-
let base = parts.pop();
|
|
118
|
-
if (!base) return null;
|
|
119
|
-
let important = false;
|
|
120
|
-
if (base.startsWith("!")) { important = true; base = base.slice(1); }
|
|
121
|
-
else if (base.endsWith("!")) { important = true; base = base.slice(0, -1); }
|
|
122
|
-
if (!/^-?[A-Za-z]/.test(base)) return null;
|
|
123
|
-
if (!parts.length && !important) return null;
|
|
124
|
-
if (parts.some((p) => !p)) return null;
|
|
125
|
-
return { token, variants: parts, base, important };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const state = (sel = "&", at = []) => ({ sel, at });
|
|
129
|
-
|
|
130
|
-
/** Selector pattern of a variant that only changes the element's selector ("&:hover"), or null. */
|
|
131
|
-
function innerPattern(name) {
|
|
132
|
-
const r = applyVariant(name, [state()]);
|
|
133
|
-
return r && r.length === 1 && !r[0].at.length ? r[0].sel : null;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const patternOf = (name) => {
|
|
137
|
-
const a = arbitrary(name);
|
|
138
|
-
if (a !== null) return a.includes("&") ? a : `&${a}`;
|
|
139
|
-
return innerPattern(name);
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Apply one variant to a list of branches.
|
|
144
|
-
* @returns {{sel:string, at:string[]}[]|null} null when the variant is unknown
|
|
145
|
-
*/
|
|
146
|
-
export function applyVariant(name, branches) {
|
|
147
|
-
const mapSel = (fn) => branches.map((b) => ({ ...b, sel: fn(b.sel) }));
|
|
148
|
-
const wrap = (at) => branches.map((b) => ({ ...b, at: [...b.at, at] }));
|
|
149
|
-
let m;
|
|
150
|
-
|
|
151
|
-
if (Object.hasOwn(PSEUDO_CLASS, name)) return mapSel((s) => addSimple(s, PSEUDO_CLASS[name]));
|
|
152
|
-
// marker: and selection: also reach the descendants (<ul class="marker:…"> colors the bullets of its <li>s).
|
|
153
|
-
if (name === "marker" || name === "selection") {
|
|
154
|
-
return branches.flatMap((b) => [{ ...b, sel: b.sel + PSEUDO_ELEMENT[name] }, { ...b, sel: `${b.sel} *${PSEUDO_ELEMENT[name]}` }]);
|
|
155
|
-
}
|
|
156
|
-
if (Object.hasOwn(PSEUDO_ELEMENT, name)) return mapSel((s) => s + PSEUDO_ELEMENT[name]);
|
|
157
|
-
if (Object.hasOwn(MEDIA, name)) return wrap(`@media ${MEDIA[name]}`);
|
|
158
|
-
if (Object.hasOwn(SCROLL_STATE, name)) return wrap(`@container ${SCROLL_STATE[name]}`);
|
|
159
|
-
if (name === "starting") return wrap("@starting-style");
|
|
160
|
-
if (name === "rtl" || name === "ltr") return mapSel((s) => addSimple(s, `:where(:dir(${name}), [dir="${name}"], [dir="${name}"] *)`));
|
|
161
|
-
if (name === "dark") {
|
|
162
|
-
return branches.flatMap((b) => [
|
|
163
|
-
{ sel: `${DARK_EXPLICIT} ${b.sel}`, at: b.at },
|
|
164
|
-
{ sel: `${DARK_SYSTEM} ${b.sel}`, at: [...b.at, "@media (prefers-color-scheme: dark)"] },
|
|
165
|
-
]);
|
|
166
|
-
}
|
|
167
|
-
if (name === "*") return mapSel((s) => `${s} > *`);
|
|
168
|
-
if (name === "**") return mapSel((s) => `${s} *`);
|
|
169
|
-
|
|
170
|
-
// viewport: sm, max-sm, min-sm, min-[600px], max-[600px]
|
|
171
|
-
if (Object.hasOwn(BREAKPOINTS, name)) return wrap(`@media (min-width: ${BREAKPOINTS[name]})`);
|
|
172
|
-
if ((m = name.match(/^(min|max)-(.+)$/))) {
|
|
173
|
-
const [, kind, rest] = m;
|
|
174
|
-
const a = arbitrary(rest);
|
|
175
|
-
const v = a ?? BREAKPOINTS[rest];
|
|
176
|
-
if (v) return wrap(kind === "min" ? `@media (min-width: ${v})` : `@media (width < ${v})`);
|
|
177
|
-
}
|
|
178
|
-
// container queries: cq-md, cq-max-md, cq-[400px], cq-md/name (and the short form @md)
|
|
179
|
-
if ((m = name.match(/^(?:cq-|@)(?:(min|max)-)?(.+?)(?:\/([\w-]+))?$/))) {
|
|
180
|
-
const [, kind, size, cname] = m;
|
|
181
|
-
const a = arbitrary(size);
|
|
182
|
-
const v = a ?? CONTAINER_SIZES[size];
|
|
183
|
-
if (v) return wrap(`@container ${cname ? cname + " " : ""}${kind === "max" ? `(width < ${v})` : `(min-width: ${v})`}`);
|
|
184
|
-
}
|
|
185
|
-
// supports
|
|
186
|
-
if ((m = name.match(/^(not-)?supports-(.+)$/))) {
|
|
187
|
-
const a = arbitrary(m[2]);
|
|
188
|
-
const cond = a !== null ? (/^[\w-]+\s*:/.test(a) ? `(${a})` : /^[\w-]+$/.test(a) ? `(${a}: inherit)` : a) : `(${m[2]}: inherit)`;
|
|
189
|
-
return wrap(`@supports ${m[1] ? "not " : ""}${cond}`);
|
|
190
|
-
}
|
|
191
|
-
// data-* / aria-*
|
|
192
|
-
if ((m = name.match(/^data-(.+)$/))) {
|
|
193
|
-
const a = arbitrary(m[1]);
|
|
194
|
-
return mapSel((s) => addSimple(s, a !== null ? `[data-${quoteAttr(a)}]` : `[data-${m[1]}]`));
|
|
195
|
-
}
|
|
196
|
-
if ((m = name.match(/^aria-(.+)$/))) {
|
|
197
|
-
const a = arbitrary(m[1]);
|
|
198
|
-
if (a === null && !ARIA.includes(m[1])) return null;
|
|
199
|
-
return mapSel((s) => addSimple(s, a !== null ? `[aria-${quoteAttr(a)}]` : `[aria-${m[1]}="true"]`));
|
|
200
|
-
}
|
|
201
|
-
// group-* / peer-* (optionally named: group-hover/card)
|
|
202
|
-
if ((m = name.match(/^(group|peer)-(.+?)(?:\/([\w-]+))?$/))) {
|
|
203
|
-
const [, kind, innerName, gname] = m;
|
|
204
|
-
const pattern = patternOf(innerName);
|
|
205
|
-
if (!pattern) return null;
|
|
206
|
-
const marker = `:where(.${kind}${gname ? "\\/" + esc(gname) : ""})`;
|
|
207
|
-
const rel = pattern.replace(/&/g, marker);
|
|
208
|
-
return mapSel((s) => addSimple(s, `:is(${rel} ${kind === "group" ? "*" : "~ *"})`));
|
|
209
|
-
}
|
|
210
|
-
// has-* / not-* / in-*
|
|
211
|
-
if ((m = name.match(/^(has|not|in)-(.+)$/))) {
|
|
212
|
-
const [, kind, innerName] = m;
|
|
213
|
-
const pattern = patternOf(innerName);
|
|
214
|
-
if (!pattern && kind === "not") {
|
|
215
|
-
// not-md, not-print, not-supports-[..]
|
|
216
|
-
const inner = applyVariant(innerName, [state()]);
|
|
217
|
-
if (inner && inner.length === 1 && inner[0].at.length === 1) {
|
|
218
|
-
const at = inner[0].at[0];
|
|
219
|
-
if (/^@media /.test(at)) { const p = at.slice(7); return wrap(`@media not ${p.startsWith("(") ? "all and " : ""}${p}`); }
|
|
220
|
-
if (/^@supports not /.test(at)) return wrap(at.replace("@supports not ", "@supports "));
|
|
221
|
-
if (/^@supports /.test(at)) return wrap(at.replace("@supports ", "@supports not "));
|
|
222
|
-
}
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
if (!pattern || !pattern.startsWith("&")) return null;
|
|
226
|
-
const rest = pattern.slice(1);
|
|
227
|
-
if (kind === "has") return mapSel((s) => addSimple(s, `:has(${rest})`));
|
|
228
|
-
if (kind === "not") return mapSel((s) => addSimple(s, `:not(${rest})`));
|
|
229
|
-
return mapSel((s) => `:where(${rest || "*"}) ${s}`);
|
|
230
|
-
}
|
|
231
|
-
if ((m = name.match(/^nth-(last-)?(of-type-)?(.+)$/))) {
|
|
232
|
-
const a = arbitrary(m[3]);
|
|
233
|
-
const arg = a ?? (/^\d+$/.test(m[3]) ? m[3] : /^(odd|even)$/.test(m[3]) ? m[3] : null);
|
|
234
|
-
if (arg === null) return null;
|
|
235
|
-
return mapSel((s) => addSimple(s, `:nth-${m[1] ? "last-" : ""}${m[2] ? "of-type" : "child"}(${arg})`));
|
|
236
|
-
}
|
|
237
|
-
// arbitrary variant: [&>svg]:, [&_p]:, [@media(min-width:900px)]:
|
|
238
|
-
if ((m = name.match(/^\[([\s\S]+)\]$/))) {
|
|
239
|
-
const body = arbitrary(name);
|
|
240
|
-
if (body.startsWith("@")) return wrap(body.replace(/^@(media|supports|container)\s*/, "@$1 "));
|
|
241
|
-
if (!body.includes("&")) return null;
|
|
242
|
-
return mapSel((s) => body.replace(/&/g, s));
|
|
243
|
-
}
|
|
244
|
-
return null;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** Apply all variants left to right. Returns branches or null if any variant is unknown. */
|
|
248
|
-
export function resolveVariants(variants) {
|
|
249
|
-
let branches = [state()];
|
|
250
|
-
for (const v of variants) {
|
|
251
|
-
branches = applyVariant(v, branches);
|
|
252
|
-
if (!branches) return null;
|
|
253
|
-
}
|
|
254
|
-
return branches;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/** Cascade rank of a variant list: states < group/peer < attribute < media < dark < screens < containers. */
|
|
258
|
-
export function rankOf(variants) {
|
|
259
|
-
let rank = 0;
|
|
260
|
-
for (const v of variants) {
|
|
261
|
-
let r;
|
|
262
|
-
if (Object.hasOwn(PSEUDO_ELEMENT, v)) r = 100;
|
|
263
|
-
else if (STRUCT_ORDER.includes(v)) r = 200 + STRUCT_ORDER.indexOf(v);
|
|
264
|
-
else if (Object.hasOwn(PSEUDO_CLASS, v)) r = 300 + STATE_ORDER.indexOf(v);
|
|
265
|
-
else if (/^(group|peer|in)-/.test(v)) r = 400;
|
|
266
|
-
else if (/^(has|not|aria|data|nth)-|^[*\[]/.test(v)) r = 500;
|
|
267
|
-
else if (/^(not-)?supports-/.test(v)) r = 600;
|
|
268
|
-
else if (v === "rtl" || v === "ltr") r = 650;
|
|
269
|
-
else if (Object.hasOwn(MEDIA, v) || Object.hasOwn(SCROLL_STATE, v)) r = 700;
|
|
270
|
-
else if (v === "dark") r = 800;
|
|
271
|
-
else if (v === "starting") r = 900;
|
|
272
|
-
else if (/^max-/.test(v)) r = 1000 - Object.keys(BREAKPOINTS).indexOf(v.slice(4)) * 10;
|
|
273
|
-
else if (Object.hasOwn(BREAKPOINTS, v) || /^min-/.test(v)) r = 1100 + Object.keys(BREAKPOINTS).indexOf(v.replace(/^min-/, "")) * 10;
|
|
274
|
-
else r = 2000;
|
|
275
|
-
rank += r;
|
|
276
|
-
}
|
|
277
|
-
return rank;
|
|
278
|
-
}
|
|
1
|
+
// File: packages/core/src/variants.mjs
|
|
2
|
+
// On-demand variants for the OmniaCSS engine: hover:, md:, group-hover:, has-[..]:, aria-*, data-*, dark:, cq-md: …
|
|
3
|
+
// A variant maps a list of "branches" ({ sel, at }) around the utility: `sel` is a selector template where `&` stands
|
|
4
|
+
// for the utility class, `at` the list of wrapping at-rules (outermost first). Most variants keep one branch; dark: makes two.
|
|
5
|
+
// Map: breakpoints -> maps -> parseCandidate -> applyVariant -> resolveVariants
|
|
6
|
+
// Author: Serge Cools | Framework: OmniaCSS
|
|
7
|
+
|
|
8
|
+
/** Viewport breakpoints (min-width). sm/md/lg match the native responsive utilities (_responsive.scss). */
|
|
9
|
+
export const BREAKPOINTS = { sm: "36rem", md: "50rem", lg: "64rem", xl: "80rem", "2xl": "96rem" };
|
|
10
|
+
|
|
11
|
+
/** Container-query sizes for cq-<size>: (xs..xl match _containment.scss). */
|
|
12
|
+
export const CONTAINER_SIZES = {
|
|
13
|
+
"3xs": "16rem", "2xs": "18rem", xs: "20rem", sm: "24rem", md: "28rem", lg: "32rem", xl: "36rem",
|
|
14
|
+
"2xl": "42rem", "3xl": "48rem", "4xl": "56rem", "5xl": "64rem", "6xl": "72rem", "7xl": "80rem",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const PSEUDO_CLASS = {
|
|
18
|
+
hover: ":hover", focus: ":focus", "focus-visible": ":focus-visible", "focus-within": ":focus-within", active: ":active",
|
|
19
|
+
visited: ":visited", target: ":target", disabled: ":disabled", enabled: ":enabled", checked: ":checked", indeterminate: ":indeterminate",
|
|
20
|
+
default: ":default", required: ":required", optional: ":optional", valid: ":valid", invalid: ":invalid", "user-valid": ":user-valid",
|
|
21
|
+
"user-invalid": ":user-invalid", "in-range": ":in-range", "out-of-range": ":out-of-range", "read-only": ":read-only",
|
|
22
|
+
"placeholder-shown": ":placeholder-shown", autofill: ":autofill", empty: ":empty",
|
|
23
|
+
first: ":first-child", last: ":last-child", only: ":only-child", odd: ":nth-child(odd)", even: ":nth-child(even)",
|
|
24
|
+
"first-of-type": ":first-of-type", "last-of-type": ":last-of-type", "only-of-type": ":only-of-type",
|
|
25
|
+
open: ":is([open], :popover-open, :open)", "popover-open": ":popover-open", inert: ":is([inert], [inert] *)",
|
|
26
|
+
};
|
|
27
|
+
const STATE_ORDER = ["visited", "target", "open", "popover-open", "default", "checked", "indeterminate", "placeholder-shown", "autofill", "optional", "required", "valid", "invalid", "user-valid", "user-invalid", "in-range", "out-of-range", "read-only", "empty", "focus-within", "hover", "focus", "focus-visible", "active", "enabled", "disabled", "inert"];
|
|
28
|
+
const STRUCT_ORDER = ["first", "last", "only", "odd", "even", "first-of-type", "last-of-type", "only-of-type"];
|
|
29
|
+
|
|
30
|
+
const PSEUDO_ELEMENT = {
|
|
31
|
+
before: "::before", after: "::after", "first-letter": "::first-letter", "first-line": "::first-line", marker: "::marker",
|
|
32
|
+
placeholder: "::placeholder", backdrop: "::backdrop", file: "::file-selector-button", selection: "::selection", "details-content": "::details-content",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const MEDIA = {
|
|
36
|
+
print: "print", "motion-safe": "(prefers-reduced-motion: no-preference)", "motion-reduce": "(prefers-reduced-motion: reduce)",
|
|
37
|
+
portrait: "(orientation: portrait)", landscape: "(orientation: landscape)", "contrast-more": "(prefers-contrast: more)",
|
|
38
|
+
"contrast-less": "(prefers-contrast: less)", "forced-colors": "(forced-colors: active)", "inverted-colors": "(inverted-colors: inverted)",
|
|
39
|
+
"pointer-fine": "(pointer: fine)", "pointer-coarse": "(pointer: coarse)", "pointer-none": "(pointer: none)",
|
|
40
|
+
"any-pointer-fine": "(any-pointer: fine)", "any-pointer-coarse": "(any-pointer: coarse)", "any-pointer-none": "(any-pointer: none)",
|
|
41
|
+
"hover-capable": "(hover: hover)", noscript: "(scripting: none)", "auto-dark": "(prefers-color-scheme: dark)",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Scroll-state container queries (the element must sit inside a `container-scroll-state` ancestor). */
|
|
45
|
+
const SCROLL_STATE = {
|
|
46
|
+
stuck: "not scroll-state(stuck: none)", "stuck-top": "scroll-state(stuck: top)", "stuck-bottom": "scroll-state(stuck: bottom)",
|
|
47
|
+
"stuck-left": "scroll-state(stuck: left)", "stuck-right": "scroll-state(stuck: right)",
|
|
48
|
+
snapped: "not scroll-state(snapped: none)", "snapped-x": "scroll-state(snapped: x)", "snapped-y": "scroll-state(snapped: y)",
|
|
49
|
+
"scrollable-top": "scroll-state(scrollable: top)", "scrollable-bottom": "scroll-state(scrollable: bottom)",
|
|
50
|
+
"scrollable-left": "scroll-state(scrollable: left)", "scrollable-right": "scroll-state(scrollable: right)",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const ARIA = ["busy", "checked", "disabled", "expanded", "hidden", "pressed", "readonly", "required", "selected"];
|
|
54
|
+
|
|
55
|
+
/** Fixed variant names per group (for editor tooling such as the OmniaCSS IntelliSense extension). */
|
|
56
|
+
export const VARIANT_GROUPS = {
|
|
57
|
+
pseudoClass: Object.keys(PSEUDO_CLASS), pseudoElement: Object.keys(PSEUDO_ELEMENT), media: Object.keys(MEDIA),
|
|
58
|
+
scrollState: Object.keys(SCROLL_STATE), aria: ARIA, other: ["dark", "rtl", "ltr", "starting", "*", "**"],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const DARK_EXPLICIT = ':where(.dark, [data-theme="dark"], :root:has(.oc-theme-switch input[value="dark"]:checked))';
|
|
62
|
+
const DARK_SYSTEM = ':where(:root:not([data-theme="light"], .light, :has(.oc-theme-switch input[value="light"]:checked)))';
|
|
63
|
+
|
|
64
|
+
/** Escape a string for use in a class selector. */
|
|
65
|
+
export function esc(s) {
|
|
66
|
+
let out = "";
|
|
67
|
+
for (let i = 0; i < s.length; i++) {
|
|
68
|
+
const ch = s[i];
|
|
69
|
+
if (/[0-9]/.test(ch) && (i === 0 || (i === 1 && s[0] === "-"))) out += "\\3" + ch + " ";
|
|
70
|
+
else if (/[A-Za-z0-9_\-¡-]/.test(ch)) out += ch;
|
|
71
|
+
else out += "\\" + ch;
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Split on `sep` outside of [] and (). */
|
|
77
|
+
export function splitTop(str, sep = ":") {
|
|
78
|
+
const out = [];
|
|
79
|
+
let depth = 0, cur = "";
|
|
80
|
+
for (let i = 0; i < str.length; i++) {
|
|
81
|
+
const ch = str[i];
|
|
82
|
+
if (ch === "\\") { cur += ch + (str[++i] ?? ""); continue; }
|
|
83
|
+
if (ch === "[" || ch === "(") depth++;
|
|
84
|
+
else if (ch === "]" || ch === ")") depth--;
|
|
85
|
+
if (ch === sep && depth === 0) { out.push(cur); cur = ""; } else cur += ch;
|
|
86
|
+
}
|
|
87
|
+
out.push(cur);
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** `[a_b]` -> `a b` (underscore = space, `\_` stays an underscore). */
|
|
92
|
+
const arbitrary = (s) => {
|
|
93
|
+
const m = s.match(/^\[([\s\S]+)\]$/);
|
|
94
|
+
return m ? m[1].replace(/\\_/g, "\u0000").replace(/_/g, " ").replace(/\u0000/g, "_") : null;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const quoteAttr = (s) => {
|
|
98
|
+
const m = s.match(/^([^=~|^$*]+)([~|^$*]?=)(.*)$/);
|
|
99
|
+
if (!m) return s;
|
|
100
|
+
const v = /^["']/.test(m[3]) ? m[3] : `"${m[3]}"`;
|
|
101
|
+
return `${m[1]}${m[2]}${v}`;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** Append a simple selector in front of a trailing pseudo-element: `&::after` + `:hover` -> `&:hover::after`. */
|
|
105
|
+
function addSimple(sel, suffix) {
|
|
106
|
+
const m = sel.match(/^(.*?)(::[a-z-]+)$/s);
|
|
107
|
+
return m ? m[1] + suffix + m[2] : sel + suffix;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse a class token: `md:hover:!bg-x` -> { variants: ["md","hover"], base: "bg-x", important: true }.
|
|
112
|
+
* Returns null when there is nothing to do (no variants and no `!`).
|
|
113
|
+
*/
|
|
114
|
+
export function parseCandidate(token) {
|
|
115
|
+
if (token.length > 120) return null;
|
|
116
|
+
const parts = splitTop(token, ":");
|
|
117
|
+
let base = parts.pop();
|
|
118
|
+
if (!base) return null;
|
|
119
|
+
let important = false;
|
|
120
|
+
if (base.startsWith("!")) { important = true; base = base.slice(1); }
|
|
121
|
+
else if (base.endsWith("!")) { important = true; base = base.slice(0, -1); }
|
|
122
|
+
if (!/^-?[A-Za-z]/.test(base)) return null;
|
|
123
|
+
if (!parts.length && !important) return null;
|
|
124
|
+
if (parts.some((p) => !p)) return null;
|
|
125
|
+
return { token, variants: parts, base, important };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const state = (sel = "&", at = []) => ({ sel, at });
|
|
129
|
+
|
|
130
|
+
/** Selector pattern of a variant that only changes the element's selector ("&:hover"), or null. */
|
|
131
|
+
function innerPattern(name) {
|
|
132
|
+
const r = applyVariant(name, [state()]);
|
|
133
|
+
return r && r.length === 1 && !r[0].at.length ? r[0].sel : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const patternOf = (name) => {
|
|
137
|
+
const a = arbitrary(name);
|
|
138
|
+
if (a !== null) return a.includes("&") ? a : `&${a}`;
|
|
139
|
+
return innerPattern(name);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Apply one variant to a list of branches.
|
|
144
|
+
* @returns {{sel:string, at:string[]}[]|null} null when the variant is unknown
|
|
145
|
+
*/
|
|
146
|
+
export function applyVariant(name, branches) {
|
|
147
|
+
const mapSel = (fn) => branches.map((b) => ({ ...b, sel: fn(b.sel) }));
|
|
148
|
+
const wrap = (at) => branches.map((b) => ({ ...b, at: [...b.at, at] }));
|
|
149
|
+
let m;
|
|
150
|
+
|
|
151
|
+
if (Object.hasOwn(PSEUDO_CLASS, name)) return mapSel((s) => addSimple(s, PSEUDO_CLASS[name]));
|
|
152
|
+
// marker: and selection: also reach the descendants (<ul class="marker:…"> colors the bullets of its <li>s).
|
|
153
|
+
if (name === "marker" || name === "selection") {
|
|
154
|
+
return branches.flatMap((b) => [{ ...b, sel: b.sel + PSEUDO_ELEMENT[name] }, { ...b, sel: `${b.sel} *${PSEUDO_ELEMENT[name]}` }]);
|
|
155
|
+
}
|
|
156
|
+
if (Object.hasOwn(PSEUDO_ELEMENT, name)) return mapSel((s) => s + PSEUDO_ELEMENT[name]);
|
|
157
|
+
if (Object.hasOwn(MEDIA, name)) return wrap(`@media ${MEDIA[name]}`);
|
|
158
|
+
if (Object.hasOwn(SCROLL_STATE, name)) return wrap(`@container ${SCROLL_STATE[name]}`);
|
|
159
|
+
if (name === "starting") return wrap("@starting-style");
|
|
160
|
+
if (name === "rtl" || name === "ltr") return mapSel((s) => addSimple(s, `:where(:dir(${name}), [dir="${name}"], [dir="${name}"] *)`));
|
|
161
|
+
if (name === "dark") {
|
|
162
|
+
return branches.flatMap((b) => [
|
|
163
|
+
{ sel: `${DARK_EXPLICIT} ${b.sel}`, at: b.at },
|
|
164
|
+
{ sel: `${DARK_SYSTEM} ${b.sel}`, at: [...b.at, "@media (prefers-color-scheme: dark)"] },
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
if (name === "*") return mapSel((s) => `${s} > *`);
|
|
168
|
+
if (name === "**") return mapSel((s) => `${s} *`);
|
|
169
|
+
|
|
170
|
+
// viewport: sm, max-sm, min-sm, min-[600px], max-[600px]
|
|
171
|
+
if (Object.hasOwn(BREAKPOINTS, name)) return wrap(`@media (min-width: ${BREAKPOINTS[name]})`);
|
|
172
|
+
if ((m = name.match(/^(min|max)-(.+)$/))) {
|
|
173
|
+
const [, kind, rest] = m;
|
|
174
|
+
const a = arbitrary(rest);
|
|
175
|
+
const v = a ?? BREAKPOINTS[rest];
|
|
176
|
+
if (v) return wrap(kind === "min" ? `@media (min-width: ${v})` : `@media (width < ${v})`);
|
|
177
|
+
}
|
|
178
|
+
// container queries: cq-md, cq-max-md, cq-[400px], cq-md/name (and the short form @md)
|
|
179
|
+
if ((m = name.match(/^(?:cq-|@)(?:(min|max)-)?(.+?)(?:\/([\w-]+))?$/))) {
|
|
180
|
+
const [, kind, size, cname] = m;
|
|
181
|
+
const a = arbitrary(size);
|
|
182
|
+
const v = a ?? CONTAINER_SIZES[size];
|
|
183
|
+
if (v) return wrap(`@container ${cname ? cname + " " : ""}${kind === "max" ? `(width < ${v})` : `(min-width: ${v})`}`);
|
|
184
|
+
}
|
|
185
|
+
// supports
|
|
186
|
+
if ((m = name.match(/^(not-)?supports-(.+)$/))) {
|
|
187
|
+
const a = arbitrary(m[2]);
|
|
188
|
+
const cond = a !== null ? (/^[\w-]+\s*:/.test(a) ? `(${a})` : /^[\w-]+$/.test(a) ? `(${a}: inherit)` : a) : `(${m[2]}: inherit)`;
|
|
189
|
+
return wrap(`@supports ${m[1] ? "not " : ""}${cond}`);
|
|
190
|
+
}
|
|
191
|
+
// data-* / aria-*
|
|
192
|
+
if ((m = name.match(/^data-(.+)$/))) {
|
|
193
|
+
const a = arbitrary(m[1]);
|
|
194
|
+
return mapSel((s) => addSimple(s, a !== null ? `[data-${quoteAttr(a)}]` : `[data-${m[1]}]`));
|
|
195
|
+
}
|
|
196
|
+
if ((m = name.match(/^aria-(.+)$/))) {
|
|
197
|
+
const a = arbitrary(m[1]);
|
|
198
|
+
if (a === null && !ARIA.includes(m[1])) return null;
|
|
199
|
+
return mapSel((s) => addSimple(s, a !== null ? `[aria-${quoteAttr(a)}]` : `[aria-${m[1]}="true"]`));
|
|
200
|
+
}
|
|
201
|
+
// group-* / peer-* (optionally named: group-hover/card)
|
|
202
|
+
if ((m = name.match(/^(group|peer)-(.+?)(?:\/([\w-]+))?$/))) {
|
|
203
|
+
const [, kind, innerName, gname] = m;
|
|
204
|
+
const pattern = patternOf(innerName);
|
|
205
|
+
if (!pattern) return null;
|
|
206
|
+
const marker = `:where(.${kind}${gname ? "\\/" + esc(gname) : ""})`;
|
|
207
|
+
const rel = pattern.replace(/&/g, marker);
|
|
208
|
+
return mapSel((s) => addSimple(s, `:is(${rel} ${kind === "group" ? "*" : "~ *"})`));
|
|
209
|
+
}
|
|
210
|
+
// has-* / not-* / in-*
|
|
211
|
+
if ((m = name.match(/^(has|not|in)-(.+)$/))) {
|
|
212
|
+
const [, kind, innerName] = m;
|
|
213
|
+
const pattern = patternOf(innerName);
|
|
214
|
+
if (!pattern && kind === "not") {
|
|
215
|
+
// not-md, not-print, not-supports-[..]
|
|
216
|
+
const inner = applyVariant(innerName, [state()]);
|
|
217
|
+
if (inner && inner.length === 1 && inner[0].at.length === 1) {
|
|
218
|
+
const at = inner[0].at[0];
|
|
219
|
+
if (/^@media /.test(at)) { const p = at.slice(7); return wrap(`@media not ${p.startsWith("(") ? "all and " : ""}${p}`); }
|
|
220
|
+
if (/^@supports not /.test(at)) return wrap(at.replace("@supports not ", "@supports "));
|
|
221
|
+
if (/^@supports /.test(at)) return wrap(at.replace("@supports ", "@supports not "));
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
if (!pattern || !pattern.startsWith("&")) return null;
|
|
226
|
+
const rest = pattern.slice(1);
|
|
227
|
+
if (kind === "has") return mapSel((s) => addSimple(s, `:has(${rest})`));
|
|
228
|
+
if (kind === "not") return mapSel((s) => addSimple(s, `:not(${rest})`));
|
|
229
|
+
return mapSel((s) => `:where(${rest || "*"}) ${s}`);
|
|
230
|
+
}
|
|
231
|
+
if ((m = name.match(/^nth-(last-)?(of-type-)?(.+)$/))) {
|
|
232
|
+
const a = arbitrary(m[3]);
|
|
233
|
+
const arg = a ?? (/^\d+$/.test(m[3]) ? m[3] : /^(odd|even)$/.test(m[3]) ? m[3] : null);
|
|
234
|
+
if (arg === null) return null;
|
|
235
|
+
return mapSel((s) => addSimple(s, `:nth-${m[1] ? "last-" : ""}${m[2] ? "of-type" : "child"}(${arg})`));
|
|
236
|
+
}
|
|
237
|
+
// arbitrary variant: [&>svg]:, [&_p]:, [@media(min-width:900px)]:
|
|
238
|
+
if ((m = name.match(/^\[([\s\S]+)\]$/))) {
|
|
239
|
+
const body = arbitrary(name);
|
|
240
|
+
if (body.startsWith("@")) return wrap(body.replace(/^@(media|supports|container)\s*/, "@$1 "));
|
|
241
|
+
if (!body.includes("&")) return null;
|
|
242
|
+
return mapSel((s) => body.replace(/&/g, s));
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Apply all variants left to right. Returns branches or null if any variant is unknown. */
|
|
248
|
+
export function resolveVariants(variants) {
|
|
249
|
+
let branches = [state()];
|
|
250
|
+
for (const v of variants) {
|
|
251
|
+
branches = applyVariant(v, branches);
|
|
252
|
+
if (!branches) return null;
|
|
253
|
+
}
|
|
254
|
+
return branches;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Cascade rank of a variant list: states < group/peer < attribute < media < dark < screens < containers. */
|
|
258
|
+
export function rankOf(variants) {
|
|
259
|
+
let rank = 0;
|
|
260
|
+
for (const v of variants) {
|
|
261
|
+
let r;
|
|
262
|
+
if (Object.hasOwn(PSEUDO_ELEMENT, v)) r = 100;
|
|
263
|
+
else if (STRUCT_ORDER.includes(v)) r = 200 + STRUCT_ORDER.indexOf(v);
|
|
264
|
+
else if (Object.hasOwn(PSEUDO_CLASS, v)) r = 300 + STATE_ORDER.indexOf(v);
|
|
265
|
+
else if (/^(group|peer|in)-/.test(v)) r = 400;
|
|
266
|
+
else if (/^(has|not|aria|data|nth)-|^[*\[]/.test(v)) r = 500;
|
|
267
|
+
else if (/^(not-)?supports-/.test(v)) r = 600;
|
|
268
|
+
else if (v === "rtl" || v === "ltr") r = 650;
|
|
269
|
+
else if (Object.hasOwn(MEDIA, v) || Object.hasOwn(SCROLL_STATE, v)) r = 700;
|
|
270
|
+
else if (v === "dark") r = 800;
|
|
271
|
+
else if (v === "starting") r = 900;
|
|
272
|
+
else if (/^max-/.test(v)) r = 1000 - Object.keys(BREAKPOINTS).indexOf(v.slice(4)) * 10;
|
|
273
|
+
else if (Object.hasOwn(BREAKPOINTS, v) || /^min-/.test(v)) r = 1100 + Object.keys(BREAKPOINTS).indexOf(v.replace(/^min-/, "")) * 10;
|
|
274
|
+
else r = 2000;
|
|
275
|
+
rank += r;
|
|
276
|
+
}
|
|
277
|
+
return rank;
|
|
278
|
+
}
|