Files
rose-ash/sx/sx/reactive-islands/demo.sx
giles d40a9c6796 sx-tools: WASM kernel updates, TW/CSSX rework, content refresh, new debugging tools
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>
2026-04-02 11:31:57 +00:00

633 lines
24 KiB
Plaintext

(defcomp
~reactive-islands/demo/reactive-islands-demo-content
()
(~docs/page
:title "Reactive Islands — Examples"
(~docs/section
:title "Live interactive islands"
:id "intro"
(p
(strong "Every example below is a live interactive island")
" — not a static code snippet. Click the buttons, type in the inputs. The signal runtime is defined in "
(code "signals.sx")
", bootstrapped to JavaScript. No hand-written signal logic.")
(p
"Each island uses "
(code "defisland")
" with signals ("
(code "signal")
", "
(code "deref")
", "
(code "reset!")
", "
(code "swap!")
"), derived values ("
(code "computed")
"), side effects ("
(code "effect")
"), and batch updates ("
(code "batch")
")."))
(~docs/section
:title "Examples"
:id "examples"
(ol
(~tw :tokens "space-y-1")
(map
(fn
(item)
(li
(a
:href (get item "href")
:sx-get (get item "href")
:sx-target "#sx-content"
:sx-select "#sx-content"
:sx-swap "outerHTML"
:sx-push-url "true"
(~tw :tokens "text-violet-600 hover:underline")
(get item "label"))))
reactive-examples-nav-items)))))
(defcomp
~reactive-islands/demo/example-counter
()
(~docs/page
:title "Signal + Computed + Effect"
(p
"A signal holds a value. A computed derives from it. Click the buttons — the counter and doubled value update instantly, no server round-trip.")
(~reactive-islands/index/demo-counter :initial 0)
(~docs/code
:src (highlight
(component-source "~reactive-islands/index/demo-counter")
"lisp"))
(p
(code "(deref count)")
" in a text position creates a reactive text node. When "
(code "count")
" changes, "
(em "only that text node")
" updates. "
(code "doubled")
" recomputes automatically. No diffing.")))
(defcomp
~reactive-islands/demo/example-temperature
()
(~docs/page
:title "Temperature Converter"
(p
"Two derived values from one signal. Click to change Celsius — Fahrenheit updates reactively.")
(~reactive-islands/index/demo-temperature)
(~docs/code
:src (highlight
(component-source "~reactive-islands/index/demo-temperature")
"lisp"))
(p
"The actual implementation uses "
(code "computed")
" for Fahrenheit: "
(code "(computed (fn () (+ (* (deref celsius) 1.8) 32)))")
". The "
(code "(deref fahrenheit)")
" in the span creates a reactive text node that updates when celsius changes.")
(div
(~tw :tokens "mt-6")
(~reactive-islands/test-runner-placeholder)
(script
:type "text/sx-test"
:data-for "temperature"
"(defsuite \"temperature converter\" (deftest \"initial celsius is 20\" (let ((celsius (signal 20))) (assert-signal-value celsius 20))) (deftest \"computed fahrenheit = celsius * 1.8 + 32\" (let ((celsius (signal 20)) (fahrenheit (computed (fn () (+ (* (deref celsius) 1.8) 32))))) (assert-signal-value fahrenheit 68) (assert-computed-depends-on fahrenheit celsius))) (deftest \"+5 increments celsius\" (let ((celsius (signal 20)) (btn (mock-element \"button\"))) (mock-add-listener! btn \"click\" (fn (e) (swap! celsius (fn (c) (+ c 5))))) (simulate-click btn) (assert-signal-value celsius 25))) (deftest \"fahrenheit updates on celsius change\" (let ((celsius (signal 20)) (fahrenheit (computed (fn () (+ (* (deref celsius) 1.8) 32))))) (reset! celsius 0) (assert-signal-value fahrenheit 32) (reset! celsius 100) (assert-signal-value fahrenheit 212))) (deftest \"multiple clicks accumulate\" (let ((celsius (signal 20)) (fahrenheit (computed (fn () (+ (* (deref celsius) 1.8) 32)))) (btn (mock-element \"button\"))) (mock-add-listener! btn \"click\" (fn (e) (swap! celsius (fn (c) (+ c 5))))) (simulate-click btn) (simulate-click btn) (simulate-click btn) (assert-signal-value celsius 35) (assert-signal-value fahrenheit 95))))"))))
(defcomp
~reactive-islands/demo/example-stopwatch
()
(~docs/page
:title "Effect + Cleanup: Stopwatch"
(p
"Effects can return cleanup functions. This stopwatch starts a "
(code "set-interval")
" — the cleanup clears it when the running signal toggles off.")
(~reactive-islands/index/demo-stopwatch)
(~docs/code
:src (highlight
(component-source "~reactive-islands/index/demo-stopwatch")
"lisp"))
(p
"Three effects, each tracking different signals. The timer effect's cleanup fires before each re-run — toggling "
(code "running")
" off clears the interval. No hook rules: effects can appear anywhere, in any order.")))
(defcomp
~reactive-islands/demo/example-imperative
()
(~docs/page
:title "Imperative Pattern"
(p
"For complex reactivity (dynamic classes, conditional text), use the imperative pattern: "
(code "create-text-node")
" + "
(code "effect")
" + "
(code "dom-set-text-content")
".")
(~reactive-islands/index/demo-imperative)
(~docs/code
:src (highlight
(component-source "~reactive-islands/index/demo-imperative")
"lisp"))
(p
"Two patterns exist: "
(strong "declarative")
" ("
(code "(span (deref sig))")
" — auto-reactive via "
(code "reactive-text")
") and "
(strong "imperative")
" ("
(code "create-text-node")
" + "
(code "effect")
" — explicit, full control). Use declarative for simple text, imperative for dynamic classes, conditional DOM, or complex updates.")))
(defcomp
~reactive-islands/demo/example-reactive-list
()
(~docs/page
:title "Reactive List"
(p
"When "
(code "map")
" is used with "
(code "(deref signal)")
" inside an island, it auto-upgrades to a reactive list. With "
(code ":key")
" attributes, existing DOM nodes are reused across updates — only additions, removals, and reorderings touch the DOM.")
(~reactive-islands/index/demo-reactive-list)
(~docs/code
:src (highlight
(component-source "~reactive-islands/index/demo-reactive-list")
"lisp"))
(p
(code ":key")
" identifies each list item. When items change, the reconciler matches old and new keys — reusing existing DOM nodes, inserting new ones, and removing stale ones. Without keys, the list falls back to clear-and-rerender. "
(code "batch")
" groups the two signal writes into one update pass.")))
(defcomp
~reactive-islands/demo/example-input-binding
()
(~docs/page
:title "Input Binding"
(p
"The "
(code ":bind")
" attribute creates a two-way link between a signal and a form element. Type in the input — the signal updates. Change the signal — the input updates. Works with text inputs, checkboxes, radios, textareas, and selects.")
(~reactive-islands/index/demo-input-binding)
(~docs/code
:src (highlight
(component-source "~reactive-islands/index/demo-input-binding")
"lisp"))
(p
(code ":bind")
" detects the element type automatically — text inputs use "
(code "value")
" + "
(code "input")
" event, checkboxes use "
(code "checked")
" + "
(code "change")
" event. The effect only updates the DOM when the value actually changed, preventing cursor jump.")))
(defcomp
~reactive-islands/demo/example-portal
()
(~docs/page
:title "Portals"
(p
"A "
(code "portal")
" renders children into a DOM node "
(em "outside")
" the island's subtree. Essential for modals, tooltips, and toasts — anything that must escape "
(code "overflow:hidden")
" or z-index stacking.")
(~reactive-islands/index/demo-portal)
(~docs/code
:src (highlight
(component-source "~reactive-islands/index/demo-portal")
"lisp"))
(p
"The portal content lives in "
(code "#portal-root")
" (typically at the page body level), not inside the island. On island disposal, portal content is automatically removed from its target — the "
(code "register-in-scope")
" mechanism handles cleanup.")))
(defcomp
~reactive-islands/demo/example-error-boundary
()
(~docs/page
:title "Error Boundaries"
(p
"When an island's rendering or effect throws, "
(code "error-boundary")
" catches the error and renders a fallback. The fallback receives the error and a retry function. Partial effects created before the error are disposed automatically.")
(~reactive-islands/index/demo-error-boundary)
(~docs/code
:src (highlight
(component-source "~reactive-islands/index/demo-error-boundary")
"lisp"))
(p
"React equivalent: "
(code "componentDidCatch")
" / "
(code "ErrorBoundary")
". SX's version is simpler — one form, not a class. The "
(code "error-boundary")
" form is a render-dom special form in "
(code "adapter-dom.sx")
".")))
(defcomp
~reactive-islands/demo/example-refs
()
(~docs/page
:title "Refs — Imperative DOM Access"
(p
"The "
(code ":ref")
" attribute captures a DOM element handle into a dict. Use it for imperative operations: focusing, measuring, reading values.")
(~reactive-islands/index/demo-refs)
(~docs/code
:src (highlight
(component-source "~reactive-islands/index/demo-refs")
"lisp"))
(p
"React equivalent: "
(code "useRef")
". In SX, a ref is just "
(code "(dict \"current\" nil)")
" — no special API. The "
(code ":ref")
" attribute sets "
(code "(dict-set! ref \"current\" el)")
" when the element is created. Read it with "
(code "(get ref \"current\")")
".")))
(defcomp
~reactive-islands/demo/example-dynamic-class
()
(~docs/page
:title "Dynamic Class and Style"
(p
"React uses "
(code "className")
" and "
(code "style")
" props with state. SX does the same — "
(code "(deref signal)")
" inside a "
(code ":class")
" or "
(code ":style")
" attribute creates a reactive binding. The attribute updates when the signal changes.")
(~reactive-islands/index/demo-dynamic-class)
(~docs/code
:src (highlight
(component-source "~reactive-islands/index/demo-dynamic-class")
"lisp"))
(p
"React equivalent: "
(code "className={danger ? 'red' : 'green'}")
" and "
(code "style={{fontSize: size}}")
". In SX the "
(code "str")
" + "
(code "if")
" + "
(code "deref")
" pattern handles it — no "
(code "classnames")
" library needed. For complex conditional classes, use a "
(code "computed")
" or a CSSX "
(code "defcomp")
" that returns a class string.")))
(defcomp
~reactive-islands/demo/example-resource
()
(~docs/page
:title "Resource + Suspense Pattern"
(p
(code "resource")
" wraps an async operation into a signal with "
(code "loading")
"/"
(code "data")
"/"
(code "error")
" states. Combined with "
(code "cond")
" + "
(code "deref")
", this is the suspense pattern — no special form needed.")
(~reactive-islands/index/demo-resource)
(~docs/code
:src (highlight
(component-source "~reactive-islands/index/demo-resource")
"lisp"))
(p
"React equivalent: "
(code "Suspense")
" + "
(code "use()")
" or "
(code "useSWR")
". SX doesn't need a special "
(code "suspense")
" form because "
(code "resource")
" returns a signal and "
(code "cond")
" + "
(code "deref")
" creates reactive conditional rendering. When the promise resolves, the signal updates and the "
(code "cond")
" branch switches automatically.")))
(defcomp
~reactive-islands/demo/example-transition
()
(~docs/page
:title "Transition Pattern"
(p
"React's "
(code "startTransition")
" defers non-urgent updates so typing stays responsive. In SX: "
(code "schedule-idle")
" + "
(code "batch")
". The filter runs during idle time, not blocking the input event.")
(~reactive-islands/index/demo-transition)
(~docs/code
:src (highlight
(component-source "~reactive-islands/index/demo-transition")
"lisp"))
(p
"React equivalent: "
(code "startTransition(() => setFiltered(...))")
". SX uses "
(code "schedule-idle")
" ("
(code "requestIdleCallback")
" under the hood) to defer the expensive "
(code "filter")
" operation, and "
(code "batch")
" to group the result into one update. Fine-grained signals already avoid the jank that makes transitions critical in React — this pattern is for truly expensive computations.")))
(defcomp
~reactive-islands/demo/example-stores
()
(~docs/page
:title "Shared Stores"
(p
"React uses "
(code "Context")
" or state management libraries for cross-component state. SX uses "
(code "def-store")
" / "
(code "use-store")
" — named signal containers that persist across island creation/destruction.")
(~reactive-islands/index/demo-store-writer)
(~reactive-islands/index/demo-store-reader)
(~docs/code
:src (highlight
(str
(component-source "~reactive-islands/index/demo-store-writer")
"\n\n"
(component-source "~reactive-islands/index/demo-store-reader"))
"lisp"))
(p
"React equivalent: "
(code "createContext")
" + "
(code "useContext")
" or Redux/Zustand. Stores are simpler — just named dicts of signals at page scope. "
(code "def-store")
" creates once, "
(code "use-store")
" retrieves. Stores survive island disposal but clear on full page navigation.")))
(defcomp
~reactive-islands/demo/example-event-bridge-demo
()
(~docs/page
:title "Event Bridge"
(p
"Server-rendered content inside an island (an htmx \"lake\") can communicate with island signals via DOM custom events. Buttons with "
(code "data-sx-emit")
" dispatch events that island effects catch.")
(~reactive-islands/index/demo-event-bridge)
(~docs/code
:src (highlight
(component-source "~reactive-islands/index/demo-event-bridge")
"lisp"))
(p
"The "
(code "data-sx-emit")
" attribute is processed by the client engine — it adds a click handler that dispatches a CustomEvent with the JSON from "
(code "data-sx-emit-detail")
". The event bubbles up to the island container where "
(code "bridge-event")
" catches it.")))
(defcomp
~reactive-islands/demo/example-defisland
()
(~docs/page
:title "How defisland Works"
(p
(code "defisland")
" creates a reactive component. Same calling convention as "
(code "defcomp")
" — keyword args, rest children — but with a reactive boundary. Inside an island, "
(code "deref")
" subscribes DOM nodes to signals.")
(~docs/code
:src (highlight
";; Definition — same syntax as defcomp\n(defisland ~reactive-islands/demo/counter (&key initial)\n (let ((count (signal (or initial 0))))\n (div\n (span (deref count)) ;; reactive text node\n (button :on-click (fn (e) (swap! count inc)) ;; event handler\n \"+\"))))\n\n;; Usage — same as any component\n(~reactive-islands/demo/counter :initial 42)\n\n;; Server-side rendering:\n;; <div data-sx-island=\"counter\" data-sx-state='{\"initial\":42}'>\n;; <span>42</span><button>+</button>\n;; </div>\n;;\n;; Client hydrates: signals + effects + event handlers attach"
"lisp"))
(p
"Each "
(code "deref")
" call registers the enclosing DOM node as a subscriber. Signal changes update "
(em "only")
" the subscribed nodes — no virtual DOM, no diffing, no component re-renders.")))
(defcomp
~reactive-islands/demo/example-tests
()
(~docs/page
:title "Test Suite"
(p
"17 tests verify the signal runtime against the spec. All pass in the Python test runner (which uses the hand-written evaluator with native platform primitives).")
(~docs/code
:src (highlight
";; Signal basics (6 tests)\n(assert-true (signal? (signal 42)))\n(assert-equal 42 (deref (signal 42)))\n(assert-equal 5 (deref 5)) ;; non-signal passthrough\n\n;; reset! changes value\n(let ((s (signal 0)))\n (reset! s 10)\n (assert-equal 10 (deref s)))\n\n;; reset! does NOT notify when value unchanged (identical? check)\n\n;; Computed (3 tests)\n(let ((a (signal 3)) (b (signal 4))\n (sum (computed (fn () (+ (deref a) (deref b))))))\n (assert-equal 7 (deref sum))\n (reset! a 10)\n (assert-equal 14 (deref sum)))\n\n;; Effects (4 tests) — immediate run, re-run on change, dispose, cleanup\n;; Batch (1 test) — defers notifications, deduplicates subscribers\n;; defisland (3 tests) — creates island, callable, accepts children"
"lisp"))
(p
(~tw :tokens "mt-2 text-sm text-stone-500")
"Run: "
(code "python3 shared/sx/tests/run.py signals"))))
(defcomp
~reactive-islands/demo/example-coverage
()
(~docs/page
:title "React Feature Coverage"
(p
"Every React feature has an SX equivalent — most are simpler because signals are fine-grained.")
(div
(~tw :tokens "overflow-x-auto rounded border border-stone-200")
(table
(~tw :tokens "w-full text-left text-sm")
(thead
(tr
(~tw :tokens "border-b border-stone-200 bg-stone-100")
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "React")
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "SX")
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Demo")))
(tbody
(tr
(~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 text-stone-700") "useState")
(td
(~tw :tokens "px-3 py-2 font-mono text-xs text-violet-700")
"(signal value)")
(td (~tw :tokens "px-3 py-2 text-xs text-stone-500") "#1"))
(tr
(~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 text-stone-700") "useMemo")
(td
(~tw :tokens "px-3 py-2 font-mono text-xs text-violet-700")
"(computed (fn () ...))")
(td (~tw :tokens "px-3 py-2 text-xs text-stone-500") "#1, #2"))
(tr
(~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 text-stone-700") "useEffect")
(td
(~tw :tokens "px-3 py-2 font-mono text-xs text-violet-700")
"(effect (fn () ...))")
(td (~tw :tokens "px-3 py-2 text-xs text-stone-500") "#3"))
(tr
(~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 text-stone-700") "useRef")
(td
(~tw :tokens "px-3 py-2 font-mono text-xs text-violet-700")
"(dict \"current\" nil) + :ref")
(td (~tw :tokens "px-3 py-2 text-xs text-stone-500") "#9"))
(tr
(~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 text-stone-700") "useCallback")
(td
(~tw :tokens "px-3 py-2 font-mono text-xs text-violet-700")
"(fn (...) ...) — no dep arrays")
(td (~tw :tokens "px-3 py-2 text-xs text-stone-500") "N/A"))
(tr
(~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 text-stone-700") "className / style")
(td
(~tw :tokens "px-3 py-2 font-mono text-xs text-violet-700")
":class (str ...) :style (str ...)")
(td (~tw :tokens "px-3 py-2 text-xs text-stone-500") "#10"))
(tr
(~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 text-stone-700") "Controlled inputs")
(td
(~tw :tokens "px-3 py-2 font-mono text-xs text-violet-700")
":bind signal")
(td (~tw :tokens "px-3 py-2 text-xs text-stone-500") "#6"))
(tr
(~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 text-stone-700") "key prop")
(td
(~tw :tokens "px-3 py-2 font-mono text-xs text-violet-700")
":key value")
(td (~tw :tokens "px-3 py-2 text-xs text-stone-500") "#5"))
(tr
(~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 text-stone-700") "createPortal")
(td
(~tw :tokens "px-3 py-2 font-mono text-xs text-violet-700")
"(portal \"#target\" ...)")
(td (~tw :tokens "px-3 py-2 text-xs text-stone-500") "#7"))
(tr
(~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 text-stone-700") "ErrorBoundary")
(td
(~tw :tokens "px-3 py-2 font-mono text-xs text-violet-700")
"(error-boundary fallback ...)")
(td (~tw :tokens "px-3 py-2 text-xs text-stone-500") "#8"))
(tr
(~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 text-stone-700") "Suspense + use()")
(td
(~tw :tokens "px-3 py-2 font-mono text-xs text-violet-700")
"(resource fn) + cond/deref")
(td (~tw :tokens "px-3 py-2 text-xs text-stone-500") "#11"))
(tr
(~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 text-stone-700") "startTransition")
(td
(~tw :tokens "px-3 py-2 font-mono text-xs text-violet-700")
"schedule-idle + batch")
(td (~tw :tokens "px-3 py-2 text-xs text-stone-500") "#12"))
(tr
(~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 text-stone-700") "Context / Redux")
(td
(~tw :tokens "px-3 py-2 font-mono text-xs text-violet-700")
"def-store / use-store")
(td (~tw :tokens "px-3 py-2 text-xs text-stone-500") "#13"))
(tr
(~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 text-stone-700") "Virtual DOM / diffing")
(td
(~tw :tokens "px-3 py-2 text-xs text-stone-500")
"N/A — fine-grained signals update exact DOM nodes")
(td (~tw :tokens "px-3 py-2 text-xs text-stone-500") ""))
(tr
(~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 text-stone-700") "JSX / build step")
(td
(~tw :tokens "px-3 py-2 text-xs text-stone-500")
"N/A — s-expressions are the syntax")
(td (~tw :tokens "px-3 py-2 text-xs text-stone-500") ""))
(tr
(~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 text-stone-700") "Server Components")
(td
(~tw :tokens "px-3 py-2 text-xs text-stone-500")
"N/A — aser mode already expands server-side")
(td (~tw :tokens "px-3 py-2 text-xs text-stone-500") ""))
(tr
(~tw :tokens "border-b border-stone-100")
(td (~tw :tokens "px-3 py-2 text-stone-700") "Concurrent rendering")
(td
(~tw :tokens "px-3 py-2 text-xs text-stone-500")
"N/A — fine-grained updates are inherently incremental")
(td (~tw :tokens "px-3 py-2 text-xs text-stone-500") ""))
(tr
(td (~tw :tokens "px-3 py-2 text-stone-700") "Hooks rules")
(td
(~tw :tokens "px-3 py-2 text-xs text-stone-500")
"N/A — signals are values, no ordering rules")
(td (~tw :tokens "px-3 py-2 text-xs text-stone-500") "")))))))