Build tooling: updated OCaml bootstrapper, compile-modules, bundle.sh, sx-build-all. WASM browser: rebuilt sx_browser.bc.js/wasm, sx-platform-2.js, .sxbc bytecode files. CSSX/Tailwind: reworked cssx.sx templates and tw-layout, added tw-type support. Content: refreshed essays, plans, geography, reactive islands, docs, demos, handlers. New tools: bisect_sxbc.sh, test-spa.js, render-trace.sx, morph playwright spec. Tests: added test-match.sx, test-examples.sx, updated test-tw.sx and web tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
457 lines
18 KiB
Plaintext
457 lines
18 KiB
Plaintext
(defcomp
|
|
~geography/demo-example
|
|
(&key demo code)
|
|
(div
|
|
(~tw :tokens "grid grid-cols-1 lg:grid-cols-2 gap-4 my-6 items-start")
|
|
(div
|
|
(~tw :tokens "border border-dashed border-stone-300 rounded-lg p-4 bg-stone-50 min-h-[80px]")
|
|
demo)
|
|
(div
|
|
(~tw :tokens "not-prose bg-stone-100 rounded-lg p-4 overflow-x-auto")
|
|
(pre
|
|
(~tw :tokens "text-sm leading-relaxed whitespace-pre-wrap break-words")
|
|
(code code)))))
|
|
|
|
(defcomp
|
|
~geography/demo-callout
|
|
(&key type)
|
|
(make-spread
|
|
(cond
|
|
(= type "info")
|
|
{:data-callout "info" :style "border-left:4px solid #3b82f6;padding:0.5rem 0.75rem;background:#eff6ff;border-radius:0.25rem"}
|
|
(= type "warning")
|
|
{:data-callout "warning" :style "border-left:4px solid #f59e0b;padding:0.5rem 0.75rem;background:#fffbeb;border-radius:0.25rem"}
|
|
(= type "success")
|
|
{:data-callout "success" :style "border-left:4px solid #10b981;padding:0.5rem 0.75rem;background:#ecfdf5;border-radius:0.25rem"}
|
|
:else {:data-callout "default" :style "border-left:4px solid #78716c;padding:0.5rem 0.75rem;background:#fafaf9;border-radius:0.25rem"})))
|
|
|
|
(defcomp
|
|
~geography/demo-spread-basic
|
|
()
|
|
(div
|
|
(~tw :tokens "space-y-3")
|
|
(div
|
|
(~geography/demo-callout :type "info")
|
|
(p (~tw :tokens "text-sm") "Info — styled by its child."))
|
|
(div
|
|
(~geography/demo-callout :type "warning")
|
|
(p (~tw :tokens "text-sm") "Warning — same component, different type."))
|
|
(div
|
|
(~geography/demo-callout :type "success")
|
|
(p (~tw :tokens "text-sm") "Success — child tells parent how to look."))))
|
|
|
|
(defcomp
|
|
~geography/demo-cssx-tw
|
|
()
|
|
(div
|
|
(~tw :tokens "space-y-3")
|
|
(div
|
|
(~tw :tokens "bg-violet-100 rounded-lg p-4")
|
|
(h4
|
|
(~tw :tokens "text-violet-800 font-semibold text-lg")
|
|
"Styled via ~cssx/tw")
|
|
(p
|
|
(~tw :tokens "text-stone-600 text-sm mt-1")
|
|
"Classes injected from spread child."))
|
|
(div
|
|
(~tw :tokens "bg-rose-50 rounded-lg p-4 border border-rose-200")
|
|
(h4
|
|
(~tw :tokens "text-rose-700 font-semibold text-lg")
|
|
"Different tokens")
|
|
(p
|
|
(~tw :tokens "text-stone-600 text-sm mt-1")
|
|
"CSS rules JIT-generated. No build step."))))
|
|
|
|
(defcomp
|
|
~geography/demo-semantic-vars
|
|
()
|
|
(let
|
|
((card (~tw :tokens "bg-stone-50 rounded-lg p-4 shadow-sm border border-stone-200"))
|
|
(heading (~tw :tokens "text-violet-700 text-lg font-bold"))
|
|
(body-text (~tw :tokens "text-stone-600 text-sm mt-1")))
|
|
(div
|
|
(~tw :tokens "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
|
|
(~tw
|
|
:tokens (str
|
|
"rounded-lg p-4 transition-all duration-300 bg-"
|
|
(deref colour)
|
|
"-100 border border-"
|
|
(deref colour)
|
|
"-300"))
|
|
(h4
|
|
(~tw
|
|
:tokens (str "text-" (deref colour) "-800 font-semibold text-lg"))
|
|
(str "Theme: " (deref colour)))
|
|
(p
|
|
(~tw :tokens "text-stone-600 text-sm mt-2 mb-3")
|
|
"Click to change theme. Reactive spreads surgically update classes.")
|
|
(div
|
|
(~tw :tokens "flex gap-2 flex-wrap")
|
|
(button
|
|
:on-click (fn (e) (reset! colour "violet"))
|
|
(~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"))
|
|
(~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"))
|
|
(~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"))
|
|
(~tw
|
|
:tokens "bg-emerald-500 text-white px-3 py-1.5 rounded text-sm font-medium")
|
|
"Emerald")))))
|
|
|
|
(defcomp
|
|
~geography/spreads-content
|
|
()
|
|
(~docs/page
|
|
:title "Spreads"
|
|
(p
|
|
(~tw :tokens "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))"
|
|
(~tw :tokens "text-violet-600 hover:underline")
|
|
"scopes")
|
|
" — every element creates a scope, and spread children emit into it.")
|
|
(~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))"
|
|
(~tw :tokens "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."))
|
|
(~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
|
|
:src (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."))
|
|
(~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
|
|
(~tw :tokens "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."))))
|
|
(~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 "<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)")))))
|
|
(~docs/section
|
|
:title "CSSX: the first application"
|
|
:id "cssx"
|
|
(p (code "~cssx/tw") " is the primary consumer of spreads:")
|
|
(~geography/demo-example
|
|
:demo (~geography/demo-cssx-tw)
|
|
:code (highlight
|
|
"(defcomp ~cssx/tw (tokens)\n (let ((token-list\n (filter (fn (t) (not (= t \"\")))\n (split (or tokens \"\") \" \")))\n (results\n (map cssx-process-token\n token-list))\n (classes (map (fn (r)\n (get r \"cls\")) results))\n (rules (map (fn (r)\n (get r \"rule\")) results)))\n (for-each (fn (rule)\n (collect! \"cssx\" rule)) rules)\n (make-spread\n {\"class\" (join \" \" classes)})))"
|
|
"lisp"))
|
|
(p
|
|
"It uses "
|
|
(code "make-spread")
|
|
" to inject classes, "
|
|
(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 mechanism enables:")
|
|
(ul
|
|
(~tw :tokens "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")
|
|
(li
|
|
(code "~conditional-attrs")
|
|
" — presence/absence of attributes based on state")
|
|
(li "Any component that needs to inject attributes onto its parent")))
|
|
(~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:")
|
|
(~geography/demo-example
|
|
:demo (~geography/demo-semantic-vars)
|
|
:code (highlight
|
|
"(let ((card (~cssx/tw :tokens\n \"bg-stone-50 rounded-lg p-4\n shadow-sm border\"))\n (heading (~cssx/tw :tokens\n \"text-violet-700 text-lg\n font-bold\"))\n (body (~cssx/tw :tokens\n \"text-stone-600 text-sm\")))\n ;; Reuse everywhere:\n (div card\n (h4 heading \"First Card\")\n (p body \"Same variables.\"))\n (div card\n (h4 heading \"Second Card\")\n (p body \"Consistent look.\")))"
|
|
"lisp"))
|
|
(p
|
|
"These are semantic names wrapping utility tokens. Change the definition, "
|
|
"every use updates. No build step, no CSS-in-JS runtime. Just "
|
|
(code "let")
|
|
".")
|
|
(p
|
|
"Namespacing prevents clashes — "
|
|
(code "~app/heading")
|
|
" vs "
|
|
(code "~admin/heading")
|
|
" are different components in different namespaces."))
|
|
(~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")
|
|
" — 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 "
|
|
(code "provide")
|
|
"/"
|
|
(code "emit!")
|
|
" — render-time dynamic scope "
|
|
"that already existed for other reasons:")
|
|
(ul
|
|
(~tw :tokens "list-disc pl-5 space-y-1 text-stone-600")
|
|
(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."))
|
|
(~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))"
|
|
(~tw :tokens "text-violet-600 hover:underline")
|
|
"See the full provide/context/emit! article")
|
|
" for the general primitive, nested scoping, 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 "
|
|
(a
|
|
:href "/sx/(language.(spec.(explore.boundary)))"
|
|
(~tw :tokens "font-mono text-violet-600 hover:underline text-sm")
|
|
"boundary.sx")
|
|
" (Tier 5: Dynamic scope). The "
|
|
(code "provide")
|
|
" special form is in "
|
|
(a
|
|
:href "/sx/(language.(spec.(explore.evaluator)))"
|
|
(~tw :tokens "font-mono text-violet-600 hover:underline text-sm")
|
|
"eval.sx")
|
|
". Element rendering with provide/emit! is visible in all four adapter specs: "
|
|
(a
|
|
:href "/sx/(language.(spec.(explore.adapter-html)))"
|
|
(~tw :tokens "font-mono text-violet-600 hover:underline text-sm")
|
|
"adapter-html")
|
|
", "
|
|
(a
|
|
:href "/sx/(language.(spec.(explore.adapter-async)))"
|
|
(~tw :tokens "font-mono text-violet-600 hover:underline text-sm")
|
|
"adapter-async")
|
|
", "
|
|
(a
|
|
:href "/sx/(language.(spec.(explore.adapter-sx)))"
|
|
(~tw :tokens "font-mono text-violet-600 hover:underline text-sm")
|
|
"adapter-sx")
|
|
", "
|
|
(a
|
|
:href "/sx/(language.(spec.(explore.adapter-dom)))"
|
|
(~tw :tokens "font-mono text-violet-600 hover:underline text-sm")
|
|
"adapter-dom")
|
|
".")))))
|