Web extension module for def-forms + modifier-key clicks + CSSX SSR fix

Move defhandler/defquery/defaction/defpage/defrelation from hardcoded
evaluator dispatch to web/web-forms.sx extension module, registered via
register-special-form!. Adapters updated to use definition-form? and
dynamically extended form-name lists.

Fix modifier-key clicks (ctrl-click → new tab) in three click handlers:
bindBoostLink, bindClientRouteClick, and orchestration.sx bind-event.
Add event-modifier-key? primitive (eventModifierKey_p for transpiler).

Fix CSSX SSR: ~cssx/flush no longer drains the collected bucket on the
server, so the shell template correctly emits CSSX rules in <head>.

Add missing server-side DOM stubs (create-text-node, dom-append, etc.)
and SSR passthrough for portal/error-boundary/promise-delayed.

Passive event listeners for touch/wheel/scroll to fix touchpad scrolling.

97/97 Playwright demo tests + 4/4 isomorphic SSR tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 10:01:41 +00:00
parent 8ccf5f7c1e
commit 8a08de26cd
11 changed files with 318 additions and 48 deletions

View File

@@ -420,10 +420,11 @@ class OcamlBridge:
# All directories loaded into the Python env
all_dirs = list(set(_watched_dirs) | _dirs_from_cache)
# Isomorphic libraries: signals, rendering, freeze scopes
# Isomorphic libraries: signals, rendering, freeze scopes, web forms
web_dir = os.path.join(os.path.dirname(__file__), "../../web")
if os.path.isdir(web_dir):
for web_file in ["signals.sx", "adapter-html.sx", "adapter-sx.sx"]:
for web_file in ["signals.sx", "adapter-html.sx", "adapter-sx.sx",
"web-forms.sx"]:
path = os.path.normpath(os.path.join(web_dir, web_file))
if os.path.isfile(path):
all_files.append(path)

View File

@@ -494,16 +494,13 @@
;; =========================================================================
(defcomp ~cssx/flush () :affinity :client
(let ((rules (collected "cssx")))
(clear-collected! "cssx")
(when (not (empty? rules))
;; Append to the persistent <style id="sx-css"> in <head> if available.
;; This survives #main-panel morphs during SPA navigation.
;; Falls back to inline <style> if no head stylesheet exists.
(let ((head-style (dom-query "#sx-css")))
(if head-style
(do
(dom-set-prop head-style "textContent"
(str (dom-get-prop head-style "textContent") (join "" rules)))
nil)
(raw! (str "<style data-cssx>" (join "" rules) "</style>")))))))
(let ((rules (collected "cssx"))
(head-style (dom-query "#sx-css")))
;; On client: append rules to <style id="sx-css"> in <head>.
;; On server: head-style is nil (no DOM). Don't clear the bucket —
;; the shell's <head> template reads collected("cssx") and emits them.
(when head-style
(clear-collected! "cssx")
(when (not (empty? rules))
(dom-set-prop head-style "textContent"
(str (dom-get-prop head-style "textContent") (join "" rules)))))))