diff --git a/hosts/ocaml/browser/sx_browser.ml b/hosts/ocaml/browser/sx_browser.ml index 329054c8..ff72784f 100644 --- a/hosts/ocaml/browser/sx_browser.ml +++ b/hosts/ocaml/browser/sx_browser.ml @@ -705,8 +705,10 @@ let () = bind "call-lambda" (fun args -> match args with | [f; a; _] | [f; a] when is_callable f -> - let arg_list = match a with List l -> l | Nil -> [] | v -> [v] in - Sx_ref.trampoline (Sx_runtime.sx_call f arg_list) + (* Use cek_call instead of sx_call to avoid eval_expr copying + Dict values (signals). sx_call returns a Thunk resolved via + eval_expr which deep-copies dicts, breaking signal mutation. *) + Sx_ref.cek_call f a | _ -> raise (Eval_error "call-lambda: expected (fn args env?)")); bind "cek-call" (fun args -> match args with diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index a04c771a..b68c334a 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -24,7 +24,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-31T08:20:17Z"; + var SX_VERSION = "2026-03-31T09:56:06Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } diff --git a/sx/sx/reactive-islands/index.sx b/sx/sx/reactive-islands/index.sx index 1cbfa009..0c483504 100644 --- a/sx/sx/reactive-islands/index.sx +++ b/sx/sx/reactive-islands/index.sx @@ -1,532 +1,975 @@ -;; Reactive Islands section — top-level section for the reactive islands system. - -;; --------------------------------------------------------------------------- -;; Index / Overview -;; --------------------------------------------------------------------------- - -(defcomp ~reactive-islands/index/reactive-islands-index-content () - (~docs/page :title "Reactive Islands" - - (~docs/section :title "Architecture" :id "architecture" +(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 :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" + (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" + (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") + (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") + (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.")) - - (~docs/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") ").")))) - - (~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 :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")))) - - (~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 :class "space-y-2 mt-3" - (div :class "rounded border border-green-200 bg-green-50 p-3" - (div :class "font-semibold text-green-800 text-sm" "Swap inside island") - (p :class "text-sm text-stone-600 mt-1" "Lake content replaced. Signals survive. Effects rebind to new DOM.")) - (div :class "rounded border border-green-200 bg-green-50 p-3" - (div :class "font-semibold text-green-800 text-sm" "Swap outside island") - (p :class "text-sm text-stone-600 mt-1" "Different part of page updated. Island completely unaffected.")) - (div :class "rounded border border-amber-200 bg-amber-50 p-3" - (div :class "font-semibold text-amber-800 text-sm" "Swap replaces island") - (p :class "text-sm text-stone-600 mt-1" "Island disposed. Local signals lost. Named stores persist — new island reconnects via " (code "use-store") ".")) - (div :class "rounded border border-stone-200 p-3" - (div :class "font-semibold text-stone-800 text-sm" "Full page navigation") - (p :class "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 "Design Principles" :id "principles" - (ol :class "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 :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"))) + (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.")) + (~docs/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") + ").")))) + (~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 + :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")))) + (~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 + :class "space-y-2 mt-3" + (div + :class "rounded border border-green-200 bg-green-50 p-3" + (div + :class "font-semibold text-green-800 text-sm" + "Swap inside island") + (p + :class "text-sm text-stone-600 mt-1" + "Lake content replaced. Signals survive. Effects rebind to new DOM.")) + (div + :class "rounded border border-green-200 bg-green-50 p-3" + (div + :class "font-semibold text-green-800 text-sm" + "Swap outside island") + (p + :class "text-sm text-stone-600 mt-1" + "Different part of page updated. Island completely unaffected.")) + (div + :class "rounded border border-amber-200 bg-amber-50 p-3" + (div + :class "font-semibold text-amber-800 text-sm" + "Swap replaces island") + (p + :class "text-sm text-stone-600 mt-1" + "Island disposed. Local signals lost. Named stores persist — new island reconnects via " + (code "use-store") + ".")) + (div + :class "rounded border border-stone-200 p-3" + (div + :class "font-semibold text-stone-800 text-sm" + "Full page navigation") + (p + :class "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 "Design Principles" + :id "principles" + (ol + :class "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 + :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" + (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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 :class "border-b border-stone-100" + (td + :class "px-3 py-2 font-mono text-xs text-stone-500" + "adapter-dom.sx: portal render-dom form")) + (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 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 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)")) + (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)")))))))) + (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 ~reactive-islands/index/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" +(defisland + ~reactive-islands/index/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" + (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" + (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)))))) + (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 ~reactive-islands/index/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" +(defisland + ~reactive-islands/index/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" + (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" + (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" + (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 ~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 :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" +(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 + :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" + (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" + (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 ~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")) - ;; 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" +(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 + :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" + (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))) + (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 ~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 :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" +(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 + :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")) + (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"))) - "✕"))) + (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 ~reactive-islands/index/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" +(defisland + ~reactive-islands/index/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) + (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 ~reactive-islands/index/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" +(defisland + ~reactive-islands/index/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" + (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" + (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" + (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 ~reactive-islands/index/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" +(defisland + ~reactive-islands/index/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" + (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" + (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" + (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 ~reactive-islands/index/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"))) +(defisland + ~reactive-islands/index/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") "\"")))) + (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) "")) + (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 ~reactive-islands/index/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" +(defisland + ~reactive-islands/index/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" + (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" + (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") + (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 ~reactive-islands/index/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" +(defisland + ~reactive-islands/index/demo-resource + () + (let + ((data (resource (fn () (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...")) + (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") ")"))))))) + (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 ~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))) - ;; Set initial filtered list +(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) - ;; Filter effect — defers via schedule-idle so typing stays snappy - (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 :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))))))) + (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 + :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 ~reactive-islands/index/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" +(defisland + ~reactive-islands/index/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") + (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 ~reactive-islands/index/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"))) +(defisland + ~reactive-islands/index/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")))) - -;; 14. Event bridge — lake→island communication via custom DOM events -(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))))))) +(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 :class "text-xs font-semibold text-stone-500 mb-2" "Event Bridge Demo") - (p :class "text-sm text-stone-600 mb-2" + (p + :class "text-xs font-semibold text-stone-500 mb-2" + "Event Bridge Demo") + (p + :class "text-sm text-stone-600 mb-2" "The buttons below simulate server-rendered content dispatching events into the island.") - (div :class "flex gap-2 mb-3" + (div + :class "flex gap-2 mb-3" (button :data-sx-emit "inbox:message" :data-sx-emit-detail "{\"text\":\"Hello from the lake!\"}" @@ -541,15 +984,15 @@ :on-click (fn (e) (reset! messages (list))) :class "px-3 py-1.5 bg-stone-200 text-stone-700 rounded text-sm hover:bg-stone-300" "Clear")) - (div :class "min-h-12 p-3 rounded bg-stone-100 border border-stone-200" - (if (empty? (deref messages)) + (div + :class "min-h-12 p-3 rounded bg-stone-100 border border-stone-200" + (if + (empty? (deref messages)) (p :class "text-stone-400 text-sm" "No messages yet.") - (ul :class "space-y-1" - (map (fn (msg) - (li :class "text-sm text-stone-700" (str "→ " msg))) + (ul + :class "space-y-1" + (map + (fn + (msg) + (li :class "text-sm text-stone-700" (str "→ " msg))) (deref messages)))))))) - - -;; --------------------------------------------------------------------------- -;; Examples page — shows what's been implemented -;; ---------------------------------------------------------------------------