From e1a5e3eb89023df82ee2b8fd33a2e2f6e1160651 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 13 Mar 2026 16:12:47 +0000 Subject: [PATCH] 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 --- sx/sx/spreads.sx | 132 ++++++++++++++++++++++++----------------------- 1 file changed, 67 insertions(+), 65 deletions(-) diff --git a/sx/sx/spreads.sx b/sx/sx/spreads.sx index 774a14b..4d36744 100644 --- a/sx/sx/spreads.sx +++ b/sx/sx/spreads.sx @@ -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 \"\")))" "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 "\")))" "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;; →
hello
\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 "