New hierarchy: Geography (Reactive Islands, Hypermedia Lakes, Marshes, Isomorphism), Language (Docs, Specs, Bootstrappers, Testing), Applications (CSSX, Protocols), Etc (Essays, Philosophy, Plans). All routes updated to match: /reactive/* → /geography/reactive/*, /docs/* → /language/docs/*, /essays/* → /etc/essays/*, etc. Updates nav-data.sx, all defpage routes, API endpoints, internal links across 43 files. Enhanced find-nav-match for nested group resolution. Also includes: page-helpers-demo sf-total fix (reduce instead of set!), rebootstrapped sx-browser.js and sx_ref.py, defensive slice/rest guards. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
164 lines
14 KiB
Plaintext
164 lines
14 KiB
Plaintext
;; ---------------------------------------------------------------------------
|
|
;; Plan — the full design document (moved from plans section)
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(defcomp ~reactive-islands-plan-content ()
|
|
(~doc-page :title "Reactive Islands Plan"
|
|
|
|
(~doc-section :title "Context" :id "context"
|
|
(p "SX already has a sliding bar for " (em "where") " rendering happens — server-side HTML, SX wire format for client rendering, or any point between. This is the isomorphism bar. It controls the render boundary.")
|
|
(p "There is a second bar, orthogonal to the first: " (em "how state flows.") " On one end, all state lives on the server — every user action is a round-trip, every UI update is a fresh render. This is the htmx model. On the other end, state lives on the client — signals, subscriptions, fine-grained DOM updates without server involvement. This is the React model.")
|
|
(p "These two bars are independent. You can have server-rendered HTML with client state (SSR + hydrated React). You can have client-rendered components with server state (current SX). The combination creates four quadrants:")
|
|
|
|
(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 (Next.js)"))
|
|
(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 plan)")))))
|
|
|
|
(p "Today SX occupies the bottom-left quadrant — client-rendered components with server state. This plan adds the bottom-right: " (strong "reactive islands") " with client-local signals. A page can mix all four quadrants. Most content stays hypermedia. Interactive regions opt into reactivity. The author controls where each component sits on both bars."))
|
|
|
|
(~doc-section :title "The Spectrum" :id "spectrum"
|
|
(p "Four levels of client interactivity. Each is independently valuable. Each is opt-in per component.")
|
|
|
|
(~doc-subsection :title "Level 0: Pure Hypermedia"
|
|
(p "The default. " (code "sx-get") ", " (code "sx-post") ", " (code "sx-swap") ". Server renders everything. Client swaps fragments. No client state. No JavaScript state management. This is where 90% of a typical application should live."))
|
|
|
|
(~doc-subsection :title "Level 1: Local DOM Operations"
|
|
(p "Imperative escape hatches for micro-interactions too small for a server round-trip: toggling a menu, switching a tab, showing a tooltip. " (code "toggle!") ", " (code "set-attr!") ", " (code "on-event") ". No reactive graph. Just do the thing directly."))
|
|
|
|
(~doc-subsection :title "Level 2: Reactive Islands"
|
|
(p (code "defisland") " components with local signals. Fine-grained DOM updates — no virtual DOM, no diffing, no component re-renders. A signal change updates only the DOM nodes that read it. Islands are isolated by default. The server can render their initial state."))
|
|
|
|
(~doc-subsection :title "Level 3: Connected Islands"
|
|
(p "Islands that share state via signal props or named stores (" (code "def-store") " / " (code "use-store") "). Plus event bridges for htmx lake-to-island communication. This is where SX starts to feel like React — but only in the regions that need it. The surrounding page remains hypermedia.")))
|
|
|
|
(~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 "/geography/reactive/event-bridge" :sx-get "/geography/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"
|
|
(div :class "rounded border border-green-200 bg-green-50 p-3"
|
|
(div :class "font-semibold text-green-800" "Swap inside island")
|
|
(p :class "text-sm text-stone-600 mt-1" "Lake content replaced. Signals survive. Effects can rebind to new DOM. User state intact."))
|
|
(div :class "rounded border border-green-200 bg-green-50 p-3"
|
|
(div :class "font-semibold text-green-800" "Swap outside island")
|
|
(p :class "text-sm text-stone-600 mt-1" "Different part of page updated. Island completely unaffected. User state intact."))
|
|
(div :class "rounded border border-amber-200 bg-amber-50 p-3"
|
|
(div :class "font-semibold text-amber-800" "Swap replaces island")
|
|
(p :class "text-sm text-stone-600 mt-1" "Island disposed. Local signals lost. Named stores persist — new island reconnects via use-store."))
|
|
(div :class "rounded border border-stone-200 p-3"
|
|
(div :class "font-semibold text-stone-800" "Full page navigation")
|
|
(p :class "text-sm text-stone-600 mt-1" "Everything cleared. clean slate. clear-stores wipes the registry.")))))
|
|
|
|
(~doc-section :title "Reactive DOM Rendering" :id "reactive-rendering"
|
|
(p "The existing " (code "renderDOM") " function walks the AST and creates DOM nodes. Inside an island, it becomes signal-aware:")
|
|
|
|
(~doc-subsection :title "Text bindings"
|
|
(~doc-code :code (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."))
|
|
|
|
(~doc-subsection :title "Attribute bindings"
|
|
(~doc-code :code (highlight ";; (div :class (str \"panel \" (if (deref open?) \"visible\" \"hidden\")))\n;; effect(() => div.className = ...)" "lisp")))
|
|
|
|
(~doc-subsection :title "Conditional fragments"
|
|
(~doc-code :code (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."))
|
|
|
|
(~doc-subsection :title "List rendering"
|
|
(~doc-code :code (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.")))
|
|
|
|
(~doc-section :title "Status" :id "status"
|
|
(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" "Task")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Status")
|
|
(th :class "px-3 py-2 font-medium text-stone-600" "Description")))
|
|
(tbody
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Signal runtime")
|
|
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
|
(td :class "px-3 py-2 text-stone-700" "signals.sx: signal, deref, reset!, swap!, computed, effect, batch"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Named stores (L3)")
|
|
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
|
(td :class "px-3 py-2 text-stone-700" "signals.sx: def-store, use-store, clear-stores"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Event bridge")
|
|
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
|
(td :class "px-3 py-2 text-stone-700" "signals.sx: emit-event, on-event, bridge-event"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Event bindings")
|
|
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
|
(td :class "px-3 py-2 text-stone-700" "adapter-dom.sx: :on-click (fn ...) → domListen"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "data-sx-emit")
|
|
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
|
(td :class "px-3 py-2 text-stone-700" "orchestration.sx: auto-dispatch custom events from server content"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Client hydration")
|
|
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
|
(td :class "px-3 py-2 text-stone-700" "boot.sx: hydrate-island, dispose-island, post-swap wiring"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Bootstrapping")
|
|
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
|
(td :class "px-3 py-2 text-stone-700" "All functions transpiled to JS and Python, platform primitives implemented"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Island disposal")
|
|
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
|
(td :class "px-3 py-2 text-stone-700" "boot.sx, orchestration.sx: effects/computeds auto-register disposers, pre-swap cleanup"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Reactive list")
|
|
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
|
(td :class "px-3 py-2 text-stone-700" "adapter-dom.sx: map + deref auto-upgrades to reactive-list"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Input binding + keyed lists")
|
|
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
|
(td :class "px-3 py-2 text-stone-700" "adapter-dom.sx: :bind signal, :key attr"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Portals")
|
|
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
|
(td :class "px-3 py-2 text-stone-700" "adapter-dom.sx: portal render-dom form"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Error boundaries")
|
|
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
|
|
(td :class "px-3 py-2 text-stone-700" "adapter-dom.sx: error-boundary render-dom form"))
|
|
(tr :class "border-b border-stone-100"
|
|
(td :class "px-3 py-2 text-stone-700" "Suspense")
|
|
(td :class "px-3 py-2 text-stone-500 font-medium" "N/A")
|
|
(td :class "px-3 py-2 text-stone-700" "covered by existing primitives"))
|
|
(tr
|
|
(td :class "px-3 py-2 text-stone-700" "Transitions")
|
|
(td :class "px-3 py-2 text-stone-500 font-medium" "N/A")
|
|
(td :class "px-3 py-2 text-stone-700" "covered by existing primitives"))))))
|
|
|
|
(~doc-section :title "Design Principles" :id "principles"
|
|
(ol :class "space-y-3 text-stone-600 list-decimal list-inside"
|
|
(li (strong "Islands are opt-in.") " " (code "defcomp") " remains the default. Components are inert unless you choose " (code "defisland") ". No reactive overhead for static content.")
|
|
(li (strong "Signals are values, not hooks.") " Create them anywhere. Pass them as arguments. Store them in dicts. No rules about calling order or conditional creation.")
|
|
(li (strong "Fine-grained, not component-grained.") " A signal change updates the specific DOM node that reads it. The island does not re-render. There is no virtual DOM and no diffing beyond the morph algorithm already in SxEngine.")
|
|
(li (strong "The server is still the authority.") " Islands handle client interactions. The server handles auth, data, routing. The server can push state into islands via OOB swaps. Islands can submit data to the server via " (code "sx-post") ".")
|
|
(li (strong "Spec-first.") " Signal semantics live in " (code "signals.sx") ". Bootstrapped to JS and Python. The same primitives will work in future hosts — Go, Rust, native.")
|
|
(li (strong "No build step.") " Reactive bindings are created at runtime during DOM rendering. No JSX compilation, no Babel transforms, no Vite plugins."))
|
|
|
|
(p :class "mt-4" "The recommendation from the " (a :href "/etc/essays/client-reactivity" :class "text-violet-700 underline" "Client Reactivity") " essay was: \"Tier 4 probably never.\" This plan is what happens when the answer changes. The design avoids every footgun that essay warns about — no useState cascading to useEffect cascading to Context cascading to a state management library. Signals are one primitive. Islands are one boundary. The rest is composition."))))
|
|
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Phase 2 Plan — remaining reactive features
|
|
;; ---------------------------------------------------------------------------
|