Implement CSSX Phase 2: native SX style primitives
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Replace Tailwind class strings with native SX expressions: (css :flex :gap-4 :hover:bg-sky-200) instead of :class "flex gap-4 ..." - Add style_dict.py: 516 atoms, variants, breakpoints, keyframes, patterns - Add style_resolver.py: memoized resolver with variant splitting - Add StyleValue type to types.py (frozen dataclass with class_name, declarations, etc.) - Add css and merge-styles primitives to primitives.py - Add defstyle and defkeyframes special forms to evaluator.py and async_eval.py - Integrate StyleValue into html.py and async_eval.py render paths - Add register_generated_rule() to css_registry.py, fix media query selector - Add style dict JSON delivery with localStorage caching to helpers.py - Add client-side css primitive, resolver, and style injection to sx.js Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,7 @@ from __future__ import annotations
|
||||
import inspect
|
||||
from typing import Any
|
||||
|
||||
from .types import Component, Keyword, Lambda, Macro, NIL, Symbol
|
||||
from .types import Component, Keyword, Lambda, Macro, NIL, StyleValue, Symbol
|
||||
from .evaluator import _expand_macro, EvalError
|
||||
from .primitives import _PRIMITIVES
|
||||
from .primitives_io import IO_PRIMITIVES, RequestContext, execute_io
|
||||
@@ -294,6 +294,16 @@ async def _asf_defcomp(expr, env, ctx):
|
||||
return _sf_defcomp(expr, env)
|
||||
|
||||
|
||||
async def _asf_defstyle(expr, env, ctx):
|
||||
from .evaluator import _sf_defstyle
|
||||
return _sf_defstyle(expr, env)
|
||||
|
||||
|
||||
async def _asf_defkeyframes(expr, env, ctx):
|
||||
from .evaluator import _sf_defkeyframes
|
||||
return _sf_defkeyframes(expr, env)
|
||||
|
||||
|
||||
async def _asf_defmacro(expr, env, ctx):
|
||||
from .evaluator import _sf_defmacro
|
||||
return _sf_defmacro(expr, env)
|
||||
@@ -430,6 +440,8 @@ _ASYNC_SPECIAL_FORMS: dict[str, Any] = {
|
||||
"lambda": _asf_lambda,
|
||||
"fn": _asf_lambda,
|
||||
"define": _asf_define,
|
||||
"defstyle": _asf_defstyle,
|
||||
"defkeyframes": _asf_defkeyframes,
|
||||
"defcomp": _asf_defcomp,
|
||||
"defmacro": _asf_defmacro,
|
||||
"defhandler": _asf_defhandler,
|
||||
@@ -684,6 +696,18 @@ async def _arender_element(
|
||||
children.append(arg)
|
||||
i += 1
|
||||
|
||||
# Handle :style StyleValue — convert to class and register CSS rule
|
||||
style_val = attrs.get("style")
|
||||
if isinstance(style_val, StyleValue):
|
||||
from .css_registry import register_generated_rule
|
||||
register_generated_rule(style_val)
|
||||
existing_class = attrs.get("class")
|
||||
if existing_class and existing_class is not NIL and existing_class is not False:
|
||||
attrs["class"] = f"{existing_class} {style_val.class_name}"
|
||||
else:
|
||||
attrs["class"] = style_val.class_name
|
||||
del attrs["style"]
|
||||
|
||||
class_val = attrs.get("class")
|
||||
if class_val is not None and class_val is not NIL and class_val is not False:
|
||||
collector = css_class_collector.get(None)
|
||||
@@ -897,6 +921,8 @@ _ASYNC_RENDER_FORMS: dict[str, Any] = {
|
||||
"begin": _arsf_begin,
|
||||
"do": _arsf_begin,
|
||||
"define": _arsf_define,
|
||||
"defstyle": _arsf_define,
|
||||
"defkeyframes": _arsf_define,
|
||||
"defcomp": _arsf_define,
|
||||
"defmacro": _arsf_define,
|
||||
"defhandler": _arsf_define,
|
||||
@@ -1125,23 +1151,54 @@ async def _aser_call(
|
||||
"""Serialize ``(name :key val child ...)`` — evaluate args but keep
|
||||
as sx source instead of rendering to HTML."""
|
||||
parts = [name]
|
||||
extra_class: str | None = None # from :style StyleValue conversion
|
||||
i = 0
|
||||
while i < len(args):
|
||||
arg = args[i]
|
||||
if isinstance(arg, Keyword) and i + 1 < len(args):
|
||||
val = await _aser(args[i + 1], env, ctx)
|
||||
if val is not NIL and val is not None:
|
||||
parts.append(f":{arg.name}")
|
||||
parts.append(serialize(val))
|
||||
# :style StyleValue → convert to :class and register CSS
|
||||
if arg.name == "style" and isinstance(val, StyleValue):
|
||||
from .css_registry import register_generated_rule
|
||||
register_generated_rule(val)
|
||||
extra_class = val.class_name
|
||||
else:
|
||||
parts.append(f":{arg.name}")
|
||||
parts.append(serialize(val))
|
||||
i += 2
|
||||
else:
|
||||
result = await _aser(arg, env, ctx)
|
||||
if result is not NIL and result is not None:
|
||||
parts.append(serialize(result))
|
||||
i += 1
|
||||
# If we converted a :style to a class, merge into existing :class or add it
|
||||
if extra_class:
|
||||
_merge_class_into_parts(parts, extra_class)
|
||||
return SxExpr("(" + " ".join(parts) + ")")
|
||||
|
||||
|
||||
def _merge_class_into_parts(parts: list[str], class_name: str) -> None:
|
||||
"""Merge an extra class name into the serialized parts list.
|
||||
|
||||
If :class already exists, append to it. Otherwise add :class.
|
||||
"""
|
||||
for i, p in enumerate(parts):
|
||||
if p == ":class" and i + 1 < len(parts):
|
||||
# Existing :class — append our class
|
||||
existing = parts[i + 1]
|
||||
if existing.startswith('"') and existing.endswith('"'):
|
||||
# Quoted string — insert before closing quote
|
||||
parts[i + 1] = existing[:-1] + " " + class_name + '"'
|
||||
else:
|
||||
# Expression — wrap in (str ...)
|
||||
parts[i + 1] = f'(str {existing} " {class_name}")'
|
||||
return
|
||||
# No existing :class — add one
|
||||
parts.insert(1, f'"{class_name}"')
|
||||
parts.insert(1, ":class")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Serialize-mode special forms
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1347,6 +1404,8 @@ _ASER_FORMS: dict[str, Any] = {
|
||||
"lambda": _assf_lambda,
|
||||
"fn": _assf_lambda,
|
||||
"define": _assf_define,
|
||||
"defstyle": _assf_define,
|
||||
"defkeyframes": _assf_define,
|
||||
"defcomp": _assf_define,
|
||||
"defmacro": _assf_define,
|
||||
"defhandler": _assf_define,
|
||||
|
||||
Reference in New Issue
Block a user