Add suspense, resource, and transitions — Phase 2 complete

- suspense render-dom form: shows fallback while resource loads, swaps
  to body content when resource signal resolves
- resource async signal: wraps promise into signal with loading/data/error
  dict, auto-transitions on resolve/reject via promise-then
- transition: defers signal writes to requestIdleCallback, sets pending
  signal for UI feedback during expensive operations
- Added schedule-idle, promise-then platform functions
- All Phase 2 features now marked Done in status tables

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 16:40:13 +00:00
parent a496ee6ae6
commit 7efd1b401b
6 changed files with 211 additions and 23 deletions

View File

@@ -14,7 +14,7 @@
// ========================================================================= // =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-08T16:35:00Z"; var SX_VERSION = "2026-03-08T16:36:52Z";
function isNil(x) { return x === NIL || x === null || x === undefined; } function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); } function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -1546,7 +1546,7 @@ return result; }, args);
var renderDomUnknownComponent = function(name) { return error((String("Unknown component: ") + String(name))); }; var renderDomUnknownComponent = function(name) { return error((String("Unknown component: ") + String(name))); };
// RENDER_DOM_FORMS // RENDER_DOM_FORMS
var RENDER_DOM_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defisland", "defmacro", "defstyle", "defhandler", "map", "map-indexed", "filter", "for-each", "portal", "error-boundary"]; var RENDER_DOM_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defisland", "defmacro", "defstyle", "defhandler", "map", "map-indexed", "filter", "for-each", "portal", "error-boundary", "suspense"];
// render-dom-form? // render-dom-form?
var isRenderDomForm = function(name) { return contains(RENDER_DOM_FORMS, name); }; var isRenderDomForm = function(name) { return contains(RENDER_DOM_FORMS, name); };
@@ -1604,7 +1604,7 @@ return result; }, args);
return domAppend(frag, val); return domAppend(frag, val);
})(); }, coll); })(); }, coll);
return frag; return frag;
})() : (isSxTruthy((name == "filter")) ? renderToDom(trampoline(evalExpr(expr, env)), env, ns) : (isSxTruthy((name == "portal")) ? renderDomPortal(args, env, ns) : (isSxTruthy((name == "error-boundary")) ? renderDomErrorBoundary(args, env, ns) : (isSxTruthy((name == "for-each")) ? (function() { })() : (isSxTruthy((name == "filter")) ? renderToDom(trampoline(evalExpr(expr, env)), env, ns) : (isSxTruthy((name == "portal")) ? renderDomPortal(args, env, ns) : (isSxTruthy((name == "error-boundary")) ? renderDomErrorBoundary(args, env, ns) : (isSxTruthy((name == "suspense")) ? renderDomSuspense(args, env, ns) : (isSxTruthy((name == "for-each")) ? (function() {
var f = trampoline(evalExpr(nth(expr, 1), env)); var f = trampoline(evalExpr(nth(expr, 1), env));
var coll = trampoline(evalExpr(nth(expr, 2), env)); var coll = trampoline(evalExpr(nth(expr, 2), env));
var frag = createFragment(); var frag = createFragment();
@@ -1613,7 +1613,7 @@ return result; }, args);
return domAppend(frag, val); return domAppend(frag, val);
})(); } } })(); } }
return frag; return frag;
})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns)))))))))))))); }; })() : renderToDom(trampoline(evalExpr(expr, env)), env, ns))))))))))))))); };
// render-lambda-dom // render-lambda-dom
var renderLambdaDom = function(f, args, env, ns) { return (function() { var renderLambdaDom = function(f, args, env, ns) { return (function() {
@@ -1805,6 +1805,30 @@ return (boundaryDisposers = []); });
})(); })();
})(); }; })(); };
// render-dom-suspense
var renderDomSuspense = function(args, env, ns) { return (function() {
var fallbackExpr = first(args);
var bodyExprs = rest(args);
var container = domCreateElement("div", NIL);
domSetAttr(container, "data-sx-suspense", "true");
domAppend(container, renderToDom(fallbackExpr, env, ns));
(function() {
var bodyDisposers = [];
effect(function() { { var _c = bodyDisposers; for (var _i = 0; _i < _c.length; _i++) { var d = _c[_i]; d(); } }
bodyDisposers = [];
return tryCatch(function() { return (function() {
var frag = createFragment();
withIslandScope(function(disposable) { bodyDisposers.push(disposable);
return registerInScope(disposable); }, function() { return forEach(function(child) { return domAppend(frag, renderToDom(child, env, ns)); }, bodyExprs); });
domSetProp(container, "innerHTML", "");
return domAppend(container, frag);
})(); }, function(err) { return NIL; }); });
return registerInScope(function() { { var _c = bodyDisposers; for (var _i = 0; _i < _c.length; _i++) { var d = _c[_i]; d(); } }
return (bodyDisposers = []); });
})();
return container;
})(); };
// === Transpiled from engine === // === Transpiled from engine ===
@@ -3114,6 +3138,11 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
return state; return state;
})(); }; })(); };
// transition
var transition = function(pendingSig, thunk) { reset_b(pendingSig, true);
return scheduleIdle(function() { batch(thunk);
return reset_b(pendingSig, false); }); };
// ========================================================================= // =========================================================================
// Platform interface — DOM adapter (browser-only) // Platform interface — DOM adapter (browser-only)
@@ -3406,6 +3435,11 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
function promiseResolve(val) { return Promise.resolve(val); } function promiseResolve(val) { return Promise.resolve(val); }
function promiseThen(p, onResolve, onReject) {
if (!p || !p.then) return p;
return onReject ? p.then(onResolve, onReject) : p.then(onResolve);
}
function promiseCatch(p, fn) { return p && p.catch ? p.catch(fn) : p; } function promiseCatch(p, fn) { return p && p.catch ? p.catch(fn) : p; }
// --- Abort controllers --- // --- Abort controllers ---
@@ -3849,6 +3883,10 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
function errorMessage(e) { function errorMessage(e) {
return e && e.message ? e.message : String(e); return e && e.message ? e.message : String(e);
} }
function scheduleIdle(fn) {
if (typeof requestIdleCallback !== "undefined") requestIdleCallback(fn);
else setTimeout(fn, 0);
}
function elementValue(el) { return el && el.value !== undefined ? el.value : NIL; } function elementValue(el) { return el && el.value !== undefined ? el.value : NIL; }
function domAddListener(el, event, fn, opts) { function domAddListener(el, event, fn, opts) {

View File

@@ -14,7 +14,7 @@
// ========================================================================= // =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-08T16:35:00Z"; var SX_VERSION = "2026-03-08T16:36:52Z";
function isNil(x) { return x === NIL || x === null || x === undefined; } function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); } function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -1546,7 +1546,7 @@ return result; }, args);
var renderDomUnknownComponent = function(name) { return error((String("Unknown component: ") + String(name))); }; var renderDomUnknownComponent = function(name) { return error((String("Unknown component: ") + String(name))); };
// RENDER_DOM_FORMS // RENDER_DOM_FORMS
var RENDER_DOM_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defisland", "defmacro", "defstyle", "defhandler", "map", "map-indexed", "filter", "for-each", "portal", "error-boundary"]; var RENDER_DOM_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defisland", "defmacro", "defstyle", "defhandler", "map", "map-indexed", "filter", "for-each", "portal", "error-boundary", "suspense"];
// render-dom-form? // render-dom-form?
var isRenderDomForm = function(name) { return contains(RENDER_DOM_FORMS, name); }; var isRenderDomForm = function(name) { return contains(RENDER_DOM_FORMS, name); };
@@ -1604,7 +1604,7 @@ return result; }, args);
return domAppend(frag, val); return domAppend(frag, val);
})(); }, coll); })(); }, coll);
return frag; return frag;
})() : (isSxTruthy((name == "filter")) ? renderToDom(trampoline(evalExpr(expr, env)), env, ns) : (isSxTruthy((name == "portal")) ? renderDomPortal(args, env, ns) : (isSxTruthy((name == "error-boundary")) ? renderDomErrorBoundary(args, env, ns) : (isSxTruthy((name == "for-each")) ? (function() { })() : (isSxTruthy((name == "filter")) ? renderToDom(trampoline(evalExpr(expr, env)), env, ns) : (isSxTruthy((name == "portal")) ? renderDomPortal(args, env, ns) : (isSxTruthy((name == "error-boundary")) ? renderDomErrorBoundary(args, env, ns) : (isSxTruthy((name == "suspense")) ? renderDomSuspense(args, env, ns) : (isSxTruthy((name == "for-each")) ? (function() {
var f = trampoline(evalExpr(nth(expr, 1), env)); var f = trampoline(evalExpr(nth(expr, 1), env));
var coll = trampoline(evalExpr(nth(expr, 2), env)); var coll = trampoline(evalExpr(nth(expr, 2), env));
var frag = createFragment(); var frag = createFragment();
@@ -1613,7 +1613,7 @@ return result; }, args);
return domAppend(frag, val); return domAppend(frag, val);
})(); } } })(); } }
return frag; return frag;
})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns)))))))))))))); }; })() : renderToDom(trampoline(evalExpr(expr, env)), env, ns))))))))))))))); };
// render-lambda-dom // render-lambda-dom
var renderLambdaDom = function(f, args, env, ns) { return (function() { var renderLambdaDom = function(f, args, env, ns) { return (function() {
@@ -1805,6 +1805,30 @@ return (boundaryDisposers = []); });
})(); })();
})(); }; })(); };
// render-dom-suspense
var renderDomSuspense = function(args, env, ns) { return (function() {
var fallbackExpr = first(args);
var bodyExprs = rest(args);
var container = domCreateElement("div", NIL);
domSetAttr(container, "data-sx-suspense", "true");
domAppend(container, renderToDom(fallbackExpr, env, ns));
(function() {
var bodyDisposers = [];
effect(function() { { var _c = bodyDisposers; for (var _i = 0; _i < _c.length; _i++) { var d = _c[_i]; d(); } }
bodyDisposers = [];
return tryCatch(function() { return (function() {
var frag = createFragment();
withIslandScope(function(disposable) { bodyDisposers.push(disposable);
return registerInScope(disposable); }, function() { return forEach(function(child) { return domAppend(frag, renderToDom(child, env, ns)); }, bodyExprs); });
domSetProp(container, "innerHTML", "");
return domAppend(container, frag);
})(); }, function(err) { return NIL; }); });
return registerInScope(function() { { var _c = bodyDisposers; for (var _i = 0; _i < _c.length; _i++) { var d = _c[_i]; d(); } }
return (bodyDisposers = []); });
})();
return container;
})(); };
// === Transpiled from engine === // === Transpiled from engine ===
@@ -3114,6 +3138,11 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
return state; return state;
})(); }; })(); };
// transition
var transition = function(pendingSig, thunk) { reset_b(pendingSig, true);
return scheduleIdle(function() { batch(thunk);
return reset_b(pendingSig, false); }); };
// ========================================================================= // =========================================================================
// Platform interface — DOM adapter (browser-only) // Platform interface — DOM adapter (browser-only)
@@ -3406,6 +3435,11 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
function promiseResolve(val) { return Promise.resolve(val); } function promiseResolve(val) { return Promise.resolve(val); }
function promiseThen(p, onResolve, onReject) {
if (!p || !p.then) return p;
return onReject ? p.then(onResolve, onReject) : p.then(onResolve);
}
function promiseCatch(p, fn) { return p && p.catch ? p.catch(fn) : p; } function promiseCatch(p, fn) { return p && p.catch ? p.catch(fn) : p; }
// --- Abort controllers --- // --- Abort controllers ---
@@ -3849,6 +3883,10 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
function errorMessage(e) { function errorMessage(e) {
return e && e.message ? e.message : String(e); return e && e.message ? e.message : String(e);
} }
function scheduleIdle(fn) {
if (typeof requestIdleCallback !== "undefined") requestIdleCallback(fn);
else setTimeout(fn, 0);
}
function elementValue(el) { return el && el.value !== undefined ? el.value : NIL; } function elementValue(el) { return el && el.value !== undefined ? el.value : NIL; }
function domAddListener(el, event, fn, opts) { function domAddListener(el, event, fn, opts) {

View File

@@ -309,7 +309,8 @@
(define RENDER_DOM_FORMS (define RENDER_DOM_FORMS
(list "if" "when" "cond" "case" "let" "let*" "begin" "do" (list "if" "when" "cond" "case" "let" "let*" "begin" "do"
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler" "define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
"map" "map-indexed" "filter" "for-each" "portal" "error-boundary")) "map" "map-indexed" "filter" "for-each" "portal"
"error-boundary" "suspense"))
(define render-dom-form? (define render-dom-form?
(fn (name) (fn (name)
@@ -434,6 +435,10 @@
(= name "error-boundary") (= name "error-boundary")
(render-dom-error-boundary args env ns) (render-dom-error-boundary args env ns)
;; suspense — show fallback while resource is loading
(= name "suspense")
(render-dom-suspense args env ns)
;; for-each (render variant) ;; for-each (render variant)
(= name "for-each") (= name "for-each")
(let ((f (trampoline (eval-expr (nth expr 1) env))) (let ((f (trampoline (eval-expr (nth expr 1) env)))
@@ -823,6 +828,66 @@
container)))) container))))
;; --------------------------------------------------------------------------
;; render-dom-suspense — show fallback while resource is loading
;; --------------------------------------------------------------------------
;;
;; (suspense fallback-expr body...)
;;
;; Renders fallback-expr initially. When used with a resource signal,
;; an effect watches the resource state and swaps in the body content
;; once loading is complete. If the resource errors, renders the error.
;;
;; The simplest pattern: wrap a resource deref in suspense.
;;
;; (suspense
;; (div "Loading...")
;; (let ((data (get (deref user-resource) "data")))
;; (div (get data "name"))))
(define render-dom-suspense
(fn (args env ns)
(let ((fallback-expr (first args))
(body-exprs (rest args))
(container (dom-create-element "div" nil)))
(dom-set-attr container "data-sx-suspense" "true")
;; Render fallback immediately
(dom-append container (render-to-dom fallback-expr env ns))
;; Try to render body — if it works, replace fallback
;; The body typically derefs a resource signal, which triggers
;; an effect that re-renders when the resource resolves
(let ((body-disposers (list)))
(effect (fn ()
;; Dispose previous body renders
(for-each (fn (d) (d)) body-disposers)
(set! body-disposers (list))
;; Try rendering the body
(try-catch
(fn ()
(let ((frag (create-fragment)))
(with-island-scope
(fn (disposable)
(append! body-disposers disposable)
(register-in-scope disposable))
(fn ()
(for-each
(fn (child)
(dom-append frag (render-to-dom child env ns)))
body-exprs)))
;; Success — replace container content with body
(dom-set-prop container "innerHTML" "")
(dom-append container frag)))
(fn (err)
;; Body threw — keep showing fallback (or show error)
nil))))
;; Register cleanup
(register-in-scope
(fn ()
(for-each (fn (d) (d)) body-disposers)
(set! body-disposers (list)))))
container)))
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
;; Platform interface — DOM adapter ;; Platform interface — DOM adapter
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------

View File

@@ -449,6 +449,7 @@ class JSEmitter:
"dom-focus": "domFocus", "dom-focus": "domFocus",
"try-catch": "tryCatch", "try-catch": "tryCatch",
"error-message": "errorMessage", "error-message": "errorMessage",
"schedule-idle": "scheduleIdle",
"element-value": "elementValue", "element-value": "elementValue",
"validate-for-request": "validateForRequest", "validate-for-request": "validateForRequest",
"with-transition": "withTransition", "with-transition": "withTransition",
@@ -3031,6 +3032,11 @@ PLATFORM_ORCHESTRATION_JS = """
function promiseResolve(val) { return Promise.resolve(val); } function promiseResolve(val) { return Promise.resolve(val); }
function promiseThen(p, onResolve, onReject) {
if (!p || !p.then) return p;
return onReject ? p.then(onResolve, onReject) : p.then(onResolve);
}
function promiseCatch(p, fn) { return p && p.catch ? p.catch(fn) : p; } function promiseCatch(p, fn) { return p && p.catch ? p.catch(fn) : p; }
// --- Abort controllers --- // --- Abort controllers ---
@@ -3474,6 +3480,10 @@ PLATFORM_ORCHESTRATION_JS = """
function errorMessage(e) { function errorMessage(e) {
return e && e.message ? e.message : String(e); return e && e.message ? e.message : String(e);
} }
function scheduleIdle(fn) {
if (typeof requestIdleCallback !== "undefined") requestIdleCallback(fn);
else setTimeout(fn, 0);
}
function elementValue(el) { return el && el.value !== undefined ? el.value : NIL; } function elementValue(el) { return el && el.value !== undefined ? el.value : NIL; }
function domAddListener(el, event, fn, opts) { function domAddListener(el, event, fn, opts) {

View File

@@ -438,3 +438,29 @@
(fn (data) (reset! state (dict "loading" false "data" data "error" nil))) (fn (data) (reset! state (dict "loading" false "data" data "error" nil)))
(fn (err) (reset! state (dict "loading" false "data" nil "error" err)))) (fn (err) (reset! state (dict "loading" false "data" nil "error" err))))
state))) state)))
;; ==========================================================================
;; 16. Transitions — non-urgent signal updates
;; ==========================================================================
;;
;; Transitions mark updates as non-urgent. The thunk's signal writes are
;; deferred to an idle callback, keeping the UI responsive during expensive
;; computations.
;;
;; (transition pending-signal thunk)
;;
;; Sets pending-signal to true, schedules thunk via requestIdleCallback
;; (or setTimeout 0 as fallback), then sets pending-signal to false when
;; the thunk completes. Signal writes inside the thunk are batched.
;;
;; Platform interface required:
;; (schedule-idle thunk) → void — requestIdleCallback or setTimeout(fn, 0)
(define transition
(fn (pending-sig thunk)
(reset! pending-sig true)
(schedule-idle
(fn ()
(batch thunk)
(reset! pending-sig false)))))

View File

@@ -149,7 +149,19 @@
(td :class "px-3 py-2 text-stone-700" "Phase 2 remaining") (td :class "px-3 py-2 text-stone-700" "Phase 2 remaining")
(td :class "px-3 py-2 text-stone-500 font-medium" "P2") (td :class "px-3 py-2 text-stone-500 font-medium" "P2")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" (td :class "px-3 py-2 font-mono text-xs text-stone-500"
(a :href "/reactive-islands/phase2" :sx-get "/reactive-islands/phase2" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "Error boundaries, suspense, transitions"))))))))) (a :href "/reactive-islands/phase2" :sx-get "/reactive-islands/phase2" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "Error boundaries, suspense, transitions")))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Error boundaries")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx: error-boundary render-dom form"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Suspense + resource")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx, signals.sx"))
(tr
(td :class "px-3 py-2 text-stone-700" "Transitions")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "signals.sx: transition, schedule-idle"))))))))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Live demo islands ;; Live demo islands
@@ -407,12 +419,8 @@
(p :class "mt-2 text-sm text-stone-500" "Run: " (code "python3 shared/sx/tests/run.py signals"))) (p :class "mt-2 text-sm text-stone-500" "Run: " (code "python3 shared/sx/tests/run.py signals")))
(~doc-section :title "What's next" :id "next" (~doc-section :title "What's next" :id "next"
(p "Phase 1 and Phase 2 P0/P1 features are complete. The remaining P2 features are optional enhancements:") (p "Phase 1 and Phase 2 are complete. The reactive islands system now includes: signals, effects, computed values, islands, disposal, stores, event bridges, reactive DOM rendering, input binding, keyed reconciliation, refs, portals, error boundaries, suspense, resource, and transitions.")
(ul :class "space-y-2 text-stone-600 list-disc pl-5" (p "See the " (a :href "/reactive-islands/phase2" :sx-get "/reactive-islands/phase2" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "Phase 2 plan") " for the full feature list and design details."))))
(li (strong "Error boundaries") " — catch errors in island subtrees, render fallback UI")
(li (strong "Suspense + resource") " — async-aware rendering with loading states")
(li (strong "Transitions") " — non-urgent signal updates for expensive re-renders"))
(p "See the " (a :href "/reactive-islands/phase2" :sx-get "/reactive-islands/phase2" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "Phase 2 plan") " for details."))))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
@@ -660,11 +668,14 @@
(td :class "px-3 py-2 text-stone-700" "Refs + portals") (td :class "px-3 py-2 text-stone-700" "Refs + portals")
(td :class "px-3 py-2 text-green-700 font-medium" "Done") (td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "ref, ref-get, ref-set!, :ref, portal")) (td :class "px-3 py-2 text-stone-700" "ref, ref-get, ref-set!, :ref, portal"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Error boundaries + suspense")
(td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 text-stone-700" "error-boundary, suspense, resource"))
(tr (tr
(td :class "px-3 py-2 text-stone-700" "Phase 2 remaining") (td :class "px-3 py-2 text-stone-700" "Transitions")
(td :class "px-3 py-2 text-violet-700 font-medium" (td :class "px-3 py-2 text-green-700 font-medium" "Done")
(a :href "/reactive-islands/phase2" :sx-get "/reactive-islands/phase2" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "Details →")) (td :class "px-3 py-2 text-stone-700" "signals.sx: transition, schedule-idle"))))))
(td :class "px-3 py-2 text-stone-700" "Error boundaries, suspense, transitions"))))))
(~doc-section :title "Design Principles" :id "principles" (~doc-section :title "Design Principles" :id "principles"
(ol :class "space-y-3 text-stone-600 list-decimal list-inside" (ol :class "space-y-3 text-stone-600 list-decimal list-inside"
@@ -720,17 +731,17 @@
(tr :class "border-b border-stone-100" (tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Error boundaries") (td :class "px-3 py-2 text-stone-700" "Error boundaries")
(td :class "px-3 py-2 text-stone-500 text-xs" "componentDidCatch") (td :class "px-3 py-2 text-stone-500 text-xs" "componentDidCatch")
(td :class "px-3 py-2 text-stone-500 font-medium" "P2") (td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx")) (td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx"))
(tr :class "border-b border-stone-100" (tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Suspense") (td :class "px-3 py-2 text-stone-700" "Suspense")
(td :class "px-3 py-2 text-stone-500 text-xs" "Suspense + lazy") (td :class "px-3 py-2 text-stone-500 text-xs" "Suspense + lazy")
(td :class "px-3 py-2 text-stone-500 font-medium" "P2") (td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx, signals.sx")) (td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx, signals.sx"))
(tr (tr
(td :class "px-3 py-2 text-stone-700" "Transitions") (td :class "px-3 py-2 text-stone-700" "Transitions")
(td :class "px-3 py-2 text-stone-500 text-xs" "startTransition") (td :class "px-3 py-2 text-stone-500 text-xs" "startTransition")
(td :class "px-3 py-2 text-stone-500 font-medium" "P2") (td :class "px-3 py-2 text-green-700 font-medium" "Done")
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "signals.sx")))))) (td :class "px-3 py-2 font-mono text-xs text-stone-500" "signals.sx"))))))
;; ----------------------------------------------------------------------- ;; -----------------------------------------------------------------------