Files
rose-ash/sx/sx/reactive-islands/index.sx
giles 1341c144da URL restructure, 404 page, trailing slash normalization, layout fixes
- Rename /reactive-islands/ → /reactive/, /reference/ → /hypermedia/reference/,
  /examples/ → /hypermedia/examples/ across all .sx and .py files
- Add 404 error page (not-found.sx) working on both server refresh and
  client-side SX navigation via orchestration.sx error response handling
- Add trailing slash redirect (GET only, excludes /api/, /static/, /internal/)
- Remove blue sky-500 header bar from SX docs layout (conditional on header-rows)
- Fix 405 on API endpoints from trailing slash redirect hitting POST/PUT/DELETE
- Fix client-side 404: orchestration.sx now swaps error response content
  instead of silently dropping it
- Add new plan files and home page component

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 21:30:18 +00:00

489 lines
28 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
;; Reactive Islands section — top-level section for the reactive islands system.
;; ---------------------------------------------------------------------------
;; Index / Overview
;; ---------------------------------------------------------------------------
(defcomp ~reactive-islands-index-content ()
(~doc-page :title "Reactive Islands"
(~doc-section :title "Architecture" :id "architecture"
(p "Two orthogonal bars control how an SX page works:")
(ul :class "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 :class "overflow-x-auto mt-4 mb-4"
(table :class "w-full text-sm text-left"
(thead
(tr :class "border-b border-stone-200"
(th :class "py-2 px-3 font-semibold text-stone-700" "")
(th :class "py-2 px-3 font-semibold text-stone-700" "Server State")
(th :class "py-2 px-3 font-semibold text-stone-700" "Client State")))
(tbody :class "text-stone-600"
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-semibold text-stone-700" "Server Rendering")
(td :class "py-2 px-3" "Pure hypermedia (htmx)")
(td :class "py-2 px-3" "SSR + hydrated islands"))
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-semibold text-stone-700" "Client Rendering")
(td :class "py-2 px-3" "SX wire format (current)")
(td :class "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."))
(~doc-section :title "Four Levels" :id "levels"
(div :class "space-y-4"
(div :class "rounded border border-stone-200 p-4"
(div :class "font-semibold text-stone-800" "Level 0: Pure Hypermedia")
(p :class "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 :class "rounded border border-stone-200 p-4"
(div :class "font-semibold text-stone-800" "Level 1: Local DOM Operations")
(p :class "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 :class "rounded border border-violet-300 bg-violet-50 p-4"
(div :class "font-semibold text-violet-900" "Level 2: Reactive Islands")
(p :class "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 :class "rounded border border-stone-200 p-4"
(div :class "font-semibold text-stone-800" "Level 3: Connected Islands")
(p :class "text-sm text-stone-600 mt-1"
"Islands that share state via signal props or named stores (" (code "def-store") " / " (code "use-store") ")."))))
(~doc-section :title "Signal Primitives" :id "signals"
(~doc-code :code (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."))
(~doc-section :title "Island Lifecycle" :id "lifecycle"
(ol :class "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"))))
(~doc-section :title "Implementation Status" :id "status"
(p :class "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 :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
(th :class "px-3 py-2 font-medium text-stone-600" "Layer")
(th :class "px-3 py-2 font-medium text-stone-600" "Status")
(th :class "px-3 py-2 font-medium text-stone-600" "Files")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Signal runtime spec")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "signals.sx (291 lines)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "defisland special form")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "eval.sx, special-forms.sx, render.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "DOM adapter (reactive rendering)")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx (+140 lines)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "HTML adapter (SSR)")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-html.sx (+65 lines)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "JS bootstrapper")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "bootstrap_js.py, sx-ref.js (4769 lines)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Python bootstrapper")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "bootstrap_py.py, sx_ref.py"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Test suite")
(td :class "px-3 py-2 text-green-700 font-medium" "17/17")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "test-signals.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Named stores (L3)")
(td :class "px-3 py-2 text-green-700 font-medium" "Spec'd")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "signals.sx: def-store, use-store, clear-stores"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Event bridge")
(td :class "px-3 py-2 text-green-700 font-medium" "Spec'd")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "signals.sx: emit-event, on-event, bridge-event"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Client hydration")
(td :class "px-3 py-2 text-green-700 font-medium" "Spec'd")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "boot.sx: sx-hydrate-islands, hydrate-island, dispose-island"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Event bindings")
(td :class "px-3 py-2 text-green-700 font-medium" "Spec'd")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx: :on-click → domListen"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "data-sx-emit processing")
(td :class "px-3 py-2 text-green-700 font-medium" "Spec'd")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "orchestration.sx: process-emit-elements"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Island disposal")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "boot.sx, orchestration.sx: dispose-islands-in pre-swap"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Reactive list")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx: map + deref auto-upgrades"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Input binding")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx: :bind signal, bind-input"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Keyed reconciliation")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx: :key attr, extract-key"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Portals")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx: portal render-dom form"))
(tr
(td :class "px-3 py-2 text-stone-700" "Phase 2 remaining")
(td :class "px-3 py-2 text-stone-500 font-medium" "P2")
(td :class "px-3 py-2 font-mono text-xs text-stone-500"
(a :href "/reactive/phase2" :sx-get "/reactive/phase2" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "Error boundaries + resource + patterns")))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Error boundaries")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx: error-boundary render-dom form"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Resource (async signal)")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "signals.sx: resource, promise-then"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Suspense pattern")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "resource + cond/deref (no special form)"))
(tr
(td :class "px-3 py-2 text-stone-700" "Transition pattern")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "schedule-idle + batch (no special form)"))))))))
;; ---------------------------------------------------------------------------
;; Live demo islands
;; ---------------------------------------------------------------------------
;; 1. Counter — basic signal + effect
(defisland ~demo-counter (&key initial)
(let ((count (signal (or initial 0)))
(doubled (computed (fn () (* 2 (deref count))))))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
(div :class "flex items-center gap-4"
(button :class "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 :class "text-2xl font-bold text-violet-900 w-12 text-center"
(deref count))
(button :class "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 :class "text-sm text-stone-500 mt-2"
"doubled: " (span :class "font-mono text-violet-700" (deref doubled))))))
;; 2. Temperature converter — computed derived signal
(defisland ~demo-temperature ()
(let ((celsius (signal 20))
(fahrenheit (computed (fn () (+ (* (deref celsius) 1.8) 32)))))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
(div :class "flex items-center gap-3"
(div :class "flex items-center gap-2"
(button :class "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 :class "font-mono text-lg font-bold text-violet-900 w-16 text-center"
(deref celsius))
(button :class "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 :class "text-stone-500" "°C"))
(span :class "text-stone-400" "=")
(span :class "font-mono text-lg font-bold text-violet-900"
(deref fahrenheit))
(span :class "text-stone-500" "°F")))))
;; 3. Imperative counter — shows create-text-node + effect pattern
(defisland ~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 :class "rounded border border-stone-200 bg-stone-50 p-4 my-4"
(p :class "text-sm text-stone-600 mb-2" "Imperative style — explicit " (code "effect") " + " (code "create-text-node") ":")
(div :class "flex items-center gap-4"
(button :class "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 :class "text-2xl font-bold text-stone-900 w-12 text-center"
text-node)
(button :class "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))
"+")))))
;; 4. Stopwatch — effect with cleanup (interval), fully imperative
(defisland ~demo-stopwatch ()
(let ((running (signal false))
(elapsed (signal 0))
(time-text (create-text-node "0.0s"))
(btn-text (create-text-node "Start"))
;; Timer effect — creates/clears interval based on running signal
(_e1 (effect (fn ()
(when (deref running)
(let ((id (set-interval (fn () (swap! elapsed inc)) 100)))
(fn () (clear-interval id)))))))
;; Display effect
(_e2 (effect (fn ()
(let ((e (deref elapsed)))
(dom-set-text-content time-text
(str (floor (/ e 10)) "." (mod e 10) "s"))))))
;; Button label effect
(_e3 (effect (fn ()
(dom-set-text-content btn-text
(if (deref running) "Stop" "Start"))))))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
(div :class "flex items-center gap-4"
(span :class "font-mono text-2xl font-bold text-violet-900 w-24 text-center"
time-text)
(button :class "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 :class "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")))))
;; 5. Reactive list — map over a signal, auto-updates when signal changes
(defisland ~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 :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
(div :class "flex items-center gap-3 mb-3"
(button :class "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 :class "text-sm text-stone-500"
(deref (computed (fn () (len (deref items))))) " items"))
(ul :class "space-y-1"
(map (fn (item)
(li :key (str (get item "id"))
:class "flex items-center justify-between bg-white rounded px-3 py-2 text-sm"
(span (get item "text"))
(button :class "text-stone-400 hover:text-red-500 text-xs"
:on-click (fn (e) (remove-item (get item "id")))
"✕")))
(deref items))))))
;; 6. Input binding — two-way signal binding for form elements
(defisland ~demo-input-binding ()
(let ((name (signal ""))
(agreed (signal false)))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4 space-y-3"
(div :class "flex items-center gap-3"
(input :type "text" :bind name
:placeholder "Type your name..."
:class "px-3 py-1.5 rounded border border-stone-300 text-sm focus:outline-none focus:border-violet-400 w-48")
(span :class "text-sm text-stone-600"
"Hello, "
(strong (deref name))
"!"))
(div :class "flex items-center gap-2"
(input :type "checkbox" :bind agreed :id "agree-cb"
:class "rounded border-stone-300")
(label :for "agree-cb" :class "text-sm text-stone-600" "I agree to the terms"))
(when (deref agreed)
(p :class "text-sm text-green-700" "Thanks for agreeing!")))))
;; 7. Portal — render into a remote DOM target
(defisland ~demo-portal ()
(let ((open? (signal false)))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
(button :class "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 :class "fixed inset-0 bg-black/50 flex items-center justify-center z-50"
:on-click (fn (e) (reset! open? false))
(div :class "bg-white rounded-lg p-6 max-w-md shadow-xl"
:on-click (fn (e) (stop-propagation e))
(h2 :class "text-lg font-bold text-stone-800 mb-2" "Portal Modal")
(p :class "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 :class "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"))))))))
;; 8. Error boundary — catch errors, render fallback with retry
(defisland ~demo-error-boundary ()
(let ((throw? (signal false)))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
(div :class "flex items-center gap-3 mb-3"
(button :class "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 :class "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
;; Fallback: receives (err retry-fn)
(fn (err retry-fn)
(div :class "p-3 bg-red-50 border border-red-200 rounded"
(p :class "text-red-700 font-medium text-sm" "Caught: " (error-message err))
(button :class "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")))
;; Children: the happy path
(do
(when (deref throw?)
(error "Intentional explosion!"))
(p :class "text-sm text-green-700"
"Everything is fine. Click \"Trigger Error\" to throw."))))))
;; 9. Refs — imperative DOM access via :ref attribute
(defisland ~demo-refs ()
(let ((my-ref (dict "current" nil))
(msg (signal "")))
(div :class "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"
:class "px-3 py-1.5 rounded border border-stone-300 text-sm focus:outline-none focus:border-violet-400 w-64")
(div :class "flex items-center gap-3"
(button :class "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 :class "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 :class "text-sm text-stone-600 font-mono" (deref msg))))))
;; 10. Dynamic class/style — computed signals drive class and style reactively
(defisland ~demo-dynamic-class ()
(let ((danger (signal false))
(size (signal 16)))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4 space-y-3"
(div :class "flex items-center gap-3"
(button :class "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 :class "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 :class "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."))))
;; 11. Resource + suspense pattern — async data with loading/error states
(defisland ~demo-resource ()
(let ((data (resource (fn ()
;; Simulate async fetch with a delayed promise
(promise-delayed 1500 (dict "name" "Ada Lovelace"
"role" "First Programmer"
"year" 1843))))))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
(cond
(get (deref data) "loading")
(div :class "flex items-center gap-2 text-stone-500"
(span :class "inline-block w-4 h-4 border-2 border-stone-300 border-t-violet-600 rounded-full animate-spin")
(span :class "text-sm" "Loading..."))
(get (deref data) "error")
(div :class "p-3 bg-red-50 border border-red-200 rounded"
(p :class "text-red-700 text-sm" "Error: " (get (deref data) "error")))
:else
(let ((d (get (deref data) "data")))
(div :class "space-y-1"
(p :class "font-bold text-stone-800" (get d "name"))
(p :class "text-sm text-stone-600" (get d "role") " (" (get d "year") ")")))))))
;; 12. Transition pattern — deferred updates for expensive operations
(defisland ~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)))
;; Set initial filtered list
(reset! filtered all-items)
;; Filter effect — defers via schedule-idle so typing stays snappy
(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 :class "rounded border border-violet-200 bg-violet-50 p-4 my-4 space-y-3"
(div :class "flex items-center gap-3"
(input :type "text" :bind query :placeholder "Filter features..."
:class "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 :class "text-xs text-stone-400" "Filtering...")))
(ul :class "space-y-1"
(map (fn (item)
(li :key item :class "text-sm text-stone-700 bg-white rounded px-3 py-1.5"
item))
(deref filtered))))))
;; 13. Shared stores — cross-island state via def-store / use-store
(defisland ~demo-store-writer ()
(let ((store (def-store "demo-theme" (fn ()
(dict "color" (signal "violet")
"dark" (signal false))))))
(div :class "rounded border border-stone-200 bg-stone-50 p-4 my-2"
(p :class "text-xs font-semibold text-stone-500 mb-2" "Island A — Store Writer")
(div :class "flex items-center gap-3"
(select :bind (get store "color")
:class "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 :class "flex items-center gap-1 text-sm text-stone-600"
(input :type "checkbox" :bind (get store "dark")
:class "rounded border-stone-300")
"Dark mode")))))
(defisland ~demo-store-reader ()
(let ((store (use-store "demo-theme")))
(div :class "rounded border border-stone-200 bg-stone-50 p-4 my-2"
(p :class "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"))))
;; ---------------------------------------------------------------------------
;; Demo page — shows what's been implemented
;; ---------------------------------------------------------------------------