From 8f88e52b2754587bec9c5f1bd93730e0f7c7c9c3 Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 11 Mar 2026 21:51:05 +0000 Subject: [PATCH] Add DOM primitives (dom-set-prop, dom-call-method, dom-post-message), bump SW cache v2, remove video demo New platform_js primitives for direct DOM property/method access and cross-origin iframe communication. Service worker static cache bumped to v2 to flush stale assets. Removed experimental video embed from header island, routes, and home page. Co-Authored-By: Claude Opus 4.6 --- shared/static/scripts/sx-browser.js | 15 ++++++++++++--- shared/static/scripts/sx-sw.js | 2 +- shared/sx/ref/platform_js.py | 13 +++++++++++-- sx/bp/pages/routes.py | 17 ----------------- sx/sx/docs-content.sx | 4 +--- sx/sx/layouts.sx | 23 ++--------------------- sx/sxc/home.sx | 29 ----------------------------- 7 files changed, 27 insertions(+), 76 deletions(-) diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index f505d9df..8966cf77 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:02:07Z"; + var SX_VERSION = "2026-03-11T21:11:04Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -3999,12 +3999,20 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { 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; + } + // Post a message to an iframe's contentWindow without exposing the cross-origin + // Window object to the SX evaluator (which would trigger _thunk access errors). + function domPostMessage(iframe, msg, origin) { + try { + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage(msg, origin || '*'); + } + } catch(e) { console.error("[sx] domPostMessage error:", e); } return NIL; } @@ -5213,6 +5221,7 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { PRIMITIVES["dom-get-prop"] = domGetProp; PRIMITIVES["dom-set-prop"] = domSetProp; PRIMITIVES["dom-call-method"] = domCallMethod; + PRIMITIVES["dom-post-message"] = domPostMessage; PRIMITIVES["stop-propagation"] = stopPropagation_; PRIMITIVES["error-message"] = errorMessage; PRIMITIVES["schedule-idle"] = scheduleIdle; diff --git a/shared/static/scripts/sx-sw.js b/shared/static/scripts/sx-sw.js index 1b11e8d6..d2a950b4 100644 --- a/shared/static/scripts/sx-sw.js +++ b/shared/static/scripts/sx-sw.js @@ -14,7 +14,7 @@ var IDB_NAME = "sx-offline"; var IDB_VERSION = 1; var IDB_STORE = "responses"; -var STATIC_CACHE = "sx-static-v1"; +var STATIC_CACHE = "sx-static-v2"; // --------------------------------------------------------------------------- // IndexedDB helpers diff --git a/shared/sx/ref/platform_js.py b/shared/sx/ref/platform_js.py index beb07e70..c0d8bcae 100644 --- a/shared/sx/ref/platform_js.py +++ b/shared/sx/ref/platform_js.py @@ -1659,12 +1659,20 @@ PLATFORM_DOM_JS = """ 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; + } + // Post a message to an iframe's contentWindow without exposing the cross-origin + // Window object to the SX evaluator (which would trigger _thunk access errors). + function domPostMessage(iframe, msg, origin) { + try { + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage(msg, origin || '*'); + } + } catch(e) { console.error("[sx] domPostMessage error:", e); } return NIL; } @@ -2886,6 +2894,7 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False, has_deps=False, has_ PRIMITIVES["dom-get-prop"] = domGetProp; PRIMITIVES["dom-set-prop"] = domSetProp; PRIMITIVES["dom-call-method"] = domCallMethod; + PRIMITIVES["dom-post-message"] = domPostMessage; PRIMITIVES["stop-propagation"] = stopPropagation_; PRIMITIVES["error-message"] = errorMessage; PRIMITIVES["schedule-idle"] = scheduleIdle; diff --git a/sx/bp/pages/routes.py b/sx/bp/pages/routes.py index 30be8426..e295d962 100644 --- a/sx/bp/pages/routes.py +++ b/sx/bp/pages/routes.py @@ -1049,23 +1049,6 @@ def register(url_prefix: str = "/") -> Blueprint: resp.headers["SX-Retarget"] = "#ref-hdr-retarget-alt" return resp - # --- Random video (marsh demo: reactive click + hypermedia fetch) --- - - _last_video = {"id": ""} - - @bp.get("/api/random-video") - async def api_random_video(): - from shared.sx.helpers import sx_response - videos = ["mSil_hBqbac", "-EX4mgvnSc8", "maOM81a-Gyg", "OHPJemGxUjE"] - available = [v for v in videos if v != _last_video["id"]] - video_id = random.choice(available) - _last_video["id"] = video_id - return sx_response(f'(~video-embed :video-id "{video_id}")') - - @bp.get("/api/clear-video") - async def api_clear_video(): - return Response("", content_type="text/html") - # --- Event demos --- @bp.get("/geography/hypermedia/reference/api/error-500") diff --git a/sx/sx/docs-content.sx b/sx/sx/docs-content.sx index 27690e36..d281d625 100644 --- a/sx/sx/docs-content.sx +++ b/sx/sx/docs-content.sx @@ -2,9 +2,7 @@ (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 "~video-player") "lisp")) - (~doc-code :code (highlight (component-source "~video-embed") "lisp")))) + (~doc-code :code (highlight (component-source "~sx-header") "lisp")))) (defcomp ~docs-introduction-content () (~doc-page :title "Introduction" diff --git a/sx/sx/layouts.sx b/sx/sx/layouts.sx index 7f3d05c9..75a8ff9f 100644 --- a/sx/sx/layouts.sx +++ b/sx/sx/layouts.sx @@ -32,7 +32,7 @@ (span :style (str (display "block") (mb 2) (cssx (:text (colour "violet" 699) (size "4xl") (weight "bold") (family "mono")))) "()"))) - ;; Tagline — NOT in anchor. Clicking "reactive" cycles colour + fetches video. + ;; Tagline — clicking "reactive" cycles colour. (p :style (str (mb 1) (cssx (:text (colour "stone" 500) (size "lg")))) "Framework free " (span @@ -42,15 +42,7 @@ :on-click (fn (e) (batch (fn () (swap! idx inc) - (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-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" - :sx-trigger "fetch-video" + (reset! shade (+ 400 (* (mod (* (deref idx) 137) 5) 50)))))) "reactive") " hypermedia") ;; Lake: server morphs copyright on navigation without disturbing signals. @@ -62,12 +54,6 @@ "margin-left:0.5em;") (str "· " path)))))))) -;; Video player island — defisland so the morph algorithm preserves it across -;; 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 :id "video-embed" - :style "position:relative;width:20rem;max-width:66vw;")) ;; @css grid grid-cols-3 @@ -147,11 +133,6 @@ :style (str "opacity:" (+ (* (/ 1 depth) 0.75) 0.25) ";" "transition:opacity 0.3s;") (~sx-header :path (or path "/"))) - ;; 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. - ;; 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 ae5dfdd9..fe4196f4 100644 --- a/sx/sxc/home.sx +++ b/sx/sxc/home.sx @@ -1,34 +1,5 @@ ;; SX docs — home page components -;; 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) - (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"