;; ---------------------------------------------------------------------------
;; Spreads — child-to-parent communication across render boundaries
;; ---------------------------------------------------------------------------
;; ---- Layout helper ----
(defcomp ~geography/demo-example (&key demo code)
(div :class "grid grid-cols-1 lg:grid-cols-2 gap-4 my-6 items-start"
(div :class "border border-dashed border-stone-300 rounded-lg p-4 bg-stone-50 min-h-[80px]"
demo)
(div :class "not-prose bg-stone-100 rounded-lg p-4 overflow-x-auto"
(pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" (code code)))))
;; ---- Demo components ----
(defcomp ~geography/demo-callout (&key type)
(make-spread
(cond
(= type "info") {"style" "border-left:4px solid #3b82f6;padding:0.5rem 0.75rem;background:#eff6ff;border-radius:0.25rem"
"data-callout" "info"}
(= type "warning") {"style" "border-left:4px solid #f59e0b;padding:0.5rem 0.75rem;background:#fffbeb;border-radius:0.25rem"
"data-callout" "warning"}
(= type "success") {"style" "border-left:4px solid #10b981;padding:0.5rem 0.75rem;background:#ecfdf5;border-radius:0.25rem"
"data-callout" "success"}
:else {"style" "border-left:4px solid #78716c;padding:0.5rem 0.75rem;background:#fafaf9;border-radius:0.25rem"
"data-callout" "default"})))
(defcomp ~geography/demo-spread-basic ()
(div :class "space-y-3"
(div (~geography/demo-callout :type "info")
(p :class "text-sm" "Info — styled by its child."))
(div (~geography/demo-callout :type "warning")
(p :class "text-sm" "Warning — same component, different type."))
(div (~geography/demo-callout :type "success")
(p :class "text-sm" "Success — child tells parent how to look."))))
(defcomp ~geography/demo-cssx-tw ()
(div :class "space-y-3"
(div (~cssx/tw :tokens "bg-violet-100 rounded-lg p-4")
(h4 (~cssx/tw :tokens "text-violet-800 font-semibold text-lg") "Styled via ~cssx/tw")
(p (~cssx/tw :tokens "text-stone-600 text-sm mt-1")
"Classes injected from spread child."))
(div (~cssx/tw :tokens "bg-rose-50 rounded-lg p-4 border border-rose-200")
(h4 (~cssx/tw :tokens "text-rose-700 font-semibold text-lg") "Different tokens")
(p (~cssx/tw :tokens "text-stone-600 text-sm mt-1")
"CSS rules JIT-generated. No build step."))))
(defcomp ~geography/demo-semantic-vars ()
(let ((card (~cssx/tw :tokens "bg-stone-50 rounded-lg p-4 shadow-sm border border-stone-200"))
(heading (~cssx/tw :tokens "text-violet-700 text-lg font-bold"))
(body-text (~cssx/tw :tokens "text-stone-600 text-sm mt-1")))
(div :class "space-y-3"
(div card
(h4 heading "First Card")
(p body-text "Named spreads bound with let."))
(div card
(h4 heading "Second Card")
(p body-text "Same variables, consistent look.")))))
(defisland ~geography/demo-reactive-spread ()
(let ((colour (signal "violet")))
(div (~cssx/tw :tokens (str "rounded-lg p-4 transition-all duration-300 bg-" (deref colour) "-100 border border-" (deref colour) "-300"))
(h4 (~cssx/tw :tokens (str "text-" (deref colour) "-800 font-semibold text-lg"))
(str "Theme: " (deref colour)))
(p (~cssx/tw :tokens "text-stone-600 text-sm mt-2 mb-3")
"Click to change theme. Reactive spreads surgically update classes.")
(div :class "flex gap-2 flex-wrap"
(button :on-click (fn (e) (reset! colour "violet"))
(~cssx/tw :tokens "bg-violet-500 text-white px-3 py-1.5 rounded text-sm font-medium")
"Violet")
(button :on-click (fn (e) (reset! colour "rose"))
(~cssx/tw :tokens "bg-rose-500 text-white px-3 py-1.5 rounded text-sm font-medium")
"Rose")
(button :on-click (fn (e) (reset! colour "amber"))
(~cssx/tw :tokens "bg-amber-500 text-white px-3 py-1.5 rounded text-sm font-medium")
"Amber")
(button :on-click (fn (e) (reset! colour "emerald"))
(~cssx/tw :tokens "bg-emerald-500 text-white px-3 py-1.5 rounded text-sm font-medium")
"Emerald")))))
;; ---- Page content ----
(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. "
"Internally, spreads work through "
(a :href "/sx/(geography.(scopes))" :class "text-violet-600 hover:underline" "scopes")
" — every element creates a scope, and spread children emit into it.")
;; =====================================================================
;; I. How it works
;; =====================================================================
(~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.")
(~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 "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."))
;; =====================================================================
;; II. collect! — the other upward channel
;; =====================================================================
(~docs/section :title "collect! — the other upward channel" :id "collect"
(p "Spreads use " (code "scope") "/" (code "emit!") " (scoped, no dedup). "
(code "collect!") "/" (code "collected") " is also backed by scopes — "
"a lazy root scope 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 \"\")))" "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.")
(~geography/demo-example
:demo (~geography/demo-reactive-spread)
:code (highlight "(defisland ~themed-card ()\n (let ((theme (signal \"violet\")))\n (div (~cssx/tw :tokens\n (str \"bg-\" (deref theme)\n \"-100 p-4\"))\n (button\n :on-click\n (fn (e) (reset! theme \"rose\"))\n \"change theme\"))))" "lisp"))
(p "When " (code "theme") " changes:")
(ul :class "list-disc pl-5 space-y-1 text-stone-600"
(li "Old classes are removed from the element")
(li "New classes 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."))))
;; =====================================================================
;; IV. Orthogonality
;; =====================================================================
(~docs/section :title "Orthogonality across boundaries" :id "orthogonality"
(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")
: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 "