diff --git a/sx/sx/nav-data.sx b/sx/sx/nav-data.sx index a88937c0..cd273128 100644 --- a/sx/sx/nav-data.sx +++ b/sx/sx/nav-data.sx @@ -63,6 +63,15 @@ (dict :label "Comparisons" :href "/sx/(applications.(cssx.comparison))") (dict :label "Philosophy" :href "/sx/(applications.(cssx.philosophy))"))) +(define reactive-runtime-nav-items (list + (dict :label "Ref" :href "/sx/(applications.(reactive-runtime.ref))") + (dict :label "Foreign FFI" :href "/sx/(applications.(reactive-runtime.foreign))") + (dict :label "State Machines" :href "/sx/(applications.(reactive-runtime.machine))") + (dict :label "Commands" :href "/sx/(applications.(reactive-runtime.commands))") + (dict :label "Render Loop" :href "/sx/(applications.(reactive-runtime.loop))") + (dict :label "Keyed Lists" :href "/sx/(applications.(reactive-runtime.keyed-lists))") + (dict :label "App Shell" :href "/sx/(applications.(reactive-runtime.app-shell))"))) + (define essays-nav-items (list (dict :label "Why S-Expressions" :href "/sx/(etc.(essay.why-sexps))" :summary "Why SX uses s-expressions instead of HTML templates, JSX, or any other syntax.") @@ -243,8 +252,6 @@ :summary "The computational floor — from scoped effects through algebraic effects and delimited continuations to the CEK machine. Why three registers are irreducible, and the three-axis model (depth, topology, linearity).") (dict :label "Deref as Shift" :href "/sx/(etc.(plan.cek-reactive))" :summary "Phase B: replace explicit effect wrapping with implicit continuation capture. Deref inside reactive-reset performs shift, capturing the rest of the expression as the subscriber.") - (dict :label "Reactive Runtime" :href "/sx/(etc.(plan.reactive-runtime))" - :summary "Seven feature layers — ref, foreign FFI, state machines, commands with undo/redo, render loops, keyed lists, client-first app shell. Zero new platform primitives.") (dict :label "Rust/WASM Host" :href "/sx/(etc.(plan.rust-wasm-host))" :summary "Bootstrap the SX spec to Rust, compile to WASM, replace sx-browser.js. Shared platform layer for DOM, phased rollout from parse to full parity.") (dict :label "Isolated Evaluator" :href "/sx/(etc.(plan.isolated-evaluator))" @@ -444,7 +451,8 @@ {:label "SX URLs" :href "/sx/(applications.(sx-urls))"} {:label "CSSX" :href "/sx/(applications.(cssx))" :children cssx-nav-items} {:label "Protocols" :href "/sx/(applications.(protocol))" :children protocols-nav-items} - {:label "sx-pub" :href "/sx/(applications.(sx-pub))"})} + {:label "sx-pub" :href "/sx/(applications.(sx-pub))"} + {:label "Reactive Runtime" :href "/sx/(applications.(reactive-runtime))" :children reactive-runtime-nav-items})} {:label "Etc" :href "/sx/(etc)" :children (list {:label "Essays" :href "/sx/(etc.(essay))" :children essays-nav-items} diff --git a/sx/sx/page-functions.sx b/sx/sx/page-functions.sx index ac7ac1f3..7ad48496 100644 --- a/sx/sx/page-functions.sx +++ b/sx/sx/page-functions.sx @@ -468,6 +468,11 @@ '(~sx-pub/overview-content) nil))) +;; Reactive Runtime (under applications) +;; Convention: ~reactive-runtime/{slug}-content +(define reactive-runtime + (make-page-fn "~reactive-runtime/overview-content" "~reactive-runtime/" nil "-content")) + ;; Essays (under etc) ;; Convention: ~essays/{slug}/essay-{slug} (define essay diff --git a/sx/sx/plans/reactive-runtime.sx b/sx/sx/plans/reactive-runtime.sx deleted file mode 100644 index 8c03cc46..00000000 --- a/sx/sx/plans/reactive-runtime.sx +++ /dev/null @@ -1,209 +0,0 @@ -;; Reactive Application Runtime — 7 Feature Layers -;; Zero new platform primitives. All macros composing existing signals + DOM ops. - -(defcomp ~plans/reactive-runtime/plan-reactive-runtime-content () - (~docs/page :title "Reactive Application Runtime" - - (p :class "text-stone-500 text-sm italic mb-8" - "Seven feature layers that take SX from reactive values to a full reactive application runtime. " - "Every layer is a macro expanding to existing primitives — zero new CEK frames, " - "zero new platform primitives. Proves the existing platform interface is sufficient " - "for any application pattern.") - - ;; ===================================================================== - ;; Motivation - ;; ===================================================================== - - (~docs/section :title "Motivation" :id "motivation" - - (p "SX has signals, computed, effects, batch, islands, lakes, stores, and bridge events — " - "sufficient for reactive documents (forms, toggles, counters, live search). " - "But complex client-heavy apps (drawing tools, editors, games) need structure on top:") - - (table :class "w-full mb-6 text-sm" - (thead - (tr (th :class "text-left p-2" "Have") (th :class "text-left p-2" "Missing"))) - (tbody - (tr (td :class "p-2" "Signal holds a value") (td :class "p-2" "Ref holds a value " (em "without") " reactivity")) - (tr (td :class "p-2" "Effect runs on change") (td :class "p-2" "Loop runs " (em "continuously"))) - (tr (td :class "p-2" "Store shares state") (td :class "p-2" "Machine manages " (em "modal") " state")) - (tr (td :class "p-2" "reset!/swap! update") (td :class "p-2" "Commands update " (em "with history"))) - (tr (td :class "p-2" "DOM rendering") (td :class "p-2" "Foreign calls " (em "any host API"))) - (tr (td :class "p-2" "Server-first hydration") (td :class "p-2" "Client-first " (em "app shell")))))) - - ;; ===================================================================== - ;; Implementation Order - ;; ===================================================================== - - (~docs/section :title "Implementation Order" :id "order" - - (~docs/code :code (highlight - "L0 Ref -> standalone, trivial (~95 LOC)\nL1 Foreign FFI -> standalone, macro over dom-call-method (~140 LOC)\nL5 Keyed Lists -> enhances existing reactive-list (~155 LOC)\nL2 State Machine -> uses signals (~200 LOC)\nL4 Render Loop -> uses refs (L0), existing RAF (~140 LOC)\nL3 Commands -> extends def-store, uses signals (~320 LOC)\nL6 App Shell -> orchestrates all above (~330 LOC)\n Total: ~1380 LOC" - "text")) - - (p "Order rationale: L0 and L1 are independent foundations. L5 enhances existing code. " - "L2 and L4 depend on L0. L3 builds on signals. L6 ties everything together.")) - - ;; ===================================================================== - ;; L0: Ref - ;; ===================================================================== - - (~docs/section :title "Layer 0: Ref (Mutable Box Without Reactivity)" :id "l0-ref" - - (p "A " (code "ref") " is like a signal but with NO subscriber tracking, NO notifications. " - "Just a mutable cell for DOM handles, canvas contexts, timer IDs.") - - (~docs/code :code (highlight - "(ref initial-value) ;; create ref, auto-registers dispose in island scope\n(ref-deref r) ;; read (no tracking)\n(ref-set! r v) ;; write (no notification)\n(ref? x) ;; predicate" - "lisp")) - - (p "Plain dicts with " (code "__ref") " marker (mirrors " (code "__signal") " pattern). " - "On island disposal, auto-nil'd via " (code "register-in-scope") ". ~35 lines of spec.") - - (~docs/subsection :title "Files" - (ul :class "list-disc pl-6 mb-4 space-y-1" - (li (strong "NEW") " " (code "shared/sx/ref/refs.sx") " — ref, ref-deref, ref-set!, ref?") - (li (strong "NEW") " " (code "shared/sx/ref/test-refs.sx") " — tests: create, read, set, predicate, scope disposal") - (li "Add " (code "\"refs\"") " to " (code "SPEC_MODULES") " in both platform files")))) - - ;; ===================================================================== - ;; L1: Foreign - ;; ===================================================================== - - (~docs/section :title "Layer 1: Foreign (Host API Interop)" :id "l1-foreign" - - (p "Clean boundary for calling host APIs (Canvas, WebGL, WebAudio) from SX code. " - "Uses " (strong "existing") " platform primitives — no new ones needed:") - - (ul :class "list-disc pl-6 mb-4 space-y-1" - (li (code "dom-call-method(obj, \"methodName\", args...)") " — method calls") - (li (code "dom-get-prop(obj, \"propName\")") " — property getter") - (li (code "dom-set-prop(obj, \"propName\", value)") " — property setter")) - - (p (code "def-foreign") " is a macro that generates calls to these existing primitives.") - - (~docs/code :code (highlight - "(def-foreign canvas-2d\n (fill-rect x y w h) ;; method call\n (:fill-style color)) ;; property setter (keyword = property)\n\n;; Get host object via existing primitive\n(let ((ctx (dom-call-method canvas \"getContext\" \"2d\")))\n (canvas-2d.fill-rect ctx 0 0 100 100)\n (canvas-2d.fill-style! ctx \"red\"))" - "lisp")) - - (~docs/subsection :title "Macro Expansion" - (~docs/code :code (highlight - "(def-foreign canvas-2d\n (fill-rect x y w h)\n (:fill-style color))\n;; expands to:\n(do\n (define canvas-2d.fill-rect\n (fn (ctx x y w h) (dom-call-method ctx \"fillRect\" x y w h)))\n (define canvas-2d.fill-style!\n (fn (ctx color) (dom-set-prop ctx \"fillStyle\" color))))" - "lisp")) - - (p "Dot notation works because " (code "ident-char?") " includes " (code ".") ". " - "The macro converts SX naming (" (code "fill-rect") ") to host naming (" (code "fillRect") ") via camelCase transform."))) - - ;; ===================================================================== - ;; L5: Keyed Lists - ;; ===================================================================== - - (~docs/section :title "Layer 5: Keyed List Reconciliation" :id "l5-keyed" - - (p "Enhance existing " (code "reactive-list") " (adapter-dom.sx:1000) with explicit " (code ":key") " parameter. " - "Current code already has keyed reconciliation via DOM " (code "key") " attributes — this adds " - "an explicit key extraction callback and stable identity tracking.") - - (~docs/code :code (highlight - "(map (fn (el) (~shape-handle el)) (deref items) :key (fn (el) (get el :id)))" - "lisp")) - - (p "Changes to " (code "render-dom-list") " detection (line 563-575) and " (code "reactive-list") " implementation. " - "No new platform primitives — existing DOM ops suffice.")) - - ;; ===================================================================== - ;; L2: State Machines - ;; ===================================================================== - - (~docs/section :title "Layer 2: State Machines (defmachine)" :id "l2-machine" - - (p "Modal state management for complex UI modes. Machine state IS a signal — " - "composes naturally with computed/effects.") - - (~docs/code :code (highlight - "(defmachine drawing-tool\n :initial :idle\n :states {\n :idle {:on {:pointer-down (fn (ev) {:state :drawing\n :actions [(start-shape! ev)]})}}\n :drawing {:on {:pointer-move (fn (ev) {:state :drawing\n :actions [(update-shape! ev)]})\n :pointer-up (fn (ev) {:state :idle\n :actions [(finish-shape! ev)]})}}})\n\n(machine-send! drawing-tool :pointer-down event)\n(machine-matches? drawing-tool :drawing) ;; reactive via deref\n(machine-state drawing-tool) ;; returns the state signal" - "lisp")) - - (p (code "defmachine") " is a macro expanding to a " (code "let") " with a signal for current state, " - "a transitions dict, and a " (code "send!") " function. Built on signals + dicts.")) - - ;; ===================================================================== - ;; L4: Render Loop - ;; ===================================================================== - - (~docs/section :title "Layer 4: Continuous Rendering Loop (defloop)" :id "l4-loop" - - (p (code "requestAnimationFrame") " integration for canvas/animation apps, " - "with island lifecycle cleanup.") - - (~docs/code :code (highlight - "(defloop render-loop (fn (timestamp dt)\n (let ((ctx (ref-deref ctx-ref)))\n (clear-canvas! ctx)\n (draw-scene! ctx (deref elements)))))\n\n(loop-start! render-loop)\n(loop-stop! render-loop)\n(loop-running? render-loop) ;; reactive signal" - "lisp")) - - (p "Uses the " (strong "running-ref pattern") " to avoid needing " (code "cancelAnimationFrame") ":") - - (~docs/code :code (highlight - "(let ((running (ref true))\n (last-ts (ref 0)))\n (define tick (fn (ts)\n (when (ref-deref running)\n (let ((dt (- ts (ref-deref last-ts))))\n (ref-set! last-ts ts)\n (user-fn ts dt))\n (request-animation-frame tick))))\n (request-animation-frame tick)\n ;; stop: (ref-set! running false) -- loop dies on next frame" - "lisp")) - - (p "Uses existing " (code "request-animation-frame") " (already wired in JS). " - "Auto-stops on island disposal via " (code "register-in-scope") ". " - "Depends on L0 (refs).")) - - ;; ===================================================================== - ;; L3: Commands - ;; ===================================================================== - - (~docs/section :title "Layer 3: Commands with History (Undo/Redo)" :id "l3-commands" - - (p "Command pattern built into signal stores. Commands are s-expressions " - "in a history stack — trivially serializable.") - - (~docs/code :code (highlight - "(def-command-store canvas-state\n :initial {:elements '() :selection nil}\n :commands {\n :add-element (fn (state el)\n (assoc state :elements\n (append (get state :elements) (list el))))\n :move-element (fn (state id dx dy) ...)\n }\n :max-history 100)\n\n(dispatch! canvas-state :add-element rect-1)\n(undo! canvas-state)\n(redo! canvas-state)\n(can-undo? canvas-state) ;; reactive\n(can-redo? canvas-state) ;; reactive\n(group-start! canvas-state \"drag\") ;; transaction grouping\n(group-end! canvas-state)" - "lisp")) - - (p "Macro wraps signal with undo-stack and redo-stack (both signals of lists). " - (code "group-start!") "/" (code "group-end!") " collapses multiple dispatches " - "into one undo entry — essential for drag operations.")) - - ;; ===================================================================== - ;; L6: App Shell - ;; ===================================================================== - - (~docs/section :title "Layer 6: Client-First App Shell (defapp)" :id "l6-app" - - (p "Skip SSR entirely for canvas-heavy apps. Server returns minimal HTML shell, " - "all rendering client-side.") - - (~docs/code :code (highlight - "(defapp excalidraw\n :render :client\n :entry ~drawing-app\n :stores (canvas-state tool-state)\n :routes {\"/\" ~drawing-app\n \"/gallery\" ~gallery-view}\n :head [(link :rel \"stylesheet\" :href \"/static/app.css\")])" - "lisp")) - - (p "Macro generates:") - (ul :class "list-disc pl-6 mb-4 space-y-1" - (li (strong "Server:") " minimal HTML shell (doctype + " (code "
") " + SX loader)") - (li (strong "Client:") " " (code "sx-app-boot") " function using existing " (code "sx-mount") " for initial render"))) - - ;; ===================================================================== - ;; Zero Platform Primitives - ;; ===================================================================== - - (~docs/section :title "Zero New Platform Primitives" :id "zero-primitives" - - (p "All 7 layers are pure " (code ".sx") " macros composing existing primitives:") - - (table :class "w-full mb-6 text-sm" - (thead - (tr (th :class "text-left p-2" "Existing Primitive") (th :class "text-left p-2" "Used By"))) - (tbody - (tr (td :class "p-2" (code "signal, deref, reset!, swap!, computed, effect")) (td :class "p-2" "L2, L3, L4")) - (tr (td :class "p-2" (code "dom-call-method, dom-get-prop, dom-set-prop")) (td :class "p-2" "L1 (Foreign FFI)")) - (tr (td :class "p-2" (code "request-animation-frame")) (td :class "p-2" "L4 (Render Loop)")) - (tr (td :class "p-2" (code "register-in-scope, scope-push!, scope-pop!")) (td :class "p-2" "L0, L4")) - (tr (td :class "p-2" (code "sx-mount, sx-hydrate-islands")) (td :class "p-2" "L6 (App Shell)")) - (tr (td :class "p-2" (code "DOM ops (dom-insert-after, dom-remove, ...)")) (td :class "p-2" "L5 (Keyed Lists)")))) - - (p :class "text-stone-600 italic" - "This validates SX's architecture: the existing platform interface is complete " - "enough for any application pattern.")))) diff --git a/sx/sx/reactive-runtime.sx b/sx/sx/reactive-runtime.sx new file mode 100644 index 00000000..41303a9c --- /dev/null +++ b/sx/sx/reactive-runtime.sx @@ -0,0 +1,327 @@ +;; Reactive Application Runtime — 7 Feature Layers +;; Zero new platform primitives. All functions composing existing signals + DOM ops. +;; Lives under Applications: /(applications.(reactive-runtime)) + +;; --------------------------------------------------------------------------- +;; Overview page +;; --------------------------------------------------------------------------- + +(defcomp ~reactive-runtime/overview-content () + (~docs/page :title "Reactive Application Runtime" + + (p :class "text-stone-500 text-sm italic mb-8" + "Seven feature layers that take SX from reactive documents to a full application runtime. " + "Every layer is a pure SX function composing existing primitives — zero new CEK frames, " + "zero new platform primitives. The existing platform interface is sufficient " + "for any application pattern.") + + ;; ===================================================================== + ;; Motivation + ;; ===================================================================== + + (~docs/section :title "Motivation" :id "motivation" + + (p "SX has signals, computed, effects, batch, islands, lakes, stores, and bridge events — " + "sufficient for reactive documents (forms, toggles, counters, live search). " + "But complex client-heavy apps (drawing tools, editors, games) need structure on top:") + + (table :class "w-full mb-6 text-sm" + (thead + (tr (th :class "text-left p-2" "Have") (th :class "text-left p-2" "Need"))) + (tbody + (tr (td :class "p-2" "Signal holds a value") (td :class "p-2" "Ref holds a value " (em "without") " reactivity")) + (tr (td :class "p-2" "Effect runs on change") (td :class "p-2" "Loop runs " (em "continuously"))) + (tr (td :class "p-2" "Store shares state") (td :class "p-2" "Machine manages " (em "modal") " state")) + (tr (td :class "p-2" "reset!/swap! update") (td :class "p-2" "Commands update " (em "with history"))) + (tr (td :class "p-2" "DOM rendering") (td :class "p-2" "Foreign calls " (em "any host API"))) + (tr (td :class "p-2" "Server-first hydration") (td :class "p-2" "Client-first " (em "app shell")))))) + + ;; ===================================================================== + ;; The Seven Layers + ;; ===================================================================== + + (~docs/section :title "The Seven Layers" :id "layers" + + (table :class "w-full mb-6 text-sm" + (thead + (tr (th :class "text-left p-2" "Layer") (th :class "text-left p-2" "What") (th :class "text-left p-2" "Builds On"))) + (tbody + (tr (td :class "p-2 font-mono" "L0") (td :class "p-2" "Ref — mutable box without reactivity") (td :class "p-2" "register-in-scope")) + (tr (td :class "p-2 font-mono" "L1") (td :class "p-2" "Foreign — host API interop") (td :class "p-2" "host-call, host-get, host-set!")) + (tr (td :class "p-2 font-mono" "L2") (td :class "p-2" "State machines — modal state as signal") (td :class "p-2" "signal, deref, reset!")) + (tr (td :class "p-2 font-mono" "L3") (td :class "p-2" "Commands — undo/redo signal stores") (td :class "p-2" "signal, computed, L0")) + (tr (td :class "p-2 font-mono" "L4") (td :class "p-2" "Render loop — continuous rAF") (td :class "p-2" "L0, request-animation-frame")) + (tr (td :class "p-2 font-mono" "L5") (td :class "p-2" "Keyed lists — explicit key reconciliation") (td :class "p-2" "reactive-list (existing)")) + (tr (td :class "p-2 font-mono" "L6") (td :class "p-2" "App shell — client-first rendering") (td :class "p-2" "sx-mount, all above"))))) + + ;; ===================================================================== + ;; Implementation Plan + ;; ===================================================================== + + (~docs/section :title "Implementation Plan" :id "implementation" + + (p "All seven layers live in a single spec module: " (code "web/reactive-runtime.sx") ". " + "Every layer is a plain " (code "define") " function — no macros, no new special forms. " + "The OCaml evaluator bootstraps them to JavaScript via the standard transpiler pipeline.") + + (~docs/subsection :title "Implementation Order" + (~docs/code :code (highlight + "L0 Ref → standalone, trivial (~35 LOC)\nL1 Foreign FFI → standalone, function factories (~100 LOC)\nL5 Keyed Lists → enhances existing reactive-list (~155 LOC)\nL2 State Machine → uses signals + dicts (~200 LOC)\nL4 Render Loop → uses L0 refs + existing rAF (~140 LOC)\nL3 Commands → extends stores, uses signals (~320 LOC)\nL6 App Shell → orchestrates all above (~330 LOC)\n Total: ~1280 LOC" + "text")) + + (p "L0 and L1 are independent foundations. L5 enhances existing code. " + "L2 and L4 depend on L0. L3 builds on signals. L6 ties everything together.")) + + (~docs/subsection :title "Build Integration" + (p "One new entry in " (code "SPEC_MODULES") " (hosts/javascript/platform.py):") + (~docs/code :code (highlight + "SPEC_MODULES = {\n ...\n \"reactive-runtime\": (\"reactive-runtime.sx\", \"reactive-runtime (application patterns)\"),\n}\nSPEC_MODULE_ORDER = [..., \"reactive-runtime\"]" + "python")) + (p "Auto-included when the " (code "dom") " adapter is present. " + "Depends on " (code "signals") " (loaded first via module ordering).")) + + (~docs/subsection :title "Existing Primitives Used" + (table :class "w-full mb-6 text-sm" + (thead + (tr (th :class "text-left p-2" "Primitive") (th :class "text-left p-2" "Used By"))) + (tbody + (tr (td :class "p-2" (code "signal, deref, reset!, swap!, computed, effect")) (td :class "p-2" "L2, L3, L4")) + (tr (td :class "p-2" (code "host-call, host-get, host-set!")) (td :class "p-2" "L1 (Foreign FFI)")) + (tr (td :class "p-2" (code "request-animation-frame")) (td :class "p-2" "L4 (Render Loop)")) + (tr (td :class "p-2" (code "register-in-scope, scope-push!, scope-pop!")) (td :class "p-2" "L0, L4")) + (tr (td :class "p-2" (code "sx-mount, sx-hydrate-islands")) (td :class "p-2" "L6 (App Shell)")) + (tr (td :class "p-2" (code "DOM ops (dom-insert-after, dom-remove, …)")) (td :class "p-2" "L5 (Keyed Lists)")))) + + (p :class "text-stone-600 italic" + "Zero new platform primitives validates that the existing interface is complete " + "for any application pattern."))))) + + +;; --------------------------------------------------------------------------- +;; L0: Ref +;; --------------------------------------------------------------------------- + +(defcomp ~reactive-runtime/ref-content () + (~docs/page :title "Layer 0: Ref (Mutable Box)" + + (p :class "text-stone-500 text-sm italic mb-8" + "A non-reactive mutable cell for DOM handles, canvas contexts, and timer IDs. " + "Like a signal but with no subscriber tracking and no notifications.") + + (~docs/section :title "API" :id "ref-api" + + (~docs/code :code (highlight + ";; Create a ref — auto-registers disposal in island scope\n(define my-ref (ref initial-value))\n\n;; Read (no tracking, no subscriptions)\n(ref-deref my-ref)\n\n;; Write (no notifications, no re-renders)\n(ref-set! my-ref new-value)\n\n;; Predicate\n(ref? my-ref) ;; => true" + "lisp")) + + (p "Plain dicts with a " (code "__ref") " marker key (mirrors the " (code "__signal") " pattern). " + "On island disposal, the ref value is auto-nilled via " (code "register-in-scope") ".")) + + (~docs/section :title "When to Use Ref vs Signal" :id "ref-vs-signal" + + (table :class "w-full mb-6 text-sm" + (thead + (tr (th :class "text-left p-2" "Use Ref") (th :class "text-left p-2" "Use Signal"))) + (tbody + (tr (td :class "p-2" "DOM element handle") (td :class "p-2" "UI state that triggers re-render")) + (tr (td :class "p-2" "Canvas 2D context") (td :class "p-2" "Form input value")) + (tr (td :class "p-2" "Timer/interval ID") (td :class "p-2" "Counter, toggle, selection")) + (tr (td :class "p-2" "WebSocket connection") (td :class "p-2" "Anything the UI reads via deref"))))) + + (~docs/section :title "Implementation" :id "ref-impl" + + (~docs/code :code (highlight + "(define make-ref (fn (value)\n (dict \"__ref\" true \"value\" value)))\n\n(define ref? (fn (x)\n (and (dict? x) (has-key? x \"__ref\"))))\n\n(define ref (fn (initial-value)\n (let ((r (make-ref initial-value)))\n (register-in-scope (fn () (dict-set! r \"value\" nil)))\n r)))\n\n(define ref-deref (fn (r) (get r \"value\")))\n(define ref-set! (fn (r v) (dict-set! r \"value\" v)))" + "lisp")) + + (p "~35 lines. No dependencies beyond " (code "register-in-scope") " from signals.sx.")))) + + +;; --------------------------------------------------------------------------- +;; L1: Foreign +;; --------------------------------------------------------------------------- + +(defcomp ~reactive-runtime/foreign-content () + (~docs/page :title "Layer 1: Foreign (Host API Interop)" + + (p :class "text-stone-500 text-sm italic mb-8" + "Clean boundary for calling host APIs — Canvas, WebGL, WebAudio, any browser API — " + "from SX code. Function factories with automatic kebab-to-camelCase conversion.") + + (~docs/section :title "API" :id "foreign-api" + + (~docs/code :code (highlight + ";; Function factories — each returns a reusable function\n(define fill-rect (foreign-method \"fill-rect\"))\n(define set-fill-style! (foreign-prop-setter \"fill-style\"))\n(define get-width (foreign-prop-getter \"width\"))\n\n;; Usage — clean SX calls, no string method names\n(let ((ctx (host-call canvas \"getContext\" \"2d\")))\n (set-fill-style! ctx \"red\")\n (fill-rect ctx 0 0 (get-width canvas) 100))" + "lisp")) + + (p "Three factory functions, one helper:") + (ul :class "list-disc pl-6 mb-4 space-y-1" + (li (code "foreign-method") " — wraps " (code "host-call") " with kebab→camel conversion") + (li (code "foreign-prop-getter") " — wraps " (code "host-get")) + (li (code "foreign-prop-setter") " — wraps " (code "host-set!")) + (li (code "kebab->camel") " — " (code "\"fill-rect\"") " → " (code "\"fillRect\"")))) + + (~docs/section :title "Implementation" :id "foreign-impl" + + (~docs/code :code (highlight + "(define kebab->camel (fn (s)\n ;; \"fill-rect\" → \"fillRect\", \"font-size\" → \"fontSize\"\n (let ((parts (split s \"-\"))\n (first-part (first parts))\n (rest-parts (rest parts)))\n (str first-part\n (join \"\" (map (fn (p)\n (str (upper (slice p 0 1)) (slice p 1))) rest-parts))))))\n\n(define foreign-method (fn (method-name)\n (let ((camel (kebab->camel method-name)))\n (fn (obj &rest args)\n (apply host-call (concat (list obj camel) args))))))\n\n(define foreign-prop-getter (fn (prop-name)\n (let ((camel (kebab->camel prop-name)))\n (fn (obj) (host-get obj camel)))))\n\n(define foreign-prop-setter (fn (prop-name)\n (let ((camel (kebab->camel prop-name)))\n (fn (obj val) (host-set! obj camel val)))))" + "lisp")) + + (p "~100 lines. Uses existing host FFI primitives — no new platform interface needed.")))) + + +;; --------------------------------------------------------------------------- +;; L2: State Machines +;; --------------------------------------------------------------------------- + +(defcomp ~reactive-runtime/machine-content () + (~docs/page :title "Layer 2: State Machines" + + (p :class "text-stone-500 text-sm italic mb-8" + "Modal state management where machine state IS a signal. " + "Composes naturally with computed and effects.") + + (~docs/section :title "API" :id "machine-api" + + (~docs/code :code (highlight + "(define drawing-tool (make-machine\n {:initial :idle\n :states {:idle {:on {:pointer-down (fn (ev)\n {:state :drawing\n :actions (list (fn () (start-shape! ev)))})}}\n :drawing {:on {:pointer-move (fn (ev)\n {:state :drawing\n :actions (list (fn () (update-shape! ev)))})\n :pointer-up (fn (ev)\n {:state :idle\n :actions (list (fn () (finish-shape! ev)))})}}}}))\n\n;; Send events\n(machine-send! drawing-tool :pointer-down event)\n\n;; Read state (reactive — triggers re-render)\n(deref (machine-state drawing-tool)) ;; => :drawing\n(machine-matches? drawing-tool :idle) ;; => false" + "lisp")) + + (p (code "make-machine") " returns a dict with a state signal, transitions map, and send function. " + "Handlers return " (code "{:state :new-state :actions [fn ...]}") " — " + "actions are called after the state transition.")) + + (~docs/section :title "Implementation Sketch" :id "machine-impl" + + (~docs/code :code (highlight + "(define make-machine (fn (config)\n (let ((current (signal (get config :initial)))\n (states (get config :states)))\n (dict\n \"__machine\" true\n \"state\" current\n \"states\" states))))\n\n(define machine-state (fn (m) (get m \"state\")))\n\n(define machine-matches? (fn (m s)\n (= (deref (get m \"state\")) s)))\n\n(define machine-send! (fn (m event &rest data)\n (let ((current-state (deref (get m \"state\")))\n (state-config (get (get m \"states\") current-state))\n (handlers (get state-config :on))\n (handler (get handlers event)))\n (when handler\n (let ((result (apply handler data)))\n (when (get result :state)\n (reset! (get m \"state\") (get result :state)))\n (when (get result :actions)\n (for-each (fn (action) (action))\n (get result :actions))))))))" + "lisp")) + + (p "~200 lines with guard conditions, context data, and enter/exit hooks.")))) + + +;; --------------------------------------------------------------------------- +;; L3: Commands with History +;; --------------------------------------------------------------------------- + +(defcomp ~reactive-runtime/commands-content () + (~docs/page :title "Layer 3: Commands with History" + + (p :class "text-stone-500 text-sm italic mb-8" + "Command pattern built into signal stores. Commands are s-expressions " + "in a history stack — trivially serializable, with transaction grouping for drag operations.") + + (~docs/section :title "API" :id "commands-api" + + (~docs/code :code (highlight + "(define canvas-state (make-command-store\n {:initial {:elements (list) :selection nil}\n :commands {:add-element (fn (state el)\n (assoc state :elements\n (append (get state :elements) (list el))))\n :move-element (fn (state id dx dy) ...)}\n :max-history 100}))\n\n;; Dispatch commands\n(cmd-dispatch! canvas-state :add-element rect-1)\n\n;; Undo / redo\n(cmd-undo! canvas-state)\n(cmd-redo! canvas-state)\n\n;; Reactive predicates\n(deref (cmd-can-undo? canvas-state)) ;; => true\n(deref (cmd-can-redo? canvas-state)) ;; => false\n\n;; Transaction grouping — collapses into single undo entry\n(cmd-group-start! canvas-state \"drag\")\n(cmd-dispatch! canvas-state :move-element id 1 0)\n(cmd-dispatch! canvas-state :move-element id 1 0)\n(cmd-dispatch! canvas-state :move-element id 1 0)\n(cmd-group-end! canvas-state)\n;; One undo reverses all three moves" + "lisp"))) + + (~docs/section :title "Internals" :id "commands-internals" + + (p "A command store wraps three signals and a ref:") + (ul :class "list-disc pl-6 mb-4 space-y-1" + (li (strong "state") " signal — current application state") + (li (strong "undo-stack") " signal — list of previous states") + (li (strong "redo-stack") " signal — list of undone states") + (li (strong "group") " ref — current transaction label (nil when not grouping)")) + + (p (code "cmd-dispatch!") " applies the command function to the current state, " + "pushes the old state onto the undo stack, and clears the redo stack. " + "During a group, intermediate states are collapsed so only the pre-group " + "state appears on the undo stack.") + + (p "~320 lines. The most complex layer, but pure signals + dicts.")))) + + +;; --------------------------------------------------------------------------- +;; L4: Render Loop +;; --------------------------------------------------------------------------- + +(defcomp ~reactive-runtime/loop-content () + (~docs/page :title "Layer 4: Continuous Rendering Loop" + + (p :class "text-stone-500 text-sm italic mb-8" + (code "requestAnimationFrame") " integration for canvas and animation apps, " + "with automatic island lifecycle cleanup.") + + (~docs/section :title "API" :id "loop-api" + + (~docs/code :code (highlight + "(define render-loop (make-loop (fn (timestamp dt)\n (let ((ctx (ref-deref ctx-ref)))\n (clear-canvas! ctx)\n (draw-scene! ctx (deref elements))))))\n\n(loop-start! render-loop)\n(loop-stop! render-loop)\n(deref (loop-running? render-loop)) ;; => true (reactive)" + "lisp"))) + + (~docs/section :title "Running-Ref Pattern" :id "loop-pattern" + + (~docs/code :code (highlight + ";; Internal: no cancelAnimationFrame needed\n(let ((running (ref true))\n (last-ts (ref 0)))\n (define tick (fn (ts)\n (when (ref-deref running)\n (let ((dt (- ts (ref-deref last-ts))))\n (ref-set! last-ts ts)\n (user-fn ts dt))\n (request-animation-frame tick))))\n (request-animation-frame tick)\n ;; To stop: (ref-set! running false)\n ;; Loop dies naturally on next frame check" + "lisp")) + + (p "The loop checks a ref each frame. Setting it to " (code "false") " stops the loop " + "without needing " (code "cancelAnimationFrame") ". On island disposal, " + (code "register-in-scope") " auto-stops the loop.") + + (p "~140 lines. Depends on L0 (refs) and " (code "request-animation-frame") + " from web/lib/browser.sx.")))) + + +;; --------------------------------------------------------------------------- +;; L5: Keyed Lists +;; --------------------------------------------------------------------------- + +(defcomp ~reactive-runtime/keyed-lists-content () + (~docs/page :title "Layer 5: Keyed List Reconciliation" + + (p :class "text-stone-500 text-sm italic mb-8" + "Enhances the existing reactive-list with explicit key extraction callbacks " + "for stable identity tracking across updates.") + + (~docs/section :title "API" :id "keyed-api" + + (~docs/code :code (highlight + ";; Current: keys extracted from rendered DOM key attribute\n(map (fn (el) (~shape-handle :key (get el :id) el)) (deref items))\n\n;; Enhanced: explicit key function\n(map (fn (el) (~shape-handle el)) (deref items)\n :key (fn (el) (get el :id)))" + "lisp")) + + (p "The " (code ":key") " parameter provides a function that extracts a stable identity " + "from each item " (em "before") " rendering. This is more efficient than extracting keys " + "from rendered DOM nodes and handles cases where the rendered output doesn't have a " + "natural key attribute.")) + + (~docs/section :title "Changes" :id "keyed-changes" + + (p "Enhancement to existing " (code "adapter-dom.sx") ":") + (ul :class "list-disc pl-6 mb-4 space-y-1" + (li (code "render-dom-list") " detection — parse " (code ":key") " kwarg after " (code "(deref sig)")) + (li (code "reactive-list") " — accept optional " (code "key-fn") ", use instead of " (code "extract-key") " when provided") + (li "Fully backward compatible — without " (code ":key") ", behavior unchanged")) + + (p "~155 lines of changes. No new platform primitives — existing DOM ops suffice.")))) + + +;; --------------------------------------------------------------------------- +;; L6: App Shell +;; --------------------------------------------------------------------------- + +(defcomp ~reactive-runtime/app-shell-content () + (~docs/page :title "Layer 6: Client-First App Shell" + + (p :class "text-stone-500 text-sm italic mb-8" + "Skip SSR entirely for canvas-heavy apps. Server returns a minimal HTML shell; " + "all rendering happens client-side.") + + (~docs/section :title "API" :id "app-api" + + (~docs/code :code (highlight + "(define my-app (make-app\n {:entry ~drawing-app\n :stores (list canvas-state tool-state)\n :routes {\"/\" ~drawing-app\n \"/gallery\" ~gallery-view}\n :head (list (link :rel \"stylesheet\" :href \"/static/app.css\"))}))\n\n;; Server: generate minimal HTML shell\n(app-shell-html my-app)\n;; => ...
...
...\n\n;; Client: boot the app into the root element\n(app-boot my-app (dom-query \"#sx-app-root\"))\n\n;; Client: navigate between routes\n(app-navigate! my-app \"/gallery\")" + "lisp"))) + + (~docs/section :title "What the Server Returns" :id "app-shell" + + (~docs/code :code (highlight + "\n\n\n \n \n\n\n
\n \n\n" + "html")) + + (p "The shell contains no rendered content — just the mount point and loader. " + "Uses existing " (code "sx-mount") " and " (code "sx-hydrate-islands") " from boot.sx.") + + (p "~330 lines. Orchestrates all other layers."))))