|
|
|
|
@@ -2221,130 +2221,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
|
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
|
|