- 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>
489 lines
28 KiB
Plaintext
489 lines
28 KiB
Plaintext
;; 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
|
||
;; ---------------------------------------------------------------------------
|