Add SX editor to post edit page, prevent sx_content clearing on save
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m5s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m5s
- Add sx_content to _post_to_edit_dict so edit page receives existing content - Add SX/Koenig editor tabs, sx-editor mount point, and SxEditor.mount init - Only pass sx_content to writer_update when form field is present (prevents accidental clearing when editing via Koenig-only path) - Add csrf_exempt to example API POST/DELETE/PUT demo endpoints - Add defpage infrastructure (pages.py, layouts.py) and sx docs page definitions - Add defhandler definitions for example API handlers (examples.sx) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from quart import Blueprint, Response, make_response, request
|
||||
from shared.browser.app.csrf import csrf_exempt
|
||||
|
||||
|
||||
def register(url_prefix: str = "/") -> Blueprint:
|
||||
@@ -145,6 +146,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
oob_comp = _oob_code("click-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@csrf_exempt
|
||||
@bp.post("/examples/api/form")
|
||||
async def api_form():
|
||||
from shared.sx.helpers import sx_response
|
||||
@@ -175,6 +177,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
oob_comp = _oob_code("poll-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@csrf_exempt
|
||||
@bp.delete("/examples/api/delete/<item_id>")
|
||||
async def api_delete(item_id: str):
|
||||
from shared.sx.helpers import sx_response
|
||||
@@ -200,6 +203,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
oob_comp = _oob_code("edit-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@csrf_exempt
|
||||
@bp.post("/examples/api/edit")
|
||||
async def api_edit_save():
|
||||
from shared.sx.helpers import sx_response
|
||||
@@ -295,6 +299,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
|
||||
_jobs: dict[str, int] = {}
|
||||
|
||||
@csrf_exempt
|
||||
@bp.post("/examples/api/progress/start")
|
||||
async def api_progress_start():
|
||||
from shared.sx.helpers import sx_response
|
||||
@@ -373,6 +378,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
oob_comp = _oob_code("validate-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@csrf_exempt
|
||||
@bp.post("/examples/api/validate/submit")
|
||||
async def api_validate_submit():
|
||||
from shared.sx.helpers import sx_response
|
||||
@@ -402,6 +408,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
|
||||
# --- Reset on Submit ---
|
||||
|
||||
@csrf_exempt
|
||||
@bp.post("/examples/api/reset-submit")
|
||||
async def api_reset_submit():
|
||||
from shared.sx.helpers import sx_response
|
||||
@@ -442,6 +449,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
oob_comp = _oob_code("editrow-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@csrf_exempt
|
||||
@bp.post("/examples/api/editrow/<row_id>")
|
||||
async def api_editrow_save(row_id: str):
|
||||
from shared.sx.helpers import sx_response
|
||||
@@ -488,6 +496,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
_bulk_users[u["id"]] = dict(u)
|
||||
return _bulk_users
|
||||
|
||||
@csrf_exempt
|
||||
@bp.post("/examples/api/bulk")
|
||||
async def api_bulk():
|
||||
from shared.sx.helpers import sx_response
|
||||
@@ -517,6 +526,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
|
||||
_swap_count = {"n": 0}
|
||||
|
||||
@csrf_exempt
|
||||
@bp.post("/examples/api/swap-log")
|
||||
async def api_swap_log():
|
||||
from shared.sx.helpers import sx_response
|
||||
@@ -682,6 +692,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
oob_comp = _oob_code("pp-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@csrf_exempt
|
||||
@bp.put("/examples/api/putpatch")
|
||||
async def api_pp_put():
|
||||
from shared.sx.helpers import sx_response
|
||||
@@ -712,6 +723,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
|
||||
# --- JSON Encoding ---
|
||||
|
||||
@csrf_exempt
|
||||
@bp.post("/examples/api/json-echo")
|
||||
async def api_json_echo():
|
||||
from shared.sx.helpers import sx_response
|
||||
|
||||
367
sx/sxc/handlers/examples.sx
Normal file
367
sx/sxc/handlers/examples.sx
Normal file
@@ -0,0 +1,367 @@
|
||||
;; SX example API handlers — defhandler definitions
|
||||
;;
|
||||
;; These serve the live demos on the Examples docs pages.
|
||||
;; Each handler's source is displayed in the "Server handler" code block
|
||||
;; on its corresponding example page (self-referencing via handler-source).
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Click to Load
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler click (&key)
|
||||
(let ((now (format-time (now) "%H:%M:%S")))
|
||||
(~click-result :time now)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Form Submission
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler form (&key)
|
||||
(let ((name (form-data "name")))
|
||||
(~form-result :name name)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Polling
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler poll (&key)
|
||||
(let ((now (format-time (now) "%H:%M:%S"))
|
||||
(count (inc-counter "poll" :max 10)))
|
||||
(~poll-result :time now :count count)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Delete Row
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler delete (&key item-id)
|
||||
;; Empty response — outerHTML swap removes the row
|
||||
"")
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Inline Edit
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler edit-form (&key)
|
||||
(let ((value (request-arg "value")))
|
||||
(~inline-edit-form :value value)))
|
||||
|
||||
(defhandler edit-save (&key)
|
||||
(let ((value (form-data "value")))
|
||||
(~inline-view :value value)))
|
||||
|
||||
(defhandler edit-cancel (&key)
|
||||
(let ((value (request-arg "value")))
|
||||
(~inline-view :value value)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Out-of-Band Swaps
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler oob (&key)
|
||||
(let ((now (format-time (now) "%H:%M:%S")))
|
||||
(<>
|
||||
(p :class "text-emerald-600 font-medium" "Box A updated!")
|
||||
(p :class "text-sm text-stone-500" "at " now)
|
||||
(div :id "oob-box-b" :sx-swap-oob "innerHTML"
|
||||
(p :class "text-violet-600 font-medium" "Box B updated via OOB!")
|
||||
(p :class "text-sm text-stone-500" "at " now)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Lazy Loading
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler lazy (&key)
|
||||
(let ((now (format-time (now) "%H:%M:%S")))
|
||||
(~lazy-result :time now)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Infinite Scroll
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler scroll (&key)
|
||||
(let ((page (or (parse-int (request-arg "page")) 2))
|
||||
(start (+ (* (- page 1) 5) 1))
|
||||
(next (+ page 1)))
|
||||
(<>
|
||||
(map (fn (i)
|
||||
(div :class "px-4 py-3 border-b border-stone-100 text-sm text-stone-700"
|
||||
"Item " i " — loaded from page " page))
|
||||
(range start (+ start 5)))
|
||||
(if (<= next 6)
|
||||
(div :id "scroll-sentinel"
|
||||
:sx-get (str "/examples/api/scroll?page=" next)
|
||||
:sx-trigger "intersect once"
|
||||
:sx-target "#scroll-items"
|
||||
:sx-swap "beforeend"
|
||||
:class "p-3 text-center text-stone-400 text-sm"
|
||||
"Loading more...")
|
||||
(div :class "p-3 text-center text-stone-500 text-sm font-medium"
|
||||
"All items loaded.")))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Progress Bar
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler progress-start (&key)
|
||||
(let ((job-id (new-job)))
|
||||
(~progress-status :percent 0 :job-id job-id)))
|
||||
|
||||
(defhandler progress-status (&key)
|
||||
(let ((job-id (request-arg "job"))
|
||||
(percent (advance-job job-id)))
|
||||
(~progress-status :percent percent :job-id job-id)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Active Search
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler search (&key)
|
||||
(let ((q (request-arg "q"))
|
||||
(results (filter-list LANGUAGES q)))
|
||||
(~search-results :items results :query q)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Inline Validation
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler validate (&key)
|
||||
(let ((email (request-arg "email")))
|
||||
(cond
|
||||
((not email)
|
||||
(~validation-error :message "Email is required"))
|
||||
((not (contains? email "@"))
|
||||
(~validation-error :message "Invalid email format"))
|
||||
((contains? TAKEN_EMAILS (lower email))
|
||||
(~validation-error
|
||||
:message (str email " is already taken")))
|
||||
(t (~validation-ok :email email)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Value Select
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler values (&key)
|
||||
(let ((cat (request-arg "category"))
|
||||
(items (get VALUE_SELECT_DATA cat)))
|
||||
(if (empty? items)
|
||||
(option :value "" "No items")
|
||||
(map (fn (i) (option :value i i)) items))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Reset on Submit
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler reset-submit (&key)
|
||||
(let ((msg (or (form-data "message") "(empty)"))
|
||||
(now (format-time (now) "%H:%M:%S")))
|
||||
(~reset-message :message msg :time now)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Edit Row
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler editrow-form (&key row-id)
|
||||
(let ((row (get ROWS row-id)))
|
||||
(~edit-row-form :id row-id
|
||||
:name (get row "name")
|
||||
:price (get row "price")
|
||||
:stock (get row "stock"))))
|
||||
|
||||
(defhandler editrow-save (&key row-id)
|
||||
(let ((name (form-data "name"))
|
||||
(price (form-data "price"))
|
||||
(stock (form-data "stock")))
|
||||
(~edit-row-view :id row-id
|
||||
:name name :price price :stock stock)))
|
||||
|
||||
(defhandler editrow-cancel (&key row-id)
|
||||
(let ((row (get ROWS row-id)))
|
||||
(~edit-row-view :id row-id
|
||||
:name (get row "name")
|
||||
:price (get row "price")
|
||||
:stock (get row "stock"))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Bulk Update
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler bulk (&key)
|
||||
(let ((action (request-arg "action"))
|
||||
(ids (form-list "ids"))
|
||||
(status (if (= action "activate")
|
||||
"active" "inactive")))
|
||||
(update-users ids :status status)
|
||||
(map (fn (u)
|
||||
(~bulk-row
|
||||
:id (get u "id")
|
||||
:name (get u "name")
|
||||
:email (get u "email")
|
||||
:status (get u "status")))
|
||||
USERS)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Swap Positions
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler swap-log (&key)
|
||||
(let ((mode (request-arg "mode"))
|
||||
(n (inc-counter "swap"))
|
||||
(now (format-time (now) "%H:%M:%S")))
|
||||
(<>
|
||||
(div :class "px-3 py-2 text-sm text-stone-700"
|
||||
"[" now "] " mode " (#" n ")")
|
||||
(span :id "swap-counter"
|
||||
:sx-swap-oob "innerHTML"
|
||||
:class "self-center text-sm text-stone-500"
|
||||
"Count: " n))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Select Filter (Dashboard)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler dashboard (&key)
|
||||
(let ((now (format-time (now) "%H:%M:%S")))
|
||||
(<>
|
||||
(div :id "dash-header" :class "p-3 bg-violet-50 rounded mb-3"
|
||||
(h4 :class "font-semibold text-violet-800" "Dashboard Header")
|
||||
(p :class "text-sm text-violet-600" "Generated at " now))
|
||||
(div :id "dash-stats" :class "grid grid-cols-3 gap-3 mb-3"
|
||||
(div :class "p-3 bg-emerald-50 rounded text-center"
|
||||
(p :class "text-2xl font-bold text-emerald-700" "142")
|
||||
(p :class "text-xs text-emerald-600" "Users"))
|
||||
(div :class "p-3 bg-blue-50 rounded text-center"
|
||||
(p :class "text-2xl font-bold text-blue-700" "89")
|
||||
(p :class "text-xs text-blue-600" "Orders"))
|
||||
(div :class "p-3 bg-amber-50 rounded text-center"
|
||||
(p :class "text-2xl font-bold text-amber-700" "$4.2k")
|
||||
(p :class "text-xs text-amber-600" "Revenue")))
|
||||
(div :id "dash-footer" :class "p-3 bg-stone-50 rounded"
|
||||
(p :class "text-sm text-stone-500" "Last updated: " now)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Tabs
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler tabs (&key tab)
|
||||
(let ((content (get TAB_CONTENT tab)))
|
||||
(<> content
|
||||
(div :id "tab-buttons"
|
||||
:sx-swap-oob "innerHTML"
|
||||
:class "flex border-b border-stone-200"
|
||||
(map (fn (t)
|
||||
(~tab-btn
|
||||
:tab (first t)
|
||||
:label (last t)
|
||||
:active (if (= (first t) tab) "true" "false")))
|
||||
TAB_LIST)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Animations
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler animate (&key)
|
||||
(let ((color (random-choice
|
||||
"bg-violet-100" "bg-emerald-100"
|
||||
"bg-blue-100" "bg-amber-100"))
|
||||
(now (format-time (now) "%H:%M:%S")))
|
||||
(~anim-result :color color :time now)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Dialogs
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler dialog (&key)
|
||||
(~dialog-modal
|
||||
:title "Confirm Action"
|
||||
:message "Are you sure you want to proceed?"))
|
||||
|
||||
(defhandler dialog-close (&key)
|
||||
"")
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Keyboard Shortcuts
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler keyboard (&key)
|
||||
(let ((key (request-arg "key"))
|
||||
(actions {:s "Search panel activated"
|
||||
:n "New item created"
|
||||
:h "Help panel opened"})
|
||||
(action (get actions key)))
|
||||
(~kbd-result :key key :action action)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; PUT / PATCH
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler pp-edit-all (&key)
|
||||
(let ((p (get-profile)))
|
||||
(~pp-form-full
|
||||
:name (get p "name")
|
||||
:email (get p "email")
|
||||
:role (get p "role"))))
|
||||
|
||||
(defhandler put-profile (&key)
|
||||
(let ((name (form-data "name"))
|
||||
(email (form-data "email"))
|
||||
(role (form-data "role")))
|
||||
(~pp-view :name name :email email :role role)))
|
||||
|
||||
(defhandler pp-cancel (&key)
|
||||
(let ((p (get-profile)))
|
||||
(~pp-view
|
||||
:name (get p "name")
|
||||
:email (get p "email")
|
||||
:role (get p "role"))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; JSON Encoding
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler json-echo (&key)
|
||||
(let ((data (request-json))
|
||||
(body (json-pretty data))
|
||||
(ct (request-header "content-type")))
|
||||
(~json-result :body body :content-type ct)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Vals & Headers
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler echo-vals (&key)
|
||||
(let ((vals (request-args)))
|
||||
(~echo-result :label "values" :items vals)))
|
||||
|
||||
(defhandler echo-headers (&key)
|
||||
(let ((headers (request-headers :prefix "X-")))
|
||||
(~echo-result :label "headers" :items headers)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Loading States
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler slow (&key)
|
||||
(sleep 2000)
|
||||
(let ((now (format-time (now) "%H:%M:%S")))
|
||||
(~loading-result :time now)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Request Abort (sync replace)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler slow-search (&key)
|
||||
(let ((delay (random-int 500 2000)))
|
||||
(sleep delay)
|
||||
(let ((q (request-arg "q")))
|
||||
(~sync-result :query q :delay delay))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Retry
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler flaky (&key)
|
||||
(let ((n (inc-counter "flaky")))
|
||||
(if (!= (mod n 3) 0)
|
||||
(error 503)
|
||||
(~retry-result :attempt n
|
||||
:message "Success! The endpoint finally responded."))))
|
||||
169
sx/sxc/pages/__init__.py
Normal file
169
sx/sxc/pages/__init__.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""SX docs defpage setup — registers layouts and page helpers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def setup_sx_pages() -> None:
|
||||
"""Register sx-specific layouts, page helpers, and load page definitions.
|
||||
|
||||
Called during app startup before mount_pages().
|
||||
"""
|
||||
_register_sx_layouts()
|
||||
_register_sx_helpers()
|
||||
_load_sx_page_files()
|
||||
|
||||
|
||||
def _load_sx_page_files() -> None:
|
||||
"""Load defpage definitions from sx/sxc/pages/*.sx."""
|
||||
import os
|
||||
from shared.sx.pages import load_page_dir
|
||||
pages_dir = os.path.dirname(__file__)
|
||||
load_page_dir(pages_dir, "sx")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Layouts
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _register_sx_layouts() -> None:
|
||||
"""Register the sx docs layout presets."""
|
||||
from shared.sx.layouts import register_custom_layout
|
||||
|
||||
register_custom_layout("sx", _sx_full_headers, _sx_oob_headers)
|
||||
register_custom_layout("sx-section", _sx_section_full_headers, _sx_section_oob_headers)
|
||||
|
||||
|
||||
def _sx_full_headers(ctx: dict, **kw: Any) -> str:
|
||||
"""Full headers for sx home page: root + sx menu row."""
|
||||
from shared.sx.helpers import root_header_sx
|
||||
from sxc.sx_components import _sx_header_sx, _main_nav_sx
|
||||
|
||||
main_nav = _main_nav_sx(kw.get("section"))
|
||||
root_hdr = root_header_sx(ctx)
|
||||
sx_row = _sx_header_sx(main_nav)
|
||||
return "(<> " + root_hdr + " " + sx_row + ")"
|
||||
|
||||
|
||||
def _sx_oob_headers(ctx: dict, **kw: Any) -> str:
|
||||
"""OOB headers for sx home page."""
|
||||
from shared.sx.helpers import root_header_sx, oob_header_sx
|
||||
from sxc.sx_components import _sx_header_sx, _main_nav_sx
|
||||
|
||||
root_hdr = root_header_sx(ctx)
|
||||
main_nav = _main_nav_sx(kw.get("section"))
|
||||
sx_row = _sx_header_sx(main_nav)
|
||||
rows = "(<> " + root_hdr + " " + sx_row + ")"
|
||||
return oob_header_sx("root-header-child", "sx-header-child", rows)
|
||||
|
||||
|
||||
def _sx_section_full_headers(ctx: dict, **kw: Any) -> str:
|
||||
"""Full headers for sx section pages: root + sx row + sub row."""
|
||||
from shared.sx.helpers import root_header_sx
|
||||
from sxc.sx_components import (
|
||||
_sx_header_sx, _main_nav_sx, _sub_row_sx,
|
||||
)
|
||||
|
||||
section = kw.get("section", "")
|
||||
sub_label = kw.get("sub_label", section)
|
||||
sub_href = kw.get("sub_href", "/")
|
||||
sub_nav = kw.get("sub_nav", "")
|
||||
selected = kw.get("selected", "")
|
||||
|
||||
root_hdr = root_header_sx(ctx)
|
||||
main_nav = _main_nav_sx(section)
|
||||
sub_row = _sub_row_sx(sub_label, sub_href, sub_nav, selected)
|
||||
sx_row = _sx_header_sx(main_nav, child=sub_row)
|
||||
return "(<> " + root_hdr + " " + sx_row + ")"
|
||||
|
||||
|
||||
def _sx_section_oob_headers(ctx: dict, **kw: Any) -> str:
|
||||
"""OOB headers for sx section pages."""
|
||||
from shared.sx.helpers import root_header_sx, oob_header_sx
|
||||
from sxc.sx_components import (
|
||||
_sx_header_sx, _main_nav_sx, _sub_row_sx,
|
||||
)
|
||||
|
||||
section = kw.get("section", "")
|
||||
sub_label = kw.get("sub_label", section)
|
||||
sub_href = kw.get("sub_href", "/")
|
||||
sub_nav = kw.get("sub_nav", "")
|
||||
selected = kw.get("selected", "")
|
||||
|
||||
root_hdr = root_header_sx(ctx)
|
||||
main_nav = _main_nav_sx(section)
|
||||
sub_row = _sub_row_sx(sub_label, sub_href, sub_nav, selected)
|
||||
sx_row = _sx_header_sx(main_nav, child=sub_row)
|
||||
rows = "(<> " + root_hdr + " " + sx_row + ")"
|
||||
return oob_header_sx("root-header-child", "sx-header-child", rows)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Page helpers — Python functions callable from defpage content expressions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _register_sx_helpers() -> None:
|
||||
"""Register Python content builder functions as page helpers."""
|
||||
from shared.sx.pages import register_page_helpers
|
||||
from sxc.sx_components import (
|
||||
_docs_content_sx, _reference_content_sx,
|
||||
_protocol_content_sx, _examples_content_sx,
|
||||
_essay_content_sx,
|
||||
_docs_nav_sx, _reference_nav_sx,
|
||||
_protocols_nav_sx, _examples_nav_sx, _essays_nav_sx,
|
||||
)
|
||||
from content.highlight import highlight as _highlight
|
||||
from content.pages import (
|
||||
DOCS_NAV, REFERENCE_NAV, PROTOCOLS_NAV,
|
||||
EXAMPLES_NAV, ESSAYS_NAV,
|
||||
)
|
||||
|
||||
def _find_current(nav_list, slug, match_fn=None):
|
||||
"""Find the current nav label for a slug."""
|
||||
if match_fn:
|
||||
return match_fn(nav_list, slug)
|
||||
for label, href in nav_list:
|
||||
if href.endswith(slug):
|
||||
return label
|
||||
return None
|
||||
|
||||
def _home_content():
|
||||
"""Build home page content (uses highlight for hero code block)."""
|
||||
hero_code = _highlight(
|
||||
'(div :class "p-4 bg-white rounded shadow"\n'
|
||||
' (h1 :class "text-2xl font-bold" "Hello")\n'
|
||||
' (button :sx-get "/api/data"\n'
|
||||
' :sx-target "#result"\n'
|
||||
' "Load data"))', "lisp")
|
||||
return (
|
||||
f'(div :id "main-content"'
|
||||
f' (~sx-hero {hero_code})'
|
||||
f' (~sx-philosophy)'
|
||||
f' (~sx-how-it-works)'
|
||||
f' (~sx-credits))'
|
||||
)
|
||||
|
||||
register_page_helpers("sx", {
|
||||
# Content builders
|
||||
"home-content": _home_content,
|
||||
"docs-content": _docs_content_sx,
|
||||
"reference-content": _reference_content_sx,
|
||||
"protocol-content": _protocol_content_sx,
|
||||
"examples-content": _examples_content_sx,
|
||||
"essay-content": _essay_content_sx,
|
||||
"highlight": _highlight,
|
||||
# Nav builders
|
||||
"docs-nav": _docs_nav_sx,
|
||||
"reference-nav": _reference_nav_sx,
|
||||
"protocols-nav": _protocols_nav_sx,
|
||||
"examples-nav": _examples_nav_sx,
|
||||
"essays-nav": _essays_nav_sx,
|
||||
# Nav data (for current label lookup)
|
||||
"DOCS_NAV": DOCS_NAV,
|
||||
"REFERENCE_NAV": REFERENCE_NAV,
|
||||
"PROTOCOLS_NAV": PROTOCOLS_NAV,
|
||||
"EXAMPLES_NAV": EXAMPLES_NAV,
|
||||
"ESSAYS_NAV": ESSAYS_NAV,
|
||||
# Utility
|
||||
"find-current": _find_current,
|
||||
})
|
||||
98
sx/sxc/pages/docs.sx
Normal file
98
sx/sxc/pages/docs.sx
Normal file
@@ -0,0 +1,98 @@
|
||||
;; SX docs app — declarative page definitions
|
||||
;; These replace the GET route handlers in routes.py
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Home page
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defpage home
|
||||
:path "/"
|
||||
:auth :public
|
||||
:layout :sx
|
||||
:content (home-content))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Docs section
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defpage docs-page
|
||||
:path "/docs/<slug>"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "Docs"
|
||||
:sub-label "Docs"
|
||||
:sub-href "/docs/introduction"
|
||||
:sub-nav (docs-nav (find-current DOCS_NAV slug))
|
||||
:selected (or (find-current DOCS_NAV slug) ""))
|
||||
:content (docs-content slug))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Reference section
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defpage reference-index
|
||||
:path "/reference/"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "Reference"
|
||||
:sub-label "Reference"
|
||||
:sub-href "/reference/"
|
||||
:sub-nav (reference-nav "Attributes")
|
||||
:selected "Attributes")
|
||||
:content (reference-content ""))
|
||||
|
||||
(defpage reference-page
|
||||
:path "/reference/<slug>"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "Reference"
|
||||
:sub-label "Reference"
|
||||
:sub-href "/reference/"
|
||||
:sub-nav (reference-nav (find-current REFERENCE_NAV slug))
|
||||
:selected (or (find-current REFERENCE_NAV slug) ""))
|
||||
:content (reference-content slug))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Protocols section
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defpage protocol-page
|
||||
:path "/protocols/<slug>"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "Protocols"
|
||||
:sub-label "Protocols"
|
||||
:sub-href "/protocols/wire-format"
|
||||
:sub-nav (protocols-nav (find-current PROTOCOLS_NAV slug))
|
||||
:selected (or (find-current PROTOCOLS_NAV slug) ""))
|
||||
:content (protocol-content slug))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Examples section
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defpage examples-page
|
||||
:path "/examples/<slug>"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "Examples"
|
||||
:sub-label "Examples"
|
||||
:sub-href "/examples/click-to-load"
|
||||
:sub-nav (examples-nav (find-current EXAMPLES_NAV slug))
|
||||
:selected (or (find-current EXAMPLES_NAV slug) ""))
|
||||
:content (examples-content slug))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Essays section
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defpage essay-page
|
||||
:path "/essays/<slug>"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "Essays"
|
||||
:sub-label "Essays"
|
||||
:sub-href "/essays/sx-sucks"
|
||||
:sub-nav (essays-nav (find-current ESSAYS_NAV slug))
|
||||
:selected (or (find-current ESSAYS_NAV slug) ""))
|
||||
:content (essay-content slug))
|
||||
Reference in New Issue
Block a user