Implement CSSX Phase 2: native SX style primitives
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Replace Tailwind class strings with native SX expressions: (css :flex :gap-4 :hover:bg-sky-200) instead of :class "flex gap-4 ..." - Add style_dict.py: 516 atoms, variants, breakpoints, keyframes, patterns - Add style_resolver.py: memoized resolver with variant splitting - Add StyleValue type to types.py (frozen dataclass with class_name, declarations, etc.) - Add css and merge-styles primitives to primitives.py - Add defstyle and defkeyframes special forms to evaluator.py and async_eval.py - Integrate StyleValue into html.py and async_eval.py render paths - Add register_generated_rule() to css_registry.py, fix media query selector - Add style dict JSON delivery with localStorage caching to helpers.py - Add client-side css primitive, resolver, and style injection to sx.js Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -63,12 +63,23 @@
|
||||
function RawHTML(html) { this.html = html; }
|
||||
RawHTML.prototype._raw = true;
|
||||
|
||||
/** CSSX StyleValue — generated CSS class with rules. */
|
||||
function StyleValue(className, declarations, mediaRules, pseudoRules, keyframes) {
|
||||
this.className = className;
|
||||
this.declarations = declarations || "";
|
||||
this.mediaRules = mediaRules || [];
|
||||
this.pseudoRules = pseudoRules || [];
|
||||
this.keyframes = keyframes || [];
|
||||
}
|
||||
StyleValue.prototype._styleValue = true;
|
||||
|
||||
function isSym(x) { return x && x._sym === true; }
|
||||
function isKw(x) { return x && x._kw === true; }
|
||||
function isLambda(x) { return x && x._lambda === true; }
|
||||
function isComponent(x) { return x && x._component === true; }
|
||||
function isMacro(x) { return x && x._macro === true; }
|
||||
function isRaw(x) { return x && x._raw === true; }
|
||||
function isStyleValue(x) { return x && x._styleValue === true; }
|
||||
|
||||
// --- Parser ---
|
||||
|
||||
@@ -341,6 +352,227 @@
|
||||
return r;
|
||||
};
|
||||
|
||||
// --- CSSX Style Dictionary + Resolver ---
|
||||
|
||||
var _styleAtoms = {}; // atom → CSS declarations
|
||||
var _pseudoVariants = {}; // variant → CSS pseudo-selector
|
||||
var _responsiveBreakpoints = {}; // variant → media query
|
||||
var _styleKeyframes = {}; // name → @keyframes rule
|
||||
var _arbitraryPatterns = []; // [{re: RegExp, tmpl: string}, ...]
|
||||
var _childSelectorPrefixes = []; // ["space-x-", "space-y-", ...]
|
||||
var _styleCache = {}; // atoms-key → StyleValue
|
||||
var _injectedStyles = {}; // className → true (already in <style>)
|
||||
|
||||
function _loadStyleDict(data) {
|
||||
_styleAtoms = data.a || {};
|
||||
_pseudoVariants = data.v || {};
|
||||
_responsiveBreakpoints = data.b || {};
|
||||
_styleKeyframes = data.k || {};
|
||||
_childSelectorPrefixes = data.c || [];
|
||||
_arbitraryPatterns = [];
|
||||
var pats = data.p || [];
|
||||
for (var i = 0; i < pats.length; i++) {
|
||||
_arbitraryPatterns.push({ re: new RegExp("^" + pats[i][0] + "$"), tmpl: pats[i][1] });
|
||||
}
|
||||
_styleCache = {};
|
||||
}
|
||||
|
||||
function _splitVariant(atom) {
|
||||
// Check responsive prefix first
|
||||
for (var bp in _responsiveBreakpoints) {
|
||||
var prefix = bp + ":";
|
||||
if (atom.indexOf(prefix) === 0) {
|
||||
var rest = atom.substring(prefix.length);
|
||||
for (var pv in _pseudoVariants) {
|
||||
var inner = pv + ":";
|
||||
if (rest.indexOf(inner) === 0) return [bp + ":" + pv, rest.substring(inner.length)];
|
||||
}
|
||||
return [bp, rest];
|
||||
}
|
||||
}
|
||||
for (var pv2 in _pseudoVariants) {
|
||||
var prefix2 = pv2 + ":";
|
||||
if (atom.indexOf(prefix2) === 0) return [pv2, atom.substring(prefix2.length)];
|
||||
}
|
||||
return [null, atom];
|
||||
}
|
||||
|
||||
function _resolveAtom(atom) {
|
||||
var decls = _styleAtoms[atom];
|
||||
if (decls !== undefined) return decls;
|
||||
// Dynamic keyframes: animate-{name} → animation-name:{name}
|
||||
if (atom.indexOf("animate-") === 0) {
|
||||
var kfName = atom.substring(8);
|
||||
if (_styleKeyframes[kfName]) return "animation-name:" + kfName;
|
||||
}
|
||||
for (var i = 0; i < _arbitraryPatterns.length; i++) {
|
||||
var m = atom.match(_arbitraryPatterns[i].re);
|
||||
if (m) {
|
||||
var result = _arbitraryPatterns[i].tmpl;
|
||||
for (var j = 1; j < m.length; j++) result = result.replace("{" + (j - 1) + "}", m[j]);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function _isChildSelectorAtom(atom) {
|
||||
for (var i = 0; i < _childSelectorPrefixes.length; i++) {
|
||||
if (atom.indexOf(_childSelectorPrefixes[i]) === 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** SHA-256 hash (first 6 hex chars) for content-addressed class names. */
|
||||
function _hashStyle(input) {
|
||||
// Simple FNV-1a 32-bit hash — fast, deterministic, good distribution
|
||||
var h = 0x811c9dc5;
|
||||
for (var i = 0; i < input.length; i++) {
|
||||
h ^= input.charCodeAt(i);
|
||||
h = (h * 0x01000193) >>> 0;
|
||||
}
|
||||
return h.toString(16).padStart(8, "0").substring(0, 6);
|
||||
}
|
||||
|
||||
function _resolveStyle(atoms) {
|
||||
var key = atoms.join("\0");
|
||||
if (_styleCache[key]) return _styleCache[key];
|
||||
|
||||
var baseDecls = [], mediaRules = [], pseudoRules = [], kfNeeded = [];
|
||||
for (var i = 0; i < atoms.length; i++) {
|
||||
var a = atoms[i];
|
||||
if (!a) continue;
|
||||
if (a.charAt(0) === ":") a = a.substring(1);
|
||||
|
||||
var parts = _splitVariant(a);
|
||||
var variant = parts[0], base = parts[1];
|
||||
var decls = _resolveAtom(base);
|
||||
if (!decls) continue;
|
||||
|
||||
// Check keyframes
|
||||
if (base.indexOf("animate-") === 0) {
|
||||
var kfName = base.substring(8);
|
||||
if (_styleKeyframes[kfName]) kfNeeded.push([kfName, _styleKeyframes[kfName]]);
|
||||
}
|
||||
|
||||
if (variant === null) {
|
||||
baseDecls.push(decls);
|
||||
} else if (_responsiveBreakpoints[variant]) {
|
||||
mediaRules.push([_responsiveBreakpoints[variant], decls]);
|
||||
} else if (_pseudoVariants[variant]) {
|
||||
pseudoRules.push([_pseudoVariants[variant], decls]);
|
||||
} else {
|
||||
// Compound variant: "sm:hover" → split
|
||||
var vparts = variant.split(":");
|
||||
var mediaPart = null, pseudoPart = null;
|
||||
for (var vi = 0; vi < vparts.length; vi++) {
|
||||
if (_responsiveBreakpoints[vparts[vi]]) mediaPart = _responsiveBreakpoints[vparts[vi]];
|
||||
else if (_pseudoVariants[vparts[vi]]) pseudoPart = _pseudoVariants[vparts[vi]];
|
||||
}
|
||||
if (mediaPart) mediaRules.push([mediaPart, decls]);
|
||||
if (pseudoPart) pseudoRules.push([pseudoPart, decls]);
|
||||
if (!mediaPart && !pseudoPart) baseDecls.push(decls);
|
||||
}
|
||||
}
|
||||
|
||||
// Build hash input
|
||||
var hashInput = baseDecls.join(";");
|
||||
for (var mi = 0; mi < mediaRules.length; mi++) hashInput += "@" + mediaRules[mi][0] + "{" + mediaRules[mi][1] + "}";
|
||||
for (var pi = 0; pi < pseudoRules.length; pi++) hashInput += pseudoRules[pi][0] + "{" + pseudoRules[pi][1] + "}";
|
||||
for (var ki = 0; ki < kfNeeded.length; ki++) hashInput += kfNeeded[ki][1];
|
||||
|
||||
var cn = "sx-" + _hashStyle(hashInput);
|
||||
var sv = new StyleValue(cn, baseDecls.join(";"), mediaRules, pseudoRules, kfNeeded);
|
||||
_styleCache[key] = sv;
|
||||
|
||||
// Inject CSS rules into <style id="sx-css">
|
||||
_injectStyleValue(sv, atoms);
|
||||
return sv;
|
||||
}
|
||||
|
||||
function _injectStyleValue(sv, atoms) {
|
||||
if (_injectedStyles[sv.className]) return;
|
||||
_injectedStyles[sv.className] = true;
|
||||
|
||||
var cssTarget = document.getElementById("sx-css");
|
||||
if (!cssTarget) return;
|
||||
|
||||
var rules = [];
|
||||
if (sv.declarations) {
|
||||
// Check if any atoms need child selectors
|
||||
var hasChild = false;
|
||||
if (atoms) {
|
||||
for (var ai = 0; ai < atoms.length; ai++) {
|
||||
if (_isChildSelectorAtom(atoms[ai])) { hasChild = true; break; }
|
||||
}
|
||||
}
|
||||
if (hasChild) {
|
||||
rules.push("." + sv.className + ">:not(:first-child){" + sv.declarations + "}");
|
||||
} else {
|
||||
rules.push("." + sv.className + "{" + sv.declarations + "}");
|
||||
}
|
||||
}
|
||||
for (var pi = 0; pi < sv.pseudoRules.length; pi++) {
|
||||
var sel = sv.pseudoRules[pi][0], decls = sv.pseudoRules[pi][1];
|
||||
if (sel.indexOf("&") >= 0) {
|
||||
rules.push(sel.replace(/&/g, "." + sv.className) + "{" + decls + "}");
|
||||
} else {
|
||||
rules.push("." + sv.className + sel + "{" + decls + "}");
|
||||
}
|
||||
}
|
||||
for (var mi = 0; mi < sv.mediaRules.length; mi++) {
|
||||
rules.push("@media " + sv.mediaRules[mi][0] + "{." + sv.className + "{" + sv.mediaRules[mi][1] + "}}");
|
||||
}
|
||||
for (var ki = 0; ki < sv.keyframes.length; ki++) {
|
||||
rules.push(sv.keyframes[ki][1]);
|
||||
}
|
||||
cssTarget.textContent += rules.join("");
|
||||
}
|
||||
|
||||
function _mergeStyleValues(styles) {
|
||||
if (styles.length === 1) return styles[0];
|
||||
var allDecls = [], allMedia = [], allPseudo = [], allKf = [];
|
||||
var allAtoms = [];
|
||||
for (var i = 0; i < styles.length; i++) {
|
||||
var sv = styles[i];
|
||||
if (sv.declarations) allDecls.push(sv.declarations);
|
||||
allMedia = allMedia.concat(sv.mediaRules);
|
||||
allPseudo = allPseudo.concat(sv.pseudoRules);
|
||||
allKf = allKf.concat(sv.keyframes);
|
||||
}
|
||||
var hashInput = allDecls.join(";");
|
||||
for (var mi = 0; mi < allMedia.length; mi++) hashInput += "@" + allMedia[mi][0] + "{" + allMedia[mi][1] + "}";
|
||||
for (var pi = 0; pi < allPseudo.length; pi++) hashInput += allPseudo[pi][0] + "{" + allPseudo[pi][1] + "}";
|
||||
for (var ki = 0; ki < allKf.length; ki++) hashInput += allKf[ki][1];
|
||||
var cn = "sx-" + _hashStyle(hashInput);
|
||||
var merged = new StyleValue(cn, allDecls.join(";"), allMedia, allPseudo, allKf);
|
||||
_injectStyleValue(merged, allAtoms);
|
||||
return merged;
|
||||
}
|
||||
|
||||
// css primitive: (css :flex :gap-4 :hover:bg-sky-200) → StyleValue
|
||||
PRIMITIVES["css"] = function () {
|
||||
var atoms = [];
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
var a = arguments[i];
|
||||
if (isNil(a) || a === false) continue;
|
||||
atoms.push(isKw(a) ? a.name : String(a));
|
||||
}
|
||||
if (!atoms.length) return NIL;
|
||||
return _resolveStyle(atoms);
|
||||
};
|
||||
|
||||
// merge-styles: combine multiple StyleValues
|
||||
PRIMITIVES["merge-styles"] = function () {
|
||||
var valid = [];
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
if (isStyleValue(arguments[i])) valid.push(arguments[i]);
|
||||
}
|
||||
if (!valid.length) return NIL;
|
||||
if (valid.length === 1) return valid[0];
|
||||
return _mergeStyleValues(valid);
|
||||
};
|
||||
|
||||
// --- Evaluator ---
|
||||
|
||||
/** Unwrap thunks by re-entering the evaluator until we get an actual value. */
|
||||
@@ -583,6 +815,37 @@
|
||||
return value;
|
||||
};
|
||||
|
||||
SPECIAL_FORMS["defstyle"] = function (expr, env) {
|
||||
var name = expr[1].name;
|
||||
var value = sxEval(expr[2], env);
|
||||
env[name] = value;
|
||||
return value;
|
||||
};
|
||||
|
||||
SPECIAL_FORMS["defkeyframes"] = function (expr, env) {
|
||||
var kfName = expr[1].name;
|
||||
var steps = [];
|
||||
for (var i = 2; i < expr.length; i++) {
|
||||
var step = expr[i];
|
||||
var selector = isSym(step[0]) ? step[0].name : String(step[0]);
|
||||
var body = sxEval(step[1], env);
|
||||
var decls = isStyleValue(body) ? body.declarations : String(body);
|
||||
steps.push(selector + "{" + decls + "}");
|
||||
}
|
||||
var kfRule = "@keyframes " + kfName + "{" + steps.join("") + "}";
|
||||
|
||||
// Register in keyframes dict for animate-{name} lookup
|
||||
_styleKeyframes[kfName] = kfRule;
|
||||
_styleCache = {}; // Clear cache so new keyframes are picked up
|
||||
|
||||
// Build a StyleValue for the animation
|
||||
var cn = "sx-" + _hashStyle(kfRule);
|
||||
var sv = new StyleValue(cn, "animation-name:" + kfName, [], [], [[kfName, kfRule]]);
|
||||
_injectStyleValue(sv, []);
|
||||
env[kfName] = sv;
|
||||
return sv;
|
||||
};
|
||||
|
||||
SPECIAL_FORMS["defcomp"] = function (expr, env) {
|
||||
var nameSym = expr[1];
|
||||
var compName = nameSym.name.replace(/^~/, "");
|
||||
@@ -859,6 +1122,8 @@
|
||||
};
|
||||
|
||||
RENDER_FORMS["define"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); };
|
||||
RENDER_FORMS["defstyle"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); };
|
||||
RENDER_FORMS["defkeyframes"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); };
|
||||
RENDER_FORMS["defcomp"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); };
|
||||
RENDER_FORMS["defmacro"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); };
|
||||
RENDER_FORMS["defhandler"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); };
|
||||
@@ -1044,6 +1309,7 @@
|
||||
? document.createElementNS(SVG_NS, tag)
|
||||
: document.createElement(tag);
|
||||
|
||||
var extraClass = null;
|
||||
var i = 0;
|
||||
while (i < args.length) {
|
||||
var arg = args[i];
|
||||
@@ -1052,6 +1318,11 @@
|
||||
var attrVal = sxEval(args[i + 1], env);
|
||||
i += 2;
|
||||
if (isNil(attrVal) || attrVal === false) continue;
|
||||
// :style StyleValue → convert to class
|
||||
if (attrName === "style" && isStyleValue(attrVal)) {
|
||||
extraClass = attrVal.className;
|
||||
continue;
|
||||
}
|
||||
if (BOOLEAN_ATTRS[attrName]) {
|
||||
if (attrVal) el.setAttribute(attrName, "");
|
||||
} else if (attrVal === true) {
|
||||
@@ -1068,6 +1339,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Merge StyleValue class into element's class attribute
|
||||
if (extraClass) {
|
||||
var existing = el.getAttribute("class");
|
||||
el.setAttribute("class", existing ? existing + " " + extraClass : extraClass);
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
@@ -1361,7 +1638,7 @@
|
||||
},
|
||||
|
||||
// For testing / sx-test.js
|
||||
_types: { NIL: NIL, Symbol: Symbol, Keyword: Keyword, Lambda: Lambda, Component: Component, RawHTML: RawHTML },
|
||||
_types: { NIL: NIL, Symbol: Symbol, Keyword: Keyword, Lambda: Lambda, Component: Component, RawHTML: RawHTML, StyleValue: StyleValue },
|
||||
_eval: sxEval,
|
||||
_expandMacro: expandMacro,
|
||||
_callLambda: function (fn, args, env) { return trampoline(callLambda(fn, args, env)); },
|
||||
@@ -2664,7 +2941,7 @@
|
||||
|
||||
// --- Auto-init in browser ---
|
||||
|
||||
Sx.VERSION = "2026-03-01c-cssx";
|
||||
Sx.VERSION = "2026-03-04-cssx2";
|
||||
|
||||
// CSS class tracking for on-demand CSS delivery
|
||||
var _sxCssHash = ""; // 8-char hex hash from server
|
||||
@@ -2707,10 +2984,79 @@
|
||||
document.cookie = "sx-comp-hash=;path=/;max-age=0;SameSite=Lax";
|
||||
}
|
||||
|
||||
// --- sx-styles-hash cookie helpers ---
|
||||
|
||||
function _setSxStylesCookie(hash) {
|
||||
document.cookie = "sx-styles-hash=" + hash + ";path=/;max-age=31536000;SameSite=Lax";
|
||||
}
|
||||
|
||||
function _clearSxStylesCookie() {
|
||||
document.cookie = "sx-styles-hash=;path=/;max-age=0;SameSite=Lax";
|
||||
}
|
||||
|
||||
function _initStyleDict() {
|
||||
var scripts = document.querySelectorAll('script[type="text/sx-styles"]');
|
||||
for (var i = 0; i < scripts.length; i++) {
|
||||
var s = scripts[i];
|
||||
if (s._sxProcessed) continue;
|
||||
s._sxProcessed = true;
|
||||
|
||||
var text = s.textContent;
|
||||
var hash = s.getAttribute("data-hash");
|
||||
if (!hash) {
|
||||
if (text && text.trim()) {
|
||||
try { _loadStyleDict(JSON.parse(text)); } catch (e) { console.warn("[sx.js] style dict parse error", e); }
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var hasInline = text && text.trim();
|
||||
try {
|
||||
var cachedHash = localStorage.getItem("sx-styles-hash");
|
||||
if (cachedHash === hash) {
|
||||
if (hasInline) {
|
||||
localStorage.setItem("sx-styles-src", text);
|
||||
_loadStyleDict(JSON.parse(text));
|
||||
console.log("[sx.js] styles: downloaded (cookie stale)");
|
||||
} else {
|
||||
var cached = localStorage.getItem("sx-styles-src");
|
||||
if (cached) {
|
||||
_loadStyleDict(JSON.parse(cached));
|
||||
console.log("[sx.js] styles: cached (" + hash + ")");
|
||||
} else {
|
||||
_clearSxStylesCookie();
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (hasInline) {
|
||||
localStorage.setItem("sx-styles-hash", hash);
|
||||
localStorage.setItem("sx-styles-src", text);
|
||||
_loadStyleDict(JSON.parse(text));
|
||||
console.log("[sx.js] styles: downloaded (" + hash + ")");
|
||||
} else {
|
||||
localStorage.removeItem("sx-styles-hash");
|
||||
localStorage.removeItem("sx-styles-src");
|
||||
_clearSxStylesCookie();
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (hasInline) {
|
||||
try { _loadStyleDict(JSON.parse(text)); } catch (e2) { console.warn("[sx.js] style dict parse error", e2); }
|
||||
}
|
||||
}
|
||||
_setSxStylesCookie(hash);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof document !== "undefined") {
|
||||
var init = function () {
|
||||
console.log("[sx.js] v" + Sx.VERSION + " init");
|
||||
_initCssTracking();
|
||||
_initStyleDict();
|
||||
Sx.processScripts();
|
||||
Sx.hydrate();
|
||||
SxEngine.process();
|
||||
|
||||
@@ -22,7 +22,7 @@ from __future__ import annotations
|
||||
import inspect
|
||||
from typing import Any
|
||||
|
||||
from .types import Component, Keyword, Lambda, Macro, NIL, Symbol
|
||||
from .types import Component, Keyword, Lambda, Macro, NIL, StyleValue, Symbol
|
||||
from .evaluator import _expand_macro, EvalError
|
||||
from .primitives import _PRIMITIVES
|
||||
from .primitives_io import IO_PRIMITIVES, RequestContext, execute_io
|
||||
@@ -294,6 +294,16 @@ async def _asf_defcomp(expr, env, ctx):
|
||||
return _sf_defcomp(expr, env)
|
||||
|
||||
|
||||
async def _asf_defstyle(expr, env, ctx):
|
||||
from .evaluator import _sf_defstyle
|
||||
return _sf_defstyle(expr, env)
|
||||
|
||||
|
||||
async def _asf_defkeyframes(expr, env, ctx):
|
||||
from .evaluator import _sf_defkeyframes
|
||||
return _sf_defkeyframes(expr, env)
|
||||
|
||||
|
||||
async def _asf_defmacro(expr, env, ctx):
|
||||
from .evaluator import _sf_defmacro
|
||||
return _sf_defmacro(expr, env)
|
||||
@@ -430,6 +440,8 @@ _ASYNC_SPECIAL_FORMS: dict[str, Any] = {
|
||||
"lambda": _asf_lambda,
|
||||
"fn": _asf_lambda,
|
||||
"define": _asf_define,
|
||||
"defstyle": _asf_defstyle,
|
||||
"defkeyframes": _asf_defkeyframes,
|
||||
"defcomp": _asf_defcomp,
|
||||
"defmacro": _asf_defmacro,
|
||||
"defhandler": _asf_defhandler,
|
||||
@@ -684,6 +696,18 @@ async def _arender_element(
|
||||
children.append(arg)
|
||||
i += 1
|
||||
|
||||
# Handle :style StyleValue — convert to class and register CSS rule
|
||||
style_val = attrs.get("style")
|
||||
if isinstance(style_val, StyleValue):
|
||||
from .css_registry import register_generated_rule
|
||||
register_generated_rule(style_val)
|
||||
existing_class = attrs.get("class")
|
||||
if existing_class and existing_class is not NIL and existing_class is not False:
|
||||
attrs["class"] = f"{existing_class} {style_val.class_name}"
|
||||
else:
|
||||
attrs["class"] = style_val.class_name
|
||||
del attrs["style"]
|
||||
|
||||
class_val = attrs.get("class")
|
||||
if class_val is not None and class_val is not NIL and class_val is not False:
|
||||
collector = css_class_collector.get(None)
|
||||
@@ -897,6 +921,8 @@ _ASYNC_RENDER_FORMS: dict[str, Any] = {
|
||||
"begin": _arsf_begin,
|
||||
"do": _arsf_begin,
|
||||
"define": _arsf_define,
|
||||
"defstyle": _arsf_define,
|
||||
"defkeyframes": _arsf_define,
|
||||
"defcomp": _arsf_define,
|
||||
"defmacro": _arsf_define,
|
||||
"defhandler": _arsf_define,
|
||||
@@ -1125,23 +1151,54 @@ async def _aser_call(
|
||||
"""Serialize ``(name :key val child ...)`` — evaluate args but keep
|
||||
as sx source instead of rendering to HTML."""
|
||||
parts = [name]
|
||||
extra_class: str | None = None # from :style StyleValue conversion
|
||||
i = 0
|
||||
while i < len(args):
|
||||
arg = args[i]
|
||||
if isinstance(arg, Keyword) and i + 1 < len(args):
|
||||
val = await _aser(args[i + 1], env, ctx)
|
||||
if val is not NIL and val is not None:
|
||||
parts.append(f":{arg.name}")
|
||||
parts.append(serialize(val))
|
||||
# :style StyleValue → convert to :class and register CSS
|
||||
if arg.name == "style" and isinstance(val, StyleValue):
|
||||
from .css_registry import register_generated_rule
|
||||
register_generated_rule(val)
|
||||
extra_class = val.class_name
|
||||
else:
|
||||
parts.append(f":{arg.name}")
|
||||
parts.append(serialize(val))
|
||||
i += 2
|
||||
else:
|
||||
result = await _aser(arg, env, ctx)
|
||||
if result is not NIL and result is not None:
|
||||
parts.append(serialize(result))
|
||||
i += 1
|
||||
# If we converted a :style to a class, merge into existing :class or add it
|
||||
if extra_class:
|
||||
_merge_class_into_parts(parts, extra_class)
|
||||
return SxExpr("(" + " ".join(parts) + ")")
|
||||
|
||||
|
||||
def _merge_class_into_parts(parts: list[str], class_name: str) -> None:
|
||||
"""Merge an extra class name into the serialized parts list.
|
||||
|
||||
If :class already exists, append to it. Otherwise add :class.
|
||||
"""
|
||||
for i, p in enumerate(parts):
|
||||
if p == ":class" and i + 1 < len(parts):
|
||||
# Existing :class — append our class
|
||||
existing = parts[i + 1]
|
||||
if existing.startswith('"') and existing.endswith('"'):
|
||||
# Quoted string — insert before closing quote
|
||||
parts[i + 1] = existing[:-1] + " " + class_name + '"'
|
||||
else:
|
||||
# Expression — wrap in (str ...)
|
||||
parts[i + 1] = f'(str {existing} " {class_name}")'
|
||||
return
|
||||
# No existing :class — add one
|
||||
parts.insert(1, f'"{class_name}"')
|
||||
parts.insert(1, ":class")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Serialize-mode special forms
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1347,6 +1404,8 @@ _ASER_FORMS: dict[str, Any] = {
|
||||
"lambda": _assf_lambda,
|
||||
"fn": _assf_lambda,
|
||||
"define": _assf_define,
|
||||
"defstyle": _assf_define,
|
||||
"defkeyframes": _assf_define,
|
||||
"defcomp": _assf_define,
|
||||
"defmacro": _assf_define,
|
||||
"defhandler": _assf_define,
|
||||
|
||||
@@ -147,6 +147,48 @@ def scan_classes_from_sx(source: str) -> set[str]:
|
||||
return classes
|
||||
|
||||
|
||||
def register_generated_rule(style_val: Any) -> None:
|
||||
"""Register a generated StyleValue's CSS rules in the registry.
|
||||
|
||||
This allows generated class names (``sx-a3f2c1``) to flow through
|
||||
the existing ``lookup_rules()`` → ``SX-Css`` delta pipeline.
|
||||
"""
|
||||
from .style_dict import CHILD_SELECTOR_ATOMS
|
||||
cn = style_val.class_name
|
||||
if cn in _REGISTRY:
|
||||
return # already registered
|
||||
|
||||
parts: list[str] = []
|
||||
|
||||
# Base declarations
|
||||
if style_val.declarations:
|
||||
parts.append(f".{cn}{{{style_val.declarations}}}")
|
||||
|
||||
# Pseudo-class rules
|
||||
for sel, decls in style_val.pseudo_rules:
|
||||
if sel.startswith("::"):
|
||||
parts.append(f".{cn}{sel}{{{decls}}}")
|
||||
elif "&" in sel:
|
||||
# group-hover pattern: ":is(.group:hover) &" → .group:hover .sx-abc
|
||||
expanded = sel.replace("&", f".{cn}")
|
||||
parts.append(f"{expanded}{{{decls}}}")
|
||||
else:
|
||||
parts.append(f".{cn}{sel}{{{decls}}}")
|
||||
|
||||
# Media-query rules
|
||||
for query, decls in style_val.media_rules:
|
||||
parts.append(f"@media {query}{{.{cn}{{{decls}}}}}")
|
||||
|
||||
# Keyframes
|
||||
for _name, kf_rule in style_val.keyframes:
|
||||
parts.append(kf_rule)
|
||||
|
||||
rule_text = "".join(parts)
|
||||
order = len(_RULE_ORDER) + 10000 # after all tw.css rules
|
||||
_REGISTRY[cn] = rule_text
|
||||
_RULE_ORDER[cn] = order
|
||||
|
||||
|
||||
def registry_loaded() -> bool:
|
||||
"""True if the registry has been populated."""
|
||||
return bool(_REGISTRY)
|
||||
|
||||
@@ -366,6 +366,78 @@ def _sf_define(expr: list, env: dict) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
def _sf_defstyle(expr: list, env: dict) -> Any:
|
||||
"""``(defstyle card-base (css :rounded-xl :bg-white :shadow))``
|
||||
|
||||
Evaluates body → StyleValue, binds to name in env.
|
||||
"""
|
||||
if len(expr) < 3:
|
||||
raise EvalError("defstyle requires name and body")
|
||||
name_sym = expr[1]
|
||||
if not isinstance(name_sym, Symbol):
|
||||
raise EvalError(f"defstyle name must be symbol, got {type(name_sym).__name__}")
|
||||
value = _trampoline(_eval(expr[2], env))
|
||||
env[name_sym.name] = value
|
||||
return value
|
||||
|
||||
|
||||
def _sf_defkeyframes(expr: list, env: dict) -> Any:
|
||||
"""``(defkeyframes fade-in (from (css :opacity-0)) (to (css :opacity-100)))``
|
||||
|
||||
Builds @keyframes rule from steps, registers it, and binds the animation.
|
||||
"""
|
||||
from .types import StyleValue
|
||||
from .css_registry import register_generated_rule
|
||||
from .style_dict import KEYFRAMES
|
||||
|
||||
if len(expr) < 3:
|
||||
raise EvalError("defkeyframes requires name and at least one step")
|
||||
name_sym = expr[1]
|
||||
if not isinstance(name_sym, Symbol):
|
||||
raise EvalError(f"defkeyframes name must be symbol, got {type(name_sym).__name__}")
|
||||
|
||||
kf_name = name_sym.name
|
||||
|
||||
# Build @keyframes rule from steps
|
||||
steps: list[str] = []
|
||||
for step_expr in expr[2:]:
|
||||
if not isinstance(step_expr, list) or len(step_expr) < 2:
|
||||
raise EvalError("defkeyframes step must be (selector (css ...))")
|
||||
selector = step_expr[0]
|
||||
if isinstance(selector, Symbol):
|
||||
selector = selector.name
|
||||
else:
|
||||
selector = str(selector)
|
||||
body = _trampoline(_eval(step_expr[1], env))
|
||||
if isinstance(body, StyleValue):
|
||||
decls = body.declarations
|
||||
elif isinstance(body, str):
|
||||
decls = body
|
||||
else:
|
||||
raise EvalError(f"defkeyframes step body must be css/string, got {type(body).__name__}")
|
||||
steps.append(f"{selector}{{{decls}}}")
|
||||
|
||||
kf_rule = f"@keyframes {kf_name}{{{' '.join(steps)}}}"
|
||||
|
||||
# Register in KEYFRAMES so animate-{name} works
|
||||
KEYFRAMES[kf_name] = kf_rule
|
||||
# Clear resolver cache so new keyframes are picked up
|
||||
from .style_resolver import _resolve_cached
|
||||
_resolve_cached.cache_clear()
|
||||
|
||||
# Create a StyleValue for the animation property
|
||||
import hashlib
|
||||
h = hashlib.sha256(kf_rule.encode()).hexdigest()[:6]
|
||||
sv = StyleValue(
|
||||
class_name=f"sx-{h}",
|
||||
declarations=f"animation-name:{kf_name}",
|
||||
keyframes=((kf_name, kf_rule),),
|
||||
)
|
||||
register_generated_rule(sv)
|
||||
env[kf_name] = sv
|
||||
return sv
|
||||
|
||||
|
||||
def _sf_defcomp(expr: list, env: dict) -> Component:
|
||||
"""``(defcomp ~name (&key param1 param2 &rest children) body)``"""
|
||||
if len(expr) < 4:
|
||||
@@ -814,6 +886,8 @@ _SPECIAL_FORMS: dict[str, Any] = {
|
||||
"lambda": _sf_lambda,
|
||||
"fn": _sf_lambda,
|
||||
"define": _sf_define,
|
||||
"defstyle": _sf_defstyle,
|
||||
"defkeyframes": _sf_defkeyframes,
|
||||
"defcomp": _sf_defcomp,
|
||||
"defrelation": _sf_defrelation,
|
||||
"begin": _sf_begin,
|
||||
|
||||
@@ -611,6 +611,7 @@ details.group{{overflow:hidden}}details.group>summary{{list-style:none}}details.
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-stone-50 text-stone-900">
|
||||
<script type="text/sx-styles" data-hash="{styles_hash}">{styles_json}</script>
|
||||
<script type="text/sx" data-components data-hash="{component_hash}">{component_defs}</script>
|
||||
<script type="text/sx" data-mount="body">{page_sx}</script>
|
||||
<script src="{asset_url}/scripts/sx.js?v={sx_js_hash}"></script>
|
||||
@@ -681,6 +682,14 @@ def sx_page(ctx: dict, page_sx: str, *,
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Style dictionary for client-side css primitive
|
||||
styles_hash = _get_style_dict_hash()
|
||||
client_styles_hash = _get_sx_styles_cookie()
|
||||
if client_styles_hash and client_styles_hash == styles_hash:
|
||||
styles_json = "" # Client has cached version
|
||||
else:
|
||||
styles_json = _build_style_dict_json()
|
||||
|
||||
return _SX_PAGE_TEMPLATE.format(
|
||||
title=_html_escape(title),
|
||||
asset_url=asset_url,
|
||||
@@ -688,6 +697,8 @@ def sx_page(ctx: dict, page_sx: str, *,
|
||||
csrf=_html_escape(csrf),
|
||||
component_hash=component_hash,
|
||||
component_defs=component_defs,
|
||||
styles_hash=styles_hash,
|
||||
styles_json=styles_json,
|
||||
page_sx=page_sx,
|
||||
sx_css=sx_css,
|
||||
sx_css_classes=sx_css_classes,
|
||||
@@ -697,6 +708,58 @@ def sx_page(ctx: dict, page_sx: str, *,
|
||||
|
||||
|
||||
_SCRIPT_HASH_CACHE: dict[str, str] = {}
|
||||
_STYLE_DICT_JSON: str = ""
|
||||
_STYLE_DICT_HASH: str = ""
|
||||
|
||||
|
||||
def _build_style_dict_json() -> str:
|
||||
"""Build compact JSON style dictionary for client-side css primitive."""
|
||||
global _STYLE_DICT_JSON, _STYLE_DICT_HASH
|
||||
if _STYLE_DICT_JSON:
|
||||
return _STYLE_DICT_JSON
|
||||
|
||||
import json
|
||||
from .style_dict import (
|
||||
STYLE_ATOMS, PSEUDO_VARIANTS, RESPONSIVE_BREAKPOINTS,
|
||||
KEYFRAMES, ARBITRARY_PATTERNS, CHILD_SELECTOR_ATOMS,
|
||||
)
|
||||
|
||||
# Derive child selector prefixes from CHILD_SELECTOR_ATOMS
|
||||
prefixes = set()
|
||||
for atom in CHILD_SELECTOR_ATOMS:
|
||||
# "space-y-4" → "space-y-", "divide-y" → "divide-"
|
||||
for sep in ("space-x-", "space-y-", "divide-x", "divide-y"):
|
||||
if atom.startswith(sep):
|
||||
prefixes.add(sep)
|
||||
break
|
||||
|
||||
data = {
|
||||
"a": STYLE_ATOMS,
|
||||
"v": PSEUDO_VARIANTS,
|
||||
"b": RESPONSIVE_BREAKPOINTS,
|
||||
"k": KEYFRAMES,
|
||||
"p": ARBITRARY_PATTERNS,
|
||||
"c": sorted(prefixes),
|
||||
}
|
||||
_STYLE_DICT_JSON = json.dumps(data, separators=(",", ":"))
|
||||
_STYLE_DICT_HASH = hashlib.md5(_STYLE_DICT_JSON.encode()).hexdigest()[:8]
|
||||
return _STYLE_DICT_JSON
|
||||
|
||||
|
||||
def _get_style_dict_hash() -> str:
|
||||
"""Get the hash of the style dictionary JSON."""
|
||||
if not _STYLE_DICT_HASH:
|
||||
_build_style_dict_json()
|
||||
return _STYLE_DICT_HASH
|
||||
|
||||
|
||||
def _get_sx_styles_cookie() -> str:
|
||||
"""Read the sx-styles-hash cookie from the current request."""
|
||||
try:
|
||||
from quart import request
|
||||
return request.cookies.get("sx-styles-hash", "")
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _script_hash(filename: str) -> str:
|
||||
|
||||
@@ -27,7 +27,7 @@ from __future__ import annotations
|
||||
import contextvars
|
||||
from typing import Any
|
||||
|
||||
from .types import Component, Keyword, Lambda, Macro, NIL, Symbol
|
||||
from .types import Component, Keyword, Lambda, Macro, NIL, StyleValue, Symbol
|
||||
from .evaluator import _eval as _raw_eval, _call_component as _raw_call_component, _expand_macro, _trampoline
|
||||
|
||||
def _eval(expr, env):
|
||||
@@ -479,6 +479,19 @@ def _render_element(tag: str, args: list, env: dict[str, Any]) -> str:
|
||||
children.append(arg)
|
||||
i += 1
|
||||
|
||||
# Handle :style StyleValue — convert to class and register CSS rule
|
||||
style_val = attrs.get("style")
|
||||
if isinstance(style_val, StyleValue):
|
||||
from .css_registry import register_generated_rule
|
||||
register_generated_rule(style_val)
|
||||
# Merge into :class
|
||||
existing_class = attrs.get("class")
|
||||
if existing_class and existing_class is not NIL and existing_class is not False:
|
||||
attrs["class"] = f"{existing_class} {style_val.class_name}"
|
||||
else:
|
||||
attrs["class"] = style_val.class_name
|
||||
del attrs["style"]
|
||||
|
||||
# Collect CSS classes if collector is active
|
||||
class_val = attrs.get("class")
|
||||
if class_val is not None and class_val is not NIL and class_val is not False:
|
||||
|
||||
@@ -336,6 +336,11 @@ def serialize(expr: Any, indent: int = 0, pretty: bool = False) -> str:
|
||||
items.append(serialize(v, indent, pretty))
|
||||
return "{" + " ".join(items) + "}"
|
||||
|
||||
# StyleValue — serialize as class name string
|
||||
from .types import StyleValue
|
||||
if isinstance(expr, StyleValue):
|
||||
return f'"{expr.class_name}"'
|
||||
|
||||
# _RawHTML — pre-rendered HTML; wrap as (raw! "...") for SX wire format
|
||||
from .html import _RawHTML
|
||||
if isinstance(expr, _RawHTML):
|
||||
|
||||
@@ -573,3 +573,41 @@ def prim_route_prefix() -> str:
|
||||
"""``(route-prefix)`` → service URL prefix for dev/prod routing."""
|
||||
from shared.utils import route_prefix
|
||||
return route_prefix()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Style primitives
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@register_primitive("css")
|
||||
def prim_css(*args: Any) -> Any:
|
||||
"""``(css :flex :gap-4 :hover:bg-sky-200)`` → StyleValue.
|
||||
|
||||
Accepts keyword atoms (strings without colon prefix) and runtime
|
||||
strings. Returns a StyleValue with a content-addressed class name
|
||||
and all resolved CSS declarations.
|
||||
"""
|
||||
from .style_resolver import resolve_style
|
||||
atoms = tuple(
|
||||
(a.name if isinstance(a, Keyword) else str(a))
|
||||
for a in args if a is not None and a is not NIL and a is not False
|
||||
)
|
||||
if not atoms:
|
||||
return NIL
|
||||
return resolve_style(atoms)
|
||||
|
||||
|
||||
@register_primitive("merge-styles")
|
||||
def prim_merge_styles(*styles: Any) -> Any:
|
||||
"""``(merge-styles style1 style2)`` → merged StyleValue.
|
||||
|
||||
Merges multiple StyleValues; later declarations win.
|
||||
"""
|
||||
from .types import StyleValue
|
||||
from .style_resolver import merge_styles
|
||||
valid = [s for s in styles if isinstance(s, StyleValue)]
|
||||
if not valid:
|
||||
return NIL
|
||||
if len(valid) == 1:
|
||||
return valid[0]
|
||||
return merge_styles(valid)
|
||||
|
||||
735
shared/sx/style_dict.py
Normal file
735
shared/sx/style_dict.py
Normal file
@@ -0,0 +1,735 @@
|
||||
"""
|
||||
Style dictionary — maps keyword atoms to CSS declarations.
|
||||
|
||||
Pure data. Each key is a Tailwind-compatible class name (used as an sx keyword
|
||||
atom in ``(css :flex :gap-4 :p-2)``), and each value is the CSS declaration(s)
|
||||
that class produces. Declarations are self-contained — no ``--tw-*`` custom
|
||||
properties needed.
|
||||
|
||||
Generated from the codebase's tw.css via ``css_registry.py`` then simplified
|
||||
to remove Tailwind v3 variable indirection.
|
||||
|
||||
Used by:
|
||||
- ``style_resolver.py`` (server) — resolves ``(css ...)`` to StyleValue
|
||||
- ``sx.js`` (client) — same resolution, cached in localStorage
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Base atoms — keyword → CSS declarations
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# ~466 atoms covering all utilities used across the codebase.
|
||||
# Variants (hover:*, sm:*, focus:*, etc.) are NOT stored here — the
|
||||
# resolver splits "hover:bg-sky-200" into variant="hover" + atom="bg-sky-200"
|
||||
# and wraps the declaration in the appropriate pseudo/media rule.
|
||||
|
||||
STYLE_ATOMS: dict[str, str] = {
|
||||
# ── Display ──────────────────────────────────────────────────────────
|
||||
"block": "display:block",
|
||||
"inline-block": "display:inline-block",
|
||||
"inline": "display:inline",
|
||||
"flex": "display:flex",
|
||||
"inline-flex": "display:inline-flex",
|
||||
"table": "display:table",
|
||||
"grid": "display:grid",
|
||||
"contents": "display:contents",
|
||||
"hidden": "display:none",
|
||||
|
||||
# ── Position ─────────────────────────────────────────────────────────
|
||||
"static": "position:static",
|
||||
"fixed": "position:fixed",
|
||||
"absolute": "position:absolute",
|
||||
"relative": "position:relative",
|
||||
"inset-0": "inset:0",
|
||||
"top-0": "top:0",
|
||||
"top-1/2": "top:50%",
|
||||
"top-2": "top:.5rem",
|
||||
"top-20": "top:5rem",
|
||||
"top-[8px]": "top:8px",
|
||||
"top-full": "top:100%",
|
||||
"right-2": "right:.5rem",
|
||||
"right-[8px]": "right:8px",
|
||||
"bottom-full": "bottom:100%",
|
||||
"left-1/2": "left:50%",
|
||||
"left-2": "left:.5rem",
|
||||
"-right-2": "right:-.5rem",
|
||||
"-right-3": "right:-.75rem",
|
||||
"-top-1.5": "top:-.375rem",
|
||||
"-top-2": "top:-.5rem",
|
||||
|
||||
# ── Z-Index ──────────────────────────────────────────────────────────
|
||||
"z-10": "z-index:10",
|
||||
"z-40": "z-index:40",
|
||||
"z-50": "z-index:50",
|
||||
|
||||
# ── Grid ─────────────────────────────────────────────────────────────
|
||||
"grid-cols-1": "grid-template-columns:repeat(1,minmax(0,1fr))",
|
||||
"grid-cols-2": "grid-template-columns:repeat(2,minmax(0,1fr))",
|
||||
"grid-cols-3": "grid-template-columns:repeat(3,minmax(0,1fr))",
|
||||
"grid-cols-4": "grid-template-columns:repeat(4,minmax(0,1fr))",
|
||||
"grid-cols-5": "grid-template-columns:repeat(5,minmax(0,1fr))",
|
||||
"grid-cols-6": "grid-template-columns:repeat(6,minmax(0,1fr))",
|
||||
"grid-cols-7": "grid-template-columns:repeat(7,minmax(0,1fr))",
|
||||
"grid-cols-12": "grid-template-columns:repeat(12,minmax(0,1fr))",
|
||||
"col-span-2": "grid-column:span 2/span 2",
|
||||
"col-span-3": "grid-column:span 3/span 3",
|
||||
"col-span-4": "grid-column:span 4/span 4",
|
||||
"col-span-5": "grid-column:span 5/span 5",
|
||||
"col-span-12": "grid-column:span 12/span 12",
|
||||
"col-span-full": "grid-column:1/-1",
|
||||
|
||||
# ── Flexbox ──────────────────────────────────────────────────────────
|
||||
"flex-row": "flex-direction:row",
|
||||
"flex-col": "flex-direction:column",
|
||||
"flex-wrap": "flex-wrap:wrap",
|
||||
"flex-1": "flex:1 1 0%",
|
||||
"flex-shrink-0": "flex-shrink:0",
|
||||
"shrink-0": "flex-shrink:0",
|
||||
"flex-shrink": "flex-shrink:1",
|
||||
|
||||
# ── Alignment ────────────────────────────────────────────────────────
|
||||
"items-start": "align-items:flex-start",
|
||||
"items-end": "align-items:flex-end",
|
||||
"items-center": "align-items:center",
|
||||
"items-baseline": "align-items:baseline",
|
||||
"justify-start": "justify-content:flex-start",
|
||||
"justify-end": "justify-content:flex-end",
|
||||
"justify-center": "justify-content:center",
|
||||
"justify-between": "justify-content:space-between",
|
||||
"self-start": "align-self:flex-start",
|
||||
"self-center": "align-self:center",
|
||||
"place-items-center": "place-items:center",
|
||||
|
||||
# ── Gap ───────────────────────────────────────────────────────────────
|
||||
"gap-px": "gap:1px",
|
||||
"gap-0.5": "gap:.125rem",
|
||||
"gap-1": "gap:.25rem",
|
||||
"gap-1.5": "gap:.375rem",
|
||||
"gap-2": "gap:.5rem",
|
||||
"gap-3": "gap:.75rem",
|
||||
"gap-4": "gap:1rem",
|
||||
"gap-5": "gap:1.25rem",
|
||||
"gap-6": "gap:1.5rem",
|
||||
"gap-8": "gap:2rem",
|
||||
"gap-[4px]": "gap:4px",
|
||||
"gap-[8px]": "gap:8px",
|
||||
"gap-[16px]": "gap:16px",
|
||||
"gap-x-3": "column-gap:.75rem",
|
||||
"gap-y-1": "row-gap:.25rem",
|
||||
|
||||
# ── Margin ───────────────────────────────────────────────────────────
|
||||
"m-0": "margin:0",
|
||||
"m-2": "margin:.5rem",
|
||||
"mx-1": "margin-left:.25rem;margin-right:.25rem",
|
||||
"mx-2": "margin-left:.5rem;margin-right:.5rem",
|
||||
"mx-4": "margin-left:1rem;margin-right:1rem",
|
||||
"mx-auto": "margin-left:auto;margin-right:auto",
|
||||
"my-3": "margin-top:.75rem;margin-bottom:.75rem",
|
||||
"-mb-px": "margin-bottom:-1px",
|
||||
"mb-1": "margin-bottom:.25rem",
|
||||
"mb-2": "margin-bottom:.5rem",
|
||||
"mb-3": "margin-bottom:.75rem",
|
||||
"mb-4": "margin-bottom:1rem",
|
||||
"mb-6": "margin-bottom:1.5rem",
|
||||
"mb-8": "margin-bottom:2rem",
|
||||
"mb-12": "margin-bottom:3rem",
|
||||
"mb-[8px]": "margin-bottom:8px",
|
||||
"mb-[24px]": "margin-bottom:24px",
|
||||
"ml-1": "margin-left:.25rem",
|
||||
"ml-2": "margin-left:.5rem",
|
||||
"ml-4": "margin-left:1rem",
|
||||
"ml-auto": "margin-left:auto",
|
||||
"mr-1": "margin-right:.25rem",
|
||||
"mr-2": "margin-right:.5rem",
|
||||
"mr-3": "margin-right:.75rem",
|
||||
"mt-0.5": "margin-top:.125rem",
|
||||
"mt-1": "margin-top:.25rem",
|
||||
"mt-2": "margin-top:.5rem",
|
||||
"mt-3": "margin-top:.75rem",
|
||||
"mt-4": "margin-top:1rem",
|
||||
"mt-6": "margin-top:1.5rem",
|
||||
"mt-8": "margin-top:2rem",
|
||||
"mt-[8px]": "margin-top:8px",
|
||||
"mt-[16px]": "margin-top:16px",
|
||||
"mt-[32px]": "margin-top:32px",
|
||||
|
||||
# ── Padding ──────────────────────────────────────────────────────────
|
||||
"p-0": "padding:0",
|
||||
"p-1": "padding:.25rem",
|
||||
"p-1.5": "padding:.375rem",
|
||||
"p-2": "padding:.5rem",
|
||||
"p-3": "padding:.75rem",
|
||||
"p-4": "padding:1rem",
|
||||
"p-5": "padding:1.25rem",
|
||||
"p-6": "padding:1.5rem",
|
||||
"p-8": "padding:2rem",
|
||||
"px-1": "padding-left:.25rem;padding-right:.25rem",
|
||||
"px-1.5": "padding-left:.375rem;padding-right:.375rem",
|
||||
"px-2": "padding-left:.5rem;padding-right:.5rem",
|
||||
"px-2.5": "padding-left:.625rem;padding-right:.625rem",
|
||||
"px-3": "padding-left:.75rem;padding-right:.75rem",
|
||||
"px-4": "padding-left:1rem;padding-right:1rem",
|
||||
"px-6": "padding-left:1.5rem;padding-right:1.5rem",
|
||||
"px-[8px]": "padding-left:8px;padding-right:8px",
|
||||
"px-[12px]": "padding-left:12px;padding-right:12px",
|
||||
"px-[16px]": "padding-left:16px;padding-right:16px",
|
||||
"px-[20px]": "padding-left:20px;padding-right:20px",
|
||||
"py-0.5": "padding-top:.125rem;padding-bottom:.125rem",
|
||||
"py-1": "padding-top:.25rem;padding-bottom:.25rem",
|
||||
"py-1.5": "padding-top:.375rem;padding-bottom:.375rem",
|
||||
"py-2": "padding-top:.5rem;padding-bottom:.5rem",
|
||||
"py-3": "padding-top:.75rem;padding-bottom:.75rem",
|
||||
"py-4": "padding-top:1rem;padding-bottom:1rem",
|
||||
"py-6": "padding-top:1.5rem;padding-bottom:1.5rem",
|
||||
"py-8": "padding-top:2rem;padding-bottom:2rem",
|
||||
"py-12": "padding-top:3rem;padding-bottom:3rem",
|
||||
"py-16": "padding-top:4rem;padding-bottom:4rem",
|
||||
"py-[6px]": "padding-top:6px;padding-bottom:6px",
|
||||
"py-[12px]": "padding-top:12px;padding-bottom:12px",
|
||||
"pb-1": "padding-bottom:.25rem",
|
||||
"pb-2": "padding-bottom:.5rem",
|
||||
"pb-3": "padding-bottom:.75rem",
|
||||
"pb-4": "padding-bottom:1rem",
|
||||
"pb-6": "padding-bottom:1.5rem",
|
||||
"pb-8": "padding-bottom:2rem",
|
||||
"pb-[48px]": "padding-bottom:48px",
|
||||
"pl-2": "padding-left:.5rem",
|
||||
"pl-5": "padding-left:1.25rem",
|
||||
"pl-6": "padding-left:1.5rem",
|
||||
"pr-1": "padding-right:.25rem",
|
||||
"pr-2": "padding-right:.5rem",
|
||||
"pr-4": "padding-right:1rem",
|
||||
"pt-2": "padding-top:.5rem",
|
||||
"pt-3": "padding-top:.75rem",
|
||||
"pt-4": "padding-top:1rem",
|
||||
"pt-[16px]": "padding-top:16px",
|
||||
|
||||
# ── Width ────────────────────────────────────────────────────────────
|
||||
"w-1": "width:.25rem",
|
||||
"w-2": "width:.5rem",
|
||||
"w-4": "width:1rem",
|
||||
"w-5": "width:1.25rem",
|
||||
"w-6": "width:1.5rem",
|
||||
"w-8": "width:2rem",
|
||||
"w-10": "width:2.5rem",
|
||||
"w-11": "width:2.75rem",
|
||||
"w-12": "width:3rem",
|
||||
"w-16": "width:4rem",
|
||||
"w-20": "width:5rem",
|
||||
"w-24": "width:6rem",
|
||||
"w-28": "width:7rem",
|
||||
"w-48": "width:12rem",
|
||||
"w-1/2": "width:50%",
|
||||
"w-1/3": "width:33.333333%",
|
||||
"w-1/4": "width:25%",
|
||||
"w-1/6": "width:16.666667%",
|
||||
"w-2/6": "width:33.333333%",
|
||||
"w-3/4": "width:75%",
|
||||
"w-full": "width:100%",
|
||||
"w-auto": "width:auto",
|
||||
"w-[1em]": "width:1em",
|
||||
"w-[32px]": "width:32px",
|
||||
|
||||
# ── Height ───────────────────────────────────────────────────────────
|
||||
"h-2": "height:.5rem",
|
||||
"h-4": "height:1rem",
|
||||
"h-5": "height:1.25rem",
|
||||
"h-6": "height:1.5rem",
|
||||
"h-8": "height:2rem",
|
||||
"h-10": "height:2.5rem",
|
||||
"h-12": "height:3rem",
|
||||
"h-14": "height:3.5rem",
|
||||
"h-16": "height:4rem",
|
||||
"h-24": "height:6rem",
|
||||
"h-28": "height:7rem",
|
||||
"h-48": "height:12rem",
|
||||
"h-64": "height:16rem",
|
||||
"h-full": "height:100%",
|
||||
"h-[1em]": "height:1em",
|
||||
"h-[30vh]": "height:30vh",
|
||||
"h-[32px]": "height:32px",
|
||||
"h-[60vh]": "height:60vh",
|
||||
|
||||
# ── Min/Max Dimensions ───────────────────────────────────────────────
|
||||
"min-w-0": "min-width:0",
|
||||
"min-w-full": "min-width:100%",
|
||||
"min-w-[1.25rem]": "min-width:1.25rem",
|
||||
"min-w-[180px]": "min-width:180px",
|
||||
"min-h-0": "min-height:0",
|
||||
"min-h-20": "min-height:5rem",
|
||||
"min-h-[3rem]": "min-height:3rem",
|
||||
"min-h-[50vh]": "min-height:50vh",
|
||||
"max-w-xs": "max-width:20rem",
|
||||
"max-w-md": "max-width:28rem",
|
||||
"max-w-lg": "max-width:32rem",
|
||||
"max-w-2xl": "max-width:42rem",
|
||||
"max-w-3xl": "max-width:48rem",
|
||||
"max-w-4xl": "max-width:56rem",
|
||||
"max-w-full": "max-width:100%",
|
||||
"max-w-none": "max-width:none",
|
||||
"max-w-screen-2xl": "max-width:1536px",
|
||||
"max-w-[360px]": "max-width:360px",
|
||||
"max-w-[768px]": "max-width:768px",
|
||||
"max-h-64": "max-height:16rem",
|
||||
"max-h-96": "max-height:24rem",
|
||||
"max-h-none": "max-height:none",
|
||||
"max-h-[448px]": "max-height:448px",
|
||||
"max-h-[50vh]": "max-height:50vh",
|
||||
|
||||
# ── Typography ───────────────────────────────────────────────────────
|
||||
"text-xs": "font-size:.75rem;line-height:1rem",
|
||||
"text-sm": "font-size:.875rem;line-height:1.25rem",
|
||||
"text-base": "font-size:1rem;line-height:1.5rem",
|
||||
"text-lg": "font-size:1.125rem;line-height:1.75rem",
|
||||
"text-xl": "font-size:1.25rem;line-height:1.75rem",
|
||||
"text-2xl": "font-size:1.5rem;line-height:2rem",
|
||||
"text-3xl": "font-size:1.875rem;line-height:2.25rem",
|
||||
"text-4xl": "font-size:2.25rem;line-height:2.5rem",
|
||||
"text-5xl": "font-size:3rem;line-height:1",
|
||||
"text-6xl": "font-size:3.75rem;line-height:1",
|
||||
"text-8xl": "font-size:6rem;line-height:1",
|
||||
"text-[8px]": "font-size:8px",
|
||||
"text-[9px]": "font-size:9px",
|
||||
"text-[10px]": "font-size:10px",
|
||||
"text-[11px]": "font-size:11px",
|
||||
"text-[13px]": "font-size:13px",
|
||||
"text-[14px]": "font-size:14px",
|
||||
"text-[16px]": "font-size:16px",
|
||||
"text-[18px]": "font-size:18px",
|
||||
"text-[36px]": "font-size:36px",
|
||||
"text-[40px]": "font-size:40px",
|
||||
"text-[0.6rem]": "font-size:.6rem",
|
||||
"text-[0.65rem]": "font-size:.65rem",
|
||||
"text-[0.7rem]": "font-size:.7rem",
|
||||
"font-normal": "font-weight:400",
|
||||
"font-medium": "font-weight:500",
|
||||
"font-semibold": "font-weight:600",
|
||||
"font-bold": "font-weight:700",
|
||||
"font-mono": "font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace",
|
||||
"italic": "font-style:italic",
|
||||
"uppercase": "text-transform:uppercase",
|
||||
"capitalize": "text-transform:capitalize",
|
||||
"tabular-nums": "font-variant-numeric:tabular-nums",
|
||||
"leading-none": "line-height:1",
|
||||
"leading-tight": "line-height:1.25",
|
||||
"leading-snug": "line-height:1.375",
|
||||
"leading-relaxed": "line-height:1.625",
|
||||
"tracking-tight": "letter-spacing:-.025em",
|
||||
"tracking-wide": "letter-spacing:.025em",
|
||||
"tracking-widest": "letter-spacing:.1em",
|
||||
"text-left": "text-align:left",
|
||||
"text-center": "text-align:center",
|
||||
"text-right": "text-align:right",
|
||||
"align-top": "vertical-align:top",
|
||||
|
||||
# ── Text Colors ──────────────────────────────────────────────────────
|
||||
"text-white": "color:rgb(255 255 255)",
|
||||
"text-white/80": "color:rgba(255,255,255,.8)",
|
||||
"text-black": "color:rgb(0 0 0)",
|
||||
"text-stone-300": "color:rgb(214 211 209)",
|
||||
"text-stone-400": "color:rgb(168 162 158)",
|
||||
"text-stone-500": "color:rgb(120 113 108)",
|
||||
"text-stone-600": "color:rgb(87 83 78)",
|
||||
"text-stone-700": "color:rgb(68 64 60)",
|
||||
"text-stone-800": "color:rgb(41 37 36)",
|
||||
"text-stone-900": "color:rgb(28 25 23)",
|
||||
"text-slate-400": "color:rgb(148 163 184)",
|
||||
"text-gray-500": "color:rgb(107 114 128)",
|
||||
"text-gray-600": "color:rgb(75 85 99)",
|
||||
"text-red-500": "color:rgb(239 68 68)",
|
||||
"text-red-600": "color:rgb(220 38 38)",
|
||||
"text-red-700": "color:rgb(185 28 28)",
|
||||
"text-red-800": "color:rgb(153 27 27)",
|
||||
"text-rose-500": "color:rgb(244 63 94)",
|
||||
"text-rose-600": "color:rgb(225 29 72)",
|
||||
"text-rose-700": "color:rgb(190 18 60)",
|
||||
"text-rose-800/80": "color:rgba(159,18,57,.8)",
|
||||
"text-rose-900": "color:rgb(136 19 55)",
|
||||
"text-orange-600": "color:rgb(234 88 12)",
|
||||
"text-amber-500": "color:rgb(245 158 11)",
|
||||
"text-amber-600": "color:rgb(217 119 6)",
|
||||
"text-amber-700": "color:rgb(180 83 9)",
|
||||
"text-amber-800": "color:rgb(146 64 14)",
|
||||
"text-yellow-700": "color:rgb(161 98 7)",
|
||||
"text-green-600": "color:rgb(22 163 74)",
|
||||
"text-green-800": "color:rgb(22 101 52)",
|
||||
"text-emerald-500": "color:rgb(16 185 129)",
|
||||
"text-emerald-600": "color:rgb(5 150 105)",
|
||||
"text-emerald-700": "color:rgb(4 120 87)",
|
||||
"text-emerald-800": "color:rgb(6 95 70)",
|
||||
"text-emerald-900": "color:rgb(6 78 59)",
|
||||
"text-sky-600": "color:rgb(2 132 199)",
|
||||
"text-sky-700": "color:rgb(3 105 161)",
|
||||
"text-sky-800": "color:rgb(7 89 133)",
|
||||
"text-blue-500": "color:rgb(59 130 246)",
|
||||
"text-blue-600": "color:rgb(37 99 235)",
|
||||
"text-blue-700": "color:rgb(29 78 216)",
|
||||
"text-blue-800": "color:rgb(30 64 175)",
|
||||
"text-purple-600": "color:rgb(147 51 234)",
|
||||
"text-violet-600": "color:rgb(124 58 237)",
|
||||
"text-violet-700": "color:rgb(109 40 217)",
|
||||
"text-violet-800": "color:rgb(91 33 182)",
|
||||
|
||||
# ── Background Colors ────────────────────────────────────────────────
|
||||
"bg-transparent": "background-color:transparent",
|
||||
"bg-white": "background-color:rgb(255 255 255)",
|
||||
"bg-white/60": "background-color:rgba(255,255,255,.6)",
|
||||
"bg-white/70": "background-color:rgba(255,255,255,.7)",
|
||||
"bg-white/80": "background-color:rgba(255,255,255,.8)",
|
||||
"bg-white/90": "background-color:rgba(255,255,255,.9)",
|
||||
"bg-black": "background-color:rgb(0 0 0)",
|
||||
"bg-black/50": "background-color:rgba(0,0,0,.5)",
|
||||
"bg-stone-50": "background-color:rgb(250 250 249)",
|
||||
"bg-stone-100": "background-color:rgb(245 245 244)",
|
||||
"bg-stone-200": "background-color:rgb(231 229 228)",
|
||||
"bg-stone-300": "background-color:rgb(214 211 209)",
|
||||
"bg-stone-400": "background-color:rgb(168 162 158)",
|
||||
"bg-stone-500": "background-color:rgb(120 113 108)",
|
||||
"bg-stone-600": "background-color:rgb(87 83 78)",
|
||||
"bg-stone-700": "background-color:rgb(68 64 60)",
|
||||
"bg-stone-800": "background-color:rgb(41 37 36)",
|
||||
"bg-stone-900": "background-color:rgb(28 25 23)",
|
||||
"bg-slate-100": "background-color:rgb(241 245 249)",
|
||||
"bg-slate-200": "background-color:rgb(226 232 240)",
|
||||
"bg-gray-100": "background-color:rgb(243 244 246)",
|
||||
"bg-red-50": "background-color:rgb(254 242 242)",
|
||||
"bg-red-100": "background-color:rgb(254 226 226)",
|
||||
"bg-red-200": "background-color:rgb(254 202 202)",
|
||||
"bg-red-500": "background-color:rgb(239 68 68)",
|
||||
"bg-red-600": "background-color:rgb(220 38 38)",
|
||||
"bg-rose-50": "background-color:rgb(255 241 242)",
|
||||
"bg-rose-50/80": "background-color:rgba(255,241,242,.8)",
|
||||
"bg-orange-100": "background-color:rgb(255 237 213)",
|
||||
"bg-amber-50": "background-color:rgb(255 251 235)",
|
||||
"bg-amber-50/60": "background-color:rgba(255,251,235,.6)",
|
||||
"bg-amber-100": "background-color:rgb(254 243 199)",
|
||||
"bg-amber-500": "background-color:rgb(245 158 11)",
|
||||
"bg-amber-600": "background-color:rgb(217 119 6)",
|
||||
"bg-yellow-50": "background-color:rgb(254 252 232)",
|
||||
"bg-yellow-100": "background-color:rgb(254 249 195)",
|
||||
"bg-yellow-200": "background-color:rgb(254 240 138)",
|
||||
"bg-yellow-300": "background-color:rgb(253 224 71)",
|
||||
"bg-green-50": "background-color:rgb(240 253 244)",
|
||||
"bg-green-100": "background-color:rgb(220 252 231)",
|
||||
"bg-emerald-50": "background-color:rgb(236 253 245)",
|
||||
"bg-emerald-50/80": "background-color:rgba(236,253,245,.8)",
|
||||
"bg-emerald-100": "background-color:rgb(209 250 229)",
|
||||
"bg-emerald-200": "background-color:rgb(167 243 208)",
|
||||
"bg-emerald-500": "background-color:rgb(16 185 129)",
|
||||
"bg-emerald-600": "background-color:rgb(5 150 105)",
|
||||
"bg-sky-100": "background-color:rgb(224 242 254)",
|
||||
"bg-sky-200": "background-color:rgb(186 230 253)",
|
||||
"bg-sky-300": "background-color:rgb(125 211 252)",
|
||||
"bg-sky-400": "background-color:rgb(56 189 248)",
|
||||
"bg-sky-500": "background-color:rgb(14 165 233)",
|
||||
"bg-blue-50": "background-color:rgb(239 246 255)",
|
||||
"bg-blue-100": "background-color:rgb(219 234 254)",
|
||||
"bg-blue-600": "background-color:rgb(37 99 235)",
|
||||
"bg-purple-600": "background-color:rgb(147 51 234)",
|
||||
"bg-violet-50": "background-color:rgb(245 243 255)",
|
||||
"bg-violet-100": "background-color:rgb(237 233 254)",
|
||||
"bg-violet-200": "background-color:rgb(221 214 254)",
|
||||
"bg-violet-300": "background-color:rgb(196 181 253)",
|
||||
"bg-violet-400": "background-color:rgb(167 139 250)",
|
||||
"bg-violet-500": "background-color:rgb(139 92 246)",
|
||||
"bg-violet-600": "background-color:rgb(124 58 237)",
|
||||
|
||||
# ── Border ───────────────────────────────────────────────────────────
|
||||
"border": "border-width:1px",
|
||||
"border-2": "border-width:2px",
|
||||
"border-4": "border-width:4px",
|
||||
"border-t": "border-top-width:1px",
|
||||
"border-t-0": "border-top-width:0",
|
||||
"border-b": "border-bottom-width:1px",
|
||||
"border-b-2": "border-bottom-width:2px",
|
||||
"border-r": "border-right-width:1px",
|
||||
"border-l-4": "border-left-width:4px",
|
||||
"border-dashed": "border-style:dashed",
|
||||
"border-none": "border-style:none",
|
||||
"border-transparent": "border-color:transparent",
|
||||
"border-white": "border-color:rgb(255 255 255)",
|
||||
"border-white/30": "border-color:rgba(255,255,255,.3)",
|
||||
"border-stone-100": "border-color:rgb(245 245 244)",
|
||||
"border-stone-200": "border-color:rgb(231 229 228)",
|
||||
"border-stone-300": "border-color:rgb(214 211 209)",
|
||||
"border-stone-700": "border-color:rgb(68 64 60)",
|
||||
"border-red-200": "border-color:rgb(254 202 202)",
|
||||
"border-red-300": "border-color:rgb(252 165 165)",
|
||||
"border-rose-200": "border-color:rgb(254 205 211)",
|
||||
"border-rose-300": "border-color:rgb(253 164 175)",
|
||||
"border-amber-200": "border-color:rgb(253 230 138)",
|
||||
"border-amber-300": "border-color:rgb(252 211 77)",
|
||||
"border-yellow-200": "border-color:rgb(254 240 138)",
|
||||
"border-green-300": "border-color:rgb(134 239 172)",
|
||||
"border-emerald-100": "border-color:rgb(209 250 229)",
|
||||
"border-emerald-200": "border-color:rgb(167 243 208)",
|
||||
"border-emerald-300": "border-color:rgb(110 231 183)",
|
||||
"border-emerald-600": "border-color:rgb(5 150 105)",
|
||||
"border-blue-200": "border-color:rgb(191 219 254)",
|
||||
"border-blue-300": "border-color:rgb(147 197 253)",
|
||||
"border-violet-200": "border-color:rgb(221 214 254)",
|
||||
"border-violet-300": "border-color:rgb(196 181 253)",
|
||||
"border-violet-400": "border-color:rgb(167 139 250)",
|
||||
"border-t-white": "border-top-color:rgb(255 255 255)",
|
||||
"border-t-stone-600": "border-top-color:rgb(87 83 78)",
|
||||
"border-l-stone-400": "border-left-color:rgb(168 162 158)",
|
||||
|
||||
# ── Border Radius ────────────────────────────────────────────────────
|
||||
"rounded": "border-radius:.25rem",
|
||||
"rounded-md": "border-radius:.375rem",
|
||||
"rounded-lg": "border-radius:.5rem",
|
||||
"rounded-xl": "border-radius:.75rem",
|
||||
"rounded-2xl": "border-radius:1rem",
|
||||
"rounded-full": "border-radius:9999px",
|
||||
"rounded-t": "border-top-left-radius:.25rem;border-top-right-radius:.25rem",
|
||||
"rounded-b": "border-bottom-left-radius:.25rem;border-bottom-right-radius:.25rem",
|
||||
"rounded-[4px]": "border-radius:4px",
|
||||
"rounded-[8px]": "border-radius:8px",
|
||||
|
||||
# ── Shadow ───────────────────────────────────────────────────────────
|
||||
"shadow-sm": "box-shadow:0 1px 2px 0 rgba(0,0,0,.05)",
|
||||
"shadow": "box-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1)",
|
||||
"shadow-md": "box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1)",
|
||||
"shadow-lg": "box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1)",
|
||||
"shadow-xl": "box-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1)",
|
||||
|
||||
# ── Opacity ──────────────────────────────────────────────────────────
|
||||
"opacity-0": "opacity:0",
|
||||
"opacity-40": "opacity:.4",
|
||||
"opacity-50": "opacity:.5",
|
||||
"opacity-100": "opacity:1",
|
||||
|
||||
# ── Ring / Outline ───────────────────────────────────────────────────
|
||||
"outline-none": "outline:2px solid transparent;outline-offset:2px",
|
||||
"ring-2": "box-shadow:0 0 0 2px var(--tw-ring-color,rgb(59 130 246))",
|
||||
"ring-offset-2": "box-shadow:0 0 0 2px rgb(255 255 255),0 0 0 4px var(--tw-ring-color,rgb(59 130 246))",
|
||||
|
||||
# ── Overflow ─────────────────────────────────────────────────────────
|
||||
"overflow-hidden": "overflow:hidden",
|
||||
"overflow-x-auto": "overflow-x:auto",
|
||||
"overflow-y-auto": "overflow-y:auto",
|
||||
"overscroll-contain": "overscroll-behavior:contain",
|
||||
|
||||
# ── Text Decoration ──────────────────────────────────────────────────
|
||||
"underline": "text-decoration-line:underline",
|
||||
"line-through": "text-decoration-line:line-through",
|
||||
"no-underline": "text-decoration-line:none",
|
||||
|
||||
# ── Text Overflow ────────────────────────────────────────────────────
|
||||
"truncate": "overflow:hidden;text-overflow:ellipsis;white-space:nowrap",
|
||||
"line-clamp-2": "display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden",
|
||||
"line-clamp-3": "display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden",
|
||||
|
||||
# ── Whitespace / Word Break ──────────────────────────────────────────
|
||||
"whitespace-normal": "white-space:normal",
|
||||
"whitespace-nowrap": "white-space:nowrap",
|
||||
"whitespace-pre-line": "white-space:pre-line",
|
||||
"whitespace-pre-wrap": "white-space:pre-wrap",
|
||||
"break-words": "overflow-wrap:break-word",
|
||||
"break-all": "word-break:break-all",
|
||||
|
||||
# ── Transform ────────────────────────────────────────────────────────
|
||||
"rotate-180": "transform:rotate(180deg)",
|
||||
"-translate-x-1/2": "transform:translateX(-50%)",
|
||||
"-translate-y-1/2": "transform:translateY(-50%)",
|
||||
|
||||
# ── Transition ───────────────────────────────────────────────────────
|
||||
"transition": "transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s",
|
||||
"transition-all": "transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s",
|
||||
"transition-colors": "transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s",
|
||||
"transition-opacity": "transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s",
|
||||
"transition-transform": "transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s",
|
||||
"duration-75": "transition-duration:75ms",
|
||||
"duration-100": "transition-duration:100ms",
|
||||
"duration-150": "transition-duration:150ms",
|
||||
"duration-200": "transition-duration:200ms",
|
||||
"duration-300": "transition-duration:300ms",
|
||||
"duration-500": "transition-duration:500ms",
|
||||
"duration-700": "transition-duration:700ms",
|
||||
|
||||
# ── Animation ────────────────────────────────────────────────────────
|
||||
"animate-spin": "animation:spin 1s linear infinite",
|
||||
"animate-ping": "animation:ping 1s cubic-bezier(0,0,0.2,1) infinite",
|
||||
"animate-pulse": "animation:pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
|
||||
"animate-bounce": "animation:bounce 1s infinite",
|
||||
"animate-none": "animation:none",
|
||||
|
||||
# ── Aspect Ratio ─────────────────────────────────────────────────────
|
||||
"aspect-square": "aspect-ratio:1/1",
|
||||
"aspect-video": "aspect-ratio:16/9",
|
||||
|
||||
# ── Object Fit / Position ────────────────────────────────────────────
|
||||
"object-contain": "object-fit:contain",
|
||||
"object-cover": "object-fit:cover",
|
||||
"object-center": "object-position:center",
|
||||
"object-top": "object-position:top",
|
||||
|
||||
# ── Cursor ───────────────────────────────────────────────────────────
|
||||
"cursor-pointer": "cursor:pointer",
|
||||
"cursor-move": "cursor:move",
|
||||
|
||||
# ── User Select ──────────────────────────────────────────────────────
|
||||
"select-none": "user-select:none",
|
||||
"select-all": "user-select:all",
|
||||
|
||||
# ── Pointer Events ───────────────────────────────────────────────────
|
||||
"pointer-events-none": "pointer-events:none",
|
||||
|
||||
# ── Resize ───────────────────────────────────────────────────────────
|
||||
"resize": "resize:both",
|
||||
"resize-none": "resize:none",
|
||||
|
||||
# ── Scroll Snap ──────────────────────────────────────────────────────
|
||||
"snap-y": "scroll-snap-type:y mandatory",
|
||||
"snap-start": "scroll-snap-align:start",
|
||||
"snap-mandatory": "scroll-snap-type:y mandatory",
|
||||
|
||||
# ── List Style ───────────────────────────────────────────────────────
|
||||
"list-disc": "list-style-type:disc",
|
||||
"list-decimal": "list-style-type:decimal",
|
||||
"list-inside": "list-style-position:inside",
|
||||
|
||||
# ── Table ────────────────────────────────────────────────────────────
|
||||
"table-fixed": "table-layout:fixed",
|
||||
|
||||
# ── Backdrop ─────────────────────────────────────────────────────────
|
||||
"backdrop-blur": "backdrop-filter:blur(8px)",
|
||||
"backdrop-blur-sm": "backdrop-filter:blur(4px)",
|
||||
"backdrop-blur-md": "backdrop-filter:blur(12px)",
|
||||
|
||||
# ── Filter ───────────────────────────────────────────────────────────
|
||||
"saturate-0": "filter:saturate(0)",
|
||||
|
||||
# ── Space Between (child selector atoms) ─────────────────────────────
|
||||
# These generate `.atom > :not(:first-child)` rules
|
||||
"space-y-0": "margin-top:0",
|
||||
"space-y-0.5": "margin-top:.125rem",
|
||||
"space-y-1": "margin-top:.25rem",
|
||||
"space-y-2": "margin-top:.5rem",
|
||||
"space-y-3": "margin-top:.75rem",
|
||||
"space-y-4": "margin-top:1rem",
|
||||
"space-y-6": "margin-top:1.5rem",
|
||||
"space-y-8": "margin-top:2rem",
|
||||
"space-y-10": "margin-top:2.5rem",
|
||||
"space-x-1": "margin-left:.25rem",
|
||||
"space-x-2": "margin-left:.5rem",
|
||||
|
||||
# ── Divide (child selector atoms) ────────────────────────────────────
|
||||
# These generate `.atom > :not(:first-child)` rules
|
||||
"divide-y": "border-top-width:1px",
|
||||
"divide-stone-100": "border-color:rgb(245 245 244)",
|
||||
"divide-stone-200": "border-color:rgb(231 229 228)",
|
||||
|
||||
# ── Important modifiers ──────────────────────────────────────────────
|
||||
"!bg-stone-500": "background-color:rgb(120 113 108)!important",
|
||||
"!text-white": "color:rgb(255 255 255)!important",
|
||||
}
|
||||
|
||||
# Atoms that need a child selector: `.atom > :not(:first-child)` instead of `.atom`
|
||||
CHILD_SELECTOR_ATOMS: frozenset[str] = frozenset({
|
||||
k for k in STYLE_ATOMS
|
||||
if k.startswith(("space-x-", "space-y-", "divide-y", "divide-x"))
|
||||
and not k.startswith("divide-stone")
|
||||
})
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Pseudo-class / pseudo-element variants
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
PSEUDO_VARIANTS: dict[str, str] = {
|
||||
"hover": ":hover",
|
||||
"focus": ":focus",
|
||||
"focus-within": ":focus-within",
|
||||
"focus-visible": ":focus-visible",
|
||||
"active": ":active",
|
||||
"disabled": ":disabled",
|
||||
"first": ":first-child",
|
||||
"last": ":last-child",
|
||||
"odd": ":nth-child(odd)",
|
||||
"even": ":nth-child(even)",
|
||||
"empty": ":empty",
|
||||
"open": "[open]",
|
||||
"placeholder": "::placeholder",
|
||||
"file": "::file-selector-button",
|
||||
"aria-selected": "[aria-selected=true]",
|
||||
"group-hover": ":is(.group:hover) &",
|
||||
"group-open": ":is(.group[open]) &",
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Responsive breakpoints
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
RESPONSIVE_BREAKPOINTS: dict[str, str] = {
|
||||
"sm": "(min-width:640px)",
|
||||
"md": "(min-width:768px)",
|
||||
"lg": "(min-width:1024px)",
|
||||
"xl": "(min-width:1280px)",
|
||||
"2xl": "(min-width:1536px)",
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Keyframes — built-in animation definitions
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
KEYFRAMES: dict[str, str] = {
|
||||
"spin": "@keyframes spin{to{transform:rotate(360deg)}}",
|
||||
"ping": "@keyframes ping{75%,100%{transform:scale(2);opacity:0}}",
|
||||
"pulse": "@keyframes pulse{50%{opacity:.5}}",
|
||||
"bounce": "@keyframes bounce{0%,100%{transform:translateY(-25%);animation-timing-function:cubic-bezier(0.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,0.2,1)}}",
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Arbitrary value patterns — fallback when atom not in STYLE_ATOMS
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# Each tuple is (regex_pattern, css_template).
|
||||
# The regex captures value groups; the template uses {0}, {1}, etc.
|
||||
|
||||
ARBITRARY_PATTERNS: list[tuple[str, str]] = [
|
||||
# Width / Height
|
||||
(r"w-\[(.+)\]", "width:{0}"),
|
||||
(r"h-\[(.+)\]", "height:{0}"),
|
||||
(r"min-w-\[(.+)\]", "min-width:{0}"),
|
||||
(r"min-h-\[(.+)\]", "min-height:{0}"),
|
||||
(r"max-w-\[(.+)\]", "max-width:{0}"),
|
||||
(r"max-h-\[(.+)\]", "max-height:{0}"),
|
||||
# Spacing
|
||||
(r"p-\[(.+)\]", "padding:{0}"),
|
||||
(r"px-\[(.+)\]", "padding-left:{0};padding-right:{0}"),
|
||||
(r"py-\[(.+)\]", "padding-top:{0};padding-bottom:{0}"),
|
||||
(r"pt-\[(.+)\]", "padding-top:{0}"),
|
||||
(r"pb-\[(.+)\]", "padding-bottom:{0}"),
|
||||
(r"pl-\[(.+)\]", "padding-left:{0}"),
|
||||
(r"pr-\[(.+)\]", "padding-right:{0}"),
|
||||
(r"m-\[(.+)\]", "margin:{0}"),
|
||||
(r"mx-\[(.+)\]", "margin-left:{0};margin-right:{0}"),
|
||||
(r"my-\[(.+)\]", "margin-top:{0};margin-bottom:{0}"),
|
||||
(r"mt-\[(.+)\]", "margin-top:{0}"),
|
||||
(r"mb-\[(.+)\]", "margin-bottom:{0}"),
|
||||
(r"ml-\[(.+)\]", "margin-left:{0}"),
|
||||
(r"mr-\[(.+)\]", "margin-right:{0}"),
|
||||
# Gap
|
||||
(r"gap-\[(.+)\]", "gap:{0}"),
|
||||
(r"gap-x-\[(.+)\]", "column-gap:{0}"),
|
||||
(r"gap-y-\[(.+)\]", "row-gap:{0}"),
|
||||
# Position
|
||||
(r"top-\[(.+)\]", "top:{0}"),
|
||||
(r"right-\[(.+)\]", "right:{0}"),
|
||||
(r"bottom-\[(.+)\]", "bottom:{0}"),
|
||||
(r"left-\[(.+)\]", "left:{0}"),
|
||||
# Border radius
|
||||
(r"rounded-\[(.+)\]", "border-radius:{0}"),
|
||||
# Background / Text color
|
||||
(r"bg-\[(.+)\]", "background-color:{0}"),
|
||||
(r"text-\[(.+)\]", "font-size:{0}"),
|
||||
# Grid
|
||||
(r"grid-cols-\[(.+)\]", "grid-template-columns:{0}"),
|
||||
(r"col-span-(\d+)", "grid-column:span {0}/span {0}"),
|
||||
]
|
||||
254
shared/sx/style_resolver.py
Normal file
254
shared/sx/style_resolver.py
Normal file
@@ -0,0 +1,254 @@
|
||||
"""
|
||||
Style resolver — ``(css :flex :gap-4 :hover:bg-sky-200)`` → StyleValue.
|
||||
|
||||
Resolves a tuple of atom strings into a ``StyleValue`` with:
|
||||
- A content-addressed class name (``sx-{hash[:6]}``)
|
||||
- Base CSS declarations
|
||||
- Pseudo-class rules (hover, focus, etc.)
|
||||
- Media-query rules (responsive breakpoints)
|
||||
- Referenced @keyframes definitions
|
||||
|
||||
Resolution order per atom:
|
||||
1. Dictionary lookup in ``STYLE_ATOMS``
|
||||
2. Arbitrary value pattern match (``w-[347px]`` → ``width:347px``)
|
||||
3. Ignored (unknown atoms are silently skipped)
|
||||
|
||||
Results are memoized by input tuple for zero-cost repeat calls.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
from functools import lru_cache
|
||||
from typing import Sequence
|
||||
|
||||
from .style_dict import (
|
||||
ARBITRARY_PATTERNS,
|
||||
CHILD_SELECTOR_ATOMS,
|
||||
KEYFRAMES,
|
||||
PSEUDO_VARIANTS,
|
||||
RESPONSIVE_BREAKPOINTS,
|
||||
STYLE_ATOMS,
|
||||
)
|
||||
from .types import StyleValue
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Compiled arbitrary-value patterns
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_COMPILED_PATTERNS: list[tuple[re.Pattern, str]] = [
|
||||
(re.compile(f"^{pat}$"), tmpl)
|
||||
for pat, tmpl in ARBITRARY_PATTERNS
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def resolve_style(atoms: tuple[str, ...]) -> StyleValue:
|
||||
"""Resolve a tuple of keyword atoms into a StyleValue.
|
||||
|
||||
Each atom is a Tailwind-compatible keyword (``flex``, ``gap-4``,
|
||||
``hover:bg-sky-200``, ``sm:flex-row``, etc.). Both keywords
|
||||
(without leading colon) and runtime strings are accepted.
|
||||
"""
|
||||
return _resolve_cached(atoms)
|
||||
|
||||
|
||||
def merge_styles(styles: Sequence[StyleValue]) -> StyleValue:
|
||||
"""Merge multiple StyleValues into one.
|
||||
|
||||
Later declarations win for the same CSS property. Class name is
|
||||
recomputed from the merged declarations.
|
||||
"""
|
||||
if len(styles) == 1:
|
||||
return styles[0]
|
||||
|
||||
all_decls: list[str] = []
|
||||
all_media: list[tuple[str, str]] = []
|
||||
all_pseudo: list[tuple[str, str]] = []
|
||||
all_kf: list[tuple[str, str]] = []
|
||||
|
||||
for sv in styles:
|
||||
if sv.declarations:
|
||||
all_decls.append(sv.declarations)
|
||||
all_media.extend(sv.media_rules)
|
||||
all_pseudo.extend(sv.pseudo_rules)
|
||||
all_kf.extend(sv.keyframes)
|
||||
|
||||
merged_decls = ";".join(all_decls)
|
||||
return _build_style_value(
|
||||
merged_decls,
|
||||
tuple(all_media),
|
||||
tuple(all_pseudo),
|
||||
tuple(dict(all_kf).items()), # dedupe keyframes by name
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal resolution
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@lru_cache(maxsize=4096)
|
||||
def _resolve_cached(atoms: tuple[str, ...]) -> StyleValue:
|
||||
"""Memoized resolver."""
|
||||
base_decls: list[str] = []
|
||||
media_rules: list[tuple[str, str]] = [] # (query, decls)
|
||||
pseudo_rules: list[tuple[str, str]] = [] # (selector_suffix, decls)
|
||||
keyframes_needed: list[tuple[str, str]] = []
|
||||
|
||||
for atom in atoms:
|
||||
if not atom:
|
||||
continue
|
||||
# Strip leading colon if keyword form (":flex" → "flex")
|
||||
a = atom.lstrip(":")
|
||||
|
||||
# Split variant prefix(es): "hover:bg-sky-200" → ["hover", "bg-sky-200"]
|
||||
# "sm:hover:bg-sky-200" → ["sm", "hover", "bg-sky-200"]
|
||||
variant, base = _split_variant(a)
|
||||
|
||||
# Resolve the base atom to CSS declarations
|
||||
decls = _resolve_atom(base)
|
||||
if not decls:
|
||||
continue
|
||||
|
||||
# Check if this atom references a keyframe
|
||||
_check_keyframes(base, keyframes_needed)
|
||||
|
||||
# Route to the appropriate bucket
|
||||
if variant is None:
|
||||
base_decls.append(decls)
|
||||
elif variant in RESPONSIVE_BREAKPOINTS:
|
||||
query = RESPONSIVE_BREAKPOINTS[variant]
|
||||
media_rules.append((query, decls))
|
||||
elif variant in PSEUDO_VARIANTS:
|
||||
pseudo_sel = PSEUDO_VARIANTS[variant]
|
||||
pseudo_rules.append((pseudo_sel, decls))
|
||||
else:
|
||||
# Compound variant: "sm:hover:..." → media + pseudo
|
||||
parts = variant.split(":")
|
||||
media_part = None
|
||||
pseudo_part = None
|
||||
for p in parts:
|
||||
if p in RESPONSIVE_BREAKPOINTS:
|
||||
media_part = RESPONSIVE_BREAKPOINTS[p]
|
||||
elif p in PSEUDO_VARIANTS:
|
||||
pseudo_part = PSEUDO_VARIANTS[p]
|
||||
if media_part and pseudo_part:
|
||||
# Both media and pseudo — store as pseudo within media
|
||||
# For now, put in pseudo_rules with media annotation
|
||||
pseudo_rules.append((pseudo_part, decls))
|
||||
media_rules.append((media_part, decls))
|
||||
elif media_part:
|
||||
media_rules.append((media_part, decls))
|
||||
elif pseudo_part:
|
||||
pseudo_rules.append((pseudo_part, decls))
|
||||
else:
|
||||
# Unknown variant — treat as base
|
||||
base_decls.append(decls)
|
||||
|
||||
return _build_style_value(
|
||||
";".join(base_decls),
|
||||
tuple(media_rules),
|
||||
tuple(pseudo_rules),
|
||||
tuple(keyframes_needed),
|
||||
)
|
||||
|
||||
|
||||
def _split_variant(atom: str) -> tuple[str | None, str]:
|
||||
"""Split a potentially variant-prefixed atom.
|
||||
|
||||
Returns (variant, base) where variant is None for non-prefixed atoms.
|
||||
Examples:
|
||||
"flex" → (None, "flex")
|
||||
"hover:bg-sky-200" → ("hover", "bg-sky-200")
|
||||
"sm:flex-row" → ("sm", "flex-row")
|
||||
"sm:hover:bg-sky-200" → ("sm:hover", "bg-sky-200")
|
||||
"""
|
||||
# Check for responsive prefix first (always outermost)
|
||||
for bp in RESPONSIVE_BREAKPOINTS:
|
||||
prefix = bp + ":"
|
||||
if atom.startswith(prefix):
|
||||
rest = atom[len(prefix):]
|
||||
# Check for nested pseudo variant
|
||||
for pv in PSEUDO_VARIANTS:
|
||||
inner_prefix = pv + ":"
|
||||
if rest.startswith(inner_prefix):
|
||||
return (bp + ":" + pv, rest[len(inner_prefix):])
|
||||
return (bp, rest)
|
||||
|
||||
# Check for pseudo variant
|
||||
for pv in PSEUDO_VARIANTS:
|
||||
prefix = pv + ":"
|
||||
if atom.startswith(prefix):
|
||||
return (pv, atom[len(prefix):])
|
||||
|
||||
return (None, atom)
|
||||
|
||||
|
||||
def _resolve_atom(atom: str) -> str | None:
|
||||
"""Look up CSS declarations for a single base atom.
|
||||
|
||||
Returns None if the atom is unknown.
|
||||
"""
|
||||
# 1. Dictionary lookup
|
||||
decls = STYLE_ATOMS.get(atom)
|
||||
if decls is not None:
|
||||
return decls
|
||||
|
||||
# 2. Dynamic keyframes: animate-{name} → animation-name:{name}
|
||||
if atom.startswith("animate-"):
|
||||
name = atom[len("animate-"):]
|
||||
if name in KEYFRAMES:
|
||||
return f"animation-name:{name}"
|
||||
|
||||
# 3. Arbitrary value pattern match
|
||||
for pattern, template in _COMPILED_PATTERNS:
|
||||
m = pattern.match(atom)
|
||||
if m:
|
||||
groups = m.groups()
|
||||
result = template
|
||||
for i, g in enumerate(groups):
|
||||
result = result.replace(f"{{{i}}}", g)
|
||||
return result
|
||||
|
||||
# 4. Unknown atom — silently skip
|
||||
return None
|
||||
|
||||
|
||||
def _check_keyframes(atom: str, kf_list: list[tuple[str, str]]) -> None:
|
||||
"""If the atom references a built-in animation, add its @keyframes."""
|
||||
if atom.startswith("animate-"):
|
||||
name = atom[len("animate-"):]
|
||||
if name in KEYFRAMES:
|
||||
kf_list.append((name, KEYFRAMES[name]))
|
||||
|
||||
|
||||
def _build_style_value(
|
||||
declarations: str,
|
||||
media_rules: tuple,
|
||||
pseudo_rules: tuple,
|
||||
keyframes: tuple,
|
||||
) -> StyleValue:
|
||||
"""Build a StyleValue with a content-addressed class name."""
|
||||
# Build hash from all rules for deterministic class name
|
||||
hash_input = declarations
|
||||
for query, decls in media_rules:
|
||||
hash_input += f"@{query}{{{decls}}}"
|
||||
for sel, decls in pseudo_rules:
|
||||
hash_input += f"{sel}{{{decls}}}"
|
||||
for name, rule in keyframes:
|
||||
hash_input += rule
|
||||
|
||||
h = hashlib.sha256(hash_input.encode()).hexdigest()[:6]
|
||||
class_name = f"sx-{h}"
|
||||
|
||||
return StyleValue(
|
||||
class_name=class_name,
|
||||
declarations=declarations,
|
||||
media_rules=media_rules,
|
||||
pseudo_rules=pseudo_rules,
|
||||
keyframes=keyframes,
|
||||
)
|
||||
@@ -278,9 +278,33 @@ class ActionDef:
|
||||
return f"<action:{self.name}({', '.join(self.params)})>"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# StyleValue
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StyleValue:
|
||||
"""A resolved CSS style produced by ``(css :flex :gap-4 :hover:bg-sky-200)``.
|
||||
|
||||
Generated by the style resolver. The renderer emits ``class_name`` as a
|
||||
CSS class and registers the CSS rule for on-demand delivery.
|
||||
"""
|
||||
class_name: str # "sx-a3f2c1"
|
||||
declarations: str # "display:flex;gap:1rem"
|
||||
media_rules: tuple = () # ((query, decls), ...)
|
||||
pseudo_rules: tuple = () # ((selector, decls), ...)
|
||||
keyframes: tuple = () # (("spin", "@keyframes spin{...}"), ...)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<StyleValue {self.class_name}>"
|
||||
|
||||
def __str__(self):
|
||||
return self.class_name
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Type alias
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# An s-expression value after evaluation
|
||||
SExp = int | float | str | bool | Symbol | Keyword | Lambda | Macro | Component | HandlerDef | RelationDef | PageDef | QueryDef | ActionDef | list | dict | _Nil | None
|
||||
SExp = int | float | str | bool | Symbol | Keyword | Lambda | Macro | Component | HandlerDef | RelationDef | PageDef | QueryDef | ActionDef | StyleValue | list | dict | _Nil | None
|
||||
|
||||
Reference in New Issue
Block a user