Implement CSSX Phase 2: native SX style primitives
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:
2026-03-04 12:47:51 +00:00
parent 28388540d5
commit 19d59f5f4b
11 changed files with 1660 additions and 7 deletions

View File

@@ -611,6 +611,7 @@ details.group{{overflow:hidden}}details.group>summary{{list-style:none}}details.
</style>
</head>
<body class="bg-stone-50 text-stone-900">
<script type="text/sx-styles" data-hash="{styles_hash}">{styles_json}</script>
<script type="text/sx" data-components data-hash="{component_hash}">{component_defs}</script>
<script type="text/sx" data-mount="body">{page_sx}</script>
<script src="{asset_url}/scripts/sx.js?v={sx_js_hash}"></script>
@@ -681,6 +682,14 @@ def sx_page(ctx: dict, page_sx: str, *,
except Exception:
pass
# Style dictionary for client-side css primitive
styles_hash = _get_style_dict_hash()
client_styles_hash = _get_sx_styles_cookie()
if client_styles_hash and client_styles_hash == styles_hash:
styles_json = "" # Client has cached version
else:
styles_json = _build_style_dict_json()
return _SX_PAGE_TEMPLATE.format(
title=_html_escape(title),
asset_url=asset_url,
@@ -688,6 +697,8 @@ def sx_page(ctx: dict, page_sx: str, *,
csrf=_html_escape(csrf),
component_hash=component_hash,
component_defs=component_defs,
styles_hash=styles_hash,
styles_json=styles_json,
page_sx=page_sx,
sx_css=sx_css,
sx_css_classes=sx_css_classes,
@@ -697,6 +708,58 @@ def sx_page(ctx: dict, page_sx: str, *,
_SCRIPT_HASH_CACHE: dict[str, str] = {}
_STYLE_DICT_JSON: str = ""
_STYLE_DICT_HASH: str = ""
def _build_style_dict_json() -> str:
"""Build compact JSON style dictionary for client-side css primitive."""
global _STYLE_DICT_JSON, _STYLE_DICT_HASH
if _STYLE_DICT_JSON:
return _STYLE_DICT_JSON
import json
from .style_dict import (
STYLE_ATOMS, PSEUDO_VARIANTS, RESPONSIVE_BREAKPOINTS,
KEYFRAMES, ARBITRARY_PATTERNS, CHILD_SELECTOR_ATOMS,
)
# Derive child selector prefixes from CHILD_SELECTOR_ATOMS
prefixes = set()
for atom in CHILD_SELECTOR_ATOMS:
# "space-y-4" → "space-y-", "divide-y" → "divide-"
for sep in ("space-x-", "space-y-", "divide-x", "divide-y"):
if atom.startswith(sep):
prefixes.add(sep)
break
data = {
"a": STYLE_ATOMS,
"v": PSEUDO_VARIANTS,
"b": RESPONSIVE_BREAKPOINTS,
"k": KEYFRAMES,
"p": ARBITRARY_PATTERNS,
"c": sorted(prefixes),
}
_STYLE_DICT_JSON = json.dumps(data, separators=(",", ":"))
_STYLE_DICT_HASH = hashlib.md5(_STYLE_DICT_JSON.encode()).hexdigest()[:8]
return _STYLE_DICT_JSON
def _get_style_dict_hash() -> str:
"""Get the hash of the style dictionary JSON."""
if not _STYLE_DICT_HASH:
_build_style_dict_json()
return _STYLE_DICT_HASH
def _get_sx_styles_cookie() -> str:
"""Read the sx-styles-hash cookie from the current request."""
try:
from quart import request
return request.cookies.get("sx-styles-hash", "")
except Exception:
return ""
def _script_hash(filename: str) -> str: