Merge branch 'worktree-cssx-components' into macros
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 14m0s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 14m0s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
421
sx/sx/cssx.sx
Normal file
421
sx/sx/cssx.sx
Normal file
@@ -0,0 +1,421 @@
|
||||
;; CSSX — Styling as Components
|
||||
;; Documentation for the CSSX approach: no parallel style infrastructure,
|
||||
;; just defcomp components that decide how to style their children.
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Overview
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~cssx-overview-content ()
|
||||
(~doc-page :title "CSSX Components"
|
||||
|
||||
(~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."))
|
||||
|
||||
(~doc-section :title "Why Not a Style Dictionary?" :id "why"
|
||||
(p "SX previously had 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, and a separate caching pipeline (cookies, localStorage).")
|
||||
(p "This was ~3,000 lines of code across the spec, bootstrappers, and host implementations. "
|
||||
"It was never adopted. The codebase voted with its feet: " (code ":class") " strings "
|
||||
"with " (code "defcomp") " already covered every real use case.")
|
||||
(p "The result of that system: elements in the DOM got opaque class names like "
|
||||
(code "class=\"sx-a3f2b1\"") ". DevTools became useless. You couldn't inspect an "
|
||||
"element and understand its styling. " (strong "That was a deal breaker.")))
|
||||
|
||||
(~doc-section :title "Key Advantages" :id "advantages"
|
||||
(ul :class "list-disc pl-5 space-y-2 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 injection pipeline. 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 "Strategy-agnostic: ") "A component can apply Tailwind classes, emit "
|
||||
(code "<style>") " blocks, use inline styles, generate CSS custom properties, or "
|
||||
"any combination. Swap strategies without touching call sites.")))
|
||||
|
||||
(~doc-section :title "What Changed" :id "changes"
|
||||
(~doc-subsection :title "Removed (~3,000 lines)"
|
||||
(ul :class "list-disc pl-5 space-y-1 text-stone-600 text-sm"
|
||||
(li (code "StyleValue") " type and all plumbing (type checks in eval, render, serialize)")
|
||||
(li (code "cssx.sx") " spec module (resolve-style, resolve-atom, split-variant, hash, injection)")
|
||||
(li "Style dictionary JSON format, loading, caching (" (code "<script type=\"text/sx-styles\">") ", localStorage)")
|
||||
(li (code "style_dict.py") " (782 lines) and " (code "style_resolver.py") " (254 lines)")
|
||||
(li (code "css") " and " (code "merge-styles") " primitives")
|
||||
(li "Platform interface: " (code "fnv1a-hash") ", " (code "compile-regex") ", " (code "make-style-value") ", " (code "inject-style-value"))
|
||||
(li (code "defkeyframes") " special form")
|
||||
(li "Style dict cookies and localStorage keys")))
|
||||
|
||||
(~doc-subsection :title "Kept"
|
||||
(ul :class "list-disc pl-5 space-y-1 text-stone-600 text-sm"
|
||||
(li (code "defstyle") " — simplified to bind any value (string, function, etc.)")
|
||||
(li (code "tw.css") " — the compiled Tailwind stylesheet, delivered via CSS class tracking")
|
||||
(li (code ":class") " attribute — just takes strings, no special-casing")
|
||||
(li "CSS class delivery (" (code "SX-Css") " headers, " (code "<style id=\"sx-css\">") ")")
|
||||
(li "All component infrastructure (defcomp, caching, bundling, deps)")))
|
||||
|
||||
(~doc-subsection :title "Added"
|
||||
(p "Nothing. CSSX components are just " (code "defcomp") ". The only new thing is "
|
||||
"a convention: components whose primary purpose is styling.")))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Patterns
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~cssx-patterns-content ()
|
||||
(~doc-page :title "Patterns"
|
||||
|
||||
(~doc-section :title "Class Mapping" :id "class-mapping"
|
||||
(p "The simplest pattern: a component that maps semantic 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")
|
||||
(p "Consumers call " (code "(~btn :variant \"primary\" \"Submit\")") ". The Tailwind "
|
||||
"classes are readable in DevTools but never repeated across call sites."))
|
||||
|
||||
(~doc-section :title "Data-Driven Styling" :id "data-driven"
|
||||
(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")
|
||||
(p "The component makes a " (em "decision") " about styling based on data. "
|
||||
"No CSS preprocessor or class name convention can express \"red when value > 100\"."))
|
||||
|
||||
(~doc-section :title "Style Functions" :id "style-functions"
|
||||
(p "Reusable style logic that returns class strings — no wrapping element needed:")
|
||||
(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:\n(div :class (card-classes :elevated true) ...)\n(article :class (card-classes :bordered true) ...)"
|
||||
"lisp")
|
||||
(p "Or with " (code "defstyle") " for named bindings:")
|
||||
(highlight
|
||||
"(defstyle card-base \"rounded-lg p-4 shadow-sm\")\n(defstyle card-elevated \"rounded-lg p-4 shadow-lg\")\n\n(div :class card-base ...)"
|
||||
"lisp"))
|
||||
|
||||
(~doc-section :title "Responsive Layouts" :id "responsive"
|
||||
(p "Components that encode responsive breakpoints:")
|
||||
(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 "Emitting CSS Directly" :id "emitting-css"
|
||||
(p "Components are not limited to referencing existing classes. They can generate "
|
||||
"CSS — " (code "<style>") " tags, keyframes, custom properties — as part of their output:")
|
||||
(highlight
|
||||
"(defcomp ~pulse (&key color duration &rest children)\n (<>\n (style (str \"@keyframes sx-pulse {\"\n \"0%,100% { opacity:1 } 50% { opacity:.5 } }\"))\n (div :style (str \"animation: sx-pulse \" (or duration \"2s\") \" infinite;\"\n \"color:\" (or color \"inherit\"))\n children)))"
|
||||
"lisp")
|
||||
(highlight
|
||||
"(defcomp ~theme (&key primary surface &rest children)\n (<>\n (style (str \":root {\"\n \"--color-primary:\" (or primary \"#7c3aed\") \";\"\n \"--color-surface:\" (or surface \"#fafaf9\") \"}\"))\n children))"
|
||||
"lisp")
|
||||
(p "The CSS strategy is the component's private implementation detail. Consumers call "
|
||||
(code "(~pulse :color \"red\" \"Loading...\")") " or "
|
||||
(code "(~theme :primary \"#2563eb\" ...)") " without knowing or caring whether the "
|
||||
"component uses classes, inline styles, generated rules, or all three."))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Async CSS
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~cssx-async-content ()
|
||||
(~doc-page :title "Async CSS"
|
||||
|
||||
(~doc-section :title "The Pattern" :id "pattern"
|
||||
(p "A CSSX component that needs CSS it doesn't have yet can "
|
||||
(strong "fetch and cache it before rendering") ". This is just "
|
||||
(code "~suspense") " combined with a style component — no new infrastructure:")
|
||||
(highlight
|
||||
"(defcomp ~styled (&key css-url css-hash fallback &rest children)\n (if (css-cached? css-hash)\n ;; Already have it — render immediately\n children\n ;; Don't have it — suspense while we fetch\n (~suspense :id (str \"css-\" css-hash)\n :fallback (or fallback (span \"\"))\n (do\n (fetch-css css-url css-hash)\n children))))"
|
||||
"lisp")
|
||||
(p "The consumer never knows:")
|
||||
(highlight
|
||||
"(~styled :css-url \"/css/charts.css\" :css-hash \"abc123\"\n (~bar-chart :data metrics))"
|
||||
"lisp"))
|
||||
|
||||
(~doc-section :title "Use Cases" :id "use-cases"
|
||||
(~doc-subsection :title "Federated Components"
|
||||
(p "A " (code "~btn") " from another site arrives via IPFS with a CID pointing "
|
||||
"to its CSS. The component fetches and caches it before rendering. "
|
||||
"No coordination needed between sites.")
|
||||
(highlight
|
||||
"(defcomp ~federated-widget (&key cid &rest children)\n (let ((css-cid (str cid \"/style.css\"))\n (cached (css-cached? css-cid)))\n (if cached\n children\n (~suspense :id (str \"fed-\" cid)\n :fallback (div :class \"animate-pulse bg-stone-100 rounded h-20\")\n (do (fetch-css (str \"https://ipfs.io/ipfs/\" css-cid) css-cid)\n children)))))"
|
||||
"lisp"))
|
||||
|
||||
(~doc-subsection :title "Heavy UI Libraries"
|
||||
(p "Code editors, chart libraries, rich text editors — their CSS only loads "
|
||||
"when the component actually appears on screen:")
|
||||
(highlight
|
||||
"(defcomp ~code-editor (&key language value on-change)\n (~styled :css-url \"/css/codemirror.css\" :css-hash (asset-hash \"codemirror\")\n :fallback (pre :class \"p-4 bg-stone-900 text-stone-300 rounded\" value)\n (div :class \"cm-editor\"\n :data-language language\n :data-value value)))"
|
||||
"lisp"))
|
||||
|
||||
(~doc-subsection :title "Lazy Themes"
|
||||
(p "Theme CSS loads on first use, then is instant on subsequent visits:")
|
||||
(highlight
|
||||
"(defcomp ~lazy-theme (&key name &rest children)\n (let ((css-url (str \"/css/themes/\" name \".css\"))\n (hash (str \"theme-\" name)))\n (~styled :css-url css-url :css-hash hash\n :fallback children ;; render unstyled immediately\n children)))"
|
||||
"lisp")))
|
||||
|
||||
(~doc-section :title "How It Composes" :id "composition"
|
||||
(p "Async CSS composes with everything already in SX:")
|
||||
(ul :class "list-disc pl-5 space-y-1 text-stone-700"
|
||||
(li (code "~suspense") " handles the async gap with fallback content")
|
||||
(li "localStorage handles caching across sessions")
|
||||
(li (code "<style id=\"sx-css\">") " is the injection target (same as CSS class delivery)")
|
||||
(li "Component content-hashing tracks what the client has")
|
||||
(li "No new types, no new delivery protocol, no new spec code")))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Live Styles
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~cssx-live-content ()
|
||||
(~doc-page :title "Live Styles"
|
||||
|
||||
(~doc-section :title "Styles That Respond to Events" :id "concept"
|
||||
(p "Combine " (code "~live") " (SSE) or " (code "~ws") " (WebSocket) with style "
|
||||
"components, and you get styles that change in real-time in response to server "
|
||||
"events. No new infrastructure — just components receiving data through a "
|
||||
"persistent transport."))
|
||||
|
||||
(~doc-section :title "SSE: Live Theme Updates" :id "sse-theme"
|
||||
(p "A " (code "~live") " component declares a persistent connection to an SSE "
|
||||
"endpoint. When the server pushes a new event, " (code "resolveSuspense")
|
||||
" replaces the content:")
|
||||
(highlight
|
||||
"(~live :src \"/api/stream/brand\"\n (~suspense :id \"theme\"\n (~theme :primary \"#7c3aed\" :surface \"#fafaf9\")))"
|
||||
"lisp")
|
||||
(p "Server pushes a new theme:")
|
||||
(highlight
|
||||
"event: sx-resolve\ndata: {\"id\": \"theme\", \"sx\": \"(~theme :primary \\\"#2563eb\\\" :surface \\\"#1e1e2e\\\")\"}"
|
||||
"text")
|
||||
(p "The " (code "~theme") " component emits CSS custom properties. Everything "
|
||||
"using " (code "var(--color-primary)") " repaints instantly:")
|
||||
(highlight
|
||||
"(defcomp ~theme (&key primary surface)\n (style (str \":root {\"\n \"--color-primary:\" (or primary \"#7c3aed\") \";\"\n \"--color-surface:\" (or surface \"#fafaf9\") \"}\")))"
|
||||
"lisp"))
|
||||
|
||||
(~doc-section :title "SSE: Live Dashboard Metrics" :id "sse-metrics"
|
||||
(p "Style changes driven by live data — the component decides the visual treatment:")
|
||||
(highlight
|
||||
"(~live :src \"/api/stream/dashboard\"\n (~suspense :id \"cpu\"\n (~metric :value 0 :label \"CPU\" :threshold 80))\n (~suspense :id \"memory\"\n (~metric :value 0 :label \"Memory\" :threshold 90))\n (~suspense :id \"requests\"\n (~metric :value 0 :label \"RPS\" :threshold 1000)))"
|
||||
"lisp")
|
||||
(p "Server pushes updated values. " (code "~metric") " turns red when "
|
||||
(code "value > threshold") " — the styling logic lives in the component, "
|
||||
"not in CSS selectors or JavaScript event handlers."))
|
||||
|
||||
(~doc-section :title "WebSocket: Collaborative Design" :id "ws-design"
|
||||
(p "Bidirectional channel for real-time collaboration. A designer adjusts a color, "
|
||||
"all connected clients see the change:")
|
||||
(highlight
|
||||
"(~ws :src \"/ws/design-studio\"\n (~suspense :id \"canvas-theme\"\n (~theme :primary \"#7c3aed\")))"
|
||||
"lisp")
|
||||
(p "Client sends a color change:")
|
||||
(highlight
|
||||
";; Designer picks a new primary color\n(sx-send ws-conn '(theme-update :primary \"#dc2626\"))"
|
||||
"lisp")
|
||||
(p "Server broadcasts to all connected clients via " (code "sx-resolve") " — "
|
||||
"every client's " (code "~theme") " component re-renders with the new color."))
|
||||
|
||||
(~doc-section :title "Why This Works" :id "why"
|
||||
(p "Every one of these patterns is just a " (code "defcomp") " receiving data "
|
||||
"through a persistent transport. The styling strategy — CSS custom properties, "
|
||||
"class swaps, inline styles, " (code "<style>") " blocks — is the component's "
|
||||
"private business. The transport doesn't know or care.")
|
||||
(p "A parallel style system would have needed its own streaming, its own caching, "
|
||||
"its own delta protocol for each of these use cases — duplicating what components "
|
||||
"already do.")
|
||||
(p :class "mt-4 text-stone-500 italic"
|
||||
"Note: ~live and ~ws are planned (see Live Streaming). The patterns shown here "
|
||||
"will work as described once the streaming transport is implemented. The component "
|
||||
"and suspense infrastructure they depend on already exists."))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Comparison with CSS Technologies
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~cssx-comparison-content ()
|
||||
(~doc-page :title "Comparisons"
|
||||
|
||||
(~doc-section :title "styled-components / Emotion" :id "styled-components"
|
||||
(p (a :href "https://styled-components.com" :class "text-violet-600 hover:underline" "styled-components")
|
||||
" pioneered the idea that styling belongs in components. But it generates CSS "
|
||||
"at runtime, injects " (code "<style>") " tags, and produces opaque hashed class "
|
||||
"names (" (code "class=\"sc-bdfBwQ fNMpVx\"") "). Open DevTools and you see gibberish. "
|
||||
"It also carries significant runtime cost — parsing CSS template literals, hashing, "
|
||||
"deduplicating — and needs a separate SSR extraction step (" (code "ServerStyleSheet") ").")
|
||||
(p "CSSX components share the core insight (" (em "styling is a component concern")
|
||||
") but without the runtime machinery. When a component applies Tailwind classes, "
|
||||
"there's zero CSS generation overhead. When it does emit " (code "<style>")
|
||||
" blocks, it's explicit — not hidden behind a tagged template literal. "
|
||||
"And the DOM is always readable."))
|
||||
|
||||
(~doc-section :title "CSS Modules" :id "css-modules"
|
||||
(p (a :href "https://github.com/css-modules/css-modules" :class "text-violet-600 hover:underline" "CSS Modules")
|
||||
" scope class names to avoid collisions by rewriting them at build time: "
|
||||
(code ".button") " becomes " (code ".button_abc123")
|
||||
". This solves the global namespace problem but creates the same opacity issue — "
|
||||
"hashed names in the DOM that you can't grep for or reason about.")
|
||||
(p "CSSX components don't need scoping because component boundaries already provide "
|
||||
"isolation. A " (code "~btn") " owns its markup. There's nothing to collide with."))
|
||||
|
||||
(~doc-section :title "Tailwind CSS" :id "tailwind"
|
||||
(p "Tailwind is " (em "complementary") ", not competitive. CSSX components are the "
|
||||
"semantic layer on top. Raw Tailwind in markup — "
|
||||
(code ":class \"px-4 py-2 bg-blue-600 text-white font-medium rounded hover:bg-blue-700\"")
|
||||
" — is powerful but verbose and duplicated across call sites.")
|
||||
(p "A CSSX component wraps that string once: " (code "(~btn :variant \"primary\" \"Submit\")")
|
||||
". The Tailwind classes are still there, readable in DevTools, but consumers don't "
|
||||
"repeat them. This is the same pattern Tailwind's own docs recommend ("
|
||||
(em "\"extracting components\"") ") — CSSX components are just SX's native way of doing it."))
|
||||
|
||||
(~doc-section :title "Vanilla Extract" :id "vanilla-extract"
|
||||
(p (a :href "https://vanilla-extract.style" :class "text-violet-600 hover:underline" "Vanilla Extract")
|
||||
" is zero-runtime CSS-in-JS: styles are written in TypeScript, compiled to static "
|
||||
"CSS at build time, and referenced by generated class names. It avoids the runtime "
|
||||
"cost of styled-components but still requires a build step, a bundler plugin, and "
|
||||
"TypeScript. The generated class names are again opaque.")
|
||||
(p "CSSX components need no build step for styling — they're evaluated at render time "
|
||||
"like any other component. And since the component chooses its own strategy, it can "
|
||||
"reference pre-built classes (zero runtime) " (em "or") " generate CSS on the fly — "
|
||||
"same API either way."))
|
||||
|
||||
(~doc-section :title "Design Tokens / Style Dictionary" :id "design-tokens"
|
||||
(p "The " (a :href "https://amzn.github.io/style-dictionary/" :class "text-violet-600 hover:underline" "Style Dictionary")
|
||||
" pattern — a JSON/YAML file mapping token names to values, compiled to "
|
||||
"platform-specific output — is essentially what the old CSSX was. It's the "
|
||||
"industry standard for design systems.")
|
||||
(p "The problem is that it's a parallel system: separate file format, separate build "
|
||||
"pipeline, separate caching, separate tooling. CSSX components eliminate all of "
|
||||
"that by expressing tokens as component parameters: "
|
||||
(code "(~theme :primary \"#7c3aed\")") " instead of "
|
||||
(code "{\"color\": {\"primary\": {\"value\": \"#7c3aed\"}}}")
|
||||
". Same result, no parallel infrastructure."))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Philosophy
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~cssx-philosophy-content ()
|
||||
(~doc-page :title "Philosophy"
|
||||
|
||||
(~doc-section :title "The Collapse" :id "collapse"
|
||||
(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."))
|
||||
|
||||
(~doc-section :title "Proof by Deletion" :id "proof"
|
||||
(p "The strongest validation: we built the full parallel system — style dictionary, "
|
||||
"StyleValue type, content-addressed hashing, runtime injection, localStorage "
|
||||
"caching — and then deleted it because nobody used it. The codebase already had "
|
||||
"the answer: " (code "defcomp") " with " (code ":class") " strings.")
|
||||
(p "3,000 lines of infrastructure removed. Zero lines added. Every use case still works."))
|
||||
|
||||
(~doc-section :title "The Right Abstraction Level" :id "abstraction"
|
||||
(p "CSS-in-JS puts styling " (em "below") " components — you style elements, then compose "
|
||||
"them. Utility CSS puts styling " (em "beside") " components — classes in markup, logic "
|
||||
"elsewhere. Both create a seam between what something does and how it looks.")
|
||||
(p "CSSX components put styling " (em "inside") " components — at the same level as "
|
||||
"structure and behavior. A " (code "~metric") " component knows its own thresholds, "
|
||||
"its own color scheme, its own responsive behavior. Styling is just another "
|
||||
"decision the component makes, not a separate concern."))
|
||||
|
||||
(~doc-section :title "Relationship to Other Plans" :id "relationships"
|
||||
(ul :class "list-disc pl-5 space-y-2 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.")
|
||||
(li (strong "Live Streaming: ") "SSE and WebSocket transports push data to components. "
|
||||
"Style components react to that data like any other component — no separate style "
|
||||
"streaming protocol.")))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; CSS Delivery
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~cssx-delivery-content ()
|
||||
(~doc-page :title "CSS Delivery"
|
||||
|
||||
(~doc-section :title "Multiple Strategies" :id "strategies"
|
||||
(p "A CSSX component chooses its own styling strategy — and each strategy has its "
|
||||
"own delivery path:")
|
||||
(ul :class "list-disc pl-5 space-y-1 text-stone-700"
|
||||
(li (strong "Tailwind classes: ") "Delivered via the on-demand protocol below. "
|
||||
"The server ships only the rules the page actually uses.")
|
||||
(li (strong "Inline styles: ") "No delivery needed — " (code ":style") " attributes "
|
||||
"are part of the markup.")
|
||||
(li (strong "Emitted " (code "<style>") " blocks: ") "Components can emit CSS rules "
|
||||
"directly. They arrive as part of the rendered HTML — keyframes, custom properties, "
|
||||
"scoped rules, anything.")
|
||||
(li (strong "External stylesheets: ") "Components can reference pre-loaded CSS files "
|
||||
"or lazy-load them via " (code "~suspense") " (see " (a :href "/cssx/async" "Async CSS") ").")
|
||||
(li (strong "Custom properties: ") "A " (code "~theme") " component sets "
|
||||
(code "--color-primary") " etc. via a " (code "<style>") " block. Everything "
|
||||
"using " (code "var()") " repaints automatically."))
|
||||
(p "The protocol below handles the Tailwind utility class case — the most common "
|
||||
"strategy — but it's not the only game in town."))
|
||||
|
||||
(~doc-section :title "On-Demand Tailwind Delivery" :id "on-demand"
|
||||
(p "When components use Tailwind utility classes, SX ships " (em "only the CSS rules "
|
||||
"actually used on the page") ", computed at render time. No build step, no purging, "
|
||||
"no unused CSS.")
|
||||
(p "The server pre-parses the full Tailwind CSS file into an in-memory registry at "
|
||||
"startup. When a response is rendered, SX scans all " (code ":class") " values in "
|
||||
"the output, looks up only those classes, and embeds the matching rules."))
|
||||
|
||||
(~doc-section :title "The Protocol" :id "protocol"
|
||||
(p "First page load gets the full set of used rules. Subsequent navigations send a "
|
||||
"hash of what the client already has, and the server ships only the delta:")
|
||||
(highlight
|
||||
"# First page load:\nGET / HTTP/1.1\n\nHTTP/1.1 200 OK\nContent-Type: text/html\n# Full CSS in <style id=\"sx-css\"> + hash in <meta name=\"sx-css-classes\">\n\n# Subsequent navigation:\nGET /about HTTP/1.1\nSX-Css: a1b2c3d4\n\nHTTP/1.1 200 OK\nContent-Type: text/sx\nSX-Css-Hash: e5f6g7h8\nSX-Css-Add: bg-blue-500,text-white,rounded-lg\n# Only new rules in <style data-sx-css>"
|
||||
"bash")
|
||||
(p "The client merges new rules into the existing " (code "<style id=\"sx-css\">")
|
||||
" block and updates its hash. No flicker, no duplicate rules."))
|
||||
|
||||
(~doc-section :title "Component-Aware Scanning" :id "scanning"
|
||||
(p "CSS scanning happens at two levels:")
|
||||
(ul :class "list-disc pl-5 space-y-1 text-stone-700"
|
||||
(li (strong "Registration time: ") "When a component is defined via " (code "defcomp")
|
||||
", its body is scanned for class strings. The component records which CSS classes "
|
||||
"it uses.")
|
||||
(li (strong "Render time: ") "The rendered output is scanned for any classes the "
|
||||
"static scan missed — dynamically constructed class strings, conditional classes, etc."))
|
||||
(p "This means the CSS registry knows roughly what a page needs before rendering, "
|
||||
"and catches any stragglers after."))
|
||||
|
||||
(~doc-section :title "Trade-offs" :id "tradeoffs"
|
||||
(ul :class "list-disc pl-5 space-y-2 text-stone-700"
|
||||
(li (strong "Full Tailwind in memory: ") "The parsed CSS registry is ~4MB. This is "
|
||||
"a one-time startup cost per app instance.")
|
||||
(li (strong "Regex scanning: ") "Class detection uses regex, so dynamically "
|
||||
"constructed class names (e.g. " (code "(str \"bg-\" color \"-500\")") ") can be "
|
||||
"missed. Use complete class strings in " (code "case") "/" (code "cond") " branches.")
|
||||
(li (strong "No @apply: ") "Tailwind's " (code "@apply") " is a build-time feature. "
|
||||
"On-demand delivery works with utility classes directly.")
|
||||
(li (strong "Tailwind-shaped: ") "The registry parser understands Tailwind's naming "
|
||||
"conventions. Non-Tailwind CSS works via " (code "<style>") " blocks in components.")))))
|
||||
@@ -85,28 +85,6 @@
|
||||
"Forms marked with a tail position enable " (a :href "/essays/tco" :class "text-violet-600 hover:underline" "tail-call optimization") " — recursive calls in tail position use constant stack space.")
|
||||
(div :class "space-y-10" forms))))
|
||||
|
||||
(defcomp ~docs-css-content ()
|
||||
(~doc-page :title "On-Demand CSS"
|
||||
(~doc-section :title "How it works" :id "how"
|
||||
(p :class "text-stone-600"
|
||||
"sx scans every response for CSS class names used in :class attributes. It looks up only those classes in a pre-parsed Tailwind CSS registry and ships just the rules that are needed. No build step. No purging. No unused CSS.")
|
||||
(p :class "text-stone-600"
|
||||
"On the first page load, the full set of used classes is embedded in a <style> block. A hash of the class set is stored. On subsequent navigations, the client sends the hash in the SX-Css header. The server computes the diff and sends only new rules via SX-Css-Add and a <style data-sx-css> block."))
|
||||
(~doc-section :title "The protocol" :id "protocol"
|
||||
(~doc-code :code (highlight "# First page load:\nGET / HTTP/1.1\n\nHTTP/1.1 200 OK\nContent-Type: text/html\n# Full CSS in <style id=\"sx-css\"> + hash in <meta name=\"sx-css-classes\">\n\n# Subsequent navigation:\nGET /about HTTP/1.1\nSX-Css: a1b2c3d4\n\nHTTP/1.1 200 OK\nContent-Type: text/sx\nSX-Css-Hash: e5f6g7h8\nSX-Css-Add: bg-blue-500,text-white,rounded-lg\n# Only new rules in <style data-sx-css>" "bash")))
|
||||
(~doc-section :title "Advantages" :id "advantages"
|
||||
(ul :class "space-y-2 text-stone-600"
|
||||
(li "Zero build step — no Tailwind CLI, no PostCSS, no purging")
|
||||
(li "Exact CSS — never ships a rule that isn't used on the page")
|
||||
(li "Incremental — subsequent navigations only ship new rules")
|
||||
(li "Component-aware — pre-scans component definitions at registration time")))
|
||||
(~doc-section :title "Disadvantages" :id "disadvantages"
|
||||
(ul :class "space-y-2 text-stone-600"
|
||||
(li "Requires the full Tailwind CSS file loaded in memory at startup (~4MB parsed)")
|
||||
(li "Regex-based class scanning — can miss dynamically constructed class names")
|
||||
(li "No @apply support — classes must be used directly")
|
||||
(li "Tied to Tailwind's utility class naming conventions")))))
|
||||
|
||||
(defcomp ~docs-server-rendering-content ()
|
||||
(~doc-page :title "Server Rendering"
|
||||
(~doc-section :title "Python API" :id "python"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(let* ((sc "aria-selected:bg-violet-200 aria-selected:text-violet-900")
|
||||
(items (list
|
||||
(dict :label "Docs" :href "/docs/introduction")
|
||||
(dict :label "CSSX" :href "/cssx/")
|
||||
(dict :label "Reference" :href "/reference/")
|
||||
(dict :label "Protocols" :href "/protocols/wire-format")
|
||||
(dict :label "Examples" :href "/examples/click-to-load")
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
(dict :label "Evaluator" :href "/docs/evaluator")
|
||||
(dict :label "Primitives" :href "/docs/primitives")
|
||||
(dict :label "Special Forms" :href "/docs/special-forms")
|
||||
(dict :label "CSS" :href "/docs/css")
|
||||
(dict :label "Server Rendering" :href "/docs/server-rendering")))
|
||||
|
||||
(define reference-nav-items (list
|
||||
@@ -55,13 +54,22 @@
|
||||
(dict :label "Request Abort" :href "/examples/sync-replace")
|
||||
(dict :label "Retry" :href "/examples/retry")))
|
||||
|
||||
(define cssx-nav-items (list
|
||||
(dict :label "Overview" :href "/cssx/")
|
||||
(dict :label "Patterns" :href "/cssx/patterns")
|
||||
(dict :label "Delivery" :href "/cssx/delivery")
|
||||
(dict :label "Async CSS" :href "/cssx/async")
|
||||
(dict :label "Live Styles" :href "/cssx/live")
|
||||
(dict :label "Comparisons" :href "/cssx/comparisons")
|
||||
(dict :label "Philosophy" :href "/cssx/philosophy")))
|
||||
|
||||
(define essays-nav-items (list
|
||||
(dict :label "Why S-Expressions" :href "/essays/why-sexps"
|
||||
:summary "Why SX uses s-expressions instead of HTML templates, JSX, or any other syntax.")
|
||||
(dict :label "The htmx/React Hybrid" :href "/essays/htmx-react-hybrid"
|
||||
:summary "How SX combines the server-driven simplicity of htmx with the component model of React.")
|
||||
(dict :label "On-Demand CSS" :href "/essays/on-demand-css"
|
||||
:summary "The CSSX system: keyword atoms resolved to class names, CSS rules injected on first use.")
|
||||
:summary "How SX delivers only the CSS each page needs — server scans rendered classes, sends the delta.")
|
||||
(dict :label "Client Reactivity" :href "/essays/client-reactivity"
|
||||
:summary "Reactive UI updates without a virtual DOM, diffing library, or build step.")
|
||||
(dict :label "SX Native" :href "/essays/sx-native"
|
||||
@@ -103,7 +111,6 @@
|
||||
(dict :label "SxEngine" :href "/specs/engine")
|
||||
(dict :label "Orchestration" :href "/specs/orchestration")
|
||||
(dict :label "Boot" :href "/specs/boot")
|
||||
(dict :label "CSSX" :href "/specs/cssx")
|
||||
(dict :label "Continuations" :href "/specs/continuations")
|
||||
(dict :label "call/cc" :href "/specs/callcc")
|
||||
(dict :label "Deps" :href "/specs/deps")
|
||||
@@ -147,8 +154,6 @@
|
||||
:summary "OAuth-based sharing to Facebook, Instagram, Threads, Twitter/X, LinkedIn, and Mastodon.")
|
||||
(dict :label "SX CI Pipeline" :href "/plans/sx-ci"
|
||||
:summary "Build, test, and deploy in s-expressions — CI pipelines as SX components.")
|
||||
(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.")
|
||||
(dict :label "Live Streaming" :href "/plans/live-streaming"
|
||||
:summary "SSE and WebSocket transports for re-resolving suspense slots after initial page load — live data, real-time collaboration.")))
|
||||
|
||||
@@ -177,7 +182,7 @@
|
||||
:prose "Special forms are the syntactic constructs whose arguments are NOT evaluated before dispatch. Each form has its own evaluation rules — unlike primitives, which receive pre-evaluated values. Together with primitives, special forms define the complete language surface. The registry covers control flow (if, when, cond, case, and, or), binding (let, letrec, define, set!), functions (lambda, defcomp, defmacro), sequencing (begin, do, thread-first), quoting (quote, quasiquote), continuations (reset, shift), guards (dynamic-wind), higher-order forms (map, filter, reduce), and domain-specific definitions (defstyle, defhandler, defpage, defquery, defaction).")
|
||||
(dict :slug "renderer" :filename "render.sx" :title "Renderer"
|
||||
:desc "Shared rendering registries and utilities used by all adapters."
|
||||
:prose "The renderer defines what is renderable and how arguments are parsed, but not the output format. It maintains registries of known HTML tags, SVG tags, void elements, and boolean attributes. It specifies how keyword arguments on elements become HTML attributes, how children are collected, and how special attributes (class, style, data-*) are handled. All three adapters (DOM, HTML, SX wire) share these definitions so they agree on what constitutes valid markup. The renderer also defines the StyleValue type used by the CSSX on-demand CSS system.")))
|
||||
:prose "The renderer defines what is renderable and how arguments are parsed, but not the output format. It maintains registries of known HTML tags, SVG tags, void elements, and boolean attributes. It specifies how keyword arguments on elements become HTML attributes, how children are collected, and how special attributes (class, style, data-*) are handled. All three adapters (DOM, HTML, SX wire) share these definitions so they agree on what constitutes valid markup.")))
|
||||
|
||||
(define adapter-spec-items (list
|
||||
(dict :slug "adapter-dom" :filename "adapter-dom.sx" :title "DOM Adapter"
|
||||
@@ -199,10 +204,7 @@
|
||||
(define browser-spec-items (list
|
||||
(dict :slug "boot" :filename "boot.sx" :title "Boot"
|
||||
:desc "Browser startup lifecycle: mount, hydrate, script processing."
|
||||
:prose "Boot handles the browser startup sequence and provides the public API for mounting SX content. On page load it: (1) initializes CSS tracking, (2) loads the style dictionary from inline JSON, (3) processes <script type=\"text/sx\"> tags (component definitions and mount directives), (4) hydrates [data-sx] elements, and (5) activates the engine on all elements. It also provides the public mount/hydrate/update/render-component API, and the head element hoisting logic that moves <meta>, <title>, and <link> tags from rendered content into <head>.")
|
||||
(dict :slug "cssx" :filename "cssx.sx" :title "CSSX"
|
||||
:desc "On-demand CSS: style dictionary, keyword resolution, rule injection."
|
||||
:prose "CSSX is the on-demand CSS system. It resolves keyword atoms (:flex, :gap-4, :hover:bg-sky-200) into StyleValue objects with content-addressed class names, injecting CSS rules into the document on first use. The style dictionary is a JSON blob containing: atoms (keyword to CSS declarations), pseudo-variants (hover:, focus:, etc.), responsive breakpoints (md:, lg:, etc.), keyframe animations, arbitrary value patterns, and child selector prefixes (space-x-, space-y-). Classes are only emitted when used, keeping the CSS payload minimal. The dictionary is typically served inline in a <script type=\"text/sx-styles\"> tag.")))
|
||||
:prose "Boot handles the browser startup sequence and provides the public API for mounting SX content. On page load it: (1) initializes CSS tracking, (2) processes <script type=\"text/sx\"> tags (component definitions and mount directives), (3) hydrates [data-sx] elements, and (4) activates the engine on all elements. It also provides the public mount/hydrate/update/render-component API, and the head element hoisting logic that moves <meta>, <title>, and <link> tags from rendered content into <head>.")))
|
||||
|
||||
(define extension-spec-items (list
|
||||
(dict :slug "continuations" :filename "continuations.sx" :title "Continuations"
|
||||
|
||||
124
sx/sx/plans.sx
124
sx/sx/plans.sx
@@ -2295,130 +2295,6 @@
|
||||
(td :class "px-3 py-2 text-stone-700" "Add CI primitive declarations"))))))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 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 "Strategy-agnostic: ") "A component can apply Tailwind classes, emit " (code "<style>") " blocks, use inline styles, generate CSS custom properties, or any combination. The consumer never knows or cares. Swap strategies 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-subsection :title "Emitting CSS directly"
|
||||
(p "Components are not limited to referencing existing classes. They can generate CSS — " (code "<style>") " tags, keyframes, custom properties — as part of their output:")
|
||||
(highlight
|
||||
"(defcomp ~pulse (&key color duration &rest children)\n (<>\n (style (str \"@keyframes sx-pulse {\"\n \"0%,100% { opacity:1 } 50% { opacity:.5 } }\"))\n (div :style (str \"animation: sx-pulse \" (or duration \"2s\") \" infinite;\"\n \"color:\" (or color \"inherit\"))\n children)))\n\n(defcomp ~theme (&key primary surface &rest children)\n (<>\n (style (str \":root {\"\n \"--color-primary:\" (or primary \"#7c3aed\") \";\"\n \"--color-surface:\" (or surface \"#fafaf9\") \"}\"))\n children))"
|
||||
"lisp")
|
||||
(p "The CSS strategy is the component's private implementation detail. Consumers call " (code "(~pulse :color \"red\" \"Loading...\")") " or " (code "(~theme :primary \"#2563eb\" ...)") " without knowing or caring whether the component uses classes, inline styles, generated rules, or all three.")))
|
||||
|
||||
(~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 "Comparison with CSS Technologies" :id "comparison"
|
||||
(p "CSSX components share DNA with several existing approaches but avoid the problems that make each one painful at scale.")
|
||||
|
||||
(~doc-subsection :title "styled-components / Emotion"
|
||||
(p (a :href "https://styled-components.com" :class "text-violet-600 hover:underline" "styled-components") " pioneered the idea that styling belongs in components. But it generates CSS at runtime, injects " (code "<style>") " tags, and produces opaque hashed class names (" (code "class=\"sc-bdfBwQ fNMpVx\"") "). Open DevTools and you see gibberish. It also carries significant runtime cost — parsing CSS template literals, hashing, deduplicating — and needs a separate SSR extraction step (" (code "ServerStyleSheet") ").")
|
||||
(p "CSSX components share the core insight (" (em "styling is a component concern") ") but without the runtime machinery. When a component applies Tailwind classes, there's zero CSS generation overhead. When it does emit " (code "<style>") " blocks, it's explicit — not hidden behind a tagged template literal. And the DOM is always readable."))
|
||||
|
||||
(~doc-subsection :title "CSS Modules"
|
||||
(p (a :href "https://github.com/css-modules/css-modules" :class "text-violet-600 hover:underline" "CSS Modules") " scope class names to avoid collisions by rewriting them at build time: " (code ".button") " becomes " (code ".button_abc123") ". This solves the global namespace problem but creates the same opacity issue — hashed names in the DOM that you can't grep for or reason about.")
|
||||
(p "CSSX components don't need scoping because component boundaries already provide isolation. A " (code "~btn") " owns its markup. There's nothing to collide with."))
|
||||
|
||||
(~doc-subsection :title "Tailwind CSS"
|
||||
(p "Tailwind is " (em "complementary") ", not competitive. CSSX components are the semantic layer on top. Raw Tailwind in markup — " (code ":class \"px-4 py-2 bg-blue-600 text-white font-medium rounded hover:bg-blue-700\"") " — is powerful but verbose and duplicated across call sites.")
|
||||
(p "A CSSX component wraps that string once: " (code "(~btn :variant \"primary\" \"Submit\")") ". The Tailwind classes are still there, readable in DevTools, but consumers don't repeat them. This is the same pattern Tailwind's own docs recommend (" (em "\"extracting components\"") ") — CSSX components are just SX's native way of doing it."))
|
||||
|
||||
(~doc-subsection :title "Vanilla Extract"
|
||||
(p (a :href "https://vanilla-extract.style" :class "text-violet-600 hover:underline" "Vanilla Extract") " is zero-runtime CSS-in-JS: styles are written in TypeScript, compiled to static CSS at build time, and referenced by generated class names. It avoids the runtime cost of styled-components but still requires a build step, a bundler plugin, and TypeScript. The generated class names are again opaque.")
|
||||
(p "CSSX components need no build step for styling — they're evaluated at render time like any other component. And since the component chooses its own strategy, it can reference pre-built classes (zero runtime) " (em "or") " generate CSS on the fly — same API either way."))
|
||||
|
||||
(~doc-subsection :title "Design Tokens / Style Dictionary"
|
||||
(p "The " (a :href "https://amzn.github.io/style-dictionary/" :class "text-violet-600 hover:underline" "Style Dictionary") " pattern — a JSON/YAML file mapping token names to values, compiled to platform-specific output — is essentially what the old CSSX was. It's the industry standard for design systems.")
|
||||
(p "The problem is that it's a parallel system: separate file format, separate build pipeline, separate caching, separate tooling. CSSX components eliminate all of that by expressing tokens as component parameters: " (code "(~theme :primary \"#7c3aed\")") " instead of " (code "{\"color\": {\"primary\": {\"value\": \"#7c3aed\"}}}") ". Same result, no parallel infrastructure.")))
|
||||
|
||||
(~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."))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Live Streaming — SSE & WebSocket
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
:prims (~doc-primitives-tables :primitives (primitives-data)))
|
||||
"special-forms" (~docs-special-forms-content
|
||||
:forms (~doc-special-forms-tables :forms (special-forms-data)))
|
||||
"css" (~docs-css-content)
|
||||
"server-rendering" (~docs-server-rendering-content)
|
||||
:else (~docs-introduction-content)))
|
||||
|
||||
@@ -286,6 +285,40 @@
|
||||
"no-alternative" (~essay-no-alternative)
|
||||
:else (~essays-index-content)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; CSSX section
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defpage cssx-index
|
||||
:path "/cssx/"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "CSSX"
|
||||
:sub-label "CSSX"
|
||||
:sub-href "/cssx/"
|
||||
:sub-nav (~section-nav :items cssx-nav-items :current "Overview")
|
||||
:selected "Overview")
|
||||
:content (~cssx-overview-content))
|
||||
|
||||
(defpage cssx-page
|
||||
:path "/cssx/<slug>"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "CSSX"
|
||||
:sub-label "CSSX"
|
||||
:sub-href "/cssx/"
|
||||
:sub-nav (~section-nav :items cssx-nav-items
|
||||
:current (find-current cssx-nav-items slug))
|
||||
:selected (or (find-current cssx-nav-items slug) ""))
|
||||
:content (case slug
|
||||
"patterns" (~cssx-patterns-content)
|
||||
"delivery" (~cssx-delivery-content)
|
||||
"async" (~cssx-async-content)
|
||||
"live" (~cssx-live-content)
|
||||
"comparisons" (~cssx-comparison-content)
|
||||
"philosophy" (~cssx-philosophy-content)
|
||||
:else (~cssx-overview-content)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Specs section
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -546,7 +579,6 @@
|
||||
"glue-decoupling" (~plan-glue-decoupling-content)
|
||||
"social-sharing" (~plan-social-sharing-content)
|
||||
"sx-ci" (~plan-sx-ci-content)
|
||||
"cssx-components" (~plan-cssx-components-content)
|
||||
"live-streaming" (~plan-live-streaming-content)
|
||||
:else (~plans-index-content)))
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ def _special_forms_data() -> dict:
|
||||
"filter": "Higher-Order Forms", "reduce": "Higher-Order Forms",
|
||||
"some": "Higher-Order Forms", "every?": "Higher-Order Forms",
|
||||
"for-each": "Higher-Order Forms",
|
||||
"defstyle": "Domain Definitions", "defkeyframes": "Domain Definitions",
|
||||
"defstyle": "Domain Definitions",
|
||||
"defhandler": "Domain Definitions", "defpage": "Domain Definitions",
|
||||
"defquery": "Domain Definitions", "defaction": "Domain Definitions",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user