Add 21 new interactive examples to sx docs site (27 total)
Loading: lazy loading, infinite scroll, progress bar Forms: active search, inline validation, value select, reset on submit Records: edit row, bulk update Swap/DOM: swap positions, select filter, tabs Display: animations, dialogs, keyboard shortcuts HTTP: PUT/PATCH, JSON encoding, vals & headers Resilience: loading states, request abort (sync replace), retry Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
"""SX docs page routes."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import random
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from quart import Blueprint, Response, make_response, request
|
||||
|
||||
@@ -240,6 +244,571 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
oob_wire = _oob_code("oob-wire", wire_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire})')
|
||||
|
||||
# --- Lazy Loading ---
|
||||
|
||||
@bp.get("/examples/api/lazy")
|
||||
async def api_lazy():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
sx_src = f'(~lazy-result :time "{now}")'
|
||||
comp_text = _component_source_text("lazy-result")
|
||||
wire_text = _full_wire_text(sx_src, "lazy-result")
|
||||
oob_wire = _oob_code("lazy-wire", wire_text)
|
||||
oob_comp = _oob_code("lazy-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Infinite Scroll ---
|
||||
|
||||
@bp.get("/examples/api/scroll")
|
||||
async def api_scroll():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _full_wire_text
|
||||
page = int(request.args.get("page", 2))
|
||||
start = (page - 1) * 5 + 1
|
||||
next_page = page + 1
|
||||
items_html = " ".join(
|
||||
f'(div :class "px-4 py-3 border-b border-stone-100 text-sm text-stone-700" "Item {i} — loaded from page {page}")'
|
||||
for i in range(start, start + 5)
|
||||
)
|
||||
if next_page <= 6:
|
||||
sentinel = (
|
||||
f'(div :id "scroll-sentinel"'
|
||||
f' :sx-get "/examples/api/scroll?page={next_page}"'
|
||||
f' :sx-trigger "intersect once"'
|
||||
f' :sx-target "#scroll-items"'
|
||||
f' :sx-swap "beforeend"'
|
||||
f' :class "p-3 text-center text-stone-400 text-sm"'
|
||||
f' "Loading more...")'
|
||||
)
|
||||
else:
|
||||
sentinel = (
|
||||
'(div :class "p-3 text-center text-stone-500 text-sm font-medium"'
|
||||
' "All items loaded.")'
|
||||
)
|
||||
sx_src = f'(<> {items_html} {sentinel})'
|
||||
wire_text = _full_wire_text(sx_src)
|
||||
oob_wire = _oob_code("scroll-wire", wire_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire})')
|
||||
|
||||
# --- Progress Bar ---
|
||||
|
||||
_jobs: dict[str, int] = {}
|
||||
|
||||
@bp.post("/examples/api/progress/start")
|
||||
async def api_progress_start():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
job_id = str(uuid4())[:8]
|
||||
_jobs[job_id] = 0
|
||||
sx_src = f'(~progress-status :percent 0 :job-id "{job_id}")'
|
||||
comp_text = _component_source_text("progress-status")
|
||||
wire_text = _full_wire_text(sx_src, "progress-status")
|
||||
oob_wire = _oob_code("progress-wire", wire_text)
|
||||
oob_comp = _oob_code("progress-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@bp.get("/examples/api/progress/status")
|
||||
async def api_progress_status():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
job_id = request.args.get("job", "")
|
||||
current = _jobs.get(job_id, 0)
|
||||
current = min(current + random.randint(15, 30), 100)
|
||||
_jobs[job_id] = current
|
||||
sx_src = f'(~progress-status :percent {current} :job-id "{job_id}")'
|
||||
comp_text = _component_source_text("progress-status")
|
||||
wire_text = _full_wire_text(sx_src, "progress-status")
|
||||
oob_wire = _oob_code("progress-wire", wire_text)
|
||||
oob_comp = _oob_code("progress-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Active Search ---
|
||||
|
||||
@bp.get("/examples/api/search")
|
||||
async def api_search():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
from content.pages import SEARCH_LANGUAGES
|
||||
q = request.args.get("q", "").strip().lower()
|
||||
if not q:
|
||||
results = SEARCH_LANGUAGES
|
||||
else:
|
||||
results = [lang for lang in SEARCH_LANGUAGES if q in lang.lower()]
|
||||
items_sx = " ".join(f'"{r}"' for r in results)
|
||||
escaped_q = q.replace('"', '\\"')
|
||||
sx_src = f'(~search-results :items (list {items_sx}) :query "{escaped_q}")'
|
||||
comp_text = _component_source_text("search-results")
|
||||
wire_text = _full_wire_text(sx_src, "search-results")
|
||||
oob_wire = _oob_code("search-wire", wire_text)
|
||||
oob_comp = _oob_code("search-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Inline Validation ---
|
||||
|
||||
_TAKEN_EMAILS = {"admin@example.com", "test@example.com", "user@example.com"}
|
||||
|
||||
@bp.get("/examples/api/validate")
|
||||
async def api_validate():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
email = request.args.get("email", "").strip()
|
||||
if not email:
|
||||
sx_src = '(~validation-error :message "Email is required")'
|
||||
comp_name = "validation-error"
|
||||
elif "@" not in email or "." not in email.split("@")[-1]:
|
||||
sx_src = '(~validation-error :message "Invalid email format")'
|
||||
comp_name = "validation-error"
|
||||
elif email.lower() in _TAKEN_EMAILS:
|
||||
escaped = email.replace('"', '\\"')
|
||||
sx_src = f'(~validation-error :message "{escaped} is already taken")'
|
||||
comp_name = "validation-error"
|
||||
else:
|
||||
escaped = email.replace('"', '\\"')
|
||||
sx_src = f'(~validation-ok :email "{escaped}")'
|
||||
comp_name = "validation-ok"
|
||||
comp_text = _component_source_text(comp_name)
|
||||
wire_text = _full_wire_text(sx_src, comp_name)
|
||||
oob_wire = _oob_code("validate-wire", wire_text)
|
||||
oob_comp = _oob_code("validate-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@bp.post("/examples/api/validate/submit")
|
||||
async def api_validate_submit():
|
||||
from shared.sx.helpers import sx_response
|
||||
form = await request.form
|
||||
email = form.get("email", "").strip()
|
||||
if not email or "@" not in email:
|
||||
return sx_response('(p :class "text-sm text-rose-600 mt-2" "Please enter a valid email.")')
|
||||
escaped = email.replace('"', '\\"')
|
||||
return sx_response(f'(p :class "text-sm text-emerald-600 mt-2" "Form submitted with: {escaped}")')
|
||||
|
||||
# --- Value Select ---
|
||||
|
||||
@bp.get("/examples/api/values")
|
||||
async def api_values():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
from content.pages import VALUE_SELECT_DATA
|
||||
cat = request.args.get("category", "")
|
||||
items = VALUE_SELECT_DATA.get(cat, [])
|
||||
items_sx = " ".join(f'"{i}"' for i in items)
|
||||
sx_src = f'(~value-options :items (list {items_sx}))'
|
||||
comp_text = _component_source_text("value-options")
|
||||
wire_text = _full_wire_text(sx_src, "value-options")
|
||||
oob_wire = _oob_code("values-wire", wire_text)
|
||||
oob_comp = _oob_code("values-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Reset on Submit ---
|
||||
|
||||
@bp.post("/examples/api/reset-submit")
|
||||
async def api_reset_submit():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
form = await request.form
|
||||
msg = form.get("message", "").strip() or "(empty)"
|
||||
escaped = msg.replace('"', '\\"')
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
sx_src = f'(~reset-message :message "{escaped}" :time "{now}")'
|
||||
comp_text = _component_source_text("reset-message")
|
||||
wire_text = _full_wire_text(sx_src, "reset-message")
|
||||
oob_wire = _oob_code("reset-wire", wire_text)
|
||||
oob_comp = _oob_code("reset-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Edit Row ---
|
||||
|
||||
_edit_rows: dict[str, dict] = {}
|
||||
|
||||
def _get_edit_rows() -> dict[str, dict]:
|
||||
if not _edit_rows:
|
||||
from content.pages import EDIT_ROW_DATA
|
||||
for r in EDIT_ROW_DATA:
|
||||
_edit_rows[r["id"]] = dict(r)
|
||||
return _edit_rows
|
||||
|
||||
@bp.get("/examples/api/editrow/<row_id>")
|
||||
async def api_editrow_form(row_id: str):
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
rows = _get_edit_rows()
|
||||
row = rows.get(row_id, {"id": row_id, "name": "", "price": "0", "stock": "0"})
|
||||
sx_src = (f'(~edit-row-form :id "{row["id"]}" :name "{row["name"]}"'
|
||||
f' :price "{row["price"]}" :stock "{row["stock"]}")')
|
||||
comp_text = _component_source_text("edit-row-form")
|
||||
wire_text = _full_wire_text(sx_src, "edit-row-form")
|
||||
oob_wire = _oob_code("editrow-wire", wire_text)
|
||||
oob_comp = _oob_code("editrow-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@bp.post("/examples/api/editrow/<row_id>")
|
||||
async def api_editrow_save(row_id: str):
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
form = await request.form
|
||||
rows = _get_edit_rows()
|
||||
rows[row_id] = {
|
||||
"id": row_id,
|
||||
"name": form.get("name", ""),
|
||||
"price": form.get("price", "0"),
|
||||
"stock": form.get("stock", "0"),
|
||||
}
|
||||
row = rows[row_id]
|
||||
sx_src = (f'(~edit-row-view :id "{row["id"]}" :name "{row["name"]}"'
|
||||
f' :price "{row["price"]}" :stock "{row["stock"]}")')
|
||||
comp_text = _component_source_text("edit-row-view")
|
||||
wire_text = _full_wire_text(sx_src, "edit-row-view")
|
||||
oob_wire = _oob_code("editrow-wire", wire_text)
|
||||
oob_comp = _oob_code("editrow-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@bp.get("/examples/api/editrow/<row_id>/cancel")
|
||||
async def api_editrow_cancel(row_id: str):
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
rows = _get_edit_rows()
|
||||
row = rows.get(row_id, {"id": row_id, "name": "", "price": "0", "stock": "0"})
|
||||
sx_src = (f'(~edit-row-view :id "{row["id"]}" :name "{row["name"]}"'
|
||||
f' :price "{row["price"]}" :stock "{row["stock"]}")')
|
||||
comp_text = _component_source_text("edit-row-view")
|
||||
wire_text = _full_wire_text(sx_src, "edit-row-view")
|
||||
oob_wire = _oob_code("editrow-wire", wire_text)
|
||||
oob_comp = _oob_code("editrow-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Bulk Update ---
|
||||
|
||||
_bulk_users: dict[str, dict] = {}
|
||||
|
||||
def _get_bulk_users() -> dict[str, dict]:
|
||||
if not _bulk_users:
|
||||
from content.pages import BULK_USERS
|
||||
for u in BULK_USERS:
|
||||
_bulk_users[u["id"]] = dict(u)
|
||||
return _bulk_users
|
||||
|
||||
@bp.post("/examples/api/bulk")
|
||||
async def api_bulk():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
action = request.args.get("action", "activate")
|
||||
form = await request.form
|
||||
ids = form.getlist("ids")
|
||||
users = _get_bulk_users()
|
||||
new_status = "active" if action == "activate" else "inactive"
|
||||
for uid in ids:
|
||||
if uid in users:
|
||||
users[uid]["status"] = new_status
|
||||
rows = []
|
||||
for u in users.values():
|
||||
rows.append(
|
||||
f'(~bulk-row :id "{u["id"]}" :name "{u["name"]}"'
|
||||
f' :email "{u["email"]}" :status "{u["status"]}")'
|
||||
)
|
||||
sx_src = f'(<> {" ".join(rows)})'
|
||||
comp_text = _component_source_text("bulk-row")
|
||||
wire_text = _full_wire_text(sx_src, "bulk-row")
|
||||
oob_wire = _oob_code("bulk-wire", wire_text)
|
||||
oob_comp = _oob_code("bulk-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Swap Positions ---
|
||||
|
||||
_swap_count = {"n": 0}
|
||||
|
||||
@bp.post("/examples/api/swap-log")
|
||||
async def api_swap_log():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _full_wire_text
|
||||
mode = request.args.get("mode", "beforeend")
|
||||
_swap_count["n"] += 1
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
n = _swap_count["n"]
|
||||
entry = f'(div :class "px-3 py-2 text-sm text-stone-700" "[{now}] {mode} (#{n})")'
|
||||
oob_counter = (
|
||||
f'(span :id "swap-counter" :sx-swap-oob "innerHTML"'
|
||||
f' :class "self-center text-sm text-stone-500" "Count: {n}")'
|
||||
)
|
||||
sx_src = f'(<> {entry} {oob_counter})'
|
||||
wire_text = _full_wire_text(sx_src)
|
||||
oob_wire = _oob_code("swap-wire", wire_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire})')
|
||||
|
||||
# --- Select Filter (dashboard) ---
|
||||
|
||||
@bp.get("/examples/api/dashboard")
|
||||
async def api_dashboard():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _full_wire_text
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
sx_src = (
|
||||
f'(<>'
|
||||
f' (div :id "dash-header" :class "p-3 bg-violet-50 rounded mb-3"'
|
||||
f' (h4 :class "font-semibold text-violet-800" "Dashboard Header")'
|
||||
f' (p :class "text-sm text-violet-600" "Generated at {now}"))'
|
||||
f' (div :id "dash-stats" :class "grid grid-cols-3 gap-3 mb-3"'
|
||||
f' (div :class "p-3 bg-emerald-50 rounded text-center"'
|
||||
f' (p :class "text-2xl font-bold text-emerald-700" "142")'
|
||||
f' (p :class "text-xs text-emerald-600" "Users"))'
|
||||
f' (div :class "p-3 bg-blue-50 rounded text-center"'
|
||||
f' (p :class "text-2xl font-bold text-blue-700" "89")'
|
||||
f' (p :class "text-xs text-blue-600" "Orders"))'
|
||||
f' (div :class "p-3 bg-amber-50 rounded text-center"'
|
||||
f' (p :class "text-2xl font-bold text-amber-700" "$4.2k")'
|
||||
f' (p :class "text-xs text-amber-600" "Revenue")))'
|
||||
f' (div :id "dash-footer" :class "p-3 bg-stone-50 rounded"'
|
||||
f' (p :class "text-sm text-stone-500" "Last updated: {now}")))'
|
||||
)
|
||||
wire_text = _full_wire_text(sx_src)
|
||||
oob_wire = _oob_code("filter-wire", wire_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire})')
|
||||
|
||||
# --- Tabs ---
|
||||
|
||||
_TAB_CONTENT = {
|
||||
"tab1": ('(div (p :class "text-stone-700" "Welcome to the Overview tab.")'
|
||||
' (p :class "text-stone-500 text-sm mt-2"'
|
||||
' "This is the default tab content loaded via sx-get."))'),
|
||||
"tab2": ('(div (p :class "text-stone-700" "Here are the details.")'
|
||||
' (ul :class "mt-2 space-y-1 text-sm text-stone-600"'
|
||||
' (li "Version: 1.0.0")'
|
||||
' (li "Build: 2024-01-15")'
|
||||
' (li "Engine: sx")))'),
|
||||
"tab3": ('(div (p :class "text-stone-700" "Recent history:")'
|
||||
' (ol :class "mt-2 space-y-1 text-sm text-stone-600 list-decimal list-inside"'
|
||||
' (li "Initial release")'
|
||||
' (li "Added component caching")'
|
||||
' (li "Wire format v2")))'),
|
||||
}
|
||||
|
||||
@bp.get("/examples/api/tabs/<tab>")
|
||||
async def api_tabs(tab: str):
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _full_wire_text
|
||||
sx_src = _TAB_CONTENT.get(tab, _TAB_CONTENT["tab1"])
|
||||
buttons = []
|
||||
for t, label in [("tab1", "Overview"), ("tab2", "Details"), ("tab3", "History")]:
|
||||
active = "true" if t == tab else "false"
|
||||
buttons.append(f'(~tab-btn :tab "{t}" :label "{label}" :active "{active}")')
|
||||
oob_tabs = (
|
||||
f'(div :id "tab-buttons" :sx-swap-oob "innerHTML"'
|
||||
f' :class "flex border-b border-stone-200"'
|
||||
f' {" ".join(buttons)})'
|
||||
)
|
||||
wire_text = _full_wire_text(f'(<> {sx_src} {oob_tabs})', "tab-btn")
|
||||
oob_wire = _oob_code("tabs-wire", wire_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_tabs} {oob_wire})')
|
||||
|
||||
# --- Animations ---
|
||||
|
||||
@bp.get("/examples/api/animate")
|
||||
async def api_animate():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
colors = ["bg-violet-100", "bg-emerald-100", "bg-blue-100", "bg-amber-100", "bg-rose-100"]
|
||||
color = random.choice(colors)
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
sx_src = f'(~anim-result :color "{color}" :time "{now}")'
|
||||
comp_text = _component_source_text("anim-result")
|
||||
wire_text = _full_wire_text(sx_src, "anim-result")
|
||||
oob_wire = _oob_code("anim-wire", wire_text)
|
||||
oob_comp = _oob_code("anim-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Dialogs ---
|
||||
|
||||
@bp.get("/examples/api/dialog")
|
||||
async def api_dialog():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
sx_src = '(~dialog-modal :title "Confirm Action" :message "Are you sure you want to proceed? This is a demo dialog rendered entirely with sx components.")'
|
||||
comp_text = _component_source_text("dialog-modal")
|
||||
wire_text = _full_wire_text(sx_src, "dialog-modal")
|
||||
oob_wire = _oob_code("dialog-wire", wire_text)
|
||||
oob_comp = _oob_code("dialog-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@bp.get("/examples/api/dialog/close")
|
||||
async def api_dialog_close():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _full_wire_text
|
||||
wire_text = _full_wire_text("(empty — dialog closed)")
|
||||
oob_wire = _oob_code("dialog-wire", wire_text)
|
||||
return sx_response(f'(<> {oob_wire})')
|
||||
|
||||
# --- Keyboard Shortcuts ---
|
||||
|
||||
_KBD_ACTIONS = {
|
||||
"s": "Search panel activated",
|
||||
"n": "New item created",
|
||||
"h": "Help panel opened",
|
||||
}
|
||||
|
||||
@bp.get("/examples/api/keyboard")
|
||||
async def api_keyboard():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
key = request.args.get("key", "")
|
||||
action = _KBD_ACTIONS.get(key, f"Unknown key: {key}")
|
||||
escaped_action = action.replace('"', '\\"')
|
||||
escaped_key = key.replace('"', '\\"')
|
||||
sx_src = f'(~kbd-result :key "{escaped_key}" :action "{escaped_action}")'
|
||||
comp_text = _component_source_text("kbd-result")
|
||||
wire_text = _full_wire_text(sx_src, "kbd-result")
|
||||
oob_wire = _oob_code("kbd-wire", wire_text)
|
||||
oob_comp = _oob_code("kbd-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- PUT / PATCH ---
|
||||
|
||||
_profile = {}
|
||||
|
||||
def _get_profile() -> dict:
|
||||
if not _profile:
|
||||
from content.pages import PROFILE_DEFAULT
|
||||
_profile.update(PROFILE_DEFAULT)
|
||||
return _profile
|
||||
|
||||
@bp.get("/examples/api/putpatch/edit-all")
|
||||
async def api_pp_edit_all():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
p = _get_profile()
|
||||
sx_src = f'(~pp-form-full :name "{p["name"]}" :email "{p["email"]}" :role "{p["role"]}")'
|
||||
comp_text = _component_source_text("pp-form-full")
|
||||
wire_text = _full_wire_text(sx_src, "pp-form-full")
|
||||
oob_wire = _oob_code("pp-wire", wire_text)
|
||||
oob_comp = _oob_code("pp-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@bp.put("/examples/api/putpatch")
|
||||
async def api_pp_put():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
form = await request.form
|
||||
p = _get_profile()
|
||||
p["name"] = form.get("name", p["name"])
|
||||
p["email"] = form.get("email", p["email"])
|
||||
p["role"] = form.get("role", p["role"])
|
||||
sx_src = f'(~pp-view :name "{p["name"]}" :email "{p["email"]}" :role "{p["role"]}")'
|
||||
comp_text = _component_source_text("pp-view")
|
||||
wire_text = _full_wire_text(sx_src, "pp-view")
|
||||
oob_wire = _oob_code("pp-wire", wire_text)
|
||||
oob_comp = _oob_code("pp-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@bp.get("/examples/api/putpatch/cancel")
|
||||
async def api_pp_cancel():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
p = _get_profile()
|
||||
sx_src = f'(~pp-view :name "{p["name"]}" :email "{p["email"]}" :role "{p["role"]}")'
|
||||
comp_text = _component_source_text("pp-view")
|
||||
wire_text = _full_wire_text(sx_src, "pp-view")
|
||||
oob_wire = _oob_code("pp-wire", wire_text)
|
||||
oob_comp = _oob_code("pp-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- JSON Encoding ---
|
||||
|
||||
@bp.post("/examples/api/json-echo")
|
||||
async def api_json_echo():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
data = await request.get_json(silent=True) or {}
|
||||
body = json.dumps(data, indent=2)
|
||||
ct = request.content_type or "unknown"
|
||||
escaped_body = body.replace('\\', '\\\\').replace('"', '\\"')
|
||||
escaped_ct = ct.replace('"', '\\"')
|
||||
sx_src = f'(~json-result :body "{escaped_body}" :content-type "{escaped_ct}")'
|
||||
comp_text = _component_source_text("json-result")
|
||||
wire_text = _full_wire_text(sx_src, "json-result")
|
||||
oob_wire = _oob_code("json-wire", wire_text)
|
||||
oob_comp = _oob_code("json-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Vals & Headers ---
|
||||
|
||||
@bp.get("/examples/api/echo-vals")
|
||||
async def api_echo_vals():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
vals = {k: v for k, v in request.args.items()
|
||||
if k not in ("_", "sx-request")}
|
||||
items_sx = " ".join(f'"{k}: {v}"' for k, v in vals.items())
|
||||
sx_src = f'(~echo-result :label "values" :items (list {items_sx}))'
|
||||
comp_text = _component_source_text("echo-result")
|
||||
wire_text = _full_wire_text(sx_src, "echo-result")
|
||||
oob_wire = _oob_code("vals-wire", wire_text)
|
||||
oob_comp = _oob_code("vals-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@bp.get("/examples/api/echo-headers")
|
||||
async def api_echo_headers():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
custom = {k: v for k, v in request.headers if k.lower().startswith("x-")}
|
||||
items_sx = " ".join(f'"{k}: {v}"' for k, v in custom.items())
|
||||
sx_src = f'(~echo-result :label "headers" :items (list {items_sx}))'
|
||||
comp_text = _component_source_text("echo-result")
|
||||
wire_text = _full_wire_text(sx_src, "echo-result")
|
||||
oob_wire = _oob_code("vals-wire", wire_text)
|
||||
oob_comp = _oob_code("vals-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Loading States ---
|
||||
|
||||
@bp.get("/examples/api/slow")
|
||||
async def api_slow():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
await asyncio.sleep(2)
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
sx_src = f'(~loading-result :time "{now}")'
|
||||
comp_text = _component_source_text("loading-result")
|
||||
wire_text = _full_wire_text(sx_src, "loading-result")
|
||||
oob_wire = _oob_code("loading-wire", wire_text)
|
||||
oob_comp = _oob_code("loading-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Request Abort (sync replace) ---
|
||||
|
||||
@bp.get("/examples/api/slow-search")
|
||||
async def api_slow_search():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
delay = random.uniform(0.5, 2.0)
|
||||
await asyncio.sleep(delay)
|
||||
q = request.args.get("q", "").strip()
|
||||
delay_ms = int(delay * 1000)
|
||||
escaped = q.replace('"', '\\"')
|
||||
sx_src = f'(~sync-result :query "{escaped}" :delay "{delay_ms}")'
|
||||
comp_text = _component_source_text("sync-result")
|
||||
wire_text = _full_wire_text(sx_src, "sync-result")
|
||||
oob_wire = _oob_code("sync-wire", wire_text)
|
||||
oob_comp = _oob_code("sync-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Retry ---
|
||||
|
||||
_flaky = {"n": 0}
|
||||
|
||||
@bp.get("/examples/api/flaky")
|
||||
async def api_flaky():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
_flaky["n"] += 1
|
||||
n = _flaky["n"]
|
||||
if n % 3 != 0:
|
||||
return Response("", status=503, content_type="text/plain")
|
||||
sx_src = f'(~retry-result :attempt "{n}" :message "Success! The endpoint finally responded.")'
|
||||
comp_text = _component_source_text("retry-result")
|
||||
wire_text = _full_wire_text(sx_src, "retry-result")
|
||||
oob_wire = _oob_code("retry-wire", wire_text)
|
||||
oob_comp = _oob_code("retry-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Essays
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@@ -42,6 +42,27 @@ EXAMPLES_NAV = [
|
||||
("Delete Row", "/examples/delete-row"),
|
||||
("Inline Edit", "/examples/inline-edit"),
|
||||
("OOB Swaps", "/examples/oob-swaps"),
|
||||
("Lazy Loading", "/examples/lazy-loading"),
|
||||
("Infinite Scroll", "/examples/infinite-scroll"),
|
||||
("Progress Bar", "/examples/progress-bar"),
|
||||
("Active Search", "/examples/active-search"),
|
||||
("Inline Validation", "/examples/inline-validation"),
|
||||
("Value Select", "/examples/value-select"),
|
||||
("Reset on Submit", "/examples/reset-on-submit"),
|
||||
("Edit Row", "/examples/edit-row"),
|
||||
("Bulk Update", "/examples/bulk-update"),
|
||||
("Swap Positions", "/examples/swap-positions"),
|
||||
("Select Filter", "/examples/select-filter"),
|
||||
("Tabs", "/examples/tabs"),
|
||||
("Animations", "/examples/animations"),
|
||||
("Dialogs", "/examples/dialogs"),
|
||||
("Keyboard Shortcuts", "/examples/keyboard-shortcuts"),
|
||||
("PUT / PATCH", "/examples/put-patch"),
|
||||
("JSON Encoding", "/examples/json-encoding"),
|
||||
("Vals & Headers", "/examples/vals-and-headers"),
|
||||
("Loading States", "/examples/loading-states"),
|
||||
("Request Abort", "/examples/sync-replace"),
|
||||
("Retry", "/examples/retry"),
|
||||
]
|
||||
|
||||
ESSAYS_NAV = [
|
||||
@@ -182,3 +203,36 @@ DELETE_DEMO_ITEMS = [
|
||||
("4", "Deploy to production"),
|
||||
("5", "Add unit tests"),
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Static data for new examples
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SEARCH_LANGUAGES = [
|
||||
"Python", "JavaScript", "TypeScript", "Rust", "Go", "Java", "C", "C++",
|
||||
"Ruby", "Elixir", "Haskell", "Clojure", "Scala", "Kotlin", "Swift",
|
||||
"Zig", "OCaml", "Lua", "Perl", "PHP",
|
||||
]
|
||||
|
||||
PROFILE_DEFAULT = {"name": "Ada Lovelace", "email": "ada@example.com", "role": "Engineer"}
|
||||
|
||||
BULK_USERS = [
|
||||
{"id": "1", "name": "Alice Chen", "email": "alice@example.com", "status": "active"},
|
||||
{"id": "2", "name": "Bob Rivera", "email": "bob@example.com", "status": "inactive"},
|
||||
{"id": "3", "name": "Carol Zhang", "email": "carol@example.com", "status": "active"},
|
||||
{"id": "4", "name": "Dan Okafor", "email": "dan@example.com", "status": "inactive"},
|
||||
{"id": "5", "name": "Eve Larsson", "email": "eve@example.com", "status": "active"},
|
||||
]
|
||||
|
||||
VALUE_SELECT_DATA = {
|
||||
"Languages": ["Python", "JavaScript", "Rust", "Go"],
|
||||
"Frameworks": ["Quart", "FastAPI", "React", "Svelte"],
|
||||
"Databases": ["PostgreSQL", "Redis", "SQLite", "MongoDB"],
|
||||
}
|
||||
|
||||
EDIT_ROW_DATA = [
|
||||
{"id": "1", "name": "Widget A", "price": "19.99", "stock": "142"},
|
||||
{"id": "2", "name": "Widget B", "price": "24.50", "stock": "89"},
|
||||
{"id": "3", "name": "Widget C", "price": "12.00", "stock": "305"},
|
||||
{"id": "4", "name": "Widget D", "price": "45.00", "stock": "67"},
|
||||
]
|
||||
|
||||
@@ -157,3 +157,623 @@
|
||||
:sx-swap "innerHTML"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Update both boxes")))
|
||||
|
||||
;; --- Lazy loading demo ---
|
||||
|
||||
(defcomp ~lazy-loading-demo ()
|
||||
(div :class "space-y-4"
|
||||
(p :class "text-sm text-stone-500" "The content below loads automatically when the page renders.")
|
||||
(div :id "lazy-target"
|
||||
:sx-get "/examples/api/lazy"
|
||||
:sx-trigger "load"
|
||||
:sx-swap "innerHTML"
|
||||
:class "p-6 rounded border border-stone-200 bg-stone-50 text-center"
|
||||
(div :class "animate-pulse space-y-2"
|
||||
(div :class "h-4 bg-stone-200 rounded w-3/4 mx-auto")
|
||||
(div :class "h-4 bg-stone-200 rounded w-1/2 mx-auto")))))
|
||||
|
||||
(defcomp ~lazy-result (&key time)
|
||||
(div :class "space-y-2"
|
||||
(p :class "text-stone-800 font-medium" "Content loaded on page render!")
|
||||
(p :class "text-stone-500 text-sm" (str "Loaded via sx-trigger=\"load\" at " time))))
|
||||
|
||||
;; --- Infinite scroll demo ---
|
||||
|
||||
(defcomp ~infinite-scroll-demo ()
|
||||
(div :class "h-64 overflow-y-auto border border-stone-200 rounded" :id "scroll-container"
|
||||
(div :id "scroll-items"
|
||||
(map-indexed (fn (i item)
|
||||
(div :class "px-4 py-3 border-b border-stone-100 text-sm text-stone-700"
|
||||
(str "Item " (+ i 1) " — loaded with the page")))
|
||||
(list 1 2 3 4 5))
|
||||
(div :id "scroll-sentinel"
|
||||
:sx-get "/examples/api/scroll?page=2"
|
||||
:sx-trigger "intersect once"
|
||||
:sx-target "#scroll-items"
|
||||
:sx-swap "beforeend"
|
||||
:class "p-3 text-center text-stone-400 text-sm"
|
||||
"Loading more..."))))
|
||||
|
||||
(defcomp ~scroll-items (&key items page)
|
||||
(<>
|
||||
(map (fn (item)
|
||||
(div :class "px-4 py-3 border-b border-stone-100 text-sm text-stone-700" item))
|
||||
items)
|
||||
(when (<= page 5)
|
||||
(div :id "scroll-sentinel"
|
||||
:sx-get (str "/examples/api/scroll?page=" page)
|
||||
:sx-trigger "intersect once"
|
||||
:sx-target "#scroll-items"
|
||||
:sx-swap "beforeend"
|
||||
:class "p-3 text-center text-stone-400 text-sm"
|
||||
"Loading more..."))))
|
||||
|
||||
;; --- Progress bar demo ---
|
||||
|
||||
(defcomp ~progress-bar-demo ()
|
||||
(div :class "space-y-4"
|
||||
(div :id "progress-target" :class "space-y-3"
|
||||
(div :class "w-full bg-stone-200 rounded-full h-4"
|
||||
(div :class "bg-violet-600 h-4 rounded-full transition-all" :style "width: 0%"))
|
||||
(p :class "text-sm text-stone-500 text-center" "Click start to begin."))
|
||||
(button
|
||||
:sx-post "/examples/api/progress/start"
|
||||
:sx-target "#progress-target"
|
||||
:sx-swap "innerHTML"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Start job")))
|
||||
|
||||
(defcomp ~progress-status (&key percent job-id)
|
||||
(div :class "space-y-3"
|
||||
(div :class "w-full bg-stone-200 rounded-full h-4"
|
||||
(div :class "bg-violet-600 h-4 rounded-full transition-all"
|
||||
:style (str "width: " percent "%")))
|
||||
(p :class "text-sm text-stone-500 text-center" (str percent "% complete"))
|
||||
(when (< percent 100)
|
||||
(div :sx-get (str "/examples/api/progress/status?job=" job-id)
|
||||
:sx-trigger "load delay:500ms"
|
||||
:sx-target "#progress-target"
|
||||
:sx-swap "innerHTML"))
|
||||
(when (= percent 100)
|
||||
(p :class "text-sm text-emerald-600 font-medium text-center" "Job complete!"))))
|
||||
|
||||
;; --- Active search demo ---
|
||||
|
||||
(defcomp ~active-search-demo ()
|
||||
(div :class "space-y-3"
|
||||
(input :type "text" :name "q"
|
||||
:sx-get "/examples/api/search"
|
||||
:sx-trigger "keyup delay:300ms changed"
|
||||
:sx-target "#search-results"
|
||||
:sx-swap "innerHTML"
|
||||
:placeholder "Search programming languages..."
|
||||
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500")
|
||||
(div :id "search-results" :class "border border-stone-200 rounded divide-y divide-stone-100"
|
||||
(p :class "p-3 text-sm text-stone-400" "Type to search..."))))
|
||||
|
||||
(defcomp ~search-results (&key items query)
|
||||
(<>
|
||||
(if (empty? items)
|
||||
(p :class "p-3 text-sm text-stone-400" (str "No results for \"" query "\""))
|
||||
(map (fn (item)
|
||||
(div :class "px-3 py-2 text-sm text-stone-700" item))
|
||||
items))))
|
||||
|
||||
;; --- Inline validation demo ---
|
||||
|
||||
(defcomp ~inline-validation-demo ()
|
||||
(form :class "space-y-4" :sx-post "/examples/api/validate/submit" :sx-target "#validation-result" :sx-swap "innerHTML"
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1" "Email")
|
||||
(input :type "text" :name "email" :placeholder "user@example.com"
|
||||
:sx-get "/examples/api/validate"
|
||||
:sx-trigger "blur"
|
||||
:sx-target "#email-feedback"
|
||||
:sx-swap "innerHTML"
|
||||
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500")
|
||||
(div :id "email-feedback" :class "mt-1"))
|
||||
(button :type "submit"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Submit")
|
||||
(div :id "validation-result")))
|
||||
|
||||
(defcomp ~validation-ok (&key email)
|
||||
(p :class "text-sm text-emerald-600" (str email " is available")))
|
||||
|
||||
(defcomp ~validation-error (&key message)
|
||||
(p :class "text-sm text-rose-600" message))
|
||||
|
||||
;; --- Value select demo ---
|
||||
|
||||
(defcomp ~value-select-demo ()
|
||||
(div :class "space-y-3"
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1" "Category")
|
||||
(select :name "category"
|
||||
:sx-get "/examples/api/values"
|
||||
:sx-trigger "change"
|
||||
:sx-target "#value-items"
|
||||
:sx-swap "innerHTML"
|
||||
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500"
|
||||
(option :value "" "Pick a category...")
|
||||
(option :value "Languages" "Languages")
|
||||
(option :value "Frameworks" "Frameworks")
|
||||
(option :value "Databases" "Databases")))
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1" "Item")
|
||||
(select :id "value-items"
|
||||
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500"
|
||||
(option :value "" "Select a category first...")))))
|
||||
|
||||
(defcomp ~value-options (&key items)
|
||||
(<>
|
||||
(map (fn (item) (option :value item item)) items)))
|
||||
|
||||
;; --- Reset on submit demo ---
|
||||
|
||||
(defcomp ~reset-on-submit-demo ()
|
||||
(div :class "space-y-3"
|
||||
(form :id "reset-form"
|
||||
:sx-post "/examples/api/reset-submit"
|
||||
:sx-target "#reset-result"
|
||||
:sx-swap "innerHTML"
|
||||
:sx-on:afterSwap "this.reset()"
|
||||
:class "flex gap-2"
|
||||
(input :type "text" :name "message" :placeholder "Type a message..."
|
||||
:class "flex-1 px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500")
|
||||
(button :type "submit"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Send"))
|
||||
(div :id "reset-result" :class "space-y-2"
|
||||
(p :class "text-sm text-stone-400" "Messages will appear here."))))
|
||||
|
||||
(defcomp ~reset-message (&key message time)
|
||||
(div :class "px-3 py-2 bg-stone-50 rounded text-sm text-stone-700"
|
||||
(str "[" time "] " message)))
|
||||
|
||||
;; --- Edit row demo ---
|
||||
|
||||
(defcomp ~edit-row-demo (&key rows)
|
||||
(div
|
||||
(table :class "w-full text-left text-sm"
|
||||
(thead
|
||||
(tr :class "border-b border-stone-200"
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Name")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Price")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Stock")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600 w-24" "")))
|
||||
(tbody :id "edit-rows"
|
||||
(map (fn (row)
|
||||
(~edit-row-view :id (nth 0 row) :name (nth 1 row) :price (nth 2 row) :stock (nth 3 row)))
|
||||
rows)))))
|
||||
|
||||
(defcomp ~edit-row-view (&key id name price stock)
|
||||
(tr :id (str "erow-" id) :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" name)
|
||||
(td :class "px-3 py-2 text-stone-700" (str "$" price))
|
||||
(td :class "px-3 py-2 text-stone-700" stock)
|
||||
(td :class "px-3 py-2"
|
||||
(button
|
||||
:sx-get (str "/examples/api/editrow/" id)
|
||||
:sx-target (str "#erow-" id)
|
||||
:sx-swap "outerHTML"
|
||||
:class "text-sm text-violet-600 hover:text-violet-800"
|
||||
"edit"))))
|
||||
|
||||
(defcomp ~edit-row-form (&key id name price stock)
|
||||
(tr :id (str "erow-" id) :class "border-b border-stone-100 bg-violet-50"
|
||||
(td :class "px-3 py-2"
|
||||
(input :type "text" :name "name" :value name
|
||||
:class "w-full px-2 py-1 border border-stone-300 rounded text-sm"))
|
||||
(td :class "px-3 py-2"
|
||||
(input :type "text" :name "price" :value price
|
||||
:class "w-20 px-2 py-1 border border-stone-300 rounded text-sm"))
|
||||
(td :class "px-3 py-2"
|
||||
(input :type "text" :name "stock" :value stock
|
||||
:class "w-20 px-2 py-1 border border-stone-300 rounded text-sm"))
|
||||
(td :class "px-3 py-2 space-x-1"
|
||||
(button
|
||||
:sx-post (str "/examples/api/editrow/" id)
|
||||
:sx-target (str "#erow-" id)
|
||||
:sx-swap "outerHTML"
|
||||
:sx-include (str "#erow-" id)
|
||||
:class "text-sm text-emerald-600 hover:text-emerald-800"
|
||||
"save")
|
||||
(button
|
||||
:sx-get (str "/examples/api/editrow/" id "/cancel")
|
||||
:sx-target (str "#erow-" id)
|
||||
:sx-swap "outerHTML"
|
||||
:class "text-sm text-stone-500 hover:text-stone-700"
|
||||
"cancel"))))
|
||||
|
||||
;; --- Bulk update demo ---
|
||||
|
||||
(defcomp ~bulk-update-demo (&key users)
|
||||
(div :class "space-y-3"
|
||||
(form :id "bulk-form"
|
||||
(div :class "flex gap-2 mb-3"
|
||||
(button :type "button"
|
||||
:sx-post "/examples/api/bulk?action=activate"
|
||||
:sx-target "#bulk-table"
|
||||
:sx-swap "innerHTML"
|
||||
:sx-include "#bulk-form"
|
||||
:class "px-3 py-1.5 bg-emerald-600 text-white rounded text-sm hover:bg-emerald-700"
|
||||
"Activate")
|
||||
(button :type "button"
|
||||
:sx-post "/examples/api/bulk?action=deactivate"
|
||||
:sx-target "#bulk-table"
|
||||
:sx-swap "innerHTML"
|
||||
:sx-include "#bulk-form"
|
||||
:class "px-3 py-1.5 bg-stone-600 text-white rounded text-sm hover:bg-stone-700"
|
||||
"Deactivate"))
|
||||
(table :class "w-full text-left text-sm"
|
||||
(thead
|
||||
(tr :class "border-b border-stone-200"
|
||||
(th :class "px-3 py-2 w-8" "")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Name")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Email")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Status")))
|
||||
(tbody :id "bulk-table"
|
||||
(map (fn (u)
|
||||
(~bulk-row :id (nth 0 u) :name (nth 1 u) :email (nth 2 u) :status (nth 3 u)))
|
||||
users))))))
|
||||
|
||||
(defcomp ~bulk-row (&key id name email status)
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2"
|
||||
(input :type "checkbox" :name "ids" :value id))
|
||||
(td :class "px-3 py-2 text-stone-700" name)
|
||||
(td :class "px-3 py-2 text-stone-700" email)
|
||||
(td :class "px-3 py-2"
|
||||
(span :class (str "px-2 py-0.5 rounded text-xs font-medium "
|
||||
(if (= status "active")
|
||||
"bg-emerald-100 text-emerald-700"
|
||||
"bg-stone-100 text-stone-500"))
|
||||
status))))
|
||||
|
||||
;; --- Swap positions demo ---
|
||||
|
||||
(defcomp ~swap-positions-demo ()
|
||||
(div :class "space-y-3"
|
||||
(div :class "flex gap-2"
|
||||
(button
|
||||
:sx-post "/examples/api/swap-log?mode=beforeend"
|
||||
:sx-target "#swap-log"
|
||||
:sx-swap "beforeend"
|
||||
:class "px-3 py-1.5 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Add to End")
|
||||
(button
|
||||
:sx-post "/examples/api/swap-log?mode=afterbegin"
|
||||
:sx-target "#swap-log"
|
||||
:sx-swap "afterbegin"
|
||||
:class "px-3 py-1.5 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Add to Start")
|
||||
(button
|
||||
:sx-post "/examples/api/swap-log?mode=none"
|
||||
:sx-target "#swap-log"
|
||||
:sx-swap "none"
|
||||
:class "px-3 py-1.5 bg-stone-600 text-white rounded text-sm hover:bg-stone-700"
|
||||
"Silent Ping")
|
||||
(span :id "swap-counter" :class "self-center text-sm text-stone-500" "Count: 0"))
|
||||
(div :id "swap-log"
|
||||
:class "border border-stone-200 rounded h-48 overflow-y-auto divide-y divide-stone-100"
|
||||
(p :class "p-3 text-sm text-stone-400" "Log entries will appear here."))))
|
||||
|
||||
(defcomp ~swap-entry (&key time mode)
|
||||
(div :class "px-3 py-2 text-sm text-stone-700"
|
||||
(str "[" time "] " mode)))
|
||||
|
||||
;; --- Select filter demo ---
|
||||
|
||||
(defcomp ~select-filter-demo ()
|
||||
(div :class "space-y-3"
|
||||
(div :class "flex gap-2"
|
||||
(button
|
||||
:sx-get "/examples/api/dashboard"
|
||||
:sx-target "#filter-target"
|
||||
:sx-swap "innerHTML"
|
||||
:sx-select "#dash-stats"
|
||||
:class "px-3 py-1.5 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Stats Only")
|
||||
(button
|
||||
:sx-get "/examples/api/dashboard"
|
||||
:sx-target "#filter-target"
|
||||
:sx-swap "innerHTML"
|
||||
:sx-select "#dash-header"
|
||||
:class "px-3 py-1.5 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Header Only")
|
||||
(button
|
||||
:sx-get "/examples/api/dashboard"
|
||||
:sx-target "#filter-target"
|
||||
:sx-swap "innerHTML"
|
||||
:class "px-3 py-1.5 bg-stone-600 text-white rounded text-sm hover:bg-stone-700"
|
||||
"Full Dashboard"))
|
||||
(div :id "filter-target" :class "border border-stone-200 rounded p-4 bg-white"
|
||||
(p :class "text-sm text-stone-400" "Click a button to load content."))))
|
||||
|
||||
;; --- Tabs demo ---
|
||||
|
||||
(defcomp ~tabs-demo ()
|
||||
(div :class "space-y-0"
|
||||
(div :class "flex border-b border-stone-200" :id "tab-buttons"
|
||||
(~tab-btn :tab "tab1" :label "Overview" :active "true")
|
||||
(~tab-btn :tab "tab2" :label "Details" :active "false")
|
||||
(~tab-btn :tab "tab3" :label "History" :active "false"))
|
||||
(div :id "tab-content" :class "p-4 border border-t-0 border-stone-200 rounded-b"
|
||||
(p :class "text-stone-700" "Welcome to the Overview tab. This content is loaded by default.")
|
||||
(p :class "text-stone-500 text-sm mt-2" "Click the tabs above to navigate. Watch the browser URL update."))))
|
||||
|
||||
(defcomp ~tab-btn (&key tab label active)
|
||||
(button
|
||||
:sx-get (str "/examples/api/tabs/" tab)
|
||||
:sx-target "#tab-content"
|
||||
:sx-swap "innerHTML"
|
||||
:sx-push-url (str "/examples/tabs?tab=" tab)
|
||||
:class (str "px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors "
|
||||
(if (= active "true")
|
||||
"border-violet-600 text-violet-600"
|
||||
"border-transparent text-stone-500 hover:text-stone-700"))
|
||||
label))
|
||||
|
||||
;; --- Animations demo ---
|
||||
|
||||
(defcomp ~animations-demo ()
|
||||
(div :class "space-y-4"
|
||||
(button
|
||||
:sx-get "/examples/api/animate"
|
||||
:sx-target "#anim-target"
|
||||
:sx-swap "innerHTML"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Load with animation")
|
||||
(div :id "anim-target" :class "p-4 rounded border border-stone-200 bg-white text-center"
|
||||
(p :class "text-stone-400" "Content will fade in here."))))
|
||||
|
||||
(defcomp ~anim-result (&key color time)
|
||||
(div :class "sx-fade-in space-y-2"
|
||||
(style ".sx-fade-in { animation: sxFadeIn 0.5s ease-out; } @keyframes sxFadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }")
|
||||
(div :class (str "p-4 rounded transition-colors duration-700 " color)
|
||||
(p :class "font-medium" "Faded in!")
|
||||
(p :class "text-sm mt-1" (str "Loaded at " time)))))
|
||||
|
||||
;; --- Dialogs demo ---
|
||||
|
||||
(defcomp ~dialogs-demo ()
|
||||
(div
|
||||
(button
|
||||
:sx-get "/examples/api/dialog"
|
||||
:sx-target "#dialog-container"
|
||||
:sx-swap "innerHTML"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Open Dialog")
|
||||
(div :id "dialog-container")))
|
||||
|
||||
(defcomp ~dialog-modal (&key title message)
|
||||
(div :class "fixed inset-0 z-50 flex items-center justify-center"
|
||||
(div :class "absolute inset-0 bg-black/50"
|
||||
:sx-get "/examples/api/dialog/close"
|
||||
:sx-target "#dialog-container"
|
||||
:sx-swap "innerHTML")
|
||||
(div :class "relative bg-white rounded-lg shadow-xl p-6 max-w-md w-full mx-4 space-y-4"
|
||||
(h3 :class "text-lg font-semibold text-stone-800" title)
|
||||
(p :class "text-stone-600" message)
|
||||
(div :class "flex justify-end gap-2"
|
||||
(button
|
||||
:sx-get "/examples/api/dialog/close"
|
||||
:sx-target "#dialog-container"
|
||||
:sx-swap "innerHTML"
|
||||
:class "px-4 py-2 bg-stone-200 text-stone-700 rounded text-sm hover:bg-stone-300"
|
||||
"Cancel")
|
||||
(button
|
||||
:sx-get "/examples/api/dialog/close"
|
||||
:sx-target "#dialog-container"
|
||||
:sx-swap "innerHTML"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Confirm")))))
|
||||
|
||||
;; --- Keyboard shortcuts demo ---
|
||||
|
||||
(defcomp ~keyboard-shortcuts-demo ()
|
||||
(div :class "space-y-4"
|
||||
(div :class "p-4 rounded border border-stone-200 bg-stone-50"
|
||||
(p :class "text-sm text-stone-600 font-medium mb-2" "Keyboard shortcuts:")
|
||||
(div :class "flex gap-4"
|
||||
(div :class "flex items-center gap-1"
|
||||
(kbd :class "px-2 py-0.5 bg-white border border-stone-300 rounded text-xs font-mono" "s")
|
||||
(span :class "text-sm text-stone-500" "Search"))
|
||||
(div :class "flex items-center gap-1"
|
||||
(kbd :class "px-2 py-0.5 bg-white border border-stone-300 rounded text-xs font-mono" "n")
|
||||
(span :class "text-sm text-stone-500" "New item"))
|
||||
(div :class "flex items-center gap-1"
|
||||
(kbd :class "px-2 py-0.5 bg-white border border-stone-300 rounded text-xs font-mono" "h")
|
||||
(span :class "text-sm text-stone-500" "Help"))))
|
||||
(div :id "kbd-target"
|
||||
:sx-get "/examples/api/keyboard?key=s"
|
||||
:sx-trigger "keyup[key=='s'&&!event.target.matches('input,textarea')] from:body"
|
||||
:sx-swap "innerHTML"
|
||||
:class "p-4 rounded border border-stone-200 bg-white text-center"
|
||||
(p :class "text-stone-400 text-sm" "Press a shortcut key..."))
|
||||
(div :sx-get "/examples/api/keyboard?key=n"
|
||||
:sx-trigger "keyup[key=='n'&&!event.target.matches('input,textarea')] from:body"
|
||||
:sx-target "#kbd-target"
|
||||
:sx-swap "innerHTML")
|
||||
(div :sx-get "/examples/api/keyboard?key=h"
|
||||
:sx-trigger "keyup[key=='h'&&!event.target.matches('input,textarea')] from:body"
|
||||
:sx-target "#kbd-target"
|
||||
:sx-swap "innerHTML")))
|
||||
|
||||
(defcomp ~kbd-result (&key key action)
|
||||
(div :class "space-y-1"
|
||||
(p :class "text-stone-800 font-medium" action)
|
||||
(p :class "text-sm text-stone-500" (str "Triggered by pressing '" key "'"))))
|
||||
|
||||
;; --- PUT / PATCH demo ---
|
||||
|
||||
(defcomp ~put-patch-demo (&key name email role)
|
||||
(div :id "pp-target" :class "space-y-4"
|
||||
(~pp-view :name name :email email :role role)))
|
||||
|
||||
(defcomp ~pp-view (&key name email role)
|
||||
(div :class "space-y-3"
|
||||
(div :class "flex justify-between items-start"
|
||||
(div
|
||||
(p :class "text-stone-800 font-medium" name)
|
||||
(p :class "text-sm text-stone-500" email)
|
||||
(p :class "text-sm text-stone-500" role))
|
||||
(button
|
||||
:sx-get "/examples/api/putpatch/edit-all"
|
||||
:sx-target "#pp-target"
|
||||
:sx-swap "innerHTML"
|
||||
:class "text-sm text-violet-600 hover:text-violet-800"
|
||||
"Edit All (PUT)"))))
|
||||
|
||||
(defcomp ~pp-form-full (&key name email role)
|
||||
(form
|
||||
:sx-put "/examples/api/putpatch"
|
||||
:sx-target "#pp-target"
|
||||
:sx-swap "innerHTML"
|
||||
:class "space-y-3"
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1" "Name")
|
||||
(input :type "text" :name "name" :value name
|
||||
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm"))
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1" "Email")
|
||||
(input :type "text" :name "email" :value email
|
||||
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm"))
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1" "Role")
|
||||
(input :type "text" :name "role" :value role
|
||||
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm"))
|
||||
(div :class "flex gap-2"
|
||||
(button :type "submit"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
"Save All (PUT)")
|
||||
(button :type "button"
|
||||
:sx-get "/examples/api/putpatch/cancel"
|
||||
:sx-target "#pp-target"
|
||||
:sx-swap "innerHTML"
|
||||
:class "px-4 py-2 bg-stone-200 text-stone-700 rounded text-sm hover:bg-stone-300"
|
||||
"Cancel"))))
|
||||
|
||||
;; --- JSON encoding demo ---
|
||||
|
||||
(defcomp ~json-encoding-demo ()
|
||||
(div :class "space-y-4"
|
||||
(form
|
||||
:sx-post "/examples/api/json-echo"
|
||||
:sx-target "#json-result"
|
||||
:sx-swap "innerHTML"
|
||||
:sx-encoding "json"
|
||||
:class "space-y-3"
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1" "Name")
|
||||
(input :type "text" :name "name" :value "Ada Lovelace"
|
||||
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm"))
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1" "Age")
|
||||
(input :type "number" :name "age" :value "36"
|
||||
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm"))
|
||||
(button :type "submit"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Submit as JSON"))
|
||||
(div :id "json-result" :class "p-3 rounded bg-stone-50 text-stone-500 text-sm"
|
||||
"Submit the form to see the server echo the parsed JSON.")))
|
||||
|
||||
(defcomp ~json-result (&key body content-type)
|
||||
(div :class "space-y-2"
|
||||
(p :class "text-stone-800 font-medium" "Server received:")
|
||||
(pre :class "text-sm bg-stone-100 p-3 rounded overflow-x-auto" (code body))
|
||||
(p :class "text-sm text-stone-500" (str "Content-Type: " content-type))))
|
||||
|
||||
;; --- Vals & Headers demo ---
|
||||
|
||||
(defcomp ~vals-headers-demo ()
|
||||
(div :class "space-y-6"
|
||||
(div :class "space-y-2"
|
||||
(h4 :class "text-sm font-semibold text-stone-700" "sx-vals — send extra values")
|
||||
(button
|
||||
:sx-get "/examples/api/echo-vals"
|
||||
:sx-target "#vals-result"
|
||||
:sx-swap "innerHTML"
|
||||
:sx-vals "{\"source\": \"button\", \"version\": \"2.0\"}"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Send with vals")
|
||||
(div :id "vals-result" :class "p-3 rounded bg-stone-50 text-sm text-stone-400"
|
||||
"Click to see server-received values."))
|
||||
(div :class "space-y-2"
|
||||
(h4 :class "text-sm font-semibold text-stone-700" "sx-headers — send custom headers")
|
||||
(button
|
||||
:sx-get "/examples/api/echo-headers"
|
||||
:sx-target "#headers-result"
|
||||
:sx-swap "innerHTML"
|
||||
:sx-headers "{\"X-Custom-Token\": \"abc123\", \"X-Request-Source\": \"demo\"}"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Send with headers")
|
||||
(div :id "headers-result" :class "p-3 rounded bg-stone-50 text-sm text-stone-400"
|
||||
"Click to see server-received headers."))))
|
||||
|
||||
(defcomp ~echo-result (&key label items)
|
||||
(div :class "space-y-1"
|
||||
(p :class "text-stone-800 font-medium" (str "Server received " label ":"))
|
||||
(map (fn (item)
|
||||
(div :class "text-sm text-stone-600 font-mono" item))
|
||||
items)))
|
||||
|
||||
;; --- Loading states demo ---
|
||||
|
||||
(defcomp ~loading-states-demo ()
|
||||
(div :class "space-y-4"
|
||||
(style ".sx-loading-btn.sx-request { opacity: 0.7; pointer-events: none; } .sx-loading-btn.sx-request .sx-spinner { display: inline-block; } .sx-loading-btn .sx-spinner { display: none; }")
|
||||
(button
|
||||
:sx-get "/examples/api/slow"
|
||||
:sx-target "#loading-result"
|
||||
:sx-swap "innerHTML"
|
||||
:class "sx-loading-btn px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm flex items-center gap-2"
|
||||
(span :class "sx-spinner w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin")
|
||||
(span "Load slow endpoint"))
|
||||
(div :id "loading-result" :class "p-4 rounded border border-stone-200 bg-white text-center"
|
||||
(p :class "text-stone-400 text-sm" "Click the button — it takes 2 seconds."))))
|
||||
|
||||
(defcomp ~loading-result (&key time)
|
||||
(div
|
||||
(p :class "text-stone-800 font-medium" "Loaded!")
|
||||
(p :class "text-sm text-stone-500" (str "Response arrived at " time))))
|
||||
|
||||
;; --- Sync replace demo (request abort) ---
|
||||
|
||||
(defcomp ~sync-replace-demo ()
|
||||
(div :class "space-y-3"
|
||||
(input :type "text" :name "q"
|
||||
:sx-get "/examples/api/slow-search"
|
||||
:sx-trigger "keyup delay:200ms changed"
|
||||
:sx-target "#sync-result"
|
||||
:sx-swap "innerHTML"
|
||||
:sx-sync "replace"
|
||||
:placeholder "Type to search (random delay 0.5-2s)..."
|
||||
:class "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500")
|
||||
(div :id "sync-result" :class "p-4 rounded border border-stone-200 bg-white"
|
||||
(p :class "text-sm text-stone-400" "Type to trigger requests — stale ones get aborted."))))
|
||||
|
||||
(defcomp ~sync-result (&key query delay)
|
||||
(div
|
||||
(p :class "text-stone-800 font-medium" (str "Result for: \"" query "\""))
|
||||
(p :class "text-sm text-stone-500" (str "Server took " delay "ms to respond"))))
|
||||
|
||||
;; --- Retry demo ---
|
||||
|
||||
(defcomp ~retry-demo ()
|
||||
(div :class "space-y-4"
|
||||
(button
|
||||
:sx-get "/examples/api/flaky"
|
||||
:sx-target "#retry-result"
|
||||
:sx-swap "innerHTML"
|
||||
:sx-retry "exponential:1000:8000"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Call flaky endpoint")
|
||||
(div :id "retry-result" :class "p-4 rounded border border-stone-200 bg-white text-center"
|
||||
(p :class "text-stone-400 text-sm" "Endpoint fails twice, succeeds on 3rd attempt."))))
|
||||
|
||||
(defcomp ~retry-result (&key attempt message)
|
||||
(div :class "space-y-1"
|
||||
(p :class "text-stone-800 font-medium" message)
|
||||
(p :class "text-sm text-stone-500" (str "Succeeded on attempt #" attempt))))
|
||||
|
||||
@@ -901,6 +901,27 @@ def _examples_content_sx(slug: str) -> str:
|
||||
"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)()
|
||||
|
||||
@@ -1121,6 +1142,802 @@ def _example_oob_swaps_sx() -> str:
|
||||
)
|
||||
|
||||
|
||||
def _example_lazy_loading_sx() -> str:
|
||||
c_sx = _example_code('(div\n'
|
||||
' :sx-get "/examples/api/lazy"\n'
|
||||
' :sx-trigger "load"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' (div :class "animate-pulse" "Loading..."))')
|
||||
c_handler = _example_code('@bp.get("/examples/api/lazy")\n'
|
||||
'async def api_lazy():\n'
|
||||
' now = datetime.now().strftime(...)\n'
|
||||
' return sx_response(\n'
|
||||
' f\'(~lazy-result :time "{now}")\')',
|
||||
language="python")
|
||||
return (
|
||||
f'(~doc-page :title "Lazy Loading"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "Use sx-trigger=\\"load\\" to fetch content as soon as the element enters the DOM. '
|
||||
f'Great for deferring expensive content below the fold.")'
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Content loads automatically when the page renders."'
|
||||
f' (~example-demo (~lazy-loading-demo)))'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||
f' {c_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||
f' {_placeholder("lazy-comp")}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||
f' {c_handler}'
|
||||
f' (div :class "flex items-center justify-between mt-6"'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||
f' {_clear_components_btn()})'
|
||||
f' {_placeholder("lazy-wire")})'
|
||||
)
|
||||
|
||||
|
||||
def _example_infinite_scroll_sx() -> str:
|
||||
c_sx = _example_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...")')
|
||||
c_handler = _example_code('@bp.get("/examples/api/scroll")\n'
|
||||
'async 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)',
|
||||
language="python")
|
||||
return (
|
||||
f'(~doc-page :title "Infinite Scroll"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "A sentinel element at the bottom uses sx-trigger=\\"intersect once\\" '
|
||||
f'to load the next page when scrolled into view. Each response appends items and a new sentinel.")'
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Scroll down in the container to load more items (5 pages total)."'
|
||||
f' (~example-demo (~infinite-scroll-demo)))'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||
f' {c_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||
f' {_placeholder("scroll-comp")}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||
f' {c_handler}'
|
||||
f' (div :class "flex items-center justify-between mt-6"'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||
f' {_clear_components_btn()})'
|
||||
f' {_placeholder("scroll-wire")})'
|
||||
)
|
||||
|
||||
|
||||
def _example_progress_bar_sx() -> str:
|
||||
c_sx = _example_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")')
|
||||
c_handler = _example_code('@bp.post("/examples/api/progress/start")\n'
|
||||
'async 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}")\')',
|
||||
language="python")
|
||||
return (
|
||||
f'(~doc-page :title "Progress Bar"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "Start a server-side job, then poll for progress using sx-trigger=\\"load delay:500ms\\" on each response. '
|
||||
f'The bar fills up and stops when complete.")'
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Click start to begin a simulated job."'
|
||||
f' (~example-demo (~progress-bar-demo)))'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||
f' {c_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||
f' {_placeholder("progress-comp")}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||
f' {c_handler}'
|
||||
f' (div :class "flex items-center justify-between mt-6"'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||
f' {_clear_components_btn()})'
|
||||
f' {_placeholder("progress-wire")})'
|
||||
)
|
||||
|
||||
|
||||
def _example_active_search_sx() -> str:
|
||||
c_sx = _example_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...")')
|
||||
c_handler = _example_code('@bp.get("/examples/api/search")\n'
|
||||
'async 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}")\')',
|
||||
language="python")
|
||||
return (
|
||||
f'(~doc-page :title "Active Search"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "An input with sx-trigger=\\"keyup delay:300ms changed\\" debounces keystrokes and only fires when the value changes. '
|
||||
f'The server filters a list of programming languages.")'
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Type to search through 20 programming languages."'
|
||||
f' (~example-demo (~active-search-demo)))'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||
f' {c_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||
f' {_placeholder("search-comp")}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||
f' {c_handler}'
|
||||
f' (div :class "flex items-center justify-between mt-6"'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||
f' {_clear_components_btn()})'
|
||||
f' {_placeholder("search-wire")})'
|
||||
)
|
||||
|
||||
|
||||
def _example_inline_validation_sx() -> str:
|
||||
c_sx = _example_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")')
|
||||
c_handler = _example_code('@bp.get("/examples/api/validate")\n'
|
||||
'async 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 ...)\')',
|
||||
language="python")
|
||||
return (
|
||||
f'(~doc-page :title "Inline Validation"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "Validate an email field on blur. The server checks format and whether it is taken, '
|
||||
f'returning green or red feedback inline.")'
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Enter an email and click away (blur) to validate."'
|
||||
f' (~example-demo (~inline-validation-demo)))'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||
f' {c_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||
f' {_placeholder("validate-comp")}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||
f' {c_handler}'
|
||||
f' (div :class "flex items-center justify-between mt-6"'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||
f' {_clear_components_btn()})'
|
||||
f' {_placeholder("validate-wire")})'
|
||||
)
|
||||
|
||||
|
||||
def _example_value_select_sx() -> str:
|
||||
c_sx = _example_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"))')
|
||||
c_handler = _example_code('@bp.get("/examples/api/values")\n'
|
||||
'async 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 ...))\')',
|
||||
language="python")
|
||||
return (
|
||||
f'(~doc-page :title "Value Select"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "Two linked selects: pick a category and the second select updates with matching items via sx-get.")'
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Select a category to populate the item dropdown."'
|
||||
f' (~example-demo (~value-select-demo)))'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||
f' {c_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||
f' {_placeholder("values-comp")}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||
f' {c_handler}'
|
||||
f' (div :class "flex items-center justify-between mt-6"'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||
f' {_clear_components_btn()})'
|
||||
f' {_placeholder("values-wire")})'
|
||||
)
|
||||
|
||||
|
||||
def _example_reset_on_submit_sx() -> str:
|
||||
c_sx = _example_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"))')
|
||||
c_handler = _example_code('@bp.post("/examples/api/reset-submit")\n'
|
||||
'async 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 "...")\')',
|
||||
language="python")
|
||||
return (
|
||||
f'(~doc-page :title "Reset on Submit"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "Use sx-on:afterSwap=\\"this.reset()\\" to clear form inputs after a successful submission.")'
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Submit a message — the input resets after each send."'
|
||||
f' (~example-demo (~reset-on-submit-demo)))'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||
f' {c_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||
f' {_placeholder("reset-comp")}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||
f' {c_handler}'
|
||||
f' (div :class "flex items-center justify-between mt-6"'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||
f' {_clear_components_btn()})'
|
||||
f' {_placeholder("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
|
||||
)
|
||||
c_sx = _example_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")')
|
||||
c_handler = _example_code('@bp.get("/examples/api/editrow/<id>")\n'
|
||||
'async 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>")\n'
|
||||
'async def api_editrow_save(id):\n'
|
||||
' form = await request.form\n'
|
||||
' return sx_response(\n'
|
||||
' f\'(~edit-row-view :id ... :name ...)\')',
|
||||
language="python")
|
||||
return (
|
||||
f'(~doc-page :title "Edit Row"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "Click edit to replace a table row with input fields. Save or cancel swaps back the display row. '
|
||||
f'Uses sx-include to gather form values from the row.")'
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Click edit on any row to modify it inline."'
|
||||
f' (~example-demo (~edit-row-demo :rows (list {rows_sx}))))'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||
f' {c_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||
f' {_placeholder("editrow-comp")}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||
f' {c_handler}'
|
||||
f' (div :class "flex items-center justify-between mt-6"'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||
f' {_clear_components_btn()})'
|
||||
f' {_placeholder("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
|
||||
)
|
||||
c_sx = _example_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")')
|
||||
c_handler = _example_code('@bp.post("/examples/api/bulk")\n'
|
||||
'async 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)',
|
||||
language="python")
|
||||
return (
|
||||
f'(~doc-page :title "Bulk Update"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "Select rows with checkboxes and use Activate/Deactivate buttons. '
|
||||
f'sx-include gathers checkbox values from the form.")'
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Check some rows, then click Activate or Deactivate."'
|
||||
f' (~example-demo (~bulk-update-demo :users (list {users_sx}))))'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||
f' {c_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||
f' {_placeholder("bulk-comp")}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||
f' {c_handler}'
|
||||
f' (div :class "flex items-center justify-between mt-6"'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||
f' {_clear_components_btn()})'
|
||||
f' {_placeholder("bulk-wire")})'
|
||||
)
|
||||
|
||||
|
||||
def _example_swap_positions_sx() -> str:
|
||||
c_sx = _example_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")')
|
||||
c_handler = _example_code('@bp.post("/examples/api/swap-log")\n'
|
||||
'async 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)',
|
||||
language="python")
|
||||
return (
|
||||
f'(~doc-page :title "Swap Positions"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "Demonstrates different swap modes: beforeend appends, afterbegin prepends, '
|
||||
f'and none skips the main swap while still processing OOB updates.")'
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Try each button to see different swap behaviours."'
|
||||
f' (~example-demo (~swap-positions-demo)))'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||
f' {c_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||
f' {c_handler}'
|
||||
f' (div :class "flex items-center justify-between mt-6"'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||
f' {_clear_components_btn()})'
|
||||
f' {_placeholder("swap-wire")})'
|
||||
)
|
||||
|
||||
|
||||
def _example_select_filter_sx() -> str:
|
||||
c_sx = _example_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")')
|
||||
c_handler = _example_code('@bp.get("/examples/api/dashboard")\n'
|
||||
'async 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" ...))\')',
|
||||
language="python")
|
||||
return (
|
||||
f'(~doc-page :title "Select Filter"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "sx-select lets the client pick a specific section from the server response by CSS selector. '
|
||||
f'The server always returns the full dashboard — the client filters.")'
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Different buttons select different parts of the same server response."'
|
||||
f' (~example-demo (~select-filter-demo)))'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||
f' {c_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||
f' {c_handler}'
|
||||
f' (div :class "flex items-center justify-between mt-6"'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||
f' {_clear_components_btn()})'
|
||||
f' {_placeholder("filter-wire")})'
|
||||
)
|
||||
|
||||
|
||||
def _example_tabs_sx() -> str:
|
||||
c_sx = _example_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")')
|
||||
c_handler = _example_code('@bp.get("/examples/api/tabs/<tab>")\n'
|
||||
'async def api_tabs(tab: str):\n'
|
||||
' content = TAB_CONTENT[tab]\n'
|
||||
' return sx_response(content)',
|
||||
language="python")
|
||||
return (
|
||||
f'(~doc-page :title "Tabs"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "Tab navigation using sx-push-url to update the browser URL. '
|
||||
f'Back/forward buttons navigate between previously visited tabs.")'
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Click tabs to switch content. Watch the browser URL change."'
|
||||
f' (~example-demo (~tabs-demo)))'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||
f' {c_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||
f' {c_handler}'
|
||||
f' (div :class "flex items-center justify-between mt-6"'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||
f' {_clear_components_btn()})'
|
||||
f' {_placeholder("tabs-wire")})'
|
||||
)
|
||||
|
||||
|
||||
def _example_animations_sx() -> str:
|
||||
c_sx = _example_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!")))')
|
||||
c_handler = _example_code('@bp.get("/examples/api/animate")\n'
|
||||
'async 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}")\')',
|
||||
language="python")
|
||||
return (
|
||||
f'(~doc-page :title "Animations"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "CSS animations play on swap. The component injects a style tag with a keyframe animation '
|
||||
f'and applies the class. Each click picks a random background colour.")'
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Click to swap in content with a fade-in animation."'
|
||||
f' (~example-demo (~animations-demo)))'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||
f' {c_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||
f' {_placeholder("anim-comp")}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||
f' {c_handler}'
|
||||
f' (div :class "flex items-center justify-between mt-6"'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||
f' {_clear_components_btn()})'
|
||||
f' {_placeholder("anim-wire")})'
|
||||
)
|
||||
|
||||
|
||||
def _example_dialogs_sx() -> str:
|
||||
c_sx = _example_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")')
|
||||
c_handler = _example_code('@bp.get("/examples/api/dialog")\n'
|
||||
'async 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")\n'
|
||||
'async def api_dialog_close():\n'
|
||||
' return sx_response("")',
|
||||
language="python")
|
||||
return (
|
||||
f'(~doc-page :title "Dialogs"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "Open a modal dialog by swapping in the dialog component. Close by swapping in empty content. '
|
||||
f'Pure sx — no JavaScript library needed.")'
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Click to open a modal dialog."'
|
||||
f' (~example-demo (~dialogs-demo)))'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||
f' {c_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||
f' {_placeholder("dialog-comp")}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||
f' {c_handler}'
|
||||
f' (div :class "flex items-center justify-between mt-6"'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||
f' {_clear_components_btn()})'
|
||||
f' {_placeholder("dialog-wire")})'
|
||||
)
|
||||
|
||||
|
||||
def _example_keyboard_shortcuts_sx() -> str:
|
||||
c_sx = _example_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...")')
|
||||
c_handler = _example_code('@bp.get("/examples/api/keyboard")\n'
|
||||
'async 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]}")\')',
|
||||
language="python")
|
||||
return (
|
||||
f'(~doc-page :title "Keyboard Shortcuts"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "Use sx-trigger with keyup event filters and from:body to listen for global keyboard shortcuts. '
|
||||
f'The filter prevents firing when typing in inputs.")'
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Press s, n, or h on your keyboard."'
|
||||
f' (~example-demo (~keyboard-shortcuts-demo)))'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||
f' {c_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||
f' {_placeholder("kbd-comp")}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||
f' {c_handler}'
|
||||
f' (div :class "flex items-center justify-between mt-6"'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||
f' {_clear_components_btn()})'
|
||||
f' {_placeholder("kbd-wire")})'
|
||||
)
|
||||
|
||||
|
||||
def _example_put_patch_sx() -> str:
|
||||
from content.pages import PROFILE_DEFAULT
|
||||
c_sx = _example_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)"))')
|
||||
c_handler = _example_code('@bp.put("/examples/api/putpatch")\n'
|
||||
'async def api_put():\n'
|
||||
' form = await request.form\n'
|
||||
' # Full replacement\n'
|
||||
' return sx_response(\'(~pp-view ...)\')',
|
||||
language="python")
|
||||
n, e, r = PROFILE_DEFAULT["name"], PROFILE_DEFAULT["email"], PROFILE_DEFAULT["role"]
|
||||
return (
|
||||
f'(~doc-page :title "PUT / PATCH"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "sx-put replaces the entire resource. This example shows a profile card with an Edit All button '
|
||||
f'that sends a PUT with all fields.")'
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Click Edit All to replace the full profile via PUT."'
|
||||
f' (~example-demo (~put-patch-demo :name "{n}" :email "{e}" :role "{r}")))'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||
f' {c_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||
f' {_placeholder("pp-comp")}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||
f' {c_handler}'
|
||||
f' (div :class "flex items-center justify-between mt-6"'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||
f' {_clear_components_btn()})'
|
||||
f' {_placeholder("pp-wire")})'
|
||||
)
|
||||
|
||||
|
||||
def _example_json_encoding_sx() -> str:
|
||||
c_sx = _example_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"))')
|
||||
c_handler = _example_code('@bp.post("/examples/api/json-echo")\n'
|
||||
'async 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}")\')',
|
||||
language="python")
|
||||
return (
|
||||
f'(~doc-page :title "JSON Encoding"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "Use sx-encoding=\\"json\\" to send form data as a JSON body instead of URL-encoded form data. '
|
||||
f'The server echoes back what it received.")'
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Submit the form and see the JSON body the server received."'
|
||||
f' (~example-demo (~json-encoding-demo)))'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||
f' {c_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||
f' {_placeholder("json-comp")}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||
f' {c_handler}'
|
||||
f' (div :class "flex items-center justify-between mt-6"'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||
f' {_clear_components_btn()})'
|
||||
f' {_placeholder("json-wire")})'
|
||||
)
|
||||
|
||||
|
||||
def _example_vals_and_headers_sx() -> str:
|
||||
c_sx = _example_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")')
|
||||
c_handler = _example_code('@bp.get("/examples/api/echo-vals")\n'
|
||||
'async 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")\n'
|
||||
'async 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 (...))\')',
|
||||
language="python")
|
||||
return (
|
||||
f'(~doc-page :title "Vals & Headers"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "sx-vals adds extra key/value pairs to the request parameters. '
|
||||
f'sx-headers adds custom HTTP headers. The server echoes back what it received.")'
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Click each button to see what the server receives."'
|
||||
f' (~example-demo (~vals-headers-demo)))'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||
f' {c_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||
f' {_placeholder("vals-comp")}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||
f' {c_handler}'
|
||||
f' (div :class "flex items-center justify-between mt-6"'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||
f' {_clear_components_btn()})'
|
||||
f' {_placeholder("vals-wire")})'
|
||||
)
|
||||
|
||||
|
||||
def _example_loading_states_sx() -> str:
|
||||
c_sx = _example_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")')
|
||||
c_handler = _example_code('@bp.get("/examples/api/slow")\n'
|
||||
'async def api_slow():\n'
|
||||
' await asyncio.sleep(2)\n'
|
||||
' return sx_response(\n'
|
||||
' f\'(~loading-result :time "{now}")\')',
|
||||
language="python")
|
||||
return (
|
||||
f'(~doc-page :title "Loading States"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "sx.js adds the .sx-request CSS class to any element that has an active request. '
|
||||
f'Use pure CSS to show spinners, disable buttons, or change opacity during loading.")'
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Click the button — it shows a spinner during the 2-second request."'
|
||||
f' (~example-demo (~loading-states-demo)))'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||
f' {c_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||
f' {_placeholder("loading-comp")}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||
f' {c_handler}'
|
||||
f' (div :class "flex items-center justify-between mt-6"'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||
f' {_clear_components_btn()})'
|
||||
f' {_placeholder("loading-wire")})'
|
||||
)
|
||||
|
||||
|
||||
def _example_sync_replace_sx() -> str:
|
||||
c_sx = _example_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...")')
|
||||
c_handler = _example_code('@bp.get("/examples/api/slow-search")\n'
|
||||
'async 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}")\')',
|
||||
language="python")
|
||||
return (
|
||||
f'(~doc-page :title "Request Abort"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "sx-sync=\\"replace\\" aborts any in-flight request before sending a new one. '
|
||||
f'This prevents stale responses from overwriting newer ones, even with random server delays.")'
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Type quickly — only the latest result appears despite random 0.5-2s server delays."'
|
||||
f' (~example-demo (~sync-replace-demo)))'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||
f' {c_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||
f' {_placeholder("sync-comp")}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||
f' {c_handler}'
|
||||
f' (div :class "flex items-center justify-between mt-6"'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||
f' {_clear_components_btn()})'
|
||||
f' {_placeholder("sync-wire")})'
|
||||
)
|
||||
|
||||
|
||||
def _example_retry_sx() -> str:
|
||||
c_sx = _example_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")')
|
||||
c_handler = _example_code('@bp.get("/examples/api/flaky")\n'
|
||||
'async 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} ...)\')',
|
||||
language="python")
|
||||
return (
|
||||
f'(~doc-page :title "Retry"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
f' "sx-retry=\\"exponential:1000:8000\\" retries failed requests with exponential backoff '
|
||||
f'starting at 1s up to 8s. The endpoint fails the first 2 attempts and succeeds on the 3rd.")'
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Click the button — watch it retry automatically after failures."'
|
||||
f' (~example-demo (~retry-demo)))'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||
f' {c_sx}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||
f' {_placeholder("retry-comp")}'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||
f' {c_handler}'
|
||||
f' (div :class "flex items-center justify-between mt-6"'
|
||||
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||
f' {_clear_components_btn()})'
|
||||
f' {_placeholder("retry-wire")})'
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Essays
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user