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>
Streaming resolve scripts arrive after boot, so any extra component
defs sent as <script type="text/sx"> tags weren't being loaded.
Fix in the spec (boot.sx): call (process-sx-scripts nil) at the
start of resolve-suspense so late-arriving component defs are
available in componentEnv before rendering.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Server streams HTML shell with ~suspense placeholders immediately,
then sends resolution <script> chunks as async IO completes. Browser
renders loading skeletons instantly, replacing them with real content
as data arrives via __sxResolve().
- defpage :stream true opts pages into streaming response
- ~suspense component renders fallback with data-suspense attr
- resolve-suspense in boot.sx (spec) + bootstrapped to sx-browser.js
- __sxPending queue handles resolution before sx-browser.js loads
- execute_page_streaming() async generator with concurrent IO tasks
- Streaming demo page at /isomorphism/streaming with 1.5s simulated delay
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add boundary.sx declaring all 34 I/O primitives, 32 page helpers, and 9
allowed boundary types. Runtime validation in boundary.py checks every
registration against the spec — undeclared primitives/helpers crash at
startup with SX_BOUNDARY_STRICT=1 (now set in both dev and prod).
Key changes:
- Move 5 I/O-in-disguise primitives (app-url, asset-url, config,
jinja-global, relations-from) from primitives.py to primitives_io.py
- Remove duplicate url-for/route-prefix from primitives.py (already in IO)
- Fix parse-datetime to return ISO string instead of raw datetime
- Add datetime→isoformat conversion in _convert_result at the edge
- Wrap page helper return values with boundary type validation
- Replace all SxExpr(f"...") patterns with sx_call() or _sx_fragment()
- Add assert declaration to primitives.sx
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dict values (e.g. {:X-CSRFToken csrf}) passed as component kwargs were
not being evaluated through sxEval — symbols stayed unresolved in the DOM.
Also add Cache-Control: no-cache headers for /static/ in dev mode so
browser always fetches fresh JS/CSS without needing hard refresh.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sx-headers attributes now use native SX dict format {:key val} instead of
JSON strings. Eliminates manual JSON string construction in both .sx files
and Python callers.
- sx.js: parse sx-headers/sx-vals as SX dict ({: prefix) with JSON fallback,
add _serializeDict for dict→attribute serialization, fix verbInfo scope in
_doFetch error handler
- html.py: serialize dict attribute values via SX serialize() not str()
- All .sx files: {:X-CSRFToken csrf} replaces (str "{\"X-CSRFToken\": ...}")
- All Python callers: {"X-CSRFToken": csrf} dict replaces f-string JSON
- Blog like: extract ~blog-like-toggle, fix POST returning wrong component,
fix emoji escapes in .sx (parser has no \U support), fix card :hx-headers
keyword mismatch, wrap sx_content in SxExpr for evaluation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When an HTML tag like (div) appears as a kwarg value in SX wire format,
callComponent evaluates it with sxEval (data mode) which doesn't handle
HTML tags. Now sxEval delegates to renderDOM for any render expression
(HTML tags, SVG tags, fragments, raw!, components).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
_aser_component expands component bodies in SX wire format mode,
but was evaluating kwarg values with async_eval (HTML mode). This
caused SxExpr kwargs to be fully rendered to HTML strings, which
then broke when serialized back to SX — producing bare symbols
like 'div' that the client couldn't resolve.
Fix: use _aser() for kwarg evaluation to keep values in SX format.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Logs env keys (non-function) when a symbol lookup fails, to help
diagnose which component/context is missing the expected binding.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The JS contains? used `k in c` which throws TypeError on strings.
The Python version silently returned False for strings. Both now
use indexOf/`in` for substring matching on strings.
Fixes: sx.js MOUNT PARSE ERROR on blog index where
(contains? current-local-href "?") was evaluated client-side.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(filter (feTurbulence ...)) inside (svg ...) has no keyword first arg,
so the keyword-only check dispatched it as a HO function. Now also
check SVG/MathML context (ns in client, _svg_context in server).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Hyphenated names like app-url are variables, not custom elements.
Only treat as custom element when first arg is a Keyword (tag call pattern).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix filter/map dispatching as HO functions when used as SVG/HTML tags
(peek at first arg — Keyword means tag call, not function call)
- Add html: prefix escape hatch to force any name to render as an element
- Support custom elements (hyphenated names) per Web Components spec
- SVG/MathML namespace auto-detection: client threads ns param through
render chain; server uses _svg_context ContextVar so unknown tags
inside (svg ...) or (math ...) render as elements without enumeration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Soviet constructivist poster with paper texture filters, grid lines,
aged stain spots, and "(<x>)" symbol in red.
Add missing SVG filter primitive tags to both server (html.py) and
client (sx.js): feTurbulence, feColorMatrix, feBlend,
feComponentTransfer, feFuncR/G/B/A, feDisplacementMap, feComposite,
feFlood, feImage, feMorphology, feSpecularLighting, feDiffuseLighting,
fePointLight, feSpotLight, feDistantLight.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
Implement missing SxEngine features:
- SSE (sx-sse, sx-sse-swap) with EventSource management and auto-cleanup
- Response headers: SX-Trigger, SX-Retarget, SX-Reswap, SX-Redirect,
SX-Refresh, SX-Location, SX-Replace-Url, SX-Trigger-After-Swap/Settle
- View Transitions API: transition:true swap modifier + global config
- every:<time> trigger for polling (setInterval)
- sx-replace-url (replaceState instead of pushState)
- sx-disabled-elt (disable elements during request)
- sx-prompt (window.prompt, value sent as SX-Prompt header)
- sx-params (filter form parameters: *, none, not x,y, x,y)
Adds docs (ATTR_DETAILS, BEHAVIOR_ATTRS, headers, events), demo
components in reference.sx, API endpoints (prompt-echo, sse-time),
and 27 new unit tests for engine logic.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add sx-preserve/sx-ignore (morph skip), sx-indicator (loading element),
sx-validate (form validation), sx-boost (progressive enhancement),
sx-preload (hover prefetch with 30s cache), and sx-optimistic (instant
UI preview with rollback). Move all from HTMX_MISSING_ATTRS to
SX_UNIQUE_ATTRS with full ATTR_DETAILS docs and reference.sx demos.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both evaluators now use thunk-based trampolining to eliminate stack
overflow on deep tail recursion (verified at 50K+ depth). Mirrors
the sync evaluator TCO added in 5069072.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Re-read verb URL from element attributes at execution time so morphed
nav links navigate to the correct destination
- Reset retry backoff on fresh requests; skip error modal when sx-retry
handles the failure
- Strip attribute selectors in CSS registry so aria-selected:* classes
resolve correctly for on-demand CSS
- Add @css annotations for dynamic aria-selected variant classes
- Add SX docs integration test suite (102 tests)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Nest admin header inside post-header-child (layouts.py/helpers.py) so
full-page DOM matches OOB swap structure, eliminating duplicate headers
- Clear post-header-child on post layout OOB to remove stale admin rows
- Read SX initial content from #sx-content-input instead of
window.__SX_INITIAL__ to avoid escaping issues through SX pipeline
- Fix client-side SX parser RE_STRING to handle escaped newlines
- Clear root element in SxEditor.mount() to prevent double content on
HTMX re-mount
- Remove unused ~blog-editor-sx-initial component
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sx.js only appended INPUT values to GET request URLs. SELECT and
TEXTAREA elements with a name attribute were silently ignored,
so the category parameter was never sent.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wrap 15 call sites in events/sx_components.py where sx-generating
functions were passed as plain strings to sx_call(), causing raw
s-expression source to leak into the rendered page.
- Add dev-mode pretty-printing (RELOAD=true) for sx responses and
full page sx source — indented output in Network tab and View Source.
- Fix Sx.render to handle multiple top-level expressions by falling
back to parseAll and returning a DocumentFragment.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both Python and JS parsers used next_token() which returns plain strings
for both delimiter characters and string values, making them
indistinguishable. A string whose value is ")" or "(" would be
misinterpreted as a structural delimiter, causing parse errors.
Fix: use peek() (raw character) for all structural decisions in
parseExpr before consuming via next_token(). Also add enhanced error
logging to sx.js mount/loadComponents for easier future debugging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When server omits component source (cache hit), the script tag has
empty textContent. The early `if (!text.trim()) continue` was
skipping the data-components handler entirely, so components never
loaded from localStorage.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Server computes SHA-256 hash of all component source at startup.
Client signals its cached hash via cookie (sx-comp-hash). On full
page load: cookie match → server sends empty script tag with just
the hash; mismatch → sends full source. Client loads from
localStorage on hit, parses inline + caches on miss.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Process sx-swap-oob and hx-swap-oob elements in the popstate handler
so sidebar, filter, menu, and headers are restored on back navigation
- Disable the 62.5% base font-size hack that leaked globally and caused
all fonts to shrink when navigating to/from the editor
- Cache-bust sx.js to v=20260301d
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Parse tw.css into per-class lookup registry at startup
- Pre-scan component CSS classes at registration time (avoid per-request regex)
- Compress SX-Css header: 8-char hash replaces full class list (LRU cache)
- Add ;@css comment annotation for dynamically constructed class names
- Safelist bg-sky-{100..400} in Tailwind config for menu-row-sx dynamic shades
- Client sends/receives hash, falls back gracefully on cache miss
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Always re-fetch on popstate (drop LRU cache) for fresh content on back/forward
- Save/restore scroll position via pushState
- Add id="root-header-child" to ~app-body so OOB swaps can target it
- Fix OOB renderers: nest root-row inside root-header-child swap instead of
separate OOB that clobbers it
- Fix 3+ header rows dropped: wrap all headers in single fragment instead of
concatenating outside (<> ...)
- Strip <script data-components> from text/sx responses before renderToString
- Fall back to location.assign for cross-origin pushState (SecurityError)
- Move blog/sx/nav.sx to shared/sx/templates/ so all services have nav components
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Scripts inserted via innerHTML/insertAdjacentHTML don't execute.
Add _activateScripts() to _swapContent that recreates script tags
(without type or type=text/javascript) as live elements. This fixes
editor.js not loading when navigating to edit pages via sx-get.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Scripts inserted via innerHTML (template.content) don't execute.
When raw! renders HTML containing <script> tags, recreate them as
live elements so the browser fetches and executes them. Fixes
editor.js not loading on HTMX navigation to edit pages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three issues with the eager kwarg evaluation in renderComponentDOM and
renderStrComponent:
1. Data arrays (e.g. tags list of dicts) were being passed to sxEval
which tried to call a dict as a function — causing blank pages.
Fix: only evaluate arrays with a Symbol head (actual expressions);
pass data arrays through as-is.
2. Expression arrays like (get t "src") inside map lambdas lost their
scope when deferred — causing "get,t,src" URLs. Fix: eagerly evaluate
these Symbol-headed expressions in the caller's env.
3. Bare symbol `t` used as boolean in editor.sx threw "Undefined symbol".
Fix: use `true` literal instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
renderComponentDOM now eagerly renders kwarg values that are render
expressions (HTML tags, <>, ~components) into DOM nodes. But renderDOM
treated any non-array object as a dict and returned an empty fragment,
silently discarding pre-rendered content. Add a nodeType check to pass
DOM nodes through unchanged.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The string renderer's component call had the same deferred-evaluation
bug — and this is the path actually used for blog card rendering via
renderToString. Apply the same _isRenderExpr check to route render-only
forms through renderStr while data expressions go through sxEval.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous fix eagerly evaluated all kwarg expressions via sxEval,
which broke render-only forms (<>, raw!, HTML tags, ~components) that
only exist in the render pipeline. Now detect render expressions by
checking if the head symbol is an HTML/SVG tag, <>, raw!, or ~component,
and route those through renderDOM while data expressions still go
through sxEval for correct scope resolution.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
renderComponentDOM was deferring evaluation of complex expressions
(arrays) passed as component kwargs, storing raw AST instead. When the
component body later used these values as attributes, the caller's env
(with lambda params like t, a) was no longer available, producing
stringified arrays like "get,t,src" as attribute values — which browsers
interpreted as relative URLs.
Evaluate all non-literal kwarg values eagerly in the caller's env,
matching the behavior of callComponent and the Python-side renderer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename all sexp directories, files, identifiers, and references to sx.
artdag/ excluded (separate media processing DSL).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>