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), - )