Add reactive runtime demos + sx-web federation plan
7 interactive island demos for the reactive runtime layers: - L0 Ref (timer + DOM handle), L1 Foreign (canvas via host-call), L2 Machine (traffic light), L3 Commands (undo/redo), L4 Loop (bouncing ball), L5 Keyed Lists, L6 App Shell Fix OCaml build: add (wrapped false) to lib/dune, remove Sx. qualifiers. Fix JS build: include dom-lib + browser-lib in adapter compilation. New plan: sx-web federated component web — browser nodes via WebTransport, server nodes via IPFS, in-browser authoring, AI composition layer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -130,7 +130,7 @@ def compile_ref_to_js(
|
||||
("render.sx", "render (core)"),
|
||||
("web-forms.sx", "web-forms (defstyle, deftype, defeffect, defrelation)"),
|
||||
]
|
||||
for name in ("parser", "html", "sx", "dom", "engine", "orchestration", "boot"):
|
||||
for name in ("parser", "html", "sx", "dom-lib", "browser-lib", "dom", "engine", "orchestration", "boot"):
|
||||
if name in adapter_set:
|
||||
sx_files.append(ADAPTER_FILES[name])
|
||||
# Use explicit ordering for spec modules (respects dependencies)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module T = Sx.Sx_types
|
||||
module P = Sx.Sx_parser
|
||||
module R = Sx.Sx_ref
|
||||
module T = Sx_types
|
||||
module P = Sx_parser
|
||||
module R = Sx_ref
|
||||
open T
|
||||
|
||||
let () =
|
||||
|
||||
@@ -10,12 +10,7 @@
|
||||
dune exec bin/run_tests.exe -- test-primitives # specific test
|
||||
dune exec bin/run_tests.exe -- --foundation # foundation only *)
|
||||
|
||||
module Sx_types = Sx.Sx_types
|
||||
module Sx_parser = Sx.Sx_parser
|
||||
module Sx_primitives = Sx.Sx_primitives
|
||||
module Sx_runtime = Sx.Sx_runtime
|
||||
module Sx_ref = Sx.Sx_ref
|
||||
module Sx_render = Sx.Sx_render
|
||||
(* Modules accessed directly — library is unwrapped *)
|
||||
|
||||
open Sx_types
|
||||
open Sx_parser
|
||||
@@ -273,7 +268,7 @@ let make_test_env () =
|
||||
| _ -> raise (Eval_error "append!: expected list and value"));
|
||||
|
||||
(* --- HTML Renderer (from sx_render.ml library module) --- *)
|
||||
Sx.Sx_render.setup_render_env env;
|
||||
Sx_render.setup_render_env env;
|
||||
|
||||
(* Stubs needed by adapter-html.sx when loaded at test time *)
|
||||
bind "set-render-active!" (fun _args -> Nil);
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
(library
|
||||
(name sx))
|
||||
(name sx)
|
||||
(wrapped false))
|
||||
|
||||
@@ -257,7 +257,9 @@
|
||||
(dict :label "Isolated Evaluator" :href "/sx/(etc.(plan.isolated-evaluator))"
|
||||
:summary "Core/application split, shared sx-platform.js, isolated JS evaluator, Rust WASM via handle table. Only language-defining spec gets bootstrapped; everything else is runtime-evaluated .sx.")
|
||||
(dict :label "Mother Language" :href "/sx/(etc.(plan.mother-language))"
|
||||
:summary "SX as its own compiler. OCaml as substrate (closest to CEK), Koka as alternative (compile-time linearity), ultimately self-hosting. One language, every target.")))
|
||||
:summary "SX as its own compiler. OCaml as substrate (closest to CEK), Koka as alternative (compile-time linearity), ultimately self-hosting. One language, every target.")
|
||||
(dict :label "sx-web" :href "/sx/(etc.(plan.sx-web))"
|
||||
:summary "Federated component web. Browser nodes via WebTransport, server nodes via IPFS, content-addressed SX verified by CID. In-browser editing, testing, publishing. AI composition over the federated graph.")))
|
||||
|
||||
(define reactive-examples-nav-items (list
|
||||
{:label "Counter" :href "/sx/(geography.(reactive.(examples.counter)))"}
|
||||
|
||||
337
sx/sx/plans/sx-web.sx
Normal file
337
sx/sx/plans/sx-web.sx
Normal file
@@ -0,0 +1,337 @@
|
||||
;; sx-web: Federated Component Web
|
||||
;; Browser-native peer network with content-addressed SX components.
|
||||
;; Builds on sx-pub (phases 1-4 complete: publishing, federation, anchoring, IPFS).
|
||||
|
||||
(defcomp ~plans/sx-web/plan-sx-web-content ()
|
||||
(~docs/page :title "sx-web: Federated Component Web"
|
||||
|
||||
(p :class "text-stone-500 text-sm italic mb-8"
|
||||
"A two-tier peer network where server nodes pin to IPFS and browser tabs "
|
||||
"participate via WebTransport. Components, tests, and content are all "
|
||||
"content-addressed SX expressions verified by CID regardless of source. "
|
||||
"Builds on sx-pub phases 1–4 (publishing, federation, anchoring, IPFS).")
|
||||
|
||||
;; =====================================================================
|
||||
;; Architecture
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Architecture" :id "architecture"
|
||||
|
||||
(p "Two tiers, one logical network. Trust anchored on CIDs, not sources.")
|
||||
|
||||
(table :class "w-full mb-6 text-sm"
|
||||
(thead
|
||||
(tr (th :class "text-left p-2" "") (th :class "text-left p-2" "Server Node") (th :class "text-left p-2" "Browser Node")))
|
||||
(tbody
|
||||
(tr (td :class "p-2 font-medium" "Runtime") (td :class "p-2" "OCaml + Python") (td :class "p-2" "JavaScript (sx-browser.js)"))
|
||||
(tr (td :class "p-2 font-medium" "Storage") (td :class "p-2" "IPFS daemon + PostgreSQL") (td :class "p-2" "IndexedDB + Cache API"))
|
||||
(tr (td :class "p-2 font-medium" "IPFS") (td :class "p-2" "Full DHT peer, pins content") (td :class "p-2" "Verifies CIDs, caches locally"))
|
||||
(tr (td :class "p-2 font-medium" "Transport") (td :class "p-2" "HTTP/3 + WebTransport server") (td :class "p-2" "WebTransport client"))
|
||||
(tr (td :class "p-2 font-medium" "Availability") (td :class "p-2" "Long-lived, always on") (td :class "p-2" "Ephemeral, online when open"))
|
||||
(tr (td :class "p-2 font-medium" "Trust") (td :class "p-2" "CID verification + blockchain anchor") (td :class "p-2" "CID verification (same guarantee)"))))
|
||||
|
||||
(p "A component fetched from IPFS, from a server, from another browser tab, "
|
||||
"or from local IndexedDB cache is verified the same way: hash it, check the CID. "
|
||||
"The transport is irrelevant to the trust model."))
|
||||
|
||||
;; =====================================================================
|
||||
;; Phase 1: Browser Node Foundation
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Phase 1: Browser Node Foundation" :id "phase-1"
|
||||
|
||||
(p :class "text-stone-600 italic mb-4"
|
||||
"Make the browser a first-class participant, not just a viewer.")
|
||||
|
||||
(~docs/subsection :title "1a. Content Store (IndexedDB)"
|
||||
(p "Browser-local content-addressed store. Same interface as server-side "
|
||||
(code "content-put") "/" (code "content-get") " but backed by IndexedDB.")
|
||||
|
||||
(~docs/code :src (highlight
|
||||
";; Browser content store — pure SX, platform provides IndexedDB ops\n(define browser-store (make-content-store\n {:backend :indexeddb\n :db-name \"sx-web\"\n :verify true})) ;; always verify CID on read\n\n;; Same API as server store\n(content-put browser-store sx-source) ;; → CID\n(content-get browser-store cid) ;; → SX source | nil\n(content-has? browser-store cid) ;; → bool\n(content-pin browser-store cid) ;; mark as persistent\n(content-unpin browser-store cid) ;; allow eviction"
|
||||
"lisp"))
|
||||
|
||||
(p "Two IndexedDB object stores:")
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li (strong "blobs") " — CID → SX source bytes (content-addressed, immutable)")
|
||||
(li (strong "pins") " — CID → {pinned, last-accessed, size, source-node} (eviction metadata)"))
|
||||
|
||||
(p "Unpinned content evicted LRU when storage exceeds quota. "
|
||||
"Pinned content survives until explicitly unpinned."))
|
||||
|
||||
(~docs/subsection :title "1b. Service Worker"
|
||||
(p "Intercepts fetch requests to resolve CIDs locally before hitting the network.")
|
||||
|
||||
(~docs/code :src (highlight
|
||||
";; Service Worker routes\n;;\n;; /sx-web/cid/<cid> → check IndexedDB first, then fetch from home node\n;; /sx-web/resolve/<path> → resolve path→CID via local index, then fetch\n;; /sx-web/eval/<cid> → fetch + evaluate, return rendered result\n;;\n;; Cache strategy:\n;; CID routes are immutable — cache forever once verified\n;; Path routes check freshness against home node"
|
||||
"text"))
|
||||
|
||||
(p "The Service Worker makes the browser work offline for all pinned components. "
|
||||
"Online, it deduplicates fetches across tabs — one IndexedDB, shared cache."))
|
||||
|
||||
(~docs/subsection :title "1c. CID Verification in JavaScript"
|
||||
(p "SHA3-256 hashing in the browser for CID verification. "
|
||||
"Uses SubtleCrypto API (hardware-accelerated on modern browsers).")
|
||||
|
||||
(~docs/code :src (highlight
|
||||
";; New platform primitive for browser host\n(define content-hash\n (fn (source)\n (host-call (host-get (host-global \"crypto\") \"subtle\")\n \"digest\" \"SHA-256\" (encode-utf8 source))))\n\n;; Verification: hash content, compare to claimed CID\n(define verify-cid\n (fn (cid source)\n (= cid (content-hash source))))"
|
||||
"lisp"))
|
||||
|
||||
(p "One new platform primitive: " (code "content-hash") ". "
|
||||
"Everything else (verification, pinning, eviction) is pure SX."))
|
||||
|
||||
(~docs/subsection :title "1d. Deliverables"
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li (code "web/lib/content-store.sx") " — browser content store (~120 LOC)")
|
||||
(li (code "web/lib/sw-routes.sx") " — Service Worker CID routing (~80 LOC)")
|
||||
(li "Platform primitive: " (code "content-hash") " in JS host (~15 LOC)")
|
||||
(li "IndexedDB schema: blobs + pins stores"))))
|
||||
|
||||
;; =====================================================================
|
||||
;; Phase 2: WebTransport Peer Layer
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Phase 2: WebTransport Peer Layer" :id "phase-2"
|
||||
|
||||
(p :class "text-stone-600 italic mb-4"
|
||||
"Bidirectional streams between browsers and servers. The protocol is SX.")
|
||||
|
||||
(~docs/subsection :title "2a. SX Protocol Definition"
|
||||
(p "The protocol is an SX component. Nodes fetch, evaluate, and speak it.")
|
||||
|
||||
(~docs/code :src (highlight
|
||||
"(defprotocol sx-sync \"0.1\"\n :messages {\n ;; Content exchange\n :have (fn (cids) ...) ;; I have these CIDs\n :want (fn (cids) ...) ;; I need these CIDs\n :provide (fn (cid source) ...) ;; Here's the content\n :reject (fn (cid reason) ...) ;; Can't provide\n\n ;; Discovery\n :announce (fn (cid metadata) ...) ;; New content published\n :query (fn (pattern) ...) ;; Search for components\n :results (fn (matches) ...) ;; Search results\n\n ;; Identity\n :hello (fn (node-id pubkey) ...) ;; Handshake\n :verify (fn (challenge sig) ...) ;; Prove identity\n }\n\n :flow {\n ;; Content resolution\n :want → :provide | :reject\n ;; Publishing\n :announce → :want (from interested peers)\n ;; Connection\n :hello → :verify → ready\n })"
|
||||
"lisp"))
|
||||
|
||||
(p "The protocol definition is itself content-addressed. Nodes pin the protocol "
|
||||
"version they speak. Protocol upgrades are published to sx-pub like any component."))
|
||||
|
||||
(~docs/subsection :title "2b. WebTransport Server (Server Nodes)"
|
||||
(p "HTTP/3 WebTransport endpoint on server nodes. "
|
||||
"Browsers connect and exchange SX messages over bidirectional streams.")
|
||||
|
||||
(~docs/code :src (highlight
|
||||
";; Server-side: WebTransport handler\n;;\n;; One stream per conversation:\n;; Browser → Server: (want (list \"Qm..abc\" \"Qm..def\"))\n;; Server → Browser: (provide \"Qm..abc\" \"(defcomp ...)\")\n;; Server → Browser: (provide \"Qm..def\" \"(defisland ...)\")\n;;\n;; Push notifications:\n;; Server → Browser: (announce \"Qm..xyz\" {:author \"...\" :type :component})\n;;\n;; Peer introduction:\n;; Server → Browser: (peers (list {:id \"...\" :transport \"wt://...\"}))"
|
||||
"text"))
|
||||
|
||||
(p "Server nodes also relay between browsers that can't connect directly. "
|
||||
"A browser publishes through its home node, which delivers to followers "
|
||||
"over existing sx-pub federation " (em "and") " pushes to connected browser peers."))
|
||||
|
||||
(~docs/subsection :title "2c. WebTransport Client (Browser Nodes)"
|
||||
(p "Browser connects to home node on page load. Receives push updates. "
|
||||
"Can also connect to peer nodes for direct exchange.")
|
||||
|
||||
(~docs/code :src (highlight
|
||||
";; Browser-side: WebTransport client\n(define sx-peer\n (fn (url)\n (let ((wt (host-call (host-global \"WebTransport\") \"new\" url))\n (ready (host-get wt \"ready\")))\n (promise-then ready\n (fn ()\n ;; Send hello with identity\n (peer-send wt (sx-hello (node-id) (node-pubkey)))\n ;; Listen for incoming messages\n (peer-listen wt (fn (msg)\n (dispatch-protocol msg))))))))\n\n;; Connect to home node on boot\n(sx-peer (str \"https://\" home-node \"/sx-web/wt\"))"
|
||||
"lisp"))
|
||||
|
||||
(p "Three new platform primitives for the browser host:")
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li (code "wt-connect") " — open WebTransport session")
|
||||
(li (code "wt-send") " — send SX expression on stream")
|
||||
(li (code "wt-listen") " — register handler for incoming messages")))
|
||||
|
||||
(~docs/subsection :title "2d. Deliverables"
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li (code "web/lib/sx-sync.sx") " — protocol definition (~200 LOC)")
|
||||
(li (code "web/lib/peer.sx") " — browser peer client (~150 LOC)")
|
||||
(li "Server: WebTransport endpoint in Quart/Hypercorn (~200 LOC Python)")
|
||||
(li "Platform primitives: " (code "wt-connect, wt-send, wt-listen") " (~50 LOC JS)")
|
||||
(li "Peer relay logic on server nodes (~100 LOC Python)"))))
|
||||
|
||||
;; =====================================================================
|
||||
;; Phase 3: In-Browser Authoring
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Phase 3: In-Browser Authoring" :id "phase-3"
|
||||
|
||||
(p :class "text-stone-600 italic mb-4"
|
||||
"Edit, preview, test, and publish from a browser tab. No local dev environment.")
|
||||
|
||||
(~docs/subsection :title "3a. Live Editor"
|
||||
(p "SX editor component with real-time preview. "
|
||||
"Edit source on the left, rendered output on the right. "
|
||||
"The editor is itself an SX island.")
|
||||
|
||||
(~docs/code :src (highlight
|
||||
"(defisland ~sx-web/editor (&key initial-source)\n (let ((source (signal (or initial-source \"\")))\n (parsed (computed (fn () (try-parse (deref source)))))\n (preview (computed (fn () (try-render (deref parsed))))))\n (div :class \"flex h-full\"\n ;; Source editor (textarea or CodeMirror via foreign FFI)\n (div :class \"flex-1\"\n (textarea :bind source :class \"font-mono ...\"))\n ;; Live preview\n (div :class \"flex-1 border-l\"\n (if (get (deref preview) \"error\")\n (div :class \"text-red-600\" (get (deref preview) \"error\"))\n (div (get (deref preview) \"result\")))))))"
|
||||
"lisp"))
|
||||
|
||||
(p "The evaluator runs client-side. No server round-trip for preview. "
|
||||
"Syntax errors, type errors, and render errors show inline as you type."))
|
||||
|
||||
(~docs/subsection :title "3b. In-Browser Test Runner"
|
||||
(p "Tests are SX expressions. The browser evaluator runs them directly.")
|
||||
|
||||
(~docs/code :src (highlight
|
||||
";; Tests travel with components\n(deftest ~my-component/tests\n (test \"renders title\"\n (let ((html (render-to-html (~my-component :title \"Hello\"))))\n (assert-contains html \"Hello\")))\n\n (test \"handles empty props\"\n (let ((html (render-to-html (~my-component))))\n (assert-contains html \"Untitled\"))))\n\n;; Run in browser — same evaluator, same results\n(run-tests ~my-component/tests)\n;; => {:passed 2 :failed 0 :errors ()}"
|
||||
"lisp"))
|
||||
|
||||
(p "Component + tests + docs are published as a single unit to sx-pub. "
|
||||
"Any node can re-run the tests to verify the component works."))
|
||||
|
||||
(~docs/subsection :title "3c. Publish Flow"
|
||||
(p "From browser, one action: edit → test → publish → federate.")
|
||||
|
||||
(~docs/code :src (highlight
|
||||
";; Browser publish flow\n(define publish!\n (fn (source)\n (let ((cid (content-hash source))\n (tests (extract-tests source))\n (results (run-tests tests)))\n (when (all-passed? results)\n ;; Pin locally\n (content-pin browser-store cid)\n ;; Push to home node via WebTransport\n (peer-send home-peer\n (sx-publish cid source\n {:tests results\n :requires (extract-requires source)}))\n ;; Home node pins to IPFS + anchors + federates\n cid))))"
|
||||
"lisp"))
|
||||
|
||||
(p "The browser handles editing, testing, hashing, and local pinning. "
|
||||
"The home server handles IPFS pinning, blockchain anchoring, and federation delivery. "
|
||||
"Clean split: browsers create, servers distribute."))
|
||||
|
||||
(~docs/subsection :title "3d. Deliverables"
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li (code "web/lib/editor.sx") " — live editor island (~300 LOC)")
|
||||
(li (code "web/lib/test-runner.sx") " — browser test runner (~150 LOC)")
|
||||
(li (code "web/lib/publish.sx") " — publish flow (~100 LOC)")
|
||||
(li "Test extraction from SX source (find deftest forms)"))))
|
||||
|
||||
;; =====================================================================
|
||||
;; Phase 4: Component Discovery & Resolution
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Phase 4: Component Discovery & Resolution" :id "phase-4"
|
||||
|
||||
(p :class "text-stone-600 italic mb-4"
|
||||
"Find, resolve, and depend on components across the federation.")
|
||||
|
||||
(~docs/subsection :title "4a. Dependency Resolution"
|
||||
(p "Components declare dependencies via " (code ":requires") ". "
|
||||
"Resolution checks local cache first, then home node, then federation.")
|
||||
|
||||
(~docs/code :src (highlight
|
||||
";; Component with declared dependencies\n(defcomp ~charts/bar-chart (&key data labels)\n :requires ((~cssx/tw \"Qm..css\") ;; pinned by CID\n (~charts/axis \"Qm..axis\") ;; specific version\n (~charts/tooltip :latest)) ;; resolve latest from publisher\n ...)\n\n;; Resolution order:\n;; 1. Local IndexedDB (by CID)\n;; 2. Service Worker cache\n;; 3. Home node (WebTransport :want)\n;; 4. Federation query (WebTransport :query)\n;; 5. IPFS gateway (HTTP fallback)"
|
||||
"lisp"))
|
||||
|
||||
(p "CID dependencies are immutable — resolve once, cache forever. "
|
||||
(code ":latest") " dependencies check the publisher's feed for the current CID."))
|
||||
|
||||
(~docs/subsection :title "4b. Component Browser"
|
||||
(p "Browse, search, and preview components from the federated network. "
|
||||
"Each component renders its own preview because it IS executable SX.")
|
||||
|
||||
(~docs/code :src (highlight
|
||||
";; Search the federation\n(peer-send home-peer\n (sx-query {:type :component\n :tags [\"chart\" \"visualization\"]\n :has-tests true}))\n\n;; Results include CID, metadata, AND the component itself\n;; Click to preview — evaluates client-side\n;; Click to pin — stores in IndexedDB\n;; Click to fork — opens in editor with source"
|
||||
"lisp"))
|
||||
|
||||
(p "The component browser is itself an SX component published to sx-pub."))
|
||||
|
||||
(~docs/subsection :title "4c. Deliverables"
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li (code "web/lib/resolver.sx") " — dependency resolution chain (~200 LOC)")
|
||||
(li (code "sx/sx/tools/component-browser.sx") " — federated browser (~400 LOC)")
|
||||
(li (code "sx/sx/tools/search.sx") " — search interface (~150 LOC)")
|
||||
(li "Server: search index over pinned components"))))
|
||||
|
||||
;; =====================================================================
|
||||
;; Phase 5: Node Bootstrap
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Phase 5: Node Bootstrap" :id "phase-5"
|
||||
|
||||
(p :class "text-stone-600 italic mb-4"
|
||||
"Standing up a new node: one command, minutes to first render.")
|
||||
|
||||
(~docs/subsection :title "5a. Server Node Bootstrap"
|
||||
(~docs/code :src (highlight
|
||||
"# One command. Pulls Docker image, starts all services.\ndocker run -d --name sx-node \\\n -e SX_SEED_PEER=pub.sx-web.org \\\n -e SX_DOMAIN=my-node.example.com \\\n -p 443:443 \\\n ghcr.io/rose-ash/sx-node:latest\n\n# What happens:\n# 1. IPFS daemon starts, connects to network\n# 2. Fetches spec CIDs from seed peer\n# 3. Evaluates specs — now has a working SX evaluator\n# 4. Follows seed peer — receives component announcements\n# 5. Caddy auto-provisions TLS via Let's Encrypt\n# 6. WebTransport endpoint live\n# 7. Ready to serve, publish, and federate"
|
||||
"bash"))
|
||||
|
||||
(p "The bootstrap IS the proof: if your node can fetch specs from the network, "
|
||||
"evaluate them, and render components — the system works. "
|
||||
"No manual configuration beyond domain name and seed peer."))
|
||||
|
||||
(~docs/subsection :title "5b. Browser Node Bootstrap"
|
||||
(~docs/code :src (highlight
|
||||
";; Visit any sx-web node in a browser\n;; → sx-browser.js loads (evaluator + signals + DOM adapter)\n;; → Service Worker installs (CID caching)\n;; → WebTransport connects to host node\n;; → Spec CIDs pinned to IndexedDB\n;; → You're a node.\n;;\n;; No install. No signup. No CLI.\n;; Close the tab → ephemeral node disappears\n;; Reopen → reconnects, IndexedDB still has your pins"
|
||||
"text"))
|
||||
|
||||
(p "A browser becomes a node by visiting a URL. "
|
||||
"Leaving keeps your pins. Returning restores your state. "
|
||||
"The barrier to entry is typing an address."))
|
||||
|
||||
(~docs/subsection :title "5c. Deliverables"
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li "Docker image: " (code "sx-node") " with OCaml host, IPFS, Caddy, PostgreSQL")
|
||||
(li "Bootstrap script: fetch specs, verify CIDs, initialize DB")
|
||||
(li "Seed peer protocol: advertise available specs + popular components")
|
||||
(li "Browser auto-bootstrap: Service Worker install + spec pinning on first visit"))))
|
||||
|
||||
;; =====================================================================
|
||||
;; Phase 6: AI Composition Layer
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Phase 6: AI Composition Layer" :id "phase-6"
|
||||
|
||||
(p :class "text-stone-600 italic mb-4"
|
||||
"The federated graph is simultaneously training data, retrieval context, and composition target.")
|
||||
|
||||
(~docs/subsection :title "6a. Component Retrieval"
|
||||
(p "AI resolves components by CID, not by guessing. "
|
||||
"The federation is a typed, tested, searchable context window.")
|
||||
|
||||
(~docs/code :src (highlight
|
||||
";; AI composition query\n(ai-compose\n {:intent \"dashboard with sales chart and user table\"\n :constraints {:has-tests true\n :min-pins 10 ;; popularity signal\n :verified true} ;; blockchain-anchored\n :context (deref current-page)})\n\n;; AI doesn't generate from scratch — it assembles:\n;; 1. Search federation for matching components\n;; 2. Check tests pass for each candidate\n;; 3. Resolve dependencies\n;; 4. Compose into page, respecting :requires\n;; 5. Output is auditable: list of CIDs + composition logic"
|
||||
"lisp"))
|
||||
|
||||
(p "The output is a composition of existing, tested, content-addressed components. "
|
||||
"Not generated code — assembled code. Deterministic, verifiable, traceable."))
|
||||
|
||||
(~docs/subsection :title "6b. Self-Improving Tooling"
|
||||
(p "AI tools are SX components published to sx-pub. "
|
||||
"They improve as the network grows — more components to compose from, "
|
||||
"more tests to validate against, more usage patterns to learn from.")
|
||||
|
||||
(p "The network effect works for both humans and AI:")
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li "Every component published is more material for AI composition")
|
||||
(li "Every test attached is more validation for AI output")
|
||||
(li "Every fork is a preference signal for AI ranking")
|
||||
(li "Every pin is a popularity signal for AI retrieval")))
|
||||
|
||||
(~docs/subsection :title "6c. Deliverables"
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li (code "web/lib/ai-compose.sx") " — composition interface (~200 LOC)")
|
||||
(li "Embedding index over component metadata + signatures")
|
||||
(li "Retrieval API: search by intent, filter by quality signals")
|
||||
(li "Composition validator: check output resolves, tests pass"))))
|
||||
|
||||
;; =====================================================================
|
||||
;; Implementation Order
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Implementation Order" :id "order"
|
||||
|
||||
(~docs/code :src (highlight
|
||||
"Phase 1 Browser Node Foundation ~400 LOC SX + ~100 LOC JS\n IndexedDB store, Service Worker, CID verification\n Prerequisite: none (extends existing sx-browser.js)\n\nPhase 2 WebTransport Peer Layer ~550 LOC SX + ~250 LOC Python\n Protocol definition, server endpoint, browser client\n Prerequisite: Phase 1 (needs content store)\n\nPhase 3 In-Browser Authoring ~550 LOC SX\n Editor, test runner, publish flow\n Prerequisite: Phase 1 + 2 (needs store + peer connection)\n\nPhase 4 Component Discovery ~750 LOC SX\n Dependency resolution, component browser, search\n Prerequisite: Phase 2 (needs federation queries)\n\nPhase 5 Node Bootstrap ~300 LOC config + scripts\n Docker image, auto-bootstrap, seed protocol\n Prerequisite: Phase 1–4 (packages everything)\n\nPhase 6 AI Composition ~400 LOC SX + embedding index\n Retrieval, composition, validation\n Prerequisite: Phase 4 (needs searchable component graph)\n\n Total new SX: ~2650 LOC\n Total new platform: ~350 LOC (JS + Python)\n Timeline: Phases are independent workstreams after Phase 1"
|
||||
"text"))
|
||||
|
||||
(p "Phase 1 is the foundation — everything else depends on the browser being able to "
|
||||
"store, verify, and cache content-addressed SX. After that, phases 2–4 can proceed "
|
||||
"in parallel. Phase 5 packages it all. Phase 6 builds on the populated network."))
|
||||
|
||||
;; =====================================================================
|
||||
;; What Already Exists
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "What Already Exists (sx-pub Phases 1–4)" :id "existing"
|
||||
|
||||
(table :class "w-full mb-6 text-sm"
|
||||
(thead
|
||||
(tr (th :class "text-left p-2" "Capability") (th :class "text-left p-2" "Status") (th :class "text-left p-2" "Location")))
|
||||
(tbody
|
||||
(tr (td :class "p-2" "IPFS async client") (td :class "p-2 text-green-700" "Done") (td :class "p-2 font-mono text-xs" "shared/utils/ipfs_client.py"))
|
||||
(tr (td :class "p-2" "Merkle tree + proofs") (td :class "p-2 text-green-700" "Done") (td :class "p-2 font-mono text-xs" "shared/utils/anchoring.py"))
|
||||
(tr (td :class "p-2" "OpenTimestamps anchoring") (td :class "p-2 text-green-700" "Done") (td :class "p-2 font-mono text-xs" "shared/utils/anchoring.py"))
|
||||
(tr (td :class "p-2" "ActivityPub federation") (td :class "p-2 text-green-700" "Done") (td :class "p-2 font-mono text-xs" "shared/infrastructure/activitypub.py"))
|
||||
(tr (td :class "p-2" "sx-pub HTTP handlers") (td :class "p-2 text-green-700" "Done") (td :class "p-2 font-mono text-xs" "sx/sx/handlers/pub-api.sx"))
|
||||
(tr (td :class "p-2" "Publishing + CID resolution") (td :class "p-2 text-green-700" "Done") (td :class "p-2 font-mono text-xs" "sx/sxc/pages/pub_helpers.py"))
|
||||
(tr (td :class "p-2" "SX wire format (aser)") (td :class "p-2 text-green-700" "Done") (td :class "p-2 font-mono text-xs" "web/adapter-sx.sx"))
|
||||
(tr (td :class "p-2" "Content store (in-memory)") (td :class "p-2 text-green-700" "Done") (td :class "p-2 font-mono text-xs" "lib/content.sx"))
|
||||
(tr (td :class "p-2" "Component localStorage cache") (td :class "p-2 text-green-700" "Done") (td :class "p-2 font-mono text-xs" "web/boot.sx"))
|
||||
(tr (td :class "p-2" "Docker dev environment") (td :class "p-2 text-green-700" "Done") (td :class "p-2 font-mono text-xs" "docker-compose.dev-pub.yml"))))
|
||||
|
||||
(p "The server-side infrastructure is complete. The plan above extends it to browsers."))))
|
||||
@@ -108,6 +108,12 @@
|
||||
"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 "Live Demo" :id "demo"
|
||||
(p "Two patterns that refs formalize: an interval ID captured in an effect closure, and a DOM element handle stored in a dict. Neither needs reactivity — they're infrastructure state.")
|
||||
(~reactive-runtime/demo-ref)
|
||||
(~docs/code :src (highlight "(defisland ~demo-ref ()\n (let ((input-ref (dict \"current\" nil))\n (ticks (signal 0))\n (running (signal false))\n (last-value (signal \"\")))\n ;; Timer ID captured in effect closure — the ref pattern\n (effect (fn ()\n (when (deref running)\n (let ((id (set-interval (fn () (swap! ticks inc)) 500)))\n (fn () (clear-interval id))))))\n ;; DOM handle in a dict — also the ref pattern\n (input :ref input-ref :type \"text\" ...)\n (button :on-click (fn (e)\n (reset! last-value\n (dom-get-prop (get input-ref \"current\") \"value\")))\n \"Read\")))" "lisp"))
|
||||
(p "The timer ID lives in a closure — nothing reads it reactively. The DOM element is a " (code "(dict \"current\" nil)") " set by " (code ":ref") ". The " (code "ref") " function formalizes both into a single primitive with auto-disposal."))
|
||||
|
||||
(~docs/section :title "API" :id "ref-api"
|
||||
|
||||
(~docs/code :src (highlight
|
||||
@@ -148,6 +154,12 @@
|
||||
"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 "Live Demo" :id "demo"
|
||||
(p "Click the canvas to place colored rectangles. Uses " (code "host-call") " for canvas 2D context method calls and " (code "host-set!") " for property writes — the raw primitives that " (code "foreign-method") " and " (code "foreign-prop-setter") " will wrap.")
|
||||
(~reactive-runtime/demo-foreign)
|
||||
(~docs/code :src (highlight "(defisland ~demo-foreign ()\n (let ((canvas-ref (dict \"current\" nil))\n (color (signal \"#8b5cf6\"))\n (rects (signal (list))))\n ;; Draw effect — re-runs when rects changes\n (effect (fn ()\n (let ((el (get canvas-ref \"current\")))\n (when el\n (let ((ctx (host-call el \"getContext\" \"2d\")))\n (host-call ctx \"clearRect\" 0 0 280 160)\n (for-each (fn (r)\n (host-set! ctx \"fillStyle\" (get r \"c\"))\n (host-call ctx \"fillRect\" (get r \"x\") (get r \"y\") 20 20))\n (deref rects)))))))\n (canvas :ref canvas-ref :on-click (fn (e) ...)\n :width \"280\" :height \"160\")))" "lisp"))
|
||||
(p "Every canvas operation is a raw " (code "host-call") " or " (code "host-set!") ". The foreign FFI layer wraps these into named SX functions: " (code "(fill-rect ctx 0 0 280 160)") " instead of " (code "(host-call ctx \"fillRect\" 0 0 280 160)") "."))
|
||||
|
||||
(~docs/section :title "API" :id "foreign-api"
|
||||
|
||||
(~docs/code :src (highlight
|
||||
@@ -181,6 +193,12 @@
|
||||
"Modal state management where machine state IS a signal. "
|
||||
"Composes naturally with computed and effects.")
|
||||
|
||||
(~docs/section :title "Live Demo" :id "demo"
|
||||
(p "A traffic light built from a signal and a transitions dict. Auto-advance uses an effect with " (code "set-timeout") " — when state changes, the effect re-runs and schedules the next transition.")
|
||||
(~reactive-runtime/demo-machine)
|
||||
(~docs/code :src (highlight "(defisland ~demo-machine ()\n (let ((state (signal \"red\"))\n (transitions (dict \"red\" \"green\" \"green\" \"yellow\" \"yellow\" \"red\"))\n (auto (signal false)))\n ;; Auto-advance: effect depends on both state and auto\n (effect (fn ()\n (when (deref auto)\n (let ((delay (if (= (deref state) \"yellow\") 1000 2500)))\n (let ((id (set-timeout\n (fn () (reset! state (get transitions (deref state))))\n delay)))\n (fn () (clear-timeout id)))))))\n ;; Display: reactive class on each light\n (div :class (str \"...\" (if (= (deref state) \"red\")\n \"bg-red-500\" \"bg-red-900/30\")) ...)))" "lisp"))
|
||||
(p "The machine state is a signal — " (code "(deref state)") " in the effect body subscribes, so when the timeout fires and resets state, the effect re-runs with the new delay. The " (code "make-machine") " function wraps this pattern with transition tables, guards, and enter/exit hooks."))
|
||||
|
||||
(~docs/section :title "API" :id "machine-api"
|
||||
|
||||
(~docs/code :src (highlight
|
||||
@@ -211,6 +229,12 @@
|
||||
"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 "Live Demo" :id "demo"
|
||||
(p "A counter with full undo/redo. Each operation pushes the previous state onto the undo stack. Undo pops from undo and pushes to redo. All state is reactive — buttons disable when their stack is empty.")
|
||||
(~reactive-runtime/demo-commands)
|
||||
(~docs/code :src (highlight "(defisland ~demo-commands ()\n (let ((value (signal 0))\n (undo-stack (signal (list)))\n (redo-stack (signal (list)))\n (do-cmd (fn (new-val)\n (batch (fn ()\n (swap! undo-stack (fn (s) (cons (deref value) s)))\n (reset! redo-stack (list))\n (reset! value new-val)))))\n (undo (fn ()\n (when (> (len (deref undo-stack)) 0)\n (batch (fn ()\n (let ((prev (first (deref undo-stack))))\n (swap! redo-stack (fn (r) (cons (deref value) r)))\n (reset! value prev)\n (swap! undo-stack rest)))))))\n (redo (fn () ...)))\n (span (deref value))\n (button :on-click (fn (e) (do-cmd (+ (deref value) 1))) \"+1\")\n (button :on-click (fn (e) (undo)) \"Undo\")))" "lisp"))
|
||||
(p "Three signals and two functions. " (code "batch") " groups the three writes (push old, clear redo, set new) into one notification pass. The stacks use " (code "cons") "/" (code "first") "/" (code "rest") " — standard list operations. " (code "make-command-store") " wraps this pattern and adds transaction grouping."))
|
||||
|
||||
(~docs/section :title "API" :id "commands-api"
|
||||
|
||||
(~docs/code :src (highlight
|
||||
@@ -245,6 +269,12 @@
|
||||
(code "requestAnimationFrame") " integration for canvas and animation apps, "
|
||||
"with automatic island lifecycle cleanup.")
|
||||
|
||||
(~docs/section :title "Live Demo" :id "demo"
|
||||
(p "A ball bouncing across a track. Position is a signal — the " (code ":style") " attribute reads it reactively. The animation runs via " (code "set-interval") " at ~60fps; the effect's cleanup stops it when paused.")
|
||||
(~reactive-runtime/demo-loop)
|
||||
(~docs/code :src (highlight "(defisland ~demo-loop ()\n (let ((running (signal true))\n (x (signal 0))\n (dir (signal 1))\n (frames (signal 0)))\n ;; Animation effect — interval simulates rAF\n (effect (fn ()\n (when (deref running)\n (let ((id (set-interval\n (fn ()\n (batch (fn ()\n (swap! frames inc)\n (when (>= (deref x) 230) (reset! dir -1))\n (when (<= (deref x) 0) (reset! dir 1))\n (swap! x (fn (v) (+ v (* (deref dir) 3)))))))\n 16)))\n (fn () (clear-interval id))))))\n ;; Ball position driven by reactive style\n (div :class \"relative h-12 ...\" :style (str \"...\")\n (div :style (str \"left:\" (deref x) \"px\") ...))))" "lisp"))
|
||||
(p "The " (code "running") " signal is read in the effect body — toggling it triggers cleanup (stops the interval) and re-evaluation (starts a new one or does nothing). The real " (code "make-loop") " uses " (code "request-animation-frame") " with the running-ref pattern instead."))
|
||||
|
||||
(~docs/section :title "API" :id "loop-api"
|
||||
|
||||
(~docs/code :src (highlight
|
||||
@@ -276,6 +306,12 @@
|
||||
"Enhances the existing reactive-list with explicit key extraction callbacks "
|
||||
"for stable identity tracking across updates.")
|
||||
|
||||
(~docs/section :title "Live Demo" :id "demo"
|
||||
(p "Items with stable " (code ":key") " identities. Reverse and rotate reorder the signal list — the reconciler moves existing DOM nodes instead of recreating them. Add and remove demonstrate insertion and deletion.")
|
||||
(~reactive-runtime/demo-keyed-lists)
|
||||
(~docs/code :src (highlight "(defisland ~demo-keyed-lists ()\n (let ((items (signal (list\n (dict \"id\" 1 \"text\" \"Alpha\" \"color\" \"violet\")\n (dict \"id\" 2 \"text\" \"Beta\" \"color\" \"blue\") ...)))\n (next-id (signal 6)))\n (button :on-click (fn (e) (swap! items reverse)) \"Reverse\")\n (button :on-click (fn (e)\n (swap! items (fn (old) (append (rest old) (first old)))))\n \"Rotate\")\n (ul (map (fn (item)\n (li :key (str (get item \"id\"))\n :class (str \"bg-\" (get item \"color\") \"-100 ...\")\n (span (get item \"text\"))\n (button :on-click (fn (e) (remove-item (get item \"id\"))) \"✕\")))\n (deref items)))))" "lisp"))
|
||||
(p (code ":key") " on each " (code "li") " gives the reconciler stable identity. When " (code "reverse") " flips the list, the existing DOM nodes are moved — not destroyed and recreated. This preserves focus, scroll position, CSS transitions, and internal island state."))
|
||||
|
||||
(~docs/section :title "API" :id "keyed-api"
|
||||
|
||||
(~docs/code :src (highlight
|
||||
@@ -309,6 +345,12 @@
|
||||
"Skip SSR entirely for canvas-heavy apps. Server returns a minimal HTML shell; "
|
||||
"all rendering happens client-side.")
|
||||
|
||||
(~docs/section :title "Live Demo" :id "demo"
|
||||
(p "A mini single-page app rendered entirely client-side. Navigation switches views via a " (code "route") " signal — no server round-trips. Counter state persists across view switches because signals live in closures, not the DOM.")
|
||||
(~reactive-runtime/demo-app-shell)
|
||||
(~docs/code :src (highlight "(defisland ~demo-app-shell ()\n (let ((route (signal \"home\"))\n (count (signal 0)))\n ;; Client-side nav\n (nav\n (button :on-click (fn (e) (reset! route \"home\"))\n :class (str ... (if (= (deref route) \"home\") \"active\" \"inactive\"))\n \"Home\")\n ...)\n ;; Reactive view switching\n (cond\n (= (deref route) \"home\")\n (div (p \"Client-rendered. No server round-trip.\"))\n (= (deref route) \"counter\")\n (div (span (deref count)) ...)\n (= (deref route) \"about\")\n (div (p \"Entirely client-rendered.\")))))" "lisp"))
|
||||
(p "This island IS a mini app shell. The " (code "make-app") " function generalizes the pattern: server returns " (code "<div id=\"sx-app-root\">") " + SX loader, " (code "app-boot") " mounts the entry component, " (code "app-navigate!") " switches routes — all using existing " (code "sx-mount") " and signals."))
|
||||
|
||||
(~docs/section :title "API" :id "app-api"
|
||||
|
||||
(~docs/code :src (highlight
|
||||
@@ -325,3 +367,336 @@
|
||||
"Uses existing " (code "sx-mount") " and " (code "sx-hydrate-islands") " from boot.sx.")
|
||||
|
||||
(p "~330 lines. Orchestrates all other layers."))))
|
||||
|
||||
|
||||
;; ===========================================================================
|
||||
;; Live demo islands
|
||||
;; ===========================================================================
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; L0: Ref — timer ID in closure, DOM handle in dict
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defisland ~reactive-runtime/demo-ref ()
|
||||
(let ((input-ref (dict "current" nil))
|
||||
(ticks (signal 0))
|
||||
(running (signal false))
|
||||
(last-value (signal "")))
|
||||
(let ((_eff (effect (fn ()
|
||||
(when (deref running)
|
||||
(let ((id (set-interval (fn () (swap! ticks inc)) 500)))
|
||||
(fn () (clear-interval id))))))))
|
||||
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
|
||||
(p :class "text-xs font-semibold text-stone-500 mb-2"
|
||||
"Timer — interval ID captured in effect closure")
|
||||
(div :class "flex items-center gap-3 mb-4"
|
||||
(span :class "text-2xl font-bold text-violet-900 font-mono w-16 text-center"
|
||||
(deref ticks))
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
|
||||
:on-click (fn (e) (swap! running not))
|
||||
(if (deref running) "Stop" "Start"))
|
||||
(button :class "px-3 py-1 rounded bg-stone-300 text-stone-700 text-sm hover:bg-stone-400"
|
||||
:on-click (fn (e) (do (reset! running false) (reset! ticks 0)))
|
||||
"Reset"))
|
||||
(p :class "text-xs font-semibold text-stone-500 mb-2"
|
||||
"DOM handle — element ref in dict")
|
||||
(div :class "flex items-center gap-3"
|
||||
(input :ref input-ref :type "text" :placeholder "Type something…"
|
||||
:class "px-3 py-1.5 rounded border border-stone-300 text-sm focus:outline-none focus:border-violet-400 w-48")
|
||||
(button :class "px-3 py-1 rounded bg-stone-300 text-stone-700 text-sm hover:bg-stone-400"
|
||||
:on-click (fn (e)
|
||||
(reset! last-value (dom-get-prop (get input-ref "current") "value")))
|
||||
"Read")
|
||||
(when (not (= (deref last-value) ""))
|
||||
(span :class "text-sm text-stone-600 font-mono"
|
||||
"\"" (deref last-value) "\"")))))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; L1: Foreign — canvas drawing via host-call
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defisland ~reactive-runtime/demo-foreign ()
|
||||
(let ((canvas-ref (dict "current" nil))
|
||||
(color (signal "#8b5cf6"))
|
||||
(count (signal 0)))
|
||||
(let ((_eff (effect (fn ()
|
||||
(let ((el (get canvas-ref "current"))
|
||||
(n (deref count))
|
||||
(c (deref color)))
|
||||
(when el
|
||||
(let ((ctx (host-call el "getContext" "2d")))
|
||||
(host-call ctx "clearRect" 0 0 280 160)
|
||||
(host-set! ctx "fillStyle" c)
|
||||
(host-call ctx "fillRect" 10 10 (min (* n 25) 260) (min (* n 18) 140)))))))))
|
||||
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
|
||||
(canvas :ref canvas-ref :width "280" :height "160"
|
||||
:class "border border-stone-300 rounded bg-white block mb-3")
|
||||
(div :class "flex items-center gap-3"
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
|
||||
:on-click (fn (e) (swap! count inc))
|
||||
"Add")
|
||||
(select :bind color
|
||||
:class "px-2 py-1 rounded border border-stone-300 text-sm"
|
||||
(option :value "#8b5cf6" "Violet")
|
||||
(option :value "#3b82f6" "Blue")
|
||||
(option :value "#ef4444" "Red")
|
||||
(option :value "#22c55e" "Green"))
|
||||
(button :class "px-3 py-1 rounded bg-stone-300 text-stone-700 text-sm hover:bg-stone-400"
|
||||
:on-click (fn (e) (reset! count 0))
|
||||
"Clear")
|
||||
(span :class "text-sm text-stone-500"
|
||||
(deref count) " squares"))))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; L2: State Machine — traffic light
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defisland ~reactive-runtime/demo-machine ()
|
||||
(let ((state (signal "red"))
|
||||
(transitions (dict "red" "green" "green" "yellow" "yellow" "red"))
|
||||
(auto (signal false)))
|
||||
(let ((_eff (effect (fn ()
|
||||
(when (deref auto)
|
||||
(let ((delay (if (= (deref state) "yellow") 1000 2500)))
|
||||
(let ((id (set-timeout
|
||||
(fn () (reset! state (get transitions (deref state))))
|
||||
delay)))
|
||||
(fn () (clear-timeout id)))))))))
|
||||
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
|
||||
(div :class "flex items-center gap-6"
|
||||
;; Traffic light
|
||||
(div :class "flex flex-col gap-2 p-3 bg-stone-800 rounded-lg"
|
||||
(div :class (str "w-10 h-10 rounded-full transition-colors "
|
||||
(if (= (deref state) "red") "bg-red-500 shadow-lg shadow-red-500/50" "bg-red-900/30")))
|
||||
(div :class (str "w-10 h-10 rounded-full transition-colors "
|
||||
(if (= (deref state) "yellow") "bg-yellow-400 shadow-lg shadow-yellow-400/50" "bg-yellow-900/30")))
|
||||
(div :class (str "w-10 h-10 rounded-full transition-colors "
|
||||
(if (= (deref state) "green") "bg-green-500 shadow-lg shadow-green-500/50" "bg-green-900/30"))))
|
||||
;; Controls
|
||||
(div :class "space-y-3"
|
||||
(div :class "flex items-center gap-2"
|
||||
(span :class "text-sm font-medium text-stone-700" "State:")
|
||||
(span :class "text-sm font-mono text-violet-700" (deref state)))
|
||||
(div :class "flex items-center gap-2"
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
|
||||
:on-click (fn (e)
|
||||
(reset! state (get transitions (deref state))))
|
||||
"Next")
|
||||
(button :class (str "px-3 py-1 rounded text-sm font-medium "
|
||||
(if (deref auto)
|
||||
"bg-amber-500 text-white hover:bg-amber-600"
|
||||
"bg-stone-300 text-stone-700 hover:bg-stone-400"))
|
||||
:on-click (fn (e) (swap! auto not))
|
||||
(if (deref auto) "Auto: ON" "Auto: OFF")))))))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; L3: Commands — counter with undo/redo
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defisland ~reactive-runtime/demo-commands ()
|
||||
(let ((value (signal 0))
|
||||
(history (signal (list)))
|
||||
(future (signal (list)))
|
||||
(undo-class (signal "bg-stone-200 text-stone-400 cursor-not-allowed"))
|
||||
(redo-class (signal "bg-stone-200 text-stone-400 cursor-not-allowed")))
|
||||
(let ((do-cmd (fn (new-val)
|
||||
(let ((old-val (deref value))
|
||||
(old-hist (deref history)))
|
||||
(do
|
||||
(reset! history (cons old-val old-hist))
|
||||
(reset! future (list))
|
||||
(reset! value new-val)))))
|
||||
(undo (fn ()
|
||||
(let ((h (deref history)))
|
||||
(when (> (len h) 0)
|
||||
(let ((prev (first h))
|
||||
(old-val (deref value))
|
||||
(f (deref future)))
|
||||
(do
|
||||
(reset! future (cons old-val f))
|
||||
(reset! history (rest h))
|
||||
(reset! value prev)))))))
|
||||
(redo (fn ()
|
||||
(let ((f (deref future)))
|
||||
(when (> (len f) 0)
|
||||
(let ((next-val (first f))
|
||||
(old-val (deref value))
|
||||
(h (deref history)))
|
||||
(do
|
||||
(reset! history (cons old-val h))
|
||||
(reset! future (rest f))
|
||||
(reset! value next-val)))))))
|
||||
(_e1 (effect (fn ()
|
||||
(reset! undo-class
|
||||
(if (> (len (deref history)) 0)
|
||||
"bg-stone-600 text-white hover:bg-stone-700"
|
||||
"bg-stone-200 text-stone-400 cursor-not-allowed")))))
|
||||
(_e2 (effect (fn ()
|
||||
(reset! redo-class
|
||||
(if (> (len (deref future)) 0)
|
||||
"bg-stone-600 text-white hover:bg-stone-700"
|
||||
"bg-stone-200 text-stone-400 cursor-not-allowed"))))))
|
||||
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
|
||||
(div :class "flex items-center gap-3 mb-3"
|
||||
(span :class "text-3xl font-bold text-violet-900 font-mono w-20 text-center"
|
||||
(deref value))
|
||||
(div :class "flex gap-1"
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
|
||||
:on-click (fn (e) (do-cmd (+ (deref value) 1)))
|
||||
"+1")
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
|
||||
:on-click (fn (e) (do-cmd (+ (deref value) 5)))
|
||||
"+5")
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
|
||||
:on-click (fn (e) (do-cmd (* (deref value) 2)))
|
||||
"×2")
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
|
||||
:on-click (fn (e) (do-cmd 0))
|
||||
"0")))
|
||||
(div :class "flex items-center gap-3"
|
||||
(button :class (str "px-3 py-1 rounded text-sm font-medium " (deref undo-class))
|
||||
:on-click (fn (e) (undo))
|
||||
"Undo")
|
||||
(button :class (str "px-3 py-1 rounded text-sm font-medium " (deref redo-class))
|
||||
:on-click (fn (e) (redo))
|
||||
"Redo")
|
||||
(span :class "text-xs text-stone-400 font-mono"
|
||||
"history: " (deref (computed (fn () (len (deref history)))))))))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; L4: Render Loop — bouncing ball
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defisland ~reactive-runtime/demo-loop ()
|
||||
(let ((running (signal true))
|
||||
(x (signal 0))
|
||||
(dir (signal 1))
|
||||
(frames (signal 0)))
|
||||
(let ((_eff (effect (fn ()
|
||||
(when (deref running)
|
||||
(let ((id (set-interval
|
||||
(fn ()
|
||||
(batch (fn ()
|
||||
(swap! frames inc)
|
||||
(when (>= (deref x) 230) (reset! dir -1))
|
||||
(when (<= (deref x) 0) (reset! dir 1))
|
||||
(swap! x (fn (v) (+ v (* (deref dir) 3)))))))
|
||||
16)))
|
||||
(fn () (clear-interval id))))))))
|
||||
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
|
||||
(div :class "relative h-12 bg-white rounded border border-stone-200 mb-3 overflow-hidden"
|
||||
(div :class "absolute top-1 w-10 h-10 rounded-full bg-violet-500 transition-none"
|
||||
:style (str "left:" (deref x) "px")))
|
||||
(div :class "flex items-center gap-3"
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
|
||||
:on-click (fn (e) (swap! running not))
|
||||
(if (deref running) "Pause" "Play"))
|
||||
(button :class "px-3 py-1 rounded bg-stone-300 text-stone-700 text-sm hover:bg-stone-400"
|
||||
:on-click (fn (e)
|
||||
(batch (fn ()
|
||||
(reset! running false)
|
||||
(reset! x 0)
|
||||
(reset! dir 1)
|
||||
(reset! frames 0))))
|
||||
"Reset")
|
||||
(span :class "text-sm text-stone-500 font-mono"
|
||||
"frames: " (deref frames)))))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; L5: Keyed Lists — reorderable items
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defisland ~reactive-runtime/demo-keyed-lists ()
|
||||
(let ((next-id (signal 1))
|
||||
(items (signal (list)))
|
||||
(colors (list "violet" "blue" "green" "amber" "red" "stone"))
|
||||
(add-item (fn (e)
|
||||
(let ((id (deref next-id))
|
||||
(old (deref items)))
|
||||
(do
|
||||
(reset! items (append old (dict "id" id
|
||||
"text" (str "Item " id)
|
||||
"color" (nth colors (mod (- id 1) 6)))))
|
||||
(reset! next-id (+ id 1))))))
|
||||
(remove-item (fn (id)
|
||||
(reset! items (filter (fn (item) (not (= (get item "id") id))) (deref items))))))
|
||||
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
|
||||
(div :class "flex items-center gap-2 mb-3"
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
|
||||
:on-click add-item
|
||||
"Add Item")
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
|
||||
:on-click (fn (e) (reset! items (reverse (deref items))))
|
||||
"Reverse")
|
||||
(span :class "text-sm text-stone-500"
|
||||
(deref (computed (fn () (len (deref items))))) " items"))
|
||||
(ul :class "space-y-1"
|
||||
(map (fn (item)
|
||||
(li :key (str (get item "id"))
|
||||
:class (str "flex items-center justify-between rounded px-3 py-2 text-sm bg-" (get item "color") "-100 text-" (get item "color") "-800")
|
||||
(span (get item "text"))
|
||||
(button :class "text-stone-400 hover:text-red-500 text-xs ml-2"
|
||||
:on-click (fn (e) (remove-item (get item "id")))
|
||||
"✕")))
|
||||
(deref items))))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; L6: App Shell — client-side mini-app
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defisland ~reactive-runtime/demo-app-shell ()
|
||||
(let ((route (signal "home"))
|
||||
(count (signal 0)))
|
||||
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
|
||||
;; Client-side nav bar
|
||||
(nav :class "flex gap-1 border-b border-stone-200 pb-2 mb-3"
|
||||
(button :class (str "px-2 py-1 text-xs rounded "
|
||||
(if (= (deref route) "home")
|
||||
"bg-violet-600 text-white font-medium"
|
||||
"bg-stone-200 text-stone-600 hover:bg-stone-300"))
|
||||
:on-click (fn (e) (reset! route "home"))
|
||||
"Home")
|
||||
(button :class (str "px-2 py-1 text-xs rounded "
|
||||
(if (= (deref route) "counter")
|
||||
"bg-violet-600 text-white font-medium"
|
||||
"bg-stone-200 text-stone-600 hover:bg-stone-300"))
|
||||
:on-click (fn (e) (reset! route "counter"))
|
||||
"Counter")
|
||||
(button :class (str "px-2 py-1 text-xs rounded "
|
||||
(if (= (deref route) "about")
|
||||
"bg-violet-600 text-white font-medium"
|
||||
"bg-stone-200 text-stone-600 hover:bg-stone-300"))
|
||||
:on-click (fn (e) (reset! route "about"))
|
||||
"About"))
|
||||
;; Reactive view switching
|
||||
(div :class "min-h-24"
|
||||
(if (= (deref route) "home")
|
||||
(div :class "p-3"
|
||||
(h3 :class "font-bold text-stone-800 mb-1" "Home")
|
||||
(p :class "text-sm text-stone-600"
|
||||
"Client-side rendered. No server round-trip for navigation."))
|
||||
(if (= (deref route) "counter")
|
||||
(div :class "p-3"
|
||||
(h3 :class "font-bold text-stone-800 mb-2" "Counter")
|
||||
(div :class "flex items-center gap-3"
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
|
||||
:on-click (fn (e) (swap! count dec))
|
||||
"−")
|
||||
(span :class "text-2xl font-bold text-violet-900 font-mono w-12 text-center"
|
||||
(deref count))
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
|
||||
:on-click (fn (e) (swap! count inc))
|
||||
"+"))
|
||||
(p :class "text-xs text-stone-400 mt-2"
|
||||
"State persists across view switches — signals live in closures."))
|
||||
(div :class "p-3"
|
||||
(h3 :class "font-bold text-stone-800 mb-1" "About")
|
||||
(p :class "text-sm text-stone-600"
|
||||
"This mini-app is entirely client-rendered. The server provides only "
|
||||
"the component definition and mount point — zero HTML content."))))))))
|
||||
|
||||
Reference in New Issue
Block a user