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>
1157 lines
44 KiB
Plaintext
1157 lines
44 KiB
Plaintext
(defcomp
|
||
~reactive-islands/index/reactive-islands-index-content
|
||
()
|
||
(~docs/page
|
||
:title "Reactive Islands"
|
||
(~docs/section
|
||
:title "Architecture"
|
||
:id "architecture"
|
||
(p "Two orthogonal bars control how an SX page works:")
|
||
(ul
|
||
(~tw :tokens "space-y-1 text-stone-600 list-disc pl-5")
|
||
(li
|
||
(strong "Render boundary")
|
||
" — where rendering happens (server HTML vs client DOM)")
|
||
(li
|
||
(strong "State flow")
|
||
" — how state flows (server state vs client signals)"))
|
||
(div
|
||
(~tw :tokens "overflow-x-auto mt-4 mb-4")
|
||
(table
|
||
(~tw :tokens "w-full text-sm text-left")
|
||
(thead
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-200")
|
||
(th (~tw :tokens "py-2 px-3 font-semibold text-stone-700") "")
|
||
(th
|
||
(~tw :tokens "py-2 px-3 font-semibold text-stone-700")
|
||
"Server State")
|
||
(th
|
||
(~tw :tokens "py-2 px-3 font-semibold text-stone-700")
|
||
"Client State")))
|
||
(tbody
|
||
(~tw :tokens "text-stone-600")
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td
|
||
(~tw :tokens "py-2 px-3 font-semibold text-stone-700")
|
||
"Server Rendering")
|
||
(td (~tw :tokens "py-2 px-3") "Pure hypermedia (htmx)")
|
||
(td (~tw :tokens "py-2 px-3") "SSR + hydrated islands"))
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td
|
||
(~tw :tokens "py-2 px-3 font-semibold text-stone-700")
|
||
"Client Rendering")
|
||
(td (~tw :tokens "py-2 px-3") "SX wire format (current)")
|
||
(td
|
||
(~tw :tokens "py-2 px-3 font-semibold text-violet-700")
|
||
"Reactive islands (this)")))))
|
||
(p
|
||
"Most content stays pure hypermedia. Interactive regions opt into reactivity. The author controls where each component sits on both bars."))
|
||
(~docs/section
|
||
:title "Four Levels"
|
||
:id "levels"
|
||
(div
|
||
(~tw :tokens "space-y-4")
|
||
(div
|
||
(~tw :tokens "rounded border border-stone-200 p-4")
|
||
(div
|
||
(~tw :tokens "font-semibold text-stone-800")
|
||
"Level 0: Pure Hypermedia")
|
||
(p
|
||
(~tw :tokens "text-sm text-stone-600 mt-1")
|
||
"The default. "
|
||
(code "sx-get")
|
||
", "
|
||
(code "sx-post")
|
||
", "
|
||
(code "sx-swap")
|
||
". Server renders everything. No client state. 90% of a typical application."))
|
||
(div
|
||
(~tw :tokens "rounded border border-stone-200 p-4")
|
||
(div
|
||
(~tw :tokens "font-semibold text-stone-800")
|
||
"Level 1: Local DOM Operations")
|
||
(p
|
||
(~tw :tokens "text-sm text-stone-600 mt-1")
|
||
"Imperative escapes: "
|
||
(code "toggle!")
|
||
", "
|
||
(code "set-attr!")
|
||
", "
|
||
(code "on-event")
|
||
". Micro-interactions too small for a server round-trip."))
|
||
(div
|
||
(~tw :tokens "rounded border border-violet-300 bg-violet-50 p-4")
|
||
(div
|
||
(~tw :tokens "font-semibold text-violet-900")
|
||
"Level 2: Reactive Islands")
|
||
(p
|
||
(~tw :tokens "text-sm text-stone-600 mt-1")
|
||
(code "defisland")
|
||
" components with local signals. Fine-grained DOM updates "
|
||
(em "without")
|
||
" virtual DOM, diffing, or component re-renders. A signal change updates only the DOM nodes that read it."))
|
||
(div
|
||
(~tw :tokens "rounded border border-stone-200 p-4")
|
||
(div
|
||
(~tw :tokens "font-semibold text-stone-800")
|
||
"Level 3: Connected Islands")
|
||
(p
|
||
(~tw :tokens "text-sm text-stone-600 mt-1")
|
||
"Islands that share state via signal props or named stores ("
|
||
(code "def-store")
|
||
" / "
|
||
(code "use-store")
|
||
")."))))
|
||
(~docs/section
|
||
:title "Signal Primitives"
|
||
:id "signals"
|
||
(~docs/code
|
||
:src (highlight
|
||
"(signal v) ;; create a reactive container\n(deref s) ;; read value — subscribes in reactive context\n(reset! s v) ;; write new value — notifies subscribers\n(swap! s f) ;; update via function: (f old-value)\n(computed fn) ;; derived signal — auto-tracks dependencies\n(effect fn) ;; side effect — re-runs when deps change\n(batch fn) ;; group writes — one notification pass"
|
||
"lisp"))
|
||
(p
|
||
"Signals are values, not hooks. Create them anywhere — conditionals, loops, closures. No rules of hooks. Pass them as arguments, store them in dicts, share between islands."))
|
||
(~docs/section
|
||
:title "Island Lifecycle"
|
||
:id "lifecycle"
|
||
(ol
|
||
(~tw :tokens "space-y-2 text-stone-600 list-decimal list-inside")
|
||
(li
|
||
(strong "Definition: ")
|
||
(code "defisland")
|
||
" registers a reactive component (like "
|
||
(code "defcomp")
|
||
" + island flag)")
|
||
(li
|
||
(strong "Server render: ")
|
||
"Body evaluated with initial values. "
|
||
(code "deref")
|
||
" returns plain value. Output wrapped in "
|
||
(code "data-sx-island")
|
||
" / "
|
||
(code "data-sx-state"))
|
||
(li
|
||
(strong "Client hydration: ")
|
||
"Finds "
|
||
(code "data-sx-island")
|
||
" elements, creates signals from serialized state, re-renders in reactive context")
|
||
(li
|
||
(strong "Updates: ")
|
||
"Signal changes update only subscribed DOM nodes. No full island re-render")
|
||
(li
|
||
(strong "Disposal: ")
|
||
"Island removed from DOM — all signals and effects cleaned up via "
|
||
(code "with-island-scope"))))
|
||
(~docs/section
|
||
:title "htmx Lakes"
|
||
:id "lakes"
|
||
(p
|
||
"An htmx lake is server-driven content "
|
||
(em "inside")
|
||
" a reactive island. The island provides the reactive boundary; the lake content is swapped via "
|
||
(code "sx-get")
|
||
"/"
|
||
(code "sx-post")
|
||
" like normal hypermedia. This works because signals live in closures, not the DOM.")
|
||
(div
|
||
(~tw :tokens "space-y-2 mt-3")
|
||
(div
|
||
(~tw :tokens "rounded border border-green-200 bg-green-50 p-3")
|
||
(div
|
||
(~tw :tokens "font-semibold text-green-800 text-sm")
|
||
"Swap inside island")
|
||
(p
|
||
(~tw :tokens "text-sm text-stone-600 mt-1")
|
||
"Lake content replaced. Signals survive. Effects rebind to new DOM."))
|
||
(div
|
||
(~tw :tokens "rounded border border-green-200 bg-green-50 p-3")
|
||
(div
|
||
(~tw :tokens "font-semibold text-green-800 text-sm")
|
||
"Swap outside island")
|
||
(p
|
||
(~tw :tokens "text-sm text-stone-600 mt-1")
|
||
"Different part of page updated. Island completely unaffected."))
|
||
(div
|
||
(~tw :tokens "rounded border border-amber-200 bg-amber-50 p-3")
|
||
(div
|
||
(~tw :tokens "font-semibold text-amber-800 text-sm")
|
||
"Swap replaces island")
|
||
(p
|
||
(~tw :tokens "text-sm text-stone-600 mt-1")
|
||
"Island disposed. Local signals lost. Named stores persist — new island reconnects via "
|
||
(code "use-store")
|
||
"."))
|
||
(div
|
||
(~tw :tokens "rounded border border-stone-200 p-3")
|
||
(div
|
||
(~tw :tokens "font-semibold text-stone-800 text-sm")
|
||
"Full page navigation")
|
||
(p
|
||
(~tw :tokens "text-sm text-stone-600 mt-1")
|
||
"Everything cleared. "
|
||
(code "clear-stores")
|
||
" wipes the registry."))))
|
||
(~docs/section
|
||
:title "Event Bridge"
|
||
:id "event-bridge"
|
||
(p
|
||
"A lake has no access to island signals, but can communicate back via DOM custom events. Elements with "
|
||
(code "data-sx-emit")
|
||
" dispatch a "
|
||
(code "CustomEvent")
|
||
" on click; an island effect catches it and updates a signal.")
|
||
(~docs/code
|
||
:src (highlight
|
||
";; Island listens for events from server-rendered lake content\n(bridge-event container \"cart:add\" items\n (fn (detail) (append (deref items) detail)))\n\n;; Server-rendered button dispatches CustomEvent on click\n(button :data-sx-emit \"cart:add\"\n :data-sx-emit-detail (json-serialize (dict :id 42))\n \"Add to Cart\")"
|
||
"lisp"))
|
||
(p
|
||
"Three primitives: "
|
||
(code "emit-event")
|
||
" (dispatch), "
|
||
(code "on-event")
|
||
" (listen), "
|
||
(code "bridge-event")
|
||
" (listen + update signal with automatic cleanup)."))
|
||
(~docs/section
|
||
:title "Named Stores"
|
||
:id "stores"
|
||
(p
|
||
"A named store is a dict of signals at "
|
||
(em "page")
|
||
" scope — not island scope. Multiple islands share the same signals. Stores survive island destruction and recreation.")
|
||
(~docs/code
|
||
:src (highlight
|
||
";; Create once — idempotent, returns existing on second call\n(def-store \"cart\" (fn ()\n (dict :items (signal (list))\n :count (computed (fn () (length (deref items)))))))\n\n;; Use from any island, anywhere in the DOM\n(let ((store (use-store \"cart\")))\n (span (deref (get store \"count\"))))"
|
||
"lisp"))
|
||
(p
|
||
(code "def-store")
|
||
" creates, "
|
||
(code "use-store")
|
||
" retrieves, "
|
||
(code "clear-stores")
|
||
" wipes all on full page navigation."))
|
||
(~docs/section
|
||
:title "Examples"
|
||
:id "examples"
|
||
(p "Each example below shows a live island and its source.")
|
||
(~docs/section
|
||
:title "Counter"
|
||
:id "demo-counter"
|
||
(p "Signals, computed, and " (code "swap!") ".")
|
||
(~reactive-islands/index/demo-counter :initial 0)
|
||
(~docs/code
|
||
:src (highlight
|
||
(component-source "~reactive-islands/index/demo-counter")
|
||
"lisp")))
|
||
(~docs/section
|
||
:title "Temperature Converter"
|
||
:id "demo-temperature"
|
||
(p
|
||
"Two signals, each derived from the other via "
|
||
(code "effect")
|
||
".")
|
||
(~reactive-islands/index/demo-temperature)
|
||
(~docs/code
|
||
:src (highlight
|
||
(component-source "~reactive-islands/index/demo-temperature")
|
||
"lisp")))
|
||
(~docs/section
|
||
:title "Imperative Handlers"
|
||
:id "demo-imperative"
|
||
(p "Multi-statement " (code "(do ...)") " bodies in event handlers.")
|
||
(~reactive-islands/index/demo-imperative)
|
||
(~docs/code
|
||
:src (highlight
|
||
(component-source "~reactive-islands/index/demo-imperative")
|
||
"lisp")))
|
||
(~docs/section
|
||
:title "Stopwatch"
|
||
:id "demo-stopwatch"
|
||
(p
|
||
(code "set-interval")
|
||
" and "
|
||
(code "clear-interval")
|
||
" with signal-driven UI.")
|
||
(~reactive-islands/index/demo-stopwatch)
|
||
(~docs/code
|
||
:src (highlight
|
||
(component-source "~reactive-islands/index/demo-stopwatch")
|
||
"lisp")))
|
||
(~docs/section
|
||
:title "Reactive List"
|
||
:id "demo-reactive-list"
|
||
(p "Dynamic list with keyed reconciliation.")
|
||
(~reactive-islands/index/demo-reactive-list)
|
||
(~docs/code
|
||
:src (highlight
|
||
(component-source "~reactive-islands/index/demo-reactive-list")
|
||
"lisp")))
|
||
(~docs/section
|
||
:title "Input Binding"
|
||
:id "demo-input-binding"
|
||
(p "Two-way binding via " (code ":bind") " attribute.")
|
||
(~reactive-islands/index/demo-input-binding)
|
||
(~docs/code
|
||
:src (highlight
|
||
(component-source "~reactive-islands/index/demo-input-binding")
|
||
"lisp")))
|
||
(~docs/section
|
||
:title "Dynamic Classes"
|
||
:id "demo-dynamic-class"
|
||
(p
|
||
"Reactive class toggling with "
|
||
(code "deref")
|
||
" in attribute expressions.")
|
||
(~reactive-islands/index/demo-dynamic-class)
|
||
(~docs/code
|
||
:src (highlight
|
||
(component-source "~reactive-islands/index/demo-dynamic-class")
|
||
"lisp")))
|
||
(~docs/section
|
||
:title "Portal"
|
||
:id "demo-portal"
|
||
(p "Render content outside the island's DOM subtree.")
|
||
(~reactive-islands/index/demo-portal)
|
||
(~docs/code
|
||
:src (highlight
|
||
(component-source "~reactive-islands/index/demo-portal")
|
||
"lisp")))
|
||
(~docs/section
|
||
:title "Error Boundary"
|
||
:id "demo-error-boundary"
|
||
(p "Catch rendering errors without crashing the page.")
|
||
(~reactive-islands/index/demo-error-boundary)
|
||
(~docs/code
|
||
:src (highlight
|
||
(component-source "~reactive-islands/index/demo-error-boundary")
|
||
"lisp")))
|
||
(~docs/section
|
||
:title "DOM Refs"
|
||
:id "demo-refs"
|
||
(p "Access raw DOM elements via " (code ":ref") " signal.")
|
||
(~reactive-islands/index/demo-refs)
|
||
(~docs/code
|
||
:src (highlight
|
||
(component-source "~reactive-islands/index/demo-refs")
|
||
"lisp")))
|
||
(~docs/section
|
||
:title "Resource"
|
||
:id "demo-resource"
|
||
(p "Async data fetching with loading state.")
|
||
(~reactive-islands/index/demo-resource)
|
||
(~docs/code
|
||
:src (highlight
|
||
(component-source "~reactive-islands/index/demo-resource")
|
||
"lisp")))
|
||
(~docs/section
|
||
:title "Transition"
|
||
:id "demo-transition"
|
||
(p
|
||
"Debounced search with "
|
||
(code "schedule-idle")
|
||
" and "
|
||
(code "batch")
|
||
".")
|
||
(~reactive-islands/index/demo-transition)
|
||
(~docs/code
|
||
:src (highlight
|
||
(component-source "~reactive-islands/index/demo-transition")
|
||
"lisp")))
|
||
(~docs/section
|
||
:title "Named Stores"
|
||
:id "demo-stores"
|
||
(p
|
||
"Two islands sharing state via "
|
||
(code "def-store")
|
||
" / "
|
||
(code "use-store")
|
||
".")
|
||
(~reactive-islands/index/demo-store-writer)
|
||
(~docs/code
|
||
:src (highlight
|
||
(component-source "~reactive-islands/index/demo-store-writer")
|
||
"lisp"))
|
||
(~reactive-islands/index/demo-store-reader)
|
||
(~docs/code
|
||
:src (highlight
|
||
(component-source "~reactive-islands/index/demo-store-reader")
|
||
"lisp")))
|
||
(~docs/section
|
||
:title "Event Bridge"
|
||
:id "demo-event-bridge"
|
||
(p
|
||
"Server-rendered content communicating with an island via "
|
||
(code "bridge-event")
|
||
".")
|
||
(~reactive-islands/index/demo-event-bridge)
|
||
(~docs/code
|
||
:src (highlight
|
||
(component-source "~reactive-islands/index/demo-event-bridge")
|
||
"lisp"))))
|
||
(~docs/section
|
||
:title "Design Principles"
|
||
:id "principles"
|
||
(ol
|
||
(~tw :tokens "space-y-2 text-stone-600 list-decimal list-inside")
|
||
(li
|
||
(strong "Islands are opt-in.")
|
||
" "
|
||
(code "defcomp")
|
||
" is the default. "
|
||
(code "defisland")
|
||
" adds reactivity. No overhead for static content.")
|
||
(li
|
||
(strong "Signals are values, not hooks.")
|
||
" Create anywhere — conditionals, loops, closures. No rules of hooks, no dependency arrays.")
|
||
(li
|
||
(strong "Fine-grained, not component-grained.")
|
||
" A signal change updates the specific DOM node that reads it. No virtual DOM, no diffing, no component re-renders.")
|
||
(li
|
||
(strong "The server is still the authority.")
|
||
" Islands handle client interactions. The server handles auth, data, routing.")
|
||
(li
|
||
(strong "Spec-first.")
|
||
" Signal semantics live in "
|
||
(code "signals.sx")
|
||
". Bootstrapped to JS and Python. Same primitives on future hosts.")
|
||
(li
|
||
(strong "No build step.")
|
||
" Reactive bindings created at runtime. No JSX compilation, no bundler plugins.")))
|
||
(~docs/section
|
||
:title "Implementation Status"
|
||
:id "status"
|
||
(p
|
||
(~tw :tokens "text-stone-600 mb-3")
|
||
"All signal logic lives in "
|
||
(code ".sx")
|
||
" spec files and is bootstrapped to JavaScript and Python. No SX-specific logic in host languages.")
|
||
(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") "Layer")
|
||
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Status")
|
||
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Files")))
|
||
(tbody
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td (~tw :tokens "px-3 py-2 text-stone-700") "Signal runtime spec")
|
||
(td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Done")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500")
|
||
"signals.sx (291 lines)"))
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td (~tw :tokens "px-3 py-2 text-stone-700") "defisland special form")
|
||
(td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Done")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500")
|
||
"eval.sx, special-forms.sx, render.sx"))
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 text-stone-700")
|
||
"DOM adapter (reactive rendering)")
|
||
(td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Done")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500")
|
||
"adapter-dom.sx (+140 lines)"))
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td (~tw :tokens "px-3 py-2 text-stone-700") "HTML adapter (SSR)")
|
||
(td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Done")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500")
|
||
"adapter-html.sx (+65 lines)"))
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td (~tw :tokens "px-3 py-2 text-stone-700") "JS bootstrapper")
|
||
(td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Done")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500")
|
||
"bootstrap_js.py, sx-ref.js (4769 lines)"))
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td (~tw :tokens "px-3 py-2 text-stone-700") "Python bootstrapper")
|
||
(td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Done")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500")
|
||
"bootstrap_py.py, sx_ref.py"))
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td (~tw :tokens "px-3 py-2 text-stone-700") "Test suite")
|
||
(td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "17/17")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500")
|
||
"test-signals.sx"))
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td (~tw :tokens "px-3 py-2 text-stone-700") "Named stores (L3)")
|
||
(td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Spec'd")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500")
|
||
"signals.sx: def-store, use-store, clear-stores"))
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td (~tw :tokens "px-3 py-2 text-stone-700") "Event bridge")
|
||
(td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Spec'd")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500")
|
||
"signals.sx: emit-event, on-event, bridge-event"))
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td (~tw :tokens "px-3 py-2 text-stone-700") "Client hydration")
|
||
(td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Spec'd")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500")
|
||
"boot.sx: sx-hydrate-islands, hydrate-island, dispose-island"))
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td (~tw :tokens "px-3 py-2 text-stone-700") "Event bindings")
|
||
(td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Spec'd")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500")
|
||
"adapter-dom.sx: :on-click → domListen"))
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td (~tw :tokens "px-3 py-2 text-stone-700") "data-sx-emit processing")
|
||
(td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Spec'd")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500")
|
||
"orchestration.sx: process-emit-elements"))
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td (~tw :tokens "px-3 py-2 text-stone-700") "Island disposal")
|
||
(td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Done")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500")
|
||
"boot.sx, orchestration.sx: dispose-islands-in pre-swap"))
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td (~tw :tokens "px-3 py-2 text-stone-700") "Reactive list")
|
||
(td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Done")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500")
|
||
"adapter-dom.sx: map + deref auto-upgrades"))
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td (~tw :tokens "px-3 py-2 text-stone-700") "Input binding")
|
||
(td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Done")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500")
|
||
"adapter-dom.sx: :bind signal, bind-input"))
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td (~tw :tokens "px-3 py-2 text-stone-700") "Keyed reconciliation")
|
||
(td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Done")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500")
|
||
"adapter-dom.sx: :key attr, extract-key"))
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td (~tw :tokens "px-3 py-2 text-stone-700") "Portals")
|
||
(td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Done")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500")
|
||
"adapter-dom.sx: portal render-dom form"))
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td (~tw :tokens "px-3 py-2 text-stone-700") "Error boundaries")
|
||
(td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Done")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500")
|
||
"adapter-dom.sx: error-boundary render-dom form"))
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td (~tw :tokens "px-3 py-2 text-stone-700") "Resource (async signal)")
|
||
(td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Done")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500")
|
||
"signals.sx: resource, promise-then"))
|
||
(tr
|
||
(~tw :tokens "border-b border-stone-100")
|
||
(td (~tw :tokens "px-3 py-2 text-stone-700") "Suspense pattern")
|
||
(td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Done")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500")
|
||
"resource + cond/deref (no special form)"))
|
||
(tr
|
||
(td (~tw :tokens "px-3 py-2 text-stone-700") "Transition pattern")
|
||
(td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Done")
|
||
(td
|
||
(~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500")
|
||
"schedule-idle + batch (no special form)"))))))))
|
||
|
||
(defisland
|
||
~reactive-islands/index/demo-counter
|
||
(&key initial)
|
||
(let
|
||
((count (signal (or initial 0)))
|
||
(doubled (computed (fn () (* 2 (deref count))))))
|
||
(div
|
||
(~tw :tokens "rounded border border-violet-200 bg-violet-50 p-4 my-4")
|
||
(div
|
||
(~tw :tokens "flex items-center gap-4")
|
||
(button
|
||
(~tw :tokens "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700")
|
||
:on-click (fn (e) (swap! count dec))
|
||
"−")
|
||
(span
|
||
(~tw :tokens "text-2xl font-bold text-violet-900 w-12 text-center")
|
||
(deref count))
|
||
(button
|
||
(~tw :tokens "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700")
|
||
:on-click (fn (e) (swap! count inc))
|
||
"+"))
|
||
(p
|
||
(~tw :tokens "text-sm text-stone-500 mt-2")
|
||
"doubled: "
|
||
(span (~tw :tokens "font-mono text-violet-700") (deref doubled))))))
|
||
|
||
(defisland
|
||
~reactive-islands/index/demo-temperature
|
||
()
|
||
(let
|
||
((celsius (signal 20))
|
||
(fahrenheit (computed (fn () (+ (* (deref celsius) 1.8) 32)))))
|
||
(div
|
||
(~tw :tokens "rounded border border-violet-200 bg-violet-50 p-4 my-4")
|
||
(div
|
||
(~tw :tokens "flex items-center gap-3")
|
||
(div
|
||
(~tw :tokens "flex items-center gap-2")
|
||
(button
|
||
(~tw :tokens "px-2 py-1 rounded bg-stone-200 text-stone-700 text-sm hover:bg-stone-300")
|
||
:on-click (fn (e) (swap! celsius (fn (c) (- c 5))))
|
||
"−5")
|
||
(span
|
||
(~tw :tokens "font-mono text-lg font-bold text-violet-900 w-16 text-center")
|
||
(deref celsius))
|
||
(button
|
||
(~tw :tokens "px-2 py-1 rounded bg-stone-200 text-stone-700 text-sm hover:bg-stone-300")
|
||
:on-click (fn (e) (swap! celsius (fn (c) (+ c 5))))
|
||
"+5")
|
||
(span (~tw :tokens "text-stone-500") "°C"))
|
||
(span (~tw :tokens "text-stone-400") "=")
|
||
(span
|
||
(~tw :tokens "font-mono text-lg font-bold text-violet-900")
|
||
(deref fahrenheit))
|
||
(span (~tw :tokens "text-stone-500") "°F")))))
|
||
|
||
(defisland
|
||
~reactive-islands/index/demo-imperative
|
||
()
|
||
(let
|
||
((count (signal 0))
|
||
(text-node (create-text-node "0"))
|
||
(_eff
|
||
(effect
|
||
(fn () (dom-set-text-content text-node (str (deref count)))))))
|
||
(div
|
||
(~tw :tokens "rounded border border-stone-200 bg-stone-50 p-4 my-4")
|
||
(p
|
||
(~tw :tokens "text-sm text-stone-600 mb-2")
|
||
"Imperative style — explicit "
|
||
(code "effect")
|
||
" + "
|
||
(code "create-text-node")
|
||
":")
|
||
(div
|
||
(~tw :tokens "flex items-center gap-4")
|
||
(button
|
||
(~tw :tokens "px-3 py-1 rounded bg-stone-600 text-white text-sm font-medium hover:bg-stone-700")
|
||
:on-click (fn (e) (swap! count dec))
|
||
"−")
|
||
(span
|
||
(~tw :tokens "text-2xl font-bold text-stone-900 w-12 text-center")
|
||
text-node)
|
||
(button
|
||
(~tw :tokens "px-3 py-1 rounded bg-stone-600 text-white text-sm font-medium hover:bg-stone-700")
|
||
:on-click (fn (e) (swap! count inc))
|
||
"+")))))
|
||
|
||
(defisland
|
||
~reactive-islands/index/demo-stopwatch
|
||
()
|
||
(let
|
||
((running (signal false))
|
||
(elapsed (signal 0))
|
||
(time-text (create-text-node "0.0s"))
|
||
(btn-text (create-text-node "Start"))
|
||
(_e1
|
||
(effect
|
||
(fn
|
||
()
|
||
(when
|
||
(deref running)
|
||
(let
|
||
((id (set-interval (fn () (swap! elapsed inc)) 100)))
|
||
(fn () (clear-interval id)))))))
|
||
(_e2
|
||
(effect
|
||
(fn
|
||
()
|
||
(let
|
||
((e (deref elapsed)))
|
||
(dom-set-text-content
|
||
time-text
|
||
(str (floor (/ e 10)) "." (mod e 10) "s"))))))
|
||
(_e3
|
||
(effect
|
||
(fn
|
||
()
|
||
(dom-set-text-content
|
||
btn-text
|
||
(if (deref running) "Stop" "Start"))))))
|
||
(div
|
||
(~tw :tokens "rounded border border-violet-200 bg-violet-50 p-4 my-4")
|
||
(div
|
||
(~tw :tokens "flex items-center gap-4")
|
||
(span
|
||
(~tw :tokens "font-mono text-2xl font-bold text-violet-900 w-24 text-center")
|
||
time-text)
|
||
(button
|
||
(~tw :tokens "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700")
|
||
:on-click (fn (e) (swap! running not))
|
||
btn-text)
|
||
(button
|
||
(~tw :tokens "px-3 py-1 rounded bg-stone-300 text-stone-700 text-sm hover:bg-stone-400")
|
||
:on-click (fn (e) (do (reset! running false) (reset! elapsed 0)))
|
||
"Reset")))))
|
||
|
||
(defisland
|
||
~reactive-islands/index/demo-reactive-list
|
||
()
|
||
(let
|
||
((next-id (signal 1))
|
||
(items (signal (list)))
|
||
(add-item
|
||
(fn
|
||
(e)
|
||
(batch
|
||
(fn
|
||
()
|
||
(swap!
|
||
items
|
||
(fn
|
||
(old)
|
||
(append
|
||
old
|
||
(dict
|
||
"id"
|
||
(deref next-id)
|
||
"text"
|
||
(str "Item " (deref next-id))))))
|
||
(swap! next-id inc)))))
|
||
(remove-item
|
||
(fn
|
||
(id)
|
||
(swap!
|
||
items
|
||
(fn
|
||
(old)
|
||
(filter (fn (item) (not (= (get item "id") id))) old))))))
|
||
(div
|
||
(~tw :tokens "rounded border border-violet-200 bg-violet-50 p-4 my-4")
|
||
(div
|
||
(~tw :tokens "flex items-center gap-3 mb-3")
|
||
(button
|
||
(~tw :tokens "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700")
|
||
:on-click add-item
|
||
"Add Item")
|
||
(span
|
||
(~tw :tokens "text-sm text-stone-500")
|
||
(deref (computed (fn () (len (deref items)))))
|
||
" items"))
|
||
(ul
|
||
(~tw :tokens "space-y-1")
|
||
(map
|
||
(fn
|
||
(item)
|
||
(li
|
||
:key (str (get item "id"))
|
||
(~tw :tokens "flex items-center justify-between bg-white rounded px-3 py-2 text-sm")
|
||
(span (get item "text"))
|
||
(button
|
||
(~tw :tokens "text-stone-400 hover:text-red-500 text-xs")
|
||
:on-click (fn (e) (remove-item (get item "id")))
|
||
"✕")))
|
||
(deref items))))))
|
||
|
||
(defisland
|
||
~reactive-islands/index/demo-input-binding
|
||
()
|
||
(let
|
||
((name (signal "")) (agreed (signal false)))
|
||
(div
|
||
(~tw :tokens "rounded border border-violet-200 bg-violet-50 p-4 my-4 space-y-3")
|
||
(div
|
||
(~tw :tokens "flex items-center gap-3")
|
||
(input
|
||
:type "text"
|
||
:bind name
|
||
:placeholder "Type your name..."
|
||
(~tw :tokens "px-3 py-1.5 rounded border border-stone-300 text-sm focus:outline-none focus:border-violet-400 w-48"))
|
||
(span
|
||
(~tw :tokens "text-sm text-stone-600")
|
||
"Hello, "
|
||
(strong (deref name))
|
||
"!"))
|
||
(div
|
||
(~tw :tokens "flex items-center gap-2")
|
||
(input
|
||
:type "checkbox"
|
||
:bind agreed
|
||
:id "agree-cb"
|
||
(~tw :tokens "rounded border-stone-300"))
|
||
(label
|
||
:for "agree-cb"
|
||
(~tw :tokens "text-sm text-stone-600")
|
||
"I agree to the terms"))
|
||
(when
|
||
(deref agreed)
|
||
(p (~tw :tokens "text-sm text-green-700") "Thanks for agreeing!")))))
|
||
|
||
(defisland
|
||
~reactive-islands/index/demo-portal
|
||
()
|
||
(let
|
||
((open? (signal false)))
|
||
(div
|
||
(~tw :tokens "rounded border border-violet-200 bg-violet-50 p-4 my-4")
|
||
(button
|
||
(~tw :tokens "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700")
|
||
:on-click (fn (e) (swap! open? not))
|
||
(if (deref open?) "Close Modal" "Open Modal"))
|
||
(portal
|
||
"#portal-root"
|
||
(when
|
||
(deref open?)
|
||
(div
|
||
(~tw :tokens "fixed inset-0 bg-black/50 flex items-center justify-center z-50")
|
||
:on-click (fn (e) (reset! open? false))
|
||
(div
|
||
(~tw :tokens "bg-white rounded-lg p-6 max-w-md shadow-xl")
|
||
:on-click (fn (e) (stop-propagation e))
|
||
(h2
|
||
(~tw :tokens "text-lg font-bold text-stone-800 mb-2")
|
||
"Portal Modal")
|
||
(p
|
||
(~tw :tokens "text-stone-600 text-sm mb-4")
|
||
"This content is rendered into "
|
||
(code "#portal-root")
|
||
" — outside the island's DOM subtree. It escapes overflow:hidden, z-index stacking, and layout constraints.")
|
||
(button
|
||
(~tw :tokens "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700")
|
||
:on-click (fn (e) (reset! open? false))
|
||
"Close"))))))))
|
||
|
||
(defisland
|
||
~reactive-islands/index/demo-error-boundary
|
||
()
|
||
(let
|
||
((throw? (signal false)))
|
||
(div
|
||
(~tw :tokens "rounded border border-violet-200 bg-violet-50 p-4 my-4")
|
||
(div
|
||
(~tw :tokens "flex items-center gap-3 mb-3")
|
||
(button
|
||
(~tw :tokens "px-3 py-1 rounded bg-red-600 text-white text-sm font-medium hover:bg-red-700")
|
||
:on-click (fn (e) (reset! throw? true))
|
||
"Trigger Error")
|
||
(button
|
||
(~tw :tokens "px-3 py-1 rounded bg-stone-300 text-stone-700 text-sm hover:bg-stone-400")
|
||
:on-click (fn (e) (reset! throw? false))
|
||
"Reset"))
|
||
(error-boundary
|
||
(fn
|
||
(err retry-fn)
|
||
(div
|
||
(~tw :tokens "p-3 bg-red-50 border border-red-200 rounded")
|
||
(p
|
||
(~tw :tokens "text-red-700 font-medium text-sm")
|
||
"Caught: "
|
||
(error-message err))
|
||
(button
|
||
(~tw :tokens "mt-2 px-3 py-1 rounded bg-red-600 text-white text-sm hover:bg-red-700")
|
||
:on-click (fn (e) (do (reset! throw? false) (invoke retry-fn)))
|
||
"Retry")))
|
||
(do
|
||
(when (deref throw?) (error "Intentional explosion!"))
|
||
(p
|
||
(~tw :tokens "text-sm text-green-700")
|
||
"Everything is fine. Click \"Trigger Error\" to throw."))))))
|
||
|
||
(defisland
|
||
~reactive-islands/index/demo-refs
|
||
()
|
||
(let
|
||
((my-ref (dict "current" nil)) (msg (signal "")))
|
||
(div
|
||
(~tw :tokens "rounded border border-violet-200 bg-violet-50 p-4 my-4 space-y-3")
|
||
(input
|
||
:ref my-ref
|
||
:type "text"
|
||
:placeholder "I can be focused programmatically"
|
||
(~tw :tokens "px-3 py-1.5 rounded border border-stone-300 text-sm focus:outline-none focus:border-violet-400 w-64"))
|
||
(div
|
||
(~tw :tokens "flex items-center gap-3")
|
||
(button
|
||
(~tw :tokens "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700")
|
||
:on-click (fn (e) (dom-focus (get my-ref "current")))
|
||
"Focus Input")
|
||
(button
|
||
(~tw :tokens "px-3 py-1 rounded bg-stone-300 text-stone-700 text-sm hover:bg-stone-400")
|
||
:on-click (fn
|
||
(e)
|
||
(let
|
||
((el (get my-ref "current")))
|
||
(reset!
|
||
msg
|
||
(str
|
||
"Tag: "
|
||
(dom-tag-name el)
|
||
", value: \""
|
||
(dom-get-prop el "value")
|
||
"\""))))
|
||
"Read Input"))
|
||
(when
|
||
(not (= (deref msg) ""))
|
||
(p (~tw :tokens "text-sm text-stone-600 font-mono") (deref msg))))))
|
||
|
||
(defisland
|
||
~reactive-islands/index/demo-dynamic-class
|
||
()
|
||
(let
|
||
((danger (signal false)) (size (signal 16)))
|
||
(div
|
||
(~tw :tokens "rounded border border-violet-200 bg-violet-50 p-4 my-4 space-y-3")
|
||
(div
|
||
(~tw :tokens "flex items-center gap-3")
|
||
(button
|
||
(~tw :tokens "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700")
|
||
:on-click (fn (e) (swap! danger not))
|
||
(if (deref danger) "Safe mode" "Danger mode"))
|
||
(button
|
||
(~tw :tokens "px-3 py-1 rounded bg-stone-300 text-stone-700 text-sm hover:bg-stone-400")
|
||
:on-click (fn (e) (swap! size (fn (s) (+ s 2))))
|
||
"Bigger")
|
||
(button
|
||
(~tw :tokens "px-3 py-1 rounded bg-stone-300 text-stone-700 text-sm hover:bg-stone-400")
|
||
:on-click (fn (e) (swap! size (fn (s) (max 10 (- s 2)))))
|
||
"Smaller"))
|
||
(div
|
||
:class (str
|
||
"p-3 rounded font-medium transition-colors "
|
||
(if
|
||
(deref danger)
|
||
"bg-red-100 text-red-800"
|
||
"bg-green-100 text-green-800"))
|
||
:style (str "font-size:" (deref size) "px")
|
||
"This element's class and style are reactive."))))
|
||
|
||
(defisland
|
||
~reactive-islands/index/demo-resource
|
||
()
|
||
(let
|
||
((data (resource (fn () (promise-delayed 1500 (dict "name" "Ada Lovelace" "role" "First Programmer" "year" 1843))))))
|
||
(div
|
||
(~tw :tokens "rounded border border-violet-200 bg-violet-50 p-4 my-4")
|
||
(cond
|
||
(get (deref data) "loading")
|
||
(div
|
||
(~tw :tokens "flex items-center gap-2 text-stone-500")
|
||
(span
|
||
(~tw :tokens "inline-block w-4 h-4 border-2 border-stone-300 border-t-violet-600 rounded-full animate-spin"))
|
||
(span (~tw :tokens "text-sm") "Loading..."))
|
||
(get (deref data) "error")
|
||
(div
|
||
(~tw :tokens "p-3 bg-red-50 border border-red-200 rounded")
|
||
(p
|
||
(~tw :tokens "text-red-700 text-sm")
|
||
"Error: "
|
||
(get (deref data) "error")))
|
||
:else (let
|
||
((d (get (deref data) "data")))
|
||
(div
|
||
(~tw :tokens "space-y-1")
|
||
(p (~tw :tokens "font-bold text-stone-800") (get d "name"))
|
||
(p
|
||
(~tw :tokens "text-sm text-stone-600")
|
||
(get d "role")
|
||
" ("
|
||
(get d "year")
|
||
")")))))))
|
||
|
||
(defisland
|
||
~reactive-islands/index/demo-transition
|
||
()
|
||
(let
|
||
((query (signal ""))
|
||
(all-items
|
||
(list
|
||
"Signals"
|
||
"Effects"
|
||
"Computed"
|
||
"Batch"
|
||
"Stores"
|
||
"Islands"
|
||
"Portals"
|
||
"Error Boundaries"
|
||
"Resources"
|
||
"Input Binding"
|
||
"Keyed Lists"
|
||
"Event Bridge"
|
||
"Reactive Text"
|
||
"Reactive Attrs"
|
||
"Reactive Fragments"
|
||
"Disposal"
|
||
"Hydration"
|
||
"CSSX"
|
||
"Macros"
|
||
"Refs"))
|
||
(filtered (signal (list)))
|
||
(pending (signal false)))
|
||
(reset! filtered all-items)
|
||
(let
|
||
((_eff (effect (fn () (let ((q (lower (deref query)))) (if (= q "") (do (reset! pending false) (reset! filtered all-items)) (do (reset! pending true) (schedule-idle (fn () (batch (fn () (reset! filtered (filter (fn (item) (contains? (lower item) q)) all-items)) (reset! pending false))))))))))))
|
||
(div
|
||
(~tw :tokens "rounded border border-violet-200 bg-violet-50 p-4 my-4 space-y-3")
|
||
(div
|
||
(~tw :tokens "flex items-center gap-3")
|
||
(input
|
||
:type "text"
|
||
:bind query
|
||
:placeholder "Filter features..."
|
||
(~tw :tokens "px-3 py-1.5 rounded border border-stone-300 text-sm focus:outline-none focus:border-violet-400 w-48"))
|
||
(when
|
||
(deref pending)
|
||
(span (~tw :tokens "text-xs text-stone-400") "Filtering...")))
|
||
(ul
|
||
(~tw :tokens "space-y-1")
|
||
(map
|
||
(fn
|
||
(item)
|
||
(li
|
||
:key item
|
||
(~tw :tokens "text-sm text-stone-700 bg-white rounded px-3 py-1.5")
|
||
item))
|
||
(deref filtered)))))))
|
||
|
||
(defisland
|
||
~reactive-islands/index/demo-store-writer
|
||
()
|
||
(let
|
||
((store (def-store "demo-theme" (fn () (dict "color" (signal "violet") "dark" (signal false))))))
|
||
(div
|
||
(~tw :tokens "rounded border border-stone-200 bg-stone-50 p-4 my-2")
|
||
(p
|
||
(~tw :tokens "text-xs font-semibold text-stone-500 mb-2")
|
||
"Island A — Store Writer")
|
||
(div
|
||
(~tw :tokens "flex items-center gap-3")
|
||
(select
|
||
:bind (get store "color")
|
||
(~tw :tokens "px-2 py-1 rounded border border-stone-300 text-sm")
|
||
(option :value "violet" "Violet")
|
||
(option :value "blue" "Blue")
|
||
(option :value "green" "Green")
|
||
(option :value "red" "Red"))
|
||
(label
|
||
(~tw :tokens "flex items-center gap-1 text-sm text-stone-600")
|
||
(input
|
||
:type "checkbox"
|
||
:bind (get store "dark")
|
||
(~tw :tokens "rounded border-stone-300"))
|
||
"Dark mode")))))
|
||
|
||
(defisland
|
||
~reactive-islands/index/demo-store-reader
|
||
()
|
||
(let
|
||
((store (use-store "demo-theme")))
|
||
(div
|
||
(~tw :tokens "rounded border border-stone-200 bg-stone-50 p-4 my-2")
|
||
(p
|
||
(~tw :tokens "text-xs font-semibold text-stone-500 mb-2")
|
||
"Island B — Store Reader")
|
||
(div
|
||
:class (str
|
||
"p-3 rounded font-medium text-sm "
|
||
(if
|
||
(deref (get store "dark"))
|
||
(str
|
||
"bg-"
|
||
(deref (get store "color"))
|
||
"-900 text-"
|
||
(deref (get store "color"))
|
||
"-100")
|
||
(str
|
||
"bg-"
|
||
(deref (get store "color"))
|
||
"-100 text-"
|
||
(deref (get store "color"))
|
||
"-800")))
|
||
"Styled by signals from Island A"))))
|
||
|
||
(defisland
|
||
~reactive-islands/index/demo-event-bridge
|
||
()
|
||
(let
|
||
((messages (signal (list)))
|
||
(_eff
|
||
(effect
|
||
(fn
|
||
()
|
||
(let
|
||
((cb (host-callback (fn (e) (swap! messages (fn (old) (append old (host-get (event-detail e) "text"))))))))
|
||
(host-call (dom-document) "addEventListener" "inbox:message" cb)
|
||
(fn
|
||
()
|
||
(host-call
|
||
(dom-document)
|
||
"removeEventListener"
|
||
"inbox:message"
|
||
cb)))))))
|
||
(div
|
||
(p
|
||
(~tw :tokens "text-xs font-semibold text-stone-500 mb-2")
|
||
"Event Bridge Demo")
|
||
(p
|
||
(~tw :tokens "text-sm text-stone-600 mb-2")
|
||
"The buttons below simulate server-rendered content dispatching events into the island.")
|
||
(div
|
||
(~tw :tokens "flex gap-2 mb-3")
|
||
(button
|
||
:data-sx-emit "inbox:message"
|
||
:data-sx-emit-detail "{\"text\":\"Hello from the lake!\"}"
|
||
(~tw :tokens "px-3 py-1.5 bg-violet-600 text-white rounded text-sm hover:bg-violet-700")
|
||
"Send \"Hello\"")
|
||
(button
|
||
:data-sx-emit "inbox:message"
|
||
:data-sx-emit-detail "{\"text\":\"Another message\"}"
|
||
(~tw :tokens "px-3 py-1.5 bg-blue-600 text-white rounded text-sm hover:bg-blue-700")
|
||
"Send \"Another\"")
|
||
(button
|
||
:on-click (fn (e) (reset! messages (list)))
|
||
(~tw :tokens "px-3 py-1.5 bg-stone-200 text-stone-700 rounded text-sm hover:bg-stone-300")
|
||
"Clear"))
|
||
(div
|
||
(~tw :tokens "min-h-12 p-3 rounded bg-stone-100 border border-stone-200")
|
||
(if
|
||
(empty? (deref messages))
|
||
(p (~tw :tokens "text-stone-400 text-sm") "No messages yet.")
|
||
(ul
|
||
(~tw :tokens "space-y-1")
|
||
(map
|
||
(fn
|
||
(msg)
|
||
(li (~tw :tokens "text-sm text-stone-700") (str "→ " msg)))
|
||
(deref messages))))))))
|