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" (p :class "text-stone-500 text-sm italic mb-8"
"A spread is a value that, when returned as a child of an element, " "A spread is a value that, when returned as a child of an element, "
"injects attributes onto its parent instead of rendering as content. " "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" (~docs/section :title "How it works" :id "mechanism"
(p "The spread system has three orthogonal primitives. Each operates at a " (p "Every element wraps its children in a " (code "provide") " scope named "
"different level of the render pipeline.") (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" (~geography/demo-example
(p "A spread is a value type. " (code "make-spread") " creates one from a dict of " :demo (~geography/demo-spread-basic)
"attributes. When the renderer encounters a spread as a child of an element, " :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"))
"it merges the attrs onto the parent element instead of appending a DOM node.")
(~geography/demo-example (p (code "class") " values are appended (space-joined). "
:demo (~geography/demo-spread-basic) (code "style") " values are appended (semicolon-joined). "
: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")) "All other attributes overwrite.")
(p (code "class") " values are appended (space-joined). " (p "Tolerant " (code "emit!") " means a spread outside any element context "
(code "style") " values are appended (semicolon-joined). " "(in a fragment, " (code "begin") ", or bare " (code "map") ") silently vanishes "
"All other attributes overwrite.")) "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 " ;; II. collect! — the other upward channel
"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."))
(~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, " (p "Inside an island, when a spread's value depends on signals, "
(code "reactive-spread") " tracks signal dependencies and surgically " (code "reactive-spread") " tracks signal dependencies and surgically "
"updates the parent element's attributes when signals change.") "updates the parent element's attributes when signals change.")
@@ -138,12 +155,12 @@
(li "No re-render. No VDOM. No diffing. Just attr surgery.")))) (li "No re-render. No VDOM. No diffing. Just attr surgery."))))
;; ===================================================================== ;; =====================================================================
;; II. Orthogonality ;; IV. Orthogonality
;; ===================================================================== ;; =====================================================================
(~docs/section :title "Orthogonality across boundaries" :id "orthogonality" (~docs/section :title "Orthogonality across boundaries" :id "orthogonality"
(p "The three primitives operate at different levels of the render pipeline. " (p "Spread (via provide/emit!), collect!, and reactive-spread operate at different "
"They compose without knowing about each other.") "levels of the render pipeline. They compose without knowing about each other.")
(~docs/table (~docs/table
:headers (list "Primitive" "Direction" "Boundary" "When") :headers (list "Primitive" "Direction" "Boundary" "When")
@@ -179,11 +196,11 @@
(list "Client rendering" "spread in wire format (aser)" "reactive-spread (live)"))))) (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" (~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 (~geography/demo-example
:demo (~geography/demo-cssx-tw) :demo (~geography/demo-cssx-tw)
@@ -193,7 +210,7 @@
(code "collect!") " to accumulate CSS rules for batch flushing, and " (code "collect!") " to accumulate CSS rules for batch flushing, and "
"when called inside an island with signal-dependent tokens, " "when called inside an island with signal-dependent tokens, "
(code "reactive-spread") " makes it live.") (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" (ul :class "list-disc pl-5 space-y-1 text-stone-600"
(li (code "~aria") " — reactive accessibility attributes driven by UI state") (li (code "~aria") " — reactive accessibility attributes driven by UI state")
(li (code "~data-attrs") " — signal-driven data attributes for coordination") (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"))) (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" (~docs/section :title "Semantic style variables" :id "variables"
@@ -218,7 +235,7 @@
(code "~admin/heading") " are different components in different namespaces.")) (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" (~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") (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. " ". They don't inject attrs onto an existing element from a child position. "
"And they need a build step, a runtime, a theme provider.") "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" (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 "provide/emit!") " — scoped upward communication (the general mechanism)")
(li (strong "collect!") " — render-time accumulation (existed for CSS rule batching)") (li (strong "make-spread") " — creates spread values that emit into element-attrs providers")
(li (strong "reactive-spread") " — just the obvious combination of spread + effect")) (li (strong "reactive-spread") " — wraps emit! in a signal effect for live updates"))
(p "No new concepts. No new runtime. No new API surface.")) (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" (~docs/section :title "The general primitive" :id "provide"
(p "Spread and collect are both instances of the same pattern: " (p "Spreads, collect!, and context are all instances of one mechanism: "
(strong "child communicates upward through the render tree") ". " (code "provide") "/" (code "emit!") " — render-time dynamic scope. "
"The general form is " (code "provide") "/" (code "context") "/" (code "emit!") (a :href "/sx/(geography.(provide))" :class "text-violet-600 hover:underline"
" — render-time dynamic scope. Spreads are now implemented on top of this mechanism.") "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" (~docs/table
(p "Every element rendering wraps its children in a provider scope named " :headers (list "Mechanism" "General form" "Direction")
(code "\"element-attrs\"") ". When the renderer encounters a spread child, it " :rows (list
"emits the spread's attrs into this scope instead of returning a DOM node. " (list "spread" "emit! into element-attrs provider" "upward (child → parent)")
"After all children render, the element collects emitted attrs and merges them.") (list "collect! / collected" "emit! / emitted (global, deduped)" "upward (child → scope)")
(~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")) (list "theme / config" "context" "downward (scope → child)")))
(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/note (~docs/note
(p (strong "Spec: ") "The provide/emit! primitives are declared in " (p (strong "Spec: ") "The provide/emit! primitives are declared in "