Add Spreads page under Geography — spread/collect/reactive-spread docs

Documents the three orthogonal primitives (spread, collect!, reactive-spread),
their operation across server/client/morph boundaries, CSSX as use case,
semantic style variables, and the planned provide/context/emit! unification.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 05:25:42 +00:00
parent 36b070f796
commit 9806aec60c
4 changed files with 249 additions and 0 deletions

View File

@@ -368,6 +368,8 @@
:children (list
{:label "Reference" :href "/sx/(geography.(hypermedia.(reference)))" :children reference-nav-items}
{:label "Examples" :href "/sx/(geography.(hypermedia.(example)))" :children examples-nav-items})}
{:label "Spreads" :href "/sx/(geography.(spreads))"
:summary "Child-to-parent communication across render boundaries — spread, collect!, reactive-spread, and the path to provide/context/emit!."}
{:label "Marshes" :href "/sx/(geography.(marshes))"
:summary "Where reactivity and hypermedia interpenetrate — server writes to signals, reactive transforms reshape server content, client state modifies how hypermedia is interpreted."}
{:label "Isomorphism" :href "/sx/(geography.(isomorphism))" :children isomorphism-nav-items})}

235
sx/sx/spreads.sx Normal file
View File

@@ -0,0 +1,235 @@
;; ---------------------------------------------------------------------------
;; 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 "(defcomp ~highlight (&key colour)
(make-spread {\"class\" (str \"highlight-\" colour)
\"data-highlight\" colour}))")
(p "Use it as a child of any element:")
(~docs/code :code "(div (~highlight :colour \"yellow\")
\"This div gets class=highlight-yellow\")")
(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 ";; Deep inside a component tree:
(collect! \"cssx\" \".sx-bg-red-500{background-color:hsl(0,72%,53%)}\")
;; At the flush point (once, in the layout):
(let ((rules (collected \"cssx\")))
(clear-collected! \"cssx\")
(raw! (str \"<style>\" (join \"\" rules) \"</style>\")))")
(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 "(defisland ~themed-card ()
(let ((theme (signal \"violet\")))
(div (~cssx/tw :tokens (str \"bg-\" (deref theme) \"-500 p-4\"))
(button :on-click (fn (e) (reset! theme \"rose\"))
\"change theme\"))))")
(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 \u2192 parent" "element boundary" "render time")
(list "collect!" "child \u2192 ancestor" "render tree" "render time")
(list "reactive-spread" "child \u2192 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 "(defcomp ~cssx/tw (tokens)
(let ((token-list (filter (fn (t) (not (= t \"\")))
(split (or tokens \"\") \" \")))
(results (map cssx-process-token token-list))
(valid (filter (fn (r) (not (nil? r))) results))
(classes (map (fn (r) (get r \"cls\")) valid))
(rules (map (fn (r) (get r \"rule\")) valid))
(_ (for-each (fn (rule) (collect! \"cssx\" rule)) rules)))
(if (empty? classes)
nil
(make-spread {\"class\" (join \" \" classes)
\"data-tw\" (or tokens \"\")}))))")
(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") " \u2014 reactive accessibility attributes driven by UI state")
(li (code "~data-attrs") " \u2014 signal-driven data attributes for coordination")
(li (code "~conditional-attrs") " \u2014 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 ";; Define once
(define heading-style (~cssx/tw :tokens \"text-violet-700 text-2xl font-bold\"))
(define nav-link (~cssx/tw :tokens \"text-stone-500 text-sm\"))
(define card-base (~cssx/tw :tokens \"bg-stone-50 rounded-lg p-4\"))
;; Use everywhere
(div card-base
(h1 heading-style \"Title\")
(a nav-link :href \"/\" \"Home\"))")
(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 \u2014 " (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")
" \u2014 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") " \u2014 child-to-parent attr injection (existed for component composition)")
(li (strong "collect!") " \u2014 render-time accumulation (existed for CSS rule batching)")
(li (strong "reactive-spread") " \u2014 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!")
" \u2014 render-time dynamic scope.")
(~docs/subsection :title "The unification"
(~docs/table
:headers (list "Current" "General form" "Direction")
:rows (list
(list (code "collect! / collected") (code "emit! / emitted") "upward (child \u2192 scope)")
(list (code "make-spread") (str "emit! into implicit " "\"parent-attrs\" provider") "upward (child \u2192 parent)")
(list "(nothing yet)" (code "context") "downward (scope \u2192 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 ";; Downward: theme context
(provide \"theme\" {:primary \"violet\" :font \"serif\"}
(h1 :style (str \"color:\" (get (context \"theme\") :primary))
\"Themed heading\"))
;; Upward: script accumulation (like collect!)
(provide \"scripts\" nil
(div
(emit! \"scripts\" \"analytics.js\")
(div (emit! \"scripts\" \"charts.js\") \"chart\"))
(for-each (fn (s) (script :src s)) (emitted \"scripts\")))
;; Both at once
(provide \"page\" {:title \"Home\"}
(h1 (context \"page\" :title))
(emit! \"page\" {:meta \"og:title\" :content \"Home\"})
(for-each (fn (m) (meta :name (get m :meta) :content (get m :content)))
(emitted \"page\")))"))
(~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 \u2014 no current equivalent)"))
(p "The reactive-spread we just built would naturally follow \u2014 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.")))))

View File

@@ -611,6 +611,16 @@
"phase2" (~reactive-islands/phase2/reactive-islands-phase2-content)
:else (~reactive-islands/index/reactive-islands-index-content))))
;; ---------------------------------------------------------------------------
;; Spreads section (under Geography)
;; ---------------------------------------------------------------------------
(defpage spreads-index
:path "/geography/spreads/"
:auth :public
:layout :sx-docs
:content (~layouts/doc :path "/sx/(geography.(spreads))" (~geography/spreads-content)))
;; ---------------------------------------------------------------------------
;; Marshes section (under Geography)
;; ---------------------------------------------------------------------------

View File

@@ -267,6 +267,8 @@ _REDIRECT_PATTERNS = [
lambda m: f"/sx/(geography.(reactive.{m.group(1)}))"),
(re.compile(r"^/geography/isomorphism/(.+?)/?$"),
lambda m: f"/sx/(geography.(isomorphism.{m.group(1)}))"),
(re.compile(r"^/geography/spreads/?$"),
"/sx/(geography.(spreads))"),
(re.compile(r"^/geography/marshes/?$"),
"/sx/(geography.(marshes))"),
(re.compile(r"^/applications/cssx/(.+?)/?$"),