Component names now reflect filesystem location using / as path separator and : as namespace separator for shared components: ~sx-header → ~layouts/header ~layout-app-body → ~shared:layout/app-body ~blog-admin-dashboard → ~admin/dashboard 209 files, 4,941 replacements across all services. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
422 lines
29 KiB
Plaintext
422 lines
29 KiB
Plaintext
;; 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 ()
|
|
(~docs/page :title "CSSX Components"
|
|
|
|
(~docs/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 "(~cssx/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."))
|
|
|
|
(~docs/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.")))
|
|
|
|
(~docs/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 "(~cssx/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 (~cssx/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.")))
|
|
|
|
(~docs/section :title "What Changed" :id "changes"
|
|
(~docs/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")))
|
|
|
|
(~docs/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)")))
|
|
|
|
(~docs/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 ()
|
|
(~docs/page :title "Patterns"
|
|
|
|
(~docs/section :title "Class Mapping" :id "class-mapping"
|
|
(p "The simplest pattern: a component that maps semantic keywords to class strings.")
|
|
(highlight
|
|
"(defcomp ~cssx/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 "(~cssx/btn :variant \"primary\" \"Submit\")") ". The Tailwind "
|
|
"classes are readable in DevTools but never repeated across call sites."))
|
|
|
|
(~docs/section :title "Data-Driven Styling" :id "data-driven"
|
|
(p "Styling that responds to data values — impossible with static CSS:")
|
|
(highlight
|
|
"(defcomp ~cssx/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\"."))
|
|
|
|
(~docs/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"))
|
|
|
|
(~docs/section :title "Responsive Layouts" :id "responsive"
|
|
(p "Components that encode responsive breakpoints:")
|
|
(highlight
|
|
"(defcomp ~cssx/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"))
|
|
|
|
(~docs/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 ~cssx/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 ~cssx/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 "(~cssx/pulse :color \"red\" \"Loading...\")") " or "
|
|
(code "(~cssx/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 ()
|
|
(~docs/page :title "Async CSS"
|
|
|
|
(~docs/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 "~shared:pages/suspense") " combined with a style component — no new infrastructure:")
|
|
(highlight
|
|
"(defcomp ~cssx/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 (~shared:pages/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
|
|
"(~cssx/styled :css-url \"/css/charts.css\" :css-hash \"abc123\"\n (~bar-chart :data metrics))"
|
|
"lisp"))
|
|
|
|
(~docs/section :title "Use Cases" :id "use-cases"
|
|
(~docs/subsection :title "Federated Components"
|
|
(p "A " (code "~cssx/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 ~cssx/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 (~shared:pages/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"))
|
|
|
|
(~docs/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 ~cssx/code-editor (&key language value on-change)\n (~cssx/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"))
|
|
|
|
(~docs/subsection :title "Lazy Themes"
|
|
(p "Theme CSS loads on first use, then is instant on subsequent visits:")
|
|
(highlight
|
|
"(defcomp ~cssx/lazy-theme (&key name &rest children)\n (let ((css-url (str \"/css/themes/\" name \".css\"))\n (hash (str \"theme-\" name)))\n (~cssx/styled :css-url css-url :css-hash hash\n :fallback children ;; render unstyled immediately\n children)))"
|
|
"lisp")))
|
|
|
|
(~docs/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 "~shared:pages/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 ()
|
|
(~docs/page :title "Live Styles"
|
|
|
|
(~docs/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."))
|
|
|
|
(~docs/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 (~shared:pages/suspense :id \"theme\"\n (~cssx/theme :primary \"#7c3aed\" :surface \"#fafaf9\")))"
|
|
"lisp")
|
|
(p "Server pushes a new theme:")
|
|
(highlight
|
|
"event: sx-resolve\ndata: {\"id\": \"theme\", \"sx\": \"(~cssx/theme :primary \\\"#2563eb\\\" :surface \\\"#1e1e2e\\\")\"}"
|
|
"text")
|
|
(p "The " (code "~cssx/theme") " component emits CSS custom properties. Everything "
|
|
"using " (code "var(--color-primary)") " repaints instantly:")
|
|
(highlight
|
|
"(defcomp ~cssx/theme (&key primary surface)\n (style (str \":root {\"\n \"--color-primary:\" (or primary \"#7c3aed\") \";\"\n \"--color-surface:\" (or surface \"#fafaf9\") \"}\")))"
|
|
"lisp"))
|
|
|
|
(~docs/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 (~shared:pages/suspense :id \"cpu\"\n (~cssx/metric :value 0 :label \"CPU\" :threshold 80))\n (~shared:pages/suspense :id \"memory\"\n (~cssx/metric :value 0 :label \"Memory\" :threshold 90))\n (~shared:pages/suspense :id \"requests\"\n (~cssx/metric :value 0 :label \"RPS\" :threshold 1000)))"
|
|
"lisp")
|
|
(p "Server pushes updated values. " (code "~cssx/metric") " turns red when "
|
|
(code "value > threshold") " — the styling logic lives in the component, "
|
|
"not in CSS selectors or JavaScript event handlers."))
|
|
|
|
(~docs/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 (~shared:pages/suspense :id \"canvas-theme\"\n (~cssx/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 "~cssx/theme") " component re-renders with the new color."))
|
|
|
|
(~docs/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 ()
|
|
(~docs/page :title "Comparisons"
|
|
|
|
(~docs/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."))
|
|
|
|
(~docs/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 "~cssx/btn") " owns its markup. There's nothing to collide with."))
|
|
|
|
(~docs/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 "(~cssx/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."))
|
|
|
|
(~docs/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."))
|
|
|
|
(~docs/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 "(~cssx/theme :primary \"#7c3aed\")") " instead of "
|
|
(code "{\"color\": {\"primary\": {\"value\": \"#7c3aed\"}}}")
|
|
". Same result, no parallel infrastructure."))))
|
|
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Philosophy
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(defcomp ~cssx/philosophy-content ()
|
|
(~docs/page :title "Philosophy"
|
|
|
|
(~docs/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."))
|
|
|
|
(~docs/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."))
|
|
|
|
(~docs/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 "~cssx/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."))
|
|
|
|
(~docs/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 "~cssx/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 ()
|
|
(~docs/page :title "CSS Delivery"
|
|
|
|
(~docs/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 "~shared:pages/suspense") " (see " (a :href "/sx/(applications.(cssx.async))" "Async CSS") ").")
|
|
(li (strong "Custom properties: ") "A " (code "~cssx/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."))
|
|
|
|
(~docs/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."))
|
|
|
|
(~docs/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."))
|
|
|
|
(~docs/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."))
|
|
|
|
(~docs/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.")))))
|