URL restructure, 404 page, trailing slash normalization, layout fixes

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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 21:30:18 +00:00
parent e149dfe968
commit 1341c144da
35 changed files with 2305 additions and 438 deletions

View File

@@ -28,7 +28,7 @@
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
(li (strong "Swap inside island: ") "Signals survive. The lake content is replaced but the island's signal closures are untouched. Effects re-bind to new DOM nodes if needed.")
(li (strong "Swap outside island: ") "Signals survive. The island is not affected by swaps to other parts of the page.")
(li (strong "Swap replaces island: ") "Signals are " (em "lost") ". The island is disposed. This is where " (a :href "/reactive-islands/named-stores" :sx-get "/reactive-islands/named-stores" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "named stores") " come in — they persist at page level, surviving island destruction.")))
(li (strong "Swap replaces island: ") "Signals are " (em "lost") ". The island is disposed. This is where " (a :href "/reactive/named-stores" :sx-get "/reactive/named-stores" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "named stores") " come in — they persist at page level, surviving island destruction.")))
(~doc-section :title "Spec" :id "spec"
(p "The event bridge is spec'd in " (code "signals.sx") " (sections 12-13). Three functions:")

View File

@@ -145,7 +145,7 @@
(td :class "px-3 py-2 text-stone-700" "Phase 2 remaining")
(td :class "px-3 py-2 text-stone-500 font-medium" "P2")
(td :class "px-3 py-2 font-mono text-xs text-stone-500"
(a :href "/reactive-islands/phase2" :sx-get "/reactive-islands/phase2" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "Error boundaries + resource + patterns")))
(a :href "/reactive/phase2" :sx-get "/reactive/phase2" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "Error boundaries + resource + patterns")))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Error boundaries")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")

View File

@@ -272,4 +272,329 @@
(p "Islands separated client state from server content. Lakes let server content flow through islands. Marshes dissolve the boundary entirely — the same zone is simultaneously server-authored and reactively interpreted.")
(p "This is the next turn of the Hegelian spiral. The thesis (pure hypermedia) posited the server as sole authority. The antithesis (reactive islands) gave the client its own inner life. The first synthesis (islands + lakes) maintained the boundary between them. The second synthesis (marshes) " (em "sublates the boundary itself") ".")
(p "In a marsh, you can't point to a piece of DOM and say \"this is server territory\" or \"this is client territory.\" It's both. The server sent it. The client transformed it. The server can update it. The client will re-transform it. The signal reads the server data. The server data feeds the signal. Subject and substance are one.")
(p "The practical consequence: an SX application can handle " (em "any") " interaction pattern without breaking its architecture. Pure content → hypermedia. Micro-interactions → L1 DOM ops. Reactive UI → islands. Server slots → lakes. And now, for the places where reactivity and hypermedia must truly merge — marshes."))))
(p "The practical consequence: an SX application can handle " (em "any") " interaction pattern without breaking its architecture. Pure content → hypermedia. Micro-interactions → L1 DOM ops. Reactive UI → islands. Server slots → lakes. And now, for the places where reactivity and hypermedia must truly merge — marshes."))
;; =====================================================================
;; X. Live demos
;; =====================================================================
(~doc-section :title "Live demos" :id "demos"
(p (strong "These are live interactive islands") " — not static code snippets. Click the buttons. Inspect the DOM.")
;; -----------------------------------------------------------------
;; Demo 1: Server content feeds reactive state
;; -----------------------------------------------------------------
(~doc-subsection :title "Demo 1: Hypermedia feeds reactive state"
(p "Click \"Fetch Price\" to hit a real server endpoint. The response is " (em "hypermedia") " — SX content swapped into the page. But a " (code "data-init") " script in the response also writes to the " (code "\"demo-price\"") " store signal. The island's reactive UI — total, savings, price display — updates instantly from the signal change.")
(p "This is the marsh pattern: " (strong "the server response is both content and a signal write") ". Hypermedia and reactivity aren't separate — the same response does both.")
(~demo-marsh-product)
(~doc-code :code (highlight ";; Island with a store-backed price signal\n(defisland ~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 \"/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"))
(~doc-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"))
(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."))
;; -----------------------------------------------------------------
;; Demo 2: Server → Signal (simulated + live)
;; -----------------------------------------------------------------
(~doc-subsection :title "Demo 2: Server writes to signals"
(p "Two separate islands share a named store " (code "\"demo-price\"") ". Island A creates the store and has control buttons. Island B reads it. Signal changes propagate instantly across island boundaries.")
(div :class "space-y-3"
(~demo-marsh-store-writer)
(~demo-marsh-store-reader))
(p :class "mt-3 text-sm text-stone-500" "The \"Flash Sale\" buttons call " (code "(reset! price 14.99)") " — exactly what " (code "data-sx-signal=\"demo-price:14.99\"") " does during morph.")
(div :class "mt-4 rounded border border-stone-200 bg-stone-50 p-3"
(p :class "text-sm font-medium text-stone-700 mb-2" "Server endpoint (ready for morph integration):")
(div :id "marsh-flash-target"
:class "min-h-[2rem]")
(button :class "mt-2 px-3 py-1.5 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
:sx-get "/reactive/api/flash-sale"
:sx-target "#marsh-flash-target"
:sx-swap "innerHTML"
"Fetch from server"))
(~doc-code :code (highlight ";; Island A — creates the store, has control buttons\n(defisland ~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 ~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."))
;; -----------------------------------------------------------------
;; Demo 3: sx-on-settle — post-swap SX evaluation
;; -----------------------------------------------------------------
(~doc-subsection :title "Demo 3: sx-on-settle"
(p "After a swap settles, the trigger element's " (code "sx-on-settle") " attribute is parsed and evaluated as SX. This runs " (em "after") " the content is in the DOM — so you can update reactive state based on what the server returned.")
(p "Click \"Fetch Item\" to load server content. The response is pure hypermedia. But " (code "sx-on-settle") " on the button increments a fetch counter signal " (em "after") " the swap. The counter updates reactively.")
(~demo-marsh-settle)
(~doc-code :code (highlight ";; sx-on-settle runs SX after the swap settles\n(defisland ~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 \"/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."))
;; -----------------------------------------------------------------
;; Demo 4: Signal-bound triggers
;; -----------------------------------------------------------------
(~doc-subsection :title "Demo 4: Signal-bound triggers"
(p "Inside an island, " (em "all") " attributes are reactive — including " (code "sx-get") ". When an attribute value contains " (code "deref") ", the DOM adapter wraps it in an effect that re-sets the attribute when signals change.")
(p "Select a search category. The " (code "sx-get") " URL on the search button changes reactively. Click \"Search\" to fetch from the current endpoint. The URL was computed from the " (code "mode") " signal at render time and updates whenever the mode changes.")
(~demo-marsh-signal-url)
(~doc-code :code (highlight ";; sx-get URL computed from a signal\n(defisland ~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 \"/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."))
;; -----------------------------------------------------------------
;; Demo 5: Reactive view transform
;; -----------------------------------------------------------------
(~doc-subsection :title "Demo 5: Reactive view transform"
(p "A view-mode signal controls how items are displayed. Click \"Fetch Catalog\" to load items from the server, then toggle the view mode. The " (em "same") " data re-renders differently based on client state — no server round-trip for view changes.")
(~demo-marsh-view-transform)
(~doc-code :code (highlight ";; View mode transforms display without refetch\n(defisland ~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 \"/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."))
)))
;; ===========================================================================
;; Live demo islands
;; ===========================================================================
;; Demo 1: Hypermedia feeds reactive state
(defisland ~demo-marsh-product ()
(let ((price (def-store "demo-price" (fn () (signal 19.99))))
(qty (signal 1))
(total (computed (fn () (* (deref price) (deref qty)))))
(savings (computed (fn () (- (* 19.99 (deref qty)) (deref total)))))
(on-sale? (computed (fn () (< (deref price) 19.99)))))
(div :class "rounded-lg border border-stone-200 bg-white p-4 my-4 space-y-4"
;; Product header
(div :class "flex items-center justify-between"
(div
(h4 :class "font-semibold text-stone-800" "Artisan Widget")
(p :class "text-sm text-stone-500" "Hand-crafted by algorithms"))
(div :class "text-right"
(div :class "flex items-center gap-2"
(when (deref on-sale?)
(span :class "px-2 py-0.5 rounded-full bg-rose-100 text-rose-700 text-xs font-bold uppercase" "Sale"))
(span :class "text-2xl font-bold text-stone-800" "$" (deref price)))))
;; Quantity controls
(div :class "flex items-center gap-3"
(span :class "text-sm text-stone-600" "Qty:")
(button :class "w-7 h-7 rounded bg-stone-200 text-stone-700 text-sm font-medium hover:bg-stone-300"
:on-click (fn (e) (swap! qty (fn (q) (max 1 (- q 1))))) "")
(span :class "font-mono text-lg font-bold text-violet-900 w-8 text-center" (deref qty))
(button :class "w-7 h-7 rounded bg-stone-200 text-stone-700 text-sm font-medium hover:bg-stone-300"
:on-click (fn (e) (swap! qty inc)) "+")
(span :class "ml-4 text-sm text-stone-600" "Total:")
(span :class "text-lg font-bold text-emerald-700" "$" (deref total))
(when (> (deref savings) 0)
(span :class "text-sm text-rose-600 font-medium ml-2" "Save $" (deref savings))))
;; Server fetch button — THIS is the marsh pattern
(div :class "border-t border-stone-200 pt-3"
(div :class "flex items-center gap-3"
(button :class "px-4 py-2 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
:sx-get "/reactive/api/flash-sale"
:sx-target "#marsh-server-msg"
:sx-swap "innerHTML"
"Fetch Price from Server")
(span :class "text-xs text-stone-400" "Hits a real endpoint. Response updates the signal."))
;; Server response lands here as hypermedia
(div :id "marsh-server-msg" :class "mt-2 min-h-[2rem]")))))
;; Demo 2: Shared store — simulates data-sx-signal
(defisland ~demo-marsh-store-writer ()
(let ((price (def-store "demo-price" (fn () (signal 19.99)))))
(div :class "rounded-lg border border-amber-200 bg-amber-50 p-4"
(div :class "flex items-center justify-between mb-3"
(div :class "flex items-center gap-2"
(span :class "text-sm font-semibold text-amber-800" "Island A: Price Control")
(span :class "text-xs bg-amber-200 text-amber-700 px-1.5 py-0.5 rounded font-mono" "def-store"))
(span :class "text-2xl font-bold text-stone-800" "$" (deref price)))
(div :class "flex flex-wrap gap-2"
(button :class "px-3 py-1.5 rounded bg-rose-500 text-white text-sm font-medium hover:bg-rose-600"
:on-click (fn (e) (reset! price 14.99))
"⚡$14.99")
(button :class "px-3 py-1.5 rounded bg-rose-500 text-white text-sm font-medium hover:bg-rose-600"
:on-click (fn (e) (reset! price 9.99))
"⚡$9.99")
(button :class "px-3 py-1.5 rounded bg-rose-500 text-white text-sm font-medium hover:bg-rose-600"
:on-click (fn (e) (reset! price 29.99))
"⚡$29.99")
(button :class "px-3 py-1.5 rounded bg-stone-300 text-stone-700 text-sm font-medium hover:bg-stone-400"
:on-click (fn (e) (reset! price 19.99))
"Reset $19.99"))
(p :class "text-xs text-amber-600 mt-2"
"Each button calls " (code "(reset! price ...)") " — simulating " (code "data-sx-signal") " during morph."))))
(defisland ~demo-marsh-store-reader ()
(let ((price (def-store "demo-price" (fn () (signal 19.99)))))
(div :class "rounded-lg border border-emerald-200 bg-emerald-50 p-4"
(div :class "flex items-center justify-between"
(div :class "flex items-center gap-2"
(span :class "text-sm font-semibold text-emerald-800" "Island B: Product Display")
(span :class "text-xs bg-emerald-200 text-emerald-700 px-1.5 py-0.5 rounded font-mono" "use-store"))
(div :class "flex items-center gap-2"
(span :class "text-stone-600" "Current price:")
(span :class "text-2xl font-bold text-stone-800" "$" (deref price))))
(p :class "text-xs text-emerald-600 mt-2"
"Separate island, reads the same store. Signal changes propagate instantly across island boundaries."))))
;; Demo 3: sx-on-settle — post-swap hook
(defisland ~demo-marsh-settle ()
(let ((count (def-store "settle-count" (fn () (signal 0)))))
(div :class "rounded-lg border border-stone-200 bg-white p-4 my-4 space-y-3"
(div :class "flex items-center justify-between"
(div
(h4 :class "font-semibold text-stone-800" "Settle Hook Demo")
(p :class "text-sm text-stone-500" "Server content + client-side counter"))
(div :class "text-right"
(span :class "text-sm text-stone-600" "Fetched: ")
(span :class "text-2xl font-bold text-violet-700" (deref count))
(span :class "text-sm text-stone-600" " times")))
(div :class "flex items-center gap-3"
(button :class "px-4 py-2 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
:sx-get "/reactive/api/settle-data"
:sx-target "#settle-result"
:sx-swap "innerHTML"
:sx-on-settle "(swap! (use-store \"settle-count\") inc)"
"Fetch Item")
(button :class "px-3 py-1.5 rounded bg-stone-200 text-stone-700 text-sm font-medium hover:bg-stone-300"
:on-click (fn (e) (reset! count 0))
"Reset Counter"))
(div :id "settle-result" :class "min-h-[2rem] rounded bg-stone-50 p-2"
(p :class "text-sm text-stone-400 italic" "Nothing fetched yet.")))))
;; Demo 4: Signal-bound URL
(defisland ~demo-marsh-signal-url ()
(let ((mode (signal "products"))
(query (signal "")))
(div :class "rounded-lg border border-stone-200 bg-white p-4 my-4 space-y-3"
(h4 :class "font-semibold text-stone-800" "Signal-Bound URL Demo")
;; Mode selector
(div :class "flex gap-2"
(button :class (computed (fn ()
(str "px-3 py-1.5 rounded text-sm font-medium "
(if (= (deref mode) "products")
"bg-violet-600 text-white" "bg-stone-200 text-stone-700 hover:bg-stone-300"))))
:on-click (fn (e) (reset! mode "products"))
"Products")
(button :class (computed (fn ()
(str "px-3 py-1.5 rounded text-sm font-medium "
(if (= (deref mode) "events")
"bg-emerald-600 text-white" "bg-stone-200 text-stone-700 hover:bg-stone-300"))))
:on-click (fn (e) (reset! mode "events"))
"Events")
(button :class (computed (fn ()
(str "px-3 py-1.5 rounded text-sm font-medium "
(if (= (deref mode) "posts")
"bg-amber-600 text-white" "bg-stone-200 text-stone-700 hover:bg-stone-300"))))
:on-click (fn (e) (reset! mode "posts"))
"Posts"))
;; Search input
(div :class "flex gap-2"
(input :type "text" :bind query
:placeholder "Search..."
:class "flex-1 px-3 py-1.5 rounded border border-stone-300 text-sm focus:outline-none focus:border-violet-400")
(button :class "px-4 py-1.5 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
:sx-get (computed (fn ()
(str "/reactive/api/search/" (deref mode)
"?q=" (deref query))))
:sx-target "#signal-results"
:sx-swap "innerHTML"
"Search"))
;; Current URL display
(p :class "text-xs text-stone-400 font-mono"
"URL: " (computed (fn ()
(str "/reactive/api/search/" (deref mode) "?q=" (deref query)))))
;; Results
(div :id "signal-results" :class "min-h-[3rem] rounded bg-stone-50 p-2"
(p :class "text-sm text-stone-400 italic" "Select a category and search.")))))
;; Demo 5: Reactive view transform
;; The server sends structured data via data-init that writes to a store signal.
;; The view mode signal controls how the data is rendered — no refetch needed.
(defisland ~demo-marsh-view-transform ()
(let ((view (signal "list"))
(items (def-store "catalog-items" (fn () (signal (list))))))
(div :class "rounded-lg border border-stone-200 bg-white p-4 my-4 space-y-3"
(h4 :class "font-semibold text-stone-800" "Reactive View Transform")
;; View toggle
(div :class "flex gap-2"
(button :class (computed (fn ()
(str "px-3 py-1.5 rounded text-sm font-medium "
(if (= (deref view) "list")
"bg-violet-600 text-white" "bg-stone-200 text-stone-700 hover:bg-stone-300"))))
:on-click (fn (e) (reset! view "list"))
"List")
(button :class (computed (fn ()
(str "px-3 py-1.5 rounded text-sm font-medium "
(if (= (deref view) "grid")
"bg-violet-600 text-white" "bg-stone-200 text-stone-700 hover:bg-stone-300"))))
:on-click (fn (e) (reset! view "grid"))
"Grid")
(button :class (computed (fn ()
(str "px-3 py-1.5 rounded text-sm font-medium "
(if (= (deref view) "compact")
"bg-violet-600 text-white" "bg-stone-200 text-stone-700 hover:bg-stone-300"))))
:on-click (fn (e) (reset! view "compact"))
"Compact"))
;; Fetch button — response writes structured data to store via data-init
(div :class "flex items-center gap-3"
(button :class "px-4 py-2 rounded bg-emerald-600 text-white text-sm font-medium hover:bg-emerald-700"
:sx-get "/reactive/api/catalog"
:sx-target "#catalog-msg"
:sx-swap "innerHTML"
"Fetch Catalog")
(span :class "text-xs text-stone-400" "Server sends data + writes to signal"))
;; Server message area
(div :id "catalog-msg" :class "min-h-[1.5rem]")
;; Reactive view — re-renders when view OR items change
(div :class "min-h-[4rem]"
(when (> (len (deref items)) 0)
(cond
;; List view — full details with descriptions
(= (deref view) "list")
(div :class "space-y-2"
(map (fn (item)
(div :class "flex items-center justify-between p-2 rounded bg-stone-50 border border-stone-100"
(div
(span :class "text-sm font-medium text-stone-800" (get item "name"))
(span :class "text-xs text-stone-500 block" (get item "desc")))
(span :class "text-sm font-bold text-emerald-700" "$" (get item "price"))))
(deref items)))
;; Grid view — cards
(= (deref view) "grid")
(div :class "grid grid-cols-2 gap-2"
(map (fn (item)
(div :class "p-3 rounded bg-violet-50 border border-violet-200 text-center"
(p :class "text-sm font-semibold text-stone-800" (get item "name"))
(p :class "text-lg font-bold text-emerald-700" "$" (get item "price"))))
(deref items)))
;; Compact view — names only
:else
(div :class "flex flex-wrap gap-1"
(map (fn (item)
(span :class "px-2 py-0.5 rounded bg-stone-100 text-xs text-stone-700"
(get item "name")))
(deref items)))))))))

View File

@@ -46,7 +46,7 @@
(~doc-section :title "htmx Lakes" :id "lakes"
(p "An htmx lake is server-driven content " (em "inside") " a reactive island. The island provides the reactive boundary; the lake content is swapped via " (code "sx-get") "/" (code "sx-post") " like normal hypermedia.")
(p "This works because signals live in JavaScript closures, not in the DOM. When a swap replaces lake content, the island's signals are unaffected. The lake can communicate back to the island via the " (a :href "/reactive-islands/event-bridge" :sx-get "/reactive-islands/event-bridge" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "event bridge") ".")
(p "This works because signals live in JavaScript closures, not in the DOM. When a swap replaces lake content, the island's signals are unaffected. The lake can communicate back to the island via the " (a :href "/reactive/event-bridge" :sx-get "/reactive/event-bridge" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "event bridge") ".")
(~doc-subsection :title "Navigation scenarios"
(div :class "space-y-3"