Reframe spreads article around provide/emit! as the mechanism

Lead with provide/emit! from the first sentence. make-spread/spread?/spread-attrs
are now presented as user-facing API on top of the provide/emit! substrate,
not as independent primitives. Restructured sections, removed redundant
"deeper primitive" content that duplicated the new section I.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 16:12:47 +00:00
parent aef990735f
commit e1a5e3eb89

View File

@@ -89,39 +89,56 @@
(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.")
"Internally, spreads work through " (code "provide") "/" (code "emit!") " — "
"every element creates a provider scope, and spread children emit into it.")
;; =====================================================================
;; I. The primitives
;; I. How it works
;; =====================================================================
(~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/section :title "How it works" :id "mechanism"
(p "Every element wraps its children in a " (code "provide") " scope named "
(code "\"element-attrs\"") ". When the renderer encounters a spread child, "
"it calls " (code "emit!") " to push the spread's attrs into that scope. "
"After all children render, the element collects the emitted attrs and merges them. "
"This is the " (a :href "/sx/(geography.(provide))" :class "text-violet-600 hover:underline"
"provide/emit!") " mechanism — spreads are one instance of it.")
(p "The user-facing API is " (code "make-spread") " — it creates a spread value from a dict. "
(code "spread?") " tests for one. " (code "spread-attrs") " extracts the dict. "
"But the actual delivery from child to parent goes through provide/emit!, not through "
"return-value inspection.")
(~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.")
(~geography/demo-example
:demo (~geography/demo-spread-basic)
:code (highlight "(defcomp ~callout (&key type)\n (make-spread\n (cond\n (= type \"info\")\n {\"style\" \"border-left:4px solid\n #3b82f6; background:#eff6ff\"}\n (= type \"warning\")\n {\"style\" \"border-left:4px solid\n #f59e0b; background:#fffbeb\"}\n (= type \"success\")\n {\"style\" \"border-left:4px solid\n #10b981; background:#ecfdf5\"})))\n\n;; Child injects attrs onto parent:\n(div (~callout :type \"info\")\n \"This div gets the callout style.\")" "lisp"))
(~geography/demo-example
:demo (~geography/demo-spread-basic)
:code (highlight "(defcomp ~callout (&key type)\n (make-spread\n (cond\n (= type \"info\")\n {\"style\" \"border-left:4px solid\n #3b82f6; background:#eff6ff\"}\n (= type \"warning\")\n {\"style\" \"border-left:4px solid\n #f59e0b; background:#fffbeb\"}\n (= type \"success\")\n {\"style\" \"border-left:4px solid\n #10b981; background:#ecfdf5\"})))\n\n;; Child injects attrs onto parent:\n(div (~callout :type \"info\")\n \"This div gets the callout style.\")" "lisp"))
(p (code "class") " values are appended (space-joined). "
(code "style") " values are appended (semicolon-joined). "
"All other attributes overwrite.")
(p (code "class") " values are appended (space-joined). "
(code "style") " values are appended (semicolon-joined). "
"All other attributes overwrite."))
(p "Tolerant " (code "emit!") " means a spread outside any element context "
"(in a fragment, " (code "begin") ", or bare " (code "map") ") silently vanishes "
"instead of crashing — there's no provider to emit into, so it's a no-op."))
(~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:red}\")\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."))
;; =====================================================================
;; II. collect! — the other upward channel
;; =====================================================================
(~docs/subsection :title "3. reactive-spread (islands)"
(~docs/section :title "collect! — the other upward channel" :id "collect"
(p "Spreads use " (code "provide") "/" (code "emit!") " (scoped, no dedup). "
(code "collect!") "/" (code "collected") " is a separate upward channel — "
"global, with automatic deduplication. Used for CSS rule accumulation.")
(~docs/code :code (highlight ";; Deep inside a component tree:\n(collect! \"cssx\" \".sx-bg-red-500{background:red}\")\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 "Both are upward communication through the render tree, but with different "
"semantics — " (code "emit!") " is scoped to the nearest provider, "
(code "collect!") " is global and deduplicates."))
;; =====================================================================
;; III. Reactive spreads (islands)
;; =====================================================================
(~docs/section :title "Reactive spreads" :id "reactive"
(~docs/subsection :title "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.")
@@ -138,12 +155,12 @@
(li "No re-render. No VDOM. No diffing. Just attr surgery."))))
;; =====================================================================
;; II. Orthogonality
;; IV. 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.")
(p "Spread (via provide/emit!), collect!, and reactive-spread operate at different "
"levels of the render pipeline. They compose without knowing about each other.")
(~docs/table
:headers (list "Primitive" "Direction" "Boundary" "When")
@@ -179,11 +196,11 @@
(list "Client rendering" "spread in wire format (aser)" "reactive-spread (live)")))))
;; =====================================================================
;; III. CSSX as use case
;; V. 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:")
(p (code "~cssx/tw") " is the primary consumer of spreads:")
(~geography/demo-example
:demo (~geography/demo-cssx-tw)
@@ -193,7 +210,7 @@
(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:")
(p "But " (code "~cssx/tw") " is just one instance. The same mechanism enables:")
(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")
@@ -201,7 +218,7 @@
(li "Any component that needs to inject attributes onto its parent")))
;; =====================================================================
;; IV. Semantic variables
;; VI. Semantic variables
;; =====================================================================
(~docs/section :title "Semantic style variables" :id "variables"
@@ -218,7 +235,7 @@
(code "~admin/heading") " are different components in different namespaces."))
;; =====================================================================
;; V. What nothing else does
;; VII. What nothing else does
;; =====================================================================
(~docs/section :title "What nothing else does" :id "unique"
@@ -229,46 +246,31 @@
(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:")
(p "SX does it with " (code "provide") "/" (code "emit!") " — render-time dynamic scope "
"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"))
(li (strong "provide/emit!") " — scoped upward communication (the general mechanism)")
(li (strong "make-spread") " — creates spread values that emit into element-attrs providers")
(li (strong "reactive-spread") " — wraps emit! in a signal effect for live updates"))
(p "No new concepts. No new runtime. No new API surface."))
;; =====================================================================
;; VI. The deeper primitive
;; VIII. The general 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. Spreads are now implemented on top of this mechanism.")
(~docs/section :title "The general primitive" :id "provide"
(p "Spreads, collect!, and context are all instances of one mechanism: "
(code "provide") "/" (code "emit!") " — render-time dynamic scope. "
(a :href "/sx/(geography.(provide))" :class "text-violet-600 hover:underline"
"See the full provide/context/emit! article")
" for the general primitive, nested scoping, and its other uses.")
(~docs/subsection :title "How spreads use provide/emit! internally"
(p "Every element rendering wraps its children in a provider scope named "
(code "\"element-attrs\"") ". When the renderer encounters a spread child, it "
"emits the spread's attrs into this scope instead of returning a DOM node. "
"After all children render, the element collects emitted attrs and merges them.")
(~docs/code :code (highlight ";; What happens inside element rendering:\n;; 1. Element creates a provider scope\n(provide-push! \"element-attrs\" nil)\n\n;; 2. Children render — spreads emit into scope\n;; (make-spread {:class \"card\"}) → (emit! \"element-attrs\" {:class \"card\"})\n\n;; 3. Element collects emitted attrs\n(for-each\n (fn (spread-dict) (merge-spread-attrs attrs spread-dict))\n (emitted \"element-attrs\"))\n(provide-pop! \"element-attrs\")" "lisp"))
(p (code "emit!") " is tolerant — when no provider exists (a spread outside any element), "
"it silently returns nil. This means spreads in non-element contexts "
"(fragments, " (code "begin") ", bare " (code "map") ") vanish without error.")
(p "Stored spreads work naturally:")
(~docs/code :code (highlight ";; Spread value stored in a let binding\n(let ((card (make-spread {:class \"card\"})))\n (div card \"hello\"))\n;; → <div class=\"card\">hello</div>\n;;\n;; `card` holds a _Spread object.\n;; When div renders it, the adapter sees type \"spread\"\n;; → emits into div's \"element-attrs\" provider\n;; → returns \"\" (no content)\n;; div merges the emitted attrs after children." "lisp")))
(~docs/subsection :title "The unification"
(~docs/table
:headers (list "Mechanism" "General form" "Direction")
:rows (list
(list "collect! / collected" "emit! / emitted" "upward (child → scope)")
(list "spread" "emit! into element-attrs provider" "upward (child → parent)")
(list "theme / config" "context" "downward (scope → child)")))
(p "Three mechanisms, one substrate. "
(a :href "/sx/(geography.(provide))" :class "text-violet-600 hover:underline"
"See the full provide/context/emit! article")
" for the general primitive and its other uses."))
(~docs/table
:headers (list "Mechanism" "General form" "Direction")
:rows (list
(list "spread" "emit! into element-attrs provider" "upward (child → parent)")
(list "collect! / collected" "emit! / emitted (global, deduped)" "upward (child → scope)")
(list "theme / config" "context" "downward (scope → child)")))
(~docs/note
(p (strong "Spec: ") "The provide/emit! primitives are declared in "