Fix keyboard shortcuts + trigger filter + sx-on event mapping

1. parse-trigger-spec: strip [condition] from event names, store as
   "filter" modifier
2. bind-event: native SX filter for key=='X' patterns (extracts key
   char and checks event.key + not-input guard)
3. bind-event from: modifier: resolve "body"/"document"/"window" to
   direct DOM references instead of dom-query
4. sx-platform-2.js: global keyboard dispatch — WASM host-callbacks
   on document/body don't fire, so keyboard triggers with from:body
   are handled from JS, calling execute-request via K.eval
5. bind-inline-handlers: map afterSwap/beforeRequest to sx: prefix,
   eval JS bodies via Function constructor

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 23:19:25 +00:00
parent 235d73d837
commit 683e334546
34 changed files with 278 additions and 118 deletions

View File

@@ -398,6 +398,22 @@
"children:", islands[j].children.length);
}
console.log("[sx] boot done");
// Global keyboard shortcut dispatch — WASM host-callbacks on
// document/body don't fire, so handle from:body keyboard
// triggers in JS and call execute-request via the SX engine.
document.addEventListener("keyup", function(e) {
if (e.target && e.target.matches && e.target.matches("input,textarea,select")) return;
var sel = '[sx-trigger*="key==\'' + e.key + '\'"]';
var els = document.querySelectorAll(sel);
for (var i = 0; i < els.length; i++) {
var el = els[i];
if (!el.id) el.id = "_sx_kbd_" + Math.random().toString(36).slice(2);
try {
K.eval('(execute-request (dom-query-by-id "' + el.id + '") nil nil)');
} catch(err) { console.warn("[sx] keyboard dispatch error:", err); }
}
});
}
}
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"magic":"SXBC","version":1,"hash":"bfa1b3e64a390451","module":{"arity":0,"bytecode":[52,1,0,0,128,0,0,5,51,3,0,128,2,0,5,51,5,0,128,4,0,5,51,7,0,128,6,0,5,51,9,0,128,8,0,5,51,11,0,128,10,0,5,51,13,0,128,12,0,5,51,15,0,128,14,0,5,51,17,0,128,16,0,50],"constants":[{"t":"s","v":"freeze-registry"},{"t":"s","v":"dict"},{"t":"s","v":"freeze-signal"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[20,0,0,1,1,0,2,48,2,17,2,20,2,0,33,62,0,20,4,0,20,2,0,52,3,0,2,6,34,5,0,5,52,5,0,0,17,3,20,6,0,20,7,0,1,9,0,20,9,0,1,10,0,20,11,0,52,8,0,4,48,2,5,20,4,0,20,2,0,20,7,0,52,12,0,3,32,1,0,2,50],"constants":[{"t":"s","v":"context"},{"t":"s","v":"sx-freeze-scope"},{"t":"s","v":"scope-name"},{"t":"s","v":"get"},{"t":"s","v":"freeze-registry"},{"t":"s","v":"list"},{"t":"s","v":"append!"},{"t":"s","v":"entries"},{"t":"s","v":"dict"},{"t":"s","v":"name"},{"t":"s","v":"signal"},{"t":"s","v":"sig"},{"t":"s","v":"dict-set!"}]}},{"t":"s","v":"freeze-scope"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[20,0,0,1,1,0,20,2,0,48,2,5,20,4,0,20,2,0,52,5,0,0,52,3,0,3,5,20,6,0,20,7,0,2,48,2,5,20,8,0,1,1,0,48,1,5,2,50],"constants":[{"t":"s","v":"scope-push!"},{"t":"s","v":"sx-freeze-scope"},{"t":"s","v":"name"},{"t":"s","v":"dict-set!"},{"t":"s","v":"freeze-registry"},{"t":"s","v":"list"},{"t":"s","v":"cek-call"},{"t":"s","v":"body-fn"},{"t":"s","v":"scope-pop!"}]}},{"t":"s","v":"cek-freeze-scope"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[20,1,0,20,2,0,52,0,0,2,6,34,5,0,5,52,3,0,0,17,1,52,4,0,0,17,2,51,6,0,20,7,0,52,5,0,2,5,1,2,0,20,2,0,1,8,0,20,9,0,52,4,0,4,50],"constants":[{"t":"s","v":"get"},{"t":"s","v":"freeze-registry"},{"t":"s","v":"name"},{"t":"s","v":"list"},{"t":"s","v":"dict"},{"t":"s","v":"for-each"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[20,1,0,20,3,0,1,4,0,52,2,0,2,20,5,0,20,3,0,1,6,0,52,2,0,2,48,1,52,0,0,3,50],"constants":[{"t":"s","v":"dict-set!"},{"t":"s","v":"signals-dict"},{"t":"s","v":"get"},{"t":"s","v":"entry"},{"t":"s","v":"name"},{"t":"s","v":"signal-value"},{"t":"s","v":"signal"}]}},{"t":"s","v":"entries"},{"t":"s","v":"signals"},{"t":"s","v":"signals-dict"}]}},{"t":"s","v":"cek-freeze-all"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[51,1,0,20,3,0,52,2,0,1,52,0,0,2,50],"constants":[{"t":"s","v":"map"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[20,0,0,20,1,0,49,1,50],"constants":[{"t":"s","v":"cek-freeze-scope"},{"t":"s","v":"name"}]}},{"t":"s","v":"keys"},{"t":"s","v":"freeze-registry"}]}},{"t":"s","v":"cek-thaw-scope"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[20,1,0,20,2,0,52,0,0,2,6,34,5,0,5,52,3,0,0,17,2,20,4,0,1,5,0,52,0,0,2,17,3,20,6,0,33,13,0,51,8,0,20,9,0,52,7,0,2,32,1,0,2,50],"constants":[{"t":"s","v":"get"},{"t":"s","v":"freeze-registry"},{"t":"s","v":"name"},{"t":"s","v":"list"},{"t":"s","v":"frozen"},{"t":"s","v":"signals"},{"t":"s","v":"values"},{"t":"s","v":"for-each"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[20,1,0,1,2,0,52,0,0,2,17,1,20,1,0,1,3,0,52,0,0,2,17,2,20,4,0,20,5,0,52,0,0,2,17,3,20,8,0,52,7,0,1,52,6,0,1,33,14,0,20,9,0,20,10,0,20,8,0,49,2,32,1,0,2,50],"constants":[{"t":"s","v":"get"},{"t":"s","v":"entry"},{"t":"s","v":"name"},{"t":"s","v":"signal"},{"t":"s","v":"values"},{"t":"s","v":"sig-name"},{"t":"s","v":"not"},{"t":"s","v":"nil?"},{"t":"s","v":"val"},{"t":"s","v":"reset!"},{"t":"s","v":"sig"}]}},{"t":"s","v":"entries"}]}},{"t":"s","v":"cek-thaw-all"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[51,1,0,20,2,0,52,0,0,2,50],"constants":[{"t":"s","v":"for-each"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[20,0,0,20,2,0,1,3,0,52,1,0,2,20,2,0,49,2,50],"constants":[{"t":"s","v":"cek-thaw-scope"},{"t":"s","v":"get"},{"t":"s","v":"frozen"},{"t":"s","v":"name"}]}},{"t":"s","v":"frozen-list"}]}},{"t":"s","v":"freeze-to-sx"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[20,0,0,20,1,0,20,2,0,48,1,49,1,50],"constants":[{"t":"s","v":"sx-serialize"},{"t":"s","v":"cek-freeze-scope"},{"t":"s","v":"name"}]}},{"t":"s","v":"thaw-from-sx"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[20,0,0,20,1,0,48,1,17,1,20,4,0,52,3,0,1,52,2,0,1,33,30,0,20,4,0,52,5,0,1,17,2,20,6,0,20,8,0,1,9,0,52,7,0,2,20,8,0,49,2,32,1,0,2,50],"constants":[{"t":"s","v":"sx-parse"},{"t":"s","v":"sx-text"},{"t":"s","v":"not"},{"t":"s","v":"empty?"},{"t":"s","v":"parsed"},{"t":"s","v":"first"},{"t":"s","v":"cek-thaw-scope"},{"t":"s","v":"get"},{"t":"s","v":"frozen"},{"t":"s","v":"name"}]}}]}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -453,7 +453,18 @@
((timer nil)
(last-val nil)
(listen-target
(if (get mods "from") (dom-query (get mods "from")) el)))
(let
((from-sel (get mods "from")))
(cond
(nil? from-sel)
el
(= from-sel "body")
(dom-body)
(= from-sel "document")
(dom-document)
(= from-sel "window")
(dom-window)
:else (dom-query from-sel)))))
(when
listen-target
(dom-add-listener
@@ -462,7 +473,7 @@
(fn
(e)
(let
((should-fire (if (get mods "filter") (host-call (host-call (dom-window) "Function" "event" (get mods "filter")) "call" el e) true)))
((should-fire (if (get mods "filter") (let ((f (get mods "filter"))) (let ((key-match (index-of f "key=='"))) (if (>= key-match 0) (let ((key-char (slice f (+ key-match 5) (+ key-match 6)))) (and (= (host-get e "key") key-char) (not (dom-matches? (host-get e "target") "input,textarea,select")))) true))) true)))
(when
(get mods "changed")
(let
@@ -1492,7 +1503,7 @@
verb-info
(when
(not (dom-has-attr? el "sx-disable"))
(bind-triggers el verb-info)
(do (bind-triggers el verb-info) (bind-preload-for el))
(bind-preload-for el))))))
(define

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"magic":"SXBC","version":1,"hash":"5c28976e14b0d85b","module":{"arity":0,"bytecode":[51,1,0,128,0,0,5,51,3,0,128,2,0,5,51,5,0,128,4,0,5,51,7,0,128,6,0,5,51,9,0,128,8,0,5,51,11,0,128,10,0,50],"constants":[{"t":"s","v":"with-marsh-scope"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[52,0,0,0,17,2,20,1,0,51,2,0,20,3,0,48,2,5,20,4,0,20,5,0,1,6,0,20,7,0,49,3,50],"constants":[{"t":"s","v":"list"},{"t":"s","v":"with-island-scope"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[20,0,0,20,1,0,20,2,0,49,2,50],"constants":[{"t":"s","v":"append!"},{"t":"s","v":"disposers"},{"t":"s","v":"d"}]}},{"t":"s","v":"body-fn"},{"t":"s","v":"dom-set-data"},{"t":"s","v":"marsh-el"},{"t":"s","v":"sx-marsh-disposers"},{"t":"s","v":"disposers"}]}},{"t":"s","v":"dispose-marsh-scope"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[20,0,0,20,1,0,1,2,0,48,2,17,1,20,3,0,33,26,0,51,5,0,20,3,0,52,4,0,2,5,20,6,0,20,1,0,1,2,0,2,49,3,32,1,0,2,50],"constants":[{"t":"s","v":"dom-get-data"},{"t":"s","v":"marsh-el"},{"t":"s","v":"sx-marsh-disposers"},{"t":"s","v":"disposers"},{"t":"s","v":"for-each"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[20,0,0,20,1,0,2,49,2,50],"constants":[{"t":"s","v":"cek-call"},{"t":"s","v":"d"}]}},{"t":"s","v":"dom-set-data"}]}},{"t":"s","v":"emit-event"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[20,0,0,20,1,0,20,2,0,20,3,0,49,3,50],"constants":[{"t":"s","v":"dom-dispatch"},{"t":"s","v":"el"},{"t":"s","v":"event-name"},{"t":"s","v":"detail"}]}},{"t":"s","v":"on-event"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[20,0,0,20,1,0,20,2,0,20,3,0,49,3,50],"constants":[{"t":"s","v":"dom-on"},{"t":"s","v":"el"},{"t":"s","v":"event-name"},{"t":"s","v":"handler"}]}},{"t":"s","v":"bridge-event"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[20,0,0,51,1,0,49,1,50],"constants":[{"t":"s","v":"effect"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[20,0,0,20,1,0,20,2,0,51,3,0,48,3,17,0,20,4,0,50],"constants":[{"t":"s","v":"dom-on"},{"t":"s","v":"el"},{"t":"s","v":"event-name"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[20,0,0,20,1,0,48,1,17,1,20,2,0,33,18,0,20,3,0,20,2,0,20,5,0,52,4,0,1,48,2,32,3,0,20,5,0,17,2,20,6,0,20,7,0,20,8,0,49,2,50],"constants":[{"t":"s","v":"event-detail"},{"t":"s","v":"e"},{"t":"s","v":"transform-fn"},{"t":"s","v":"cek-call"},{"t":"s","v":"list"},{"t":"s","v":"detail"},{"t":"s","v":"reset!"},{"t":"s","v":"target-signal"},{"t":"s","v":"new-val"}]}},{"t":"s","v":"remove"}]}}]}},{"t":"s","v":"resource"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[20,0,0,1,2,0,3,1,3,0,2,1,4,0,2,52,1,0,6,48,1,17,1,20,5,0,20,6,0,20,7,0,2,48,2,51,8,0,51,9,0,48,3,5,20,10,0,50],"constants":[{"t":"s","v":"signal"},{"t":"s","v":"dict"},{"t":"s","v":"loading"},{"t":"s","v":"data"},{"t":"s","v":"error"},{"t":"s","v":"promise-then"},{"t":"s","v":"cek-call"},{"t":"s","v":"fetch-fn"},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[20,0,0,20,1,0,1,3,0,4,1,4,0,20,4,0,1,5,0,2,52,2,0,6,49,2,50],"constants":[{"t":"s","v":"reset!"},{"t":"s","v":"state"},{"t":"s","v":"dict"},{"t":"s","v":"loading"},{"t":"s","v":"data"},{"t":"s","v":"error"}]}},{"t":"code","v":{"arity":0,"upvalue-count":0,"bytecode":[20,0,0,20,1,0,1,3,0,4,1,4,0,2,1,5,0,20,6,0,52,2,0,6,49,2,50],"constants":[{"t":"s","v":"reset!"},{"t":"s","v":"state"},{"t":"s","v":"dict"},{"t":"s","v":"loading"},{"t":"s","v":"data"},{"t":"s","v":"error"},{"t":"s","v":"err"}]}},{"t":"s","v":"state"}]}}]}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1792,7 +1792,7 @@
blake2_js_for_wasm_create: blake2_js_for_wasm_create};
}
(globalThis))
({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["sx-afdcb8e8",[2]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,4]],["dune__exe__Sx_browser-8ae21d0a",[2,3,5]],["std_exit-10fb8830",[2]],["start-80fdb768",0]],"generated":(b=>{var
({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["sx-3b0282d1",[2]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,4]],["dune__exe__Sx_browser-8ae21d0a",[2,3,5]],["std_exit-10fb8830",[2]],["start-80fdb768",0]],"generated":(b=>{var
c=b,a=b?.module?.export||b;return{"env":{"caml_ba_kind_of_typed_array":()=>{throw new
Error("caml_ba_kind_of_typed_array not implemented")},"caml_exn_with_js_backtrace":()=>{throw new
Error("caml_exn_with_js_backtrace not implemented")},"caml_int64_create_lo_mi_hi":()=>{throw new