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

- 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:
2026-03-03 10:23:33 +00:00
parent 0af07f9f2e
commit a8c0741f54
8 changed files with 1194 additions and 5 deletions

169
sx/sxc/pages/__init__.py Normal file
View 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
View 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))