From dfccd113fcab1961bd434b1bba1e51a82681558e Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 5 Mar 2026 01:48:33 +0000 Subject: [PATCH] Move sx docs page helpers from Python to pure SX composition (Phase 6) Nav data, section nav, example content, reference table builders, and all slug dispatch now live in .sx files. Python helpers reduced to data-only returns (highlight, primitives-data, reference-data, attr-detail-data). Deleted essays.py and utils.py entirely. Co-Authored-By: Claude Opus 4.6 --- sx/sx/docs.sx | 46 +++ sx/sx/examples-content.sx | 316 +++++++++++++++++++ sx/sx/nav-data.sx | 87 ++++++ sx/sxc/pages/docs.sx | 147 +++++++-- sx/sxc/pages/essays.py | 619 -------------------------------------- sx/sxc/pages/helpers.py | 225 ++++++++------ sx/sxc/pages/utils.py | 116 ------- 7 files changed, 692 insertions(+), 864 deletions(-) create mode 100644 sx/sx/examples-content.sx create mode 100644 sx/sx/nav-data.sx delete mode 100644 sx/sxc/pages/essays.py delete mode 100644 sx/sxc/pages/utils.py diff --git a/sx/sx/docs.sx b/sx/sx/docs.sx index 1747e19..43a9731 100644 --- a/sx/sx/docs.sx +++ b/sx/sx/docs.sx @@ -63,3 +63,49 @@ (button :onclick "localStorage.removeItem('sx-components-hash');localStorage.removeItem('sx-components-src');var e=Sx.getEnv();Object.keys(e).forEach(function(k){if(k.charAt(0)==='~')delete e[k]});var b=this;b.textContent='Cleared!';setTimeout(function(){b.textContent='Clear component cache'},2000)" :class "text-xs text-stone-400 hover:text-stone-600 border border-stone-200 rounded px-2 py-1 transition-colors" "Clear component cache")) + +;; --------------------------------------------------------------------------- +;; Data-driven table builders — replace Python sx_call() composition +;; --------------------------------------------------------------------------- + +;; Build attr table from a list of {name, desc, exists, href} dicts. +;; Replaces _attr_table_sx() in utils.py. +(defcomp ~doc-attr-table-from-data (&key title attrs) + (~doc-attr-table :title title + :rows (<> (map (fn (a) + (~doc-attr-row + :attr (get a "name") + :description (get a "desc") + :exists (get a "exists") + :href (get a "href"))) + attrs)))) + +;; Build headers table from a list of {name, value, desc} dicts. +;; Replaces _headers_table_sx() in utils.py. +(defcomp ~doc-headers-table-from-data (&key title headers) + (~doc-headers-table :title title + :rows (<> (map (fn (h) + (~doc-headers-row + :name (get h "name") + :value (get h "value") + :description (get h "desc"))) + headers)))) + +;; Build two-col table from a list of {name, desc} dicts. +;; Replaces the _reference_events_sx / _reference_js_api_sx builders. +(defcomp ~doc-two-col-table-from-data (&key title intro col1 col2 items) + (~doc-two-col-table :title title :intro intro :col1 col1 :col2 col2 + :rows (<> (map (fn (item) + (~doc-two-col-row + :name (get item "name") + :description (get item "desc"))) + items)))) + +;; Build all primitives category tables from a {category: [prim, ...]} dict. +;; Replaces _primitives_section_sx() in utils.py. +(defcomp ~doc-primitives-tables (&key primitives) + (<> (map (fn (cat) + (~doc-primitives-table + :category cat + :primitives (get primitives cat))) + (keys primitives)))) diff --git a/sx/sx/examples-content.sx b/sx/sx/examples-content.sx new file mode 100644 index 0000000..a497927 --- /dev/null +++ b/sx/sx/examples-content.sx @@ -0,0 +1,316 @@ +;; Example page defcomps — one per example. +;; Each calls ~example-page-content with static string data. +;; Replaces all _example_*_sx() builders in essays.py. + +(defcomp ~example-click-to-load () + (~example-page-content + :title "Click to Load" + :description "The simplest sx interaction: click a button, fetch content from the server, swap it in." + :demo-description "Click the button to load server-rendered content." + :demo (~click-to-load-demo) + :sx-code "(button\n :sx-get \"/examples/api/click\"\n :sx-target \"#click-result\"\n :sx-swap \"innerHTML\"\n \"Load content\")" + :handler-code "@bp.get(\"/examples/api/click\")\nasync def api_click():\n now = datetime.now().strftime(...)\n return sx_response(\n f'(~click-result :time \"{now}\")')" + :comp-placeholder-id "click-comp" + :wire-placeholder-id "click-wire" + :wire-note "The server responds with content-type text/sx. New CSS rules are prepended as a style tag. Clear the component cache to see component definitions included in the wire response.")) + +(defcomp ~example-form-submission () + (~example-page-content + :title "Form Submission" + :description "Forms with sx-post submit via AJAX and swap the response into a target." + :demo-description "Enter a name and submit." + :demo (~form-demo) + :sx-code "(form\n :sx-post \"/examples/api/form\"\n :sx-target \"#form-result\"\n :sx-swap \"innerHTML\"\n (input :type \"text\" :name \"name\")\n (button :type \"submit\" \"Submit\"))" + :handler-code "@bp.post(\"/examples/api/form\")\nasync def api_form():\n form = await request.form\n name = form.get(\"name\", \"\")\n return sx_response(\n f'(~form-result :name \"{name}\")')" + :comp-placeholder-id "form-comp" + :wire-placeholder-id "form-wire")) + +(defcomp ~example-polling () + (~example-page-content + :title "Polling" + :description "Use sx-trigger with \"every\" to poll the server at regular intervals." + :demo-description "This div polls the server every 2 seconds." + :demo (~polling-demo) + :sx-code "(div\n :sx-get \"/examples/api/poll\"\n :sx-trigger \"load, every 2s\"\n :sx-swap \"innerHTML\"\n \"Loading...\")" + :handler-code "@bp.get(\"/examples/api/poll\")\nasync def api_poll():\n poll_count[\"n\"] += 1\n now = datetime.now().strftime(\"%H:%M:%S\")\n count = min(poll_count[\"n\"], 10)\n return sx_response(\n f'(~poll-result :time \"{now}\" :count {count})')" + :comp-placeholder-id "poll-comp" + :wire-placeholder-id "poll-wire" + :wire-note "Updates every 2 seconds — watch the time and count change.")) + +(defcomp ~example-delete-row () + (~example-page-content + :title "Delete Row" + :description "sx-delete with sx-swap \"outerHTML\" and an empty response removes the row from the DOM." + :demo-description "Click delete to remove a row. Uses sx-confirm for confirmation." + :demo (~delete-demo :items (list + (list "1" "Implement dark mode") + (list "2" "Fix login bug") + (list "3" "Write documentation") + (list "4" "Deploy to production") + (list "5" "Add unit tests"))) + :sx-code "(button\n :sx-delete \"/api/delete/1\"\n :sx-target \"#row-1\"\n :sx-swap \"outerHTML\"\n :sx-confirm \"Delete this item?\"\n \"delete\")" + :handler-code "@bp.delete(\"/examples/api/delete/\")\nasync def api_delete(item_id: str):\n # Empty response — outerHTML swap removes the row\n return Response(\"\", status=200,\n content_type=\"text/sx\")" + :comp-placeholder-id "delete-comp" + :wire-placeholder-id "delete-wire" + :wire-note "Empty body — outerHTML swap replaces the target element with nothing.")) + +(defcomp ~example-inline-edit () + (~example-page-content + :title "Inline Edit" + :description "Click edit to swap a display view for an edit form. Save swaps back." + :demo-description "Click edit, modify the text, save or cancel." + :demo (~inline-edit-demo) + :sx-code ";; View mode — shows text + edit button\n(~inline-view :value \"some text\")\n\n;; Edit mode — returned by server on click\n(~inline-edit-form :value \"some text\")" + :handler-code "@bp.get(\"/examples/api/edit\")\nasync def api_edit_form():\n value = request.args.get(\"value\", \"\")\n return sx_response(\n f'(~inline-edit-form :value \"{value}\")')\n\n@bp.post(\"/examples/api/edit\")\nasync def api_edit_save():\n form = await request.form\n value = form.get(\"value\", \"\")\n return sx_response(\n f'(~inline-view :value \"{value}\")')" + :comp-placeholder-id "edit-comp" + :comp-heading "Components" + :handler-heading "Server handlers" + :wire-placeholder-id "edit-wire")) + +(defcomp ~example-oob-swaps () + (~example-page-content + :title "Out-of-Band Swaps" + :description "sx-swap-oob lets a single response update multiple elements anywhere in the DOM." + :demo-description "One request updates both Box A (via sx-target) and Box B (via sx-swap-oob)." + :demo (~oob-demo) + :sx-code ";; Button targets Box A\n(button\n :sx-get \"/examples/api/oob\"\n :sx-target \"#oob-box-a\"\n :sx-swap \"innerHTML\"\n \"Update both boxes\")" + :handler-code "@bp.get(\"/examples/api/oob\")\nasync def api_oob():\n now = datetime.now().strftime(\"%H:%M:%S\")\n return sx_response(\n f'(<>'\n f' (p \"Box A updated at {now}\")'\n f' (div :id \"oob-box-b\"'\n f' :sx-swap-oob \"innerHTML\"'\n f' (p \"Box B updated at {now}\")))')" + :wire-placeholder-id "oob-wire" + :wire-note "The fragment contains both the main content and an OOB element. sx.js splits them: main content goes to sx-target, OOB elements find their targets by ID.")) + +(defcomp ~example-lazy-loading () + (~example-page-content + :title "Lazy Loading" + :description "Use sx-trigger=\"load\" to fetch content as soon as the element enters the DOM. Great for deferring expensive content below the fold." + :demo-description "Content loads automatically when the page renders." + :demo (~lazy-loading-demo) + :sx-code "(div\n :sx-get \"/examples/api/lazy\"\n :sx-trigger \"load\"\n :sx-swap \"innerHTML\"\n (div :class \"animate-pulse\" \"Loading...\"))" + :handler-code "@bp.get(\"/examples/api/lazy\")\nasync def api_lazy():\n now = datetime.now().strftime(...)\n return sx_response(\n f'(~lazy-result :time \"{now}\")')" + :comp-placeholder-id "lazy-comp" + :wire-placeholder-id "lazy-wire")) + +(defcomp ~example-infinite-scroll () + (~example-page-content + :title "Infinite Scroll" + :description "A sentinel element at the bottom uses sx-trigger=\"intersect once\" to load the next page when scrolled into view. Each response appends items and a new sentinel." + :demo-description "Scroll down in the container to load more items (5 pages total)." + :demo (~infinite-scroll-demo) + :sx-code "(div :id \"scroll-sentinel\"\n :sx-get \"/examples/api/scroll?page=2\"\n :sx-trigger \"intersect once\"\n :sx-target \"#scroll-items\"\n :sx-swap \"beforeend\"\n \"Loading more...\")" + :handler-code "@bp.get(\"/examples/api/scroll\")\nasync def api_scroll():\n page = int(request.args.get(\"page\", 2))\n items = [f\"Item {i}\" for i in range(...)]\n # Include next sentinel if more pages\n return sx_response(items_sx + sentinel_sx)" + :comp-placeholder-id "scroll-comp" + :wire-placeholder-id "scroll-wire")) + +(defcomp ~example-progress-bar () + (~example-page-content + :title "Progress Bar" + :description "Start a server-side job, then poll for progress using sx-trigger=\"load delay:500ms\" on each response. The bar fills up and stops when complete." + :demo-description "Click start to begin a simulated job." + :demo (~progress-bar-demo) + :sx-code ";; Start the job\n(button\n :sx-post \"/examples/api/progress/start\"\n :sx-target \"#progress-target\"\n :sx-swap \"innerHTML\")\n\n;; Each response re-polls via sx-trigger=\"load\"\n(div :sx-get \"/api/progress/status?job=ID\"\n :sx-trigger \"load delay:500ms\"\n :sx-target \"#progress-target\"\n :sx-swap \"innerHTML\")" + :handler-code "@bp.post(\"/examples/api/progress/start\")\nasync def api_progress_start():\n job_id = str(uuid4())[:8]\n _jobs[job_id] = 0\n return sx_response(\n f'(~progress-status :percent 0 :job-id \"{job_id}\")')" + :comp-placeholder-id "progress-comp" + :wire-placeholder-id "progress-wire")) + +(defcomp ~example-active-search () + (~example-page-content + :title "Active Search" + :description "An input with sx-trigger=\"keyup delay:300ms changed\" debounces keystrokes and only fires when the value changes. The server filters a list of programming languages." + :demo-description "Type to search through 20 programming languages." + :demo (~active-search-demo) + :sx-code "(input :type \"text\" :name \"q\"\n :sx-get \"/examples/api/search\"\n :sx-trigger \"keyup delay:300ms changed\"\n :sx-target \"#search-results\"\n :sx-swap \"innerHTML\"\n :placeholder \"Search...\")" + :handler-code "@bp.get(\"/examples/api/search\")\nasync def api_search():\n q = request.args.get(\"q\", \"\").lower()\n results = [l for l in LANGUAGES if q in l.lower()]\n return sx_response(\n f'(~search-results :items (...) :query \"{q}\")')" + :comp-placeholder-id "search-comp" + :wire-placeholder-id "search-wire")) + +(defcomp ~example-inline-validation () + (~example-page-content + :title "Inline Validation" + :description "Validate an email field on blur. The server checks format and whether it is taken, returning green or red feedback inline." + :demo-description "Enter an email and click away (blur) to validate." + :demo (~inline-validation-demo) + :sx-code "(input :type \"text\" :name \"email\"\n :sx-get \"/examples/api/validate\"\n :sx-trigger \"blur\"\n :sx-target \"#email-feedback\"\n :sx-swap \"innerHTML\"\n :placeholder \"user@example.com\")" + :handler-code "@bp.get(\"/examples/api/validate\")\nasync def api_validate():\n email = request.args.get(\"email\", \"\")\n if \"@\" not in email:\n return sx_response('(~validation-error ...)')\n return sx_response('(~validation-ok ...)')" + :comp-placeholder-id "validate-comp" + :wire-placeholder-id "validate-wire")) + +(defcomp ~example-value-select () + (~example-page-content + :title "Value Select" + :description "Two linked selects: pick a category and the second select updates with matching items via sx-get." + :demo-description "Select a category to populate the item dropdown." + :demo (~value-select-demo) + :sx-code "(select :name \"category\"\n :sx-get \"/examples/api/values\"\n :sx-trigger \"change\"\n :sx-target \"#value-items\"\n :sx-swap \"innerHTML\"\n (option \"Languages\")\n (option \"Frameworks\")\n (option \"Databases\"))" + :handler-code "@bp.get(\"/examples/api/values\")\nasync def api_values():\n cat = request.args.get(\"category\", \"\")\n items = VALUE_SELECT_DATA.get(cat, [])\n return sx_response(\n f'(~value-options :items (list ...))')" + :comp-placeholder-id "values-comp" + :wire-placeholder-id "values-wire")) + +(defcomp ~example-reset-on-submit () + (~example-page-content + :title "Reset on Submit" + :description "Use sx-on:afterSwap=\"this.reset()\" to clear form inputs after a successful submission." + :demo-description "Submit a message — the input resets after each send." + :demo (~reset-on-submit-demo) + :sx-code "(form :id \"reset-form\"\n :sx-post \"/examples/api/reset-submit\"\n :sx-target \"#reset-result\"\n :sx-swap \"innerHTML\"\n :sx-on:afterSwap \"this.reset()\"\n (input :type \"text\" :name \"message\")\n (button :type \"submit\" \"Send\"))" + :handler-code "@bp.post(\"/examples/api/reset-submit\")\nasync def api_reset_submit():\n form = await request.form\n msg = form.get(\"message\", \"\")\n return sx_response(\n f'(~reset-message :message \"{msg}\" :time \"...\")')" + :comp-placeholder-id "reset-comp" + :wire-placeholder-id "reset-wire")) + +(defcomp ~example-edit-row () + (~example-page-content + :title "Edit Row" + :description "Click edit to replace a table row with input fields. Save or cancel swaps back the display row. Uses sx-include to gather form values from the row." + :demo-description "Click edit on any row to modify it inline." + :demo (~edit-row-demo :rows (list + (list "1" "Widget A" "19.99" "142") + (list "2" "Widget B" "24.50" "89") + (list "3" "Widget C" "12.00" "305") + (list "4" "Widget D" "45.00" "67"))) + :sx-code "(button\n :sx-get \"/examples/api/editrow/1\"\n :sx-target \"#erow-1\"\n :sx-swap \"outerHTML\"\n \"edit\")\n\n;; Save sends form data via POST\n(button\n :sx-post \"/examples/api/editrow/1\"\n :sx-target \"#erow-1\"\n :sx-swap \"outerHTML\"\n :sx-include \"#erow-1\"\n \"save\")" + :handler-code "@bp.get(\"/examples/api/editrow/\")\nasync def api_editrow_form(id):\n row = EDIT_ROW_DATA[id]\n return sx_response(\n f'(~edit-row-form :id ... :name ...)')\n\n@bp.post(\"/examples/api/editrow/\")\nasync def api_editrow_save(id):\n form = await request.form\n return sx_response(\n f'(~edit-row-view :id ... :name ...)')" + :comp-placeholder-id "editrow-comp" + :wire-placeholder-id "editrow-wire")) + +(defcomp ~example-bulk-update () + (~example-page-content + :title "Bulk Update" + :description "Select rows with checkboxes and use Activate/Deactivate buttons. sx-include gathers checkbox values from the form." + :demo-description "Check some rows, then click Activate or Deactivate." + :demo (~bulk-update-demo :users (list + (list "1" "Alice Chen" "alice@example.com" "active") + (list "2" "Bob Rivera" "bob@example.com" "inactive") + (list "3" "Carol Zhang" "carol@example.com" "active") + (list "4" "Dan Okafor" "dan@example.com" "inactive") + (list "5" "Eve Larsson" "eve@example.com" "active"))) + :sx-code "(button\n :sx-post \"/examples/api/bulk?action=activate\"\n :sx-target \"#bulk-table\"\n :sx-swap \"innerHTML\"\n :sx-include \"#bulk-form\"\n \"Activate\")" + :handler-code "@bp.post(\"/examples/api/bulk\")\nasync def api_bulk():\n action = request.args.get(\"action\")\n form = await request.form\n ids = form.getlist(\"ids\")\n # Update matching users\n return sx_response(updated_rows)" + :comp-placeholder-id "bulk-comp" + :wire-placeholder-id "bulk-wire")) + +(defcomp ~example-swap-positions () + (~example-page-content + :title "Swap Positions" + :description "Demonstrates different swap modes: beforeend appends, afterbegin prepends, and none skips the main swap while still processing OOB updates." + :demo-description "Try each button to see different swap behaviours." + :demo (~swap-positions-demo) + :sx-code ";; Append to end\n(button :sx-post \"/api/swap-log?mode=beforeend\"\n :sx-target \"#swap-log\" :sx-swap \"beforeend\"\n \"Add to End\")\n\n;; Prepend to start\n(button :sx-post \"/api/swap-log?mode=afterbegin\"\n :sx-target \"#swap-log\" :sx-swap \"afterbegin\"\n \"Add to Start\")\n\n;; No swap — OOB counter update only\n(button :sx-post \"/api/swap-log?mode=none\"\n :sx-target \"#swap-log\" :sx-swap \"none\"\n \"Silent Ping\")" + :handler-code "@bp.post(\"/examples/api/swap-log\")\nasync def api_swap_log():\n mode = request.args.get(\"mode\")\n # OOB counter updates on every request\n oob = f'(span :id \"swap-counter\" :sx-swap-oob \"innerHTML\" \"Count: {n}\")'\n return sx_response(entry + oob)" + :wire-placeholder-id "swap-wire")) + +(defcomp ~example-select-filter () + (~example-page-content + :title "Select Filter" + :description "sx-select lets the client pick a specific section from the server response by CSS selector. The server always returns the full dashboard — the client filters." + :demo-description "Different buttons select different parts of the same server response." + :demo (~select-filter-demo) + :sx-code ";; Pick just the stats section from the response\n(button\n :sx-get \"/examples/api/dashboard\"\n :sx-target \"#filter-target\"\n :sx-swap \"innerHTML\"\n :sx-select \"#dash-stats\"\n \"Stats Only\")\n\n;; No sx-select — get the full response\n(button\n :sx-get \"/examples/api/dashboard\"\n :sx-target \"#filter-target\"\n :sx-swap \"innerHTML\"\n \"Full Dashboard\")" + :handler-code "@bp.get(\"/examples/api/dashboard\")\nasync def api_dashboard():\n # Returns header + stats + footer\n # Client uses sx-select to pick sections\n return sx_response(\n '(<> (div :id \"dash-header\" ...) '\n ' (div :id \"dash-stats\" ...) '\n ' (div :id \"dash-footer\" ...))')" + :wire-placeholder-id "filter-wire")) + +(defcomp ~example-tabs () + (~example-page-content + :title "Tabs" + :description "Tab navigation using sx-push-url to update the browser URL. Back/forward buttons navigate between previously visited tabs." + :demo-description "Click tabs to switch content. Watch the browser URL change." + :demo (~tabs-demo) + :sx-code "(button\n :sx-get \"/examples/api/tabs/tab1\"\n :sx-target \"#tab-content\"\n :sx-swap \"innerHTML\"\n :sx-push-url \"/examples/tabs?tab=tab1\"\n \"Overview\")" + :handler-code "@bp.get(\"/examples/api/tabs/\")\nasync def api_tabs(tab: str):\n content = TAB_CONTENT[tab]\n return sx_response(content)" + :wire-placeholder-id "tabs-wire")) + +(defcomp ~example-animations () + (~example-page-content + :title "Animations" + :description "CSS animations play on swap. The component injects a style tag with a keyframe animation and applies the class. Each click picks a random background colour." + :demo-description "Click to swap in content with a fade-in animation." + :demo (~animations-demo) + :sx-code "(button\n :sx-get \"/examples/api/animate\"\n :sx-target \"#anim-target\"\n :sx-swap \"innerHTML\"\n \"Load with animation\")\n\n;; Component uses CSS animation class\n(defcomp ~anim-result (&key color time)\n (div :class \"sx-fade-in ...\"\n (style \".sx-fade-in { animation: sxFadeIn 0.5s }\")\n (p \"Faded in!\")))" + :handler-code "@bp.get(\"/examples/api/animate\")\nasync def api_animate():\n colors = [\"bg-violet-100\", \"bg-emerald-100\", ...]\n color = random.choice(colors)\n return sx_response(\n f'(~anim-result :color \"{color}\" :time \"{now}\")')" + :comp-placeholder-id "anim-comp" + :wire-placeholder-id "anim-wire")) + +(defcomp ~example-dialogs () + (~example-page-content + :title "Dialogs" + :description "Open a modal dialog by swapping in the dialog component. Close by swapping in empty content. Pure sx — no JavaScript library needed." + :demo-description "Click to open a modal dialog." + :demo (~dialogs-demo) + :sx-code "(button\n :sx-get \"/examples/api/dialog\"\n :sx-target \"#dialog-container\"\n :sx-swap \"innerHTML\"\n \"Open Dialog\")\n\n;; Dialog closes by swapping empty content\n(button\n :sx-get \"/examples/api/dialog/close\"\n :sx-target \"#dialog-container\"\n :sx-swap \"innerHTML\"\n \"Close\")" + :handler-code "@bp.get(\"/examples/api/dialog\")\nasync def api_dialog():\n return sx_response(\n '(~dialog-modal :title \"Confirm\"'\n ' :message \"Are you sure?\")')\n\n@bp.get(\"/examples/api/dialog/close\")\nasync def api_dialog_close():\n return sx_response(\"\")" + :comp-placeholder-id "dialog-comp" + :wire-placeholder-id "dialog-wire")) + +(defcomp ~example-keyboard-shortcuts () + (~example-page-content + :title "Keyboard Shortcuts" + :description "Use sx-trigger with keyup event filters and from:body to listen for global keyboard shortcuts. The filter prevents firing when typing in inputs." + :demo-description "Press s, n, or h on your keyboard." + :demo (~keyboard-shortcuts-demo) + :sx-code "(div :id \"kbd-target\"\n :sx-get \"/examples/api/keyboard?key=s\"\n :sx-trigger \"keyup[key=='s'&&!event.target.matches('input,textarea')] from:body\"\n :sx-swap \"innerHTML\"\n \"Press a shortcut key...\")" + :handler-code "@bp.get(\"/examples/api/keyboard\")\nasync def api_keyboard():\n key = request.args.get(\"key\", \"\")\n actions = {\"s\": \"Search\", \"n\": \"New item\", \"h\": \"Help\"}\n return sx_response(\n f'(~kbd-result :key \"{key}\" :action \"{actions[key]}\")')" + :comp-placeholder-id "kbd-comp" + :wire-placeholder-id "kbd-wire")) + +(defcomp ~example-put-patch () + (~example-page-content + :title "PUT / PATCH" + :description "sx-put replaces the entire resource. This example shows a profile card with an Edit All button that sends a PUT with all fields." + :demo-description "Click Edit All to replace the full profile via PUT." + :demo (~put-patch-demo :name "Ada Lovelace" :email "ada@example.com" :role "Engineer") + :sx-code ";; Replace entire resource\n(form :sx-put \"/examples/api/putpatch\"\n :sx-target \"#pp-target\" :sx-swap \"innerHTML\"\n (input :name \"name\") (input :name \"email\")\n (button \"Save All (PUT)\"))" + :handler-code "@bp.put(\"/examples/api/putpatch\")\nasync def api_put():\n form = await request.form\n # Full replacement\n return sx_response('(~pp-view ...)')" + :comp-placeholder-id "pp-comp" + :wire-placeholder-id "pp-wire")) + +(defcomp ~example-json-encoding () + (~example-page-content + :title "JSON Encoding" + :description "Use sx-encoding=\"json\" to send form data as a JSON body instead of URL-encoded form data. The server echoes back what it received." + :demo-description "Submit the form and see the JSON body the server received." + :demo (~json-encoding-demo) + :sx-code "(form\n :sx-post \"/examples/api/json-echo\"\n :sx-target \"#json-result\"\n :sx-swap \"innerHTML\"\n :sx-encoding \"json\"\n (input :name \"name\" :value \"Ada\")\n (input :type \"number\" :name \"age\" :value \"36\")\n (button \"Submit as JSON\"))" + :handler-code "@bp.post(\"/examples/api/json-echo\")\nasync def api_json_echo():\n data = await request.get_json()\n body = json.dumps(data, indent=2)\n ct = request.content_type\n return sx_response(\n f'(~json-result :body \"{body}\" :content-type \"{ct}\")')" + :comp-placeholder-id "json-comp" + :wire-placeholder-id "json-wire")) + +(defcomp ~example-vals-and-headers () + (~example-page-content + :title "Vals & Headers" + :description "sx-vals adds extra key/value pairs to the request parameters. sx-headers adds custom HTTP headers. The server echoes back what it received." + :demo-description "Click each button to see what the server receives." + :demo (~vals-headers-demo) + :sx-code ";; Send extra values with the request\n(button\n :sx-get \"/examples/api/echo-vals\"\n :sx-vals \"{\\\"source\\\": \\\"button\\\"}\"\n \"Send with vals\")\n\n;; Send custom headers\n(button\n :sx-get \"/examples/api/echo-headers\"\n :sx-headers \"{\\\"X-Custom-Token\\\": \\\"abc123\\\"}\"\n \"Send with headers\")" + :handler-code "@bp.get(\"/examples/api/echo-vals\")\nasync def api_echo_vals():\n vals = dict(request.args)\n return sx_response(\n f'(~echo-result :label \"values\" :items (...))')\n\n@bp.get(\"/examples/api/echo-headers\")\nasync def api_echo_headers():\n custom = {k: v for k, v in request.headers\n if k.startswith(\"X-\")}\n return sx_response(\n f'(~echo-result :label \"headers\" :items (...))')" + :comp-placeholder-id "vals-comp" + :wire-placeholder-id "vals-wire")) + +(defcomp ~example-loading-states () + (~example-page-content + :title "Loading States" + :description "sx.js adds the .sx-request CSS class to any element that has an active request. Use pure CSS to show spinners, disable buttons, or change opacity during loading." + :demo-description "Click the button — it shows a spinner during the 2-second request." + :demo (~loading-states-demo) + :sx-code ";; .sx-request class added during request\n(style \".sx-loading-btn.sx-request {\n opacity: 0.7; pointer-events: none; }\n.sx-loading-btn.sx-request .sx-spinner {\n display: inline-block; }\n.sx-loading-btn .sx-spinner {\n display: none; }\")\n\n(button :class \"sx-loading-btn\"\n :sx-get \"/examples/api/slow\"\n :sx-target \"#loading-result\"\n (span :class \"sx-spinner animate-spin\" \"...\")\n \"Load slow endpoint\")" + :handler-code "@bp.get(\"/examples/api/slow\")\nasync def api_slow():\n await asyncio.sleep(2)\n return sx_response(\n f'(~loading-result :time \"{now}\")')" + :comp-placeholder-id "loading-comp" + :wire-placeholder-id "loading-wire")) + +(defcomp ~example-sync-replace () + (~example-page-content + :title "Request Abort" + :description "sx-sync=\"replace\" aborts any in-flight request before sending a new one. This prevents stale responses from overwriting newer ones, even with random server delays." + :demo-description "Type quickly — only the latest result appears despite random 0.5-2s server delays." + :demo (~sync-replace-demo) + :sx-code "(input :type \"text\" :name \"q\"\n :sx-get \"/examples/api/slow-search\"\n :sx-trigger \"keyup delay:200ms changed\"\n :sx-target \"#sync-result\"\n :sx-swap \"innerHTML\"\n :sx-sync \"replace\"\n \"Type to search...\")" + :handler-code "@bp.get(\"/examples/api/slow-search\")\nasync def api_slow_search():\n delay = random.uniform(0.5, 2.0)\n await asyncio.sleep(delay)\n q = request.args.get(\"q\", \"\")\n return sx_response(\n f'(~sync-result :query \"{q}\" :delay \"{delay_ms}\")')" + :comp-placeholder-id "sync-comp" + :wire-placeholder-id "sync-wire")) + +(defcomp ~example-retry () + (~example-page-content + :title "Retry" + :description "sx-retry=\"exponential:1000:8000\" retries failed requests with exponential backoff starting at 1s up to 8s. The endpoint fails the first 2 attempts and succeeds on the 3rd." + :demo-description "Click the button — watch it retry automatically after failures." + :demo (~retry-demo) + :sx-code "(button\n :sx-get \"/examples/api/flaky\"\n :sx-target \"#retry-result\"\n :sx-swap \"innerHTML\"\n :sx-retry \"exponential:1000:8000\"\n \"Call flaky endpoint\")" + :handler-code "@bp.get(\"/examples/api/flaky\")\nasync def api_flaky():\n _flaky[\"n\"] += 1\n if _flaky[\"n\"] % 3 != 0:\n return Response(\"\", status=503)\n return sx_response(\n f'(~retry-result :attempt {n} ...)')" + :comp-placeholder-id "retry-comp" + :wire-placeholder-id "retry-wire")) diff --git a/sx/sx/nav-data.sx b/sx/sx/nav-data.sx new file mode 100644 index 0000000..36cdce5 --- /dev/null +++ b/sx/sx/nav-data.sx @@ -0,0 +1,87 @@ +;; Navigation data and section-nav component for sx docs. +;; Replaces Python nav tuples from content/pages.py and _nav_items_sx() from utils.py. +;; @css aria-selected:bg-violet-200 aria-selected:text-violet-900 + +(define docs-nav-items (list + (dict :label "Introduction" :href "/docs/introduction") + (dict :label "Getting Started" :href "/docs/getting-started") + (dict :label "Components" :href "/docs/components") + (dict :label "Evaluator" :href "/docs/evaluator") + (dict :label "Primitives" :href "/docs/primitives") + (dict :label "CSS" :href "/docs/css") + (dict :label "Server Rendering" :href "/docs/server-rendering"))) + +(define reference-nav-items (list + (dict :label "Attributes" :href "/reference/attributes") + (dict :label "Headers" :href "/reference/headers") + (dict :label "Events" :href "/reference/events") + (dict :label "JS API" :href "/reference/js-api"))) + +(define protocols-nav-items (list + (dict :label "Wire Format" :href "/protocols/wire-format") + (dict :label "Fragments" :href "/protocols/fragments") + (dict :label "Resolver I/O" :href "/protocols/resolver-io") + (dict :label "Internal Services" :href "/protocols/internal-services") + (dict :label "ActivityPub" :href "/protocols/activitypub") + (dict :label "Future" :href "/protocols/future"))) + +(define examples-nav-items (list + (dict :label "Click to Load" :href "/examples/click-to-load") + (dict :label "Form Submission" :href "/examples/form-submission") + (dict :label "Polling" :href "/examples/polling") + (dict :label "Delete Row" :href "/examples/delete-row") + (dict :label "Inline Edit" :href "/examples/inline-edit") + (dict :label "OOB Swaps" :href "/examples/oob-swaps") + (dict :label "Lazy Loading" :href "/examples/lazy-loading") + (dict :label "Infinite Scroll" :href "/examples/infinite-scroll") + (dict :label "Progress Bar" :href "/examples/progress-bar") + (dict :label "Active Search" :href "/examples/active-search") + (dict :label "Inline Validation" :href "/examples/inline-validation") + (dict :label "Value Select" :href "/examples/value-select") + (dict :label "Reset on Submit" :href "/examples/reset-on-submit") + (dict :label "Edit Row" :href "/examples/edit-row") + (dict :label "Bulk Update" :href "/examples/bulk-update") + (dict :label "Swap Positions" :href "/examples/swap-positions") + (dict :label "Select Filter" :href "/examples/select-filter") + (dict :label "Tabs" :href "/examples/tabs") + (dict :label "Animations" :href "/examples/animations") + (dict :label "Dialogs" :href "/examples/dialogs") + (dict :label "Keyboard Shortcuts" :href "/examples/keyboard-shortcuts") + (dict :label "PUT / PATCH" :href "/examples/put-patch") + (dict :label "JSON Encoding" :href "/examples/json-encoding") + (dict :label "Vals & Headers" :href "/examples/vals-and-headers") + (dict :label "Loading States" :href "/examples/loading-states") + (dict :label "Request Abort" :href "/examples/sync-replace") + (dict :label "Retry" :href "/examples/retry"))) + +(define essays-nav-items (list + (dict :label "sx sucks" :href "/essays/sx-sucks") + (dict :label "Why S-Expressions" :href "/essays/why-sexps") + (dict :label "The htmx/React Hybrid" :href "/essays/htmx-react-hybrid") + (dict :label "On-Demand CSS" :href "/essays/on-demand-css") + (dict :label "Client Reactivity" :href "/essays/client-reactivity") + (dict :label "SX Native" :href "/essays/sx-native") + (dict :label "The SX Manifesto" :href "/essays/sx-manifesto") + (dict :label "Tail-Call Optimization" :href "/essays/tail-call-optimization") + (dict :label "Continuations" :href "/essays/continuations"))) + +;; Find the current nav label for a slug by matching href suffix. +;; Returns the label string or nil if no match. +(define find-current + (fn (items slug) + (when slug + (some (fn (item) + (when (ends-with? (get item "href") slug) + (get item "label"))) + items)))) + +;; Generic section nav — builds nav links from a list of items. +;; Replaces _nav_items_sx() and all section-specific nav builders in utils.py. +(defcomp ~section-nav (&key items current) + (<> (map (fn (item) + (~nav-link + :href (get item "href") + :label (get item "label") + :is-selected (when (= (get item "label") current) "true") + :select-colours "aria-selected:bg-violet-200 aria-selected:text-violet-900")) + items))) diff --git a/sx/sxc/pages/docs.sx b/sx/sxc/pages/docs.sx index 165ca4d..5d910ea 100644 --- a/sx/sxc/pages/docs.sx +++ b/sx/sxc/pages/docs.sx @@ -1,5 +1,6 @@ ;; SX docs app — declarative page definitions -;; These replace the GET route handlers in routes.py +;; All content dispatched via case + direct component references. +;; Navigation built from SX data (nav-data.sx), no Python intermediaries. ;; --------------------------------------------------------------------------- ;; Home page @@ -9,7 +10,7 @@ :path "/" :auth :public :layout :sx - :content (home-content)) + :content (~sx-home-content)) ;; --------------------------------------------------------------------------- ;; Docs section @@ -22,9 +23,9 @@ :section "Docs" :sub-label "Docs" :sub-href "/docs/introduction" - :sub-nav (docs-nav "Introduction") + :sub-nav (~section-nav :items docs-nav-items :current "Introduction") :selected "Introduction") - :content (docs-content "introduction")) + :content (~docs-introduction-content)) (defpage docs-page :path "/docs/" @@ -33,9 +34,19 @@ :section "Docs" :sub-label "Docs" :sub-href "/docs/introduction" - :sub-nav (docs-nav (find-current DOCS_NAV slug)) - :selected (or (find-current DOCS_NAV slug) "")) - :content (docs-content slug)) + :sub-nav (~section-nav :items docs-nav-items + :current (find-current docs-nav-items slug)) + :selected (or (find-current docs-nav-items slug) "")) + :content (case slug + "introduction" (~docs-introduction-content) + "getting-started" (~docs-getting-started-content) + "components" (~docs-components-content) + "evaluator" (~docs-evaluator-content) + "primitives" (~docs-primitives-content + :prims (~doc-primitives-tables :primitives (primitives-data))) + "css" (~docs-css-content) + "server-rendering" (~docs-server-rendering-content) + :else (~docs-introduction-content))) ;; --------------------------------------------------------------------------- ;; Reference section @@ -48,9 +59,9 @@ :section "Reference" :sub-label "Reference" :sub-href "/reference/" - :sub-nav (reference-nav "") + :sub-nav (~section-nav :items reference-nav-items :current "") :selected "") - :content (reference-index-content)) + :content (~reference-index-content)) (defpage reference-page :path "/reference/" @@ -59,9 +70,30 @@ :section "Reference" :sub-label "Reference" :sub-href "/reference/" - :sub-nav (reference-nav (find-current REFERENCE_NAV slug)) - :selected (or (find-current REFERENCE_NAV slug) "")) - :content (reference-content slug)) + :sub-nav (~section-nav :items reference-nav-items + :current (find-current reference-nav-items slug)) + :selected (or (find-current reference-nav-items slug) "")) + :data (reference-data slug) + :content (case slug + "attributes" (~reference-attrs-content + :req-table (~doc-attr-table-from-data :title "Request Attributes" :attrs req-attrs) + :beh-table (~doc-attr-table-from-data :title "Behavior Attributes" :attrs beh-attrs) + :uniq-table (~doc-attr-table-from-data :title "Unique to sx" :attrs uniq-attrs)) + "headers" (~reference-headers-content + :req-table (~doc-headers-table-from-data :title "Request Headers" :headers req-headers) + :resp-table (~doc-headers-table-from-data :title "Response Headers" :headers resp-headers)) + "events" (~reference-events-content + :table (~doc-two-col-table-from-data + :intro "sx fires custom DOM events at various points in the request lifecycle." + :col1 "Event" :col2 "Description" :items events-list)) + "js-api" (~reference-js-api-content + :table (~doc-two-col-table-from-data + :intro "The client-side sx.js library exposes a public API for programmatic use." + :col1 "Method" :col2 "Description" :items js-api-list)) + :else (~reference-attrs-content + :req-table (~doc-attr-table-from-data :title "Request Attributes" :attrs req-attrs) + :beh-table (~doc-attr-table-from-data :title "Behavior Attributes" :attrs beh-attrs) + :uniq-table (~doc-attr-table-from-data :title "Unique to sx" :attrs uniq-attrs)))) (defpage reference-attr-detail :path "/reference/attributes/" @@ -70,9 +102,18 @@ :section "Reference" :sub-label "Reference" :sub-href "/reference/" - :sub-nav (reference-nav "Attributes") + :sub-nav (~section-nav :items reference-nav-items :current "Attributes") :selected "Attributes") - :content (reference-attr-detail slug)) + :data (attr-detail-data slug) + :content (if attr-not-found + (~reference-attr-not-found :slug slug) + (~reference-attr-detail-content + :title attr-title + :description attr-description + :demo attr-demo + :example-code attr-example + :handler-code attr-handler + :wire-placeholder-id attr-wire-id))) ;; --------------------------------------------------------------------------- ;; Protocols section @@ -85,9 +126,9 @@ :section "Protocols" :sub-label "Protocols" :sub-href "/protocols/wire-format" - :sub-nav (protocols-nav "Wire Format") + :sub-nav (~section-nav :items protocols-nav-items :current "Wire Format") :selected "Wire Format") - :content (protocol-content "wire-format")) + :content (~protocol-wire-format-content)) (defpage protocol-page :path "/protocols/" @@ -96,9 +137,17 @@ :section "Protocols" :sub-label "Protocols" :sub-href "/protocols/wire-format" - :sub-nav (protocols-nav (find-current PROTOCOLS_NAV slug)) - :selected (or (find-current PROTOCOLS_NAV slug) "")) - :content (protocol-content slug)) + :sub-nav (~section-nav :items protocols-nav-items + :current (find-current protocols-nav-items slug)) + :selected (or (find-current protocols-nav-items slug) "")) + :content (case slug + "wire-format" (~protocol-wire-format-content) + "fragments" (~protocol-fragments-content) + "resolver-io" (~protocol-resolver-io-content) + "internal-services" (~protocol-internal-services-content) + "activitypub" (~protocol-activitypub-content) + "future" (~protocol-future-content) + :else (~protocol-wire-format-content))) ;; --------------------------------------------------------------------------- ;; Examples section @@ -111,9 +160,9 @@ :section "Examples" :sub-label "Examples" :sub-href "/examples/click-to-load" - :sub-nav (examples-nav "Click to Load") + :sub-nav (~section-nav :items examples-nav-items :current "Click to Load") :selected "Click to Load") - :content (examples-content "click-to-load")) + :content (~example-click-to-load)) (defpage examples-page :path "/examples/" @@ -122,9 +171,38 @@ :section "Examples" :sub-label "Examples" :sub-href "/examples/click-to-load" - :sub-nav (examples-nav (find-current EXAMPLES_NAV slug)) - :selected (or (find-current EXAMPLES_NAV slug) "")) - :content (examples-content slug)) + :sub-nav (~section-nav :items examples-nav-items + :current (find-current examples-nav-items slug)) + :selected (or (find-current examples-nav-items slug) "")) + :content (case slug + "click-to-load" (~example-click-to-load) + "form-submission" (~example-form-submission) + "polling" (~example-polling) + "delete-row" (~example-delete-row) + "inline-edit" (~example-inline-edit) + "oob-swaps" (~example-oob-swaps) + "lazy-loading" (~example-lazy-loading) + "infinite-scroll" (~example-infinite-scroll) + "progress-bar" (~example-progress-bar) + "active-search" (~example-active-search) + "inline-validation" (~example-inline-validation) + "value-select" (~example-value-select) + "reset-on-submit" (~example-reset-on-submit) + "edit-row" (~example-edit-row) + "bulk-update" (~example-bulk-update) + "swap-positions" (~example-swap-positions) + "select-filter" (~example-select-filter) + "tabs" (~example-tabs) + "animations" (~example-animations) + "dialogs" (~example-dialogs) + "keyboard-shortcuts" (~example-keyboard-shortcuts) + "put-patch" (~example-put-patch) + "json-encoding" (~example-json-encoding) + "vals-and-headers" (~example-vals-and-headers) + "loading-states" (~example-loading-states) + "sync-replace" (~example-sync-replace) + "retry" (~example-retry) + :else (~example-click-to-load))) ;; --------------------------------------------------------------------------- ;; Essays section @@ -137,9 +215,9 @@ :section "Essays" :sub-label "Essays" :sub-href "/essays/sx-sucks" - :sub-nav (essays-nav "sx sucks") + :sub-nav (~section-nav :items essays-nav-items :current "sx sucks") :selected "sx sucks") - :content (essay-content "sx-sucks")) + :content (~essay-sx-sucks)) (defpage essay-page :path "/essays/" @@ -148,6 +226,17 @@ :section "Essays" :sub-label "Essays" :sub-href "/essays/sx-sucks" - :sub-nav (essays-nav (find-current ESSAYS_NAV slug)) - :selected (or (find-current ESSAYS_NAV slug) "")) - :content (essay-content slug)) + :sub-nav (~section-nav :items essays-nav-items + :current (find-current essays-nav-items slug)) + :selected (or (find-current essays-nav-items slug) "")) + :content (case slug + "sx-sucks" (~essay-sx-sucks) + "why-sexps" (~essay-why-sexps) + "htmx-react-hybrid" (~essay-htmx-react-hybrid) + "on-demand-css" (~essay-on-demand-css) + "client-reactivity" (~essay-client-reactivity) + "sx-native" (~essay-sx-native) + "sx-manifesto" (~essay-sx-manifesto) + "tail-call-optimization" (~essay-tail-call-optimization) + "continuations" (~essay-continuations) + :else (~essay-sx-sucks))) diff --git a/sx/sxc/pages/essays.py b/sx/sxc/pages/essays.py deleted file mode 100644 index 7d8e1c5..0000000 --- a/sx/sxc/pages/essays.py +++ /dev/null @@ -1,619 +0,0 @@ -"""All content generator functions and dispatchers for sx docs pages. - -Content markup lives in .sx files (sx/sx/*.sx). Python functions here are thin -dispatchers that either return a component name or pass data values via sx_call. -""" -from __future__ import annotations - -from shared.sx.helpers import sx_call, SxExpr -from .utils import _attr_table_sx, _primitives_section_sx, _headers_table_sx - - -# --------------------------------------------------------------------------- -# Dispatcher functions — route slugs to content builders -# --------------------------------------------------------------------------- - -async def _docs_content_sx(slug: str) -> str: - """Route to the right docs content builder.""" - import inspect - builders = { - "introduction": _docs_introduction_sx, - "getting-started": _docs_getting_started_sx, - "components": _docs_components_sx, - "evaluator": _docs_evaluator_sx, - "primitives": _docs_primitives_sx, - "css": _docs_css_sx, - "server-rendering": _docs_server_rendering_sx, - } - builder = builders.get(slug, _docs_introduction_sx) - result = builder() - return await result if inspect.isawaitable(result) else result - - -async def _reference_content_sx(slug: str) -> str: - import inspect - builders = { - "attributes": _reference_attrs_sx, - "headers": _reference_headers_sx, - "events": _reference_events_sx, - "js-api": _reference_js_api_sx, - } - result = builders.get(slug or "", _reference_attrs_sx)() - return await result if inspect.isawaitable(result) else result - - -def _protocol_content_sx(slug: str) -> str: - builders = { - "wire-format": _protocol_wire_format_sx, - "fragments": _protocol_fragments_sx, - "resolver-io": _protocol_resolver_io_sx, - "internal-services": _protocol_internal_services_sx, - "activitypub": _protocol_activitypub_sx, - "future": _protocol_future_sx, - } - return builders.get(slug, _protocol_wire_format_sx)() - - -def _examples_content_sx(slug: str) -> str: - builders = { - "click-to-load": _example_click_to_load_sx, - "form-submission": _example_form_submission_sx, - "polling": _example_polling_sx, - "delete-row": _example_delete_row_sx, - "inline-edit": _example_inline_edit_sx, - "oob-swaps": _example_oob_swaps_sx, - "lazy-loading": _example_lazy_loading_sx, - "infinite-scroll": _example_infinite_scroll_sx, - "progress-bar": _example_progress_bar_sx, - "active-search": _example_active_search_sx, - "inline-validation": _example_inline_validation_sx, - "value-select": _example_value_select_sx, - "reset-on-submit": _example_reset_on_submit_sx, - "edit-row": _example_edit_row_sx, - "bulk-update": _example_bulk_update_sx, - "swap-positions": _example_swap_positions_sx, - "select-filter": _example_select_filter_sx, - "tabs": _example_tabs_sx, - "animations": _example_animations_sx, - "dialogs": _example_dialogs_sx, - "keyboard-shortcuts": _example_keyboard_shortcuts_sx, - "put-patch": _example_put_patch_sx, - "json-encoding": _example_json_encoding_sx, - "vals-and-headers": _example_vals_and_headers_sx, - "loading-states": _example_loading_states_sx, - "sync-replace": _example_sync_replace_sx, - "retry": _example_retry_sx, - } - return builders.get(slug, _example_click_to_load_sx)() - - -def _essay_content_sx(slug: str) -> str: - builders = { - "sx-sucks": _essay_sx_sucks, - "why-sexps": _essay_why_sexps, - "htmx-react-hybrid": _essay_htmx_react_hybrid, - "on-demand-css": _essay_on_demand_css, - "client-reactivity": _essay_client_reactivity, - "sx-native": _essay_sx_native, - "sx-manifesto": _essay_sx_manifesto, - "tail-call-optimization": _essay_tail_call_optimization, - "continuations": _essay_continuations, - } - return builders.get(slug, _essay_sx_sucks)() - - -# --------------------------------------------------------------------------- -# Docs content — self-contained .sx components (sx/sx/docs-content.sx) -# --------------------------------------------------------------------------- - -def _docs_introduction_sx() -> str: - return "(~docs-introduction-content)" - -def _docs_getting_started_sx() -> str: - return "(~docs-getting-started-content)" - -def _docs_components_sx() -> str: - return "(~docs-components-content)" - -def _docs_evaluator_sx() -> str: - return "(~docs-evaluator-content)" - -def _docs_primitives_sx() -> str: - prims = _primitives_section_sx() - return sx_call("docs-primitives-content", prims=SxExpr(prims)) - -def _docs_css_sx() -> str: - return "(~docs-css-content)" - -def _docs_server_rendering_sx() -> str: - return "(~docs-server-rendering-content)" - - -# --------------------------------------------------------------------------- -# Protocol content — self-contained .sx components (sx/sx/protocols.sx) -# --------------------------------------------------------------------------- - -def _protocol_wire_format_sx() -> str: - return "(~protocol-wire-format-content)" - -def _protocol_fragments_sx() -> str: - return "(~protocol-fragments-content)" - -def _protocol_resolver_io_sx() -> str: - return "(~protocol-resolver-io-content)" - -def _protocol_internal_services_sx() -> str: - return "(~protocol-internal-services-content)" - -def _protocol_activitypub_sx() -> str: - return "(~protocol-activitypub-content)" - -def _protocol_future_sx() -> str: - return "(~protocol-future-content)" - - -# --------------------------------------------------------------------------- -# Essay content — self-contained .sx components (sx/sx/essays.sx) -# --------------------------------------------------------------------------- - -def _essay_sx_sucks() -> str: - return "(~essay-sx-sucks)" - -def _essay_why_sexps() -> str: - return "(~essay-why-sexps)" - -def _essay_htmx_react_hybrid() -> str: - return "(~essay-htmx-react-hybrid)" - -def _essay_on_demand_css() -> str: - return "(~essay-on-demand-css)" - -def _essay_client_reactivity() -> str: - return "(~essay-client-reactivity)" - -def _essay_sx_native() -> str: - return "(~essay-sx-native)" - -def _essay_sx_manifesto() -> str: - return "(~essay-sx-manifesto)" - -def _essay_tail_call_optimization() -> str: - return "(~essay-tail-call-optimization)" - -def _essay_continuations() -> str: - return "(~essay-continuations)" - - -# --------------------------------------------------------------------------- -# Reference pages — data-driven, page layouts in .sx (sx/sx/reference.sx) -# --------------------------------------------------------------------------- - -def _reference_index_sx() -> str: - return "(~reference-index-content)" - - -def _reference_attr_detail_sx(slug: str) -> str: - from content.pages import ATTR_DETAILS - detail = ATTR_DETAILS.get(slug) - if not detail: - return sx_call("reference-attr-not-found", slug=slug) - demo_name = detail.get("demo") - demo = SxExpr(f"(~{demo_name})") if demo_name else None - wire_placeholder_id = None - if "handler" in detail: - wire_id = slug.replace(":", "-").replace("*", "star") - wire_placeholder_id = f"ref-wire-{wire_id}" - return sx_call("reference-attr-detail-content", - title=slug, - description=detail["description"], - demo=demo, - example_code=detail["example"], - handler_code=detail.get("handler"), - wire_placeholder_id=wire_placeholder_id) - - -def _reference_attrs_sx() -> str: - from content.pages import REQUEST_ATTRS, BEHAVIOR_ATTRS, SX_UNIQUE_ATTRS - return sx_call("reference-attrs-content", - req_table=SxExpr(_attr_table_sx("Request Attributes", REQUEST_ATTRS)), - beh_table=SxExpr(_attr_table_sx("Behavior Attributes", BEHAVIOR_ATTRS)), - uniq_table=SxExpr(_attr_table_sx("Unique to sx", SX_UNIQUE_ATTRS))) - - -def _reference_headers_sx() -> str: - from content.pages import REQUEST_HEADERS, RESPONSE_HEADERS - return sx_call("reference-headers-content", - req_table=SxExpr(_headers_table_sx("Request Headers", REQUEST_HEADERS)), - resp_table=SxExpr(_headers_table_sx("Response Headers", RESPONSE_HEADERS))) - - -def _reference_events_sx() -> str: - from content.pages import EVENTS - rows = [] - for name, desc in EVENTS: - rows.append(sx_call("doc-two-col-row", name=name, description=desc)) - rows_sx = "(<> " + " ".join(rows) + ")" - table = sx_call("doc-two-col-table", - intro="sx fires custom DOM events at various points in the request lifecycle.", - col1="Event", col2="Description", rows=SxExpr(rows_sx)) - return sx_call("reference-events-content", table=SxExpr(table)) - - -def _reference_js_api_sx() -> str: - from content.pages import JS_API - rows = [] - for name, desc in JS_API: - rows.append(sx_call("doc-two-col-row", name=name, description=desc)) - rows_sx = "(<> " + " ".join(rows) + ")" - table = sx_call("doc-two-col-table", - intro="The client-side sx.js library exposes a public API for programmatic use.", - col1="Method", col2="Description", rows=SxExpr(rows_sx)) - return sx_call("reference-js-api-content", table=SxExpr(table)) - - -# --------------------------------------------------------------------------- -# Example pages — template in .sx (sx/sx/examples.sx), data values from Python -# --------------------------------------------------------------------------- - -def _build_example(title, description, demo_description, demo, sx_code, - handler_code, *, comp_placeholder_id=None, - wire_placeholder_id=None, wire_note=None, - comp_heading=None, handler_heading=None) -> str: - """Build an example page by passing data values to the .sx template.""" - kw = dict( - title=title, description=description, - demo_description=demo_description, - demo=SxExpr(demo), - sx_code=sx_code, handler_code=handler_code, - ) - if comp_placeholder_id: - kw["comp_placeholder_id"] = comp_placeholder_id - if wire_placeholder_id: - kw["wire_placeholder_id"] = wire_placeholder_id - if wire_note: - kw["wire_note"] = wire_note - if comp_heading: - kw["comp_heading"] = comp_heading - if handler_heading: - kw["handler_heading"] = handler_heading - return sx_call("example-page-content", **kw) - - -def _example_click_to_load_sx() -> str: - return _build_example( - title="Click to Load", - description="The simplest sx interaction: click a button, fetch content from the server, swap it in.", - demo_description="Click the button to load server-rendered content.", - demo="(~click-to-load-demo)", - sx_code='(button\n :sx-get "/examples/api/click"\n :sx-target "#click-result"\n :sx-swap "innerHTML"\n "Load content")', - handler_code='@bp.get("/examples/api/click")\nasync def api_click():\n now = datetime.now().strftime(...)\n return sx_response(\n f\'(~click-result :time "{now}")\')', - comp_placeholder_id="click-comp", - wire_placeholder_id="click-wire", - wire_note="The server responds with content-type text/sx. New CSS rules are prepended as a style tag. Clear the component cache to see component definitions included in the wire response.") - - -def _example_form_submission_sx() -> str: - return _build_example( - title="Form Submission", - description="Forms with sx-post submit via AJAX and swap the response into a target.", - demo_description="Enter a name and submit.", - demo="(~form-demo)", - sx_code='(form\n :sx-post "/examples/api/form"\n :sx-target "#form-result"\n :sx-swap "innerHTML"\n (input :type "text" :name "name")\n (button :type "submit" "Submit"))', - handler_code='@bp.post("/examples/api/form")\nasync def api_form():\n form = await request.form\n name = form.get("name", "")\n return sx_response(\n f\'(~form-result :name "{name}")\')', - comp_placeholder_id="form-comp", - wire_placeholder_id="form-wire") - - -def _example_polling_sx() -> str: - return _build_example( - title="Polling", - description='Use sx-trigger with "every" to poll the server at regular intervals.', - demo_description="This div polls the server every 2 seconds.", - demo="(~polling-demo)", - sx_code='(div\n :sx-get "/examples/api/poll"\n :sx-trigger "load, every 2s"\n :sx-swap "innerHTML"\n "Loading...")', - handler_code='@bp.get("/examples/api/poll")\nasync def api_poll():\n poll_count["n"] += 1\n now = datetime.now().strftime("%H:%M:%S")\n count = min(poll_count["n"], 10)\n return sx_response(\n f\'(~poll-result :time "{now}" :count {count})\')', - comp_placeholder_id="poll-comp", - wire_placeholder_id="poll-wire", - wire_note="Updates every 2 seconds — watch the time and count change.") - - -def _example_delete_row_sx() -> str: - from content.pages import DELETE_DEMO_ITEMS - items_sx = " ".join(f'(list "{id}" "{name}")' for id, name in DELETE_DEMO_ITEMS) - return _build_example( - title="Delete Row", - description='sx-delete with sx-swap "outerHTML" and an empty response removes the row from the DOM.', - demo_description="Click delete to remove a row. Uses sx-confirm for confirmation.", - demo=f"(~delete-demo :items (list {items_sx}))", - sx_code='(button\n :sx-delete "/api/delete/1"\n :sx-target "#row-1"\n :sx-swap "outerHTML"\n :sx-confirm "Delete this item?"\n "delete")', - handler_code='@bp.delete("/examples/api/delete/")\nasync def api_delete(item_id: str):\n # Empty response — outerHTML swap removes the row\n return Response("", status=200,\n content_type="text/sx")', - comp_placeholder_id="delete-comp", - wire_placeholder_id="delete-wire", - wire_note="Empty body — outerHTML swap replaces the target element with nothing.") - - -def _example_inline_edit_sx() -> str: - return _build_example( - title="Inline Edit", - description="Click edit to swap a display view for an edit form. Save swaps back.", - demo_description="Click edit, modify the text, save or cancel.", - demo="(~inline-edit-demo)", - sx_code=';; View mode — shows text + edit button\n(~inline-view :value "some text")\n\n;; Edit mode — returned by server on click\n(~inline-edit-form :value "some text")', - handler_code='@bp.get("/examples/api/edit")\nasync def api_edit_form():\n value = request.args.get("value", "")\n return sx_response(\n f\'(~inline-edit-form :value "{value}")\')\n\n@bp.post("/examples/api/edit")\nasync def api_edit_save():\n form = await request.form\n value = form.get("value", "")\n return sx_response(\n f\'(~inline-view :value "{value}")\')', - comp_placeholder_id="edit-comp", - comp_heading="Components", - handler_heading="Server handlers", - wire_placeholder_id="edit-wire") - - -def _example_oob_swaps_sx() -> str: - return _build_example( - title="Out-of-Band Swaps", - description="sx-swap-oob lets a single response update multiple elements anywhere in the DOM.", - demo_description="One request updates both Box A (via sx-target) and Box B (via sx-swap-oob).", - demo="(~oob-demo)", - sx_code=';; Button targets Box A\n(button\n :sx-get "/examples/api/oob"\n :sx-target "#oob-box-a"\n :sx-swap "innerHTML"\n "Update both boxes")', - handler_code='@bp.get("/examples/api/oob")\nasync def api_oob():\n now = datetime.now().strftime("%H:%M:%S")\n return sx_response(\n f\'(<>\'\n f\' (p "Box A updated at {now}")\'\n f\' (div :id "oob-box-b"\'\n f\' :sx-swap-oob "innerHTML"\'\n f\' (p "Box B updated at {now}")))\')', - wire_placeholder_id="oob-wire", - wire_note="The fragment contains both the main content and an OOB element. sx.js splits them: main content goes to sx-target, OOB elements find their targets by ID.") - - -def _example_lazy_loading_sx() -> str: - return _build_example( - title="Lazy Loading", - description='Use sx-trigger="load" to fetch content as soon as the element enters the DOM. Great for deferring expensive content below the fold.', - demo_description="Content loads automatically when the page renders.", - demo="(~lazy-loading-demo)", - sx_code='(div\n :sx-get "/examples/api/lazy"\n :sx-trigger "load"\n :sx-swap "innerHTML"\n (div :class "animate-pulse" "Loading..."))', - handler_code='@bp.get("/examples/api/lazy")\nasync def api_lazy():\n now = datetime.now().strftime(...)\n return sx_response(\n f\'(~lazy-result :time "{now}")\')', - comp_placeholder_id="lazy-comp", - wire_placeholder_id="lazy-wire") - - -def _example_infinite_scroll_sx() -> str: - return _build_example( - title="Infinite Scroll", - description='A sentinel element at the bottom uses sx-trigger="intersect once" to load the next page when scrolled into view. Each response appends items and a new sentinel.', - demo_description="Scroll down in the container to load more items (5 pages total).", - demo="(~infinite-scroll-demo)", - sx_code='(div :id "scroll-sentinel"\n :sx-get "/examples/api/scroll?page=2"\n :sx-trigger "intersect once"\n :sx-target "#scroll-items"\n :sx-swap "beforeend"\n "Loading more...")', - handler_code='@bp.get("/examples/api/scroll")\nasync def api_scroll():\n page = int(request.args.get("page", 2))\n items = [f"Item {i}" for i in range(...)]\n # Include next sentinel if more pages\n return sx_response(items_sx + sentinel_sx)', - comp_placeholder_id="scroll-comp", - wire_placeholder_id="scroll-wire") - - -def _example_progress_bar_sx() -> str: - return _build_example( - title="Progress Bar", - description='Start a server-side job, then poll for progress using sx-trigger="load delay:500ms" on each response. The bar fills up and stops when complete.', - demo_description="Click start to begin a simulated job.", - demo="(~progress-bar-demo)", - sx_code=';; Start the job\n(button\n :sx-post "/examples/api/progress/start"\n :sx-target "#progress-target"\n :sx-swap "innerHTML")\n\n;; Each response re-polls via sx-trigger="load"\n(div :sx-get "/api/progress/status?job=ID"\n :sx-trigger "load delay:500ms"\n :sx-target "#progress-target"\n :sx-swap "innerHTML")', - handler_code='@bp.post("/examples/api/progress/start")\nasync def api_progress_start():\n job_id = str(uuid4())[:8]\n _jobs[job_id] = 0\n return sx_response(\n f\'(~progress-status :percent 0 :job-id "{job_id}")\')', - comp_placeholder_id="progress-comp", - wire_placeholder_id="progress-wire") - - -def _example_active_search_sx() -> str: - return _build_example( - title="Active Search", - description='An input with sx-trigger="keyup delay:300ms changed" debounces keystrokes and only fires when the value changes. The server filters a list of programming languages.', - demo_description="Type to search through 20 programming languages.", - demo="(~active-search-demo)", - sx_code='(input :type "text" :name "q"\n :sx-get "/examples/api/search"\n :sx-trigger "keyup delay:300ms changed"\n :sx-target "#search-results"\n :sx-swap "innerHTML"\n :placeholder "Search...")', - handler_code='@bp.get("/examples/api/search")\nasync def api_search():\n q = request.args.get("q", "").lower()\n results = [l for l in LANGUAGES if q in l.lower()]\n return sx_response(\n f\'(~search-results :items (...) :query "{q}")\')', - comp_placeholder_id="search-comp", - wire_placeholder_id="search-wire") - - -def _example_inline_validation_sx() -> str: - return _build_example( - title="Inline Validation", - description="Validate an email field on blur. The server checks format and whether it is taken, returning green or red feedback inline.", - demo_description="Enter an email and click away (blur) to validate.", - demo="(~inline-validation-demo)", - sx_code='(input :type "text" :name "email"\n :sx-get "/examples/api/validate"\n :sx-trigger "blur"\n :sx-target "#email-feedback"\n :sx-swap "innerHTML"\n :placeholder "user@example.com")', - handler_code='@bp.get("/examples/api/validate")\nasync def api_validate():\n email = request.args.get("email", "")\n if "@" not in email:\n return sx_response(\'(~validation-error ...)\')\n return sx_response(\'(~validation-ok ...)\')', - comp_placeholder_id="validate-comp", - wire_placeholder_id="validate-wire") - - -def _example_value_select_sx() -> str: - return _build_example( - title="Value Select", - description="Two linked selects: pick a category and the second select updates with matching items via sx-get.", - demo_description="Select a category to populate the item dropdown.", - demo="(~value-select-demo)", - sx_code='(select :name "category"\n :sx-get "/examples/api/values"\n :sx-trigger "change"\n :sx-target "#value-items"\n :sx-swap "innerHTML"\n (option "Languages")\n (option "Frameworks")\n (option "Databases"))', - handler_code='@bp.get("/examples/api/values")\nasync def api_values():\n cat = request.args.get("category", "")\n items = VALUE_SELECT_DATA.get(cat, [])\n return sx_response(\n f\'(~value-options :items (list ...))\')', - comp_placeholder_id="values-comp", - wire_placeholder_id="values-wire") - - -def _example_reset_on_submit_sx() -> str: - return _build_example( - title="Reset on Submit", - description='Use sx-on:afterSwap="this.reset()" to clear form inputs after a successful submission.', - demo_description="Submit a message — the input resets after each send.", - demo="(~reset-on-submit-demo)", - sx_code='(form :id "reset-form"\n :sx-post "/examples/api/reset-submit"\n :sx-target "#reset-result"\n :sx-swap "innerHTML"\n :sx-on:afterSwap "this.reset()"\n (input :type "text" :name "message")\n (button :type "submit" "Send"))', - handler_code='@bp.post("/examples/api/reset-submit")\nasync def api_reset_submit():\n form = await request.form\n msg = form.get("message", "")\n return sx_response(\n f\'(~reset-message :message "{msg}" :time "...")\')', - comp_placeholder_id="reset-comp", - wire_placeholder_id="reset-wire") - - -def _example_edit_row_sx() -> str: - from content.pages import EDIT_ROW_DATA - rows_sx = " ".join( - f'(list "{r["id"]}" "{r["name"]}" "{r["price"]}" "{r["stock"]}")' - for r in EDIT_ROW_DATA - ) - return _build_example( - title="Edit Row", - description="Click edit to replace a table row with input fields. Save or cancel swaps back the display row. Uses sx-include to gather form values from the row.", - demo_description="Click edit on any row to modify it inline.", - demo=f"(~edit-row-demo :rows (list {rows_sx}))", - sx_code='(button\n :sx-get "/examples/api/editrow/1"\n :sx-target "#erow-1"\n :sx-swap "outerHTML"\n "edit")\n\n;; Save sends form data via POST\n(button\n :sx-post "/examples/api/editrow/1"\n :sx-target "#erow-1"\n :sx-swap "outerHTML"\n :sx-include "#erow-1"\n "save")', - handler_code='@bp.get("/examples/api/editrow/")\nasync def api_editrow_form(id):\n row = EDIT_ROW_DATA[id]\n return sx_response(\n f\'(~edit-row-form :id ... :name ...)\')\n\n@bp.post("/examples/api/editrow/")\nasync def api_editrow_save(id):\n form = await request.form\n return sx_response(\n f\'(~edit-row-view :id ... :name ...)\')', - comp_placeholder_id="editrow-comp", - wire_placeholder_id="editrow-wire") - - -def _example_bulk_update_sx() -> str: - from content.pages import BULK_USERS - users_sx = " ".join( - f'(list "{u["id"]}" "{u["name"]}" "{u["email"]}" "{u["status"]}")' - for u in BULK_USERS - ) - return _build_example( - title="Bulk Update", - description="Select rows with checkboxes and use Activate/Deactivate buttons. sx-include gathers checkbox values from the form.", - demo_description="Check some rows, then click Activate or Deactivate.", - demo=f"(~bulk-update-demo :users (list {users_sx}))", - sx_code='(button\n :sx-post "/examples/api/bulk?action=activate"\n :sx-target "#bulk-table"\n :sx-swap "innerHTML"\n :sx-include "#bulk-form"\n "Activate")', - handler_code='@bp.post("/examples/api/bulk")\nasync def api_bulk():\n action = request.args.get("action")\n form = await request.form\n ids = form.getlist("ids")\n # Update matching users\n return sx_response(updated_rows)', - comp_placeholder_id="bulk-comp", - wire_placeholder_id="bulk-wire") - - -def _example_swap_positions_sx() -> str: - return _build_example( - title="Swap Positions", - description="Demonstrates different swap modes: beforeend appends, afterbegin prepends, and none skips the main swap while still processing OOB updates.", - demo_description="Try each button to see different swap behaviours.", - demo="(~swap-positions-demo)", - sx_code=';; Append to end\n(button :sx-post "/api/swap-log?mode=beforeend"\n :sx-target "#swap-log" :sx-swap "beforeend"\n "Add to End")\n\n;; Prepend to start\n(button :sx-post "/api/swap-log?mode=afterbegin"\n :sx-target "#swap-log" :sx-swap "afterbegin"\n "Add to Start")\n\n;; No swap — OOB counter update only\n(button :sx-post "/api/swap-log?mode=none"\n :sx-target "#swap-log" :sx-swap "none"\n "Silent Ping")', - handler_code='@bp.post("/examples/api/swap-log")\nasync def api_swap_log():\n mode = request.args.get("mode")\n # OOB counter updates on every request\n oob = f\'(span :id "swap-counter" :sx-swap-oob "innerHTML" "Count: {n}")\'\n return sx_response(entry + oob)', - wire_placeholder_id="swap-wire") - - -def _example_select_filter_sx() -> str: - return _build_example( - title="Select Filter", - description="sx-select lets the client pick a specific section from the server response by CSS selector. The server always returns the full dashboard — the client filters.", - demo_description="Different buttons select different parts of the same server response.", - demo="(~select-filter-demo)", - sx_code=';; Pick just the stats section from the response\n(button\n :sx-get "/examples/api/dashboard"\n :sx-target "#filter-target"\n :sx-swap "innerHTML"\n :sx-select "#dash-stats"\n "Stats Only")\n\n;; No sx-select — get the full response\n(button\n :sx-get "/examples/api/dashboard"\n :sx-target "#filter-target"\n :sx-swap "innerHTML"\n "Full Dashboard")', - handler_code='@bp.get("/examples/api/dashboard")\nasync def api_dashboard():\n # Returns header + stats + footer\n # Client uses sx-select to pick sections\n return sx_response(\n \'(<> (div :id "dash-header" ...) \'\n \' (div :id "dash-stats" ...) \'\n \' (div :id "dash-footer" ...))\')', - wire_placeholder_id="filter-wire") - - -def _example_tabs_sx() -> str: - return _build_example( - title="Tabs", - description="Tab navigation using sx-push-url to update the browser URL. Back/forward buttons navigate between previously visited tabs.", - demo_description="Click tabs to switch content. Watch the browser URL change.", - demo="(~tabs-demo)", - sx_code='(button\n :sx-get "/examples/api/tabs/tab1"\n :sx-target "#tab-content"\n :sx-swap "innerHTML"\n :sx-push-url "/examples/tabs?tab=tab1"\n "Overview")', - handler_code='@bp.get("/examples/api/tabs/")\nasync def api_tabs(tab: str):\n content = TAB_CONTENT[tab]\n return sx_response(content)', - wire_placeholder_id="tabs-wire") - - -def _example_animations_sx() -> str: - return _build_example( - title="Animations", - description="CSS animations play on swap. The component injects a style tag with a keyframe animation and applies the class. Each click picks a random background colour.", - demo_description="Click to swap in content with a fade-in animation.", - demo="(~animations-demo)", - sx_code='(button\n :sx-get "/examples/api/animate"\n :sx-target "#anim-target"\n :sx-swap "innerHTML"\n "Load with animation")\n\n;; Component uses CSS animation class\n(defcomp ~anim-result (&key color time)\n (div :class "sx-fade-in ..."\n (style ".sx-fade-in { animation: sxFadeIn 0.5s }")\n (p "Faded in!")))', - handler_code='@bp.get("/examples/api/animate")\nasync def api_animate():\n colors = ["bg-violet-100", "bg-emerald-100", ...]\n color = random.choice(colors)\n return sx_response(\n f\'(~anim-result :color "{color}" :time "{now}")\')', - comp_placeholder_id="anim-comp", - wire_placeholder_id="anim-wire") - - -def _example_dialogs_sx() -> str: - return _build_example( - title="Dialogs", - description="Open a modal dialog by swapping in the dialog component. Close by swapping in empty content. Pure sx — no JavaScript library needed.", - demo_description="Click to open a modal dialog.", - demo="(~dialogs-demo)", - sx_code='(button\n :sx-get "/examples/api/dialog"\n :sx-target "#dialog-container"\n :sx-swap "innerHTML"\n "Open Dialog")\n\n;; Dialog closes by swapping empty content\n(button\n :sx-get "/examples/api/dialog/close"\n :sx-target "#dialog-container"\n :sx-swap "innerHTML"\n "Close")', - handler_code='@bp.get("/examples/api/dialog")\nasync def api_dialog():\n return sx_response(\n \'(~dialog-modal :title "Confirm"\'\n \' :message "Are you sure?")\')\n\n@bp.get("/examples/api/dialog/close")\nasync def api_dialog_close():\n return sx_response("")', - comp_placeholder_id="dialog-comp", - wire_placeholder_id="dialog-wire") - - -def _example_keyboard_shortcuts_sx() -> str: - return _build_example( - title="Keyboard Shortcuts", - description="Use sx-trigger with keyup event filters and from:body to listen for global keyboard shortcuts. The filter prevents firing when typing in inputs.", - demo_description="Press s, n, or h on your keyboard.", - demo="(~keyboard-shortcuts-demo)", - sx_code='(div :id "kbd-target"\n :sx-get "/examples/api/keyboard?key=s"\n :sx-trigger "keyup[key==\'s\'&&!event.target.matches(\'input,textarea\')] from:body"\n :sx-swap "innerHTML"\n "Press a shortcut key...")', - handler_code='@bp.get("/examples/api/keyboard")\nasync def api_keyboard():\n key = request.args.get("key", "")\n actions = {"s": "Search", "n": "New item", "h": "Help"}\n return sx_response(\n f\'(~kbd-result :key "{key}" :action "{actions[key]}")\')', - comp_placeholder_id="kbd-comp", - wire_placeholder_id="kbd-wire") - - -def _example_put_patch_sx() -> str: - from content.pages import PROFILE_DEFAULT - n, e, r = PROFILE_DEFAULT["name"], PROFILE_DEFAULT["email"], PROFILE_DEFAULT["role"] - return _build_example( - title="PUT / PATCH", - description="sx-put replaces the entire resource. This example shows a profile card with an Edit All button that sends a PUT with all fields.", - demo_description="Click Edit All to replace the full profile via PUT.", - demo=f'(~put-patch-demo :name "{n}" :email "{e}" :role "{r}")', - sx_code=';; Replace entire resource\n(form :sx-put "/examples/api/putpatch"\n :sx-target "#pp-target" :sx-swap "innerHTML"\n (input :name "name") (input :name "email")\n (button "Save All (PUT)"))', - handler_code='@bp.put("/examples/api/putpatch")\nasync def api_put():\n form = await request.form\n # Full replacement\n return sx_response(\'(~pp-view ...)\')', - comp_placeholder_id="pp-comp", - wire_placeholder_id="pp-wire") - - -def _example_json_encoding_sx() -> str: - return _build_example( - title="JSON Encoding", - description='Use sx-encoding="json" to send form data as a JSON body instead of URL-encoded form data. The server echoes back what it received.', - demo_description="Submit the form and see the JSON body the server received.", - demo="(~json-encoding-demo)", - sx_code='(form\n :sx-post "/examples/api/json-echo"\n :sx-target "#json-result"\n :sx-swap "innerHTML"\n :sx-encoding "json"\n (input :name "name" :value "Ada")\n (input :type "number" :name "age" :value "36")\n (button "Submit as JSON"))', - handler_code='@bp.post("/examples/api/json-echo")\nasync def api_json_echo():\n data = await request.get_json()\n body = json.dumps(data, indent=2)\n ct = request.content_type\n return sx_response(\n f\'(~json-result :body "{body}" :content-type "{ct}")\')', - comp_placeholder_id="json-comp", - wire_placeholder_id="json-wire") - - -def _example_vals_and_headers_sx() -> str: - return _build_example( - title="Vals & Headers", - description="sx-vals adds extra key/value pairs to the request parameters. sx-headers adds custom HTTP headers. The server echoes back what it received.", - demo_description="Click each button to see what the server receives.", - demo="(~vals-headers-demo)", - sx_code=';; Send extra values with the request\n(button\n :sx-get "/examples/api/echo-vals"\n :sx-vals "{\\"source\\": \\"button\\"}"\n "Send with vals")\n\n;; Send custom headers\n(button\n :sx-get "/examples/api/echo-headers"\n :sx-headers "{\\"X-Custom-Token\\": \\"abc123\\"}"\n "Send with headers")', - handler_code='@bp.get("/examples/api/echo-vals")\nasync def api_echo_vals():\n vals = dict(request.args)\n return sx_response(\n f\'(~echo-result :label "values" :items (...))\')\n\n@bp.get("/examples/api/echo-headers")\nasync def api_echo_headers():\n custom = {k: v for k, v in request.headers\n if k.startswith("X-")}\n return sx_response(\n f\'(~echo-result :label "headers" :items (...))\')', - comp_placeholder_id="vals-comp", - wire_placeholder_id="vals-wire") - - -def _example_loading_states_sx() -> str: - return _build_example( - title="Loading States", - description="sx.js adds the .sx-request CSS class to any element that has an active request. Use pure CSS to show spinners, disable buttons, or change opacity during loading.", - demo_description="Click the button — it shows a spinner during the 2-second request.", - demo="(~loading-states-demo)", - sx_code=';; .sx-request class added during request\n(style ".sx-loading-btn.sx-request {\n opacity: 0.7; pointer-events: none; }\n.sx-loading-btn.sx-request .sx-spinner {\n display: inline-block; }\n.sx-loading-btn .sx-spinner {\n display: none; }")\n\n(button :class "sx-loading-btn"\n :sx-get "/examples/api/slow"\n :sx-target "#loading-result"\n (span :class "sx-spinner animate-spin" "...")\n "Load slow endpoint")', - handler_code='@bp.get("/examples/api/slow")\nasync def api_slow():\n await asyncio.sleep(2)\n return sx_response(\n f\'(~loading-result :time "{now}")\')', - comp_placeholder_id="loading-comp", - wire_placeholder_id="loading-wire") - - -def _example_sync_replace_sx() -> str: - return _build_example( - title="Request Abort", - description='sx-sync="replace" aborts any in-flight request before sending a new one. This prevents stale responses from overwriting newer ones, even with random server delays.', - demo_description="Type quickly — only the latest result appears despite random 0.5-2s server delays.", - demo="(~sync-replace-demo)", - sx_code='(input :type "text" :name "q"\n :sx-get "/examples/api/slow-search"\n :sx-trigger "keyup delay:200ms changed"\n :sx-target "#sync-result"\n :sx-swap "innerHTML"\n :sx-sync "replace"\n "Type to search...")', - handler_code='@bp.get("/examples/api/slow-search")\nasync def api_slow_search():\n delay = random.uniform(0.5, 2.0)\n await asyncio.sleep(delay)\n q = request.args.get("q", "")\n return sx_response(\n f\'(~sync-result :query "{q}" :delay "{delay_ms}")\')', - comp_placeholder_id="sync-comp", - wire_placeholder_id="sync-wire") - - -def _example_retry_sx() -> str: - return _build_example( - title="Retry", - description='sx-retry="exponential:1000:8000" retries failed requests with exponential backoff starting at 1s up to 8s. The endpoint fails the first 2 attempts and succeeds on the 3rd.', - demo_description="Click the button — watch it retry automatically after failures.", - demo="(~retry-demo)", - sx_code='(button\n :sx-get "/examples/api/flaky"\n :sx-target "#retry-result"\n :sx-swap "innerHTML"\n :sx-retry "exponential:1000:8000"\n "Call flaky endpoint")', - handler_code='@bp.get("/examples/api/flaky")\nasync def api_flaky():\n _flaky["n"] += 1\n if _flaky["n"] % 3 != 0:\n return Response("", status=503)\n return sx_response(\n f\'(~retry-result :attempt {n} ...)\')', - comp_placeholder_id="retry-comp", - wire_placeholder_id="retry-wire") diff --git a/sx/sxc/pages/helpers.py b/sx/sxc/pages/helpers.py index 701e761..0ac82f9 100644 --- a/sx/sxc/pages/helpers.py +++ b/sx/sxc/pages/helpers.py @@ -1,110 +1,135 @@ -"""Public partials and page helper registration for sx docs.""" +"""Page helper registration for sx docs. + +All helpers return data values (dicts, lists) — no sx_call(), no SxExpr. +Markup composition lives entirely in .sx files. +""" from __future__ import annotations -from .essays import ( - _docs_content_sx, _reference_content_sx, _protocol_content_sx, - _examples_content_sx, _essay_content_sx, - _reference_index_sx, _reference_attr_detail_sx, -) -from .utils import _docs_nav_sx, _reference_nav_sx, _protocols_nav_sx, _examples_nav_sx, _essays_nav_sx - - -def home_content_sx() -> str: - """Home page content as sx wire format.""" - return ( - '(section :id "main-panel"' - ' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"' - ' (~sx-home-content))' - ) - - -async def docs_content_partial_sx(slug: str) -> str: - """Docs content as sx wire format.""" - inner = await _docs_content_sx(slug) - return ( - f'(section :id "main-panel"' - f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"' - f' {inner})' - ) - - -async def reference_content_partial_sx(slug: str) -> str: - inner = await _reference_content_sx(slug) - return ( - f'(section :id "main-panel"' - f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"' - f' {inner})' - ) - - -async def protocol_content_partial_sx(slug: str) -> str: - inner = await _protocol_content_sx(slug) - return ( - f'(section :id "main-panel"' - f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"' - f' {inner})' - ) - - -async def examples_content_partial_sx(slug: str) -> str: - inner = await _examples_content_sx(slug) - return ( - f'(section :id "main-panel"' - f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"' - f' {inner})' - ) - - -async def essay_content_partial_sx(slug: str) -> str: - inner = await _essay_content_sx(slug) - return ( - f'(section :id "main-panel"' - f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"' - f' {inner})' - ) - def _register_sx_helpers() -> None: - """Register Python content builder functions as page helpers.""" + """Register Python data helpers as page helpers.""" from shared.sx.pages import register_page_helpers from content.highlight import highlight as _highlight - from content.pages import ( - DOCS_NAV, REFERENCE_NAV, PROTOCOLS_NAV, - EXAMPLES_NAV, ESSAYS_NAV, - ) - - def _find_current(nav_list, slug, match_fn=None): - """Find the current nav label for a slug.""" - if match_fn: - return match_fn(nav_list, slug) - for label, href in nav_list: - if href.endswith(slug): - return label - return None register_page_helpers("sx", { - # Content builders - "home-content": lambda: "(~sx-home-content)", - "docs-content": _docs_content_sx, - "reference-content": _reference_content_sx, - "reference-index-content": _reference_index_sx, - "reference-attr-detail": _reference_attr_detail_sx, - "protocol-content": _protocol_content_sx, - "examples-content": _examples_content_sx, - "essay-content": _essay_content_sx, "highlight": _highlight, - # Nav builders - "docs-nav": _docs_nav_sx, - "reference-nav": _reference_nav_sx, - "protocols-nav": _protocols_nav_sx, - "examples-nav": _examples_nav_sx, - "essays-nav": _essays_nav_sx, - # Nav data (for current label lookup) - "DOCS_NAV": DOCS_NAV, - "REFERENCE_NAV": REFERENCE_NAV, - "PROTOCOLS_NAV": PROTOCOLS_NAV, - "EXAMPLES_NAV": EXAMPLES_NAV, - "ESSAYS_NAV": ESSAYS_NAV, - # Utility - "find-current": _find_current, + "primitives-data": _primitives_data, + "reference-data": _reference_data, + "attr-detail-data": _attr_detail_data, }) + + +def _primitives_data() -> dict: + """Return the PRIMITIVES dict for the primitives docs page.""" + from content.pages import PRIMITIVES + return PRIMITIVES + + +def _reference_data(slug: str) -> dict: + """Return reference table data for a given slug. + + Returns a dict whose keys become SX env bindings: + - attributes: req-attrs, beh-attrs, uniq-attrs + - headers: req-headers, resp-headers + - events: events-list + - js-api: js-api-list + """ + from content.pages import ( + REQUEST_ATTRS, BEHAVIOR_ATTRS, SX_UNIQUE_ATTRS, + REQUEST_HEADERS, RESPONSE_HEADERS, + EVENTS, JS_API, ATTR_DETAILS, + ) + + if slug == "attributes": + return { + "req-attrs": [ + {"name": a, "desc": d, "exists": e, + "href": f"/reference/attributes/{a}" if e and a in ATTR_DETAILS else None} + for a, d, e in REQUEST_ATTRS + ], + "beh-attrs": [ + {"name": a, "desc": d, "exists": e, + "href": f"/reference/attributes/{a}" if e and a in ATTR_DETAILS else None} + for a, d, e in BEHAVIOR_ATTRS + ], + "uniq-attrs": [ + {"name": a, "desc": d, "exists": e, + "href": f"/reference/attributes/{a}" if e and a in ATTR_DETAILS else None} + for a, d, e in SX_UNIQUE_ATTRS + ], + } + elif slug == "headers": + return { + "req-headers": [ + {"name": n, "value": v, "desc": d} + for n, v, d in REQUEST_HEADERS + ], + "resp-headers": [ + {"name": n, "value": v, "desc": d} + for n, v, d in RESPONSE_HEADERS + ], + } + elif slug == "events": + return { + "events-list": [ + {"name": n, "desc": d} + for n, d in EVENTS + ], + } + elif slug == "js-api": + return { + "js-api-list": [ + {"name": n, "desc": d} + for n, d in JS_API + ], + } + # Default — return attrs data for fallback + return { + "req-attrs": [ + {"name": a, "desc": d, "exists": e, + "href": f"/reference/attributes/{a}" if e and a in ATTR_DETAILS else None} + for a, d, e in REQUEST_ATTRS + ], + "beh-attrs": [ + {"name": a, "desc": d, "exists": e, + "href": f"/reference/attributes/{a}" if e and a in ATTR_DETAILS else None} + for a, d, e in BEHAVIOR_ATTRS + ], + "uniq-attrs": [ + {"name": a, "desc": d, "exists": e, + "href": f"/reference/attributes/{a}" if e and a in ATTR_DETAILS else None} + for a, d, e in SX_UNIQUE_ATTRS + ], + } + + +def _attr_detail_data(slug: str) -> dict: + """Return attribute detail data for a specific attribute slug. + + Returns a dict whose keys become SX env bindings: + - attr-title, attr-description, attr-example, attr-handler + - attr-demo (component call or None) + - attr-wire-id (wire placeholder id or None) + - attr-not-found (truthy if not found) + """ + from content.pages import ATTR_DETAILS + from shared.sx.helpers import SxExpr + + detail = ATTR_DETAILS.get(slug) + if not detail: + return {"attr-not-found": True} + + demo_name = detail.get("demo") + wire_id = None + if "handler" in detail: + wire_id = f"ref-wire-{slug.replace(':', '-').replace('*', 'star')}" + + return { + "attr-not-found": None, + "attr-title": slug, + "attr-description": detail["description"], + "attr-example": detail["example"], + "attr-handler": detail.get("handler"), + "attr-demo": SxExpr(f"(~{demo_name})") if demo_name else None, + "attr-wire-id": wire_id, + } diff --git a/sx/sxc/pages/utils.py b/sx/sxc/pages/utils.py deleted file mode 100644 index 9012cb9..0000000 --- a/sx/sxc/pages/utils.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Shared utility functions for sx docs pages.""" -from __future__ import annotations - -from shared.sx.helpers import ( - sx_call, SxExpr, -) - - -def _nav_items_sx(items: list[tuple[str, str]], current: str | None = None) -> SxExpr: - """Build nav link items as sx.""" - parts = [] - for label, href in items: - parts.append(sx_call("nav-link", - href=href, label=label, - is_selected="true" if current == label else None, - select_colours="aria-selected:bg-violet-200 aria-selected:text-violet-900", - )) - return SxExpr("(<> " + " ".join(parts) + ")") - - -def _doc_nav_sx(items: list[tuple[str, str]], current: str) -> str: - """Build the in-page doc navigation pills.""" - items_sx = " ".join( - f'(list "{label}" "{href}")' - for label, href in items - ) - return sx_call("doc-nav", items=SxExpr(f"(list {items_sx})"), current=current) - - -def _attr_table_sx(title: str, attrs: list[tuple[str, str, bool]]) -> str: - """Build an attribute reference table.""" - from content.pages import ATTR_DETAILS - rows = [] - for attr, desc, exists in attrs: - href = f"/reference/attributes/{attr}" if exists and attr in ATTR_DETAILS else None - rows.append(sx_call("doc-attr-row", attr=attr, description=desc, - exists="true" if exists else None, - href=href)) - rows_sx = "(<> " + " ".join(rows) + ")" - return sx_call("doc-attr-table", title=title, rows=SxExpr(rows_sx)) - - -def _headers_table_sx(title: str, headers: list[tuple[str, str, str]]) -> str: - """Build a headers reference table.""" - rows = [] - for name, value, desc in headers: - rows.append(sx_call("doc-headers-row", - name=name, value=value, description=desc)) - rows_sx = "(<> " + " ".join(rows) + ")" - return sx_call("doc-headers-table", title=title, rows=SxExpr(rows_sx)) - - -def _primitives_section_sx() -> str: - """Build the primitives section.""" - from content.pages import PRIMITIVES - parts = [] - for category, prims in PRIMITIVES.items(): - prims_sx = " ".join(f'"{p}"' for p in prims) - parts.append(sx_call("doc-primitives-table", - category=category, - primitives=SxExpr(f"(list {prims_sx})"))) - return " ".join(parts) - - -def _sx_header_sx(nav: str | None = None, *, child: str | None = None) -> str: - """Build the sx docs menu-row.""" - label_sx = sx_call("sx-docs-label") - return sx_call("menu-row-sx", - id="sx-row", level=1, colour="violet", - link_href="/", link_label="sx", - link_label_content=label_sx, - nav=SxExpr(nav) if nav else None, - child_id="sx-header-child", - child=SxExpr(child) if child else None, - ) - - -def _docs_nav_sx(current: str | None = None) -> str: - from content.pages import DOCS_NAV - return _nav_items_sx(DOCS_NAV, current) - - -def _reference_nav_sx(current: str | None = None) -> str: - from content.pages import REFERENCE_NAV - return _nav_items_sx(REFERENCE_NAV, current) - - -def _protocols_nav_sx(current: str | None = None) -> str: - from content.pages import PROTOCOLS_NAV - return _nav_items_sx(PROTOCOLS_NAV, current) - - -def _examples_nav_sx(current: str | None = None) -> str: - from content.pages import EXAMPLES_NAV - return _nav_items_sx(EXAMPLES_NAV, current) - - -def _essays_nav_sx(current: str | None = None) -> str: - from content.pages import ESSAYS_NAV - return _nav_items_sx(ESSAYS_NAV, current) - - -def _main_nav_sx(current_section: str | None = None) -> str: - from content.pages import MAIN_NAV - return _nav_items_sx(MAIN_NAV, current_section) - - -def _sub_row_sx(sub_label: str, sub_href: str, sub_nav: str, - selected: str = "") -> str: - """Build the level-2 sub-section menu-row.""" - return sx_call("menu-row-sx", - id="sx-sub-row", level=2, colour="violet", - link_href=sub_href, link_label=sub_label, - selected=selected or None, - nav=SxExpr(sub_nav), - )