Add DOM primitives (dom-set-prop, dom-call-method, dom-post-message), bump SW cache v2, remove video demo
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6m57s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6m57s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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-11T21:02:07Z";
|
var SX_VERSION = "2026-03-11T21:11:04Z";
|
||||||
|
|
||||||
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); }
|
||||||
@@ -3999,12 +3999,20 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
|||||||
function domCallMethod() {
|
function domCallMethod() {
|
||||||
var obj = arguments[0], method = arguments[1];
|
var obj = arguments[0], method = arguments[1];
|
||||||
var args = Array.prototype.slice.call(arguments, 2);
|
var args = Array.prototype.slice.call(arguments, 2);
|
||||||
console.log("[sx] dom-call-method:", obj, method, args);
|
|
||||||
if (obj && typeof obj[method] === 'function') {
|
if (obj && typeof obj[method] === 'function') {
|
||||||
try { return obj[method].apply(obj, args); }
|
try { return obj[method].apply(obj, args); }
|
||||||
catch(e) { console.error("[sx] dom-call-method error:", e); return NIL; }
|
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;
|
return NIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5213,6 +5221,7 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
|||||||
PRIMITIVES["dom-get-prop"] = domGetProp;
|
PRIMITIVES["dom-get-prop"] = domGetProp;
|
||||||
PRIMITIVES["dom-set-prop"] = domSetProp;
|
PRIMITIVES["dom-set-prop"] = domSetProp;
|
||||||
PRIMITIVES["dom-call-method"] = domCallMethod;
|
PRIMITIVES["dom-call-method"] = domCallMethod;
|
||||||
|
PRIMITIVES["dom-post-message"] = domPostMessage;
|
||||||
PRIMITIVES["stop-propagation"] = stopPropagation_;
|
PRIMITIVES["stop-propagation"] = stopPropagation_;
|
||||||
PRIMITIVES["error-message"] = errorMessage;
|
PRIMITIVES["error-message"] = errorMessage;
|
||||||
PRIMITIVES["schedule-idle"] = scheduleIdle;
|
PRIMITIVES["schedule-idle"] = scheduleIdle;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
var IDB_NAME = "sx-offline";
|
var IDB_NAME = "sx-offline";
|
||||||
var IDB_VERSION = 1;
|
var IDB_VERSION = 1;
|
||||||
var IDB_STORE = "responses";
|
var IDB_STORE = "responses";
|
||||||
var STATIC_CACHE = "sx-static-v1";
|
var STATIC_CACHE = "sx-static-v2";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// IndexedDB helpers
|
// IndexedDB helpers
|
||||||
|
|||||||
@@ -1659,12 +1659,20 @@ PLATFORM_DOM_JS = """
|
|||||||
function domCallMethod() {
|
function domCallMethod() {
|
||||||
var obj = arguments[0], method = arguments[1];
|
var obj = arguments[0], method = arguments[1];
|
||||||
var args = Array.prototype.slice.call(arguments, 2);
|
var args = Array.prototype.slice.call(arguments, 2);
|
||||||
console.log("[sx] dom-call-method:", obj, method, args);
|
|
||||||
if (obj && typeof obj[method] === 'function') {
|
if (obj && typeof obj[method] === 'function') {
|
||||||
try { return obj[method].apply(obj, args); }
|
try { return obj[method].apply(obj, args); }
|
||||||
catch(e) { console.error("[sx] dom-call-method error:", e); return NIL; }
|
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;
|
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-get-prop"] = domGetProp;
|
||||||
PRIMITIVES["dom-set-prop"] = domSetProp;
|
PRIMITIVES["dom-set-prop"] = domSetProp;
|
||||||
PRIMITIVES["dom-call-method"] = domCallMethod;
|
PRIMITIVES["dom-call-method"] = domCallMethod;
|
||||||
|
PRIMITIVES["dom-post-message"] = domPostMessage;
|
||||||
PRIMITIVES["stop-propagation"] = stopPropagation_;
|
PRIMITIVES["stop-propagation"] = stopPropagation_;
|
||||||
PRIMITIVES["error-message"] = errorMessage;
|
PRIMITIVES["error-message"] = errorMessage;
|
||||||
PRIMITIVES["schedule-idle"] = scheduleIdle;
|
PRIMITIVES["schedule-idle"] = scheduleIdle;
|
||||||
|
|||||||
@@ -1049,23 +1049,6 @@ def register(url_prefix: str = "/") -> Blueprint:
|
|||||||
resp.headers["SX-Retarget"] = "#ref-hdr-retarget-alt"
|
resp.headers["SX-Retarget"] = "#ref-hdr-retarget-alt"
|
||||||
return resp
|
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 ---
|
# --- Event demos ---
|
||||||
|
|
||||||
@bp.get("/geography/hypermedia/reference/api/error-500")
|
@bp.get("/geography/hypermedia/reference/api/error-500")
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
(defcomp ~sx-home-content ()
|
(defcomp ~sx-home-content ()
|
||||||
(div :id "main-content" :class "max-w-3xl mx-auto px-4 py-6"
|
(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 ()
|
(defcomp ~docs-introduction-content ()
|
||||||
(~doc-page :title "Introduction"
|
(~doc-page :title "Introduction"
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
(span :style (str (display "block") (mb 2)
|
(span :style (str (display "block") (mb 2)
|
||||||
(cssx (:text (colour "violet" 699) (size "4xl") (weight "bold") (family "mono"))))
|
(cssx (:text (colour "violet" 699) (size "4xl") (weight "bold") (family "mono"))))
|
||||||
"(<sx>)")))
|
"(<sx>)")))
|
||||||
;; 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"))))
|
(p :style (str (mb 1) (cssx (:text (colour "stone" 500) (size "lg"))))
|
||||||
"Framework free "
|
"Framework free "
|
||||||
(span
|
(span
|
||||||
@@ -42,15 +42,7 @@
|
|||||||
:on-click (fn (e)
|
:on-click (fn (e)
|
||||||
(batch (fn ()
|
(batch (fn ()
|
||||||
(swap! idx inc)
|
(swap! idx inc)
|
||||||
(reset! shade (+ 400 (* (mod (* (deref idx) 137) 5) 50)))))
|
(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"
|
|
||||||
"reactive")
|
"reactive")
|
||||||
" hypermedia")
|
" hypermedia")
|
||||||
;; Lake: server morphs copyright on navigation without disturbing signals.
|
;; Lake: server morphs copyright on navigation without disturbing signals.
|
||||||
@@ -62,12 +54,6 @@
|
|||||||
"margin-left:0.5em;")
|
"margin-left:0.5em;")
|
||||||
(str "· " path))))))))
|
(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
|
;; @css grid grid-cols-3
|
||||||
|
|
||||||
@@ -147,11 +133,6 @@
|
|||||||
:style (str "opacity:" (+ (* (/ 1 depth) 0.75) 0.25) ";"
|
:style (str "opacity:" (+ (* (/ 1 depth) 0.75) 0.25) ";"
|
||||||
"transition:opacity 0.3s;")
|
"transition:opacity 0.3s;")
|
||||||
(~sx-header :path (or path "/")))
|
(~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 <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
|
;; Sibling arrows for EVERY level in the trail
|
||||||
;; Trail row i is level (i+2) of depth — opacity = (i+2)/depth
|
;; Trail row i is level (i+2) of depth — opacity = (i+2)/depth
|
||||||
;; Last row (leaf) gets is-leaf for larger current page title
|
;; Last row (leaf) gets is-leaf for larger current page title
|
||||||
|
|||||||
@@ -1,34 +1,5 @@
|
|||||||
;; SX docs — home page components
|
;; 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)
|
(defcomp ~sx-hero (&key &rest children)
|
||||||
(div :class "max-w-4xl mx-auto px-6 py-16 text-center"
|
(div :class "max-w-4xl mx-auto px-6 py-16 text-center"
|
||||||
|
|||||||
Reference in New Issue
Block a user