Remove CSSX style dictionary infrastructure — styling is just components
The entire parallel CSS system (StyleValue type, style dictionary, keyword atom resolver, content-addressed class generation, runtime CSS injection, localStorage caching) was built but never adopted — the codebase already uses :class strings with defcomp components for all styling. Remove ~3,000 lines of unused infrastructure. Deleted: - cssx.sx spec module (317 lines) - style_dict.py (782 lines) and style_resolver.py (254 lines) - StyleValue type, defkeyframes special form, build-keyframes platform fn - Style dict JSON delivery (<script type="text/sx-styles">), cookies, localStorage - css/merge-styles primitives, inject-style-value, fnv1a-hash platform interface Simplified: - defstyle now binds any value (string, function) — no StyleValue type needed - render-attrs no longer special-cases :style StyleValue → class conversion - Boot sequence skips style dict init step Preserved: - tw.css parsing + CSS class delivery (SX-Css headers, <style id="sx-css">) - All component infrastructure (defcomp, caching, bundling, deps) - defstyle as a binding form for reusable class strings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -45,7 +45,7 @@ import contextvars
|
||||
import inspect
|
||||
from typing import Any
|
||||
|
||||
from .types import Component, Keyword, Lambda, Macro, NIL, StyleValue, Symbol
|
||||
from .types import Component, Keyword, Lambda, Macro, NIL, Symbol
|
||||
|
||||
# When True, _aser expands known components server-side instead of serializing
|
||||
# them for client rendering. Set during page slot evaluation so Python-only
|
||||
@@ -425,11 +425,6 @@ async def _asf_defstyle(expr, env, ctx):
|
||||
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)
|
||||
@@ -568,7 +563,6 @@ _ASYNC_SPECIAL_FORMS: dict[str, Any] = {
|
||||
"fn": _asf_lambda,
|
||||
"define": _asf_define,
|
||||
"defstyle": _asf_defstyle,
|
||||
"defkeyframes": _asf_defkeyframes,
|
||||
"defcomp": _asf_defcomp,
|
||||
"defmacro": _asf_defmacro,
|
||||
"defhandler": _asf_defhandler,
|
||||
@@ -884,18 +878,6 @@ 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)
|
||||
@@ -1120,7 +1102,6 @@ _ASYNC_RENDER_FORMS: dict[str, Any] = {
|
||||
"do": _arsf_begin,
|
||||
"define": _arsf_define,
|
||||
"defstyle": _arsf_define,
|
||||
"defkeyframes": _arsf_define,
|
||||
"defcomp": _arsf_define,
|
||||
"defmacro": _arsf_define,
|
||||
"defhandler": _arsf_define,
|
||||
@@ -1441,41 +1422,35 @@ async def _aser_call(
|
||||
|
||||
try:
|
||||
parts = [name]
|
||||
extra_class: str | None = None # from :style StyleValue conversion
|
||||
extra_class: str | None = None
|
||||
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:
|
||||
# :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}")
|
||||
# Plain list → serialize for the client.
|
||||
# Rendered items (SxExpr) → wrap in (<> ...) fragment.
|
||||
# Data items (dicts, strings, numbers) → (list ...)
|
||||
# so the client gets an iterable array, not a
|
||||
# DocumentFragment that breaks map/filter.
|
||||
if isinstance(val, list):
|
||||
live = [v for v in val
|
||||
if v is not NIL and v is not None]
|
||||
items = [serialize(v) for v in live]
|
||||
if not items:
|
||||
parts.append("nil")
|
||||
elif any(isinstance(v, SxExpr) for v in live):
|
||||
parts.append(
|
||||
"(<> " + " ".join(items) + ")"
|
||||
)
|
||||
else:
|
||||
parts.append(
|
||||
"(list " + " ".join(items) + ")"
|
||||
)
|
||||
parts.append(f":{arg.name}")
|
||||
# Plain list → serialize for the client.
|
||||
# Rendered items (SxExpr) → wrap in (<> ...) fragment.
|
||||
# Data items (dicts, strings, numbers) → (list ...)
|
||||
# so the client gets an iterable array, not a
|
||||
# DocumentFragment that breaks map/filter.
|
||||
if isinstance(val, list):
|
||||
live = [v for v in val
|
||||
if v is not NIL and v is not None]
|
||||
items = [serialize(v) for v in live]
|
||||
if not items:
|
||||
parts.append("nil")
|
||||
elif any(isinstance(v, SxExpr) for v in live):
|
||||
parts.append(
|
||||
"(<> " + " ".join(items) + ")"
|
||||
)
|
||||
else:
|
||||
parts.append(serialize(val))
|
||||
parts.append(
|
||||
"(list " + " ".join(items) + ")"
|
||||
)
|
||||
else:
|
||||
parts.append(serialize(val))
|
||||
i += 2
|
||||
else:
|
||||
result = await _aser(arg, env, ctx)
|
||||
@@ -1725,7 +1700,6 @@ _ASER_FORMS: dict[str, Any] = {
|
||||
"fn": _assf_lambda,
|
||||
"define": _assf_define,
|
||||
"defstyle": _assf_define,
|
||||
"defkeyframes": _assf_define,
|
||||
"defcomp": _assf_define,
|
||||
"defmacro": _assf_define,
|
||||
"defhandler": _assf_define,
|
||||
|
||||
@@ -94,10 +94,10 @@ def validate_helper(service: str, name: str) -> None:
|
||||
def validate_boundary_value(value: Any, context: str = "") -> None:
|
||||
"""Validate that a value is an allowed SX boundary type.
|
||||
|
||||
Allowed: int, float, str, bool, None/NIL, list, dict, SxExpr, StyleValue.
|
||||
Allowed: int, float, str, bool, None/NIL, list, dict, SxExpr.
|
||||
NOT allowed: datetime, ORM models, Quart objects, raw callables.
|
||||
"""
|
||||
from .types import NIL, StyleValue
|
||||
from .types import NIL
|
||||
from .parser import SxExpr
|
||||
|
||||
if value is None or value is NIL:
|
||||
@@ -106,8 +106,6 @@ def validate_boundary_value(value: Any, context: str = "") -> None:
|
||||
return
|
||||
if isinstance(value, SxExpr):
|
||||
return
|
||||
if isinstance(value, StyleValue):
|
||||
return
|
||||
if isinstance(value, list):
|
||||
for item in value:
|
||||
validate_boundary_value(item, context)
|
||||
|
||||
@@ -147,48 +147,6 @@ 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)
|
||||
|
||||
@@ -493,9 +493,9 @@ def _sf_define(expr: list, env: dict) -> Any:
|
||||
|
||||
|
||||
def _sf_defstyle(expr: list, env: dict) -> Any:
|
||||
"""``(defstyle card-base (css :rounded-xl :bg-white :shadow))``
|
||||
"""``(defstyle card-base ...)``
|
||||
|
||||
Evaluates body → StyleValue, binds to name in env.
|
||||
Evaluates body and binds to name in env.
|
||||
"""
|
||||
if len(expr) < 3:
|
||||
raise EvalError("defstyle requires name and body")
|
||||
@@ -507,63 +507,6 @@ def _sf_defstyle(expr: list, env: dict) -> Any:
|
||||
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:
|
||||
@@ -1060,7 +1003,6 @@ _SPECIAL_FORMS: dict[str, Any] = {
|
||||
"fn": _sf_lambda,
|
||||
"define": _sf_define,
|
||||
"defstyle": _sf_defstyle,
|
||||
"defkeyframes": _sf_defkeyframes,
|
||||
"defcomp": _sf_defcomp,
|
||||
"defrelation": _sf_defrelation,
|
||||
"begin": _sf_begin,
|
||||
|
||||
@@ -645,7 +645,6 @@ 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-pages">{pages_sx}</script>
|
||||
<script type="text/sx" data-mount="body">{page_sx}</script>
|
||||
@@ -820,14 +819,6 @@ def sx_page(ctx: dict, page_sx: str, *,
|
||||
import logging
|
||||
logging.getLogger("sx").warning("Pretty-print page_sx failed: %s", e)
|
||||
|
||||
# Style dictionary for client-side css primitive
|
||||
styles_hash = _get_style_dict_hash()
|
||||
client_styles_hash = _get_sx_styles_cookie()
|
||||
if not _is_dev_mode() and client_styles_hash and client_styles_hash == styles_hash:
|
||||
styles_json = "" # Client has cached version
|
||||
else:
|
||||
styles_json = _build_style_dict_json()
|
||||
|
||||
# Page registry for client-side routing
|
||||
import logging
|
||||
_plog = logging.getLogger("sx.pages")
|
||||
@@ -844,8 +835,6 @@ 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,
|
||||
pages_sx=pages_sx,
|
||||
page_sx=page_sx,
|
||||
sx_css=sx_css,
|
||||
@@ -907,10 +896,6 @@ def sx_page_streaming_parts(ctx: dict, page_html: str, *,
|
||||
title = ctx.get("base_title", "Rose Ash")
|
||||
csrf = _get_csrf_token()
|
||||
|
||||
styles_hash = _get_style_dict_hash()
|
||||
client_styles_hash = _get_sx_styles_cookie()
|
||||
styles_json = "" if (not _is_dev_mode() and client_styles_hash == styles_hash) else _build_style_dict_json()
|
||||
|
||||
import logging
|
||||
from quart import current_app
|
||||
pages_sx = _build_pages_sx(current_app.name)
|
||||
@@ -953,7 +938,6 @@ def sx_page_streaming_parts(ctx: dict, page_html: str, *,
|
||||
'</style>\n'
|
||||
'</head>\n'
|
||||
'<body class="bg-stone-50 text-stone-900">\n'
|
||||
f'<script type="text/sx-styles" data-hash="{styles_hash}">{styles_json}</script>\n'
|
||||
f'<script type="text/sx" data-components data-hash="{component_hash}">{component_defs}</script>\n'
|
||||
f'<script type="text/sx-pages">{pages_sx}</script>\n'
|
||||
# Server-rendered HTML — suspense placeholders are real DOM elements
|
||||
@@ -989,58 +973,6 @@ def sx_streaming_resolve_script(suspension_id: str, sx_source: 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 RuntimeError:
|
||||
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, StyleValue, Symbol
|
||||
from .types import Component, Keyword, Lambda, Macro, NIL, Symbol
|
||||
from .evaluator import _eval as _raw_eval, _call_component as _raw_call_component, _expand_macro, _trampoline
|
||||
|
||||
def _eval(expr, env):
|
||||
@@ -510,19 +510,6 @@ 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:
|
||||
|
||||
@@ -380,11 +380,6 @@ 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):
|
||||
|
||||
@@ -89,37 +89,6 @@ def prim_strip_tags(s: str) -> str:
|
||||
return re.sub(r"<[^>]+>", "", s)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# stdlib.style
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@register_primitive("css")
|
||||
def prim_css(*args: Any) -> Any:
|
||||
"""``(css :flex :gap-4 :hover:bg-sky-200)`` → StyleValue."""
|
||||
from .types import Keyword
|
||||
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."""
|
||||
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)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# stdlib.debug
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -38,7 +38,6 @@ Only these types may cross the host-SX boundary:
|
||||
| list | `list` | `Array` | `Vec<SxValue>` |
|
||||
| dict | `dict` | `Object` / `Map` | `HashMap<String, SxValue>` |
|
||||
| sx-source | `SxExpr` wrapper | `string` | `String` |
|
||||
| style-value | `StyleValue` | `StyleValue` | `StyleValue` |
|
||||
|
||||
**NOT allowed:** ORM models, datetime objects, request objects, raw callables, framework types. Convert at the edge before crossing.
|
||||
|
||||
|
||||
@@ -52,9 +52,6 @@
|
||||
(create-fragment)
|
||||
(render-dom-list expr env ns))
|
||||
|
||||
;; Style value → text of class name
|
||||
"style-value" (create-text-node (style-value-class expr))
|
||||
|
||||
;; Fallback
|
||||
:else (create-text-node (str expr)))))
|
||||
|
||||
@@ -147,8 +144,7 @@
|
||||
(let ((new-ns (cond (= tag "svg") SVG_NS
|
||||
(= tag "math") MATH_NS
|
||||
:else ns))
|
||||
(el (dom-create-element tag new-ns))
|
||||
(extra-class nil))
|
||||
(el (dom-create-element tag new-ns)))
|
||||
|
||||
;; Process args: keywords → attrs, others → children
|
||||
(reduce
|
||||
@@ -168,9 +164,6 @@
|
||||
;; nil or false → skip
|
||||
(or (nil? attr-val) (= attr-val false))
|
||||
nil
|
||||
;; :style StyleValue → convert to class
|
||||
(and (= attr-name "style") (style-value? attr-val))
|
||||
(set! extra-class (style-value-class attr-val))
|
||||
;; Boolean attr
|
||||
(contains? BOOLEAN_ATTRS attr-name)
|
||||
(when attr-val (dom-set-attr el attr-name ""))
|
||||
@@ -190,12 +183,6 @@
|
||||
(dict "i" 0 "skip" false)
|
||||
args)
|
||||
|
||||
;; Merge StyleValue class
|
||||
(when extra-class
|
||||
(let ((existing (dom-get-attr el "class")))
|
||||
(dom-set-attr el "class"
|
||||
(if existing (str existing " " extra-class) extra-class))))
|
||||
|
||||
el)))
|
||||
|
||||
|
||||
@@ -297,7 +284,7 @@
|
||||
|
||||
(define RENDER_DOM_FORMS
|
||||
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
|
||||
"define" "defcomp" "defmacro" "defstyle" "defkeyframes" "defhandler"
|
||||
"define" "defcomp" "defmacro" "defstyle" "defhandler"
|
||||
"map" "map-indexed" "filter" "for-each"))
|
||||
|
||||
(define render-dom-form?
|
||||
@@ -450,7 +437,6 @@
|
||||
;;
|
||||
;; From render.sx:
|
||||
;; HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS, definition-form?
|
||||
;; style-value?, style-value-class
|
||||
;;
|
||||
;; From eval.sx:
|
||||
;; eval-expr, trampoline, expand-macro, process-bindings, eval-cond
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"boolean" (if val "true" "false")
|
||||
"list" (render-list-to-html val env)
|
||||
"raw-html" (raw-html-content val)
|
||||
"style-value" (style-value-class val)
|
||||
:else (escape-html (str val)))))
|
||||
|
||||
|
||||
@@ -51,7 +50,7 @@
|
||||
|
||||
(define RENDER_HTML_FORMS
|
||||
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
|
||||
"define" "defcomp" "defmacro" "defstyle" "defkeyframes" "defhandler"
|
||||
"define" "defcomp" "defmacro" "defstyle" "defhandler"
|
||||
"map" "map-indexed" "filter" "for-each"))
|
||||
|
||||
(define render-html-form?
|
||||
@@ -293,7 +292,7 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; Inherited from render.sx:
|
||||
;; escape-html, escape-attr, raw-html-content, style-value?, style-value-class
|
||||
;; escape-html, escape-attr, raw-html-content
|
||||
;;
|
||||
;; From eval.sx:
|
||||
;; eval-expr, trampoline, expand-macro, process-bindings, eval-cond
|
||||
|
||||
@@ -26,7 +26,7 @@ import contextvars
|
||||
import inspect
|
||||
from typing import Any
|
||||
|
||||
from ..types import Component, Keyword, Lambda, Macro, NIL, StyleValue, Symbol
|
||||
from ..types import Component, Keyword, Lambda, Macro, NIL, Symbol
|
||||
from ..parser import SxExpr, serialize
|
||||
from ..primitives_io import IO_PRIMITIVES, RequestContext, execute_io
|
||||
from ..html import (
|
||||
@@ -250,18 +250,6 @@ async def _arender_element(tag, args, env, ctx):
|
||||
children.append(arg)
|
||||
i += 1
|
||||
|
||||
# StyleValue → class
|
||||
style_val = attrs.get("style")
|
||||
if isinstance(style_val, StyleValue):
|
||||
from ..css_registry import register_generated_rule
|
||||
register_generated_rule(style_val)
|
||||
existing = attrs.get("class")
|
||||
if existing and existing is not NIL and existing is not False:
|
||||
attrs["class"] = f"{existing} {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)
|
||||
@@ -464,7 +452,6 @@ _ASYNC_RENDER_FORMS = {
|
||||
"do": _arsf_begin,
|
||||
"define": _arsf_define,
|
||||
"defstyle": _arsf_define,
|
||||
"defkeyframes": _arsf_define,
|
||||
"defcomp": _arsf_define,
|
||||
"defmacro": _arsf_define,
|
||||
"defhandler": _arsf_define,
|
||||
@@ -716,23 +703,18 @@ async def _aser_call(name, args, env, ctx):
|
||||
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:
|
||||
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}")
|
||||
if isinstance(val, list):
|
||||
live = [v for v in val if v is not NIL and v is not None]
|
||||
items = [serialize(v) for v in live]
|
||||
if not items:
|
||||
parts.append("nil")
|
||||
elif any(isinstance(v, SxExpr) for v in live):
|
||||
parts.append("(<> " + " ".join(items) + ")")
|
||||
else:
|
||||
parts.append("(list " + " ".join(items) + ")")
|
||||
parts.append(f":{arg.name}")
|
||||
if isinstance(val, list):
|
||||
live = [v for v in val if v is not NIL and v is not None]
|
||||
items = [serialize(v) for v in live]
|
||||
if not items:
|
||||
parts.append("nil")
|
||||
elif any(isinstance(v, SxExpr) for v in live):
|
||||
parts.append("(<> " + " ".join(items) + ")")
|
||||
else:
|
||||
parts.append(serialize(val))
|
||||
parts.append("(list " + " ".join(items) + ")")
|
||||
else:
|
||||
parts.append(serialize(val))
|
||||
i += 2
|
||||
else:
|
||||
result = await _aser(arg, env, ctx)
|
||||
@@ -996,7 +978,6 @@ _ASER_FORMS = {
|
||||
"fn": _assf_lambda,
|
||||
"define": _assf_define,
|
||||
"defstyle": _assf_define,
|
||||
"defkeyframes": _assf_define,
|
||||
"defcomp": _assf_define,
|
||||
"defmacro": _assf_define,
|
||||
"defhandler": _assf_define,
|
||||
|
||||
@@ -3,16 +3,14 @@
|
||||
;;
|
||||
;; Handles the browser startup lifecycle:
|
||||
;; 1. CSS tracking init
|
||||
;; 2. Style dictionary loading (from <script type="text/sx-styles">)
|
||||
;; 3. Component script processing (from <script type="text/sx">)
|
||||
;; 4. Hydration of [data-sx] elements
|
||||
;; 5. Engine element processing
|
||||
;; 2. Component script processing (from <script type="text/sx">)
|
||||
;; 3. Hydration of [data-sx] elements
|
||||
;; 4. Engine element processing
|
||||
;;
|
||||
;; Also provides the public mounting/hydration API:
|
||||
;; mount, hydrate, update, render-component
|
||||
;;
|
||||
;; Depends on:
|
||||
;; cssx.sx — load-style-dict
|
||||
;; orchestration.sx — process-elements, engine-init
|
||||
;; adapter-dom.sx — render-to-dom
|
||||
;; render.sx — shared registries
|
||||
@@ -275,58 +273,6 @@
|
||||
(set-sx-comp-cookie hash))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Style dictionary initialization
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define init-style-dict
|
||||
(fn ()
|
||||
;; Process <script type="text/sx-styles"> tags with caching.
|
||||
(let ((scripts (query-style-scripts)))
|
||||
(for-each
|
||||
(fn (s)
|
||||
(when (not (is-processed? s "styles"))
|
||||
(mark-processed! s "styles")
|
||||
(let ((text (dom-text-content s))
|
||||
(hash (dom-get-attr s "data-hash")))
|
||||
(if (nil? hash)
|
||||
;; No hash — just parse inline
|
||||
(when (and text (not (empty? (trim text))))
|
||||
(parse-and-load-style-dict text))
|
||||
;; Hash-based caching
|
||||
(let ((has-inline (and text (not (empty? (trim text))))))
|
||||
(let ((cached-hash (local-storage-get "sx-styles-hash")))
|
||||
(if (= cached-hash hash)
|
||||
;; Cache hit
|
||||
(if has-inline
|
||||
(do
|
||||
(local-storage-set "sx-styles-src" text)
|
||||
(parse-and-load-style-dict text)
|
||||
(log-info "styles: downloaded (cookie stale)"))
|
||||
(let ((cached (local-storage-get "sx-styles-src")))
|
||||
(if cached
|
||||
(do
|
||||
(parse-and-load-style-dict cached)
|
||||
(log-info (str "styles: cached (" hash ")")))
|
||||
(do
|
||||
(clear-sx-styles-cookie)
|
||||
(browser-reload)))))
|
||||
;; Cache miss
|
||||
(if has-inline
|
||||
(do
|
||||
(local-storage-set "sx-styles-hash" hash)
|
||||
(local-storage-set "sx-styles-src" text)
|
||||
(parse-and-load-style-dict text)
|
||||
(log-info (str "styles: downloaded (" hash ")")))
|
||||
(do
|
||||
(local-storage-remove "sx-styles-hash")
|
||||
(local-storage-remove "sx-styles-src")
|
||||
(clear-sx-styles-cookie)
|
||||
(browser-reload)))))
|
||||
(set-sx-styles-cookie hash))))))
|
||||
scripts))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Page registry for client-side routing
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -375,7 +321,6 @@
|
||||
(do
|
||||
(log-info (str "sx-browser " SX_VERSION))
|
||||
(init-css-tracking)
|
||||
(init-style-dict)
|
||||
(process-page-scripts)
|
||||
(process-sx-scripts nil)
|
||||
(sx-hydrate-elements nil)
|
||||
@@ -389,9 +334,6 @@
|
||||
;; From orchestration.sx:
|
||||
;; process-elements, init-css-tracking
|
||||
;;
|
||||
;; From cssx.sx:
|
||||
;; load-style-dict
|
||||
;;
|
||||
;; === DOM / Render ===
|
||||
;; (resolve-mount-target target) → Element (string → querySelector, else identity)
|
||||
;; (sx-render-with-env source extra-env) → DOM node (parse + render with componentEnv + extra)
|
||||
@@ -420,7 +362,6 @@
|
||||
;;
|
||||
;; === Script queries ===
|
||||
;; (query-sx-scripts root) → list of <script type="text/sx"> elements
|
||||
;; (query-style-scripts) → list of <script type="text/sx-styles"> elements
|
||||
;; (query-page-scripts) → list of <script type="text/sx-pages"> elements
|
||||
;;
|
||||
;; === localStorage ===
|
||||
@@ -431,8 +372,6 @@
|
||||
;; === Cookies ===
|
||||
;; (set-sx-comp-cookie hash) → void
|
||||
;; (clear-sx-comp-cookie) → void
|
||||
;; (set-sx-styles-cookie hash) → void
|
||||
;; (clear-sx-styles-cookie) → void
|
||||
;;
|
||||
;; === Env ===
|
||||
;; (parse-env-attr el) → dict (parse data-sx-env JSON attr)
|
||||
@@ -444,8 +383,6 @@
|
||||
;; (log-parse-error label text err) → void (diagnostic parse error)
|
||||
;;
|
||||
;; === JSON parsing ===
|
||||
;; (parse-and-load-style-dict text) → void (JSON.parse + load-style-dict)
|
||||
;;
|
||||
;; === Processing markers ===
|
||||
;; (mark-processed! el key) → void
|
||||
;; (is-processed? el key) → boolean
|
||||
|
||||
@@ -191,10 +191,6 @@ class JSEmitter:
|
||||
"ho-every": "hoEvery",
|
||||
"ho-for-each": "hoForEach",
|
||||
"sf-defstyle": "sfDefstyle",
|
||||
"sf-defkeyframes": "sfDefkeyframes",
|
||||
"build-keyframes": "buildKeyframes",
|
||||
"style-value?": "isStyleValue",
|
||||
"style-value-class": "styleValueClass",
|
||||
"kf-name": "kfName",
|
||||
"special-form?": "isSpecialForm",
|
||||
"ho-form?": "isHoForm",
|
||||
@@ -443,32 +439,6 @@ class JSEmitter:
|
||||
"format-date": "formatDate",
|
||||
"format-decimal": "formatDecimal",
|
||||
"parse-int": "parseInt_",
|
||||
# cssx.sx
|
||||
"_style-atoms": "_styleAtoms",
|
||||
"_pseudo-variants": "_pseudoVariants",
|
||||
"_responsive-breakpoints": "_responsiveBreakpoints",
|
||||
"_style-keyframes": "_styleKeyframes",
|
||||
"_arbitrary-patterns": "_arbitraryPatterns",
|
||||
"_child-selector-prefixes": "_childSelectorPrefixes",
|
||||
"_style-cache": "_styleCache",
|
||||
"_injected-styles": "_injectedStyles",
|
||||
"load-style-dict": "loadStyleDict",
|
||||
"split-variant": "splitVariant",
|
||||
"resolve-atom": "resolveAtom",
|
||||
"is-child-selector-atom?": "isChildSelectorAtom",
|
||||
"hash-style": "hashStyle",
|
||||
"resolve-style": "resolveStyle",
|
||||
"merge-style-values": "mergeStyleValues",
|
||||
"fnv1a-hash": "fnv1aHash",
|
||||
"compile-regex": "compileRegex",
|
||||
"regex-match": "regexMatch",
|
||||
"regex-replace-groups": "regexReplaceGroups",
|
||||
"make-style-value": "makeStyleValue_",
|
||||
"style-value-declarations": "styleValueDeclarations",
|
||||
"style-value-media-rules": "styleValueMediaRules",
|
||||
"style-value-pseudo-rules": "styleValuePseudoRules",
|
||||
"style-value-keyframes": "styleValueKeyframes_",
|
||||
"inject-style-value": "injectStyleValue",
|
||||
# boot.sx
|
||||
"HEAD_HOIST_SELECTOR": "HEAD_HOIST_SELECTOR",
|
||||
"hoist-head-elements-full": "hoistHeadElementsFull",
|
||||
@@ -478,7 +448,6 @@ class JSEmitter:
|
||||
"sx-render-component": "sxRenderComponent",
|
||||
"process-sx-scripts": "processSxScripts",
|
||||
"process-component-script": "processComponentScript",
|
||||
"init-style-dict": "initStyleDict",
|
||||
"SX_VERSION": "SX_VERSION",
|
||||
"boot-init": "bootInit",
|
||||
"resolve-suspense": "resolveSuspense",
|
||||
@@ -490,21 +459,17 @@ class JSEmitter:
|
||||
"set-document-title": "setDocumentTitle",
|
||||
"remove-head-element": "removeHeadElement",
|
||||
"query-sx-scripts": "querySxScripts",
|
||||
"query-style-scripts": "queryStyleScripts",
|
||||
"local-storage-get": "localStorageGet",
|
||||
"local-storage-set": "localStorageSet",
|
||||
"local-storage-remove": "localStorageRemove",
|
||||
"set-sx-comp-cookie": "setSxCompCookie",
|
||||
"clear-sx-comp-cookie": "clearSxCompCookie",
|
||||
"set-sx-styles-cookie": "setSxStylesCookie",
|
||||
"clear-sx-styles-cookie": "clearSxStylesCookie",
|
||||
"parse-env-attr": "parseEnvAttr",
|
||||
"store-env-attr": "storeEnvAttr",
|
||||
"to-kebab": "toKebab",
|
||||
"log-info": "logInfo",
|
||||
"log-warn": "logWarn",
|
||||
"log-parse-error": "logParseError",
|
||||
"parse-and-load-style-dict": "parseAndLoadStyleDict",
|
||||
"_page-routes": "_pageRoutes",
|
||||
"process-page-scripts": "processPageScripts",
|
||||
"query-page-scripts": "queryPageScripts",
|
||||
@@ -1044,7 +1009,6 @@ ADAPTER_FILES = {
|
||||
"dom": ("adapter-dom.sx", "adapter-dom"),
|
||||
"engine": ("engine.sx", "engine"),
|
||||
"orchestration": ("orchestration.sx","orchestration"),
|
||||
"cssx": ("cssx.sx", "cssx"),
|
||||
"boot": ("boot.sx", "boot"),
|
||||
}
|
||||
|
||||
@@ -1052,8 +1016,7 @@ ADAPTER_FILES = {
|
||||
ADAPTER_DEPS = {
|
||||
"engine": ["dom"],
|
||||
"orchestration": ["engine", "dom"],
|
||||
"cssx": [],
|
||||
"boot": ["dom", "engine", "orchestration", "cssx", "parser"],
|
||||
"boot": ["dom", "engine", "orchestration", "parser"],
|
||||
"parser": [],
|
||||
}
|
||||
|
||||
@@ -1292,7 +1255,7 @@ ASYNC_IO_JS = '''
|
||||
|
||||
// define/defcomp/defmacro — eval for side effects
|
||||
if (hname === "define" || hname === "defcomp" || hname === "defmacro" ||
|
||||
hname === "defstyle" || hname === "defkeyframes" || hname === "defhandler") {
|
||||
hname === "defstyle" || hname === "defhandler") {
|
||||
trampoline(evalExpr(expr, env));
|
||||
return null;
|
||||
}
|
||||
@@ -1414,11 +1377,7 @@ ASYNC_IO_JS = '''
|
||||
})(attrName, attrVal);
|
||||
} else {
|
||||
if (!isNil(attrVal) && attrVal !== false) {
|
||||
if (attrName === "class" && attrVal && attrVal._styleValue) {
|
||||
el.setAttribute("class", (el.getAttribute("class") || "") + " " + attrVal.className);
|
||||
} else if (attrName === "style" && attrVal && attrVal._styleValue) {
|
||||
el.setAttribute("class", (el.getAttribute("class") || "") + " " + attrVal.className);
|
||||
} else if (contains(BOOLEAN_ATTRS, attrName)) {
|
||||
if (contains(BOOLEAN_ATTRS, attrName)) {
|
||||
if (isSxTruthy(attrVal)) el.setAttribute(attrName, "");
|
||||
} else if (attrVal === true) {
|
||||
el.setAttribute(attrName, "");
|
||||
@@ -1829,7 +1788,6 @@ def compile_ref_to_js(
|
||||
"dom": PLATFORM_DOM_JS,
|
||||
"engine": PLATFORM_ENGINE_PURE_JS,
|
||||
"orchestration": PLATFORM_ORCHESTRATION_JS,
|
||||
"cssx": PLATFORM_CSSX_JS,
|
||||
"boot": PLATFORM_BOOT_JS,
|
||||
}
|
||||
|
||||
@@ -1864,7 +1822,7 @@ def compile_ref_to_js(
|
||||
("eval.sx", "eval"),
|
||||
("render.sx", "render (core)"),
|
||||
]
|
||||
for name in ("parser", "html", "sx", "dom", "engine", "orchestration", "cssx", "boot"):
|
||||
for name in ("parser", "html", "sx", "dom", "engine", "orchestration", "boot"):
|
||||
if name in adapter_set:
|
||||
sx_files.append(ADAPTER_FILES[name])
|
||||
for name in sorted(spec_mod_set):
|
||||
@@ -1895,7 +1853,6 @@ def compile_ref_to_js(
|
||||
has_dom = "dom" in adapter_set
|
||||
has_engine = "engine" in adapter_set
|
||||
has_orch = "orchestration" in adapter_set
|
||||
has_cssx = "cssx" in adapter_set
|
||||
has_boot = "boot" in adapter_set
|
||||
has_parser = "parser" in adapter_set
|
||||
adapter_label = "+".join(sorted(adapter_set)) if adapter_set else "core-only"
|
||||
@@ -1937,7 +1894,7 @@ def compile_ref_to_js(
|
||||
# Platform JS for selected adapters
|
||||
if not has_dom:
|
||||
parts.append("\n var _hasDom = false;\n")
|
||||
for name in ("dom", "engine", "orchestration", "cssx", "boot"):
|
||||
for name in ("dom", "engine", "orchestration", "boot"):
|
||||
if name in adapter_set and name in adapter_platform:
|
||||
parts.append(adapter_platform[name])
|
||||
|
||||
@@ -1946,7 +1903,7 @@ def compile_ref_to_js(
|
||||
parts.append(CONTINUATIONS_JS)
|
||||
if has_dom:
|
||||
parts.append(ASYNC_IO_JS)
|
||||
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label, has_deps, has_router))
|
||||
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps, has_router))
|
||||
parts.append(EPILOGUE)
|
||||
from datetime import datetime, timezone
|
||||
build_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
@@ -2019,15 +1976,6 @@ PREAMBLE = '''\
|
||||
function RawHTML(html) { this.html = html; }
|
||||
RawHTML.prototype._raw = true;
|
||||
|
||||
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 != null && x._sym === true; }
|
||||
function isKw(x) { return x != null && x._kw === true; }
|
||||
|
||||
@@ -2224,30 +2172,6 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
|
||||
PRIMITIVES["strip-tags"] = function(s) { return String(s).replace(/<[^>]+>/g, ""); };
|
||||
''',
|
||||
|
||||
"stdlib.style": '''
|
||||
// stdlib.style
|
||||
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 new StyleValue("sx-" + atoms.join("-"), atoms.join(";"), [], [], []);
|
||||
};
|
||||
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];
|
||||
var allDecls = valid.map(function(v) { return v.declarations; }).join(";");
|
||||
return new StyleValue("sx-merged", allDecls, [], [], []);
|
||||
};
|
||||
''',
|
||||
|
||||
"stdlib.debug": '''
|
||||
// stdlib.debug
|
||||
PRIMITIVES["assert"] = function(cond, msg) {
|
||||
@@ -2295,7 +2219,6 @@ PLATFORM_JS_PRE = '''
|
||||
if (x._component) return "component";
|
||||
if (x._macro) return "macro";
|
||||
if (x._raw) return "raw-html";
|
||||
if (x._styleValue) return "style-value";
|
||||
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
|
||||
if (Array.isArray(x)) return "list";
|
||||
if (typeof x === "object") return "dict";
|
||||
@@ -2342,27 +2265,6 @@ PLATFORM_JS_PRE = '''
|
||||
function isComponent(x) { return x != null && x._component === true; }
|
||||
function isMacro(x) { return x != null && x._macro === true; }
|
||||
|
||||
function isStyleValue(x) { return x != null && x._styleValue === true; }
|
||||
function styleValueClass(x) { return x.className; }
|
||||
function styleValue_p(x) { return x != null && x._styleValue === true; }
|
||||
|
||||
function buildKeyframes(kfName, steps, env) {
|
||||
// Platform implementation of defkeyframes
|
||||
var parts = [];
|
||||
for (var i = 0; i < steps.length; i++) {
|
||||
var step = steps[i];
|
||||
var selector = isSym(step[0]) ? step[0].name : String(step[0]);
|
||||
var body = trampoline(evalExpr(step[1], env));
|
||||
var decls = isStyleValue(body) ? body.declarations : String(body);
|
||||
parts.push(selector + "{" + decls + "}");
|
||||
}
|
||||
var kfRule = "@keyframes " + kfName + "{" + parts.join("") + "}";
|
||||
var cn = "sx-ref-kf-" + kfName;
|
||||
var sv = new StyleValue(cn, "animation-name:" + kfName, [], [], [[kfName, kfRule]]);
|
||||
env[kfName] = sv;
|
||||
return sv;
|
||||
}
|
||||
|
||||
function envHas(env, name) { return name in env; }
|
||||
function envGet(env, name) { return env[name]; }
|
||||
function envSet(env, name, val) { env[name] = val; }
|
||||
@@ -2488,7 +2390,7 @@ PLATFORM_JS_POST = '''
|
||||
function isSpecialForm(n) { return n in {
|
||||
"if":1,"when":1,"cond":1,"case":1,"and":1,"or":1,"let":1,"let*":1,
|
||||
"lambda":1,"fn":1,"define":1,"defcomp":1,"defmacro":1,"defstyle":1,
|
||||
"defkeyframes":1,"defhandler":1,"begin":1,"do":1,
|
||||
"defhandler":1,"begin":1,"do":1,
|
||||
"quote":1,"quasiquote":1,"->":1,"set!":1
|
||||
}; }
|
||||
function isHoForm(n) { return n in {
|
||||
@@ -2499,7 +2401,7 @@ PLATFORM_JS_POST = '''
|
||||
|
||||
function isDefinitionForm(name) {
|
||||
return name === "define" || name === "defcomp" || name === "defmacro" ||
|
||||
name === "defstyle" || name === "defkeyframes" || name === "defhandler";
|
||||
name === "defstyle" || name === "defhandler";
|
||||
}
|
||||
|
||||
function indexOf_(s, ch) {
|
||||
@@ -2877,11 +2779,7 @@ PLATFORM_DOM_JS = """
|
||||
var attrVal = trampoline(evalExpr(args[i + 1], env));
|
||||
i++; // skip value
|
||||
if (isNil(attrVal) || attrVal === false) continue;
|
||||
if (attrName === "class" && attrVal && attrVal._styleValue) {
|
||||
extraClasses.push(attrVal.className);
|
||||
} else if (attrName === "style" && attrVal && attrVal._styleValue) {
|
||||
extraClasses.push(attrVal.className);
|
||||
} else if (contains(BOOLEAN_ATTRS, attrName)) {
|
||||
if (contains(BOOLEAN_ATTRS, attrName)) {
|
||||
if (isSxTruthy(attrVal)) el.setAttribute(attrName, "");
|
||||
} else if (attrVal === true) {
|
||||
el.setAttribute(attrName, "");
|
||||
@@ -3763,102 +3661,6 @@ PLATFORM_ORCHESTRATION_JS = """
|
||||
}
|
||||
"""
|
||||
|
||||
PLATFORM_CSSX_JS = """
|
||||
// =========================================================================
|
||||
// Platform interface — CSSX (style dictionary)
|
||||
// =========================================================================
|
||||
|
||||
function fnv1aHash(input) {
|
||||
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 compileRegex(pattern) {
|
||||
try { return new RegExp(pattern); } catch (e) { return null; }
|
||||
}
|
||||
|
||||
function regexMatch(re, s) {
|
||||
if (!re) return NIL;
|
||||
var m = s.match(re);
|
||||
return m ? Array.prototype.slice.call(m) : NIL;
|
||||
}
|
||||
|
||||
function regexReplaceGroups(tmpl, match) {
|
||||
var result = tmpl;
|
||||
for (var j = 1; j < match.length; j++) {
|
||||
result = result.split("{" + (j - 1) + "}").join(match[j]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function makeStyleValue_(cn, decls, media, pseudo, kf) {
|
||||
return new StyleValue(cn, decls || "", media || [], pseudo || [], kf || []);
|
||||
}
|
||||
|
||||
function styleValueDeclarations(sv) { return sv.declarations; }
|
||||
function styleValueMediaRules(sv) { return sv.mediaRules; }
|
||||
function styleValuePseudoRules(sv) { return sv.pseudoRules; }
|
||||
function styleValueKeyframes_(sv) { return sv.keyframes; }
|
||||
|
||||
function injectStyleValue(sv, atoms) {
|
||||
if (_injectedStyles[sv.className]) return;
|
||||
_injectedStyles[sv.className] = true;
|
||||
|
||||
if (!_hasDom) return;
|
||||
var cssTarget = document.getElementById("sx-css");
|
||||
if (!cssTarget) return;
|
||||
|
||||
var rules = [];
|
||||
// Child-selector atoms are now routed to pseudoRules by the resolver
|
||||
// with selector ">:not(:first-child)", so base declarations are always
|
||||
// applied directly to the class.
|
||||
if (sv.declarations) {
|
||||
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("");
|
||||
}
|
||||
|
||||
// Replace stub css primitive with real CSSX implementation
|
||||
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);
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
"""
|
||||
|
||||
PLATFORM_BOOT_JS = """
|
||||
// =========================================================================
|
||||
// Platform interface — Boot (mount, hydrate, scripts, cookies)
|
||||
@@ -3916,12 +3718,6 @@ PLATFORM_BOOT_JS = """
|
||||
r.querySelectorAll('script[type="text/sx"]'));
|
||||
}
|
||||
|
||||
function queryStyleScripts() {
|
||||
if (!_hasDom) return [];
|
||||
return Array.prototype.slice.call(
|
||||
document.querySelectorAll('script[type="text/sx-styles"]'));
|
||||
}
|
||||
|
||||
function queryPageScripts() {
|
||||
if (!_hasDom) return [];
|
||||
return Array.prototype.slice.call(
|
||||
@@ -3953,14 +3749,6 @@ PLATFORM_BOOT_JS = """
|
||||
if (_hasDom) document.cookie = "sx-comp-hash=;path=/;max-age=0;SameSite=Lax";
|
||||
}
|
||||
|
||||
function setSxStylesCookie(hash) {
|
||||
if (_hasDom) document.cookie = "sx-styles-hash=" + hash + ";path=/;max-age=31536000;SameSite=Lax";
|
||||
}
|
||||
|
||||
function clearSxStylesCookie() {
|
||||
if (_hasDom) document.cookie = "sx-styles-hash=;path=/;max-age=0;SameSite=Lax";
|
||||
}
|
||||
|
||||
// --- Env helpers ---
|
||||
|
||||
function parseEnvAttr(el) {
|
||||
@@ -4009,10 +3797,6 @@ PLATFORM_BOOT_JS = """
|
||||
}
|
||||
}
|
||||
|
||||
function parseAndLoadStyleDict(text) {
|
||||
try { loadStyleDict(JSON.parse(text)); }
|
||||
catch (e) { if (typeof console !== "undefined") console.warn("[sx-ref] style dict parse error", e); }
|
||||
}
|
||||
"""
|
||||
|
||||
def fixups_js(has_html, has_sx, has_dom):
|
||||
@@ -4039,7 +3823,7 @@ def fixups_js(has_html, has_sx, has_dom):
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label, has_deps=False, has_router=False):
|
||||
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps=False, has_router=False):
|
||||
# Parser: use compiled sxParse from parser.sx, or inline a minimal fallback
|
||||
if has_parser:
|
||||
parser = '''
|
||||
|
||||
@@ -199,11 +199,6 @@ class PyEmitter:
|
||||
"ho-every": "ho_every",
|
||||
"ho-for-each": "ho_for_each",
|
||||
"sf-defstyle": "sf_defstyle",
|
||||
"sf-defkeyframes": "sf_defkeyframes",
|
||||
"build-keyframes": "build_keyframes",
|
||||
"style-value?": "is_style_value",
|
||||
"style-value-class": "style_value_class",
|
||||
"kf-name": "kf_name",
|
||||
"special-form?": "is_special_form",
|
||||
"ho-form?": "is_ho_form",
|
||||
"strip-prefix": "strip_prefix",
|
||||
@@ -1080,7 +1075,7 @@ from typing import Any
|
||||
# =========================================================================
|
||||
|
||||
from shared.sx.types import (
|
||||
NIL, Symbol, Keyword, Lambda, Component, Continuation, Macro, StyleValue,
|
||||
NIL, Symbol, Keyword, Lambda, Component, Continuation, Macro,
|
||||
HandlerDef, QueryDef, ActionDef, PageDef, _ShiftSignal,
|
||||
)
|
||||
from shared.sx.parser import SxExpr
|
||||
@@ -1189,8 +1184,6 @@ def type_of(x):
|
||||
return "macro"
|
||||
if isinstance(x, _RawHTML):
|
||||
return "raw-html"
|
||||
if isinstance(x, StyleValue):
|
||||
return "style-value"
|
||||
if isinstance(x, Continuation):
|
||||
return "continuation"
|
||||
if isinstance(x, list):
|
||||
@@ -1355,14 +1348,6 @@ def is_macro(x):
|
||||
return isinstance(x, Macro)
|
||||
|
||||
|
||||
def is_style_value(x):
|
||||
return isinstance(x, StyleValue)
|
||||
|
||||
|
||||
def style_value_class(x):
|
||||
return x.class_name
|
||||
|
||||
|
||||
def env_has(env, name):
|
||||
return name in env
|
||||
|
||||
@@ -1513,8 +1498,6 @@ def serialize(val):
|
||||
if t == "raw-html":
|
||||
escaped = escape_string(raw_html_content(val))
|
||||
return '(raw! "' + escaped + '")'
|
||||
if t == "style-value":
|
||||
return '"' + style_value_class(val) + '"'
|
||||
if t == "list":
|
||||
if not val:
|
||||
return "()"
|
||||
@@ -1534,7 +1517,7 @@ def serialize(val):
|
||||
_SPECIAL_FORM_NAMES = frozenset([
|
||||
"if", "when", "cond", "case", "and", "or",
|
||||
"let", "let*", "lambda", "fn",
|
||||
"define", "defcomp", "defmacro", "defstyle", "defkeyframes",
|
||||
"define", "defcomp", "defmacro", "defstyle",
|
||||
"defhandler", "defpage", "defquery", "defaction", "defrelation",
|
||||
"begin", "do", "quote", "quasiquote",
|
||||
"->", "set!",
|
||||
@@ -1696,7 +1679,7 @@ def aser_special(name, expr, env):
|
||||
fn(item)
|
||||
return results if results else NIL
|
||||
# Definition forms — evaluate for side effects
|
||||
if name in ("define", "defcomp", "defmacro", "defstyle", "defkeyframes",
|
||||
if name in ("define", "defcomp", "defmacro", "defstyle",
|
||||
"defhandler", "defpage", "defquery", "defaction", "defrelation"):
|
||||
trampoline(eval_expr(expr, env))
|
||||
return NIL
|
||||
|
||||
@@ -123,4 +123,4 @@
|
||||
|
||||
(define-boundary-types
|
||||
(list "number" "string" "boolean" "nil" "keyword"
|
||||
"list" "dict" "sx-source" "style-value"))
|
||||
"list" "dict" "sx-source"))
|
||||
|
||||
@@ -1,317 +0,0 @@
|
||||
;; ==========================================================================
|
||||
;; cssx.sx — On-demand CSS style dictionary
|
||||
;;
|
||||
;; Resolves keyword atoms (e.g. :flex, :gap-4, :hover:bg-sky-200) into
|
||||
;; StyleValue objects with content-addressed class names. CSS rules are
|
||||
;; injected into the document on first use.
|
||||
;;
|
||||
;; The style dictionary is loaded from a JSON blob (typically served
|
||||
;; inline in a <script type="text/sx-styles"> tag) containing:
|
||||
;; a — atom → CSS declarations map
|
||||
;; v — pseudo-variant → CSS pseudo-selector map
|
||||
;; b — responsive breakpoint → media query map
|
||||
;; k — keyframe name → @keyframes rule map
|
||||
;; p — arbitrary patterns: [[regex, template], ...]
|
||||
;; c — child selector prefixes: ["space-x-", "space-y-", ...]
|
||||
;;
|
||||
;; Depends on:
|
||||
;; render.sx — StyleValue type
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; State — populated by load-style-dict
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define _style-atoms (dict))
|
||||
(define _pseudo-variants (dict))
|
||||
(define _responsive-breakpoints (dict))
|
||||
(define _style-keyframes (dict))
|
||||
(define _arbitrary-patterns (list))
|
||||
(define _child-selector-prefixes (list))
|
||||
(define _style-cache (dict))
|
||||
(define _injected-styles (dict))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Load style dictionary from parsed JSON data
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define load-style-dict
|
||||
(fn (data)
|
||||
(set! _style-atoms (or (get data "a") (dict)))
|
||||
(set! _pseudo-variants (or (get data "v") (dict)))
|
||||
(set! _responsive-breakpoints (or (get data "b") (dict)))
|
||||
(set! _style-keyframes (or (get data "k") (dict)))
|
||||
(set! _child-selector-prefixes (or (get data "c") (list)))
|
||||
;; Compile arbitrary patterns from [regex, template] pairs
|
||||
(set! _arbitrary-patterns
|
||||
(map
|
||||
(fn (pair)
|
||||
(dict "re" (compile-regex (str "^" (first pair) "$"))
|
||||
"tmpl" (nth pair 1)))
|
||||
(or (get data "p") (list))))
|
||||
;; Clear cache on reload
|
||||
(set! _style-cache (dict))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Variant splitting
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define split-variant
|
||||
(fn (atom)
|
||||
;; Parse variant prefixes: "sm:hover:bg-sky-200" → ["sm:hover", "bg-sky-200"]
|
||||
;; Returns [variant, base] where variant is nil for no variant.
|
||||
|
||||
;; Check responsive prefix first
|
||||
(let ((result nil))
|
||||
(for-each
|
||||
(fn (bp)
|
||||
(when (nil? result)
|
||||
(let ((prefix (str bp ":")))
|
||||
(when (starts-with? atom prefix)
|
||||
(let ((rest-atom (slice atom (len prefix))))
|
||||
;; Check for compound variant (sm:hover:...)
|
||||
(let ((inner-match nil))
|
||||
(for-each
|
||||
(fn (pv)
|
||||
(when (nil? inner-match)
|
||||
(let ((inner-prefix (str pv ":")))
|
||||
(when (starts-with? rest-atom inner-prefix)
|
||||
(set! inner-match
|
||||
(list (str bp ":" pv)
|
||||
(slice rest-atom (len inner-prefix))))))))
|
||||
(keys _pseudo-variants))
|
||||
(set! result
|
||||
(or inner-match (list bp rest-atom)))))))))
|
||||
(keys _responsive-breakpoints))
|
||||
|
||||
(when (nil? result)
|
||||
;; Check pseudo variants
|
||||
(for-each
|
||||
(fn (pv)
|
||||
(when (nil? result)
|
||||
(let ((prefix (str pv ":")))
|
||||
(when (starts-with? atom prefix)
|
||||
(set! result (list pv (slice atom (len prefix))))))))
|
||||
(keys _pseudo-variants)))
|
||||
|
||||
(or result (list nil atom)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Atom resolution
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define resolve-atom
|
||||
(fn (atom)
|
||||
;; Look up atom → CSS declarations string, or nil
|
||||
(let ((decls (dict-get _style-atoms atom)))
|
||||
(if (not (nil? decls))
|
||||
decls
|
||||
;; Dynamic keyframes: animate-{name}
|
||||
(if (starts-with? atom "animate-")
|
||||
(let ((kf-name (slice atom 8)))
|
||||
(if (dict-has? _style-keyframes kf-name)
|
||||
(str "animation-name:" kf-name)
|
||||
nil))
|
||||
;; Try arbitrary patterns
|
||||
(let ((match-result nil))
|
||||
(for-each
|
||||
(fn (pat)
|
||||
(when (nil? match-result)
|
||||
(let ((m (regex-match (get pat "re") atom)))
|
||||
(when m
|
||||
(set! match-result
|
||||
(regex-replace-groups (get pat "tmpl") m))))))
|
||||
_arbitrary-patterns)
|
||||
match-result))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Child selector detection
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define is-child-selector-atom?
|
||||
(fn (atom)
|
||||
(some
|
||||
(fn (prefix) (starts-with? atom prefix))
|
||||
_child-selector-prefixes)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; FNV-1a 32-bit hash → 6 hex chars
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define hash-style
|
||||
(fn (input)
|
||||
;; FNV-1a 32-bit hash for content-addressed class names
|
||||
(fnv1a-hash input)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Full style resolution pipeline
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define resolve-style
|
||||
(fn (atoms)
|
||||
;; Resolve a list of atom strings into a StyleValue.
|
||||
;; Uses content-addressed caching.
|
||||
(let ((key (join "\0" atoms)))
|
||||
(let ((cached (dict-get _style-cache key)))
|
||||
(if (not (nil? cached))
|
||||
cached
|
||||
;; Resolve each atom
|
||||
(let ((base-decls (list))
|
||||
(media-rules (list))
|
||||
(pseudo-rules (list))
|
||||
(kf-needed (list)))
|
||||
(for-each
|
||||
(fn (a)
|
||||
(when a
|
||||
(let ((clean (if (starts-with? a ":") (slice a 1) a)))
|
||||
(let ((parts (split-variant clean)))
|
||||
(let ((variant (first parts))
|
||||
(base (nth parts 1))
|
||||
(decls (resolve-atom base)))
|
||||
(when decls
|
||||
;; Check keyframes
|
||||
(when (starts-with? base "animate-")
|
||||
(let ((kf-name (slice base 8)))
|
||||
(when (dict-has? _style-keyframes kf-name)
|
||||
(append! kf-needed
|
||||
(list kf-name (dict-get _style-keyframes kf-name))))))
|
||||
|
||||
(cond
|
||||
(nil? variant)
|
||||
(if (is-child-selector-atom? base)
|
||||
(append! pseudo-rules
|
||||
(list ">:not(:first-child)" decls))
|
||||
(append! base-decls decls))
|
||||
|
||||
(dict-has? _responsive-breakpoints variant)
|
||||
(append! media-rules
|
||||
(list (dict-get _responsive-breakpoints variant) decls))
|
||||
|
||||
(dict-has? _pseudo-variants variant)
|
||||
(append! pseudo-rules
|
||||
(list (dict-get _pseudo-variants variant) decls))
|
||||
|
||||
;; Compound variant: "sm:hover"
|
||||
:else
|
||||
(let ((vparts (split variant ":"))
|
||||
(media-part nil)
|
||||
(pseudo-part nil))
|
||||
(for-each
|
||||
(fn (vp)
|
||||
(cond
|
||||
(dict-has? _responsive-breakpoints vp)
|
||||
(set! media-part (dict-get _responsive-breakpoints vp))
|
||||
(dict-has? _pseudo-variants vp)
|
||||
(set! pseudo-part (dict-get _pseudo-variants vp))))
|
||||
vparts)
|
||||
(when media-part
|
||||
(append! media-rules (list media-part decls)))
|
||||
(when pseudo-part
|
||||
(append! pseudo-rules (list pseudo-part decls)))
|
||||
(when (and (nil? media-part) (nil? pseudo-part))
|
||||
(append! base-decls decls))))))))))
|
||||
atoms)
|
||||
|
||||
;; Build hash input
|
||||
(let ((hash-input (join ";" base-decls)))
|
||||
(for-each
|
||||
(fn (mr)
|
||||
(set! hash-input
|
||||
(str hash-input "@" (first mr) "{" (nth mr 1) "}")))
|
||||
media-rules)
|
||||
(for-each
|
||||
(fn (pr)
|
||||
(set! hash-input
|
||||
(str hash-input (first pr) "{" (nth pr 1) "}")))
|
||||
pseudo-rules)
|
||||
(for-each
|
||||
(fn (kf)
|
||||
(set! hash-input (str hash-input (nth kf 1))))
|
||||
kf-needed)
|
||||
|
||||
(let ((cn (str "sx-" (hash-style hash-input)))
|
||||
(sv (make-style-value cn
|
||||
(join ";" base-decls)
|
||||
media-rules
|
||||
pseudo-rules
|
||||
kf-needed)))
|
||||
(dict-set! _style-cache key sv)
|
||||
;; Inject CSS rules
|
||||
(inject-style-value sv atoms)
|
||||
sv))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Merge multiple StyleValues
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define merge-style-values
|
||||
(fn (styles)
|
||||
(if (= (len styles) 1)
|
||||
(first styles)
|
||||
(let ((all-decls (list))
|
||||
(all-media (list))
|
||||
(all-pseudo (list))
|
||||
(all-kf (list)))
|
||||
(for-each
|
||||
(fn (sv)
|
||||
(when (style-value-declarations sv)
|
||||
(append! all-decls (style-value-declarations sv)))
|
||||
(set! all-media (concat all-media (style-value-media-rules sv)))
|
||||
(set! all-pseudo (concat all-pseudo (style-value-pseudo-rules sv)))
|
||||
(set! all-kf (concat all-kf (style-value-keyframes sv))))
|
||||
styles)
|
||||
|
||||
(let ((hash-input (join ";" all-decls)))
|
||||
(for-each
|
||||
(fn (mr)
|
||||
(set! hash-input
|
||||
(str hash-input "@" (first mr) "{" (nth mr 1) "}")))
|
||||
all-media)
|
||||
(for-each
|
||||
(fn (pr)
|
||||
(set! hash-input
|
||||
(str hash-input (first pr) "{" (nth pr 1) "}")))
|
||||
all-pseudo)
|
||||
(for-each
|
||||
(fn (kf)
|
||||
(set! hash-input (str hash-input (nth kf 1))))
|
||||
all-kf)
|
||||
|
||||
(let ((cn (str "sx-" (hash-style hash-input)))
|
||||
(merged (make-style-value cn
|
||||
(join ";" all-decls)
|
||||
all-media all-pseudo all-kf)))
|
||||
(inject-style-value merged (list))
|
||||
merged))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform interface — CSSX
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; Hash:
|
||||
;; (fnv1a-hash input) → 6-char hex string (FNV-1a 32-bit)
|
||||
;;
|
||||
;; Regex:
|
||||
;; (compile-regex pattern) → compiled regex object
|
||||
;; (regex-match re str) → match array or nil
|
||||
;; (regex-replace-groups tmpl match) → string with {0},{1},... replaced
|
||||
;;
|
||||
;; StyleValue construction:
|
||||
;; (make-style-value cn decls media pseudo kf) → StyleValue object
|
||||
;; (style-value-declarations sv) → declarations string
|
||||
;; (style-value-media-rules sv) → list of [query, decls] pairs
|
||||
;; (style-value-pseudo-rules sv) → list of [selector, decls] pairs
|
||||
;; (style-value-keyframes sv) → list of [name, rule] pairs
|
||||
;;
|
||||
;; CSS injection:
|
||||
;; (inject-style-value sv atoms) → void (append CSS rules to <style id="sx-css">)
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -143,7 +143,6 @@
|
||||
(= name "defcomp") (sf-defcomp args env)
|
||||
(= name "defmacro") (sf-defmacro args env)
|
||||
(= name "defstyle") (sf-defstyle args env)
|
||||
(= name "defkeyframes") (sf-defkeyframes args env)
|
||||
(= name "defhandler") (sf-defhandler args env)
|
||||
(= name "defpage") (sf-defpage args env)
|
||||
(= name "defquery") (sf-defquery args env)
|
||||
@@ -559,23 +558,13 @@
|
||||
|
||||
(define sf-defstyle
|
||||
(fn (args env)
|
||||
;; (defstyle name expr) — bind name to evaluated expr (typically a StyleValue)
|
||||
;; (defstyle name expr) — bind name to evaluated expr (string, function, etc.)
|
||||
(let ((name-sym (first args))
|
||||
(value (trampoline (eval-expr (nth args 1) env))))
|
||||
(env-set! env (symbol-name name-sym) value)
|
||||
value)))
|
||||
|
||||
|
||||
(define sf-defkeyframes
|
||||
(fn (args env)
|
||||
;; (defkeyframes name (selector body) ...) — build @keyframes rule,
|
||||
;; register in keyframes dict, return StyleValue.
|
||||
;; Delegates to platform: build-keyframes returns a StyleValue.
|
||||
(let ((kf-name (symbol-name (first args)))
|
||||
(steps (rest args)))
|
||||
(build-keyframes kf-name steps env))))
|
||||
|
||||
|
||||
(define sf-begin
|
||||
(fn (args env)
|
||||
(if (empty? args)
|
||||
@@ -931,9 +920,6 @@
|
||||
;; (zip lists...) → list of tuples
|
||||
;;
|
||||
;;
|
||||
;; CSSX (style system):
|
||||
;; (build-keyframes name steps env) → StyleValue (platform builds @keyframes)
|
||||
;;
|
||||
;; Dynamic wind (for dynamic-wind):
|
||||
;; (push-wind! before after) → void (push wind record onto stack)
|
||||
;; (pop-wind!) → void (pop wind record from stack)
|
||||
|
||||
@@ -541,18 +541,6 @@
|
||||
;; Stdlib — Style
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-module :stdlib.style)
|
||||
|
||||
(define-primitive "css"
|
||||
:params (&rest atoms)
|
||||
:returns "style-value"
|
||||
:doc "Resolve style atoms to a StyleValue with className and CSS declarations.
|
||||
Atoms are keywords or strings: (css :flex :gap-4 :hover:bg-sky-200).")
|
||||
|
||||
(define-primitive "merge-styles"
|
||||
:params (&rest styles)
|
||||
:returns "style-value"
|
||||
:doc "Merge multiple StyleValues into one combined StyleValue.")
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
(define definition-form?
|
||||
(fn (name)
|
||||
(or (= name "define") (= name "defcomp") (= name "defmacro")
|
||||
(= name "defstyle") (= name "defkeyframes") (= name "defhandler"))))
|
||||
(= name "defstyle") (= name "defhandler"))))
|
||||
|
||||
|
||||
(define parse-element-args
|
||||
@@ -116,9 +116,6 @@
|
||||
""
|
||||
;; Nil values — skip
|
||||
(nil? val) ""
|
||||
;; StyleValue on :style → emit as class
|
||||
(and (= key "style") (style-value? val))
|
||||
(str " class=\"" (style-value-class val) "\"")
|
||||
;; Normal attr
|
||||
:else (str " " key "=\"" (escape-attr (str val)) "\""))))
|
||||
(keys attrs)))))
|
||||
@@ -202,10 +199,6 @@
|
||||
;; (escape-attr s) → attribute-value-escaped string
|
||||
;; (raw-html-content r) → unwrap RawHTML marker to string
|
||||
;;
|
||||
;; StyleValue:
|
||||
;; (style-value? x) → boolean (is x a StyleValue?)
|
||||
;; (style-value-class sv) → string (CSS class name)
|
||||
;;
|
||||
;; Serialization:
|
||||
;; (serialize val) → SX source string representation of val
|
||||
;;
|
||||
|
||||
@@ -363,20 +363,12 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-special-form "defstyle"
|
||||
:syntax (defstyle name atoms ...)
|
||||
:doc "Define a named style. Evaluates atoms to a StyleValue and binds
|
||||
it to name in the environment."
|
||||
:syntax (defstyle name expr)
|
||||
:doc "Define a named style value. Evaluates expr and binds the result
|
||||
to name in the environment. The value is typically a class string
|
||||
or a function that returns class strings."
|
||||
:tail-position "none"
|
||||
:example "(defstyle card-style :rounded-lg :shadow-md :p-4 :bg-white)")
|
||||
|
||||
(define-special-form "defkeyframes"
|
||||
:syntax (defkeyframes name steps ...)
|
||||
:doc "Define a CSS @keyframes animation. Steps are (percentage properties ...)
|
||||
pairs. Produces a StyleValue with the animation name and keyframe rules."
|
||||
:tail-position "none"
|
||||
:example "(defkeyframes fade-in
|
||||
(0 :opacity-0)
|
||||
(100 :opacity-100))")
|
||||
:example "(defstyle card-style \"rounded-lg shadow-md p-4 bg-white\")")
|
||||
|
||||
(define-special-form "defhandler"
|
||||
:syntax (defhandler name (&key params ...) body)
|
||||
|
||||
@@ -18,7 +18,7 @@ from typing import Any
|
||||
# =========================================================================
|
||||
|
||||
from shared.sx.types import (
|
||||
NIL, Symbol, Keyword, Lambda, Component, Continuation, Macro, StyleValue,
|
||||
NIL, Symbol, Keyword, Lambda, Component, Continuation, Macro,
|
||||
HandlerDef, QueryDef, ActionDef, PageDef, _ShiftSignal,
|
||||
)
|
||||
from shared.sx.parser import SxExpr
|
||||
@@ -126,8 +126,6 @@ def type_of(x):
|
||||
return "macro"
|
||||
if isinstance(x, _RawHTML):
|
||||
return "raw-html"
|
||||
if isinstance(x, StyleValue):
|
||||
return "style-value"
|
||||
if isinstance(x, Continuation):
|
||||
return "continuation"
|
||||
if isinstance(x, list):
|
||||
@@ -292,14 +290,6 @@ def is_macro(x):
|
||||
return isinstance(x, Macro)
|
||||
|
||||
|
||||
def is_style_value(x):
|
||||
return isinstance(x, StyleValue)
|
||||
|
||||
|
||||
def style_value_class(x):
|
||||
return x.class_name
|
||||
|
||||
|
||||
def env_has(env, name):
|
||||
return name in env
|
||||
|
||||
@@ -450,8 +440,6 @@ def serialize(val):
|
||||
if t == "raw-html":
|
||||
escaped = escape_string(raw_html_content(val))
|
||||
return '(raw! "' + escaped + '")'
|
||||
if t == "style-value":
|
||||
return '"' + style_value_class(val) + '"'
|
||||
if t == "list":
|
||||
if not val:
|
||||
return "()"
|
||||
@@ -471,7 +459,7 @@ def serialize(val):
|
||||
_SPECIAL_FORM_NAMES = frozenset([
|
||||
"if", "when", "cond", "case", "and", "or",
|
||||
"let", "let*", "lambda", "fn",
|
||||
"define", "defcomp", "defmacro", "defstyle", "defkeyframes",
|
||||
"define", "defcomp", "defmacro", "defstyle",
|
||||
"defhandler", "defpage", "defquery", "defaction", "defrelation",
|
||||
"begin", "do", "quote", "quasiquote",
|
||||
"->", "set!",
|
||||
@@ -633,7 +621,7 @@ def aser_special(name, expr, env):
|
||||
fn(item)
|
||||
return results if results else NIL
|
||||
# Definition forms — evaluate for side effects
|
||||
if name in ("define", "defcomp", "defmacro", "defstyle", "defkeyframes",
|
||||
if name in ("define", "defcomp", "defmacro", "defstyle",
|
||||
"defhandler", "defpage", "defquery", "defaction", "defrelation"):
|
||||
trampoline(eval_expr(expr, env))
|
||||
return NIL
|
||||
@@ -955,7 +943,7 @@ trampoline = lambda val: (lambda result: (trampoline(eval_expr(thunk_expr(result
|
||||
eval_expr = lambda expr, env: _sx_case(type_of(expr), [('number', lambda: expr), ('string', lambda: expr), ('boolean', lambda: expr), ('nil', lambda: NIL), ('symbol', lambda: (lambda name: (env_get(env, name) if sx_truthy(env_has(env, name)) else (get_primitive(name) if sx_truthy(is_primitive(name)) else (True if sx_truthy((name == 'true')) else (False if sx_truthy((name == 'false')) else (NIL if sx_truthy((name == 'nil')) else error(sx_str('Undefined symbol: ', name))))))))(symbol_name(expr))), ('keyword', lambda: keyword_name(expr)), ('dict', lambda: map_dict(lambda k, v: trampoline(eval_expr(v, env)), expr)), ('list', lambda: ([] if sx_truthy(empty_p(expr)) else eval_list(expr, env))), (None, lambda: expr)])
|
||||
|
||||
# eval-list
|
||||
eval_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: trampoline(eval_expr(x, env)), expr) if sx_truthy((not sx_truthy(((type_of(head) == 'symbol') if sx_truthy((type_of(head) == 'symbol')) else ((type_of(head) == 'lambda') if sx_truthy((type_of(head) == 'lambda')) else (type_of(head) == 'list')))))) else ((lambda name: (sf_if(args, env) if sx_truthy((name == 'if')) else (sf_when(args, env) if sx_truthy((name == 'when')) else (sf_cond(args, env) if sx_truthy((name == 'cond')) else (sf_case(args, env) if sx_truthy((name == 'case')) else (sf_and(args, env) if sx_truthy((name == 'and')) else (sf_or(args, env) if sx_truthy((name == 'or')) else (sf_let(args, env) if sx_truthy((name == 'let')) else (sf_let(args, env) if sx_truthy((name == 'let*')) else (sf_letrec(args, env) if sx_truthy((name == 'letrec')) else (sf_lambda(args, env) if sx_truthy((name == 'lambda')) else (sf_lambda(args, env) if sx_truthy((name == 'fn')) else (sf_define(args, env) if sx_truthy((name == 'define')) else (sf_defcomp(args, env) if sx_truthy((name == 'defcomp')) else (sf_defmacro(args, env) if sx_truthy((name == 'defmacro')) else (sf_defstyle(args, env) if sx_truthy((name == 'defstyle')) else (sf_defkeyframes(args, env) if sx_truthy((name == 'defkeyframes')) else (sf_defhandler(args, env) if sx_truthy((name == 'defhandler')) else (sf_defpage(args, env) if sx_truthy((name == 'defpage')) else (sf_defquery(args, env) if sx_truthy((name == 'defquery')) else (sf_defaction(args, env) if sx_truthy((name == 'defaction')) else (sf_begin(args, env) if sx_truthy((name == 'begin')) else (sf_begin(args, env) if sx_truthy((name == 'do')) else (sf_quote(args, env) if sx_truthy((name == 'quote')) else (sf_quasiquote(args, env) if sx_truthy((name == 'quasiquote')) else (sf_thread_first(args, env) if sx_truthy((name == '->')) else (sf_set_bang(args, env) if sx_truthy((name == 'set!')) else (sf_reset(args, env) if sx_truthy((name == 'reset')) else (sf_shift(args, env) if sx_truthy((name == 'shift')) else (sf_dynamic_wind(args, env) if sx_truthy((name == 'dynamic-wind')) else (ho_map(args, env) if sx_truthy((name == 'map')) else (ho_map_indexed(args, env) if sx_truthy((name == 'map-indexed')) else (ho_filter(args, env) if sx_truthy((name == 'filter')) else (ho_reduce(args, env) if sx_truthy((name == 'reduce')) else (ho_some(args, env) if sx_truthy((name == 'some')) else (ho_every(args, env) if sx_truthy((name == 'every?')) else (ho_for_each(args, env) if sx_truthy((name == 'for-each')) else ((lambda mac: make_thunk(expand_macro(mac, args, env), env))(env_get(env, name)) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (render_expr(expr, env) if sx_truthy(is_render_expr(expr)) else eval_call(head, args, env))))))))))))))))))))))))))))))))))))))))(symbol_name(head)) if sx_truthy((type_of(head) == 'symbol')) else eval_call(head, args, env))))(rest(expr)))(first(expr))
|
||||
eval_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: trampoline(eval_expr(x, env)), expr) if sx_truthy((not sx_truthy(((type_of(head) == 'symbol') if sx_truthy((type_of(head) == 'symbol')) else ((type_of(head) == 'lambda') if sx_truthy((type_of(head) == 'lambda')) else (type_of(head) == 'list')))))) else ((lambda name: (sf_if(args, env) if sx_truthy((name == 'if')) else (sf_when(args, env) if sx_truthy((name == 'when')) else (sf_cond(args, env) if sx_truthy((name == 'cond')) else (sf_case(args, env) if sx_truthy((name == 'case')) else (sf_and(args, env) if sx_truthy((name == 'and')) else (sf_or(args, env) if sx_truthy((name == 'or')) else (sf_let(args, env) if sx_truthy((name == 'let')) else (sf_let(args, env) if sx_truthy((name == 'let*')) else (sf_letrec(args, env) if sx_truthy((name == 'letrec')) else (sf_lambda(args, env) if sx_truthy((name == 'lambda')) else (sf_lambda(args, env) if sx_truthy((name == 'fn')) else (sf_define(args, env) if sx_truthy((name == 'define')) else (sf_defcomp(args, env) if sx_truthy((name == 'defcomp')) else (sf_defmacro(args, env) if sx_truthy((name == 'defmacro')) else (sf_defstyle(args, env) if sx_truthy((name == 'defstyle')) else (sf_defhandler(args, env) if sx_truthy((name == 'defhandler')) else (sf_defpage(args, env) if sx_truthy((name == 'defpage')) else (sf_defquery(args, env) if sx_truthy((name == 'defquery')) else (sf_defaction(args, env) if sx_truthy((name == 'defaction')) else (sf_begin(args, env) if sx_truthy((name == 'begin')) else (sf_begin(args, env) if sx_truthy((name == 'do')) else (sf_quote(args, env) if sx_truthy((name == 'quote')) else (sf_quasiquote(args, env) if sx_truthy((name == 'quasiquote')) else (sf_thread_first(args, env) if sx_truthy((name == '->')) else (sf_set_bang(args, env) if sx_truthy((name == 'set!')) else (sf_reset(args, env) if sx_truthy((name == 'reset')) else (sf_shift(args, env) if sx_truthy((name == 'shift')) else (sf_dynamic_wind(args, env) if sx_truthy((name == 'dynamic-wind')) else (ho_map(args, env) if sx_truthy((name == 'map')) else (ho_map_indexed(args, env) if sx_truthy((name == 'map-indexed')) else (ho_filter(args, env) if sx_truthy((name == 'filter')) else (ho_reduce(args, env) if sx_truthy((name == 'reduce')) else (ho_some(args, env) if sx_truthy((name == 'some')) else (ho_every(args, env) if sx_truthy((name == 'every?')) else (ho_for_each(args, env) if sx_truthy((name == 'for-each')) else ((lambda mac: make_thunk(expand_macro(mac, args, env), env))(env_get(env, name)) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (render_expr(expr, env) if sx_truthy(is_render_expr(expr)) else eval_call(head, args, env)))))))))))))))))))))))))))))))))))))))(symbol_name(head)) if sx_truthy((type_of(head) == 'symbol')) else eval_call(head, args, env))))(rest(expr)))(first(expr))
|
||||
|
||||
# eval-call
|
||||
eval_call = lambda head, args, env: (lambda f: (lambda evaluated_args: (apply(f, evaluated_args) if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else ((not sx_truthy(is_lambda(f))) if not sx_truthy((not sx_truthy(is_lambda(f)))) else (not sx_truthy(is_component(f)))))) else (call_lambda(f, evaluated_args, env) if sx_truthy(is_lambda(f)) else (call_component(f, args, env) if sx_truthy(is_component(f)) else error(sx_str('Not callable: ', inspect(f)))))))(map(lambda a: trampoline(eval_expr(a, env)), args)))(trampoline(eval_expr(head, env)))
|
||||
@@ -1051,9 +1039,6 @@ def parse_macro_params(params_expr):
|
||||
# sf-defstyle
|
||||
sf_defstyle = lambda args, env: (lambda name_sym: (lambda value: _sx_begin(_sx_dict_set(env, symbol_name(name_sym), value), value))(trampoline(eval_expr(nth(args, 1), env))))(first(args))
|
||||
|
||||
# sf-defkeyframes
|
||||
sf_defkeyframes = lambda args, env: (lambda kf_name: (lambda steps: build_keyframes(kf_name, steps, env))(rest(args)))(symbol_name(first(args)))
|
||||
|
||||
# sf-begin
|
||||
sf_begin = lambda args, env: (NIL if sx_truthy(empty_p(args)) else _sx_begin(for_each(lambda e: trampoline(eval_expr(e, env)), slice(args, 0, (len(args) - 1))), make_thunk(last(args), env)))
|
||||
|
||||
@@ -1164,13 +1149,13 @@ VOID_ELEMENTS = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'li
|
||||
BOOLEAN_ATTRS = ['async', 'autofocus', 'autoplay', 'checked', 'controls', 'default', 'defer', 'disabled', 'formnovalidate', 'hidden', 'inert', 'ismap', 'loop', 'multiple', 'muted', 'nomodule', 'novalidate', 'open', 'playsinline', 'readonly', 'required', 'reversed', 'selected']
|
||||
|
||||
# definition-form?
|
||||
is_definition_form = lambda name: ((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else ((name == 'defkeyframes') if sx_truthy((name == 'defkeyframes')) else (name == 'defhandler'))))))
|
||||
is_definition_form = lambda name: ((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else (name == 'defhandler')))))
|
||||
|
||||
# parse-element-args
|
||||
parse_element_args = lambda args, env: (lambda attrs: (lambda children: _sx_begin(reduce(lambda state, arg: (lambda skip: (assoc(state, 'skip', False, 'i', (get(state, 'i') + 1)) if sx_truthy(skip) else ((lambda val: _sx_begin(_sx_dict_set(attrs, keyword_name(arg), val), assoc(state, 'skip', True, 'i', (get(state, 'i') + 1))))(trampoline(eval_expr(nth(args, (get(state, 'i') + 1)), env))) if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((get(state, 'i') + 1) < len(args)))) else _sx_begin(_sx_append(children, arg), assoc(state, 'i', (get(state, 'i') + 1))))))(get(state, 'skip')), {'i': 0, 'skip': False}, args), [attrs, children]))([]))({})
|
||||
|
||||
# render-attrs
|
||||
render_attrs = lambda attrs: join('', map(lambda key: (lambda val: (sx_str(' ', key) if sx_truthy((contains_p(BOOLEAN_ATTRS, key) if not sx_truthy(contains_p(BOOLEAN_ATTRS, key)) else val)) else ('' if sx_truthy((contains_p(BOOLEAN_ATTRS, key) if not sx_truthy(contains_p(BOOLEAN_ATTRS, key)) else (not sx_truthy(val)))) else ('' if sx_truthy(is_nil(val)) else (sx_str(' class="', style_value_class(val), '"') if sx_truthy(((key == 'style') if not sx_truthy((key == 'style')) else is_style_value(val))) else sx_str(' ', key, '="', escape_attr(sx_str(val)), '"'))))))(dict_get(attrs, key)), keys(attrs)))
|
||||
render_attrs = lambda attrs: join('', map(lambda key: (lambda val: (sx_str(' ', key) if sx_truthy((contains_p(BOOLEAN_ATTRS, key) if not sx_truthy(contains_p(BOOLEAN_ATTRS, key)) else val)) else ('' if sx_truthy((contains_p(BOOLEAN_ATTRS, key) if not sx_truthy(contains_p(BOOLEAN_ATTRS, key)) else (not sx_truthy(val)))) else ('' if sx_truthy(is_nil(val)) else sx_str(' ', key, '="', escape_attr(sx_str(val)), '"')))))(dict_get(attrs, key)), keys(attrs)))
|
||||
|
||||
# eval-cond
|
||||
eval_cond = lambda clauses, env: (eval_cond_scheme(clauses, env) if sx_truthy(((not sx_truthy(empty_p(clauses))) if not sx_truthy((not sx_truthy(empty_p(clauses)))) else ((type_of(first(clauses)) == 'list') if not sx_truthy((type_of(first(clauses)) == 'list')) else (len(first(clauses)) == 2)))) else eval_cond_clojure(clauses, env))
|
||||
@@ -1191,10 +1176,10 @@ process_bindings = lambda bindings, env: (lambda local: _sx_begin(for_each(lambd
|
||||
render_to_html = lambda expr, env: _sx_case(type_of(expr), [('nil', lambda: ''), ('string', lambda: escape_html(expr)), ('number', lambda: sx_str(expr)), ('boolean', lambda: ('true' if sx_truthy(expr) else 'false')), ('list', lambda: ('' if sx_truthy(empty_p(expr)) else render_list_to_html(expr, env))), ('symbol', lambda: render_value_to_html(trampoline(eval_expr(expr, env)), env)), ('keyword', lambda: escape_html(keyword_name(expr))), ('raw-html', lambda: raw_html_content(expr)), (None, lambda: render_value_to_html(trampoline(eval_expr(expr, env)), env))])
|
||||
|
||||
# render-value-to-html
|
||||
render_value_to_html = lambda val, env: _sx_case(type_of(val), [('nil', lambda: ''), ('string', lambda: escape_html(val)), ('number', lambda: sx_str(val)), ('boolean', lambda: ('true' if sx_truthy(val) else 'false')), ('list', lambda: render_list_to_html(val, env)), ('raw-html', lambda: raw_html_content(val)), ('style-value', lambda: style_value_class(val)), (None, lambda: escape_html(sx_str(val)))])
|
||||
render_value_to_html = lambda val, env: _sx_case(type_of(val), [('nil', lambda: ''), ('string', lambda: escape_html(val)), ('number', lambda: sx_str(val)), ('boolean', lambda: ('true' if sx_truthy(val) else 'false')), ('list', lambda: render_list_to_html(val, env)), ('raw-html', lambda: raw_html_content(val)), (None, lambda: escape_html(sx_str(val)))])
|
||||
|
||||
# RENDER_HTML_FORMS
|
||||
RENDER_HTML_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defmacro', 'defstyle', 'defkeyframes', 'defhandler', 'map', 'map-indexed', 'filter', 'for-each']
|
||||
RENDER_HTML_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'map', 'map-indexed', 'filter', 'for-each']
|
||||
|
||||
# render-html-form?
|
||||
is_render_html_form = lambda name: contains_p(RENDER_HTML_FORMS, name)
|
||||
|
||||
@@ -1,782 +0,0 @@
|
||||
"""
|
||||
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",
|
||||
"table-row": "display:table-row",
|
||||
"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-0": "flex:0",
|
||||
"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-5": "margin-top:1.25rem",
|
||||
"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-3": "padding-left:.75rem",
|
||||
"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-14": "width:3.5rem",
|
||||
"w-16": "width:4rem",
|
||||
"w-20": "width:5rem",
|
||||
"w-24": "width:6rem",
|
||||
"w-28": "width:7rem",
|
||||
"w-32": "width:8rem",
|
||||
"w-40": "width:10rem",
|
||||
"w-48": "width:12rem",
|
||||
"w-56": "width:14rem",
|
||||
"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-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-0": "max-width:0",
|
||||
"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-w-[640px]": "max-width:640px",
|
||||
"max-h-32": "max-height:8rem",
|
||||
"max-h-64": "max-height:16rem",
|
||||
"max-h-72": "max-height:18rem",
|
||||
"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-md": "font-size:1rem;line-height:1.5rem", # alias for text-base
|
||||
"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": "color:rgb(159 18 57)",
|
||||
"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-green-900": "color:rgb(20 83 45)",
|
||||
"text-neutral-400": "color:rgb(163 163 163)",
|
||||
"text-neutral-500": "color:rgb(115 115 115)",
|
||||
"text-neutral-600": "color:rgb(82 82 82)",
|
||||
"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)",
|
||||
"text-violet-900": "color:rgb(76 29 149)",
|
||||
|
||||
# ── 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-green-200": "background-color:rgb(187 247 208)",
|
||||
"bg-neutral-50/70": "background-color:rgba(250,250,250,.7)",
|
||||
"bg-black/70": "background-color:rgba(0,0,0,.7)",
|
||||
"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)",
|
||||
"bg-violet-700": "background-color:rgb(109 40 217)",
|
||||
"bg-amber-200": "background-color:rgb(253 230 138)",
|
||||
"bg-blue-700": "background-color:rgb(29 78 216)",
|
||||
"bg-emerald-700": "background-color:rgb(4 120 87)",
|
||||
"bg-purple-700": "background-color:rgb(126 34 206)",
|
||||
"bg-stone-50/60": "background-color:rgba(250,250,249,.6)",
|
||||
|
||||
# ── 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": "border-left-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-neutral-200": "border-color:rgb(229 229 229)",
|
||||
"border-red-400": "border-color:rgb(248 113 113)",
|
||||
"border-stone-400": "border-color:rgb(168 162 158)",
|
||||
"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-90": "opacity:.9",
|
||||
"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))",
|
||||
"ring-stone-300": "--tw-ring-color:rgb(214 211 209)",
|
||||
"ring-stone-500": "--tw-ring-color:rgb(120 113 108)",
|
||||
"ring-violet-500": "--tw-ring-color:rgb(139 92 246)",
|
||||
"ring-blue-500": "--tw-ring-color:rgb(59 130 246)",
|
||||
"ring-green-500": "--tw-ring-color:rgb(22 163 74)",
|
||||
"ring-purple-500": "--tw-ring-color:rgb(147 51 234)",
|
||||
|
||||
# ── Overflow ─────────────────────────────────────────────────────────
|
||||
"overflow-hidden": "overflow:hidden",
|
||||
"overflow-x-auto": "overflow-x:auto",
|
||||
"overflow-y-auto": "overflow-y:auto",
|
||||
"overflow-visible": "overflow:visible",
|
||||
"overflow-y-visible": "overflow-y:visible",
|
||||
"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]",
|
||||
"invalid": ":invalid",
|
||||
"placeholder-shown": ":placeholder-shown",
|
||||
"group-hover": ":is(.group:hover) &",
|
||||
"group-open": ":is(.group[open]) &",
|
||||
"group-open/cat": ":is(.group\\/cat[open]) &",
|
||||
"group-open/filter": ":is(.group\\/filter[open]) &",
|
||||
"group-open/root": ":is(.group\\/root[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}"),
|
||||
]
|
||||
@@ -1,254 +0,0 @@
|
||||
"""
|
||||
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,
|
||||
)
|
||||
@@ -303,30 +303,6 @@ 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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Continuation
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -368,4 +344,4 @@ class _ShiftSignal(BaseException):
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# An s-expression value after evaluation
|
||||
SExp = int | float | str | bool | Symbol | Keyword | Lambda | Macro | Component | Continuation | HandlerDef | RelationDef | PageDef | QueryDef | ActionDef | StyleValue | list | dict | _Nil | None
|
||||
SExp = int | float | str | bool | Symbol | Keyword | Lambda | Macro | Component | Continuation | HandlerDef | RelationDef | PageDef | QueryDef | ActionDef | list | dict | _Nil | None
|
||||
|
||||
Reference in New Issue
Block a user