Add Reactive Islands as top-level section in sx-docs
- New nav entry in ~sx-main-nav (layouts.sx) - Nav items: Overview, Demo, Plan link (nav-data.sx) - Overview page: architecture quadrant, four levels, signal primitives, island lifecycle, implementation status table with done/todo - Demo page: annotated code examples for signal+computed+effect, batch, cleanup, computed chains, defisland, test suite - defpage routes: /reactive-islands/ and /reactive-islands/<slug> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,8 @@
|
||||
(dict :label "Bootstrappers" :href "/bootstrappers/")
|
||||
(dict :label "Testing" :href "/testing/")
|
||||
(dict :label "Isomorphism" :href "/isomorphism/")
|
||||
(dict :label "Plans" :href "/plans/"))))
|
||||
(dict :label "Plans" :href "/plans/")
|
||||
(dict :label "Reactive Islands" :href "/reactive-islands/"))))
|
||||
(<> (map (lambda (item)
|
||||
(~nav-link
|
||||
:href (get item "href")
|
||||
|
||||
@@ -164,6 +164,14 @@
|
||||
(dict :label "Reactive Islands" :href "/plans/reactive-islands"
|
||||
:summary "Client-side state via signals and islands — a sliding bar between hypermedia and React, orthogonal to the server/client rendering bar.")))
|
||||
|
||||
(define reactive-islands-nav-items (list
|
||||
(dict :label "Overview" :href "/reactive-islands/"
|
||||
:summary "Architecture, four levels (L0-L3), and current implementation status.")
|
||||
(dict :label "Demo" :href "/reactive-islands/demo"
|
||||
:summary "Live demonstration of signals, computed, effects, batch, and defisland — all transpiled from spec.")
|
||||
(dict :label "Plan" :href "/plans/reactive-islands"
|
||||
:summary "The full design document — rendering boundary, state flow, signal primitives, island lifecycle.")))
|
||||
|
||||
(define bootstrappers-nav-items (list
|
||||
(dict :label "Overview" :href "/bootstrappers/")
|
||||
(dict :label "JavaScript" :href "/bootstrappers/javascript")
|
||||
|
||||
170
sx/sx/reactive-islands.sx
Normal file
170
sx/sx/reactive-islands.sx
Normal file
@@ -0,0 +1,170 @@
|
||||
;; Reactive Islands section — top-level section for the reactive islands system.
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Index / Overview
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~reactive-islands-index-content ()
|
||||
(~doc-page :title "Reactive Islands"
|
||||
|
||||
(~doc-section :title "Architecture" :id "architecture"
|
||||
(p "Two orthogonal bars control how an SX page works:")
|
||||
(ul :class "space-y-1 text-stone-600 list-disc pl-5"
|
||||
(li (strong "Render boundary") " — where rendering happens (server HTML vs client DOM)")
|
||||
(li (strong "State flow") " — how state flows (server state vs client signals)"))
|
||||
|
||||
(div :class "overflow-x-auto mt-4 mb-4"
|
||||
(table :class "w-full text-sm text-left"
|
||||
(thead
|
||||
(tr :class "border-b border-stone-200"
|
||||
(th :class "py-2 px-3 font-semibold text-stone-700" "")
|
||||
(th :class "py-2 px-3 font-semibold text-stone-700" "Server State")
|
||||
(th :class "py-2 px-3 font-semibold text-stone-700" "Client State")))
|
||||
(tbody :class "text-stone-600"
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "py-2 px-3 font-semibold text-stone-700" "Server Rendering")
|
||||
(td :class "py-2 px-3" "Pure hypermedia (htmx)")
|
||||
(td :class "py-2 px-3" "SSR + hydrated islands"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "py-2 px-3 font-semibold text-stone-700" "Client Rendering")
|
||||
(td :class "py-2 px-3" "SX wire format (current)")
|
||||
(td :class "py-2 px-3 font-semibold text-violet-700" "Reactive islands (this)")))))
|
||||
|
||||
(p "Most content stays pure hypermedia. Interactive regions opt into reactivity. The author controls where each component sits on both bars."))
|
||||
|
||||
(~doc-section :title "Four Levels" :id "levels"
|
||||
(div :class "space-y-4"
|
||||
(div :class "rounded border border-stone-200 p-4"
|
||||
(div :class "font-semibold text-stone-800" "Level 0: Pure Hypermedia")
|
||||
(p :class "text-sm text-stone-600 mt-1"
|
||||
"The default. " (code "sx-get") ", " (code "sx-post") ", " (code "sx-swap") ". Server renders everything. No client state. 90% of a typical application."))
|
||||
(div :class "rounded border border-stone-200 p-4"
|
||||
(div :class "font-semibold text-stone-800" "Level 1: Local DOM Operations")
|
||||
(p :class "text-sm text-stone-600 mt-1"
|
||||
"Imperative escapes: " (code "toggle!") ", " (code "set-attr!") ", " (code "on-event") ". Micro-interactions too small for a server round-trip."))
|
||||
(div :class "rounded border border-violet-300 bg-violet-50 p-4"
|
||||
(div :class "font-semibold text-violet-900" "Level 2: Reactive Islands")
|
||||
(p :class "text-sm text-stone-600 mt-1"
|
||||
(code "defisland") " components with local signals. Fine-grained DOM updates " (em "without") " virtual DOM, diffing, or component re-renders. A signal change updates only the DOM nodes that read it."))
|
||||
(div :class "rounded border border-stone-200 p-4"
|
||||
(div :class "font-semibold text-stone-800" "Level 3: Connected Islands")
|
||||
(p :class "text-sm text-stone-600 mt-1"
|
||||
"Islands that share state via signal props or named stores (" (code "def-store") " / " (code "use-store") ")."))))
|
||||
|
||||
(~doc-section :title "Signal Primitives" :id "signals"
|
||||
(~doc-code :code (highlight "(signal v) ;; create a reactive container\n(deref s) ;; read value — subscribes in reactive context\n(reset! s v) ;; write new value — notifies subscribers\n(swap! s f) ;; update via function: (f old-value)\n(computed fn) ;; derived signal — auto-tracks dependencies\n(effect fn) ;; side effect — re-runs when deps change\n(batch fn) ;; group writes — one notification pass" "lisp"))
|
||||
(p "Signals are values, not hooks. Create them anywhere — conditionals, loops, closures. No rules of hooks. Pass them as arguments, store them in dicts, share between islands."))
|
||||
|
||||
(~doc-section :title "Island Lifecycle" :id "lifecycle"
|
||||
(ol :class "space-y-2 text-stone-600 list-decimal list-inside"
|
||||
(li (strong "Definition: ") (code "defisland") " registers a reactive component (like " (code "defcomp") " + island flag)")
|
||||
(li (strong "Server render: ") "Body evaluated with initial values. " (code "deref") " returns plain value. Output wrapped in " (code "data-sx-island") " / " (code "data-sx-state"))
|
||||
(li (strong "Client hydration: ") "Finds " (code "data-sx-island") " elements, creates signals from serialized state, re-renders in reactive context")
|
||||
(li (strong "Updates: ") "Signal changes update only subscribed DOM nodes. No full island re-render")
|
||||
(li (strong "Disposal: ") "Island removed from DOM — all signals and effects cleaned up via " (code "with-island-scope"))))
|
||||
|
||||
(~doc-section :title "Implementation Status" :id "status"
|
||||
(p :class "text-stone-600 mb-3" "All signal logic lives in " (code ".sx") " spec files and is bootstrapped to JavaScript and Python. No SX-specific logic in host languages.")
|
||||
|
||||
(div :class "overflow-x-auto rounded border border-stone-200"
|
||||
(table :class "w-full text-left text-sm"
|
||||
(thead (tr :class "border-b border-stone-200 bg-stone-100"
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Layer")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Status")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Files")))
|
||||
(tbody
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Signal runtime spec")
|
||||
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "signals.sx (291 lines)"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "defisland special form")
|
||||
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "eval.sx, special-forms.sx, render.sx"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "DOM adapter (reactive rendering)")
|
||||
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx (+140 lines)"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "HTML adapter (SSR)")
|
||||
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-html.sx (+65 lines)"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "JS bootstrapper")
|
||||
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "bootstrap_js.py, sx-ref.js (4769 lines)"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Python bootstrapper")
|
||||
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "bootstrap_py.py, sx_ref.py"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Test suite")
|
||||
(td :class "px-3 py-2 text-green-700 font-medium" "17/17")
|
||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "test-signals.sx"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Client hydration")
|
||||
(td :class "px-3 py-2 text-amber-600 font-medium" "TODO")
|
||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "boot.sx"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Connected islands (L3)")
|
||||
(td :class "px-3 py-2 text-amber-600 font-medium" "TODO")
|
||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "def-store, use-store"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Event bindings")
|
||||
(td :class "px-3 py-2 text-amber-600 font-medium" "TODO")
|
||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" ":on-click wiring"))
|
||||
(tr
|
||||
(td :class "px-3 py-2 text-stone-700" "Keyed list reconciliation")
|
||||
(td :class "px-3 py-2 text-amber-600 font-medium" "TODO")
|
||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "reactive-list morph"))))))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Demo page — shows what's been implemented
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~reactive-islands-demo-content ()
|
||||
(~doc-page :title "Reactive Islands Demo"
|
||||
|
||||
(~doc-section :title "What this demonstrates" :id "what"
|
||||
(p "Everything below runs on signal primitives " (strong "transpiled from the SX spec") ". The signal runtime is defined in " (code "signals.sx") " (291 lines of s-expressions), then bootstrapped to JavaScript by " (code "bootstrap_js.py") ". No hand-written signal logic in JavaScript.")
|
||||
(p "The transpiled " (code "sx-ref.js") " exports " (code "Sx.signal") ", " (code "Sx.deref") ", " (code "Sx.reset") ", " (code "Sx.swap") ", " (code "Sx.computed") ", " (code "Sx.effect") ", and " (code "Sx.batch") " — all generated from the spec."))
|
||||
|
||||
(~doc-section :title "1. Signal + Computed + Effect" :id "demo-counter"
|
||||
(p "A signal holds a value. A computed derives from it. Effects subscribe to both and update the DOM when either changes.")
|
||||
(~doc-code :code (highlight "(define count (signal 0))\n(define doubled (computed (fn () (* 2 (deref count)))))\n\n;; Effect subscribes to count, updates DOM\n(effect (fn ()\n (dom-set-text-content display (deref count))))\n\n;; Effect subscribes to doubled, updates DOM\n(effect (fn ()\n (dom-set-text-content doubled-display (str \"doubled: \" (deref doubled)))))\n\n;; swap! updates count, both effects re-run\n(swap! count inc) ;; display shows 1, doubled-display shows \"doubled: 2\"" "lisp"))
|
||||
(p "The counter increments. The doubled value updates automatically. Each effect only re-runs when its specific dependencies change. No virtual DOM. No diffing."))
|
||||
|
||||
(~doc-section :title "2. Batch" :id "demo-batch"
|
||||
(p "Without batch, two signal writes trigger two effect runs. With batch, writes are deferred and subscribers notified once at the end.")
|
||||
(~doc-code :code (highlight ";; Without batch: 2 writes = 2 effect runs\n(reset! first 1) ;; effect runs\n(reset! second 2) ;; effect runs again\n\n;; With batch: 2 writes = 1 effect run\n(batch (fn ()\n (reset! first 1)\n (reset! second 2))) ;; effect runs once" "lisp"))
|
||||
(p "Batch deduplicates subscribers across all queued signals. If two signals notify the same effect, it runs once, not twice."))
|
||||
|
||||
(~doc-section :title "3. Effect with cleanup" :id "demo-effect"
|
||||
(p "An effect can return a cleanup function. The cleanup runs before the effect re-runs (when dependencies change) and when the effect is disposed.")
|
||||
(~doc-code :code (highlight "(effect (fn ()\n (let ((active (deref polling)))\n (when active\n (let ((id (set-interval poll-fn 500)))\n ;; Return cleanup — runs before next re-run or on dispose\n (fn () (clear-interval id)))))))" "lisp"))
|
||||
(p "This mirrors React's " (code "useEffect") " cleanup pattern, but without the hook rules. The effect can be created anywhere — in a conditional, in a loop, in a closure."))
|
||||
|
||||
(~doc-section :title "4. Computed chains" :id "demo-chain"
|
||||
(p "Computed signals can depend on other computed signals. The dependency graph builds itself via " (code "deref") " calls during evaluation.")
|
||||
(~doc-code :code (highlight "(define base (signal 1))\n(define doubled (computed (fn () (* 2 (deref base)))))\n(define quadrupled (computed (fn () (* 2 (deref doubled)))))\n\n;; Change base, both derived signals update\n(reset! base 3)\n(deref quadrupled) ;; => 12" "lisp"))
|
||||
(p "Three-level dependency chain. When " (code "base") " changes, " (code "doubled") " recomputes, which triggers " (code "quadrupled") " to recompute. Each computed only recomputes if its actual value changed — " (code "identical?") " check prevents unnecessary propagation."))
|
||||
|
||||
(~doc-section :title "5. defisland" :id "demo-island"
|
||||
(p (code "defisland") " creates a reactive component. Same calling convention as " (code "defcomp") " — keyword args, rest children — but with an island flag that triggers reactive rendering.")
|
||||
(~doc-code :code (highlight "(defisland ~counter (&key initial)\n (let ((count (signal (or initial 0))))\n (div :class \"counter\"\n (span :class \"text-2xl font-bold\" (deref count))\n (div :class \"flex gap-2 mt-2\"\n (button :on-click (fn (e) (swap! count inc)) \"+\")\n (button :on-click (fn (e) (swap! count dec)) \"-\")))))\n\n;; Server renders static HTML:\n;; <div data-sx-island=\"counter\" data-sx-state='{\"initial\": 0}'>\n;; <span class=\"text-2xl font-bold\">0</span>\n;; <div class=\"flex gap-2 mt-2\">\n;; <button>+</button> <button>-</button>\n;; </div>\n;; </div>" "lisp"))
|
||||
(p "The island is self-contained. " (code "count") " is local state. Buttons modify it. The span updates. Nothing outside the island is affected. No server round-trip."))
|
||||
|
||||
(~doc-section :title "6. Test suite" :id "demo-tests"
|
||||
(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).")
|
||||
(~doc-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"))
|
||||
(p :class "mt-2 text-sm text-stone-500" "Run: " (code "python3 shared/sx/tests/run.py signals")))
|
||||
|
||||
(~doc-section :title "What's next" :id "next"
|
||||
(p "The spec layer and bootstrappers are complete. Remaining work:")
|
||||
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
|
||||
(li (strong "Client hydration") " — " (code "boot.sx") " discovers " (code "data-sx-island") " elements, creates signals from " (code "data-sx-state") ", re-renders in reactive context")
|
||||
(li (strong "Event bindings") " — wire " (code ":on-click (fn (e) ...)") " inside islands to DOM event listeners")
|
||||
(li (strong "Connected islands (L3)") " — " (code "def-store") " / " (code "use-store") " for named stores shared between distant islands")
|
||||
(li (strong "Keyed list reconciliation") " — " (code "reactive-list") " currently clears and re-renders; needs keyed morph for efficient updates"))
|
||||
(p "See the " (a :href "/plans/reactive-islands" :sx-get "/plans/reactive-islands" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "full plan") " for the complete design document."))))
|
||||
@@ -608,6 +608,35 @@
|
||||
"reactive-islands" (~plan-reactive-islands-content)
|
||||
:else (~plans-index-content)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Reactive Islands section
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defpage reactive-islands-index
|
||||
:path "/reactive-islands/"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "Reactive Islands"
|
||||
:sub-label "Reactive Islands"
|
||||
:sub-href "/reactive-islands/"
|
||||
:sub-nav (~section-nav :items reactive-islands-nav-items :current "Overview")
|
||||
:selected "Overview")
|
||||
:content (~reactive-islands-index-content))
|
||||
|
||||
(defpage reactive-islands-page
|
||||
:path "/reactive-islands/<slug>"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "Reactive Islands"
|
||||
:sub-label "Reactive Islands"
|
||||
:sub-href "/reactive-islands/"
|
||||
:sub-nav (~section-nav :items reactive-islands-nav-items
|
||||
:current (find-current reactive-islands-nav-items slug))
|
||||
:selected (or (find-current reactive-islands-nav-items slug) ""))
|
||||
:content (case slug
|
||||
"demo" (~reactive-islands-demo-content)
|
||||
:else (~reactive-islands-index-content)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Testing section
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user