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"