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 <noreply@anthropic.com>
This commit is contained in:
@@ -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))))
|
||||
|
||||
316
sx/sx/examples-content.sx
Normal file
316
sx/sx/examples-content.sx
Normal file
@@ -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/<item_id>\")\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/<id>\")\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/<id>\")\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/<tab>\")\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"))
|
||||
87
sx/sx/nav-data.sx
Normal file
87
sx/sx/nav-data.sx
Normal file
@@ -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)))
|
||||
Reference in New Issue
Block a user