Files
rose-ash/shared/sx/templates/shell.sx
giles 31ed8b20f4 Remove click buffer, add CSS cursor:pointer for island controls
The click buffer (capture + stopPropagation + replay) caused more
harm than good: synchronous XHR blocks the main thread during kernel
load, so there's no window where clicks can be captured. The buffer
was eating clicks after hydration due to property name mismatches.

Replace with pure CSS: buttons/links/[role=button] inside islands
get cursor:pointer from SSR. No JS needed, works immediately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:50:12 +00:00

102 lines
6.0 KiB
Plaintext

;; ---------------------------------------------------------------------------
;; Page shell — the outermost HTML document structure
;;
;; Replaces the Python HTML string template. All page data is computed in
;; Python and passed as keyword arguments. The component just arranges
;; the precomputed values into HTML structure.
;;
;; raw! is used for:
;; - <!doctype html> (not an element)
;; - Script/style content (must not be HTML-escaped)
;; - Pre-rendered meta HTML from callers
;; ---------------------------------------------------------------------------
(defcomp ~shared:shell/sx-page-shell (&key (title :as string) (meta-html :as string?) (csrf :as string)
(sx-css :as string?) (sx-css-classes :as string?)
(component-hash :as string?) (component-defs :as string?)
(pages-sx :as string?) (page-sx :as string?)
(body-html :as string?)
(asset-url :as string) (sx-js-hash :as string) (body-js-hash :as string?)
(wasm-hash :as string?)
(head-scripts :as list?) (inline-css :as string?) (inline-head-js :as string?)
(init-sx :as string?) (body-scripts :as list?)
(use-wasm :as boolean?))
(<>
(raw! "<!doctype html>")
(html :lang "en"
(head
(meta :charset "utf-8")
(meta :name "viewport" :content "width=device-width, initial-scale=1")
(meta :name "robots" :content "index,follow")
(meta :name "theme-color" :content "#ffffff")
(title title)
(when meta-html (raw! meta-html))
(meta :name "csrf-token" :content csrf)
(style :id "sx-css" (raw! (or sx-css "")))
;; CSSX rules from island SSR — must be in <head> so they survive
;; #main-panel morphs during SPA navigation.
(let ((cssx-rules (collected "cssx")))
(clear-collected! "cssx")
(when (not (empty? cssx-rules))
(style :data-cssx true (raw! (join "" cssx-rules)))))
(meta :name "sx-css-classes" :content (or sx-css-classes ""))
;; CDN / head scripts — configurable per app
;; Pass a list (even empty) to override defaults; nil = use defaults
(if (not (nil? head-scripts))
(map (fn (src) (script :src src)) head-scripts)
;; Default: Prism + SweetAlert (legacy apps)
(<>
(script :src "https://unpkg.com/prismjs/prism.js")
(script :src "https://unpkg.com/prismjs/components/prism-javascript.min.js")
(script :src "https://unpkg.com/prismjs/components/prism-python.min.js")
(script :src "https://unpkg.com/prismjs/components/prism-bash.min.js")
(script :src "https://cdn.jsdelivr.net/npm/sweetalert2@11")))
;; Inline JS — skipped when app provides its own inline-head-js (even empty)
(if (not (nil? inline-head-js))
(when (not (empty? inline-head-js)) (script (raw! inline-head-js)))
(<>
(script (raw! "if(matchMedia('(hover:hover) and (pointer:fine)').matches){document.documentElement.classList.add('hover-capable')}"))
(script (raw! "document.addEventListener('click',function(e){var t=e.target.closest('[data-close-details]');if(!t)return;var d=t.closest('details');if(d)d.removeAttribute('open')})"))))
;; Inline CSS — configurable per app
;; Pass a string (even empty) to override defaults; nil = use defaults
(if (not (nil? inline-css))
(style (raw! inline-css))
;; Default: all shared styles (legacy apps)
(style (raw! "details[data-toggle-group=\"mobile-panels\"]>summary{list-style:none}
details[data-toggle-group=\"mobile-panels\"]>summary::-webkit-details-marker{display:none}
@media(min-width:768px){.nav-group:focus-within .submenu,.nav-group:hover .submenu{display:block}}
img{max-width:100%;height:auto}
.clamp-2{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.clamp-3{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}
details.group{overflow:hidden}details.group>summary{list-style:none}details.group>summary::-webkit-details-marker{display:none}
.sx-indicator{display:none}.sx-request .sx-indicator{display:inline-flex}
.sx-error .sx-indicator{display:none}.sx-loading .sx-indicator{display:inline-flex}
.js-wrap.open .js-pop{display:block}.js-wrap.open .js-backdrop{display:block}"))))
(body :class "bg-stone-50 text-stone-900"
;; Server-rendered HTML — visible immediately before JS loads
(div :id "sx-root" (raw! (or body-html "")))
;; Island interactive elements — cursor pointer from SSR, no JS needed
(style (raw! "[data-sx-island] button,[data-sx-island] a,[data-sx-island] [role=button]{cursor:pointer}"))
(script :type "text/sx" :data-components true :data-hash component-hash
(raw! (or component-defs "")))
(when init-sx
(script :type "text/sx" :data-init true
(raw! init-sx)))
(script :type "text/sx-pages"
(raw! (or pages-sx "")))
(script :type "text/sx" :data-mount "#sx-root"
(raw! (or page-sx "")))
(if use-wasm
(let ((wv (or wasm-hash "dev")))
(<>
(script :src (str asset-url "/wasm/sx_browser.bc.js?v=" wv))
(script :src (str asset-url "/wasm/sx-platform.js?v=" wv))))
(script :src (str asset-url "/scripts/sx-browser.js?v=" sx-js-hash)))
;; Body scripts — configurable per app
;; Pass a list (even empty) to override defaults; nil = use defaults
(if (not (nil? body-scripts))
(map (fn (src) (script :src src)) body-scripts)
;; Default: body.js (legacy apps)
(script :src (str asset-url "/scripts/body.js?v=" body-js-hash)))))))