Merge worktree-typed into macros: defcomp type annotations

This commit is contained in:
2026-03-11 21:02:12 +00:00
parent 477ce766ff
commit 95ffc0ecb7
6 changed files with 98 additions and 45 deletions

View File

@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-11T21:00:15Z";
var SX_VERSION = "2026-03-11T21:02:07Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -1200,7 +1200,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
// process-bindings
var processBindings = function(bindings, env) { return (function() {
var local = envExtend(env);
{ var _c = bindings; for (var _i = 0; _i < _c.length; _i++) { var [Symbol('pair'), Keyword('as'), Symbol('list')] = _c[_i]; if (isSxTruthy((isSxTruthy((typeOf(pair) == "list")) && (len(pair) >= 2)))) {
{ var _c = bindings; for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; if (isSxTruthy((isSxTruthy((typeOf(pair) == "list")) && (len(pair) >= 2)))) {
(function() {
var name = (isSxTruthy((typeOf(first(pair)) == "symbol")) ? symbolName(first(pair)) : (String(first(pair))));
return envSet(local, name, trampoline(evalExpr(nth(pair, 1), local)));
@@ -2171,7 +2171,7 @@ return (function() {
var tokens = split(trim(part), " ");
return (isSxTruthy(isEmpty(tokens)) ? NIL : (isSxTruthy((isSxTruthy((first(tokens) == "every")) && (len(tokens) >= 2))) ? {["event"]: "every", ["modifiers"]: {["interval"]: parseTime(nth(tokens, 1))}} : (function() {
var mods = {};
{ var _c = rest(tokens); for (var _i = 0; _i < _c.length; _i++) { var [Symbol('tok'), Keyword('as'), Symbol('string')] = _c[_i]; (isSxTruthy((tok == "once")) ? dictSet(mods, "once", true) : (isSxTruthy((tok == "changed")) ? dictSet(mods, "changed", true) : (isSxTruthy(startsWith(tok, "delay:")) ? dictSet(mods, "delay", parseTime(slice(tok, 6))) : (isSxTruthy(startsWith(tok, "from:")) ? dictSet(mods, "from", slice(tok, 5)) : NIL)))); } }
{ var _c = rest(tokens); for (var _i = 0; _i < _c.length; _i++) { var tok = _c[_i]; (isSxTruthy((tok == "once")) ? dictSet(mods, "once", true) : (isSxTruthy((tok == "changed")) ? dictSet(mods, "changed", true) : (isSxTruthy(startsWith(tok, "delay:")) ? dictSet(mods, "delay", parseTime(slice(tok, 6))) : (isSxTruthy(startsWith(tok, "from:")) ? dictSet(mods, "from", slice(tok, 5)) : NIL)))); } }
return {["event"]: first(tokens), ["modifiers"]: mods};
})()));
})(); }, rawParts));
@@ -2217,7 +2217,7 @@ return (function() {
var parts = split(sxOr(rawSwap, DEFAULT_SWAP), " ");
var style = first(parts);
var useTransition = globalTransitions_p;
{ var _c = rest(parts); for (var _i = 0; _i < _c.length; _i++) { var [Symbol('p'), Keyword('as'), Symbol('string')] = _c[_i]; (isSxTruthy((p == "transition:true")) ? (useTransition = true) : (isSxTruthy((p == "transition:false")) ? (useTransition = false) : NIL)); } }
{ var _c = rest(parts); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; (isSxTruthy((p == "transition:true")) ? (useTransition = true) : (isSxTruthy((p == "transition:false")) ? (useTransition = false) : NIL)); } }
return {["style"]: style, ["transition"]: useTransition};
})(); };
@@ -2270,7 +2270,7 @@ return (function() {
// find-oob-swaps
var findOobSwaps = function(container) { return (function() {
var results = [];
{ var _c = ["sx-swap-oob", "hx-swap-oob"]; for (var _i = 0; _i < _c.length; _i++) { var [Symbol('attr'), Keyword('as'), Symbol('string')] = _c[_i]; (function() {
{ var _c = ["sx-swap-oob", "hx-swap-oob"]; for (var _i = 0; _i < _c.length; _i++) { var attr = _c[_i]; (function() {
var oobEls = domQueryAll(container, (String("[") + String(attr) + String("]")));
return forEach(function(oob) { return (function() {
var swapType = sxOr(domGetAttr(oob, attr), "outerHTML");
@@ -2289,7 +2289,7 @@ return (function() {
var syncAttrs = function(oldEl, newEl) { return (function() {
var raStr = sxOr(domGetAttr(oldEl, "data-sx-reactive-attrs"), "");
var reactiveAttrs = (isSxTruthy(isEmpty(raStr)) ? [] : split(raStr, ","));
{ var _c = domAttrList(newEl); for (var _i = 0; _i < _c.length; _i++) { var [Symbol('attr'), Keyword('as'), Symbol('list')] = _c[_i]; (function() {
{ var _c = domAttrList(newEl); for (var _i = 0; _i < _c.length; _i++) { var attr = _c[_i]; (function() {
var name = first(attr);
var val = nth(attr, 1);
return (isSxTruthy((isSxTruthy(!isSxTruthy((domGetAttr(oldEl, name) == val))) && !isSxTruthy(contains(reactiveAttrs, name)))) ? domSetAttr(oldEl, name, val) : NIL);
@@ -2543,7 +2543,7 @@ return (function() {
var headers = buildRequestHeaders(el, loadedComponentNames(), _cssHash);
var csrf = csrfToken();
if (isSxTruthy(extraParams)) {
{ var _c = keys(extraParams); for (var _i = 0; _i < _c.length; _i++) { var [Symbol('k'), Keyword('as'), Symbol('string')] = _c[_i]; headers[k] = get(extraParams, k); } }
{ var _c = keys(extraParams); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; headers[k] = get(extraParams, k); } }
}
if (isSxTruthy(ct)) {
headers["Content-Type"] = ct;
@@ -2784,7 +2784,7 @@ return domAppendToHead(link); }, domQueryAll(container, "link[rel=\"stylesheet\"
var base = pageName;
return (isSxTruthy(sxOr(isNil(params), isEmpty(keys(params)))) ? base : (function() {
var parts = [];
{ var _c = keys(params); for (var _i = 0; _i < _c.length; _i++) { var [Symbol('k'), Keyword('as'), Symbol('string')] = _c[_i]; parts.push((String(k) + String("=") + String(get(params, k)))); } }
{ var _c = keys(params); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; parts.push((String(k) + String("=") + String(get(params, k)))); } }
return (String(base) + String(":") + String(join("&", parts)));
})());
})(); };
@@ -2799,7 +2799,7 @@ return domAppendToHead(link); }, domQueryAll(container, "link[rel=\"stylesheet\"
var pageDataCacheSet = function(cacheKey, data) { return dictSet(_pageDataCache, cacheKey, {"data": data, "ts": nowMs()}); };
// invalidate-page-cache
var invalidatePageCache = function(pageName) { { var _c = keys(_pageDataCache); for (var _i = 0; _i < _c.length; _i++) { var [Symbol('k'), Keyword('as'), Symbol('string')] = _c[_i]; if (isSxTruthy(sxOr((k == pageName), startsWith(k, (String(pageName) + String(":")))))) {
var invalidatePageCache = function(pageName) { { var _c = keys(_pageDataCache); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; if (isSxTruthy(sxOr((k == pageName), startsWith(k, (String(pageName) + String(":")))))) {
_pageDataCache[k] = NIL;
} } }
swPostMessage({"type": "invalidate", "page": pageName});
@@ -3200,7 +3200,7 @@ return (function() {
var comp = envGet(env, fullName);
return (isSxTruthy(!isSxTruthy(isComponent(comp))) ? error((String("Unknown component: ") + String(fullName))) : (function() {
var callExpr = [makeSymbol(fullName)];
{ var _c = keys(kwargs); for (var _i = 0; _i < _c.length; _i++) { var [Symbol('k'), Keyword('as'), Symbol('string')] = _c[_i]; callExpr.push(makeKeyword(toKebab(k)));
{ var _c = keys(kwargs); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; callExpr.push(makeKeyword(toKebab(k)));
callExpr.push(dictGet(kwargs, k)); } }
return renderToDom(callExpr, env, NIL);
})());
@@ -3280,7 +3280,7 @@ callExpr.push(dictGet(kwargs, k)); } }
var kwargs = sxOr(first(sxParse(stateSx)), {});
var disposers = [];
var local = envMerge(componentClosure(comp), env);
{ var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var [Symbol('p'), Keyword('as'), Symbol('string')] = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
{ var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
return (function() {
var bodyDom = withIslandScope(function(disposable) { return append_b(disposers, disposable); }, function() { return renderToDom(componentBody(comp), local, NIL); });
domSetTextContent(el, "");
@@ -3358,7 +3358,7 @@ callExpr.push(dictGet(kwargs, k)); } }
var componentsNeeded = function(pageSource, env) { return (function() {
var direct = scanComponentsFromSource(pageSource);
var allNeeded = [];
{ var _c = direct; for (var _i = 0; _i < _c.length; _i++) { var [Symbol('name'), Keyword('as'), Symbol('string')] = _c[_i]; if (isSxTruthy(!isSxTruthy(contains(allNeeded, name)))) {
{ var _c = direct; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; if (isSxTruthy(!isSxTruthy(contains(allNeeded, name)))) {
allNeeded.push(name);
}
(function() {
@@ -3378,11 +3378,11 @@ callExpr.push(dictGet(kwargs, k)); } }
var pageCssClasses = function(pageSource, env) { return (function() {
var needed = componentsNeeded(pageSource, env);
var classes = [];
{ var _c = needed; for (var _i = 0; _i < _c.length; _i++) { var [Symbol('name'), Keyword('as'), Symbol('string')] = _c[_i]; (function() {
{ var _c = needed; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; (function() {
var val = envGet(env, name);
return (isSxTruthy((typeOf(val) == "component")) ? forEach(function(cls) { return (isSxTruthy(!isSxTruthy(contains(classes, cls))) ? append_b(classes, cls) : NIL); }, componentCssClasses(val)) : NIL);
})(); } }
{ var _c = scanCssClasses(pageSource); for (var _i = 0; _i < _c.length; _i++) { var [Symbol('cls'), Keyword('as'), Symbol('string')] = _c[_i]; if (isSxTruthy(!isSxTruthy(contains(classes, cls)))) {
{ var _c = scanCssClasses(pageSource); for (var _i = 0; _i < _c.length; _i++) { var cls = _c[_i]; if (isSxTruthy(!isSxTruthy(contains(classes, cls)))) {
classes.push(cls);
} } }
return classes;
@@ -3459,7 +3459,7 @@ callExpr.push(dictGet(kwargs, k)); } }
var serverList = [];
var clientList = [];
var ioDeps = [];
{ var _c = needed; for (var _i = 0; _i < _c.length; _i++) { var [Symbol('name'), Keyword('as'), Symbol('string')] = _c[_i]; (function() {
{ var _c = needed; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; (function() {
var target = renderTarget(name, env, ioNames);
compTargets[name] = target;
return (isSxTruthy((target == "server")) ? (append_b(serverList, name), forEach(function(ioRef) { return (isSxTruthy(!isSxTruthy(contains(ioDeps, ioRef))) ? append_b(ioDeps, ioRef) : NIL); }, componentIoRefsCached(name, env, ioNames))) : append_b(clientList, name));
@@ -3484,7 +3484,7 @@ callExpr.push(dictGet(kwargs, k)); } }
var result = {};
var items = slice(expr, 2);
var n = len(items);
{ var _c = range(0, n); for (var _i = 0; _i < _c.length; _i++) { var [Symbol('idx'), Keyword('as'), Symbol('number')] = _c[_i]; if (isSxTruthy((isSxTruthy(((idx + 1) < n)) && (typeOf(nth(items, idx)) == "keyword")))) {
{ var _c = range(0, n); for (var _i = 0; _i < _c.length; _i++) { var idx = _c[_i]; if (isSxTruthy((isSxTruthy(((idx + 1) < n)) && (typeOf(nth(items, idx)) == "keyword")))) {
(function() {
var key = keywordName(nth(items, idx));
var val = nth(items, (idx + 1));
@@ -3555,7 +3555,7 @@ callExpr.push(dictGet(kwargs, k)); } }
// build-bundle-analysis
var buildBundleAnalysis = function(pagesRaw, componentsRaw, totalComponents, totalMacros, pureCount, ioCount) { return (function() {
var pagesData = [];
{ var _c = pagesRaw; for (var _i = 0; _i < _c.length; _i++) { var [Symbol('page'), Keyword('as'), Symbol('dict')] = _c[_i]; (function() {
{ var _c = pagesRaw; for (var _i = 0; _i < _c.length; _i++) { var page = _c[_i]; (function() {
var neededNames = get(page, "needed-names");
var n = len(neededNames);
var pct = (isSxTruthy((totalComponents > 0)) ? round(((n / totalComponents) * 100)) : 0);
@@ -3564,7 +3564,7 @@ callExpr.push(dictGet(kwargs, k)); } }
var ioInPage = 0;
var pageIoRefs = [];
var compDetails = [];
{ var _c = neededNames; for (var _i = 0; _i < _c.length; _i++) { var [Symbol('compName'), Keyword('as'), Symbol('string')] = _c[_i]; (function() {
{ var _c = neededNames; for (var _i = 0; _i < _c.length; _i++) { var compName = _c[_i]; (function() {
var info = get(componentsRaw, compName);
return (isSxTruthy(!isSxTruthy(isNil(info))) ? ((isSxTruthy(get(info, "is-pure")) ? (pureInPage = (pureInPage + 1)) : ((ioInPage = (ioInPage + 1)), forEach(function(ref) { return (isSxTruthy(!isSxTruthy(some(function(r) { return (r == ref); }, pageIoRefs))) ? append_b(pageIoRefs, ref) : NIL); }, sxOr(get(info, "io-refs"), [])))), append_b(compDetails, {"name": compName, "is-pure": get(info, "is-pure"), "affinity": get(info, "affinity"), "render-target": get(info, "render-target"), "io-refs": sxOr(get(info, "io-refs"), []), "deps": sxOr(get(info, "deps"), []), "source": get(info, "source")})) : NIL);
})(); } }
@@ -3578,7 +3578,7 @@ callExpr.push(dictGet(kwargs, k)); } }
var pagesData = [];
var clientCount = 0;
var serverCount = 0;
{ var _c = pagesRaw; for (var _i = 0; _i < _c.length; _i++) { var [Symbol('page'), Keyword('as'), Symbol('dict')] = _c[_i]; (function() {
{ var _c = pagesRaw; for (var _i = 0; _i < _c.length; _i++) { var page = _c[_i]; (function() {
var hasData = get(page, "has-data");
var contentSrc = sxOr(get(page, "content-src"), "");
var mode = NIL;
@@ -3649,7 +3649,7 @@ callExpr.push(dictGet(kwargs, k)); } }
var findMatchingRoute = function(path, routes) { return (function() {
var pathSegs = splitPathSegments(path);
var result = NIL;
{ var _c = routes; for (var _i = 0; _i < _c.length; _i++) { var [Symbol('route'), Keyword('as'), Symbol('dict')] = _c[_i]; if (isSxTruthy(isNil(result))) {
{ var _c = routes; for (var _i = 0; _i < _c.length; _i++) { var route = _c[_i]; if (isSxTruthy(isNil(result))) {
(function() {
var params = matchRouteSegments(pathSegs, get(route, "parsed"));
return (isSxTruthy(!isSxTruthy(isNil(params))) ? (function() {
@@ -3697,7 +3697,7 @@ callExpr.push(dictGet(kwargs, k)); } }
var deps = [];
var computeCtx = NIL;
return (function() {
var recompute = function() { { var _c = signalDeps(s); for (var _i = 0; _i < _c.length; _i++) { var [Symbol('dep'), Keyword('as'), Symbol('signal')] = _c[_i]; signalRemoveSub(dep, recompute); } }
var recompute = function() { { var _c = signalDeps(s); for (var _i = 0; _i < _c.length; _i++) { var dep = _c[_i]; signalRemoveSub(dep, recompute); } }
signalSetDeps(s, []);
return (function() {
var ctx = makeTrackingContext(recompute);
@@ -3747,7 +3747,7 @@ return (function() {
if (isSxTruthy(cleanupFn)) {
invoke(cleanupFn);
}
{ var _c = deps; for (var _i = 0; _i < _c.length; _i++) { var [Symbol('dep'), Keyword('as'), Symbol('signal')] = _c[_i]; signalRemoveSub(dep, runEffect); } }
{ var _c = deps; for (var _i = 0; _i < _c.length; _i++) { var dep = _c[_i]; signalRemoveSub(dep, runEffect); } }
return (deps = []); };
registerInScope(disposeFn);
return disposeFn;
@@ -3771,7 +3771,7 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
return (function() {
var seen = [];
var pending = [];
{ var _c = queue; for (var _i = 0; _i < _c.length; _i++) { var [Symbol('s'), Keyword('as'), Symbol('signal')] = _c[_i]; { var _c = signalSubscribers(s); for (var _i = 0; _i < _c.length; _i++) { var [Symbol('sub'), Keyword('as'), Symbol('lambda')] = _c[_i]; if (isSxTruthy(!isSxTruthy(contains(seen, sub)))) {
{ var _c = queue; for (var _i = 0; _i < _c.length; _i++) { var s = _c[_i]; { var _c = signalSubscribers(s); for (var _i = 0; _i < _c.length; _i++) { var sub = _c[_i]; if (isSxTruthy(!isSxTruthy(contains(seen, sub)))) {
seen.push(sub);
pending.push(sub);
} } } } }
@@ -3995,6 +3995,18 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
function domGetProp(el, name) { return el ? el[name] : NIL; }
function domSetProp(el, name, val) { if (el) el[name] = val; }
// Call a method on an object with correct this binding: (dom-call-method obj "methodName" arg1 arg2 ...)
function domCallMethod() {
var obj = arguments[0], method = arguments[1];
var args = Array.prototype.slice.call(arguments, 2);
console.log("[sx] dom-call-method:", obj, method, args);
if (obj && typeof obj[method] === 'function') {
try { return obj[method].apply(obj, args); }
catch(e) { console.error("[sx] dom-call-method error:", e); return NIL; }
}
console.warn("[sx] dom-call-method: method not found or obj null", obj, method);
return NIL;
}
function domAddClass(el, cls) {
if (el && el.classList) el.classList.add(cls);
@@ -5199,6 +5211,8 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
PRIMITIVES["dom-focus"] = domFocus;
PRIMITIVES["dom-tag-name"] = domTagName;
PRIMITIVES["dom-get-prop"] = domGetProp;
PRIMITIVES["dom-set-prop"] = domSetProp;
PRIMITIVES["dom-call-method"] = domCallMethod;
PRIMITIVES["stop-propagation"] = stopPropagation_;
PRIMITIVES["error-message"] = errorMessage;
PRIMITIVES["schedule-idle"] = scheduleIdle;

View File

@@ -1369,9 +1369,16 @@
;; Inline lambda → for loop
(let ((params (nth fn-expr 1))
(body (rest (rest fn-expr)))
(p (if (= (type-of (first params)) "symbol")
(symbol-name (first params))
(str (first params))))
(raw-p (first params))
(p (cond
(= (type-of raw-p) "symbol")
(symbol-name raw-p)
;; (name :as type) annotation → extract name
(and (= (type-of raw-p) "list") (= (len raw-p) 3)
(= (type-of (nth raw-p 1)) "keyword")
(= (keyword-name (nth raw-p 1)) "as"))
(symbol-name (first raw-p))
:else (str raw-p)))
(p-js (js-mangle p)))
(str "{ var _c = " coll "; for (var _i = 0; _i < _c.length; _i++) { var "
p-js " = _c[_i]; "

View File

@@ -1655,6 +1655,18 @@ PLATFORM_DOM_JS = """
function domGetProp(el, name) { return el ? el[name] : NIL; }
function domSetProp(el, name, val) { if (el) el[name] = val; }
// Call a method on an object with correct this binding: (dom-call-method obj "methodName" arg1 arg2 ...)
function domCallMethod() {
var obj = arguments[0], method = arguments[1];
var args = Array.prototype.slice.call(arguments, 2);
console.log("[sx] dom-call-method:", obj, method, args);
if (obj && typeof obj[method] === 'function') {
try { return obj[method].apply(obj, args); }
catch(e) { console.error("[sx] dom-call-method error:", e); return NIL; }
}
console.warn("[sx] dom-call-method: method not found or obj null", obj, method);
return NIL;
}
function domAddClass(el, cls) {
if (el && el.classList) el.classList.add(cls);
@@ -2872,6 +2884,8 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False, has_deps=False, has_
PRIMITIVES["dom-focus"] = domFocus;
PRIMITIVES["dom-tag-name"] = domTagName;
PRIMITIVES["dom-get-prop"] = domGetProp;
PRIMITIVES["dom-set-prop"] = domSetProp;
PRIMITIVES["dom-call-method"] = domCallMethod;
PRIMITIVES["stop-propagation"] = stopPropagation_;
PRIMITIVES["error-message"] = errorMessage;
PRIMITIVES["schedule-idle"] = scheduleIdle;

View File

@@ -2,7 +2,9 @@
(defcomp ~sx-home-content ()
(div :id "main-content" :class "max-w-3xl mx-auto px-4 py-6"
(~doc-code :code (highlight (component-source "~sx-header") "lisp"))))
(~doc-code :code (highlight (component-source "~sx-header") "lisp"))
(~doc-code :code (highlight (component-source "~video-player") "lisp"))
(~doc-code :code (highlight (component-source "~video-embed") "lisp"))))
(defcomp ~docs-introduction-content ()
(~doc-page :title "Introduction"

View File

@@ -45,8 +45,8 @@
(reset! shade (+ 400 (* (mod (* (deref idx) 137) 5) 50)))))
;; Only fetch video if none loaded (marsh: reactive + conditional hypermedia)
(let ((embed (dom-query-by-id "video-embed")))
(when (not (dom-first-child embed))
(dom-dispatch (get e "currentTarget") "fetch-video" (dict)))))
(when (not (dom-get-prop embed "firstChild"))
(dom-dispatch (dom-get-prop e "currentTarget") "fetch-video" {}))))
:sx-get "/api/random-video"
:sx-target "#video-embed"
:sx-swap "innerHTML"
@@ -66,9 +66,8 @@
;; navigations. Content swapped in via sx-get from the reactive word click.
;; Empty initially (zero height). Iframe provides height when loaded.
(defisland ~video-player ()
(div :style "display:flex;justify-content:center;"
(div :id "video-embed"
:style "position:relative;width:66%;max-width:20rem;")))
(div :id "video-embed"
:style "position:relative;width:20rem;max-width:66vw;"))
;; @css grid grid-cols-3
@@ -151,7 +150,8 @@
;; Video island — preserved across navigation morphs (like ~sx-header).
;; Outside logo-opacity so it doesn't fade.
;; Marsh demo: reactive click triggers hypermedia fetch, result lands here.
(~video-player)
;; Island renders as inline <span>; force it to block so margin:auto centers.
(div :style "display:flex;justify-content:center;" (~video-player))
;; Sibling arrows for EVERY level in the trail
;; Trail row i is level (i+2) of depth — opacity = (i+2)/depth
;; Last row (leaf) gets is-leaf for larger current page title

View File

@@ -1,18 +1,34 @@
;; SX docs — home page components
;; YouTube embed — rendered client-side from video ID returned by /api/random-video.
;; Marsh demo: the server picks the video (hypermedia), the island triggers the fetch (reactive).
;; YouTube video embed — rendered client-side from video ID returned by /api/random-video.
;; Marsh demo: server picks video (hypermedia), island controls playback (reactive).
;; Play/pause uses YouTube postMessage API via dom-call-method — no iframe reload.
(defcomp ~video-embed (&key video-id)
(<>
(button
:sx-get "/api/clear-video" :sx-target "#video-embed"
:sx-swap "innerHTML"
:style "position:absolute;top:-0.5rem;right:-0.5rem;width:1.25rem;height:1.25rem;border-radius:50%;border:none;background:rgba(0,0,0,0.5);color:white;font-size:0.75rem;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:10;"
"x")
(iframe :src (str "https://www.youtube.com/embed/" video-id "?autoplay=1&mute=1")
:allow "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
:allowfullscreen "true"
:style "width:100%;aspect-ratio:16/9;border-radius:0.5rem;border:none;")))
(let ((playing (signal true)))
(<>
;; Close button — clears via hypermedia
(button
:sx-get "/api/clear-video" :sx-target "#video-embed"
:sx-swap "innerHTML"
:style "position:absolute;top:-0.5rem;right:-0.5rem;width:1.25rem;height:1.25rem;border-radius:50%;border:none;background:rgba(0,0,0,0.5);color:white;font-size:0.75rem;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:10;"
"x")
;; Play/pause button — reactive signal toggles YouTube via postMessage
(button
:on-click (fn (e)
(let ((iframe (dom-query "#video-iframe"))
(win (dom-get-prop iframe "contentWindow"))
(cmd (if (deref playing) "pauseVideo" "playVideo")))
(dom-call-method win "postMessage"
(str "{\"event\":\"command\",\"func\":\"" cmd "\",\"args\":[]}")
"https://www.youtube.com")
(reset! playing (not (deref playing)))))
:style "position:absolute;top:1rem;right:-0.5rem;width:1.25rem;height:1.25rem;border-radius:50%;border:none;background:rgba(0,0,0,0.5);color:white;font-size:0.75rem;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:10;"
(if (deref playing) "||" ">"))
;; Iframe — stays in DOM, playback controlled via postMessage
(iframe :id "video-iframe"
:src (str "https://www.youtube.com/embed/" video-id "?autoplay=1&enablejsapi=1&controls=0&modestbranding=1&rel=0")
:allow "accelerometer; autoplay; encrypted-media"
:style "width:100%;aspect-ratio:16/9;border-radius:0.5rem;border:none;pointer-events:none;"))))
(defcomp ~sx-hero (&key &rest children)
(div :class "max-w-4xl mx-auto px-6 py-16 text-center"