Fix empty code blocks: rename ~docs/code param, fix batched IO dispatch

Two bugs caused code blocks to render empty across the site:

1. ~docs/code component had parameter named `code` which collided with
   the HTML <code> tag name. Renamed to `src` and updated all 57
   callers. Added font-mono class for explicit monospace.

2. Batched IO dispatch in ocaml_bridge.py only skipped one leading
   number (batch ID) but the format has two (epoch + ID):
   (io-request EPOCH ID "name" args...). Changed to skip all leading
   numbers so the string name is correctly found. This fixes highlight
   and other batchable helpers returning empty results.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 18:08:40 +00:00
parent 739d04672b
commit 6f96452f70
58 changed files with 440 additions and 437 deletions

View File

@@ -27,84 +27,84 @@
(~docs/page :title "Signal + Computed + Effect"
(p "A signal holds a value. A computed derives from it. Click the buttons — the counter and doubled value update instantly, no server round-trip.")
(~reactive-islands/index/demo-counter :initial 0)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/counter (&key initial)\n (let ((count (signal (or initial 0)))\n (doubled (computed (fn () (* 2 (deref count))))))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! count dec)) \"\")\n (span (deref count))\n (button :on-click (fn (e) (swap! count inc)) \"+\")\n (p \"doubled: \" (deref doubled)))))" "lisp"))
(~docs/code :src (highlight "(defisland ~reactive-islands/demo/counter (&key initial)\n (let ((count (signal (or initial 0)))\n (doubled (computed (fn () (* 2 (deref count))))))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! count dec)) \"\")\n (span (deref count))\n (button :on-click (fn (e) (swap! count inc)) \"+\")\n (p \"doubled: \" (deref doubled)))))" "lisp"))
(p (code "(deref count)") " in a text position creates a reactive text node. When " (code "count") " changes, " (em "only that text node") " updates. " (code "doubled") " recomputes automatically. No diffing.")))
(defcomp ~reactive-islands/demo/example-temperature ()
(~docs/page :title "Temperature Converter"
(p "Two derived values from one signal. Click to change Celsius — Fahrenheit updates reactively.")
(~reactive-islands/index/demo-temperature)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/temperature ()\n (let ((celsius (signal 20)))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! celsius (fn (c) (- c 5)))) \"5\")\n (span (deref celsius))\n (button :on-click (fn (e) (swap! celsius (fn (c) (+ c 5)))) \"+5\")\n (span \"°C = \")\n (span (+ (* (deref celsius) 1.8) 32))\n (span \"°F\"))))" "lisp"))
(~docs/code :src (highlight "(defisland ~reactive-islands/demo/temperature ()\n (let ((celsius (signal 20)))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! celsius (fn (c) (- c 5)))) \"5\")\n (span (deref celsius))\n (button :on-click (fn (e) (swap! celsius (fn (c) (+ c 5)))) \"+5\")\n (span \"°C = \")\n (span (+ (* (deref celsius) 1.8) 32))\n (span \"°F\"))))" "lisp"))
(p "The actual implementation uses " (code "computed") " for Fahrenheit: " (code "(computed (fn () (+ (* (deref celsius) 1.8) 32)))") ". The " (code "(deref fahrenheit)") " in the span creates a reactive text node that updates when celsius changes.")))
(defcomp ~reactive-islands/demo/example-stopwatch ()
(~docs/page :title "Effect + Cleanup: Stopwatch"
(p "Effects can return cleanup functions. This stopwatch starts a " (code "set-interval") " — the cleanup clears it when the running signal toggles off.")
(~reactive-islands/index/demo-stopwatch)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/stopwatch ()\n (let ((running (signal false))\n (elapsed (signal 0))\n (time-text (create-text-node \"0.0s\"))\n (btn-text (create-text-node \"Start\")))\n ;; Timer: effect creates interval, cleanup clears it\n (effect (fn ()\n (when (deref running)\n (let ((id (set-interval (fn () (swap! elapsed inc)) 100)))\n (fn () (clear-interval id))))))\n ;; Display: updates text node when elapsed changes\n (effect (fn ()\n (let ((e (deref elapsed)))\n (dom-set-text-content time-text\n (str (floor (/ e 10)) \".\" (mod e 10) \"s\")))))\n ;; Button label\n (effect (fn ()\n (dom-set-text-content btn-text\n (if (deref running) \"Stop\" \"Start\"))))\n (div :class \"...\"\n (span time-text)\n (button :on-click (fn (e) (swap! running not)) btn-text)\n (button :on-click (fn (e)\n (reset! running false) (reset! elapsed 0)) \"Reset\"))))" "lisp"))
(~docs/code :src (highlight "(defisland ~reactive-islands/demo/stopwatch ()\n (let ((running (signal false))\n (elapsed (signal 0))\n (time-text (create-text-node \"0.0s\"))\n (btn-text (create-text-node \"Start\")))\n ;; Timer: effect creates interval, cleanup clears it\n (effect (fn ()\n (when (deref running)\n (let ((id (set-interval (fn () (swap! elapsed inc)) 100)))\n (fn () (clear-interval id))))))\n ;; Display: updates text node when elapsed changes\n (effect (fn ()\n (let ((e (deref elapsed)))\n (dom-set-text-content time-text\n (str (floor (/ e 10)) \".\" (mod e 10) \"s\")))))\n ;; Button label\n (effect (fn ()\n (dom-set-text-content btn-text\n (if (deref running) \"Stop\" \"Start\"))))\n (div :class \"...\"\n (span time-text)\n (button :on-click (fn (e) (swap! running not)) btn-text)\n (button :on-click (fn (e)\n (reset! running false) (reset! elapsed 0)) \"Reset\"))))" "lisp"))
(p "Three effects, each tracking different signals. The timer effect's cleanup fires before each re-run — toggling " (code "running") " off clears the interval. No hook rules: effects can appear anywhere, in any order.")))
(defcomp ~reactive-islands/demo/example-imperative ()
(~docs/page :title "Imperative Pattern"
(p "For complex reactivity (dynamic classes, conditional text), use the imperative pattern: " (code "create-text-node") " + " (code "effect") " + " (code "dom-set-text-content") ".")
(~reactive-islands/index/demo-imperative)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/imperative ()\n (let ((count (signal 0))\n (text-node (create-text-node \"0\")))\n ;; Explicit effect: re-runs when count changes\n (effect (fn ()\n (dom-set-text-content text-node (str (deref count)))))\n (div :class \"...\"\n (span text-node)\n (button :on-click (fn (e) (swap! count inc)) \"+\"))))" "lisp"))
(~docs/code :src (highlight "(defisland ~reactive-islands/demo/imperative ()\n (let ((count (signal 0))\n (text-node (create-text-node \"0\")))\n ;; Explicit effect: re-runs when count changes\n (effect (fn ()\n (dom-set-text-content text-node (str (deref count)))))\n (div :class \"...\"\n (span text-node)\n (button :on-click (fn (e) (swap! count inc)) \"+\"))))" "lisp"))
(p "Two patterns exist: " (strong "declarative") " (" (code "(span (deref sig))") " — auto-reactive via " (code "reactive-text") ") and " (strong "imperative") " (" (code "create-text-node") " + " (code "effect") " — explicit, full control). Use declarative for simple text, imperative for dynamic classes, conditional DOM, or complex updates.")))
(defcomp ~reactive-islands/demo/example-reactive-list ()
(~docs/page :title "Reactive List"
(p "When " (code "map") " is used with " (code "(deref signal)") " inside an island, it auto-upgrades to a reactive list. With " (code ":key") " attributes, existing DOM nodes are reused across updates — only additions, removals, and reorderings touch the DOM.")
(~reactive-islands/index/demo-reactive-list)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/reactive-list ()\n (let ((next-id (signal 1))\n (items (signal (list)))\n (add-item (fn (e)\n (batch (fn ()\n (swap! items (fn (old)\n (append old (dict \"id\" (deref next-id)\n \"text\" (str \"Item \" (deref next-id))))))\n (swap! next-id inc)))))\n (remove-item (fn (id)\n (swap! items (fn (old)\n (filter (fn (item) (not (= (get item \"id\") id))) old))))))\n (div\n (button :on-click add-item \"Add Item\")\n (span (deref (computed (fn () (len (deref items))))) \" items\")\n (ul\n (map (fn (item)\n (li :key (str (get item \"id\"))\n (span (get item \"text\"))\n (button :on-click (fn (e) (remove-item (get item \"id\"))) \"✕\")))\n (deref items))))))" "lisp"))
(~docs/code :src (highlight "(defisland ~reactive-islands/demo/reactive-list ()\n (let ((next-id (signal 1))\n (items (signal (list)))\n (add-item (fn (e)\n (batch (fn ()\n (swap! items (fn (old)\n (append old (dict \"id\" (deref next-id)\n \"text\" (str \"Item \" (deref next-id))))))\n (swap! next-id inc)))))\n (remove-item (fn (id)\n (swap! items (fn (old)\n (filter (fn (item) (not (= (get item \"id\") id))) old))))))\n (div\n (button :on-click add-item \"Add Item\")\n (span (deref (computed (fn () (len (deref items))))) \" items\")\n (ul\n (map (fn (item)\n (li :key (str (get item \"id\"))\n (span (get item \"text\"))\n (button :on-click (fn (e) (remove-item (get item \"id\"))) \"✕\")))\n (deref items))))))" "lisp"))
(p (code ":key") " identifies each list item. When items change, the reconciler matches old and new keys — reusing existing DOM nodes, inserting new ones, and removing stale ones. Without keys, the list falls back to clear-and-rerender. " (code "batch") " groups the two signal writes into one update pass.")))
(defcomp ~reactive-islands/demo/example-input-binding ()
(~docs/page :title "Input Binding"
(p "The " (code ":bind") " attribute creates a two-way link between a signal and a form element. Type in the input — the signal updates. Change the signal — the input updates. Works with text inputs, checkboxes, radios, textareas, and selects.")
(~reactive-islands/index/demo-input-binding)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/input-binding ()\n (let ((name (signal \"\"))\n (agreed (signal false)))\n (div\n (input :type \"text\" :bind name\n :placeholder \"Type your name...\")\n (span \"Hello, \" (strong (deref name)) \"!\")\n (input :type \"checkbox\" :bind agreed)\n (when (deref agreed)\n (p \"Thanks for agreeing!\")))))" "lisp"))
(~docs/code :src (highlight "(defisland ~reactive-islands/demo/input-binding ()\n (let ((name (signal \"\"))\n (agreed (signal false)))\n (div\n (input :type \"text\" :bind name\n :placeholder \"Type your name...\")\n (span \"Hello, \" (strong (deref name)) \"!\")\n (input :type \"checkbox\" :bind agreed)\n (when (deref agreed)\n (p \"Thanks for agreeing!\")))))" "lisp"))
(p (code ":bind") " detects the element type automatically — text inputs use " (code "value") " + " (code "input") " event, checkboxes use " (code "checked") " + " (code "change") " event. The effect only updates the DOM when the value actually changed, preventing cursor jump.")))
(defcomp ~reactive-islands/demo/example-portal ()
(~docs/page :title "Portals"
(p "A " (code "portal") " renders children into a DOM node " (em "outside") " the island's subtree. Essential for modals, tooltips, and toasts — anything that must escape " (code "overflow:hidden") " or z-index stacking.")
(~reactive-islands/index/demo-portal)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/portal ()\n (let ((open? (signal false)))\n (div\n (button :on-click (fn (e) (swap! open? not))\n (if (deref open?) \"Close Modal\" \"Open Modal\"))\n (portal \"#portal-root\"\n (when (deref open?)\n (div :class \"fixed inset-0 bg-black/50 ...\"\n :on-click (fn (e) (reset! open? false))\n (div :class \"bg-white rounded-lg p-6 ...\"\n :on-click (fn (e) (stop-propagation e))\n (h2 \"Portal Modal\")\n (p \"Rendered outside the island's DOM.\")\n (button :on-click (fn (e) (reset! open? false))\n \"Close\"))))))))" "lisp"))
(~docs/code :src (highlight "(defisland ~reactive-islands/demo/portal ()\n (let ((open? (signal false)))\n (div\n (button :on-click (fn (e) (swap! open? not))\n (if (deref open?) \"Close Modal\" \"Open Modal\"))\n (portal \"#portal-root\"\n (when (deref open?)\n (div :class \"fixed inset-0 bg-black/50 ...\"\n :on-click (fn (e) (reset! open? false))\n (div :class \"bg-white rounded-lg p-6 ...\"\n :on-click (fn (e) (stop-propagation e))\n (h2 \"Portal Modal\")\n (p \"Rendered outside the island's DOM.\")\n (button :on-click (fn (e) (reset! open? false))\n \"Close\"))))))))" "lisp"))
(p "The portal content lives in " (code "#portal-root") " (typically at the page body level), not inside the island. On island disposal, portal content is automatically removed from its target — the " (code "register-in-scope") " mechanism handles cleanup.")))
(defcomp ~reactive-islands/demo/example-error-boundary ()
(~docs/page :title "Error Boundaries"
(p "When an island's rendering or effect throws, " (code "error-boundary") " catches the error and renders a fallback. The fallback receives the error and a retry function. Partial effects created before the error are disposed automatically.")
(~reactive-islands/index/demo-error-boundary)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/error-boundary ()\n (let ((throw? (signal false)))\n (error-boundary\n ;; Fallback: receives (err retry-fn)\n (fn (err retry-fn)\n (div :class \"p-3 bg-red-50 border border-red-200 rounded\"\n (p :class \"text-red-700\" (error-message err))\n (button :on-click (fn (e)\n (reset! throw? false) (invoke retry-fn))\n \"Retry\")))\n ;; Children: the happy path\n (do\n (when (deref throw?) (error \"Intentional explosion!\"))\n (p \"Everything is fine.\")))))" "lisp"))
(~docs/code :src (highlight "(defisland ~reactive-islands/demo/error-boundary ()\n (let ((throw? (signal false)))\n (error-boundary\n ;; Fallback: receives (err retry-fn)\n (fn (err retry-fn)\n (div :class \"p-3 bg-red-50 border border-red-200 rounded\"\n (p :class \"text-red-700\" (error-message err))\n (button :on-click (fn (e)\n (reset! throw? false) (invoke retry-fn))\n \"Retry\")))\n ;; Children: the happy path\n (do\n (when (deref throw?) (error \"Intentional explosion!\"))\n (p \"Everything is fine.\")))))" "lisp"))
(p "React equivalent: " (code "componentDidCatch") " / " (code "ErrorBoundary") ". SX's version is simpler — one form, not a class. The " (code "error-boundary") " form is a render-dom special form in " (code "adapter-dom.sx") ".")))
(defcomp ~reactive-islands/demo/example-refs ()
(~docs/page :title "Refs — Imperative DOM Access"
(p "The " (code ":ref") " attribute captures a DOM element handle into a dict. Use it for imperative operations: focusing, measuring, reading values.")
(~reactive-islands/index/demo-refs)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/refs ()\n (let ((my-ref (dict \"current\" nil))\n (msg (signal \"\")))\n (input :ref my-ref :type \"text\"\n :placeholder \"I can be focused programmatically\")\n (button :on-click (fn (e)\n (dom-focus (get my-ref \"current\")))\n \"Focus Input\")\n (button :on-click (fn (e)\n (let ((el (get my-ref \"current\")))\n (reset! msg (str \"value: \" (dom-get-prop el \"value\")))))\n \"Read Input\")\n (when (not (= (deref msg) \"\"))\n (p (deref msg)))))" "lisp"))
(~docs/code :src (highlight "(defisland ~reactive-islands/demo/refs ()\n (let ((my-ref (dict \"current\" nil))\n (msg (signal \"\")))\n (input :ref my-ref :type \"text\"\n :placeholder \"I can be focused programmatically\")\n (button :on-click (fn (e)\n (dom-focus (get my-ref \"current\")))\n \"Focus Input\")\n (button :on-click (fn (e)\n (let ((el (get my-ref \"current\")))\n (reset! msg (str \"value: \" (dom-get-prop el \"value\")))))\n \"Read Input\")\n (when (not (= (deref msg) \"\"))\n (p (deref msg)))))" "lisp"))
(p "React equivalent: " (code "useRef") ". In SX, a ref is just " (code "(dict \"current\" nil)") " — no special API. The " (code ":ref") " attribute sets " (code "(dict-set! ref \"current\" el)") " when the element is created. Read it with " (code "(get ref \"current\")") ".")))
(defcomp ~reactive-islands/demo/example-dynamic-class ()
(~docs/page :title "Dynamic Class and Style"
(p "React uses " (code "className") " and " (code "style") " props with state. SX does the same — " (code "(deref signal)") " inside a " (code ":class") " or " (code ":style") " attribute creates a reactive binding. The attribute updates when the signal changes.")
(~reactive-islands/index/demo-dynamic-class)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/dynamic-class ()\n (let ((danger (signal false))\n (size (signal 16)))\n (div\n (button :on-click (fn (e) (swap! danger not))\n (if (deref danger) \"Safe mode\" \"Danger mode\"))\n (button :on-click (fn (e) (swap! size (fn (s) (+ s 2))))\n \"Bigger\")\n ;; Reactive class — recomputed when danger changes\n (div :class (str \"p-3 rounded font-medium \"\n (if (deref danger)\n \"bg-red-100 text-red-800\"\n \"bg-green-100 text-green-800\"))\n ;; Reactive style — recomputed when size changes\n :style (str \"font-size:\" (deref size) \"px\")\n \"This element's class and style are reactive.\"))))" "lisp"))
(~docs/code :src (highlight "(defisland ~reactive-islands/demo/dynamic-class ()\n (let ((danger (signal false))\n (size (signal 16)))\n (div\n (button :on-click (fn (e) (swap! danger not))\n (if (deref danger) \"Safe mode\" \"Danger mode\"))\n (button :on-click (fn (e) (swap! size (fn (s) (+ s 2))))\n \"Bigger\")\n ;; Reactive class — recomputed when danger changes\n (div :class (str \"p-3 rounded font-medium \"\n (if (deref danger)\n \"bg-red-100 text-red-800\"\n \"bg-green-100 text-green-800\"))\n ;; Reactive style — recomputed when size changes\n :style (str \"font-size:\" (deref size) \"px\")\n \"This element's class and style are reactive.\"))))" "lisp"))
(p "React equivalent: " (code "className={danger ? 'red' : 'green'}") " and " (code "style={{fontSize: size}}") ". In SX the " (code "str") " + " (code "if") " + " (code "deref") " pattern handles it — no " (code "classnames") " library needed. For complex conditional classes, use a " (code "computed") " or a CSSX " (code "defcomp") " that returns a class string.")))
(defcomp ~reactive-islands/demo/example-resource ()
(~docs/page :title "Resource + Suspense Pattern"
(p (code "resource") " wraps an async operation into a signal with " (code "loading") "/" (code "data") "/" (code "error") " states. Combined with " (code "cond") " + " (code "deref") ", this is the suspense pattern — no special form needed.")
(~reactive-islands/index/demo-resource)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/resource ()\n (let ((data (resource (fn ()\n ;; Any promise-returning function\n (promise-delayed 1500\n (dict \"name\" \"Ada Lovelace\"\n \"role\" \"First Programmer\"))))))\n ;; This IS the suspense pattern:\n (let ((state (deref data)))\n (cond\n (get state \"loading\")\n (div \"Loading...\")\n (get state \"error\")\n (div \"Error: \" (get state \"error\"))\n :else\n (div (get (get state \"data\") \"name\"))))))" "lisp"))
(~docs/code :src (highlight "(defisland ~reactive-islands/demo/resource ()\n (let ((data (resource (fn ()\n ;; Any promise-returning function\n (promise-delayed 1500\n (dict \"name\" \"Ada Lovelace\"\n \"role\" \"First Programmer\"))))))\n ;; This IS the suspense pattern:\n (let ((state (deref data)))\n (cond\n (get state \"loading\")\n (div \"Loading...\")\n (get state \"error\")\n (div \"Error: \" (get state \"error\"))\n :else\n (div (get (get state \"data\") \"name\"))))))" "lisp"))
(p "React equivalent: " (code "Suspense") " + " (code "use()") " or " (code "useSWR") ". SX doesn't need a special " (code "suspense") " form because " (code "resource") " returns a signal and " (code "cond") " + " (code "deref") " creates reactive conditional rendering. When the promise resolves, the signal updates and the " (code "cond") " branch switches automatically.")))
(defcomp ~reactive-islands/demo/example-transition ()
(~docs/page :title "Transition Pattern"
(p "React's " (code "startTransition") " defers non-urgent updates so typing stays responsive. In SX: " (code "schedule-idle") " + " (code "batch") ". The filter runs during idle time, not blocking the input event.")
(~reactive-islands/index/demo-transition)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/transition ()\n (let ((query (signal \"\"))\n (all-items (list \"Signals\" \"Effects\" ...))\n (filtered (signal (list)))\n (pending (signal false)))\n (reset! filtered all-items)\n ;; Filter effect — deferred via schedule-idle\n (effect (fn ()\n (let ((q (lower (deref query))))\n (if (= q \"\")\n (do (reset! pending false)\n (reset! filtered all-items))\n (do (reset! pending true)\n (schedule-idle (fn ()\n (batch (fn ()\n (reset! filtered\n (filter (fn (item)\n (contains? (lower item) q))\n all-items))\n (reset! pending false))))))))))\n (div\n (input :bind query :placeholder \"Filter...\")\n (when (deref pending) (span \"Filtering...\"))\n (ul (map (fn (item) (li :key item item))\n (deref filtered))))))" "lisp"))
(~docs/code :src (highlight "(defisland ~reactive-islands/demo/transition ()\n (let ((query (signal \"\"))\n (all-items (list \"Signals\" \"Effects\" ...))\n (filtered (signal (list)))\n (pending (signal false)))\n (reset! filtered all-items)\n ;; Filter effect — deferred via schedule-idle\n (effect (fn ()\n (let ((q (lower (deref query))))\n (if (= q \"\")\n (do (reset! pending false)\n (reset! filtered all-items))\n (do (reset! pending true)\n (schedule-idle (fn ()\n (batch (fn ()\n (reset! filtered\n (filter (fn (item)\n (contains? (lower item) q))\n all-items))\n (reset! pending false))))))))))\n (div\n (input :bind query :placeholder \"Filter...\")\n (when (deref pending) (span \"Filtering...\"))\n (ul (map (fn (item) (li :key item item))\n (deref filtered))))))" "lisp"))
(p "React equivalent: " (code "startTransition(() => setFiltered(...))") ". SX uses " (code "schedule-idle") " (" (code "requestIdleCallback") " under the hood) to defer the expensive " (code "filter") " operation, and " (code "batch") " to group the result into one update. Fine-grained signals already avoid the jank that makes transitions critical in React — this pattern is for truly expensive computations.")))
(defcomp ~reactive-islands/demo/example-stores ()
@@ -112,26 +112,26 @@
(p "React uses " (code "Context") " or state management libraries for cross-component state. SX uses " (code "def-store") " / " (code "use-store") " — named signal containers that persist across island creation/destruction.")
(~reactive-islands/index/demo-store-writer)
(~reactive-islands/index/demo-store-reader)
(~docs/code :code (highlight ";; Island A — creates/writes the store\n(defisland ~reactive-islands/demo/store-writer ()\n (let ((store (def-store \"theme\" (fn ()\n (dict \"color\" (signal \"violet\")\n \"dark\" (signal false))))))\n (select :bind (get store \"color\")\n (option :value \"violet\" \"Violet\")\n (option :value \"blue\" \"Blue\"))\n (input :type \"checkbox\" :bind (get store \"dark\"))))\n\n;; Island B — reads the same store, different island\n(defisland ~reactive-islands/demo/store-reader ()\n (let ((store (use-store \"theme\")))\n (div :class (str \"bg-\" (deref (get store \"color\")) \"-100\")\n \"Styled by signals from Island A\")))" "lisp"))
(~docs/code :src (highlight ";; Island A — creates/writes the store\n(defisland ~reactive-islands/demo/store-writer ()\n (let ((store (def-store \"theme\" (fn ()\n (dict \"color\" (signal \"violet\")\n \"dark\" (signal false))))))\n (select :bind (get store \"color\")\n (option :value \"violet\" \"Violet\")\n (option :value \"blue\" \"Blue\"))\n (input :type \"checkbox\" :bind (get store \"dark\"))))\n\n;; Island B — reads the same store, different island\n(defisland ~reactive-islands/demo/store-reader ()\n (let ((store (use-store \"theme\")))\n (div :class (str \"bg-\" (deref (get store \"color\")) \"-100\")\n \"Styled by signals from Island A\")))" "lisp"))
(p "React equivalent: " (code "createContext") " + " (code "useContext") " or Redux/Zustand. Stores are simpler — just named dicts of signals at page scope. " (code "def-store") " creates once, " (code "use-store") " retrieves. Stores survive island disposal but clear on full page navigation.")))
(defcomp ~reactive-islands/demo/example-event-bridge-demo ()
(~docs/page :title "Event Bridge"
(p "Server-rendered content inside an island (an htmx \"lake\") can communicate with island signals via DOM custom events. Buttons with " (code "data-sx-emit") " dispatch events that island effects catch.")
(~reactive-islands/index/demo-event-bridge)
(~docs/code :code (highlight ";; Island listens for custom events from server-rendered content\n(defisland ~reactive-islands/demo/event-bridge ()\n (let ((messages (signal (list))))\n ;; Bridge: auto-listen for \"inbox:message\" events\n (bridge-event container \"inbox:message\" messages\n (fn (detail) (append (deref messages) (get detail \"text\"))))\n (div\n ;; Lake content (server-rendered) has data-sx-emit buttons\n (div :id \"lake\"\n :sx-get \"/my-content\"\n :sx-swap \"innerHTML\"\n :sx-trigger \"load\")\n ;; Island reads the signal reactively\n (ul (map (fn (msg) (li msg)) (deref messages))))))" "lisp"))
(~docs/code :src (highlight ";; Island listens for custom events from server-rendered content\n(defisland ~reactive-islands/demo/event-bridge ()\n (let ((messages (signal (list))))\n ;; Bridge: auto-listen for \"inbox:message\" events\n (bridge-event container \"inbox:message\" messages\n (fn (detail) (append (deref messages) (get detail \"text\"))))\n (div\n ;; Lake content (server-rendered) has data-sx-emit buttons\n (div :id \"lake\"\n :sx-get \"/my-content\"\n :sx-swap \"innerHTML\"\n :sx-trigger \"load\")\n ;; Island reads the signal reactively\n (ul (map (fn (msg) (li msg)) (deref messages))))))" "lisp"))
(p "The " (code "data-sx-emit") " attribute is processed by the client engine — it adds a click handler that dispatches a CustomEvent with the JSON from " (code "data-sx-emit-detail") ". The event bubbles up to the island container where " (code "bridge-event") " catches it.")))
(defcomp ~reactive-islands/demo/example-defisland ()
(~docs/page :title "How defisland Works"
(p (code "defisland") " creates a reactive component. Same calling convention as " (code "defcomp") " — keyword args, rest children — but with a reactive boundary. Inside an island, " (code "deref") " subscribes DOM nodes to signals.")
(~docs/code :code (highlight ";; Definition — same syntax as defcomp\n(defisland ~reactive-islands/demo/counter (&key initial)\n (let ((count (signal (or initial 0))))\n (div\n (span (deref count)) ;; reactive text node\n (button :on-click (fn (e) (swap! count inc)) ;; event handler\n \"+\"))))\n\n;; Usage — same as any component\n(~reactive-islands/demo/counter :initial 42)\n\n;; Server-side rendering:\n;; <div data-sx-island=\"counter\" data-sx-state='{\"initial\":42}'>\n;; <span>42</span><button>+</button>\n;; </div>\n;;\n;; Client hydrates: signals + effects + event handlers attach" "lisp"))
(~docs/code :src (highlight ";; Definition — same syntax as defcomp\n(defisland ~reactive-islands/demo/counter (&key initial)\n (let ((count (signal (or initial 0))))\n (div\n (span (deref count)) ;; reactive text node\n (button :on-click (fn (e) (swap! count inc)) ;; event handler\n \"+\"))))\n\n;; Usage — same as any component\n(~reactive-islands/demo/counter :initial 42)\n\n;; Server-side rendering:\n;; <div data-sx-island=\"counter\" data-sx-state='{\"initial\":42}'>\n;; <span>42</span><button>+</button>\n;; </div>\n;;\n;; Client hydrates: signals + effects + event handlers attach" "lisp"))
(p "Each " (code "deref") " call registers the enclosing DOM node as a subscriber. Signal changes update " (em "only") " the subscribed nodes — no virtual DOM, no diffing, no component re-renders.")))
(defcomp ~reactive-islands/demo/example-tests ()
(~docs/page :title "Test Suite"
(p "17 tests verify the signal runtime against the spec. All pass in the Python test runner (which uses the hand-written evaluator with native platform primitives).")
(~docs/code :code (highlight ";; Signal basics (6 tests)\n(assert-true (signal? (signal 42)))\n(assert-equal 42 (deref (signal 42)))\n(assert-equal 5 (deref 5)) ;; non-signal passthrough\n\n;; reset! changes value\n(let ((s (signal 0)))\n (reset! s 10)\n (assert-equal 10 (deref s)))\n\n;; reset! does NOT notify when value unchanged (identical? check)\n\n;; Computed (3 tests)\n(let ((a (signal 3)) (b (signal 4))\n (sum (computed (fn () (+ (deref a) (deref b))))))\n (assert-equal 7 (deref sum))\n (reset! a 10)\n (assert-equal 14 (deref sum)))\n\n;; Effects (4 tests) — immediate run, re-run on change, dispose, cleanup\n;; Batch (1 test) — defers notifications, deduplicates subscribers\n;; defisland (3 tests) — creates island, callable, accepts children" "lisp"))
(~docs/code :src (highlight ";; Signal basics (6 tests)\n(assert-true (signal? (signal 42)))\n(assert-equal 42 (deref (signal 42)))\n(assert-equal 5 (deref 5)) ;; non-signal passthrough\n\n;; reset! changes value\n(let ((s (signal 0)))\n (reset! s 10)\n (assert-equal 10 (deref s)))\n\n;; reset! does NOT notify when value unchanged (identical? check)\n\n;; Computed (3 tests)\n(let ((a (signal 3)) (b (signal 4))\n (sum (computed (fn () (+ (deref a) (deref b))))))\n (assert-equal 7 (deref sum))\n (reset! a 10)\n (assert-equal 14 (deref sum)))\n\n;; Effects (4 tests) — immediate run, re-run on change, dispose, cleanup\n;; Batch (1 test) — defers notifications, deduplicates subscribers\n;; defisland (3 tests) — creates island, callable, accepts children" "lisp"))
(p :class "mt-2 text-sm text-stone-500" "Run: " (code "python3 shared/sx/tests/run.py signals"))))
(defcomp ~reactive-islands/demo/example-coverage ()

View File

@@ -17,10 +17,10 @@
(li (strong "Event bubbles: ") "The event bubbles up through the DOM tree until it reaches the island container.")
(li (strong "Effect catches: ") "An effect inside the island listens for the event name and updates a signal."))
(~docs/code :code (highlight ";; Island with an event bridge\n(defisland ~reactive-islands/event-bridge/product-page (&key product)\n (let ((cart-items (signal (list))))\n\n ;; Bridge: listen for \"cart:add\" events from server content\n (bridge-event container \"cart:add\" cart-items\n (fn (detail)\n (append (deref cart-items)\n (dict :id (get detail \"id\")\n :name (get detail \"name\")\n :price (get detail \"price\")))))\n\n (div\n ;; Island header with reactive cart count\n (div :class \"flex justify-between\"\n (h1 (get product \"name\"))\n (span :class \"badge\" (length (deref cart-items)) \" items\"))\n\n ;; htmx lake — server-rendered product details\n ;; This content is swapped by sx-get, not rendered by the island\n (div :id \"product-details\"\n :sx-get (str \"/products/\" (get product \"id\") \"/details\")\n :sx-swap \"innerHTML\"\n :sx-trigger \"load\"))))" "lisp"))
(~docs/code :src (highlight ";; Island with an event bridge\n(defisland ~reactive-islands/event-bridge/product-page (&key product)\n (let ((cart-items (signal (list))))\n\n ;; Bridge: listen for \"cart:add\" events from server content\n (bridge-event container \"cart:add\" cart-items\n (fn (detail)\n (append (deref cart-items)\n (dict :id (get detail \"id\")\n :name (get detail \"name\")\n :price (get detail \"price\")))))\n\n (div\n ;; Island header with reactive cart count\n (div :class \"flex justify-between\"\n (h1 (get product \"name\"))\n (span :class \"badge\" (length (deref cart-items)) \" items\"))\n\n ;; htmx lake — server-rendered product details\n ;; This content is swapped by sx-get, not rendered by the island\n (div :id \"product-details\"\n :sx-get (str \"/products/\" (get product \"id\") \"/details\")\n :sx-swap \"innerHTML\"\n :sx-trigger \"load\"))))" "lisp"))
(p "The server handler for " (code "/products/:id/details") " returns HTML with emit attributes:")
(~docs/code :code (highlight ";; Server-rendered response (pure HTML, no signals)\n(div\n (p (get product \"description\"))\n (div :class \"flex gap-2 mt-4\"\n (button\n :data-sx-emit \"cart:add\"\n :data-sx-emit-detail (json-serialize\n (dict :id (get product \"id\")\n :name (get product \"name\")\n :price (get product \"price\")))\n :class \"bg-violet-600 text-white px-4 py-2 rounded\"\n \"Add to Cart\")))" "lisp"))
(~docs/code :src (highlight ";; Server-rendered response (pure HTML, no signals)\n(div\n (p (get product \"description\"))\n (div :class \"flex gap-2 mt-4\"\n (button\n :data-sx-emit \"cart:add\"\n :data-sx-emit-detail (json-serialize\n (dict :id (get product \"id\")\n :name (get product \"name\")\n :price (get product \"price\")))\n :class \"bg-violet-600 text-white px-4 py-2 rounded\"\n \"Add to Cart\")))" "lisp"))
(p "The button is plain server HTML. When clicked, the client's event bridge dispatches " (code "cart:add") " with the JSON detail. The island effect catches it and appends to " (code "cart-items") ". The badge updates reactively."))
(~docs/section :title "Why signals survive swaps" :id "survival"
@@ -33,7 +33,7 @@
(~docs/section :title "Spec" :id "spec"
(p "The event bridge is spec'd in " (code "signals.sx") " (sections 12-13). Three functions:")
(~docs/code :code (highlight ";; Low-level: dispatch a custom event\n(emit-event el \"cart:add\" {:id 42 :name \"Widget\"})\n\n;; Low-level: listen for a custom event\n(on-event container \"cart:add\" (fn (e)\n (swap! items (fn (old) (append old (event-detail e))))))\n\n;; High-level: bridge an event directly to a signal\n;; Creates an effect with automatic cleanup on dispose\n(bridge-event container \"cart:add\" items\n (fn (detail) (append (deref items) detail)))" "lisp"))
(~docs/code :src (highlight ";; Low-level: dispatch a custom event\n(emit-event el \"cart:add\" {:id 42 :name \"Widget\"})\n\n;; Low-level: listen for a custom event\n(on-event container \"cart:add\" (fn (e)\n (swap! items (fn (old) (append old (event-detail e))))))\n\n;; High-level: bridge an event directly to a signal\n;; Creates an effect with automatic cleanup on dispose\n(bridge-event container \"cart:add\" items\n (fn (detail) (append (deref items) detail)))" "lisp"))
(p "Platform interface required:")
(div :class "overflow-x-auto rounded border border-stone-200 mt-2"

View File

@@ -52,7 +52,7 @@
"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 :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"))
(~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"
@@ -81,12 +81,12 @@
(~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 :code (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"))
(~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 :code (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"))
(~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"

View File

@@ -37,11 +37,11 @@
(h4 :class "font-semibold mt-4 mb-2" "Mechanism: data-sx-signal")
(p "A server-rendered element carries a " (code "data-sx-signal") " attribute naming a store signal and its new value. When the morph processes this element, it writes to the signal instead of (or in addition to) updating the DOM.")
(~docs/code :code (highlight ";; Server response includes:\n(div :data-sx-signal \"cart-count:7\"\n (span \"7 items\"))\n\n;; The morph sees data-sx-signal, parses it:\n;; store name = \"cart-count\"\n;; value = 7\n;; Then: (reset! (use-store \"cart-count\") 7)\n;;\n;; Any island anywhere on the page that reads cart-count\n;; updates immediately — fine-grained, no re-render." "lisp"))
(~docs/code :src (highlight ";; Server response includes:\n(div :data-sx-signal \"cart-count:7\"\n (span \"7 items\"))\n\n;; The morph sees data-sx-signal, parses it:\n;; store name = \"cart-count\"\n;; value = 7\n;; Then: (reset! (use-store \"cart-count\") 7)\n;;\n;; Any island anywhere on the page that reads cart-count\n;; updates immediately — fine-grained, no re-render." "lisp"))
(h4 :class "font-semibold mt-4 mb-2" "Mechanism: sx-on-settle")
(p "An " (code "sx-on-settle") " attribute on a hypermedia trigger element. After the swap completes and the DOM settles, the SX expression is evaluated. This gives the response a chance to run arbitrary reactive logic.")
(~docs/code :code (highlight ";; A search form that updates a signal after results arrive:\n(form :sx-post \"/search\" :sx-target \"#results\"\n :sx-on-settle (reset! (use-store \"result-count\") result-count)\n (input :name \"q\" :placeholder \"Search...\"))" "lisp"))
(~docs/code :src (highlight ";; A search form that updates a signal after results arrive:\n(form :sx-post \"/search\" :sx-target \"#results\"\n :sx-on-settle (reset! (use-store \"result-count\") result-count)\n (input :name \"q\" :placeholder \"Search...\"))" "lisp"))
(h4 :class "font-semibold mt-4 mb-2" "Mechanism: event bridge (already exists)")
(p "The event bridge (" (code "data-sx-emit") ") already provides server → island communication via custom DOM events. Marshes generalise this: " (code "data-sx-signal") " is a declarative shorthand for the common case of \"server says update this value.\"")
@@ -59,7 +59,7 @@
(h4 :class "font-semibold mt-4 mb-2" "Mechanism: marsh tag")
(p "A " (code "marsh") " is a zone inside an island where server content is " (em "re-evaluated") " by the island's reactive evaluator, not just inserted as static DOM. When the morph updates a marsh, the new content is parsed as SX and rendered in the island's signal context.")
(~docs/code :code (highlight ";; Inside an island — a marsh re-evaluates on morph:\n(defisland ~reactive-islands/marshes/product-card (&key product-id)\n (let ((quantity (signal 1))\n (variant (signal nil)))\n (div :class \"card\"\n ;; Lake: server content, inserted as static HTML\n (lake :id \"description\"\n (p \"Loading...\"))\n ;; Marsh: server content, evaluated with access to island signals\n (marsh :id \"controls\"\n ;; Initial content from server — has signal references:\n (div\n (select :bind variant\n (option :value \"red\" \"Red\")\n (option :value \"blue\" \"Blue\"))\n (input :type \"number\" :bind quantity))))))" "lisp"))
(~docs/code :src (highlight ";; Inside an island — a marsh re-evaluates on morph:\n(defisland ~reactive-islands/marshes/product-card (&key product-id)\n (let ((quantity (signal 1))\n (variant (signal nil)))\n (div :class \"card\"\n ;; Lake: server content, inserted as static HTML\n (lake :id \"description\"\n (p \"Loading...\"))\n ;; Marsh: server content, evaluated with access to island signals\n (marsh :id \"controls\"\n ;; Initial content from server — has signal references:\n (div\n (select :bind variant\n (option :value \"red\" \"Red\")\n (option :value \"blue\" \"Blue\"))\n (input :type \"number\" :bind quantity))))))" "lisp"))
(p "When the server sends updated marsh content (e.g., new variant options fetched from a database), the island re-evaluates it in its signal scope. The new " (code "select") " options bind to the existing " (code "variant") " signal. The reactive graph reconnects seamlessly.")
(h4 :class "font-semibold mt-4 mb-2" "Lake vs. Marsh")
@@ -100,12 +100,12 @@
(h4 :class "font-semibold mt-4 mb-2" "3a: Signal-bound hypermedia attributes")
(p "Hypermedia trigger attributes (" (code "sx-get") ", " (code "sx-post") ", " (code "sx-target") ", " (code "sx-swap") ") can reference signals. The URL, target, and swap strategy become reactive.")
(~docs/code :code (highlight ";; A search input whose endpoint depends on reactive state:\n(defisland ~reactive-islands/marshes/smart-search ()\n (let ((mode (signal \"products\"))\n (query (signal \"\")))\n (div\n ;; Mode selector — changes what we're searching\n (select :bind mode\n (option :value \"products\" \"Products\")\n (option :value \"events\" \"Events\")\n (option :value \"posts\" \"Posts\"))\n ;; Search input — endpoint changes reactively\n (input :type \"text\" :bind query\n :sx-get (computed (fn () (str \"/search/\" (deref mode) \"?q=\" (deref query))))\n :sx-trigger \"input changed delay:300ms\"\n :sx-target \"#results\")\n (div :id \"results\"))))" "lisp"))
(~docs/code :src (highlight ";; A search input whose endpoint depends on reactive state:\n(defisland ~reactive-islands/marshes/smart-search ()\n (let ((mode (signal \"products\"))\n (query (signal \"\")))\n (div\n ;; Mode selector — changes what we're searching\n (select :bind mode\n (option :value \"products\" \"Products\")\n (option :value \"events\" \"Events\")\n (option :value \"posts\" \"Posts\"))\n ;; Search input — endpoint changes reactively\n (input :type \"text\" :bind query\n :sx-get (computed (fn () (str \"/search/\" (deref mode) \"?q=\" (deref query))))\n :sx-trigger \"input changed delay:300ms\"\n :sx-target \"#results\")\n (div :id \"results\"))))" "lisp"))
(p "The " (code "sx-get") " URL isn't a static string — it's a computed signal. When the mode changes, the next search hits a different endpoint. The hypermedia trigger system reads the signal's current value at trigger time.")
(h4 :class "font-semibold mt-4 mb-2" "3b: Reactive swap transforms")
(p "A " (code "marsh-transform") " function that processes server content " (em "before") " it enters the DOM. The transform has access to island signals, so it can reshape the same server response differently based on client state.")
(~docs/code :code (highlight ";; View mode transforms how server results are displayed:\n(defisland ~reactive-islands/marshes/result-list ()\n (let ((view (signal \"list\"))\n (sort-key (signal \"date\")))\n (div\n (div :class \"flex gap-2\"\n (button :on-click (fn (e) (reset! view \"list\")) \"List\")\n (button :on-click (fn (e) (reset! view \"grid\")) \"Grid\")\n (button :on-click (fn (e) (reset! view \"compact\")) \"Compact\"))\n ;; Marsh: server sends a result list; client transforms its rendering\n (marsh :id \"results\"\n :transform (fn (sx-content)\n (case (deref view)\n \"grid\" (wrap-grid sx-content)\n \"compact\" (compact-view sx-content)\n :else sx-content))\n ;; Initial server content\n (div :class \"space-y-2\" \"Loading...\")))))" "lisp"))
(~docs/code :src (highlight ";; View mode transforms how server results are displayed:\n(defisland ~reactive-islands/marshes/result-list ()\n (let ((view (signal \"list\"))\n (sort-key (signal \"date\")))\n (div\n (div :class \"flex gap-2\"\n (button :on-click (fn (e) (reset! view \"list\")) \"List\")\n (button :on-click (fn (e) (reset! view \"grid\")) \"Grid\")\n (button :on-click (fn (e) (reset! view \"compact\")) \"Compact\"))\n ;; Marsh: server sends a result list; client transforms its rendering\n (marsh :id \"results\"\n :transform (fn (sx-content)\n (case (deref view)\n \"grid\" (wrap-grid sx-content)\n \"compact\" (compact-view sx-content)\n :else sx-content))\n ;; Initial server content\n (div :class \"space-y-2\" \"Loading...\")))))" "lisp"))
(p "The server sends the same canonical result list every time. The " (code ":transform") " function — a reactive closure over the " (code "view") " signal — reshapes it into a grid, compact list, or default list. Change the view signal → existing content is re-transformed without a server round-trip. Fetch new results → they arrive pre-sorted, then the transform applies the current view.")
(h4 :class "font-semibold mt-4 mb-2" "3c: Reactive interpretation")
@@ -124,19 +124,19 @@
(p "Five new constructs, all specced in " (code ".sx") " files, bootstrapped to every host.")
(h4 :class "font-semibold mt-4 mb-2" "1. marsh tag")
(~docs/code :code (highlight ";; In adapter-dom.sx / adapter-html.sx / adapter-sx.sx:\n;;\n;; (marsh :id \"controls\" :transform transform-fn children...)\n;;\n;; Server: renders as <div data-sx-marsh=\"controls\">children HTML</div>\n;; Client: wraps children in reactive evaluation scope\n;; Morph: re-parses incoming SX, evaluates in island scope, replaces DOM\n;;\n;; The :transform is optional. If present, it's called on the parsed SX\n;; before evaluation. The transform has full signal access." "lisp"))
(~docs/code :src (highlight ";; In adapter-dom.sx / adapter-html.sx / adapter-sx.sx:\n;;\n;; (marsh :id \"controls\" :transform transform-fn children...)\n;;\n;; Server: renders as <div data-sx-marsh=\"controls\">children HTML</div>\n;; Client: wraps children in reactive evaluation scope\n;; Morph: re-parses incoming SX, evaluates in island scope, replaces DOM\n;;\n;; The :transform is optional. If present, it's called on the parsed SX\n;; before evaluation. The transform has full signal access." "lisp"))
(h4 :class "font-semibold mt-4 mb-2" "2. data-sx-signal (morph integration)")
(~docs/code :code (highlight ";; In engine.sx, morph-children:\n;;\n;; When processing a new element with data-sx-signal=\"name:value\":\n;; 1. Parse the attribute: store-name, signal-value\n;; 2. Look up (use-store store-name) — finds or creates the signal\n;; 3. (reset! signal parsed-value)\n;; 4. Remove the data-sx-signal attribute from DOM (consumed)\n;;\n;; Values are JSON-parsed: \"7\" → 7, '\"hello\"' → \"hello\",\n;; 'true' → true, '{...}' → dict" "lisp"))
(~docs/code :src (highlight ";; In engine.sx, morph-children:\n;;\n;; When processing a new element with data-sx-signal=\"name:value\":\n;; 1. Parse the attribute: store-name, signal-value\n;; 2. Look up (use-store store-name) — finds or creates the signal\n;; 3. (reset! signal parsed-value)\n;; 4. Remove the data-sx-signal attribute from DOM (consumed)\n;;\n;; Values are JSON-parsed: \"7\" → 7, '\"hello\"' → \"hello\",\n;; 'true' → true, '{...}' → dict" "lisp"))
(h4 :class "font-semibold mt-4 mb-2" "3. Signal-bound hypermedia attributes")
(~docs/code :code (highlight ";; In orchestration.sx, resolve-trigger-attrs:\n;;\n;; Before issuing a fetch, read sx-get/sx-post/sx-target/sx-swap.\n;; If the value is a signal or computed, deref it at trigger time.\n;;\n;; (define resolve-trigger-url\n;; (fn (el attr)\n;; (let ((val (dom-get-attr el attr)))\n;; (if (signal? val) (deref val) val))))\n;;\n;; This means the URL is evaluated lazily — it reflects the current\n;; signal state at the moment the user acts, not when the DOM was built." "lisp"))
(~docs/code :src (highlight ";; In orchestration.sx, resolve-trigger-attrs:\n;;\n;; Before issuing a fetch, read sx-get/sx-post/sx-target/sx-swap.\n;; If the value is a signal or computed, deref it at trigger time.\n;;\n;; (define resolve-trigger-url\n;; (fn (el attr)\n;; (let ((val (dom-get-attr el attr)))\n;; (if (signal? val) (deref val) val))))\n;;\n;; This means the URL is evaluated lazily — it reflects the current\n;; signal state at the moment the user acts, not when the DOM was built." "lisp"))
(h4 :class "font-semibold mt-4 mb-2" "4. marsh-transform (swap pipeline)")
(~docs/code :code (highlight ";; In orchestration.sx, process-swap:\n;;\n;; After receiving server HTML and before inserting into target:\n;; 1. Find the target element\n;; 2. If target has data-sx-marsh, find its transform function\n;; 3. Parse server content as SX\n;; 4. Call transform(sx-content) — transform is a reactive closure\n;; 5. Evaluate the transformed SX in the island's signal scope\n;; 6. Replace the marsh's DOM children\n;;\n;; The transform runs inside the island's tracking context,\n;; so computed/effect dependencies are captured automatically.\n;; When a signal the transform reads changes, the marsh re-transforms." "lisp"))
(~docs/code :src (highlight ";; In orchestration.sx, process-swap:\n;;\n;; After receiving server HTML and before inserting into target:\n;; 1. Find the target element\n;; 2. If target has data-sx-marsh, find its transform function\n;; 3. Parse server content as SX\n;; 4. Call transform(sx-content) — transform is a reactive closure\n;; 5. Evaluate the transformed SX in the island's signal scope\n;; 6. Replace the marsh's DOM children\n;;\n;; The transform runs inside the island's tracking context,\n;; so computed/effect dependencies are captured automatically.\n;; When a signal the transform reads changes, the marsh re-transforms." "lisp"))
(h4 :class "font-semibold mt-4 mb-2" "5. sx-on-settle (post-swap hook)")
(~docs/code :code (highlight ";; In orchestration.sx, after swap completes:\n;;\n;; (define process-settle-hooks\n;; (fn (trigger-el)\n;; (let ((hook (dom-get-attr trigger-el \"sx-on-settle\")))\n;; (when hook\n;; (eval-expr (parse hook) (island-env trigger-el))))))\n;;\n;; The expression is evaluated in the nearest island's environment,\n;; giving it access to signals, stores, and island-local functions." "lisp")))
(~docs/code :src (highlight ";; In orchestration.sx, after swap completes:\n;;\n;; (define process-settle-hooks\n;; (fn (trigger-el)\n;; (let ((hook (dom-get-attr trigger-el \"sx-on-settle\")))\n;; (when hook\n;; (eval-expr (parse hook) (island-env trigger-el))))))\n;;\n;; The expression is evaluated in the nearest island's environment,\n;; giving it access to signals, stores, and island-local functions." "lisp")))
;; =====================================================================
;; IV. The morph enters the marsh
@@ -169,9 +169,9 @@
(td :class "px-3 py-2 font-mono text-sm text-stone-600" "data-sx-marsh")
(td :class "px-3 py-2 text-stone-600" "Parse new content as SX, apply transform, evaluate in island scope, replace DOM")))))
(~docs/code :code (highlight ";; Updated morph-island-children in engine.sx:\n\n(define morph-island-children\n (fn (old-island new-island)\n (let ((old-lakes (dom-query-all old-island \"[data-sx-lake]\"))\n (new-lakes (dom-query-all new-island \"[data-sx-lake]\"))\n (old-marshes (dom-query-all old-island \"[data-sx-marsh]\"))\n (new-marshes (dom-query-all new-island \"[data-sx-marsh]\")))\n ;; Build lookup maps\n (let ((new-lake-map (index-by-attr new-lakes \"data-sx-lake\"))\n (new-marsh-map (index-by-attr new-marshes \"data-sx-marsh\")))\n ;; Lakes: static DOM swap\n (for-each\n (fn (old-lake)\n (let ((id (dom-get-attr old-lake \"data-sx-lake\"))\n (new-lake (dict-get new-lake-map id)))\n (when new-lake\n (sync-attrs old-lake new-lake)\n (morph-children old-lake new-lake))))\n old-lakes)\n ;; Marshes: parse + evaluate + replace\n (for-each\n (fn (old-marsh)\n (let ((id (dom-get-attr old-marsh \"data-sx-marsh\"))\n (new-marsh (dict-get new-marsh-map id)))\n (when new-marsh\n (morph-marsh old-marsh new-marsh old-island))))\n old-marshes)\n ;; Signal updates from data-sx-signal\n (process-signal-updates new-island)))))" "lisp"))
(~docs/code :src (highlight ";; Updated morph-island-children in engine.sx:\n\n(define morph-island-children\n (fn (old-island new-island)\n (let ((old-lakes (dom-query-all old-island \"[data-sx-lake]\"))\n (new-lakes (dom-query-all new-island \"[data-sx-lake]\"))\n (old-marshes (dom-query-all old-island \"[data-sx-marsh]\"))\n (new-marshes (dom-query-all new-island \"[data-sx-marsh]\")))\n ;; Build lookup maps\n (let ((new-lake-map (index-by-attr new-lakes \"data-sx-lake\"))\n (new-marsh-map (index-by-attr new-marshes \"data-sx-marsh\")))\n ;; Lakes: static DOM swap\n (for-each\n (fn (old-lake)\n (let ((id (dom-get-attr old-lake \"data-sx-lake\"))\n (new-lake (dict-get new-lake-map id)))\n (when new-lake\n (sync-attrs old-lake new-lake)\n (morph-children old-lake new-lake))))\n old-lakes)\n ;; Marshes: parse + evaluate + replace\n (for-each\n (fn (old-marsh)\n (let ((id (dom-get-attr old-marsh \"data-sx-marsh\"))\n (new-marsh (dict-get new-marsh-map id)))\n (when new-marsh\n (morph-marsh old-marsh new-marsh old-island))))\n old-marshes)\n ;; Signal updates from data-sx-signal\n (process-signal-updates new-island)))))" "lisp"))
(~docs/code :code (highlight ";; morph-marsh: re-evaluate server content in island scope\n\n(define morph-marsh\n (fn (old-marsh new-marsh island-el)\n (let ((transform (get-marsh-transform old-marsh))\n (new-sx (dom-inner-sx new-marsh))\n (island-env (get-island-env island-el)))\n ;; Apply transform if present\n (let ((transformed (if transform (transform new-sx) new-sx)))\n ;; Dispose old reactive bindings in this marsh\n (dispose-marsh-scope old-marsh)\n ;; Evaluate the SX in island scope — creates new reactive bindings\n (with-marsh-scope old-marsh\n (let ((new-dom (render-to-dom transformed island-env)))\n (dom-replace-children old-marsh new-dom)))))))" "lisp")))
(~docs/code :src (highlight ";; morph-marsh: re-evaluate server content in island scope\n\n(define morph-marsh\n (fn (old-marsh new-marsh island-el)\n (let ((transform (get-marsh-transform old-marsh))\n (new-sx (dom-inner-sx new-marsh))\n (island-env (get-island-env island-el)))\n ;; Apply transform if present\n (let ((transformed (if transform (transform new-sx) new-sx)))\n ;; Dispose old reactive bindings in this marsh\n (dispose-marsh-scope old-marsh)\n ;; Evaluate the SX in island scope — creates new reactive bindings\n (with-marsh-scope old-marsh\n (let ((new-dom (render-to-dom transformed island-env)))\n (dom-replace-children old-marsh new-dom)))))))" "lisp")))
;; =====================================================================
;; V. Signal lifecycle in marshes
@@ -181,7 +181,7 @@
(p "Marshes introduce a sub-scope within the island's reactive context. When a marsh is re-evaluated (morph or transform change), its old effects and computeds must be disposed without disturbing the island's own reactive graph.")
(~docs/subsection :title "Scoping"
(~docs/code :code (highlight ";; In signals.sx:\n\n(define with-marsh-scope\n (fn (marsh-el body-fn)\n ;; Create a child scope under the current island scope\n ;; All effects/computeds created during body-fn register here\n (let ((parent-scope (current-island-scope))\n (marsh-scope (create-child-scope parent-scope (dom-get-attr marsh-el \"data-sx-marsh\"))))\n (with-scope marsh-scope\n (body-fn)))))\n\n(define dispose-marsh-scope\n (fn (marsh-el)\n ;; Dispose all effects/computeds registered in this marsh's scope\n ;; Parent island scope and sibling marshes are unaffected\n (let ((scope (get-marsh-scope marsh-el)))\n (when scope (dispose-scope scope)))))" "lisp"))
(~docs/code :src (highlight ";; In signals.sx:\n\n(define with-marsh-scope\n (fn (marsh-el body-fn)\n ;; Create a child scope under the current island scope\n ;; All effects/computeds created during body-fn register here\n (let ((parent-scope (current-island-scope))\n (marsh-scope (create-child-scope parent-scope (dom-get-attr marsh-el \"data-sx-marsh\"))))\n (with-scope marsh-scope\n (body-fn)))))\n\n(define dispose-marsh-scope\n (fn (marsh-el)\n ;; Dispose all effects/computeds registered in this marsh's scope\n ;; Parent island scope and sibling marshes are unaffected\n (let ((scope (get-marsh-scope marsh-el)))\n (when scope (dispose-scope scope)))))" "lisp"))
(p "The scoping hierarchy: " (strong "island") " → " (strong "marsh") " → " (strong "effects/computeds") ". Disposing a marsh disposes its subscope. Disposing an island disposes all its marshes. The signal graph is a tree, not a flat list."))
(~docs/subsection :title "Reactive transforms"
@@ -197,17 +197,17 @@
(~docs/subsection :title "Swap strategy as signal"
(p "The same server response inserted differently based on client state:")
(~docs/code :code (highlight ";; Chat app: append messages normally, morph when switching threads\n(defisland ~reactive-islands/marshes/chat ()\n (let ((mode (signal \"live\")))\n (div\n (div :sx-get \"/messages/latest\"\n :sx-trigger \"every 2s\"\n :sx-target \"#messages\"\n :sx-swap (computed (fn ()\n (if (= (deref mode) \"live\") \"beforeend\" \"innerHTML\")))\n (div :id \"messages\")))))" "lisp"))
(~docs/code :src (highlight ";; Chat app: append messages normally, morph when switching threads\n(defisland ~reactive-islands/marshes/chat ()\n (let ((mode (signal \"live\")))\n (div\n (div :sx-get \"/messages/latest\"\n :sx-trigger \"every 2s\"\n :sx-target \"#messages\"\n :sx-swap (computed (fn ()\n (if (= (deref mode) \"live\") \"beforeend\" \"innerHTML\")))\n (div :id \"messages\")))))" "lisp"))
(p "In " (code "\"live\"") " mode, new messages append. Switch to thread view — the same polling endpoint now replaces the whole list. The server doesn't change. The client's reactive state changes the " (em "semantics") " of the swap."))
(~docs/subsection :title "URL rewriting as signal"
(p "Reactive state transparently modifies request URLs:")
(~docs/code :code (highlight ";; Locale prefix — the server sees /fr/products, /en/products, etc.\n;; The author writes /products — the marsh layer prepends the locale.\n(def-store \"locale\" \"en\")\n\n;; In orchestration.sx, resolve-trigger-url:\n(define resolve-trigger-url\n (fn (el attr)\n (let ((raw (dom-get-attr el attr))\n (locale (deref (use-store \"locale\"))))\n (if (and locale (not (starts-with? raw (str \"/\" locale))))\n (str \"/\" locale raw)\n raw))))" "lisp"))
(~docs/code :src (highlight ";; Locale prefix — the server sees /fr/products, /en/products, etc.\n;; The author writes /products — the marsh layer prepends the locale.\n(def-store \"locale\" \"en\")\n\n;; In orchestration.sx, resolve-trigger-url:\n(define resolve-trigger-url\n (fn (el attr)\n (let ((raw (dom-get-attr el attr))\n (locale (deref (use-store \"locale\"))))\n (if (and locale (not (starts-with? raw (str \"/\" locale))))\n (str \"/\" locale raw)\n raw))))" "lisp"))
(p "Every " (code "sx-get") " and " (code "sx-post") " URL passes through the resolver. A locale signal, a preview-mode signal, an A/B-test signal — any reactive state can transparently rewrite the request the server sees."))
(~docs/subsection :title "Content rewriting as signal"
(p "Incoming server HTML passes through a reactive filter before insertion:")
(~docs/code :code (highlight ";; Dark mode — rewrites server classes before insertion\n(def-store \"theme\" \"light\")\n\n;; In orchestration.sx, after receiving server HTML:\n(define apply-theme-transform\n (fn (html-str)\n (if (= (deref (use-store \"theme\")) \"dark\")\n (-> html-str\n (replace-all \"bg-white\" \"bg-stone-900\")\n (replace-all \"text-stone-800\" \"text-stone-100\")\n (replace-all \"border-stone-200\" \"border-stone-700\"))\n html-str)))" "lisp"))
(~docs/code :src (highlight ";; Dark mode — rewrites server classes before insertion\n(def-store \"theme\" \"light\")\n\n;; In orchestration.sx, after receiving server HTML:\n(define apply-theme-transform\n (fn (html-str)\n (if (= (deref (use-store \"theme\")) \"dark\")\n (-> html-str\n (replace-all \"bg-white\" \"bg-stone-900\")\n (replace-all \"text-stone-800\" \"text-stone-100\")\n (replace-all \"border-stone-200\" \"border-stone-700\"))\n html-str)))" "lisp"))
(p "The server renders canonical light-mode HTML. The client's theme signal rewrites it at the edge. No server-side theme support needed. No separate dark-mode templates. The same document, different interpretation.")
(p "This is the Hegelian deepening: the reactive state isn't just " (em "alongside") " the hypermedia content. It " (em "constitutes the lens through which the content is perceived") ". The marsh isn't a zone in the DOM — it's a layer in the interpretation pipeline.")))
@@ -297,9 +297,9 @@
(~reactive-islands/marshes/demo-marsh-product)
(~docs/code :code (highlight ";; Island with a store-backed price signal\n(defisland ~reactive-islands/marshes/demo-marsh-product ()\n (let ((price (def-store \"demo-price\" (fn () (signal 19.99))))\n (qty (signal 1))\n (total (computed (fn () (* (deref price) (deref qty))))))\n (div\n ;; Reactive price display — updates when store changes\n (span \"$\" (deref price))\n (span \"Qty:\") (button \"-\") (span (deref qty)) (button \"+\")\n (span \"Total: $\" (deref total))\n\n ;; Fetch from server — response arrives as hypermedia\n (button :sx-get \"/sx/(geography.(reactive.(api.flash-sale)))\"\n :sx-target \"#marsh-server-msg\"\n :sx-swap \"innerHTML\"\n \"Fetch Price\")\n ;; Server response lands here:\n (div :id \"marsh-server-msg\"))))" "lisp"))
(~docs/code :src (highlight ";; Island with a store-backed price signal\n(defisland ~reactive-islands/marshes/demo-marsh-product ()\n (let ((price (def-store \"demo-price\" (fn () (signal 19.99))))\n (qty (signal 1))\n (total (computed (fn () (* (deref price) (deref qty))))))\n (div\n ;; Reactive price display — updates when store changes\n (span \"$\" (deref price))\n (span \"Qty:\") (button \"-\") (span (deref qty)) (button \"+\")\n (span \"Total: $\" (deref total))\n\n ;; Fetch from server — response arrives as hypermedia\n (button :sx-get \"/sx/(geography.(reactive.(api.flash-sale)))\"\n :sx-target \"#marsh-server-msg\"\n :sx-swap \"innerHTML\"\n \"Fetch Price\")\n ;; Server response lands here:\n (div :id \"marsh-server-msg\"))))" "lisp"))
(~docs/code :code (highlight ";; Server returns SX content + a data-init script:\n;;\n;; (<>\n;; (p \"Flash sale! Price: $14.99\")\n;; (script :type \"text/sx\" :data-init\n;; \"(reset! (use-store \\\"demo-price\\\") 14.99)\"))\n;;\n;; The <p> is swapped in as normal hypermedia content.\n;; The script writes to the store signal.\n;; The island's (deref price), total, and savings\n;; all update reactively — no re-render, no diffing." "lisp"))
(~docs/code :src (highlight ";; Server returns SX content + a data-init script:\n;;\n;; (<>\n;; (p \"Flash sale! Price: $14.99\")\n;; (script :type \"text/sx\" :data-init\n;; \"(reset! (use-store \\\"demo-price\\\") 14.99)\"))\n;;\n;; The <p> is swapped in as normal hypermedia content.\n;; The script writes to the store signal.\n;; The island's (deref price), total, and savings\n;; all update reactively — no re-render, no diffing." "lisp"))
(p "Two things happen from one server response: content appears in the swap target (hypermedia) and the price signal updates (reactivity). The island didn't fetch the price. The server didn't call a signal API. The response " (em "is") " both.")))
@@ -323,7 +323,7 @@
:sx-swap "innerHTML"
"Fetch from server"))
(~docs/code :code (highlight ";; Island A — creates the store, has control buttons\n(defisland ~reactive-islands/marshes/demo-marsh-store-writer ()\n (let ((price (def-store \"demo-price\" (fn () (signal 19.99)))))\n ;; (reset! price 14.99) is what data-sx-signal does during morph\n (button :on-click (fn (e) (reset! price 14.99))\n \"Flash Sale $14.99\")))\n\n;; Island B — reads the same store, different island\n(defisland ~reactive-islands/marshes/demo-marsh-store-reader ()\n (let ((price (def-store \"demo-price\" (fn () (signal 19.99)))))\n (span \"$\" (deref price))))\n\n;; Server returns: data-sx-signal writes to the store during morph\n;; (div :data-sx-signal \"demo-price:14.99\"\n;; (p \"Flash sale! Price updated.\"))" "lisp"))
(~docs/code :src (highlight ";; Island A — creates the store, has control buttons\n(defisland ~reactive-islands/marshes/demo-marsh-store-writer ()\n (let ((price (def-store \"demo-price\" (fn () (signal 19.99)))))\n ;; (reset! price 14.99) is what data-sx-signal does during morph\n (button :on-click (fn (e) (reset! price 14.99))\n \"Flash Sale $14.99\")))\n\n;; Island B — reads the same store, different island\n(defisland ~reactive-islands/marshes/demo-marsh-store-reader ()\n (let ((price (def-store \"demo-price\" (fn () (signal 19.99)))))\n (span \"$\" (deref price))))\n\n;; Server returns: data-sx-signal writes to the store during morph\n;; (div :data-sx-signal \"demo-price:14.99\"\n;; (p \"Flash sale! Price updated.\"))" "lisp"))
(p "In production, the server response includes " (code "data-sx-signal=\"demo-price:14.99\"") ". The morph algorithm processes this attribute, calls " (code "(reset! (use-store \"demo-price\") 14.99)") ", and removes the attribute from the DOM. Every island reading that store updates instantly — fine-grained, no re-render.")))
@@ -334,7 +334,7 @@
(~reactive-islands/marshes/demo-marsh-settle)
(~docs/code :code (highlight ";; sx-on-settle runs SX after the swap settles\n(defisland ~reactive-islands/marshes/demo-marsh-settle ()\n (let ((count (def-store \"settle-count\" (fn () (signal 0)))))\n (div\n ;; Reactive counter — updates from sx-on-settle\n (span \"Fetched: \" (deref count) \" times\")\n\n ;; Button with sx-on-settle hook\n (button :sx-get \"/sx/(geography.(reactive.(api.settle-data)))\"\n :sx-target \"#settle-result\"\n :sx-swap \"innerHTML\"\n :sx-on-settle \"(swap! (use-store \\\"settle-count\\\") inc)\"\n \"Fetch Item\")\n\n ;; Server content lands here (pure hypermedia)\n (div :id \"settle-result\"))))" "lisp"))
(~docs/code :src (highlight ";; sx-on-settle runs SX after the swap settles\n(defisland ~reactive-islands/marshes/demo-marsh-settle ()\n (let ((count (def-store \"settle-count\" (fn () (signal 0)))))\n (div\n ;; Reactive counter — updates from sx-on-settle\n (span \"Fetched: \" (deref count) \" times\")\n\n ;; Button with sx-on-settle hook\n (button :sx-get \"/sx/(geography.(reactive.(api.settle-data)))\"\n :sx-target \"#settle-result\"\n :sx-swap \"innerHTML\"\n :sx-on-settle \"(swap! (use-store \\\"settle-count\\\") inc)\"\n \"Fetch Item\")\n\n ;; Server content lands here (pure hypermedia)\n (div :id \"settle-result\"))))" "lisp"))
(p "The server knows nothing about signals or counters. It returns plain content. The " (code "sx-on-settle") " hook is a client-side concern — it runs in the global SX environment with access to all primitives.")))
@@ -345,7 +345,7 @@
(~reactive-islands/marshes/demo-marsh-signal-url)
(~docs/code :code (highlight ";; sx-get URL computed from a signal\n(defisland ~reactive-islands/marshes/demo-marsh-signal-url ()\n (let ((mode (signal \"products\"))\n (query (signal \"\")))\n (div\n ;; Mode selector — changes what we're searching\n (div :class \"flex gap-2\"\n (button :on-click (fn (e) (reset! mode \"products\"))\n :class (computed (fn () ...active-class...))\n \"Products\")\n (button :on-click (fn (e) (reset! mode \"events\")) \"Events\")\n (button :on-click (fn (e) (reset! mode \"posts\")) \"Posts\"))\n\n ;; Search button — URL is a computed expression\n (button :sx-get (computed (fn ()\n (str \"/sx/(geography.(reactive.(api.search-\"\n (deref mode) \")))\" \"?q=\" (deref query))))\n :sx-target \"#signal-results\"\n :sx-swap \"innerHTML\"\n \"Search\")\n\n (div :id \"signal-results\"))))" "lisp"))
(~docs/code :src (highlight ";; sx-get URL computed from a signal\n(defisland ~reactive-islands/marshes/demo-marsh-signal-url ()\n (let ((mode (signal \"products\"))\n (query (signal \"\")))\n (div\n ;; Mode selector — changes what we're searching\n (div :class \"flex gap-2\"\n (button :on-click (fn (e) (reset! mode \"products\"))\n :class (computed (fn () ...active-class...))\n \"Products\")\n (button :on-click (fn (e) (reset! mode \"events\")) \"Events\")\n (button :on-click (fn (e) (reset! mode \"posts\")) \"Posts\"))\n\n ;; Search button — URL is a computed expression\n (button :sx-get (computed (fn ()\n (str \"/sx/(geography.(reactive.(api.search-\"\n (deref mode) \")))\" \"?q=\" (deref query))))\n :sx-target \"#signal-results\"\n :sx-swap \"innerHTML\"\n \"Search\")\n\n (div :id \"signal-results\"))))" "lisp"))
(p "No custom plumbing. The same " (code "reactive-attr") " mechanism that makes " (code ":class") " reactive also makes " (code ":sx-get") " reactive. " (code "get-verb-info") " reads " (code "dom-get-attr") " at trigger time — it sees the current URL because the effect already updated the DOM attribute.")))
@@ -355,7 +355,7 @@
(~reactive-islands/marshes/demo-marsh-view-transform)
(~docs/code :code (highlight ";; View mode transforms display without refetch\n(defisland ~reactive-islands/marshes/demo-marsh-view-transform ()\n (let ((view (signal \"list\"))\n (items (signal nil)))\n (div\n ;; View toggle\n (div :class \"flex gap-2\"\n (button :on-click (fn (e) (reset! view \"list\")) \"List\")\n (button :on-click (fn (e) (reset! view \"grid\")) \"Grid\")\n (button :on-click (fn (e) (reset! view \"compact\")) \"Compact\"))\n\n ;; Fetch from server — stores raw data in signal\n (button :sx-get \"/sx/(geography.(reactive.(api.catalog)))\"\n :sx-target \"#catalog-raw\"\n :sx-swap \"innerHTML\"\n \"Fetch Catalog\")\n\n ;; Raw server content (hidden, used as data source)\n (div :id \"catalog-raw\" :class \"hidden\")\n\n ;; Reactive display — re-renders when view changes\n (div (computed (fn () (render-view (deref view) (deref items))))))))" "lisp"))
(~docs/code :src (highlight ";; View mode transforms display without refetch\n(defisland ~reactive-islands/marshes/demo-marsh-view-transform ()\n (let ((view (signal \"list\"))\n (items (signal nil)))\n (div\n ;; View toggle\n (div :class \"flex gap-2\"\n (button :on-click (fn (e) (reset! view \"list\")) \"List\")\n (button :on-click (fn (e) (reset! view \"grid\")) \"Grid\")\n (button :on-click (fn (e) (reset! view \"compact\")) \"Compact\"))\n\n ;; Fetch from server — stores raw data in signal\n (button :sx-get \"/sx/(geography.(reactive.(api.catalog)))\"\n :sx-target \"#catalog-raw\"\n :sx-swap \"innerHTML\"\n \"Fetch Catalog\")\n\n ;; Raw server content (hidden, used as data source)\n (div :id \"catalog-raw\" :class \"hidden\")\n\n ;; Reactive display — re-renders when view changes\n (div (computed (fn () (render-view (deref view) (deref items))))))))" "lisp"))
(p "The view signal doesn't just toggle CSS classes — it fundamentally reshapes the DOM. List view shows description. Grid view arranges in columns. Compact view shows names only. All from the same server data, transformed by client state.")))

View File

@@ -14,7 +14,7 @@
(p "Named stores solve all three. A store is a named collection of signals that lives at " (em "page") " scope, not island scope."))
(~docs/section :title "def-store / use-store" :id "api"
(~docs/code :code (highlight ";; Create a named store — called once at page level\n;; The init function creates signals and computeds\n(def-store \"cart\" (fn ()\n (let ((items (signal (list))))\n (dict\n :items items\n :count (computed (fn () (length (deref items))))\n :total (computed (fn () (reduce + 0\n (map (fn (i) (get i \"price\")) (deref items)))))))))\n\n;; Use the store from any island — returns the signal dict\n(defisland ~reactive-islands/named-stores/cart-badge ()\n (let ((store (use-store \"cart\")))\n (span :class \"badge bg-violet-100 text-violet-800 px-2 py-1 rounded-full\"\n (deref (get store \"count\")))))\n\n(defisland ~reactive-islands/named-stores/cart-drawer ()\n (let ((store (use-store \"cart\")))\n (div :class \"p-4\"\n (h2 \"Cart\")\n (ul (map (fn (item)\n (li :class \"flex justify-between py-1\"\n (span (get item \"name\"))\n (span :class \"text-stone-500\" \"\£\" (get item \"price\"))))\n (deref (get store \"items\"))))\n (div :class \"border-t pt-2 font-semibold\"\n \"Total: \£\" (deref (get store \"total\"))))))" "lisp"))
(~docs/code :src (highlight ";; Create a named store — called once at page level\n;; The init function creates signals and computeds\n(def-store \"cart\" (fn ()\n (let ((items (signal (list))))\n (dict\n :items items\n :count (computed (fn () (length (deref items))))\n :total (computed (fn () (reduce + 0\n (map (fn (i) (get i \"price\")) (deref items)))))))))\n\n;; Use the store from any island — returns the signal dict\n(defisland ~reactive-islands/named-stores/cart-badge ()\n (let ((store (use-store \"cart\")))\n (span :class \"badge bg-violet-100 text-violet-800 px-2 py-1 rounded-full\"\n (deref (get store \"count\")))))\n\n(defisland ~reactive-islands/named-stores/cart-drawer ()\n (let ((store (use-store \"cart\")))\n (div :class \"p-4\"\n (h2 \"Cart\")\n (ul (map (fn (item)\n (li :class \"flex justify-between py-1\"\n (span (get item \"name\"))\n (span :class \"text-stone-500\" \"\£\" (get item \"price\"))))\n (deref (get store \"items\"))))\n (div :class \"border-t pt-2 font-semibold\"\n \"Total: \£\" (deref (get store \"total\"))))))" "lisp"))
(p (code "def-store") " is " (strong "idempotent") " — calling it again with the same name returns the existing store. This means multiple components can call " (code "def-store") " defensively without double-creating."))
@@ -29,7 +29,7 @@
(~docs/section :title "Combining with event bridge" :id "combined"
(p "Named stores + event bridge = full lake→island→island communication:")
(~docs/code :code (highlight ";; Store persists across island lifecycle\n(def-store \"cart\" (fn () ...))\n\n;; Island 1: product page with htmx lake\n(defisland ~reactive-islands/named-stores/product-island ()\n (let ((store (use-store \"cart\")))\n ;; Bridge server-rendered \"Add\" buttons to store\n (bridge-event container \"cart:add\" (get store \"items\")\n (fn (detail) (append (deref (get store \"items\")) detail)))\n ;; Lake content swapped via sx-get\n (div :id \"product-content\" :sx-get \"/products/featured\")))\n\n;; Island 2: cart badge in header (distant in DOM)\n(defisland ~reactive-islands/named-stores/cart-badge ()\n (let ((store (use-store \"cart\")))\n (span (deref (get store \"count\")))))" "lisp"))
(~docs/code :src (highlight ";; Store persists across island lifecycle\n(def-store \"cart\" (fn () ...))\n\n;; Island 1: product page with htmx lake\n(defisland ~reactive-islands/named-stores/product-island ()\n (let ((store (use-store \"cart\")))\n ;; Bridge server-rendered \"Add\" buttons to store\n (bridge-event container \"cart:add\" (get store \"items\")\n (fn (detail) (append (deref (get store \"items\")) detail)))\n ;; Lake content swapped via sx-get\n (div :id \"product-content\" :sx-get \"/products/featured\")))\n\n;; Island 2: cart badge in header (distant in DOM)\n(defisland ~reactive-islands/named-stores/cart-badge ()\n (let ((store (use-store \"cart\")))\n (span (deref (get store \"count\")))))" "lisp"))
(p "User clicks \"Add to Cart\" in server-rendered product content. " (code "cart:add") " event fires. Product island catches it via bridge. Store's " (code "items") " signal updates. Cart badge — in a completely different island — updates reactively because it reads the same signal."))

View File

@@ -58,7 +58,7 @@
(~docs/subsection :title "Design"
(p "A new " (code ":bind") " attribute on " (code "input") ", " (code "textarea") ", and " (code "select") " elements. It takes a signal and creates a bidirectional link: signal value flows into the element, user input flows back into the signal.")
(~docs/code :code (highlight ";; Bind a signal to an input\n(defisland ~reactive-islands/phase2/login-form ()\n (let ((email (signal \"\"))\n (password (signal \"\")))\n (form :on-submit (fn (e)\n (dom-prevent-default e)\n (fetch-json \"POST\" \"/api/login\"\n (dict \"email\" (deref email)\n \"password\" (deref password))))\n (input :type \"email\" :bind email\n :placeholder \"Email\")\n (input :type \"password\" :bind password\n :placeholder \"Password\")\n (button :type \"submit\" \"Log in\"))))" "lisp"))
(~docs/code :src (highlight ";; Bind a signal to an input\n(defisland ~reactive-islands/phase2/login-form ()\n (let ((email (signal \"\"))\n (password (signal \"\")))\n (form :on-submit (fn (e)\n (dom-prevent-default e)\n (fetch-json \"POST\" \"/api/login\"\n (dict \"email\" (deref email)\n \"password\" (deref password))))\n (input :type \"email\" :bind email\n :placeholder \"Email\")\n (input :type \"password\" :bind password\n :placeholder \"Password\")\n (button :type \"submit\" \"Log in\"))))" "lisp"))
(p "The " (code ":bind") " attribute is handled in " (code "adapter-dom.sx") "'s element rendering. For a signal " (code "s") ":")
(ol :class "space-y-1 text-stone-600 list-decimal list-inside text-sm"
@@ -69,7 +69,7 @@
(li "For select: bind to " (code "value") ", handle " (code "change") " event")))
(~docs/subsection :title "Spec changes"
(~docs/code :code (highlight ";; In adapter-dom.sx, inside render-dom-element:\n;; After processing :on-* event attrs, check for :bind\n(when (dict-has? kwargs \"bind\")\n (let ((sig (dict-get kwargs \"bind\")))\n (when (signal? sig)\n (bind-input el sig))))\n\n;; New function in adapter-dom.sx:\n(define bind-input\n (fn (el sig)\n (let ((tag (lower (dom-tag-name el)))\n (is-checkbox (or (= (dom-get-attr el \"type\") \"checkbox\")\n (= (dom-get-attr el \"type\") \"radio\"))))\n ;; Set initial value\n (if is-checkbox\n (dom-set-prop el \"checked\" (deref sig))\n (dom-set-prop el \"value\" (str (deref sig))))\n ;; Signal → element (effect, auto-tracked)\n (effect (fn ()\n (if is-checkbox\n (dom-set-prop el \"checked\" (deref sig))\n (let ((v (str (deref sig))))\n (when (!= (dom-get-prop el \"value\") v)\n (dom-set-prop el \"value\" v))))))\n ;; Element → signal (event listener)\n (dom-listen el (if is-checkbox \"change\" \"input\")\n (fn (e)\n (if is-checkbox\n (reset! sig (dom-get-prop el \"checked\"))\n (reset! sig (dom-get-prop el \"value\"))))))))" "lisp"))
(~docs/code :src (highlight ";; In adapter-dom.sx, inside render-dom-element:\n;; After processing :on-* event attrs, check for :bind\n(when (dict-has? kwargs \"bind\")\n (let ((sig (dict-get kwargs \"bind\")))\n (when (signal? sig)\n (bind-input el sig))))\n\n;; New function in adapter-dom.sx:\n(define bind-input\n (fn (el sig)\n (let ((tag (lower (dom-tag-name el)))\n (is-checkbox (or (= (dom-get-attr el \"type\") \"checkbox\")\n (= (dom-get-attr el \"type\") \"radio\"))))\n ;; Set initial value\n (if is-checkbox\n (dom-set-prop el \"checked\" (deref sig))\n (dom-set-prop el \"value\" (str (deref sig))))\n ;; Signal → element (effect, auto-tracked)\n (effect (fn ()\n (if is-checkbox\n (dom-set-prop el \"checked\" (deref sig))\n (let ((v (str (deref sig))))\n (when (!= (dom-get-prop el \"value\") v)\n (dom-set-prop el \"value\" v))))))\n ;; Element → signal (event listener)\n (dom-listen el (if is-checkbox \"change\" \"input\")\n (fn (e)\n (if is-checkbox\n (reset! sig (dom-get-prop el \"checked\"))\n (reset! sig (dom-get-prop el \"value\"))))))))" "lisp"))
(p "Platform additions: " (code "dom-set-prop") " and " (code "dom-get-prop") " (property access, not attribute — " (code ".value") " not " (code "getAttribute") "). These go in the boundary as IO primitives."))
@@ -87,7 +87,7 @@
(~docs/subsection :title "Design"
(p "When items have a " (code ":key") " attribute (or a key function), " (code "reactive-list") " should reconcile by key instead of clearing.")
(~docs/code :code (highlight ";; Keyed list — items matched by :key, reused across updates\n(defisland ~reactive-islands/phase2/todo-list ()\n (let ((items (signal (list\n (dict \"id\" 1 \"text\" \"Buy milk\")\n (dict \"id\" 2 \"text\" \"Write spec\")\n (dict \"id\" 3 \"text\" \"Ship it\")))))\n (ul\n (map (fn (item)\n (li :key (get item \"id\")\n (span (get item \"text\"))\n (button :on-click (fn (e) ...)\n \"Remove\")))\n (deref items)))))" "lisp"))
(~docs/code :src (highlight ";; Keyed list — items matched by :key, reused across updates\n(defisland ~reactive-islands/phase2/todo-list ()\n (let ((items (signal (list\n (dict \"id\" 1 \"text\" \"Buy milk\")\n (dict \"id\" 2 \"text\" \"Write spec\")\n (dict \"id\" 3 \"text\" \"Ship it\")))))\n (ul\n (map (fn (item)\n (li :key (get item \"id\")\n (span (get item \"text\"))\n (button :on-click (fn (e) ...)\n \"Remove\")))\n (deref items)))))" "lisp"))
(p "The reconciliation algorithm:")
(ol :class "space-y-1 text-stone-600 list-decimal list-inside text-sm"
@@ -98,7 +98,7 @@
(li "Result: minimum DOM mutations. Focus, scroll, animations preserved.")))
(~docs/subsection :title "Spec changes"
(~docs/code :code (highlight ";; In adapter-dom.sx, replace reactive-list's effect body:\n(define reactive-list\n (fn (map-fn items-sig env ns)\n (let ((marker (create-comment \"island-list\"))\n (key-map (dict)) ;; key → DOM node\n (key-order (list))) ;; current key order\n (effect (fn ()\n (let ((parent (dom-parent marker))\n (items (deref items-sig)))\n (when parent\n (let ((new-map (dict))\n (new-keys (list)))\n ;; Render or reuse each item\n (for-each (fn (item)\n (let ((rendered (render-item map-fn item env ns))\n (key (or (dom-get-attr rendered \"key\")\n (dom-get-data rendered \"key\")\n (identity-key item))))\n (dom-remove-attr rendered \"key\")\n (if (dict-has? key-map key)\n ;; Reuse existing\n (dict-set! new-map key (dict-get key-map key))\n ;; New node\n (dict-set! new-map key rendered))\n (append! new-keys key)))\n items)\n ;; Remove stale nodes\n (for-each (fn (k)\n (when (not (dict-has? new-map k))\n (dom-remove (dict-get key-map k))))\n key-order)\n ;; Reorder to match new-keys\n (let ((cursor marker))\n (for-each (fn (k)\n (let ((node (dict-get new-map k)))\n (when (not (= node (dom-next-sibling cursor)))\n (dom-insert-after cursor node))\n (set! cursor node)))\n new-keys))\n ;; Update state\n (set! key-map new-map)\n (set! key-order new-keys))))))\n marker)))" "lisp"))
(~docs/code :src (highlight ";; In adapter-dom.sx, replace reactive-list's effect body:\n(define reactive-list\n (fn (map-fn items-sig env ns)\n (let ((marker (create-comment \"island-list\"))\n (key-map (dict)) ;; key → DOM node\n (key-order (list))) ;; current key order\n (effect (fn ()\n (let ((parent (dom-parent marker))\n (items (deref items-sig)))\n (when parent\n (let ((new-map (dict))\n (new-keys (list)))\n ;; Render or reuse each item\n (for-each (fn (item)\n (let ((rendered (render-item map-fn item env ns))\n (key (or (dom-get-attr rendered \"key\")\n (dom-get-data rendered \"key\")\n (identity-key item))))\n (dom-remove-attr rendered \"key\")\n (if (dict-has? key-map key)\n ;; Reuse existing\n (dict-set! new-map key (dict-get key-map key))\n ;; New node\n (dict-set! new-map key rendered))\n (append! new-keys key)))\n items)\n ;; Remove stale nodes\n (for-each (fn (k)\n (when (not (dict-has? new-map k))\n (dom-remove (dict-get key-map k))))\n key-order)\n ;; Reorder to match new-keys\n (let ((cursor marker))\n (for-each (fn (k)\n (let ((node (dict-get new-map k)))\n (when (not (= node (dom-next-sibling cursor)))\n (dom-insert-after cursor node))\n (set! cursor node)))\n new-keys))\n ;; Update state\n (set! key-map new-map)\n (set! key-order new-keys))))))\n marker)))" "lisp"))
(p "Falls back to current clear-and-rerender when no keys are present.")))
@@ -110,7 +110,7 @@
(p "A portal renders children into a DOM node " (em "outside") " the island's subtree. Essential for modals, tooltips, dropdown menus, and toast notifications — anything that must escape overflow:hidden, z-index stacking, or layout constraints.")
(~docs/subsection :title "Design"
(~docs/code :code (highlight ";; portal — render children into a target element\n(defisland ~reactive-islands/phase2/modal-trigger ()\n (let ((open? (signal false)))\n (div\n (button :on-click (fn (e) (swap! open? not))\n \"Open Modal\")\n\n ;; Portal: children rendered into #modal-root,\n ;; not into this island's DOM\n (portal \"#modal-root\"\n (when (deref open?)\n (div :class \"fixed inset-0 bg-black/50 flex items-center justify-center\"\n (div :class \"bg-white rounded-lg p-6 max-w-md\"\n (h2 \"Modal Title\")\n (p \"This is rendered outside the island's DOM subtree.\")\n (button :on-click (fn (e) (reset! open? false))\n \"Close\"))))))))" "lisp"))
(~docs/code :src (highlight ";; portal — render children into a target element\n(defisland ~reactive-islands/phase2/modal-trigger ()\n (let ((open? (signal false)))\n (div\n (button :on-click (fn (e) (swap! open? not))\n \"Open Modal\")\n\n ;; Portal: children rendered into #modal-root,\n ;; not into this island's DOM\n (portal \"#modal-root\"\n (when (deref open?)\n (div :class \"fixed inset-0 bg-black/50 flex items-center justify-center\"\n (div :class \"bg-white rounded-lg p-6 max-w-md\"\n (h2 \"Modal Title\")\n (p \"This is rendered outside the island's DOM subtree.\")\n (button :on-click (fn (e) (reset! open? false))\n \"Close\"))))))))" "lisp"))
(p "Implementation in " (code "adapter-dom.sx") ":")
(ol :class "space-y-1 text-stone-600 list-decimal list-inside text-sm"
@@ -132,7 +132,7 @@
(p "When an island's rendering or effect throws, the error currently propagates to the top level and may crash other islands. An error boundary catches the error and renders a fallback UI.")
(~docs/subsection :title "Design"
(~docs/code :code (highlight ";; error-boundary — catch errors in island subtrees\n(defisland ~reactive-islands/phase2/resilient-widget ()\n (error-boundary\n ;; Fallback: shown when children throw\n (fn (err)\n (div :class \"p-4 bg-red-50 border border-red-200 rounded\"\n (p :class \"text-red-700 font-medium\" \"Something went wrong\")\n (p :class \"text-red-500 text-sm\" (error-message err))))\n ;; Children: the happy path\n (do\n (~risky-component)\n (~another-component))))" "lisp"))
(~docs/code :src (highlight ";; error-boundary — catch errors in island subtrees\n(defisland ~reactive-islands/phase2/resilient-widget ()\n (error-boundary\n ;; Fallback: shown when children throw\n (fn (err)\n (div :class \"p-4 bg-red-50 border border-red-200 rounded\"\n (p :class \"text-red-700 font-medium\" \"Something went wrong\")\n (p :class \"text-red-500 text-sm\" (error-message err))))\n ;; Children: the happy path\n (do\n (~risky-component)\n (~another-component))))" "lisp"))
(p "Implementation:")
(ol :class "space-y-1 text-stone-600 list-decimal list-inside text-sm"
@@ -147,11 +147,11 @@
(p "Suspense handles async operations in the render path — data fetching, lazy-loaded components, code splitting. Show a loading placeholder until the async work completes, then swap in the result.")
(~docs/subsection :title "Design"
(~docs/code :code (highlight ";; suspense — async-aware rendering boundary\n(defisland ~reactive-islands/phase2/user-profile (&key user-id)\n (suspense\n ;; Fallback: shown during loading\n (div :class \"animate-pulse\"\n (div :class \"h-4 bg-stone-200 rounded w-3/4\")\n (div :class \"h-4 bg-stone-200 rounded w-1/2 mt-2\"))\n ;; Children: may contain async operations\n (let ((user (await (fetch-json (str \"/api/users/\" user-id)))))\n (div\n (h2 (get user \"name\"))\n (p (get user \"email\"))))))" "lisp"))
(~docs/code :src (highlight ";; suspense — async-aware rendering boundary\n(defisland ~reactive-islands/phase2/user-profile (&key user-id)\n (suspense\n ;; Fallback: shown during loading\n (div :class \"animate-pulse\"\n (div :class \"h-4 bg-stone-200 rounded w-3/4\")\n (div :class \"h-4 bg-stone-200 rounded w-1/2 mt-2\"))\n ;; Children: may contain async operations\n (let ((user (await (fetch-json (str \"/api/users/\" user-id)))))\n (div\n (h2 (get user \"name\"))\n (p (get user \"email\"))))))" "lisp"))
(p "This requires a new primitive concept: a " (strong "resource") " — an async signal that transitions through loading → resolved → error states.")
(~docs/code :code (highlight ";; resource — async signal\n(define resource\n (fn (fetch-fn)\n ;; Returns a signal-like value:\n ;; {:loading true :data nil :error nil} initially\n ;; {:loading false :data result :error nil} on success\n ;; {:loading false :data nil :error err} on failure\n (let ((state (signal (dict \"loading\" true\n \"data\" nil\n \"error\" nil))))\n ;; Kick off the async operation\n (promise-then (fetch-fn)\n (fn (data) (reset! state (dict \"loading\" false\n \"data\" data\n \"error\" nil)))\n (fn (err) (reset! state (dict \"loading\" false\n \"data\" nil\n \"error\" err))))\n state)))" "lisp"))
(~docs/code :src (highlight ";; resource — async signal\n(define resource\n (fn (fetch-fn)\n ;; Returns a signal-like value:\n ;; {:loading true :data nil :error nil} initially\n ;; {:loading false :data result :error nil} on success\n ;; {:loading false :data nil :error err} on failure\n (let ((state (signal (dict \"loading\" true\n \"data\" nil\n \"error\" nil))))\n ;; Kick off the async operation\n (promise-then (fetch-fn)\n (fn (data) (reset! state (dict \"loading\" false\n \"data\" data\n \"error\" nil)))\n (fn (err) (reset! state (dict \"loading\" false\n \"data\" nil\n \"error\" err))))\n state)))" "lisp"))
(p "Suspense is the rendering boundary; resource is the data primitive. Together they give a clean async data story without effects-that-fetch (React's " (code "useEffect") " + " (code "useState") " anti-pattern).")))
@@ -159,7 +159,7 @@
(p "Transitions mark updates as non-urgent. The UI stays interactive during expensive re-renders. React's " (code "startTransition") " defers state updates so that urgent updates (typing, clicking) aren't blocked by slow ones (filtering a large list, rendering a complex subtree).")
(~docs/subsection :title "Design"
(~docs/code :code (highlight ";; transition — non-urgent signal update\n(defisland ~reactive-islands/phase2/search-results (&key items)\n (let ((query (signal \"\"))\n (filtered (signal items))\n (is-pending (signal false)))\n ;; Typing is urgent — updates immediately\n ;; Filtering is deferred — doesn't block input\n (effect (fn ()\n (let ((q (deref query)))\n (transition is-pending\n (fn ()\n (reset! filtered\n (filter (fn (item)\n (contains? (lower (get item \"name\")) (lower q)))\n items)))))))\n (div\n (input :bind query :placeholder \"Search...\")\n (when (deref is-pending)\n (span :class \"text-stone-400\" \"Filtering...\"))\n (ul (map (fn (item) (li (get item \"name\")))\n (deref filtered))))))" "lisp"))
(~docs/code :src (highlight ";; transition — non-urgent signal update\n(defisland ~reactive-islands/phase2/search-results (&key items)\n (let ((query (signal \"\"))\n (filtered (signal items))\n (is-pending (signal false)))\n ;; Typing is urgent — updates immediately\n ;; Filtering is deferred — doesn't block input\n (effect (fn ()\n (let ((q (deref query)))\n (transition is-pending\n (fn ()\n (reset! filtered\n (filter (fn (item)\n (contains? (lower (get item \"name\")) (lower q)))\n items)))))))\n (div\n (input :bind query :placeholder \"Search...\")\n (when (deref is-pending)\n (span :class \"text-stone-400\" \"Filtering...\"))\n (ul (map (fn (item) (li (get item \"name\")))\n (deref filtered))))))" "lisp"))
(p (code "transition") " takes a pending-signal and a thunk. It sets pending to true, schedules the thunk via " (code "requestIdleCallback") " (or " (code "setTimeout 0") " as fallback), then sets pending to false when complete. Signal writes inside the thunk are batched and applied asynchronously.")
(p "This is lower priority because SX's fine-grained updates already avoid the re-render-everything problem that makes transitions critical in React. But for truly large lists or expensive computations, deferral is still valuable.")))

View File

@@ -67,18 +67,18 @@
(p "The existing " (code "renderDOM") " function walks the AST and creates DOM nodes. Inside an island, it becomes signal-aware:")
(~docs/subsection :title "Text bindings"
(~docs/code :code (highlight ";; (span (deref count)) creates:\n;; const text = document.createTextNode(sig.value)\n;; effect(() => text.nodeValue = sig.value)" "lisp"))
(~docs/code :src (highlight ";; (span (deref count)) creates:\n;; const text = document.createTextNode(sig.value)\n;; effect(() => text.nodeValue = sig.value)" "lisp"))
(p "Only the text node updates. The span is untouched."))
(~docs/subsection :title "Attribute bindings"
(~docs/code :code (highlight ";; (div :class (str \"panel \" (if (deref open?) \"visible\" \"hidden\")))\n;; effect(() => div.className = ...)" "lisp")))
(~docs/code :src (highlight ";; (div :class (str \"panel \" (if (deref open?) \"visible\" \"hidden\")))\n;; effect(() => div.className = ...)" "lisp")))
(~docs/subsection :title "Conditional fragments"
(~docs/code :code (highlight ";; (when (deref show?) (~details)) creates:\n;; A marker comment node, then:\n;; effect(() => show ? insert-after(marker, render(~details)) : remove)" "lisp"))
(~docs/code :src (highlight ";; (when (deref show?) (~details)) creates:\n;; A marker comment node, then:\n;; effect(() => show ? insert-after(marker, render(~details)) : remove)" "lisp"))
(p "Equivalent to SolidJS's " (code "Show") " — but falls out naturally from the evaluator."))
(~docs/subsection :title "List rendering"
(~docs/code :code (highlight "(map (fn (item) (li :key (get item \"id\") (get item \"name\")))\n (deref items))" "lisp"))
(~docs/code :src (highlight "(map (fn (item) (li :key (get item \"id\") (get item \"name\")))\n (deref items))" "lisp"))
(p "Keyed elements are reused and reordered. Unkeyed elements are morphed.")))
(~docs/section :title "Status" :id "status"