From 0c8c0b64261c19b639e8c1e89b5212beafa58dac Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 25 Mar 2026 14:54:43 +0000 Subject: [PATCH] Cache-bust .sx files, optimize stepper back/hydration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Platform: - sx-platform.js: extract ?v= query from script tag URL, append to all .sx file XHR requests. Prevents stale cached .sx files. Stepper performance: - do-back: use rebuild-preview (pure SX→DOM render) instead of replaying every do-step from 0. O(1) instead of O(n). - Hydration effect: same rebuild-preview instead of step replay. - Cookie save moved from do-step to button on-click handlers only. Co-Authored-By: Claude Opus 4.6 (1M context) --- hosts/ocaml/browser/sx-platform.js | 346 +++++++++++++++++++++++++++++ shared/static/wasm/sx-platform.js | 346 +++++++++++++++++++++++++++++ sx/sx/home-stepper.sx | 41 ++-- 3 files changed, 715 insertions(+), 18 deletions(-) create mode 100644 hosts/ocaml/browser/sx-platform.js create mode 100644 shared/static/wasm/sx-platform.js diff --git a/hosts/ocaml/browser/sx-platform.js b/hosts/ocaml/browser/sx-platform.js new file mode 100644 index 00000000..c89df0c0 --- /dev/null +++ b/hosts/ocaml/browser/sx-platform.js @@ -0,0 +1,346 @@ +/** + * sx-platform.js — Browser platform layer for the SX WASM kernel. + * + * Registers the 8 FFI host primitives and loads web adapter .sx files. + * This is the only JS needed beyond the WASM kernel itself. + * + * Usage: + * + * + * + * Or for js_of_ocaml mode: + * + * + */ + +(function() { + "use strict"; + + var K = globalThis.SxKernel; + if (!K) { console.error("[sx-platform] SxKernel not found"); return; } + + // ================================================================ + // 8 FFI Host Primitives + // ================================================================ + + K.registerNative("host-global", function(args) { + var name = args[0]; + if (typeof globalThis !== "undefined" && name in globalThis) return globalThis[name]; + if (typeof window !== "undefined" && name in window) return window[name]; + return null; + }); + + K.registerNative("host-get", function(args) { + var obj = args[0], prop = args[1]; + if (obj == null) return null; + var v = obj[prop]; + return v === undefined ? null : v; + }); + + K.registerNative("host-set!", function(args) { + var obj = args[0], prop = args[1], val = args[2]; + if (obj != null) obj[prop] = val; + }); + + K.registerNative("host-call", function(args) { + var obj = args[0], method = args[1]; + var callArgs = []; + for (var i = 2; i < args.length; i++) callArgs.push(args[i]); + if (obj == null) { + // Global function call + var fn = typeof globalThis !== "undefined" ? globalThis[method] : window[method]; + if (typeof fn === "function") return fn.apply(null, callArgs); + return null; + } + if (typeof obj[method] === "function") { + try { return obj[method].apply(obj, callArgs); } + catch(e) { console.error("[sx] host-call error:", e); return null; } + } + return null; + }); + + K.registerNative("host-new", function(args) { + var name = args[0]; + var cArgs = args.slice(1); + var Ctor = typeof globalThis !== "undefined" ? globalThis[name] : window[name]; + if (typeof Ctor !== "function") return null; + switch (cArgs.length) { + case 0: return new Ctor(); + case 1: return new Ctor(cArgs[0]); + case 2: return new Ctor(cArgs[0], cArgs[1]); + case 3: return new Ctor(cArgs[0], cArgs[1], cArgs[2]); + default: return new Ctor(cArgs[0], cArgs[1], cArgs[2], cArgs[3]); + } + }); + + K.registerNative("host-callback", function(args) { + var fn = args[0]; + // Native JS function — pass through + if (typeof fn === "function") return fn; + // SX callable (has __sx_handle) — wrap as JS function + if (fn && fn.__sx_handle !== undefined) { + return function() { + var a = Array.prototype.slice.call(arguments); + return K.callFn(fn, a); + }; + } + return function() {}; + }); + + K.registerNative("host-typeof", function(args) { + var obj = args[0]; + if (obj == null) return "nil"; + if (obj instanceof Element) return "element"; + if (obj instanceof Text) return "text"; + if (obj instanceof DocumentFragment) return "fragment"; + if (obj instanceof Document) return "document"; + if (obj instanceof Event) return "event"; + if (obj instanceof Promise) return "promise"; + if (obj instanceof AbortController) return "abort-controller"; + return typeof obj; + }); + + K.registerNative("host-await", function(args) { + var promise = args[0], callback = args[1]; + if (promise && typeof promise.then === "function") { + var cb; + if (typeof callback === "function") cb = callback; + else if (callback && callback.__sx_handle !== undefined) + cb = function(v) { return K.callFn(callback, [v]); }; + else cb = function() {}; + promise.then(cb); + } + }); + + // ================================================================ + // Constants expected by .sx files + // ================================================================ + + K.eval('(define SX_VERSION "wasm-1.0")'); + K.eval('(define SX_ENGINE "ocaml-vm-wasm")'); + K.eval('(define parse sx-parse)'); + K.eval('(define serialize sx-serialize)'); + + // ================================================================ + // DOM query helpers used by boot.sx / orchestration.sx + // (These are JS-native in the transpiled bundle; here via FFI.) + // ================================================================ + + K.registerNative("query-sx-scripts", function(args) { + var root = (args[0] && args[0] !== null) ? args[0] : document; + if (typeof root.querySelectorAll !== "function") root = document; + return Array.prototype.slice.call(root.querySelectorAll('script[type="text/sx"]')); + }); + + K.registerNative("query-page-scripts", function(args) { + return Array.prototype.slice.call(document.querySelectorAll('script[type="text/sx-pages"]')); + }); + + K.registerNative("query-component-scripts", function(args) { + var root = (args[0] && args[0] !== null) ? args[0] : document; + if (typeof root.querySelectorAll !== "function") root = document; + return Array.prototype.slice.call(root.querySelectorAll('script[type="text/sx"][data-components]')); + }); + + // localStorage + K.registerNative("local-storage-get", function(args) { + try { var v = localStorage.getItem(args[0]); return v === null ? null : v; } + catch(e) { return null; } + }); + K.registerNative("local-storage-set", function(args) { + try { localStorage.setItem(args[0], args[1]); } catch(e) {} + }); + K.registerNative("local-storage-remove", function(args) { + try { localStorage.removeItem(args[0]); } catch(e) {} + }); + + // log-info/log-warn defined in browser.sx; log-error as native fallback + K.registerNative("log-error", function(args) { console.error.apply(console, ["[sx]"].concat(args)); }); + + // Cookie access (browser-side) + K.registerNative("get-cookie", function(args) { + var name = args[0]; + var match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '=([^;]*)')); + return match ? decodeURIComponent(match[1]) : null; + }); + K.registerNative("set-cookie", function(args) { + document.cookie = args[0] + "=" + encodeURIComponent(args[1] || "") + ";path=/;max-age=31536000;SameSite=Lax"; + }); + + // ================================================================ + // Load SX web libraries and adapters + // ================================================================ + + // Load order follows dependency graph: + // 1. Core spec files (parser, render, primitives already compiled into WASM kernel) + // 2. Spec modules: signals, deps, router, page-helpers + // 3. Bytecode compiler + VM (for JIT in browser) + // 4. Web libraries: dom.sx, browser.sx (built on 8 FFI primitives) + // 5. Web adapters: adapter-html, adapter-sx, adapter-dom + // 6. Web framework: engine, orchestration, boot + + var _baseUrl = ""; + + // Detect base URL from current script + var _cacheBust = ""; + (function() { + if (typeof document !== "undefined") { + var scripts = document.getElementsByTagName("script"); + for (var i = scripts.length - 1; i >= 0; i--) { + var src = scripts[i].src || ""; + if (src.indexOf("sx-platform") !== -1) { + _baseUrl = src.substring(0, src.lastIndexOf("/") + 1); + var qi = src.indexOf("?"); + if (qi !== -1) _cacheBust = src.substring(qi); + break; + } + } + } + })(); + + /** + * Load an .sx file synchronously via XHR (boot-time only). + * Returns the number of expressions loaded, or an error string. + */ + function loadSxFile(path) { + var url = _baseUrl + path + _cacheBust; + try { + var xhr = new XMLHttpRequest(); + xhr.open("GET", url, false); // synchronous + xhr.send(); + if (xhr.status === 200) { + var result = K.load(xhr.responseText); + if (typeof result === "string" && result.indexOf("Error") === 0) { + console.error("[sx-platform] FAIL " + path + ":", result); + return 0; + } + console.log("[sx-platform] ok " + path + " (" + result + " exprs)"); + return result; + } else { + console.error("[sx] Failed to fetch " + path + ": HTTP " + xhr.status); + return null; + } + } catch(e) { + console.error("[sx] Failed to load " + path + ":", e); + return null; + } + } + + /** + * Load all web adapter .sx files in dependency order. + * Called after the 8 FFI primitives are registered. + */ + function loadWebStack() { + var files = [ + // Spec modules + "sx/render.sx", + "sx/signals.sx", + "sx/deps.sx", + "sx/router.sx", + "sx/page-helpers.sx", + // Freeze scope (signal persistence) + "sx/freeze.sx", + // Bytecode compiler + VM + "sx/bytecode.sx", + "sx/compiler.sx", + "sx/vm.sx", + // Web libraries (use 8 FFI primitives) + "sx/dom.sx", + "sx/browser.sx", + // Web adapters + "sx/adapter-html.sx", + "sx/adapter-sx.sx", + "sx/adapter-dom.sx", + // Boot helpers (platform functions in pure SX) + "sx/boot-helpers.sx", + // Web framework + "sx/engine.sx", + "sx/orchestration.sx", + "sx/boot.sx", + ]; + + var loaded = 0; + for (var i = 0; i < files.length; i++) { + var r = loadSxFile(files[i]); + if (typeof r === "number") loaded += r; + } + console.log("[sx-platform] Loaded " + loaded + " expressions from " + files.length + " files"); + return loaded; + } + + // ================================================================ + // Compatibility shim — expose Sx global matching current JS API + // ================================================================ + + globalThis.Sx = { + VERSION: "wasm-1.0", + parse: function(src) { return K.parse(src); }, + eval: function(src) { return K.eval(src); }, + load: function(src) { return K.load(src); }, + renderToHtml: function(expr) { return K.renderToHtml(expr); }, + callFn: function(fn, args) { return K.callFn(fn, args); }, + engine: function() { return K.engine(); }, + // Boot entry point (called by auto-init or manually) + init: function() { + if (typeof K.eval === "function") { + // Check boot-init exists + // Step through boot manually + console.log("[sx] init-css-tracking..."); + K.eval("(init-css-tracking)"); + console.log("[sx] process-page-scripts..."); + K.eval("(process-page-scripts)"); + console.log("[sx] routes after pages:", K.eval("(len _page-routes)")); + console.log("[sx] process-sx-scripts..."); + K.eval("(process-sx-scripts nil)"); + console.log("[sx] sx-hydrate-elements..."); + K.eval("(sx-hydrate-elements nil)"); + console.log("[sx] sx-hydrate-islands..."); + K.eval("(sx-hydrate-islands nil)"); + console.log("[sx] process-elements..."); + K.eval("(process-elements nil)"); + // Debug islands + console.log("[sx] ~home/stepper defined?", K.eval("(type-of ~home/stepper)")); + console.log("[sx] ~layouts/header defined?", K.eval("(type-of ~layouts/header)")); + // Try manual island query + console.log("[sx] manual island query:", K.eval("(len (dom-query-all (dom-body) \"[data-sx-island]\"))")); + // Try hydrating again + console.log("[sx] retry hydrate-islands..."); + K.eval("(sx-hydrate-islands nil)"); + // Check if links are boosted + var links = document.querySelectorAll("a[href]"); + var boosted = 0; + for (var i = 0; i < links.length; i++) { + if (links[i]._sxBoundboost) boosted++; + } + console.log("[sx] boosted links:", boosted, "/", links.length); + // Check island state + var islands = document.querySelectorAll("[data-sx-island]"); + console.log("[sx] islands:", islands.length); + for (var j = 0; j < islands.length; j++) { + console.log("[sx] island:", islands[j].getAttribute("data-sx-island"), + "hydrated:", !!islands[j]._sxBoundislandhydrated || !!islands[j]["_sxBound" + "island-hydrated"], + "children:", islands[j].children.length); + } + console.log("[sx] boot done"); + } + } + }; + + // ================================================================ + // Auto-init: load web stack and boot on DOMContentLoaded + // ================================================================ + + if (typeof document !== "undefined") { + var _doInit = function() { + loadWebStack(); + Sx.init(); + }; + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", _doInit); + } else { + _doInit(); + } + } + +})(); diff --git a/shared/static/wasm/sx-platform.js b/shared/static/wasm/sx-platform.js new file mode 100644 index 00000000..c89df0c0 --- /dev/null +++ b/shared/static/wasm/sx-platform.js @@ -0,0 +1,346 @@ +/** + * sx-platform.js — Browser platform layer for the SX WASM kernel. + * + * Registers the 8 FFI host primitives and loads web adapter .sx files. + * This is the only JS needed beyond the WASM kernel itself. + * + * Usage: + * + * + * + * Or for js_of_ocaml mode: + * + * + */ + +(function() { + "use strict"; + + var K = globalThis.SxKernel; + if (!K) { console.error("[sx-platform] SxKernel not found"); return; } + + // ================================================================ + // 8 FFI Host Primitives + // ================================================================ + + K.registerNative("host-global", function(args) { + var name = args[0]; + if (typeof globalThis !== "undefined" && name in globalThis) return globalThis[name]; + if (typeof window !== "undefined" && name in window) return window[name]; + return null; + }); + + K.registerNative("host-get", function(args) { + var obj = args[0], prop = args[1]; + if (obj == null) return null; + var v = obj[prop]; + return v === undefined ? null : v; + }); + + K.registerNative("host-set!", function(args) { + var obj = args[0], prop = args[1], val = args[2]; + if (obj != null) obj[prop] = val; + }); + + K.registerNative("host-call", function(args) { + var obj = args[0], method = args[1]; + var callArgs = []; + for (var i = 2; i < args.length; i++) callArgs.push(args[i]); + if (obj == null) { + // Global function call + var fn = typeof globalThis !== "undefined" ? globalThis[method] : window[method]; + if (typeof fn === "function") return fn.apply(null, callArgs); + return null; + } + if (typeof obj[method] === "function") { + try { return obj[method].apply(obj, callArgs); } + catch(e) { console.error("[sx] host-call error:", e); return null; } + } + return null; + }); + + K.registerNative("host-new", function(args) { + var name = args[0]; + var cArgs = args.slice(1); + var Ctor = typeof globalThis !== "undefined" ? globalThis[name] : window[name]; + if (typeof Ctor !== "function") return null; + switch (cArgs.length) { + case 0: return new Ctor(); + case 1: return new Ctor(cArgs[0]); + case 2: return new Ctor(cArgs[0], cArgs[1]); + case 3: return new Ctor(cArgs[0], cArgs[1], cArgs[2]); + default: return new Ctor(cArgs[0], cArgs[1], cArgs[2], cArgs[3]); + } + }); + + K.registerNative("host-callback", function(args) { + var fn = args[0]; + // Native JS function — pass through + if (typeof fn === "function") return fn; + // SX callable (has __sx_handle) — wrap as JS function + if (fn && fn.__sx_handle !== undefined) { + return function() { + var a = Array.prototype.slice.call(arguments); + return K.callFn(fn, a); + }; + } + return function() {}; + }); + + K.registerNative("host-typeof", function(args) { + var obj = args[0]; + if (obj == null) return "nil"; + if (obj instanceof Element) return "element"; + if (obj instanceof Text) return "text"; + if (obj instanceof DocumentFragment) return "fragment"; + if (obj instanceof Document) return "document"; + if (obj instanceof Event) return "event"; + if (obj instanceof Promise) return "promise"; + if (obj instanceof AbortController) return "abort-controller"; + return typeof obj; + }); + + K.registerNative("host-await", function(args) { + var promise = args[0], callback = args[1]; + if (promise && typeof promise.then === "function") { + var cb; + if (typeof callback === "function") cb = callback; + else if (callback && callback.__sx_handle !== undefined) + cb = function(v) { return K.callFn(callback, [v]); }; + else cb = function() {}; + promise.then(cb); + } + }); + + // ================================================================ + // Constants expected by .sx files + // ================================================================ + + K.eval('(define SX_VERSION "wasm-1.0")'); + K.eval('(define SX_ENGINE "ocaml-vm-wasm")'); + K.eval('(define parse sx-parse)'); + K.eval('(define serialize sx-serialize)'); + + // ================================================================ + // DOM query helpers used by boot.sx / orchestration.sx + // (These are JS-native in the transpiled bundle; here via FFI.) + // ================================================================ + + K.registerNative("query-sx-scripts", function(args) { + var root = (args[0] && args[0] !== null) ? args[0] : document; + if (typeof root.querySelectorAll !== "function") root = document; + return Array.prototype.slice.call(root.querySelectorAll('script[type="text/sx"]')); + }); + + K.registerNative("query-page-scripts", function(args) { + return Array.prototype.slice.call(document.querySelectorAll('script[type="text/sx-pages"]')); + }); + + K.registerNative("query-component-scripts", function(args) { + var root = (args[0] && args[0] !== null) ? args[0] : document; + if (typeof root.querySelectorAll !== "function") root = document; + return Array.prototype.slice.call(root.querySelectorAll('script[type="text/sx"][data-components]')); + }); + + // localStorage + K.registerNative("local-storage-get", function(args) { + try { var v = localStorage.getItem(args[0]); return v === null ? null : v; } + catch(e) { return null; } + }); + K.registerNative("local-storage-set", function(args) { + try { localStorage.setItem(args[0], args[1]); } catch(e) {} + }); + K.registerNative("local-storage-remove", function(args) { + try { localStorage.removeItem(args[0]); } catch(e) {} + }); + + // log-info/log-warn defined in browser.sx; log-error as native fallback + K.registerNative("log-error", function(args) { console.error.apply(console, ["[sx]"].concat(args)); }); + + // Cookie access (browser-side) + K.registerNative("get-cookie", function(args) { + var name = args[0]; + var match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '=([^;]*)')); + return match ? decodeURIComponent(match[1]) : null; + }); + K.registerNative("set-cookie", function(args) { + document.cookie = args[0] + "=" + encodeURIComponent(args[1] || "") + ";path=/;max-age=31536000;SameSite=Lax"; + }); + + // ================================================================ + // Load SX web libraries and adapters + // ================================================================ + + // Load order follows dependency graph: + // 1. Core spec files (parser, render, primitives already compiled into WASM kernel) + // 2. Spec modules: signals, deps, router, page-helpers + // 3. Bytecode compiler + VM (for JIT in browser) + // 4. Web libraries: dom.sx, browser.sx (built on 8 FFI primitives) + // 5. Web adapters: adapter-html, adapter-sx, adapter-dom + // 6. Web framework: engine, orchestration, boot + + var _baseUrl = ""; + + // Detect base URL from current script + var _cacheBust = ""; + (function() { + if (typeof document !== "undefined") { + var scripts = document.getElementsByTagName("script"); + for (var i = scripts.length - 1; i >= 0; i--) { + var src = scripts[i].src || ""; + if (src.indexOf("sx-platform") !== -1) { + _baseUrl = src.substring(0, src.lastIndexOf("/") + 1); + var qi = src.indexOf("?"); + if (qi !== -1) _cacheBust = src.substring(qi); + break; + } + } + } + })(); + + /** + * Load an .sx file synchronously via XHR (boot-time only). + * Returns the number of expressions loaded, or an error string. + */ + function loadSxFile(path) { + var url = _baseUrl + path + _cacheBust; + try { + var xhr = new XMLHttpRequest(); + xhr.open("GET", url, false); // synchronous + xhr.send(); + if (xhr.status === 200) { + var result = K.load(xhr.responseText); + if (typeof result === "string" && result.indexOf("Error") === 0) { + console.error("[sx-platform] FAIL " + path + ":", result); + return 0; + } + console.log("[sx-platform] ok " + path + " (" + result + " exprs)"); + return result; + } else { + console.error("[sx] Failed to fetch " + path + ": HTTP " + xhr.status); + return null; + } + } catch(e) { + console.error("[sx] Failed to load " + path + ":", e); + return null; + } + } + + /** + * Load all web adapter .sx files in dependency order. + * Called after the 8 FFI primitives are registered. + */ + function loadWebStack() { + var files = [ + // Spec modules + "sx/render.sx", + "sx/signals.sx", + "sx/deps.sx", + "sx/router.sx", + "sx/page-helpers.sx", + // Freeze scope (signal persistence) + "sx/freeze.sx", + // Bytecode compiler + VM + "sx/bytecode.sx", + "sx/compiler.sx", + "sx/vm.sx", + // Web libraries (use 8 FFI primitives) + "sx/dom.sx", + "sx/browser.sx", + // Web adapters + "sx/adapter-html.sx", + "sx/adapter-sx.sx", + "sx/adapter-dom.sx", + // Boot helpers (platform functions in pure SX) + "sx/boot-helpers.sx", + // Web framework + "sx/engine.sx", + "sx/orchestration.sx", + "sx/boot.sx", + ]; + + var loaded = 0; + for (var i = 0; i < files.length; i++) { + var r = loadSxFile(files[i]); + if (typeof r === "number") loaded += r; + } + console.log("[sx-platform] Loaded " + loaded + " expressions from " + files.length + " files"); + return loaded; + } + + // ================================================================ + // Compatibility shim — expose Sx global matching current JS API + // ================================================================ + + globalThis.Sx = { + VERSION: "wasm-1.0", + parse: function(src) { return K.parse(src); }, + eval: function(src) { return K.eval(src); }, + load: function(src) { return K.load(src); }, + renderToHtml: function(expr) { return K.renderToHtml(expr); }, + callFn: function(fn, args) { return K.callFn(fn, args); }, + engine: function() { return K.engine(); }, + // Boot entry point (called by auto-init or manually) + init: function() { + if (typeof K.eval === "function") { + // Check boot-init exists + // Step through boot manually + console.log("[sx] init-css-tracking..."); + K.eval("(init-css-tracking)"); + console.log("[sx] process-page-scripts..."); + K.eval("(process-page-scripts)"); + console.log("[sx] routes after pages:", K.eval("(len _page-routes)")); + console.log("[sx] process-sx-scripts..."); + K.eval("(process-sx-scripts nil)"); + console.log("[sx] sx-hydrate-elements..."); + K.eval("(sx-hydrate-elements nil)"); + console.log("[sx] sx-hydrate-islands..."); + K.eval("(sx-hydrate-islands nil)"); + console.log("[sx] process-elements..."); + K.eval("(process-elements nil)"); + // Debug islands + console.log("[sx] ~home/stepper defined?", K.eval("(type-of ~home/stepper)")); + console.log("[sx] ~layouts/header defined?", K.eval("(type-of ~layouts/header)")); + // Try manual island query + console.log("[sx] manual island query:", K.eval("(len (dom-query-all (dom-body) \"[data-sx-island]\"))")); + // Try hydrating again + console.log("[sx] retry hydrate-islands..."); + K.eval("(sx-hydrate-islands nil)"); + // Check if links are boosted + var links = document.querySelectorAll("a[href]"); + var boosted = 0; + for (var i = 0; i < links.length; i++) { + if (links[i]._sxBoundboost) boosted++; + } + console.log("[sx] boosted links:", boosted, "/", links.length); + // Check island state + var islands = document.querySelectorAll("[data-sx-island]"); + console.log("[sx] islands:", islands.length); + for (var j = 0; j < islands.length; j++) { + console.log("[sx] island:", islands[j].getAttribute("data-sx-island"), + "hydrated:", !!islands[j]._sxBoundislandhydrated || !!islands[j]["_sxBound" + "island-hydrated"], + "children:", islands[j].children.length); + } + console.log("[sx] boot done"); + } + } + }; + + // ================================================================ + // Auto-init: load web stack and boot on DOMContentLoaded + // ================================================================ + + if (typeof document !== "undefined") { + var _doInit = function() { + loadWebStack(); + Sx.init(); + }; + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", _doInit); + } else { + _doInit(); + } + } + +})(); diff --git a/sx/sx/home-stepper.sx b/sx/sx/home-stepper.sx index 939e8461..487846b2 100644 --- a/sx/sx/home-stepper.sx +++ b/sx/sx/home-stepper.sx @@ -219,17 +219,24 @@ ;; Component expressions handled by lake's reactive render nil)) (swap! step-idx inc) - (update-code-highlight) - (set-cookie "sx-home-stepper" (freeze-to-sx "home-stepper"))))) + (update-code-highlight))))) + (rebuild-preview (fn (target) + ;; Rebuild preview DOM directly from steps, without replaying do-step + (let ((container (get-preview))) + (when container + (dom-set-prop container "innerHTML" "") + (let ((expr (steps-to-preview (deref steps) target))) + (when expr + (let ((rendered (render-to-dom expr (get-render-env nil) nil))) + (when rendered (dom-append container rendered))))) + (set-stack (list container)))))) (do-back (fn () (when (> (deref step-idx) 0) - (let ((target (- (deref step-idx) 1)) - (container (get-preview))) - (when container (dom-set-prop container "innerHTML" "")) - (set-stack (list (get-preview))) - (reset! step-idx 0) - (for-each (fn (_) (do-step)) (slice (deref steps) 0 target)) - (set-cookie "sx-home-stepper" (freeze-to-sx "home-stepper"))))))) + (let ((target (- (deref step-idx) 1))) + (rebuild-preview target) + (reset! step-idx target) + (update-code-highlight) + (set-cookie "sx-home-stepper" (freeze-to-sx "home-stepper")))))))) ;; Freeze scope for persistence — same mechanism, cookie storage (freeze-scope "home-stepper" (fn () (freeze-signal "step" step-idx))) @@ -254,13 +261,7 @@ (let ((_eff (effect (fn () (schedule-idle (fn () (build-code-dom) - (let ((preview (get-preview))) - (when preview (dom-set-prop preview "innerHTML" ""))) - (batch (fn () - (let ((target (deref step-idx))) - (reset! step-idx 0) - (set-stack (list (get-preview))) - (for-each (fn (_) (do-step)) (slice (deref steps) 0 target))))) + (rebuild-preview (deref step-idx)) (update-code-highlight) (run-post-render-hooks))))))) (div :class "space-y-4" @@ -282,7 +283,9 @@ (deref code-tokens))) ;; Controls (div :class "flex items-center justify-center gap-2 md:gap-3" - (button :on-click (fn (e) (do-back)) + (button :on-click (fn (e) + (do-back) + (set-cookie "sx-home-stepper" (freeze-to-sx "home-stepper"))) :class (str "px-2 py-1 rounded text-3xl " (if (> (deref step-idx) 0) "text-stone-600 hover:text-stone-800 hover:bg-stone-100" @@ -290,7 +293,9 @@ "\u25c0") (span :class "text-sm text-stone-500 font-mono tabular-nums" (deref step-idx) " / " (len (deref steps))) - (button :on-click (fn (e) (do-step)) + (button :on-click (fn (e) + (do-step) + (set-cookie "sx-home-stepper" (freeze-to-sx "home-stepper"))) :class (str "px-2 py-1 rounded text-3xl " (if (< (deref step-idx) (len (deref steps))) "text-violet-600 hover:text-violet-800 hover:bg-violet-50"