diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 5626342..f505d9d 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -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; diff --git a/shared/sx/ref/js.sx b/shared/sx/ref/js.sx index 7f96ee6..8de1cb5 100644 --- a/shared/sx/ref/js.sx +++ b/shared/sx/ref/js.sx @@ -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]; " diff --git a/shared/sx/ref/platform_js.py b/shared/sx/ref/platform_js.py index 3086710..beb07e7 100644 --- a/shared/sx/ref/platform_js.py +++ b/shared/sx/ref/platform_js.py @@ -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; diff --git a/sx/sx/docs-content.sx b/sx/sx/docs-content.sx index d281d62..27690e3 100644 --- a/sx/sx/docs-content.sx +++ b/sx/sx/docs-content.sx @@ -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" diff --git a/sx/sx/layouts.sx b/sx/sx/layouts.sx index 65d0d74..7f3d05c 100644 --- a/sx/sx/layouts.sx +++ b/sx/sx/layouts.sx @@ -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 ; 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 diff --git a/sx/sxc/home.sx b/sx/sxc/home.sx index 65045d9..ae5dfdd 100644 --- a/sx/sxc/home.sx +++ b/sx/sxc/home.sx @@ -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"