- ~video-player defisland persists across SPA navigations (morph-safe) - Clicking "reactive" cycles colour (signal) + fetches random YouTube video (sx-get) - sx-trigger="fetch-video" + dom-first-child check: video keeps playing on repeat clicks - Close button (x) clears video via /api/clear-video hypermedia endpoint - Autoplay+mute removes YouTube's red play button overlay - Header restructured: logo in anchor, tagline outside (no accidental navigation) - Flex centering on video container Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
199 lines
8.9 KiB
Plaintext
199 lines
8.9 KiB
Plaintext
;; SX docs layout defcomps + in-page navigation.
|
|
;; Layout = root header only. Nav is in-page via ~sx-doc wrapper.
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Nav components — logo header, sibling arrows, children links
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
;; CSSX replaces Tailwind text-*/bg-*/font-* classes — computed via cssx.sx
|
|
|
|
;; Logo + tagline + copyright — always shown at top of page area.
|
|
;; The header itself is an island so the "reactive" word can cycle colours
|
|
;; on click — demonstrates inline signals without a separate component.
|
|
;;
|
|
;; Lakes (server-morphable slots) wrap the static content: logo and copyright.
|
|
;; The server can update these during navigation morphs without disturbing
|
|
;; the reactive colour-cycling state. This is Level 2-3: the water (server
|
|
;; content) flows through the island, around the rocks (reactive signals).
|
|
(defisland ~sx-header (&key path)
|
|
(let ((families (list "violet" "rose" "blue" "emerald" "amber" "cyan" "red" "teal" "pink" "indigo"))
|
|
(idx (signal 0))
|
|
(shade (signal 500))
|
|
(current-family (computed (fn ()
|
|
(nth families (mod (deref idx) (len families)))))))
|
|
(div :style (str (display "block") (max-w (get cssx-max-widths "3xl"))
|
|
(mx-auto) (px 4) (pt 8) (pb 4) (align "center"))
|
|
;; Logo — only this navigates home
|
|
(a :href "/"
|
|
:sx-get "/" :sx-target "#main-panel" :sx-select "#main-panel"
|
|
:sx-swap "outerHTML" :sx-push-url "true"
|
|
:style (str (display "block") (decoration "none"))
|
|
(lake :id "logo"
|
|
(span :style (str (display "block") (mb 2)
|
|
(cssx (:text (colour "violet" 699) (size "4xl") (weight "bold") (family "mono"))))
|
|
"(<sx>)")))
|
|
;; Tagline — NOT in anchor. Clicking "reactive" cycles colour + fetches video.
|
|
(p :style (str (mb 1) (cssx (:text (colour "stone" 500) (size "lg"))))
|
|
"Framework free "
|
|
(span
|
|
:style (str (cssx (:text (colour (deref current-family) (deref shade))
|
|
(weight "bold")))
|
|
"cursor:pointer;transition:color 0.3s,font-weight 0.3s;")
|
|
: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-first-child embed))
|
|
(dom-dispatch (get e "currentTarget") "fetch-video" (dict)))))
|
|
:sx-get "/api/random-video"
|
|
:sx-target "#video-embed"
|
|
:sx-swap "innerHTML"
|
|
:sx-trigger "fetch-video"
|
|
"reactive")
|
|
" hypermedia")
|
|
;; Lake: server morphs copyright on navigation without disturbing signals.
|
|
(lake :id "copyright"
|
|
(p :style (cssx (:text (colour "stone" 400) (size "xs")))
|
|
"© Giles Bradshaw 2026"
|
|
(when path
|
|
(span :style (str (cssx (:text (colour "stone" 300) (size "xs")))
|
|
"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 :style "display:flex;justify-content:center;"
|
|
(div :id "video-embed"
|
|
:style "position:relative;width:66%;max-width:20rem;")))
|
|
|
|
;; @css grid grid-cols-3
|
|
|
|
;; Current section with prev/next siblings.
|
|
;; 3-column grid: prev is right-aligned, current centered, next left-aligned.
|
|
;; Current page is larger in the leaf (bottom) row.
|
|
(defcomp ~nav-sibling-row (&key node siblings is-leaf level depth)
|
|
(let* ((sibs (or siblings (list)))
|
|
(count (len sibs))
|
|
;; opacity = (n/x * 3/4) + 1/4
|
|
(row-opacity (if (and level depth (> depth 0))
|
|
(+ (* (/ level depth) 0.75) 0.25)
|
|
1)))
|
|
(when (> count 0)
|
|
(let* ((idx (find-nav-index sibs node))
|
|
(prev-idx (mod (+ (- idx 1) count) count))
|
|
(next-idx (mod (+ idx 1) count))
|
|
(prev-node (nth sibs prev-idx))
|
|
(next-node (nth sibs next-idx)))
|
|
(div :class "max-w-3xl mx-auto px-4 py-2 grid grid-cols-3 items-center"
|
|
:style (str "opacity:" row-opacity ";transition:opacity 0.3s;")
|
|
(a :href (get prev-node "href")
|
|
:sx-get (get prev-node "href") :sx-target "#main-panel"
|
|
:sx-select "#main-panel" :sx-swap "outerHTML"
|
|
:sx-push-url "true"
|
|
:class "text-right"
|
|
:style (cssx (:text (colour "stone" 500) (size "sm")))
|
|
(str "← " (get prev-node "label")))
|
|
(a :href (get node "href")
|
|
:sx-get (get node "href") :sx-target "#main-panel"
|
|
:sx-select "#main-panel" :sx-swap "outerHTML"
|
|
:sx-push-url "true"
|
|
:class "text-center px-4"
|
|
:style (if is-leaf
|
|
(cssx (:text (colour "violet" 700) (size "2xl") (weight "bold")))
|
|
(cssx (:text (colour "violet" 700) (size "lg") (weight "semibold"))))
|
|
(get node "label"))
|
|
(a :href (get next-node "href")
|
|
:sx-get (get next-node "href") :sx-target "#main-panel"
|
|
:sx-select "#main-panel" :sx-swap "outerHTML"
|
|
:sx-push-url "true"
|
|
:class "text-left"
|
|
:style (cssx (:text (colour "stone" 500) (size "sm")))
|
|
(str (get next-node "label") " →")))))))
|
|
|
|
;; Children links — shown as clearly clickable buttons.
|
|
(defcomp ~nav-children (&key items)
|
|
(div :class "max-w-3xl mx-auto px-4 py-3"
|
|
(div :class "flex flex-wrap justify-center gap-2"
|
|
(map (fn (item)
|
|
(a :href (get item "href")
|
|
:sx-get (get item "href") :sx-target "#main-panel"
|
|
:sx-select "#main-panel" :sx-swap "outerHTML"
|
|
:sx-push-url "true"
|
|
:class "px-3 py-1.5 rounded border transition-colors"
|
|
:style (cssx (:text (colour "violet" 700) (size "sm"))
|
|
(:border (colour "violet" 200)))
|
|
(get item "label")))
|
|
items))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; ~sx-doc — in-page content wrapper with nav
|
|
;; Used by every defpage :content to embed nav inside the page content area.
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(defcomp ~sx-doc (&key path &rest children) :affinity :server
|
|
(let* ((nav-state (resolve-nav-path sx-nav-tree (or path "/")))
|
|
(trail (or (get nav-state "trail") (list)))
|
|
(trail-len (len trail))
|
|
;; Total nav levels: logo (1) + trail rows
|
|
(depth (+ trail-len 1)))
|
|
(<>
|
|
(div :id "sx-nav" :class "mb-6"
|
|
;; Logo opacity = (1/depth * 3/4) + 1/4
|
|
;; Wrapper is outside the island so the server morphs it directly
|
|
(div :id "logo-opacity"
|
|
: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.
|
|
(~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
|
|
(map-indexed (fn (i crumb)
|
|
(~nav-sibling-row
|
|
:node (get crumb "node")
|
|
:siblings (get crumb "siblings")
|
|
:is-leaf (= i (- trail-len 1))
|
|
:level (+ i 2)
|
|
:depth depth))
|
|
trail)
|
|
;; Children as button links
|
|
(when (get nav-state "children")
|
|
(~nav-children :items (get nav-state "children"))))
|
|
;; Page content follows
|
|
children)))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; SX docs layouts — root header only (nav is in page content via ~sx-doc)
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(defcomp ~sx-docs-layout-full ()
|
|
nil)
|
|
|
|
(defcomp ~sx-docs-layout-oob ()
|
|
nil)
|
|
|
|
(defcomp ~sx-docs-layout-mobile ()
|
|
nil)
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Standalone layouts (no root header — for sx-web.org)
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(defcomp ~sx-standalone-docs-layout-full ()
|
|
nil)
|
|
|
|
;; Standalone OOB: nothing needed — nav is in content.
|
|
(defcomp ~sx-standalone-docs-layout-oob ()
|
|
nil)
|
|
|
|
;; Standalone mobile: nothing — nav is in content.
|
|
(defcomp ~sx-standalone-docs-layout-mobile ()
|
|
nil)
|