Add CSSX Components plan: styling via defcomp instead of opaque style dict

Replace the existing CSSX plan with a component-based approach where styling
is handled by regular defcomp components that apply classes, respond to data,
and compose naturally — eliminating opaque hash-based class names.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 22:05:39 +00:00
parent 299de98ea8
commit 41c3b9f3b8
3 changed files with 103 additions and 6 deletions

View File

@@ -141,7 +141,9 @@
(dict :label "Glue Decoupling" :href "/plans/glue-decoupling"
:summary "Eliminate all cross-app model imports via glue service layer.")
(dict :label "Social Sharing" :href "/plans/social-sharing"
:summary "OAuth-based sharing to Facebook, Instagram, Threads, Twitter/X, LinkedIn, and Mastodon.")))
:summary "OAuth-based sharing to Facebook, Instagram, Threads, Twitter/X, LinkedIn, and Mastodon.")
(dict :label "CSSX Components" :href "/plans/cssx-components"
:summary "Styling as components — replace the style dictionary with regular defcomps that apply classes, respond to data, and compose naturally.")))
(define bootstrappers-nav-items (list
(dict :label "Overview" :href "/bootstrappers/")

View File

@@ -135,7 +135,7 @@
(~doc-section :title "Context" :id "context"
(p "The web is six incompatible formats duct-taped together: HTML for structure, CSS for style, JavaScript for behavior, JSON for data, server languages for backend logic, build tools for compilation. Moving anything between layers requires serialization, template languages, API contracts, and glue code. Federation (ActivityPub) adds a seventh — JSON-LD — which is inert data that every consumer must interpret from scratch and wrap in their own UI.")
(p "SX is already one evaluable format that does all six. A component definition is simultaneously structure, style (CSSX atoms), behavior (event handlers), data (the AST " (em "is") " data), server-renderable (Python evaluator), and client-renderable (JS evaluator). The pieces already exist: content-addressed DAG execution (artdag), IPFS storage with CIDs, OpenTimestamps Bitcoin anchoring, boundary-enforced sandboxing.")
(p "SX is already one evaluable format that does all six. A component definition is simultaneously structure, style (components apply classes and respond to data), behavior (event handlers), data (the AST " (em "is") " data), server-renderable (Python evaluator), and client-renderable (JS evaluator). The pieces already exist: content-addressed DAG execution (artdag), IPFS storage with CIDs, OpenTimestamps Bitcoin anchoring, boundary-enforced sandboxing.")
(p "SX-Activity wires these together into a new web. Everything — content, UI components, markdown parsers, syntax highlighters, validation logic, media, processing pipelines — is the same executable format, stored on a content-addressed network, running within each participant's own security context. " (strong "The wire format is the programming language is the component system is the package manager.")))
(~doc-section :title "Current State" :id "current-state"
@@ -394,7 +394,7 @@
(~doc-subsection :title "The insight"
(p "The web has six layers that don't talk to each other: HTML (structure), CSS (style), JavaScript (behavior), JSON (data interchange), server frameworks (backend logic), and build tools (compilation). Each has its own syntax, its own semantics, its own ecosystem. Moving data between them requires serialization, deserialization, template languages, API contracts, type coercion, and an endless parade of glue code.")
(p "SX collapses all six into one evaluable format. A component definition is simultaneously structure, style (CSSX atoms), behavior (event handlers), data (the AST is data), server-renderable (Python evaluator), and client-renderable (JS evaluator). There is no boundary between \"data\" and \"program\" — s-expressions are both.")
(p "SX collapses all six into one evaluable format. A component definition is simultaneously structure, style (components apply classes and respond to data), behavior (event handlers), data (the AST is data), server-renderable (Python evaluator), and client-renderable (JS evaluator). There is no boundary between \"data\" and \"program\" — s-expressions are both.")
(p "Once that's true, " (strong "everything becomes shareable.") " Not just UI components — markdown parsers, syntax highlighters, date formatters, validation logic, layout algorithms, color systems, animation curves. Any pure function over data. All content-addressed, all on IPFS, all executable within your own security context."))
(~doc-subsection :title "What travels on the network"
@@ -732,7 +732,7 @@
(li (code ":cid") " — content address of the canonical serialized source")
(li (code ":deps") " — dependency CIDs, not just names. A consumer can recursively resolve the entire tree by CID without name ambiguity")
(li (code ":pure") " — pre-computed purity flag. The consumer " (em "re-verifies") " this after fetching (never trust the manifest alone), but it enables fast rejection of IO-dependent components before downloading")
(li (code ":css-atoms") " — CSSX class names the component uses. The consumer can pre-resolve CSS rules without parsing the source")
(li (code ":deps") " includes style component CIDs. No separate " (code ":css-atoms") " field needed — styling is just more components")
(li (code ":params") " — parameter signature for tooling, documentation, IDE support")
(li (code ":author") " — who published this. AP actor URL, verifiable via HTTP Signatures")))
@@ -1570,8 +1570,7 @@
(li (strong "eval/parse/render: ") "Complete both sides. sx-ref.js has eval, parse, render-to-html, render-to-dom, aser.")
(li (strong "Engine: ") "engine.sx (morph, swaps, triggers, history), orchestration.sx (fetch, events), boot.sx (hydration) — all transpiled.")
(li (strong "Wire format: ") "Server _aser → SX source → client parses → renders to DOM. Boundary is clean.")
(li (strong "Component caching: ") "Hash-based localStorage for component definitions and style dictionaries.")
(li (strong "CSS on-demand: ") "CSSX resolves keywords to CSS rules, injects only used rules.")
(li (strong "Component caching: ") "Hash-based localStorage for component definitions.")
(li (strong "Boundary enforcement: ") "boundary.sx + SX_BOUNDARY_STRICT=1 validates all primitives/IO/helpers at registration.")
(li (strong "Dependency analysis: ") "deps.sx computes per-page component bundles — only definitions a page actually uses are sent.")
(li (strong "IO detection: ") "deps.sx classifies every component as pure or IO-dependent. Server expands IO components, serializes pure ones for client.")
@@ -2091,3 +2090,98 @@
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/suspense.sx")
(td :class "px-3 py-2 text-stone-700" "Streaming/suspension (new)")
(td :class "px-3 py-2 text-stone-600" "5"))))))))
;; ---------------------------------------------------------------------------
;; CSSX Components
;; ---------------------------------------------------------------------------
(defcomp ~plan-cssx-components-content ()
(~doc-page :title "CSSX Components"
(~doc-section :title "Context" :id "context"
(p "SX currently has a parallel CSS system: a style dictionary (JSON blob of atom-to-declaration mappings), a " (code "StyleValue") " type threaded through the evaluator and renderer, content-addressed hash class names (" (code "sx-a3f2b1") "), runtime CSS injection into " (code "<style id=\"sx-css\">") ", and a separate caching pipeline (" (code "<script type=\"text/sx-styles\">") ", localStorage, cookies).")
(p "This is ~300 lines of spec code (cssx.sx) plus platform interface (hash, regex, injection), plus server-side infrastructure (css_registry.py, tw.css parsing). All to solve one problem: " (em "resolving keyword atoms like ") (code ":flex :gap-4 :hover:bg-sky-200") (em " into CSS at render time."))
(p "The result: elements in the DOM get opaque class names like " (code "class=\"sx-a3f2b1\"") ". DevTools becomes useless. You can't inspect an element and understand its styling. " (strong "This is a deal breaker.")))
(~doc-section :title "The Idea" :id "idea"
(p (strong "Styling is just components.") " A CSSX component is a regular " (code "defcomp") " that decides how to style its children. It might apply Tailwind classes, or hand-written CSS classes, or inline styles, or generate rules at runtime. The implementation is the component's private business. The consumer just calls " (code "(~btn :variant \"primary\" \"Submit\")") " and doesn't care.")
(p "Because it's " (code "defcomp") ", you get everything for free: caching, bundling, dependency scanning, server/client rendering, composition. No parallel infrastructure.")
(p "Key advantages:")
(ul :class "list-disc pl-5 space-y-1 text-stone-700"
(li (strong "Readable DOM: ") "Elements have real class names, not content-addressed hashes. DevTools works.")
(li (strong "Data-driven styling: ") "Components receive data and decide styling. " (code "(~metric :value 150)") " renders red because " (code "value > 100") " — logic lives in the component, not a CSS preprocessor.")
(li (strong "One system: ") "No separate " (code "StyleValue") " type, no style dictionary JSON, no " (code "<script type=\"text/sx-styles\">") ", no " (code "sx-css") " injection. Components ARE the styling abstraction.")
(li (strong "One cache: ") "Component hash/localStorage handles everything. No separate style dict caching.")
(li (strong "Composable: ") (code "(~card :elevated true (~metric :value v))") " — styling composes like any other component.")
(li (strong "CSS-agnostic: ") "Works with Tailwind classes, BEM, CSS modules, inline styles, or any combination. Swap implementations without touching call sites.")))
(~doc-section :title "Examples" :id "examples"
(~doc-subsection :title "Simple class mapping"
(p "A button component that maps variant keywords to class strings:")
(highlight
"(defcomp ~btn (&key variant disabled &rest children)\n (button\n :class (str \"px-4 py-2 rounded font-medium transition \"\n (case variant\n \"primary\" \"bg-blue-600 text-white hover:bg-blue-700\"\n \"danger\" \"bg-red-600 text-white hover:bg-red-700\"\n \"ghost\" \"bg-transparent hover:bg-stone-100\"\n \"bg-stone-200 hover:bg-stone-300\")\n (when disabled \" opacity-50 cursor-not-allowed\"))\n :disabled disabled\n children))"
"lisp"))
(~doc-subsection :title "Data-driven styling"
(p "Styling that responds to data values — impossible with static CSS:")
(highlight
"(defcomp ~metric (&key value label threshold)\n (let ((t (or threshold 10)))\n (div :class (str \"p-3 rounded font-bold \"\n (cond\n ((> value (* t 10)) \"bg-red-500 text-white\")\n ((> value t) \"bg-amber-200 text-amber-900\")\n (:else \"bg-green-100 text-green-800\")))\n (span :class \"text-sm\" label) \": \" (span (str value)))))"
"lisp"))
(~doc-subsection :title "Style functions"
(p "Reusable style logic without wrapping — returns class strings:")
(highlight
"(define card-classes\n (fn (&key elevated bordered)\n (str \"rounded-lg p-4 \"\n (if elevated \"shadow-lg\" \"shadow-sm\")\n (when bordered \" border border-stone-200\"))))\n\n;; Usage: (div :class (card-classes :elevated true) ...)"
"lisp"))
(~doc-subsection :title "Responsive and interactive"
(p "Components can encode responsive breakpoints and interactive states as class strings — the same way you'd write Tailwind, but wrapped in a semantic component:")
(highlight
"(defcomp ~responsive-grid (&key cols &rest children)\n (div :class (str \"grid gap-4 \"\n (case (or cols 3)\n 1 \"grid-cols-1\"\n 2 \"grid-cols-1 md:grid-cols-2\"\n 3 \"grid-cols-1 md:grid-cols-2 lg:grid-cols-3\"\n 4 \"grid-cols-2 md:grid-cols-3 lg:grid-cols-4\"))\n children))"
"lisp")))
(~doc-section :title "What Changes" :id "changes"
(~doc-subsection :title "Remove"
(ul :class "list-disc pl-5 space-y-1 text-stone-700"
(li (code "StyleValue") " type and all plumbing (type checks in eval, render, serialize)")
(li (code "cssx.sx") " spec module (~300 lines: resolve-style, resolve-atom, split-variant, hash, injection)")
(li "Style dictionary JSON format, loading, caching (" (code "<script type=\"text/sx-styles\">") ", " (code "initStyleDict") ", " (code "parseAndLoadStyleDict") ")")
(li (code "<style id=\"sx-css\">") " runtime CSS injection system")
(li (code "css_registry.py") " server-side (builds style dictionary from tw.css)")
(li "Style dict cookies (" (code "sx-styles-hash") "), localStorage keys (" (code "sx-styles-src") ")")
(li "Platform interface: " (code "fnv1a-hash") ", " (code "compile-regex") ", " (code "regex-match") ", " (code "regex-replace-groups") ", " (code "make-style-value") ", " (code "inject-style-value"))))
(~doc-subsection :title "Keep"
(ul :class "list-disc pl-5 space-y-1 text-stone-700"
(li (code "defstyle") " — already just " (code "(defstyle name expr)") " which binds name to a value. Stays as sugar for defining reusable style values/functions. No " (code "StyleValue") " type needed — the value can be a string, a function, anything.")
(li (code "defkeyframes") " — could stay if we want declarative keyframe definitions. Or could become a component/function too.")
(li (code "tw.css") " — the compiled Tailwind stylesheet. Components reference its classes directly. No runtime resolution needed.")
(li (code ":class") " attribute — just takes strings now, no " (code "StyleValue") " special-casing.")))
(~doc-subsection :title "Add"
(p "Nothing new to the spec. CSSX components are just " (code "defcomp") ". The only new thing is a convention: components whose primary purpose is styling. They live in the same component files, cache the same way, bundle the same way.")))
(~doc-section :title "Migration" :id "migration"
(p "The existing codebase uses " (code ":class") " with plain Tailwind strings everywhere already. The CSSX style dictionary was an alternative path that was never widely adopted. Migration is mostly deletion:")
(ol :class "list-decimal pl-5 space-y-2 text-stone-700"
(li "Remove " (code "StyleValue") " type from " (code "types.py") ", " (code "render.sx") ", " (code "eval.sx") ", bootstrappers")
(li "Remove " (code "cssx.sx") " from spec modules and bootstrapper")
(li "Remove " (code "css_registry.py") " and style dict generation pipeline")
(li "Remove style dict loading from " (code "boot.sx") " (" (code "initStyleDict") ", " (code "queryStyleScripts") ")")
(li "Remove style-related cookies and localStorage from " (code "boot.sx") " platform interface")
(li "Remove " (code "StyleValue") " special-casing from " (code "render-attrs") " in " (code "render.sx") " and DOM adapter")
(li "Simplify " (code ":class") " / " (code ":style") " attribute handling — just strings")
(li "Convert any existing " (code "defstyle") " uses to return plain class strings instead of " (code "StyleValue") " objects"))
(p :class "mt-4 text-stone-600 italic" "Net effect: hundreds of lines of spec and infrastructure removed, zero new lines added. The component system already does everything CSSX was trying to do."))
(~doc-section :title "Relationship to Other Plans" :id "relationships"
(ul :class "list-disc pl-5 space-y-1 text-stone-700"
(li (strong "Content-Addressed Components: ") "CSSX components get CIDs like any other component. A " (code "~btn") " from one site can be shared to another via IPFS. No " (code ":css-atoms") " manifest field needed — the component carries its own styling logic.")
(li (strong "Isomorphic Rendering: ") "Components render the same on server and client. No style injection timing issues, no FOUC from late CSS loading.")
(li (strong "Component Bundling: ") "deps.sx already handles transitive component deps. Style components are just more components in the bundle — no separate style bundling.")))
(~doc-section :title "Philosophy" :id "philosophy"
(p "The web has spent two decades building increasingly complex CSS tooling: preprocessors, CSS-in-JS, atomic CSS, utility frameworks, design tokens, style dictionaries. Each solves a real problem but adds a new system with its own caching, bundling, and mental model.")
(p "CSSX components collapse all of this back to the simplest possible thing: " (strong "a function that takes data and returns markup with classes.") " That's what a component already is. There is no separate styling system because there doesn't need to be."))))

View File

@@ -531,6 +531,7 @@
"fragment-protocol" (~plan-fragment-protocol-content)
"glue-decoupling" (~plan-glue-decoupling-content)
"social-sharing" (~plan-social-sharing-content)
"cssx-components" (~plan-cssx-components-content)
:else (~plans-index-content)))
;; ---------------------------------------------------------------------------