- Extract shared components (empty-state, delete-btn, sentinel, crud-*, view-toggle, img-or-placeholder, avatar, sumup-settings-form, auth forms, order tables/detail/checkout) - Migrate all Python sx_call() callers to use shared components directly - Remove 55+ thin wrapper defcomps from domain .sx files - Remove trivial passthrough wrappers (blog-header-label, market-card-text, etc) - Unify duplicate auth flows (account + federation) into shared/sx/templates/auth.sx - Unify duplicate order views (cart + orders) into shared/sx/templates/orders.sx - Disable static file caching in dev (SEND_FILE_MAX_AGE_DEFAULT=0) - Add SX response validation and debug headers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
6.3 KiB
On-Demand CSS System — Replace Tailwind with Server-Driven Style Delivery
Context
The app recently moved from Tailwind CDN to a pre-built tw.css (92KB, Tailwind v4), but v4 broke styles since the classes target v3. Currently reverted to CDN for dev. Rather than fixing the v3/v4 mismatch, we replace Tailwind entirely with an on-demand CSS system: the server knows exactly which classes each response uses (because it renders sx→HTML), so it sends only the CSS rules needed — zero unused CSS, zero build step, zero Tailwind dependency.
This mirrors the existing SX-Components dedup protocol: client tells server what it has, server sends only what's new.
Phase 1: Minimal Viable System
Step 1: CSS Registry — shared/sx/css_registry.py (new file)
Parse tw.css at startup into a dict mapping HTML class names → CSS rule text.
load_css_registry(path)— parse tw.css once at startup, populate module-level_REGISTRY: dict[str, str]and_PREAMBLE: str(the@property/@layerdeclarations that define--tw-*vars)_css_selector_to_class(selector)— unescape CSS selectors (.sm\:hidden→sm:hidden,.h-\[60vh\]→h-[60vh])lookup_rules(classes: set[str]) -> str— return concatenated CSS for a set of class names, preserving source order from tw.cssget_preamble() -> str— return the preamble (sent once per page load)
The parser uses brace-depth tracking to split minified CSS into individual rules, extracts selectors, unescapes to get HTML class names. Rules inside @media blocks are stored with their wrapping @media.
Step 2: Class Collection During Render — shared/sx/html.py
Add a contextvars.ContextVar[set[str] | None] for collecting classes. In _render_element() (line ~460-469), when processing a class attribute, split the value and add to the collector if active.
# ~3 lines added in _render_element after the attr loop
if attr_name == "class" and attr_val:
collector = _css_class_collector.get(None)
if collector is not None:
collector.update(str(attr_val).split())
Step 3: Sx Source Scanner — shared/sx/css_registry.py
For sx pages where the body is rendered client-side (sx source sent as text, not pre-rendered HTML), we can't use the render-time collector. Instead, scan sx source text for :class "..." patterns:
def scan_classes_from_sx(source: str) -> set[str]:
"""Extract class names from :class "..." in sx source text."""
This runs on both component definitions and page sx source at response time.
Step 4: Wire Into Responses — shared/sx/helpers.py
sx_page() (full page loads, line 416):
- Scan
component_defs+page_sxfor classes viascan_classes_from_sx() - Look up rules + preamble from registry
- Replace
<script src="https://cdn.tailwindcss.com...">in_SX_PAGE_TEMPLATEwith<style id="sx-css">{preamble}\n{rules}</style> - Add
<meta name="sx-css-classes" content="{comma-separated classes}">so client knows what it has
sx_response() (fragment swaps, line 325):
- Read
SX-Cssheader from request (client's known classes) - Scan the sx source for classes
- Compute new classes = found - known
- If new rules exist, prepend
<style data-sx-css>{rules}</style>to response body - Set
SX-Css-Addresponse header with the new class names (so client can track without parsing CSS)
Step 5: Client-Side Tracking — shared/static/scripts/sx.js
Boot (page load):
- Read
<meta name="sx-css-classes">→ populate_sxCssKnowndict
Request header (in _doFetch, line ~1516):
- After sending
SX-Components, also sendSX-Css: flex,p-2,sm:hidden,...
Response handling (line ~1641 for text/sx, ~1698 for HTML):
- Strip
<style data-sx-css>blocks from response, inject into<style id="sx-css">in<head> - Read
SX-Css-Addresponse header, merge class names into_sxCssKnown
Step 6: Jinja Path Fallback — shared/browser/templates/_types/root/_head.html
For non-sx pages still using Jinja templates, serve all CSS rules (full registry dump) in <style id="sx-css">. Add a Jinja global sx_css_all() in shared/sx/jinja_bridge.py that returns preamble + all rules. This is the same 92KB but self-hosted with no Tailwind dependency. These pages can be optimized later.
Step 7: Startup — shared/sx/jinja_bridge.py
Call load_css_registry() in setup_sx_bridge() after loading components.
Files to Modify
| File | Change |
|---|---|
shared/sx/css_registry.py |
NEW — registry, parser, scanner, lookup |
shared/sx/html.py |
Add contextvar + 3 lines in _render_element |
shared/sx/helpers.py |
Modify _SX_PAGE_TEMPLATE, sx_page(), sx_response() |
shared/sx/jinja_bridge.py |
Call load_css_registry() at startup, add sx_css_all() Jinja global |
shared/static/scripts/sx.js |
_sxCssKnown tracking, SX-Css header, response CSS injection |
shared/browser/templates/_types/root/_head.html |
Replace Tailwind CDN with {{ sx_css_all() }} |
Key Design Decisions
- Source of truth: tw.css (already compiled, known to be correct for current classes)
- Dedup model: Mirrors
SX-Components— client declares what it has, server sends the diff - Header size: ~599 class names × ~10 chars ≈ 6KB header — within limits, can optimize later with hashing
- CSS ordering: Registry preserves tw.css source order (later rules win for equal specificity)
- Preamble:
--tw-*custom property defaults (~2KB) always included on first page load
Verification
- Start blog service:
./dev.sh blog - Load
https://blog.rose-ash.com/index/— verify styles match current CDN appearance - View page source — should have
<style id="sx-css">instead of Tailwind CDN script - Navigate via sx swap (click a post) — check DevTools Network tab for
SX-Cssrequest header andSX-Css-Addresponse header - Inspect
<style id="sx-css">— should grow as new pages introduce new classes - Check non-sx pages still render correctly (full CSS dump fallback)
Phase 2 (Future)
- Component-level pre-computation: Pre-scan classes per component at registration time
- Own rule generator: Replace tw.css parsing with a Python rule engine (no Tailwind dependency at all)
- Header compression: Use bitfield or hash instead of full class list
- Critical CSS: Only inline above-fold CSS, lazy-load rest