Files
rose-ash/sx/sx/spreads.sx
giles 28a6560963 Replace \uXXXX escapes with actual UTF-8 characters in .sx files
SX parser doesn't process \u escapes — they render as literal text.
Use actual UTF-8 characters (→, —, £, ⬡) directly in source.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:46:53 +00:00

187 lines
14 KiB
Plaintext

;; ---------------------------------------------------------------------------
;; Spreads — child-to-parent communication across render boundaries
;; ---------------------------------------------------------------------------
(defcomp ~geography/spreads-content ()
(~docs/page :title "Spreads"
(p :class "text-stone-500 text-sm italic mb-8"
"A spread is a value that, when returned as a child of an element, "
"injects attributes onto its parent instead of rendering as content. "
"This inverts the normal direction of data flow: children tell parents how to look.")
;; =====================================================================
;; I. The primitives
;; =====================================================================
(~docs/section :title "Three primitives" :id "primitives"
(p "The spread system has three orthogonal primitives. Each operates at a "
"different level of the render pipeline.")
(~docs/subsection :title "1. make-spread / spread? / spread-attrs"
(p "A spread is a value type. " (code "make-spread") " creates one from a dict of "
"attributes. When the renderer encounters a spread as a child of an element, "
"it merges the attrs onto the parent element instead of appending a DOM node.")
(~docs/code :code (highlight "(defcomp ~highlight (&key colour)\n (make-spread {\"class\" (str \"highlight-\" colour)\n \"data-highlight\" colour}))" "lisp"))
(p "Use it as a child of any element:")
(~docs/code :code (highlight "(div (~highlight :colour \"yellow\")\n \"This div gets class=highlight-yellow\")" "lisp"))
(p (code "class") " values are appended (space-joined). "
(code "style") " values are appended (semicolon-joined). "
"All other attributes overwrite."))
(~docs/subsection :title "2. collect! / collected / clear-collected!"
(p "Render-time accumulators. Values are collected into named buckets "
"during rendering and retrieved at flush points. Deduplication is automatic.")
(~docs/code :code (highlight ";; Deep inside a component tree:\n(collect! \"cssx\" \".sx-bg-red-500{background-color:hsl(0,72%,53%)}\")\n\n;; At the flush point (once, in the layout):\n(let ((rules (collected \"cssx\")))\n (clear-collected! \"cssx\")\n (raw! (str \"<style>\" (join \"\" rules) \"</style>\")))" "lisp"))
(p "This is upward communication through the render tree: "
"a deeply nested component contributes a CSS rule, and the layout "
"emits all accumulated rules as a single " (code "<style>") " tag. "
"No prop threading, no context providers, no global state."))
(~docs/subsection :title "3. reactive-spread (islands)"
(p "Inside an island, when a spread's value depends on signals, "
(code "reactive-spread") " tracks signal dependencies and surgically "
"updates the parent element's attributes when signals change.")
(~docs/code :code (highlight "(defisland ~themed-card ()\n (let ((theme (signal \"violet\")))\n (div (~cssx/tw :tokens (str \"bg-\" (deref theme) \"-500 p-4\"))\n (button :on-click (fn (e) (reset! theme \"rose\"))\n \"change theme\"))))" "lisp"))
(p "When " (code "theme") " changes from " (code "\"violet\"") " to "
(code "\"rose\"") ":")
(ul :class "list-disc pl-5 space-y-1 text-stone-600"
(li "Old classes (" (code "sx-bg-violet-500") ") are removed from the element")
(li "New classes (" (code "sx-bg-rose-500") ") are added")
(li "New CSS rules are JIT-generated and flushed to the live stylesheet")
(li "No re-render. No VDOM. No diffing. Just attr surgery."))))
;; =====================================================================
;; II. Orthogonality
;; =====================================================================
(~docs/section :title "Orthogonality across boundaries" :id "orthogonality"
(p "The three primitives operate at different levels of the render pipeline. "
"They compose without knowing about each other.")
(~docs/table
:headers (list "Primitive" "Direction" "Boundary" "When")
:rows (list
(list "spread" "child → parent" "element boundary" "render time")
(list "collect!" "child → ancestor" "render tree" "render time")
(list "reactive-spread" "child → parent" "element boundary" "signal change")))
(~docs/subsection :title "Server rendering (HTML adapter)"
(p "On the server, " (code "~cssx/tw") " returns a spread. The HTML renderer "
"merges the class onto the parent element. " (code "collect!") " accumulates "
"CSS rules. " (code "~cssx/flush") " emits a single " (code "<style>") " tag. "
"Result: static HTML with all classes and styles baked in."))
(~docs/subsection :title "Client hydration (DOM adapter)"
(p "On the client, the same " (code "~cssx/tw") " call inside an island "
"produces a spread that the DOM adapter merges onto the parent element. "
"If the spread depends on signals, " (code "reactive-spread") " wraps it "
"in an effect. Static spreads are applied once; reactive ones track deps."))
(~docs/subsection :title "Morph (navigation)"
(p "When the server sends new HTML during SX navigation, the morph algorithm "
"enters islands, finds lakes, updates server content. Reactive spreads are "
"protected: " (code "data-sx-reactive-attrs") " tells the morph to skip "
"attributes managed by signal effects. The server water flows through; "
"the reactive rocks stay put."))
(~docs/subsection :title "The matrix"
(~docs/table
:headers (list "" "Server state" "Client state (signals)")
:rows (list
(list "Server rendering" "spread + collect! (pure hypermedia)" "SSR + hydrated reactive-spread")
(list "Client rendering" "spread in wire format (aser)" "reactive-spread (live)")))))
;; =====================================================================
;; III. CSSX as use case
;; =====================================================================
(~docs/section :title "CSSX: the first application" :id "cssx"
(p (code "~cssx/tw") " is a component that uses all three primitives:")
(~docs/code :code (highlight "(defcomp ~cssx/tw (tokens)\n (let ((token-list (filter (fn (t) (not (= t \"\")))\n (split (or tokens \"\") \" \")))\n (results (map cssx-process-token token-list))\n (valid (filter (fn (r) (not (nil? r))) results))\n (classes (map (fn (r) (get r \"cls\")) valid))\n (rules (map (fn (r) (get r \"rule\")) valid))\n (_ (for-each (fn (rule) (collect! \"cssx\" rule)) rules)))\n (if (empty? classes)\n nil\n (make-spread {\"class\" (join \" \" classes)\n \"data-tw\" (or tokens \"\")}))))" "lisp"))
(p "It's a regular " (code "defcomp") ". It uses " (code "make-spread") " to "
"inject classes onto its parent, " (code "collect!") " to accumulate CSS rules "
"for batch flushing, and when called inside an island with signal-dependent "
"tokens, " (code "reactive-spread") " makes it live.")
(p "But " (code "~cssx/tw") " is just one instance. The same primitives enable:")
(ul :class "list-disc pl-5 space-y-1 text-stone-600"
(li (code "~aria") " — reactive accessibility attributes driven by UI state")
(li (code "~data-attrs") " — signal-driven data attributes for coordination")
(li (code "~conditional-attrs") " — presence/absence of attributes based on state")
(li "Any component that needs to inject attributes onto its parent")))
;; =====================================================================
;; IV. Semantic variables
;; =====================================================================
(~docs/section :title "Semantic style variables" :id "variables"
(p "Because " (code "~cssx/tw") " returns a spread, and spreads are values, "
"you can bind them to names:")
(~docs/code :code (highlight ";; Define once\n(define heading-style (~cssx/tw :tokens \"text-violet-700 text-2xl font-bold\"))\n(define nav-link (~cssx/tw :tokens \"text-stone-500 text-sm\"))\n(define card-base (~cssx/tw :tokens \"bg-stone-50 rounded-lg p-4\"))\n\n;; Use everywhere\n(div card-base\n (h1 heading-style \"Title\")\n (a nav-link :href \"/\" \"Home\"))" "lisp"))
(p "These are semantic names wrapping utility tokens. Change the definition, "
"every use updates. No build step, no CSS-in-JS runtime. Just " (code "define") ".")
(p "Namespacing prevents clashes — " (code "~app/heading") " vs "
(code "~admin/heading") " are different components in different namespaces."))
;; =====================================================================
;; V. What nothing else does
;; =====================================================================
(~docs/section :title "What nothing else does" :id "unique"
(p "React can't do this. In React, attributes live in the parent's JSX. "
"A child component cannot inject attrs onto its parent element. You'd need "
"to lift state up, pass callbacks, use context, or reach for " (code "forwardRef")
" — all of which couple the child to the parent's rendering logic.")
(p "CSS-in-JS libraries (styled-components, Emotion) create " (em "wrapper elements")
". They don't inject attrs onto an existing element from a child position. "
"And they need a build step, a runtime, a theme provider.")
(p "SX does it with three orthogonal primitives that already existed for other reasons:")
(ul :class "list-disc pl-5 space-y-1 text-stone-600"
(li (strong "spread") " — child-to-parent attr injection (existed for component composition)")
(li (strong "collect!") " — render-time accumulation (existed for CSS rule batching)")
(li (strong "reactive-spread") " — just the obvious combination of spread + effect"))
(p "No new concepts. No new runtime. No new API surface."))
;; =====================================================================
;; VI. The deeper primitive
;; =====================================================================
(~docs/section :title "The deeper primitive: provide / context / emit!" :id "provide"
(p "Spread and collect are both instances of the same pattern: "
(strong "child communicates upward through the render tree") ". "
"The general form is " (code "provide") "/" (code "context") "/" (code "emit!")
" — render-time dynamic scope.")
(~docs/subsection :title "The unification"
(~docs/table
:headers (list "Current" "General form" "Direction")
:rows (list
(list "collect! / collected" "emit! / emitted" "upward (child → scope)")
(list "make-spread" "emit! into implicit parent-attrs provider" "upward (child → parent)")
(list "(nothing yet)" "context" "downward (scope → child)")))
(p (code "provide") " creates a named scope with a value (downward) and an accumulator (upward). "
(code "context") " reads the value. " (code "emit!") " appends to the accumulator. "
(code "emitted") " retrieves accumulated values.")
(~docs/code :code (highlight ";; Downward: theme context\n(provide \"theme\" {:primary \"violet\" :font \"serif\"}\n (h1 :style (str \"color:\" (get (context \"theme\") :primary))\n \"Themed heading\"))\n\n;; Upward: script accumulation (like collect!)\n(provide \"scripts\" nil\n (div\n (emit! \"scripts\" \"analytics.js\")\n (div (emit! \"scripts\" \"charts.js\") \"chart\"))\n (for-each (fn (s) (script :src s)) (emitted \"scripts\")))\n\n;; Both at once\n(provide \"page\" {:title \"Home\"}\n (h1 (context \"page\" :title))\n (emit! \"page\" {:meta \"og:title\" :content \"Home\"})\n (for-each (fn (m) (meta :name (get m :meta) :content (get m :content)))\n (emitted \"page\")))" "lisp")))
(~docs/subsection :title "What this means"
(p "Three mechanisms collapse into one:")
(ul :class "list-disc pl-5 space-y-1 text-stone-600"
(li (code "collect!") " = " (code "emit!") " with deduplication")
(li (code "spread") " = " (code "emit!") " into implicit parent-attrs provider")
(li (code "collected") " = " (code "emitted"))
(li (code "context") " = downward data flow (new capability — no current equivalent)"))
(p "The reactive-spread we just built would naturally follow — the effect tracks "
"signal deps in the " (code "emit!") " call, the provider accumulates, the element applies.")
(p :class "text-stone-500 italic"
"This is the planned next step. The current primitives (spread, collect, reactive-spread) "
"work and are fully orthogonal. " (code "provide/context/emit!") " will be the deeper "
"foundation they are reimplemented on top of."))
(~docs/note
(p (strong "Plan: ") (code "provide") "/" (code "context") "/" (code "emit!") " is specced "
"and ready to implement. Per-name stacks. Each entry has a value and an emitted list. "
"Four primitives: " (code "provide") " (special form), " (code "context") ", "
(code "emit!") ", " (code "emitted") ". Platform provides " (code "provide-push!")
"/" (code "provide-pop!") ". See the implementation plan for details.")))))