From 465ce1abcb05474cb279bb0437aa9a57c7f3013d Mon Sep 17 00:00:00 2001 From: giles Date: Tue, 31 Mar 2026 07:42:59 +0000 Subject: [PATCH] Page helpers in pure SX: 3 OCaml primitives + SX data + SX helpers Add pretty-print, read-file, env-list-typed primitives to OCaml kernel. Convert Python reference data (attrs, headers, events, primitives) to SX data files. Implement page helpers (component-source, handler-source, read-spec-file, reference-data, etc.) as pure SX functions. The helper dispatcher in HTTP mode looks up named functions in the env and calls them directly, replacing the Python IO bridge path. Co-Authored-By: Claude Opus 4.6 (1M context) --- hosts/ocaml/bin/sx_server.ml | 138 +++++++++-- sx/sx/data/helpers.sx | 114 +++++++++ sx/sx/data/reference-details.sx | 420 ++++++++++++++++++++++++++++++++ sx/sx/data/reference.sx | 120 +++++++++ 4 files changed, 766 insertions(+), 26 deletions(-) create mode 100644 sx/sx/data/helpers.sx create mode 100644 sx/sx/data/reference-details.sx create mode 100644 sx/sx/data/reference.sx diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index e1f4d41d..3a029d7d 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -1907,33 +1907,116 @@ let http_load_files env files = ) files; rebind_host_extensions env +(* Pretty printer — AST value → formatted SX source string *) +let pp_atom = Sx_types.inspect + +let rec est_width = function + | Nil -> 3 | Bool true -> 4 | Bool false -> 5 + | Number n -> String.length (if Float.is_integer n then string_of_int (int_of_float n) else Printf.sprintf "%g" n) + | String s -> String.length s + 2 + | Symbol s -> String.length s + | Keyword k -> String.length k + 1 + | SxExpr s -> String.length s + 2 + | List items | ListRef { contents = items } -> + 2 + List.fold_left (fun acc x -> acc + est_width x + 1) 0 items + | _ -> 10 + +let pretty_print_value ?(max_width=80) v = + let buf = Buffer.create 4096 in + let rec pp indent v = + match v with + | List items | ListRef { contents = items } when items <> [] -> + if est_width v <= max_width - indent then + Buffer.add_string buf (pp_atom v) + else begin + Buffer.add_char buf '('; + let head = List.hd items in + Buffer.add_string buf (pp_atom head); + let child_indent = indent + 2 in + let rest = List.tl items in + let rec emit = function + | [] -> () + | Keyword k :: v :: rest -> + Buffer.add_char buf '\n'; + Buffer.add_string buf (String.make child_indent ' '); + Buffer.add_char buf ':'; + Buffer.add_string buf k; + Buffer.add_char buf ' '; + pp child_indent v; + emit rest + | item :: rest -> + Buffer.add_char buf '\n'; + Buffer.add_string buf (String.make child_indent ' '); + pp child_indent item; + emit rest + in + emit rest; + Buffer.add_char buf ')' + end + | _ -> Buffer.add_string buf (pp_atom v) + in + pp 0 v; + Buffer.contents buf + let http_setup_page_helpers env = - (* Page helpers that Python normally provides. Minimal stubs for HTTP mode. - These return empty/nil so pages render without hanging. *) let bind name fn = ignore (env_bind env name (NativeFn (name, fn))) in - (* highlight — passthrough without syntax coloring *) - bind "highlight" (fun args -> + + (* Primitive 1: pretty-print — AST → formatted SX source *) + bind "pretty-print" (fun args -> match args with - | String code :: _ -> - let escaped = escape_sx_string code in - SxExpr (Printf.sprintf "(pre :class \"text-sm overflow-x-auto\" (code \"%s\"))" escaped) - | _ -> Nil); - (* Stub all Python page helpers with nil/empty returns *) + | [v] -> String (pretty_print_value v) + | _ -> raise (Eval_error "pretty-print: expected 1 argument")); + + (* Primitive 2: read-file — path → string contents or nil *) + bind "read-file" (fun args -> + match args with + | [String path] -> + (try + let ic = open_in path in + let n = in_channel_length ic in + let s = Bytes.create n in + really_input ic s 0 n; + close_in ic; + String (Bytes.to_string s) + with _ -> Nil) + | _ -> raise (Eval_error "read-file: expected string path")); + + (* Primitive 3: env-list-typed — list all bindings of a given type *) + bind "env-list-typed" (fun args -> + match args with + | [String type_name] -> + let matches = ref [] in + Hashtbl.iter (fun id v -> + let matches_type = match type_name, v with + | "component", Component _ -> true + | "island", Island _ -> true + | "lambda", Lambda _ -> true + | "macro", Macro _ -> true + | "native", NativeFn _ -> true + | _ -> false + in + if matches_type then + matches := String (Sx_types.unintern id) :: !matches + ) env.bindings; + List (List.sort compare !matches) + | _ -> raise (Eval_error "env-list-typed: expected type name string")); + + (* helper dispatcher — looks up named function in env, calls it directly. + In coroutine mode this goes through the Python IO bridge. + In HTTP mode we dispatch locally to functions defined by SX helpers. *) + bind "helper" (fun args -> + match args with + | String name :: rest -> + (try + let fn = env_get env name in + Sx_ref.cek_call fn (List rest) + with Eval_error _ -> + Printf.eprintf "[helper] not found: %s\n%!" name; + Nil) + | _ -> raise (Eval_error "helper: expected (helper \"name\" ...args)")); + + (* Stub remaining demo/action helpers that need real IO *) let stub name = bind name (fun _args -> Nil) in - let stub_s name = bind name (fun _args -> String "") in - stub_s "component-source"; - stub_s "handler-source"; - stub "primitives-data"; - stub "special-forms-data"; - stub "reference-data"; - stub "attr-detail-data"; - stub "header-detail-data"; - stub "event-detail-data"; - stub_s "read-spec-file"; - stub "bootstrapper-data"; - stub "bundle-analyzer-data"; - stub "routing-analyzer-data"; - stub "data-test-data"; stub "run-spec-tests"; stub "run-modular-tests"; stub "streaming-demo-data"; @@ -1942,9 +2025,7 @@ let http_setup_page_helpers env = stub "action:add-demo-item"; stub "offline-demo-data"; stub "prove-data"; - stub "page-helpers-demo-data"; - stub "spec-explorer-data"; - stub "spec-explorer-data-by-slug" + stub "page-helpers-demo-data" let http_mode port = let env = make_server_env () in @@ -1972,6 +2053,11 @@ let http_mode port = let dev_path = project_dir ^ "/sx/sx" in if Sys.file_exists (docker_path ^ "/page-functions.sx") then docker_path else dev_path in + (* Expose project paths to SX helpers *) + ignore (env_bind env "_project-dir" (String project_dir)); + ignore (env_bind env "_spec-dir" (String spec_base)); + ignore (env_bind env "_lib-dir" (String lib_base)); + ignore (env_bind env "_web-dir" (String web_base)); let t0 = Unix.gettimeofday () in (* Core spec + adapters. Skip: primitives.sx (declarative metadata — all prims native in OCaml), diff --git a/sx/sx/data/helpers.sx b/sx/sx/data/helpers.sx new file mode 100644 index 00000000..e1261425 --- /dev/null +++ b/sx/sx/data/helpers.sx @@ -0,0 +1,114 @@ +(define + component-source + (fn + (name) + (let + ((val (env-get name))) + (if + (or (component? val) (island? val)) + (let + ((form-name (if (island? val) "defisland" "defcomp")) + (params (component-params val)) + (body (component-body val)) + (cname (component-name val))) + (pretty-print + (list + (make-symbol form-name) + (make-symbol (str "~" cname)) + params + body))) + (str ";;; Not found: " name))))) + +(define + handler-source + (fn + (name) + (let + ((val (env-get name))) + (if + (not (nil? val)) + (pretty-print val) + (str ";;; Handler not found: " name))))) + +(define _spec-dirs (list "spec" "web" "shared/sx/ref" "lib")) + +(define + read-spec-file + (fn + (filename) + (let + ((result nil)) + (for-each + (fn + (dir) + (when + (nil? result) + (let + ((content (read-file (str dir "/" filename)))) + (when (not (nil? content)) (set! result content))))) + _spec-dirs) + (or result (str ";; spec file not found: " filename))))) + +(define primitives-data (fn () primitives-by-category)) + +(define + _spec-search-dirs + (fn + () + (list _spec-dir _web-dir _lib-dir (str _project-dir "/shared/sx/ref")))) + +(define + read-spec-file + (fn + (filename) + (let + ((result nil)) + (for-each + (fn + (dir) + (when + (nil? result) + (let + ((content (read-file (str dir "/" filename)))) + (when (not (nil? content)) (set! result content))))) + (_spec-search-dirs)) + (or result (str ";; spec file not found: " filename))))) + +(define + attr-detail-data + (fn (slug) (build-attr-detail slug (get attr-details slug)))) + +(define + header-detail-data + (fn (slug) (build-header-detail slug (get header-details slug)))) + +(define + event-detail-data + (fn (slug) (build-event-detail slug (get event-details slug)))) + +(define _spec-slug-map {:primitives (list "primitives.sx" "Primitives" "Built-in pure functions") :render (list "render.sx" "Renderer" "Three rendering modes") :parser (list "parser.sx" "Parser" "Tokenization and parsing") :evaluator (list "evaluator.sx" "Evaluator" "CEK machine evaluator") :signals (list "signals.sx" "Signals" "Fine-grained reactive primitives") :types (list "types.sx" "Types" "Optional gradual type system")}) + +(define + spec-explorer-data-by-slug + (fn + (slug) + (let + ((entry (get _spec-slug-map (make-keyword slug)))) + (if + (nil? entry) + nil + (let + ((filename (first entry)) + (title (nth entry 1)) + (desc (nth entry 2))) + {:description desc :title title :filename filename :source (read-spec-file filename)}))))) + +(define spec-explorer-data (fn (filename title desc) {:description (or desc "") :title (or title filename) :filename filename :source (read-spec-file filename)})) + +(define bootstrapper-data (fn (target) {:target (or target "js") :status "not-implemented" :components (list)})) + +(define bundle-analyzer-data (fn () {:bundles (list) :status "not-implemented"})) + +(define routing-analyzer-data (fn () {:routes (list) :status "not-implemented"})) + +(define data-test-data (fn () {:status "not-implemented" :tests (list)})) diff --git a/sx/sx/data/reference-details.sx b/sx/sx/data/reference-details.sx new file mode 100644 index 00000000..e4fbb433 --- /dev/null +++ b/sx/sx/data/reference-details.sx @@ -0,0 +1,420 @@ +;; Detail data for reference pages +;; Auto-generated from sx/content/pages.py + +(define header-details + { + "SX-Request" + { + :direction "request" + :description "Sent on every sx-initiated request. Allows the server to distinguish AJAX partial requests from full page loads, and return the appropriate response format (fragment vs full page)." + :example ";; Server-side: check for sx request\n(if (header \"SX-Request\")\n ;; Return a fragment\n (div :class \"result\" \"Partial content\")\n ;; Return full page\n (~full-page-layout ...))" + } + "SX-Current-URL" + { + :direction "request" + :description "Sends the browser's current URL so the server knows where the user is. Useful for server-side logic that depends on context — e.g. highlighting the current nav item, or returning context-appropriate content." + :example ";; Server reads the current URL to decide context\n(let ((url (header \"SX-Current-URL\")))\n (nav\n (a :href \"/docs\" :class (if (starts-with? url \"/docs\") \"active\" \"\") \"Docs\")\n (a :href \"/api\" :class (if (starts-with? url \"/api\") \"active\" \"\") \"API\")))" + } + "SX-Target" + { + :direction "request" + :description "Tells the server which element will receive the response. The server can use this to tailor the response — for example, returning different content depending on whether the target is a sidebar, modal, or main panel." + :example ";; Server checks target to decide response format\n(let ((target (header \"SX-Target\")))\n (if (= target \"#sidebar\")\n (~compact-summary :data data)\n (~full-detail :data data)))" + } + "SX-Components" + { + :direction "request" + :description "Comma-separated list of component names the client already has cached. The server can skip sending defcomp definitions the client already knows, reducing response size. This is the component caching protocol." + :example ";; Client sends: SX-Components: ~card,~shared:layout/nav-link,~footer\n;; Server omits those defcomps from the response.\n;; Only new/changed components are sent.\n(response\n :components (filter-new known-components)\n :content (~page-content))" + } + "SX-Css" + { + :direction "request" + :description "Sends the CSS classes or hash the client already has. The server uses this to send only new CSS rules the client needs, avoiding duplicate rule injection. Part of the on-demand CSS protocol." + :example ";; Client sends hash of known CSS classes\n;; Server compares and only returns new classes\n(let ((client-css (header \"SX-Css\")))\n (set-header \"SX-Css-Add\"\n (join \",\" (diff new-classes client-css))))" + } + "SX-History-Restore" + { + :direction "request" + :description "Set to \"true\" when the browser restores a page from history (back/forward). The server can use this to return cached content or skip side effects that should only happen on initial navigation." + :example ";; Skip analytics on history restore\n(when (not (header \"SX-History-Restore\"))\n (track-page-view url))\n(~page-content :data data)" + } + "SX-Css-Hash" + { + :direction "both" + :description "Request: 8-character hash of the client's known CSS class set. Response: hash of the cumulative CSS set after this response. Client stores the response hash and sends it on the next request, enabling efficient CSS delta tracking." + :example ";; Request header: SX-Css-Hash: a1b2c3d4\n;; Server compares hash to decide if CSS diff needed\n;;\n;; Response header: SX-Css-Hash: e5f6g7h8\n;; Client stores new hash for next request" + } + "SX-Prompt" + { + :direction "request" + :description "Contains the value entered by the user in a window.prompt() dialog, triggered by the sx-prompt attribute. Allows collecting a single text input without a form." + :example ";; Button triggers a prompt dialog\n(button :sx-get \"/api/rename\"\n :sx-prompt \"Enter new name:\"\n \"Rename\")\n\n;; Server reads the prompted value\n(let ((name (header \"SX-Prompt\")))\n (span \"Renamed to: \" (strong name)))" + :demo "ref-header-prompt-demo" + } + "SX-Css-Add" + { + :direction "response" + :description "Comma-separated list of new CSS class names added by this response. The client injects the corresponding CSS rules into the document. Only classes the client doesn't already have are included." + :example ";; Server response includes new CSS classes\n;; SX-Css-Add: bg-emerald-500,text-white,rounded-xl\n;;\n;; Client automatically injects rules for these\n;; classes from the style dictionary." + } + "SX-Trigger" + { + :direction "response" + :description "Dispatch custom DOM event(s) on the target element after the response is received. Can be a simple event name or JSON for multiple events with detail data. Useful for coordinating UI updates across components." + :example ";; Simple event\n;; SX-Trigger: itemAdded\n;;\n;; Multiple events with data\n;; SX-Trigger: {\"itemAdded\": {\"id\": 42}, \"showNotification\": {\"message\": \"Saved!\"}}\n;;\n;; Listen in SX:\n(div :sx-on:itemAdded \"this.querySelector('.count').textContent = event.detail.id\")" + :demo "ref-header-trigger-demo" + } + "SX-Trigger-After-Swap" + { + :direction "response" + :description "Like SX-Trigger, but fires after the DOM swap completes. Use this when your event handler needs to reference the new DOM content that was just swapped in." + :example ";; Server signals that new content needs initialization\n;; SX-Trigger-After-Swap: contentReady\n;;\n;; Client initializes after swap\n(div :sx-on:contentReady \"initCharts(this)\")" + } + "SX-Trigger-After-Settle" + { + :direction "response" + :description "Like SX-Trigger, but fires after the DOM has fully settled — scripts executed, transitions complete. The latest point to react to a response." + :example ";; SX-Trigger-After-Settle: animationReady\n;;\n;; Trigger animations after everything has settled\n(div :sx-on:animationReady \"this.classList.add('fade-in')\")" + } + "SX-Retarget" + { + :direction "response" + :description "Override the target element for this response. The server can redirect content to a different element than what the client specified in sx-target. Useful for error messages or redirecting content dynamically." + :example ";; Client targets a form result area\n(form :sx-post \"/api/save\"\n :sx-target \"#result\" ...)\n\n;; Server redirects errors to a different element\n;; SX-Retarget: #error-banner\n(div :class \"error\" \"Validation failed\")" + :demo "ref-header-retarget-demo" + } + "SX-Reswap" + { + :direction "response" + :description "Override the swap strategy for this response. The server can change how content is inserted regardless of what the client specified in sx-swap. Useful when the server decides the swap mode based on the result." + :example ";; Client expects innerHTML swap\n(button :sx-get \"/api/check\"\n :sx-target \"#panel\" :sx-swap \"innerHTML\" ...)\n\n;; Server overrides to append instead\n;; SX-Reswap: beforeend\n(div :class \"notification\" \"New item added\")" + } + "SX-Redirect" + { + :direction "response" + :description "Redirect the browser to a new URL using full page navigation. Unlike sx-push-url which does client-side history, this triggers a real browser navigation — useful after form submissions like login or checkout." + :example ";; After successful login, redirect to dashboard\n;; SX-Redirect: /dashboard\n;;\n;; Server handler:\n(when (valid-credentials? user pass)\n (set-header \"SX-Redirect\" \"/dashboard\")\n (span \"Redirecting...\"))" + } + "SX-Refresh" + { + :direction "response" + :description "Set to \"true\" to reload the current page. A blunt tool — useful when server-side state has changed significantly and a partial update won't suffice." + :example ";; After a major state change, force refresh\n;; SX-Refresh: true\n;;\n;; Server handler:\n(when (deploy-complete?)\n (set-header \"SX-Refresh\" \"true\")\n (span \"Deployed — refreshing...\"))" + } + "SX-Location" + { + :direction "response" + :description "Trigger client-side navigation: fetch the given URL, swap it into #main-panel, and push to browser history. Like clicking an sx-boosted link, but triggered from the server. Can be a URL string or JSON with options." + :example ";; Simple: navigate to a page\n;; SX-Location: /docs/introduction\n;;\n;; With options:\n;; SX-Location: {\"path\": \"/(language.(doc.intro))\", \"target\": \"#sidebar\", \"swap\": \"innerHTML\"}" + } + "SX-Replace-Url" + { + :direction "response" + :description "Replace the current URL using history.replaceState without creating a new history entry. Useful for normalizing URLs after redirects, or updating the URL to reflect server-resolved state." + :example ";; Normalize URL after slug resolution\n;; SX-Replace-Url: /docs/introduction\n;;\n;; Server handler:\n(let ((canonical (resolve-slug slug)))\n (set-header \"SX-Replace-Url\" canonical)\n (~doc-content :slug canonical))" + } + }) + +(define event-details + { + "sx:beforeRequest" + { + :description "Fired on the triggering element before an sx request is issued. Call event.preventDefault() to cancel the request entirely. Useful for validation, confirmation, or conditional request logic." + :example ";; Cancel request if form is empty\n(form :sx-post \"/api/save\"\n :sx-target \"#result\"\n :sx-on:sx:beforeRequest \"if (!this.querySelector('input').value) event.preventDefault()\"\n (input :name \"data\" :placeholder \"Required\")\n (button :type \"submit\" \"Save\"))" + :demo "ref-event-before-request-demo" + } + "sx:afterRequest" + { + :description "Fired on the triggering element after a successful sx response is received, before the swap happens. event.detail contains the response status. Use this for logging, analytics, or pre-swap side effects." + :example ";; Log successful requests\n(button :sx-get \"/api/data\"\n :sx-target \"#result\"\n :sx-on:sx:afterRequest \"console.log('Response received', event.detail)\"\n \"Load data\")" + :demo "ref-event-after-request-demo" + } + "sx:afterSwap" + { + :description "Fired on the triggering element after the response content has been swapped into the DOM. event.detail contains the target element and swap style. Use this to initialize UI on newly inserted content." + :example ";; Run code after content is swapped in\n(button :sx-get \"/api/items\"\n :sx-target \"#item-list\"\n :sx-on:sx:afterSwap \"console.log('Swapped into', event.detail.target)\"\n \"Load items\")" + :demo "ref-event-after-swap-demo" + } + "sx:responseError" + { + :description "Fired when the server responds with an HTTP error (4xx or 5xx). event.detail contains the status code and response text. Use this for error handling, showing notifications, or retry logic." + :example ";; Show error notification\n(div :sx-on:sx:responseError \"alert('Error: ' + event.detail.status)\"\n (button :sx-get \"/api/risky\"\n :sx-target \"#result\"\n \"Try it\")\n (div :id \"result\"))" + :demo "ref-event-response-error-demo" + } + "sx:requestError" + { + :description "Fired when the request fails to send — typically a network error, DNS failure, or CORS issue. Unlike sx:responseError, no HTTP response was received at all. Aborted requests (e.g. from sx-sync) do not fire this event." + :example ";; Handle network failures\n(div :sx-on:sx:requestError \"this.querySelector('.status').textContent = 'Offline'\"\n (button :sx-get \"/api/data\"\n :sx-target \"#result\"\n \"Load\")\n (span :class \"status\")\n (div :id \"result\"))" + :demo "ref-event-request-error-demo" + } + "sx:validationFailed" + { + :description "Fired when sx-validate is set and the form fails HTML5 validation. The request is not sent. Use this to show custom validation UI or highlight invalid fields." + :example ";; Highlight invalid fields\n(form :sx-post \"/api/save\"\n :sx-validate \"true\"\n :sx-on:sx:validationFailed \"this.classList.add('shake')\"\n (input :type \"email\" :required \"true\" :name \"email\"\n :placeholder \"Email (required)\")\n (button :type \"submit\" \"Save\"))" + :demo "ref-event-validation-failed-demo" + } + "sx:clientRoute" + { + :description "Fired on the swap target after successful client-side routing. No server request was made — the page was rendered entirely in the browser from component definitions the client already has. event.detail contains the pathname. Use this to update navigation state, analytics, or other side effects that should run on client-only navigation. The event bubbles, so you can listen on document.body." + :example ";; Pages with no :data are client-routable.\n;; sx-boost containers try client routing first.\n;; On success, sx:clientRoute fires on the swap target.\n(nav :sx-boost \"#main-panel\"\n (a :href \"/(etc.(essay))\" \"Essays\")\n (a :href \"/(etc.(plan))\" \"Plans\"))\n\n;; Listen in body.js:\n;; document.body.addEventListener(\"sx:clientRoute\",\n;; function(e) { updateNav(e.detail.pathname); })" + :demo "ref-event-client-route-demo" + } + "sx:sseOpen" + { + :description "Fired when a Server-Sent Events connection is successfully established. Use this to update connection status indicators." + :example ";; Show connected status\n(div :sx-sse \"/api/stream\"\n :sx-on:sx:sseOpen \"this.querySelector('.status').textContent = 'Connected'\"\n (span :class \"status\" \"Connecting...\")\n (div :id \"messages\"))" + :demo "ref-event-sse-open-demo" + } + "sx:sseMessage" + { + :description "Fired when an SSE message is received and swapped into the DOM. event.detail contains the message data. Fires for each individual message." + :example ";; Count received messages\n(div :sx-sse \"/api/stream\"\n :sx-sse-swap \"update\"\n :sx-on:sx:sseMessage \"this.dataset.count = (parseInt(this.dataset.count||0)+1)\"\n (span :class \"count\" \"0\") \" messages received\")" + :demo "ref-event-sse-message-demo" + } + "sx:sseError" + { + :description "Fired when an SSE connection encounters an error or is closed unexpectedly. Use this to show reconnection status or fall back to polling." + :example ";; Show disconnected status\n(div :sx-sse \"/api/stream\"\n :sx-on:sx:sseError \"this.querySelector('.status').textContent = 'Disconnected'\"\n (span :class \"status\" \"Connecting...\")\n (div :id \"messages\"))" + :demo "ref-event-sse-error-demo" + } + }) + +(define attr-details + { + "sx-get" + { + :description "Issues a GET request to the given URL when triggered. The response HTML is swapped into the target element. This is the most common sx attribute — use it for loading content, navigation, and any read operation." + :demo "ref-get-demo" + :example "(button :sx-get \"/(geography.(hypermedia.(reference.(api.time))))\"\n :sx-target \"#ref-get-result\"\n :sx-swap \"innerHTML\"\n \"Load server time\")" + :handler "(defhandler ref-time (&key)\n (let ((now (format-time (now) \"%H:%M:%S\")))\n (span :class \"text-stone-800 text-sm\"\n \"Server time: \" (strong now))))" + } + "sx-post" + { + :description "Issues a POST request to the given URL. Form values from the enclosing form (or sx-include target) are sent as the request body. Use for creating resources, submitting forms, and any write operation." + :demo "ref-post-demo" + :example "(form :sx-post \"/(geography.(hypermedia.(reference.(api.greet))))\"\n :sx-target \"#ref-post-result\"\n :sx-swap \"innerHTML\"\n (input :type \"text\" :name \"name\"\n :placeholder \"Your name\")\n (button :type \"submit\" \"Greet\"))" + :handler "(defhandler ref-greet (&key)\n (let ((name (or (form-data \"name\") \"stranger\")))\n (span :class \"text-stone-800 text-sm\"\n \"Hello, \" (strong name) \"!\")))" + } + "sx-put" + { + :description "Issues a PUT request to the given URL. Used for full replacement updates of a resource. Form values are sent as the request body." + :demo "ref-put-demo" + :example "(button :sx-put \"/(geography.(hypermedia.(reference.(api.status))))\"\n :sx-target \"#ref-put-view\"\n :sx-swap \"innerHTML\"\n :sx-vals \"{\\\"status\\\": \\\"published\\\"}\"\n \"Publish\")" + :handler "(defhandler ref-status (&key)\n (let ((status (or (form-data \"status\") \"unknown\")))\n (span :class \"text-stone-700 text-sm\"\n \"Status: \" (strong status) \" — updated via PUT\")))" + } + "sx-delete" + { + :description "Issues a DELETE request to the given URL. Commonly paired with sx-confirm for a confirmation dialog, and sx-swap \"delete\" to remove the element from the DOM." + :demo "ref-delete-demo" + :example "(button :sx-delete \"/(geography.(hypermedia.(reference.(api.(item.1)))))\"\n :sx-target \"#ref-del-1\"\n :sx-swap \"delete\"\n \"Remove\")" + :handler "(defhandler ref-delete (&key item-id)\n ;; Empty response — swap \"delete\" removes the target\n \"\")" + } + "sx-patch" + { + :description "Issues a PATCH request to the given URL. Used for partial updates — only changed fields are sent. Form values are sent as the request body." + :demo "ref-patch-demo" + :example "(button :sx-patch \"/(geography.(hypermedia.(reference.(api.theme))))\"\n :sx-vals \"{\\\"theme\\\": \\\"dark\\\"}\"\n :sx-target \"#ref-patch-val\"\n :sx-swap \"innerHTML\"\n \"Dark\")" + :handler "(defhandler ref-theme (&key)\n (let ((theme (or (form-data \"theme\") \"unknown\")))\n (str theme)))" + } + "sx-trigger" + { + :description "Specifies which DOM event triggers the request. Defaults to 'click' for most elements and 'submit' for forms. Supports modifiers: once, changed, delay: