Migrate ~2,500 lines of SX markup from Python string concatenation in essays.py to proper .sx defcomp definitions: - docs-content.sx: 8 defcomps for docs pages (intro, getting-started, components, evaluator, primitives, css, server-rendering, home) - protocols.sx: 6 defcomps for protocol documentation pages - essays.sx: 9 essay defcomps (pure content, no params) - examples.sx: template defcomp receiving data values, calls highlight internally — Python passes raw code strings, never SX - reference.sx: 6 defcomps for data-driven reference pages essays.py reduced from 2,699 to 619 lines. Docs/protocol/essay functions become one-liners returning component names. Example functions use sx_call to pass data values to the template. Reference functions pass data-built component trees via SxExpr. renders.py: removed _code, _example_code, _placeholder, _clear_components_btn (now handled by .sx templates). helpers.py: removed inline hero code building, uses ~sx-home-content. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
620 lines
36 KiB
Python
620 lines
36 KiB
Python
"""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/<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.")
|
|
|
|
|
|
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/<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")
|
|
|
|
|
|
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/<tab>")\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")
|