Add OOB header swaps for sx docs navigation + enable OAuth + fragments
- OOB nav updates: AJAX navigation now swaps both menu bar levels (main nav highlighting + sub-nav with current page) using the same oob_header_sx/oob_page_sx pattern as blog/market/events - Enable OAuth for sx and test apps (removed from _NO_OAUTH, added sx to ALLOWED_CLIENTS, added app_urls for sx/test/orders) - Fetch real cross-service fragments (cart-mini, auth-menu, nav-tree) instead of hardcoding empty values - Add :selected param to ~menu-row-sx for white text current-page label - Fix duplicate element IDs: use menu-row-sx child_id/child mechanism instead of manual header_child_sx wrappers - Fix home page copy: "Server-rendered DOM over the wire (no HTML)" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,9 @@ app_urls:
|
|||||||
events: "https://events.rose-ash.com"
|
events: "https://events.rose-ash.com"
|
||||||
federation: "https://federation.rose-ash.com"
|
federation: "https://federation.rose-ash.com"
|
||||||
account: "https://account.rose-ash.com"
|
account: "https://account.rose-ash.com"
|
||||||
|
sx: "https://sx.rose-ash.com"
|
||||||
|
test: "https://test.rose-ash.com"
|
||||||
|
orders: "https://orders.rose-ash.com"
|
||||||
cache:
|
cache:
|
||||||
fs_root: /app/_snapshot # <- absolute path to your snapshot dir
|
fs_root: /app/_snapshot # <- absolute path to your snapshot dir
|
||||||
categories:
|
categories:
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ from .services import (
|
|||||||
SESSION_USER_KEY = "uid"
|
SESSION_USER_KEY = "uid"
|
||||||
ACCOUNT_SESSION_KEY = "account_sid"
|
ACCOUNT_SESSION_KEY = "account_sid"
|
||||||
|
|
||||||
ALLOWED_CLIENTS = {"blog", "market", "cart", "events", "federation", "orders", "test", "artdag", "artdag_l2"}
|
ALLOWED_CLIENTS = {"blog", "market", "cart", "events", "federation", "orders", "test", "sx", "artdag", "artdag_l2"}
|
||||||
|
|
||||||
|
|
||||||
def register(url_prefix="/auth"):
|
def register(url_prefix="/auth"):
|
||||||
|
|||||||
@@ -145,8 +145,8 @@ def create_base_app(
|
|||||||
errors(app)
|
errors(app)
|
||||||
|
|
||||||
# Auto-register OAuth client blueprint for non-account apps
|
# Auto-register OAuth client blueprint for non-account apps
|
||||||
# (account is the OAuth authorization server; test/sx are public dashboards)
|
# (account is the OAuth authorization server)
|
||||||
_NO_OAUTH = {"account", "test", "sx"}
|
_NO_OAUTH = {"account"}
|
||||||
if name not in _NO_OAUTH:
|
if name not in _NO_OAUTH:
|
||||||
from shared.infrastructure.oauth import create_oauth_blueprint
|
from shared.infrastructure.oauth import create_oauth_blueprint
|
||||||
app.register_blueprint(create_oauth_blueprint(name))
|
app.register_blueprint(create_oauth_blueprint(name))
|
||||||
|
|||||||
@@ -82,9 +82,9 @@
|
|||||||
(div :class "block md:hidden text-md font-bold"
|
(div :class "block md:hidden text-md font-bold"
|
||||||
(when auth-menu auth-menu))))
|
(when auth-menu auth-menu))))
|
||||||
|
|
||||||
; @css bg-sky-400 bg-sky-300 bg-sky-200 bg-sky-100
|
; @css bg-sky-400 bg-sky-300 bg-sky-200 bg-sky-100 bg-violet-400 bg-violet-300 bg-violet-200 bg-violet-100
|
||||||
(defcomp ~menu-row-sx (&key id level colour link-href link-label link-label-content icon
|
(defcomp ~menu-row-sx (&key id level colour link-href link-label link-label-content icon
|
||||||
hx-select nav child-id child oob external)
|
selected hx-select nav child-id child oob external)
|
||||||
(let* ((c (or colour "sky"))
|
(let* ((c (or colour "sky"))
|
||||||
(lv (or level 1))
|
(lv (or level 1))
|
||||||
(shade (str (- 500 (* lv 100)))))
|
(shade (str (- 500 (* lv 100)))))
|
||||||
@@ -102,7 +102,10 @@
|
|||||||
:class "w-full whitespace-normal flex items-center gap-2 font-bold text-2xl px-3 py-2"
|
:class "w-full whitespace-normal flex items-center gap-2 font-bold text-2xl px-3 py-2"
|
||||||
(when icon (i :class icon :aria-hidden "true"))
|
(when icon (i :class icon :aria-hidden "true"))
|
||||||
(if link-label-content link-label-content
|
(if link-label-content link-label-content
|
||||||
(when link-label (div link-label)))))
|
(<>
|
||||||
|
(when link-label (div link-label))
|
||||||
|
(when selected
|
||||||
|
(span :class "text-lg text-white/80 font-normal" selected))))))
|
||||||
(when nav
|
(when nav
|
||||||
(nav :class "hidden md:flex gap-4 text-sm ml-2 justify-end items-center flex-0"
|
(nav :class "hidden md:flex gap-4 text-sm ml-2 justify-end items-center flex-0"
|
||||||
nav)))
|
nav)))
|
||||||
|
|||||||
37
sx/app.py
37
sx/app.py
@@ -3,28 +3,41 @@ import path_setup # noqa: F401
|
|||||||
import sxc.sx_components as sx_components # noqa: F401
|
import sxc.sx_components as sx_components # noqa: F401
|
||||||
|
|
||||||
from shared.infrastructure.factory import create_base_app
|
from shared.infrastructure.factory import create_base_app
|
||||||
from shared.sx.jinja_bridge import render
|
|
||||||
|
|
||||||
from bp import register_pages
|
from bp import register_pages
|
||||||
from services import register_domain_services
|
from services import register_domain_services
|
||||||
|
|
||||||
|
|
||||||
async def sx_docs_context() -> dict:
|
async def sx_docs_context() -> dict:
|
||||||
"""SX docs app context processor — minimal, no cross-service fragments."""
|
"""SX docs app context processor — fetches cross-service fragments."""
|
||||||
|
from quart import request, g
|
||||||
from shared.infrastructure.context import base_context
|
from shared.infrastructure.context import base_context
|
||||||
|
from shared.infrastructure.cart_identity import current_cart_identity
|
||||||
|
from shared.infrastructure.fragments import fetch_fragments
|
||||||
|
|
||||||
ctx = await base_context()
|
ctx = await base_context()
|
||||||
ctx["menu_items"] = []
|
ctx["menu_items"] = []
|
||||||
blog_url = ctx.get("blog_url", "")
|
|
||||||
if callable(blog_url):
|
ident = current_cart_identity()
|
||||||
blog_url_str = blog_url("")
|
user = getattr(g, "user", None)
|
||||||
else:
|
|
||||||
blog_url_str = str(blog_url or "")
|
cart_params = {}
|
||||||
ctx["cart_mini"] = render(
|
if ident.get("user_id"):
|
||||||
"cart-mini", cart_count=0, blog_url=blog_url_str, cart_url="",
|
cart_params["user_id"] = ident["user_id"]
|
||||||
)
|
if ident.get("session_id"):
|
||||||
ctx["auth_menu"] = ""
|
cart_params["session_id"] = ident["session_id"]
|
||||||
ctx["nav_tree"] = ""
|
|
||||||
|
auth_params = {"email": user.email} if user else None
|
||||||
|
|
||||||
|
cart_mini, auth_menu, nav_tree = await fetch_fragments([
|
||||||
|
("cart", "cart-mini", cart_params or None),
|
||||||
|
("account", "auth-menu", auth_params),
|
||||||
|
("blog", "nav-tree", {"app_name": "sx", "path": request.path}),
|
||||||
|
], required=False)
|
||||||
|
ctx["cart_mini"] = cart_mini
|
||||||
|
ctx["auth_menu"] = auth_menu
|
||||||
|
ctx["nav_tree"] = nav_tree
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ def register(url_prefix: str = "/") -> Blueprint:
|
|||||||
async def index():
|
async def index():
|
||||||
if _is_sx_request():
|
if _is_sx_request():
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
from sxc.sx_components import home_content_sx
|
from sxc.sx_components import home_oob_sx
|
||||||
return sx_response(home_content_sx())
|
return sx_response(await home_oob_sx())
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
from shared.sx.page import get_template_context
|
||||||
from sxc.sx_components import render_home_page_sx
|
from sxc.sx_components import render_home_page_sx
|
||||||
@@ -42,8 +42,8 @@ def register(url_prefix: str = "/") -> Blueprint:
|
|||||||
async def docs_page(slug: str):
|
async def docs_page(slug: str):
|
||||||
if _is_sx_request():
|
if _is_sx_request():
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
from sxc.sx_components import docs_content_partial_sx
|
from sxc.sx_components import docs_oob_sx
|
||||||
return sx_response(docs_content_partial_sx(slug))
|
return sx_response(await docs_oob_sx(slug))
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
from shared.sx.page import get_template_context
|
||||||
from sxc.sx_components import render_docs_page_sx
|
from sxc.sx_components import render_docs_page_sx
|
||||||
@@ -59,8 +59,8 @@ def register(url_prefix: str = "/") -> Blueprint:
|
|||||||
async def reference_index():
|
async def reference_index():
|
||||||
if _is_sx_request():
|
if _is_sx_request():
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
from sxc.sx_components import reference_content_partial_sx
|
from sxc.sx_components import reference_oob_sx
|
||||||
return sx_response(reference_content_partial_sx(""))
|
return sx_response(await reference_oob_sx(""))
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
from shared.sx.page import get_template_context
|
||||||
from sxc.sx_components import render_reference_page_sx
|
from sxc.sx_components import render_reference_page_sx
|
||||||
@@ -72,8 +72,8 @@ def register(url_prefix: str = "/") -> Blueprint:
|
|||||||
async def reference_page(slug: str):
|
async def reference_page(slug: str):
|
||||||
if _is_sx_request():
|
if _is_sx_request():
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
from sxc.sx_components import reference_content_partial_sx
|
from sxc.sx_components import reference_oob_sx
|
||||||
return sx_response(reference_content_partial_sx(slug))
|
return sx_response(await reference_oob_sx(slug))
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
from shared.sx.page import get_template_context
|
||||||
from sxc.sx_components import render_reference_page_sx
|
from sxc.sx_components import render_reference_page_sx
|
||||||
@@ -94,8 +94,8 @@ def register(url_prefix: str = "/") -> Blueprint:
|
|||||||
async def protocol_page(slug: str):
|
async def protocol_page(slug: str):
|
||||||
if _is_sx_request():
|
if _is_sx_request():
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
from sxc.sx_components import protocol_content_partial_sx
|
from sxc.sx_components import protocol_oob_sx
|
||||||
return sx_response(protocol_content_partial_sx(slug))
|
return sx_response(await protocol_oob_sx(slug))
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
from shared.sx.page import get_template_context
|
||||||
from sxc.sx_components import render_protocol_page_sx
|
from sxc.sx_components import render_protocol_page_sx
|
||||||
@@ -116,8 +116,8 @@ def register(url_prefix: str = "/") -> Blueprint:
|
|||||||
async def examples_page(slug: str):
|
async def examples_page(slug: str):
|
||||||
if _is_sx_request():
|
if _is_sx_request():
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
from sxc.sx_components import examples_content_partial_sx
|
from sxc.sx_components import examples_oob_sx
|
||||||
return sx_response(examples_content_partial_sx(slug))
|
return sx_response(await examples_oob_sx(slug))
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
from shared.sx.page import get_template_context
|
||||||
from sxc.sx_components import render_examples_page_sx
|
from sxc.sx_components import render_examples_page_sx
|
||||||
@@ -205,8 +205,8 @@ def register(url_prefix: str = "/") -> Blueprint:
|
|||||||
async def essay_page(slug: str):
|
async def essay_page(slug: str):
|
||||||
if _is_sx_request():
|
if _is_sx_request():
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
from sxc.sx_components import essay_content_partial_sx
|
from sxc.sx_components import essay_oob_sx
|
||||||
return sx_response(essay_content_partial_sx(slug))
|
return sx_response(await essay_oob_sx(slug))
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
from shared.sx.page import get_template_context
|
||||||
from sxc.sx_components import render_essay_page_sx
|
from sxc.sx_components import render_essay_page_sx
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
;; SX docs — home page components
|
;; SX docs — home page components
|
||||||
|
|
||||||
(defcomp ~sx-hero ()
|
(defcomp ~sx-hero (&key &rest children)
|
||||||
(div :class "max-w-4xl mx-auto px-6 py-16 text-center"
|
(div :class "max-w-4xl mx-auto px-6 py-16 text-center"
|
||||||
(h1 :class "text-5xl font-bold text-stone-900 mb-4"
|
(h1 :class "text-5xl font-bold text-stone-900 mb-4"
|
||||||
(span :class "text-violet-600" "sx"))
|
(span :class "text-violet-600" "sx"))
|
||||||
@@ -10,9 +10,7 @@
|
|||||||
"A hypermedia-driven UI engine that combines htmx's server-first philosophy "
|
"A hypermedia-driven UI engine that combines htmx's server-first philosophy "
|
||||||
"with React's component model. All rendered via s-expressions over the wire.")
|
"with React's component model. All rendered via s-expressions over the wire.")
|
||||||
(div :class "bg-stone-50 border border-stone-200 rounded-lg p-6 text-left font-mono text-sm overflow-x-auto"
|
(div :class "bg-stone-50 border border-stone-200 rounded-lg p-6 text-left font-mono text-sm overflow-x-auto"
|
||||||
(pre
|
(pre :class "leading-relaxed" children))))
|
||||||
(code :class "language-lisp"
|
|
||||||
"(defcomp ~greeting (&key name)\n (div :class \"p-4 rounded bg-violet-50\"\n (h2 :class \"text-lg font-bold\"\n (str \"Hello, \" name \"!\"))\n (button\n :sx-get \"/api/greet\"\n :sx-target \"closest div\"\n :sx-swap \"outerHTML\"\n :class \"mt-2 px-4 py-2 bg-violet-600 text-white rounded\"\n \"Refresh\")))")))))
|
|
||||||
|
|
||||||
(defcomp ~sx-philosophy ()
|
(defcomp ~sx-philosophy ()
|
||||||
(div :class "max-w-4xl mx-auto px-6 py-12"
|
(div :class "max-w-4xl mx-auto px-6 py-12"
|
||||||
@@ -21,7 +19,7 @@
|
|||||||
(div :class "space-y-4"
|
(div :class "space-y-4"
|
||||||
(h3 :class "text-xl font-semibold text-violet-700" "From htmx")
|
(h3 :class "text-xl font-semibold text-violet-700" "From htmx")
|
||||||
(ul :class "space-y-2 text-stone-600"
|
(ul :class "space-y-2 text-stone-600"
|
||||||
(li "Server-rendered HTML over the wire")
|
(li "Server-rendered DOM over the wire (no HTML)")
|
||||||
(li "Hypermedia attributes on any element (sx-get, sx-post, ...)")
|
(li "Hypermedia attributes on any element (sx-get, sx-post, ...)")
|
||||||
(li "Target/swap model for partial page updates")
|
(li "Target/swap model for partial page updates")
|
||||||
(li "No client-side routing, no virtual DOM")
|
(li "No client-side routing, no virtual DOM")
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import os
|
|||||||
|
|
||||||
from shared.sx.jinja_bridge import load_sx_dir, watch_sx_dir
|
from shared.sx.jinja_bridge import load_sx_dir, watch_sx_dir
|
||||||
from shared.sx.helpers import (
|
from shared.sx.helpers import (
|
||||||
sx_call, SxExpr,
|
sx_call, SxExpr, get_asset_url,
|
||||||
root_header_sx, full_page_sx, header_child_sx,
|
root_header_sx, full_page_sx,
|
||||||
|
oob_header_sx, oob_page_sx,
|
||||||
)
|
)
|
||||||
|
from content.highlight import highlight
|
||||||
|
|
||||||
# Load .sx components from sxc/ directory (not sx/ to avoid name collision)
|
# Load .sx components from sxc/ directory (not sx/ to avoid name collision)
|
||||||
_sxc_dir = os.path.dirname(__file__)
|
_sxc_dir = os.path.dirname(__file__)
|
||||||
@@ -15,6 +17,23 @@ load_sx_dir(_sxc_dir)
|
|||||||
watch_sx_dir(_sxc_dir)
|
watch_sx_dir(_sxc_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def _full_page(ctx: dict, **kwargs) -> str:
|
||||||
|
"""full_page_sx wrapper."""
|
||||||
|
return full_page_sx(ctx, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def _code(code: str, language: str = "lisp") -> str:
|
||||||
|
"""Build a ~doc-code component with highlighted content."""
|
||||||
|
highlighted = highlight(code, language)
|
||||||
|
return f'(~doc-code {highlighted})'
|
||||||
|
|
||||||
|
|
||||||
|
def _example_code(code: str) -> str:
|
||||||
|
"""Build an ~example-source component with highlighted content."""
|
||||||
|
highlighted = highlight(code, "lisp")
|
||||||
|
return f'(~example-source {highlighted})'
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Navigation helpers
|
# Navigation helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -31,13 +50,14 @@ def _nav_items_sx(items: list[tuple[str, str]], current: str | None = None) -> s
|
|||||||
return "(<> " + " ".join(parts) + ")"
|
return "(<> " + " ".join(parts) + ")"
|
||||||
|
|
||||||
|
|
||||||
def _sx_header_sx(nav: str | None = None) -> str:
|
def _sx_header_sx(nav: str | None = None, *, child: str | None = None) -> str:
|
||||||
"""Build the sx docs menu-row."""
|
"""Build the sx docs menu-row."""
|
||||||
return sx_call("menu-row-sx",
|
return sx_call("menu-row-sx",
|
||||||
id="sx-row", level=1, colour="violet",
|
id="sx-row", level=1, colour="violet",
|
||||||
link_href="/", link_label="sx", icon="fa fa-code",
|
link_href="/", link_label="sx", icon="fa fa-code",
|
||||||
nav=SxExpr(nav) if nav else None,
|
nav=SxExpr(nav) if nav else None,
|
||||||
child_id="sx-header-child",
|
child_id="sx-header-child",
|
||||||
|
child=SxExpr(child) if child else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -74,24 +94,118 @@ def _main_nav_sx(current_section: str | None = None) -> str:
|
|||||||
def _header_stack_sx(ctx: dict, section_nav: str | None = None) -> str:
|
def _header_stack_sx(ctx: dict, section_nav: str | None = None) -> str:
|
||||||
"""Full header stack: root header + sx menu row."""
|
"""Full header stack: root header + sx menu row."""
|
||||||
hdr = root_header_sx(ctx)
|
hdr = root_header_sx(ctx)
|
||||||
inner = _sx_header_sx(section_nav)
|
sx_row = _sx_header_sx(section_nav)
|
||||||
child = header_child_sx(inner)
|
return "(<> " + hdr + " " + sx_row + ")"
|
||||||
return "(<> " + hdr + " " + child + ")"
|
|
||||||
|
|
||||||
|
def _sub_row_sx(sub_label: str, sub_href: str, sub_nav: str,
|
||||||
|
selected: str = "") -> str:
|
||||||
|
"""Build the level-2 sub-section menu-row."""
|
||||||
|
return sx_call("menu-row-sx",
|
||||||
|
id="sx-sub-row", level=2, colour="violet",
|
||||||
|
link_href=sub_href, link_label=sub_label,
|
||||||
|
selected=selected or None,
|
||||||
|
nav=SxExpr(sub_nav),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _section_header_stack_sx(ctx: dict, main_nav: str, sub_nav: str,
|
def _section_header_stack_sx(ctx: dict, main_nav: str, sub_nav: str,
|
||||||
sub_label: str, sub_href: str) -> str:
|
sub_label: str, sub_href: str,
|
||||||
|
selected: str = "") -> str:
|
||||||
"""Header stack with main nav + sub-section nav row."""
|
"""Header stack with main nav + sub-section nav row."""
|
||||||
hdr = root_header_sx(ctx)
|
hdr = root_header_sx(ctx)
|
||||||
|
sub_row = _sub_row_sx(sub_label, sub_href, sub_nav, selected)
|
||||||
|
sx_row = _sx_header_sx(main_nav, child=sub_row)
|
||||||
|
return "(<> " + hdr + " " + sx_row + ")"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# OOB helpers — rebuild header rows for AJAX navigation
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def _section_oob_sx(section: str, sub_label: str, sub_href: str,
|
||||||
|
sub_nav: str, content: str,
|
||||||
|
selected: str = "") -> str:
|
||||||
|
"""Generic OOB response: rebuild both header rows + content."""
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
ctx = await get_template_context()
|
||||||
|
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 + ")"
|
||||||
|
header_oob = oob_header_sx("root-header-child", "sx-header-child", rows)
|
||||||
|
return oob_page_sx(oobs=header_oob, content=content)
|
||||||
|
|
||||||
|
|
||||||
|
async def home_oob_sx() -> str:
|
||||||
|
"""OOB response for home page navigation."""
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
ctx = await get_template_context()
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
main_nav = _main_nav_sx()
|
||||||
sx_row = _sx_header_sx(main_nav)
|
sx_row = _sx_header_sx(main_nav)
|
||||||
sub_row = sx_call("menu-row-sx",
|
rows = "(<> " + root_hdr + " " + sx_row + ")"
|
||||||
id="sx-sub-row", level=2, colour="violet",
|
header_oob = oob_header_sx("root-header-child", "sx-header-child", rows)
|
||||||
link_href=sub_href, link_label=sub_label,
|
hero_code = highlight('(div :class "p-4 bg-white rounded shadow"\n'
|
||||||
nav=SxExpr(sub_nav),
|
' (h1 :class "text-2xl font-bold" "Hello")\n'
|
||||||
|
' (button :sx-get "/api/data"\n'
|
||||||
|
' :sx-target "#result"\n'
|
||||||
|
' "Load data"))', "lisp")
|
||||||
|
content = (
|
||||||
|
f'(div :id "main-content"'
|
||||||
|
f' (~sx-hero {hero_code})'
|
||||||
|
f' (~sx-philosophy)'
|
||||||
|
f' (~sx-how-it-works)'
|
||||||
|
f' (~sx-credits))'
|
||||||
)
|
)
|
||||||
inner = "(<> " + sx_row + " " + header_child_sx(sub_row, id="sx-header-child") + ")"
|
return oob_page_sx(oobs=header_oob, content=content)
|
||||||
child = header_child_sx(inner)
|
|
||||||
return "(<> " + hdr + " " + child + ")"
|
|
||||||
|
async def docs_oob_sx(slug: str) -> str:
|
||||||
|
"""OOB response for docs section navigation."""
|
||||||
|
from content.pages import DOCS_NAV
|
||||||
|
current = next((label for label, href in DOCS_NAV if href.endswith(slug)), None)
|
||||||
|
sub_nav = _docs_nav_sx(current)
|
||||||
|
return await _section_oob_sx("Docs", "Docs", "/docs/introduction", sub_nav,
|
||||||
|
_docs_content_sx(slug), selected=current or "")
|
||||||
|
|
||||||
|
|
||||||
|
async def reference_oob_sx(slug: str) -> str:
|
||||||
|
"""OOB response for reference section navigation."""
|
||||||
|
from content.pages import REFERENCE_NAV
|
||||||
|
current = next((label for label, href in REFERENCE_NAV
|
||||||
|
if href.rstrip("/").endswith(slug or "reference")), "Attributes")
|
||||||
|
sub_nav = _reference_nav_sx(current)
|
||||||
|
return await _section_oob_sx("Reference", "Reference", "/reference/", sub_nav,
|
||||||
|
_reference_content_sx(slug), selected=current or "")
|
||||||
|
|
||||||
|
|
||||||
|
async def protocol_oob_sx(slug: str) -> str:
|
||||||
|
"""OOB response for protocols section navigation."""
|
||||||
|
from content.pages import PROTOCOLS_NAV
|
||||||
|
current = next((label for label, href in PROTOCOLS_NAV if href.endswith(slug)), None)
|
||||||
|
sub_nav = _protocols_nav_sx(current)
|
||||||
|
return await _section_oob_sx("Protocols", "Protocols", "/protocols/wire-format", sub_nav,
|
||||||
|
_protocol_content_sx(slug), selected=current or "")
|
||||||
|
|
||||||
|
|
||||||
|
async def examples_oob_sx(slug: str) -> str:
|
||||||
|
"""OOB response for examples section navigation."""
|
||||||
|
from content.pages import EXAMPLES_NAV
|
||||||
|
current = next((label for label, href in EXAMPLES_NAV if href.endswith(slug)), None)
|
||||||
|
sub_nav = _examples_nav_sx(current)
|
||||||
|
return await _section_oob_sx("Examples", "Examples", "/examples/click-to-load", sub_nav,
|
||||||
|
_examples_content_sx(slug), selected=current or "")
|
||||||
|
|
||||||
|
|
||||||
|
async def essay_oob_sx(slug: str) -> str:
|
||||||
|
"""OOB response for essays section navigation."""
|
||||||
|
from content.pages import ESSAYS_NAV
|
||||||
|
current = next((label for label, href in ESSAYS_NAV if href.endswith(slug)), None)
|
||||||
|
sub_nav = _essays_nav_sx(current)
|
||||||
|
return await _section_oob_sx("Essays", "Essays", "/essays/sx-sucks", sub_nav,
|
||||||
|
_essay_content_sx(slug), selected=current or "")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -169,14 +283,19 @@ async def render_home_page_sx(ctx: dict) -> str:
|
|||||||
"""Full page: home."""
|
"""Full page: home."""
|
||||||
main_nav = _main_nav_sx()
|
main_nav = _main_nav_sx()
|
||||||
hdr = _header_stack_sx(ctx, main_nav)
|
hdr = _header_stack_sx(ctx, main_nav)
|
||||||
|
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")
|
||||||
content = (
|
content = (
|
||||||
'(div :id "main-content"'
|
f'(div :id "main-content"'
|
||||||
' (~sx-hero)'
|
f' (~sx-hero {hero_code})'
|
||||||
' (~sx-philosophy)'
|
f' (~sx-philosophy)'
|
||||||
' (~sx-how-it-works)'
|
f' (~sx-how-it-works)'
|
||||||
' (~sx-credits))'
|
f' (~sx-credits))'
|
||||||
)
|
)
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content)
|
return _full_page(ctx, header_rows=hdr, content=content)
|
||||||
|
|
||||||
|
|
||||||
async def render_docs_page_sx(ctx: dict, slug: str) -> str:
|
async def render_docs_page_sx(ctx: dict, slug: str) -> str:
|
||||||
@@ -185,9 +304,10 @@ async def render_docs_page_sx(ctx: dict, slug: str) -> str:
|
|||||||
current = next((label for label, href in DOCS_NAV if href.endswith(slug)), None)
|
current = next((label for label, href in DOCS_NAV if href.endswith(slug)), None)
|
||||||
main_nav = _main_nav_sx("Docs")
|
main_nav = _main_nav_sx("Docs")
|
||||||
sub_nav = _docs_nav_sx(current)
|
sub_nav = _docs_nav_sx(current)
|
||||||
hdr = _section_header_stack_sx(ctx, main_nav, sub_nav, "Docs", "/docs/introduction")
|
hdr = _section_header_stack_sx(ctx, main_nav, sub_nav, "Docs", "/docs/introduction",
|
||||||
|
selected=current or "")
|
||||||
content = _docs_content_sx(slug)
|
content = _docs_content_sx(slug)
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content)
|
return _full_page(ctx, header_rows=hdr, content=content)
|
||||||
|
|
||||||
|
|
||||||
def _docs_content_sx(slug: str) -> str:
|
def _docs_content_sx(slug: str) -> str:
|
||||||
@@ -235,98 +355,92 @@ def _docs_introduction_sx() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _docs_getting_started_sx() -> str:
|
def _docs_getting_started_sx() -> str:
|
||||||
|
c1 = _code('(div :class "p-4 bg-white rounded"\n (h1 :class "text-2xl font-bold" "Hello, world!")\n (p "This is rendered from an s-expression."))')
|
||||||
|
c2 = _code('(button\n :sx-get "/api/data"\n :sx-target "#result"\n :sx-swap "innerHTML"\n "Load data")')
|
||||||
return (
|
return (
|
||||||
'(~doc-page :title "Getting Started"'
|
f'(~doc-page :title "Getting Started"'
|
||||||
' (~doc-section :title "Minimal example" :id "minimal"'
|
f' (~doc-section :title "Minimal example" :id "minimal"'
|
||||||
' (p :class "text-stone-600"'
|
f' (p :class "text-stone-600"'
|
||||||
' "An sx response is s-expression source code with content type text/sx:")'
|
f' "An sx response is s-expression source code with content type text/sx:")'
|
||||||
' (~doc-code :language "lisp" :code'
|
f' {c1}'
|
||||||
' "(div :class \\"p-4 bg-white rounded\\"\\n'
|
f' (p :class "text-stone-600"'
|
||||||
' (h1 :class \\"text-2xl font-bold\\" \\"Hello, world!\\")\\n'
|
f' "Add sx-get to any element to make it fetch and render sx:"))'
|
||||||
' (p \\"This is rendered from an s-expression.\\"))")'
|
f' (~doc-section :title "Hypermedia attributes" :id "attrs"'
|
||||||
' (p :class "text-stone-600"'
|
f' (p :class "text-stone-600"'
|
||||||
' "Add sx-get to any element to make it fetch and render sx:"))'
|
f' "Like htmx, sx adds attributes to HTML elements to trigger HTTP requests:")'
|
||||||
' (~doc-section :title "Hypermedia attributes" :id "attrs"'
|
f' {c2}'
|
||||||
' (p :class "text-stone-600"'
|
f' (p :class "text-stone-600"'
|
||||||
' "Like htmx, sx adds attributes to HTML elements to trigger HTTP requests:")'
|
f' "sx-get, sx-post, sx-put, sx-delete, sx-patch — all work the same way. '
|
||||||
' (~doc-code :language "lisp" :code'
|
f'The response is parsed as sx and rendered into the target element.")))'
|
||||||
' "(button\\n'
|
|
||||||
' :sx-get \\"/api/data\\"\\n'
|
|
||||||
' :sx-target \\"#result\\"\\n'
|
|
||||||
' :sx-swap \\"innerHTML\\"\\n'
|
|
||||||
' \\"Load data\\")")'
|
|
||||||
' (p :class "text-stone-600"'
|
|
||||||
' "sx-get, sx-post, sx-put, sx-delete, sx-patch — all work the same way. '
|
|
||||||
'The response is parsed as sx and rendered into the target element.")))'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _docs_components_sx() -> str:
|
def _docs_components_sx() -> str:
|
||||||
|
c1 = _code('(defcomp ~card (&key title subtitle &rest children)\n'
|
||||||
|
' (div :class "border rounded p-4"\n'
|
||||||
|
' (h2 :class "font-bold" title)\n'
|
||||||
|
' (when subtitle (p :class "text-stone-500" subtitle))\n'
|
||||||
|
' (div :class "mt-3" children)))')
|
||||||
|
c2 = _code('(~card :title "My Card" :subtitle "A description"\n'
|
||||||
|
' (p "First child")\n'
|
||||||
|
' (p "Second child"))')
|
||||||
return (
|
return (
|
||||||
'(~doc-page :title "Components"'
|
f'(~doc-page :title "Components"'
|
||||||
' (~doc-section :title "defcomp" :id "defcomp"'
|
f' (~doc-section :title "defcomp" :id "defcomp"'
|
||||||
' (p :class "text-stone-600"'
|
f' (p :class "text-stone-600"'
|
||||||
' "Components are defined with defcomp. They take keyword parameters and optional children:")'
|
f' "Components are defined with defcomp. They take keyword parameters and optional children:")'
|
||||||
' (~doc-code :language "lisp" :code'
|
f' {c1}'
|
||||||
' "(defcomp ~card (&key title subtitle &rest children)\\n'
|
f' (p :class "text-stone-600"'
|
||||||
' (div :class \\"border rounded p-4\\"\\n'
|
f' "Use components with the ~ prefix:")'
|
||||||
' (h2 :class \\"font-bold\\" title)\\n'
|
f' {c2})'
|
||||||
' (when subtitle (p :class \\"text-stone-500\\" subtitle))\\n'
|
f' (~doc-section :title "Component caching" :id "caching"'
|
||||||
' (div :class \\"mt-3\\" children)))")'
|
f' (p :class "text-stone-600"'
|
||||||
' (p :class "text-stone-600"'
|
f' "Component definitions are sent in a <script type=\\"text/sx\\" data-components> block. '
|
||||||
' "Use components with the ~ prefix:") '
|
f'The client caches them in localStorage keyed by a content hash. '
|
||||||
' (~doc-code :language "lisp" :code'
|
f'On subsequent page loads, the client sends an SX-Components header listing what it has. '
|
||||||
' "(~card :title \\"My Card\\" :subtitle \\"A description\\"\\n'
|
f'The server only sends definitions the client is missing.")'
|
||||||
' (p \\"First child\\")\\n'
|
f' (p :class "text-stone-600"'
|
||||||
' (p \\"Second child\\"))"))'
|
f' "This means the first page load sends all component definitions (~5-15KB). '
|
||||||
' (~doc-section :title "Component caching" :id "caching"'
|
f'Subsequent navigations send zero component bytes — just the page content."))'
|
||||||
' (p :class "text-stone-600"'
|
f' (~doc-section :title "Parameters" :id "params"'
|
||||||
' "Component definitions are sent in a <script type=\\"text/sx\\" data-components> block. '
|
f' (p :class "text-stone-600"'
|
||||||
'The client caches them in localStorage keyed by a content hash. '
|
f' "&key declares keyword parameters. &rest children captures remaining positional arguments. '
|
||||||
'On subsequent page loads, the client sends an SX-Components header listing what it has. '
|
f'Missing parameters evaluate to nil. Components always receive all declared parameters — '
|
||||||
'The server only sends definitions the client is missing.")'
|
f'use (when param ...) or (if param ... ...) to handle optional values.")))'
|
||||||
' (p :class "text-stone-600"'
|
|
||||||
' "This means the first page load sends all component definitions (~5-15KB). '
|
|
||||||
'Subsequent navigations send zero component bytes — just the page content.")) '
|
|
||||||
' (~doc-section :title "Parameters" :id "params"'
|
|
||||||
' (p :class "text-stone-600"'
|
|
||||||
' "&key declares keyword parameters. &rest children captures remaining positional arguments. '
|
|
||||||
'Missing parameters evaluate to nil. Components always receive all declared parameters — '
|
|
||||||
'use (when param ...) or (if param ... ...) to handle optional values.")))'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _docs_evaluator_sx() -> str:
|
def _docs_evaluator_sx() -> str:
|
||||||
|
c1 = _code(';; Conditionals\n'
|
||||||
|
'(if condition then-expr else-expr)\n'
|
||||||
|
'(when condition body...)\n'
|
||||||
|
'(cond (test1 body1) (test2 body2) (else default))\n\n'
|
||||||
|
';; Bindings\n'
|
||||||
|
'(let ((name value) (name2 value2)) body...)\n'
|
||||||
|
'(define name value)\n\n'
|
||||||
|
';; Functions\n'
|
||||||
|
'(lambda (x y) (+ x y))\n'
|
||||||
|
'(fn (x) (* x x))\n\n'
|
||||||
|
';; Sequencing\n'
|
||||||
|
'(do expr1 expr2 expr3)\n'
|
||||||
|
'(begin expr1 expr2)\n\n'
|
||||||
|
';; Threading\n'
|
||||||
|
'(-> value (fn1 arg) (fn2 arg))')
|
||||||
|
c2 = _code('(map (fn (x) (* x 2)) (list 1 2 3)) ;; => (2 4 6)\n'
|
||||||
|
'(filter (fn (x) (> x 2)) (list 1 2 3 4 5)) ;; => (3 4 5)\n'
|
||||||
|
'(reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3)) ;; => 6\n'
|
||||||
|
'(some (fn (x) (> x 3)) (list 1 2 3 4)) ;; => true\n'
|
||||||
|
'(every? (fn (x) (> x 0)) (list 1 2 3)) ;; => true')
|
||||||
return (
|
return (
|
||||||
'(~doc-page :title "Evaluator"'
|
f'(~doc-page :title "Evaluator"'
|
||||||
' (~doc-section :title "Special forms" :id "special"'
|
f' (~doc-section :title "Special forms" :id "special"'
|
||||||
' (p :class "text-stone-600"'
|
f' (p :class "text-stone-600"'
|
||||||
' "Special forms have lazy evaluation — arguments are not evaluated before the form runs:")'
|
f' "Special forms have lazy evaluation — arguments are not evaluated before the form runs:")'
|
||||||
' (~doc-code :language "lisp" :code'
|
f' {c1})'
|
||||||
' ";; Conditionals\\n'
|
f' (~doc-section :title "Higher-order forms" :id "higher"'
|
||||||
'(if condition then-expr else-expr)\\n'
|
f' (p :class "text-stone-600"'
|
||||||
'(when condition body...)\\n'
|
f' "These operate on collections with function arguments:")'
|
||||||
'(cond (test1 body1) (test2 body2) (else default))\\n\\n'
|
f' {c2}))'
|
||||||
';; Bindings\\n'
|
|
||||||
'(let ((name value) (name2 value2)) body...)\\n'
|
|
||||||
'(define name value)\\n\\n'
|
|
||||||
';; Functions\\n'
|
|
||||||
'(lambda (x y) (+ x y))\\n'
|
|
||||||
'(fn (x) (* x x))\\n\\n'
|
|
||||||
';; Sequencing\\n'
|
|
||||||
'(do expr1 expr2 expr3)\\n'
|
|
||||||
'(begin expr1 expr2)\\n\\n'
|
|
||||||
';; Threading\\n'
|
|
||||||
'(-> value (fn1 arg) (fn2 arg))"))'
|
|
||||||
' (~doc-section :title "Higher-order forms" :id "higher"'
|
|
||||||
' (p :class "text-stone-600"'
|
|
||||||
' "These operate on collections with function arguments:")'
|
|
||||||
' (~doc-code :language "lisp" :code'
|
|
||||||
' "(map (fn (x) (* x 2)) (list 1 2 3)) ;; => (2 4 6)\\n'
|
|
||||||
'(filter (fn (x) (> x 2)) (list 1 2 3 4 5)) ;; => (3 4 5)\\n'
|
|
||||||
'(reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3)) ;; => 6\\n'
|
|
||||||
'(some (fn (x) (> x 3)) (list 1 2 3 4)) ;; => true\\n'
|
|
||||||
'(every? (fn (x) (> x 0)) (list 1 2 3)) ;; => true")))'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -343,78 +457,78 @@ def _docs_primitives_sx() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _docs_css_sx() -> str:
|
def _docs_css_sx() -> str:
|
||||||
|
c1 = _code('# First page load:\n'
|
||||||
|
'GET / HTTP/1.1\n\n'
|
||||||
|
'HTTP/1.1 200 OK\n'
|
||||||
|
'Content-Type: text/html\n'
|
||||||
|
'# Full CSS in <style id="sx-css"> + hash in <meta name="sx-css-classes">\n\n'
|
||||||
|
'# Subsequent navigation:\n'
|
||||||
|
'GET /about HTTP/1.1\n'
|
||||||
|
'SX-Css: a1b2c3d4\n\n'
|
||||||
|
'HTTP/1.1 200 OK\n'
|
||||||
|
'Content-Type: text/sx\n'
|
||||||
|
'SX-Css-Hash: e5f6g7h8\n'
|
||||||
|
'SX-Css-Add: bg-blue-500,text-white,rounded-lg\n'
|
||||||
|
'# Only new rules in <style data-sx-css>', "bash")
|
||||||
return (
|
return (
|
||||||
'(~doc-page :title "On-Demand CSS"'
|
f'(~doc-page :title "On-Demand CSS"'
|
||||||
' (~doc-section :title "How it works" :id "how"'
|
f' (~doc-section :title "How it works" :id "how"'
|
||||||
' (p :class "text-stone-600"'
|
f' (p :class "text-stone-600"'
|
||||||
' "sx scans every response for CSS class names used in :class attributes. '
|
f' "sx scans every response for CSS class names used in :class attributes. '
|
||||||
'It looks up only those classes in a pre-parsed Tailwind CSS registry and ships '
|
f'It looks up only those classes in a pre-parsed Tailwind CSS registry and ships '
|
||||||
'just the rules that are needed. No build step. No purging. No unused CSS.")'
|
f'just the rules that are needed. No build step. No purging. No unused CSS.")'
|
||||||
' (p :class "text-stone-600"'
|
f' (p :class "text-stone-600"'
|
||||||
' "On the first page load, the full set of used classes is embedded in a <style> block. '
|
f' "On the first page load, the full set of used classes is embedded in a <style> block. '
|
||||||
'A hash of the class set is stored. On subsequent navigations, the client sends the hash '
|
f'A hash of the class set is stored. On subsequent navigations, the client sends the hash '
|
||||||
'in the SX-Css header. The server computes the diff and sends only new rules via '
|
f'in the SX-Css header. The server computes the diff and sends only new rules via '
|
||||||
'SX-Css-Add and a <style data-sx-css> block."))'
|
f'SX-Css-Add and a <style data-sx-css> block."))'
|
||||||
' (~doc-section :title "The protocol" :id "protocol"'
|
f' (~doc-section :title "The protocol" :id "protocol"'
|
||||||
' (~doc-code :language "bash" :code'
|
f' {c1})'
|
||||||
' "# First page load:\\n'
|
f' (~doc-section :title "Advantages" :id "advantages"'
|
||||||
'GET / HTTP/1.1\\n\\n'
|
f' (ul :class "space-y-2 text-stone-600"'
|
||||||
'HTTP/1.1 200 OK\\n'
|
f' (li "Zero build step — no Tailwind CLI, no PostCSS, no purging")'
|
||||||
'Content-Type: text/html\\n'
|
f' (li "Exact CSS — never ships a rule that isn\'t used on the page")'
|
||||||
'# Full CSS in <style id=\\"sx-css\\"> + hash in <meta name=\\"sx-css-classes\\">\\n\\n'
|
f' (li "Incremental — subsequent navigations only ship new rules")'
|
||||||
'# Subsequent navigation:\\n'
|
f' (li "Component-aware — pre-scans component definitions at registration time")))'
|
||||||
'GET /about HTTP/1.1\\n'
|
f' (~doc-section :title "Disadvantages" :id "disadvantages"'
|
||||||
'SX-Css: a1b2c3d4\\n\\n'
|
f' (ul :class "space-y-2 text-stone-600"'
|
||||||
'HTTP/1.1 200 OK\\n'
|
f' (li "Requires the full Tailwind CSS file loaded in memory at startup (~4MB parsed)")'
|
||||||
'Content-Type: text/sx\\n'
|
f' (li "Regex-based class scanning — can miss dynamically constructed class names")'
|
||||||
'SX-Css-Hash: e5f6g7h8\\n'
|
f' (li "No @apply support — classes must be used directly")'
|
||||||
'SX-Css-Add: bg-blue-500,text-white,rounded-lg\\n'
|
f' (li "Tied to Tailwind\'s utility class naming conventions"))))'
|
||||||
'# Only new rules in <style data-sx-css>"))'
|
|
||||||
' (~doc-section :title "Advantages" :id "advantages"'
|
|
||||||
' (ul :class "space-y-2 text-stone-600"'
|
|
||||||
' (li "Zero build step — no Tailwind CLI, no PostCSS, no purging")'
|
|
||||||
' (li "Exact CSS — never ships a rule that isn\'t used on the page")'
|
|
||||||
' (li "Incremental — subsequent navigations only ship new rules")'
|
|
||||||
' (li "Component-aware — pre-scans component definitions at registration time")))'
|
|
||||||
' (~doc-section :title "Disadvantages" :id "disadvantages"'
|
|
||||||
' (ul :class "space-y-2 text-stone-600"'
|
|
||||||
' (li "Requires the full Tailwind CSS file loaded in memory at startup (~4MB parsed)")'
|
|
||||||
' (li "Regex-based class scanning — can miss dynamically constructed class names")'
|
|
||||||
' (li "No @apply support — classes must be used directly")'
|
|
||||||
' (li "Tied to Tailwind\'s utility class naming conventions"))))'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _docs_server_rendering_sx() -> str:
|
def _docs_server_rendering_sx() -> str:
|
||||||
|
c1 = _code('from shared.sx.helpers import sx_page, sx_response, sx_call\n'
|
||||||
|
'from shared.sx.parser import SxExpr\n\n'
|
||||||
|
'# Build a component call from Python kwargs\n'
|
||||||
|
'sx_call("card", title="Hello", subtitle="World")\n\n'
|
||||||
|
'# Return an sx wire-format response\n'
|
||||||
|
'return sx_response(sx_call("card", title="Hello"))\n\n'
|
||||||
|
'# Return a full HTML page shell with sx boot\n'
|
||||||
|
'return sx_page(ctx, page_sx)', "python")
|
||||||
return (
|
return (
|
||||||
'(~doc-page :title "Server Rendering"'
|
f'(~doc-page :title "Server Rendering"'
|
||||||
' (~doc-section :title "Python API" :id "python"'
|
f' (~doc-section :title "Python API" :id "python"'
|
||||||
' (p :class "text-stone-600"'
|
f' (p :class "text-stone-600"'
|
||||||
' "The server-side sx library provides several entry points for rendering:")'
|
f' "The server-side sx library provides several entry points for rendering:")'
|
||||||
' (~doc-code :language "python" :code'
|
f' {c1})'
|
||||||
' "from shared.sx.helpers import sx_page, sx_response, sx_call\\n'
|
f' (~doc-section :title "sx_call" :id "sx-call"'
|
||||||
'from shared.sx.parser import SxExpr\\n\\n'
|
f' (p :class "text-stone-600"'
|
||||||
'# Build a component call from Python kwargs\\n'
|
f' "sx_call converts Python kwargs to an s-expression component call. '
|
||||||
'sx_call(\\"card\\", title=\\"Hello\\", subtitle=\\"World\\")\\n\\n'
|
f'Snake_case becomes kebab-case. SxExpr values are inlined without quoting. '
|
||||||
'# Return an sx wire-format response\\n'
|
f'None becomes nil. Bools become true/false."))'
|
||||||
'return sx_response(sx_call(\\"card\\", title=\\"Hello\\"))\\n\\n'
|
f' (~doc-section :title "sx_response" :id "sx-response"'
|
||||||
'# Return a full HTML page shell with sx boot\\n'
|
f' (p :class "text-stone-600"'
|
||||||
'return sx_page(ctx, page_sx)"))'
|
f' "sx_response returns a Quart Response with content type text/sx. '
|
||||||
' (~doc-section :title "sx_call" :id "sx-call"'
|
f'It prepends missing component definitions, scans for CSS classes, '
|
||||||
' (p :class "text-stone-600"'
|
f'and sets SX-Css-Hash and SX-Css-Add headers."))'
|
||||||
' "sx_call converts Python kwargs to an s-expression component call. '
|
f' (~doc-section :title "sx_page" :id "sx-page"'
|
||||||
'Snake_case becomes kebab-case. SxExpr values are inlined without quoting. '
|
f' (p :class "text-stone-600"'
|
||||||
'None becomes nil. Bools become true/false."))'
|
f' "sx_page returns a minimal HTML document that boots the page from sx source. '
|
||||||
' (~doc-section :title "sx_response" :id "sx-response"'
|
f'The browser loads component definitions and page sx from inline <script> tags, '
|
||||||
' (p :class "text-stone-600"'
|
f'then sx.js renders everything client-side. CSS rules are pre-scanned and injected.")))'
|
||||||
' "sx_response returns a Quart Response with content type text/sx. '
|
|
||||||
'It prepends missing component definitions, scans for CSS classes, '
|
|
||||||
'and sets SX-Css-Hash and SX-Css-Add headers."))'
|
|
||||||
' (~doc-section :title "sx_page" :id "sx-page"'
|
|
||||||
' (p :class "text-stone-600"'
|
|
||||||
' "sx_page returns a minimal HTML document that boots the page from sx source. '
|
|
||||||
'The browser loads component definitions and page sx from inline <script> tags, '
|
|
||||||
'then sx.js renders everything client-side. CSS rules are pre-scanned and injected.")))'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -429,9 +543,10 @@ async def render_reference_page_sx(ctx: dict, slug: str) -> str:
|
|||||||
if href.rstrip("/").endswith(slug or "reference")), "Attributes")
|
if href.rstrip("/").endswith(slug or "reference")), "Attributes")
|
||||||
main_nav = _main_nav_sx("Reference")
|
main_nav = _main_nav_sx("Reference")
|
||||||
sub_nav = _reference_nav_sx(current)
|
sub_nav = _reference_nav_sx(current)
|
||||||
hdr = _section_header_stack_sx(ctx, main_nav, sub_nav, "Reference", "/reference/")
|
hdr = _section_header_stack_sx(ctx, main_nav, sub_nav, "Reference", "/reference/",
|
||||||
|
selected=current or "")
|
||||||
content = _reference_content_sx(slug)
|
content = _reference_content_sx(slug)
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content)
|
return _full_page(ctx, header_rows=hdr, content=content)
|
||||||
|
|
||||||
|
|
||||||
def _reference_content_sx(slug: str) -> str:
|
def _reference_content_sx(slug: str) -> str:
|
||||||
@@ -526,9 +641,10 @@ async def render_protocol_page_sx(ctx: dict, slug: str) -> str:
|
|||||||
current = next((label for label, href in PROTOCOLS_NAV if href.endswith(slug)), None)
|
current = next((label for label, href in PROTOCOLS_NAV if href.endswith(slug)), None)
|
||||||
main_nav = _main_nav_sx("Protocols")
|
main_nav = _main_nav_sx("Protocols")
|
||||||
sub_nav = _protocols_nav_sx(current)
|
sub_nav = _protocols_nav_sx(current)
|
||||||
hdr = _section_header_stack_sx(ctx, main_nav, sub_nav, "Protocols", "/protocols/wire-format")
|
hdr = _section_header_stack_sx(ctx, main_nav, sub_nav, "Protocols", "/protocols/wire-format",
|
||||||
|
selected=current or "")
|
||||||
content = _protocol_content_sx(slug)
|
content = _protocol_content_sx(slug)
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content)
|
return _full_page(ctx, header_rows=hdr, content=content)
|
||||||
|
|
||||||
|
|
||||||
def _protocol_content_sx(slug: str) -> str:
|
def _protocol_content_sx(slug: str) -> str:
|
||||||
@@ -544,18 +660,18 @@ def _protocol_content_sx(slug: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _protocol_wire_format_sx() -> str:
|
def _protocol_wire_format_sx() -> str:
|
||||||
|
c1 = _code('HTTP/1.1 200 OK\n'
|
||||||
|
'Content-Type: text/sx\n'
|
||||||
|
'SX-Css-Hash: a1b2c3d4\n\n'
|
||||||
|
'(div :class "p-4"\n'
|
||||||
|
' (~card :title "Hello"))', "bash")
|
||||||
return (
|
return (
|
||||||
'(~doc-page :title "Wire Format"'
|
f'(~doc-page :title "Wire Format"'
|
||||||
' (~doc-section :title "The text/sx content type" :id "content-type"'
|
f' (~doc-section :title "The text/sx content type" :id "content-type"'
|
||||||
' (p :class "text-stone-600"'
|
f' (p :class "text-stone-600"'
|
||||||
' "sx responses use content type text/sx. The body is s-expression source code. '
|
f' "sx responses use content type text/sx. The body is s-expression source code. '
|
||||||
'The client parses and evaluates it, then renders the result into the DOM.")'
|
f'The client parses and evaluates it, then renders the result into the DOM.")'
|
||||||
' (~doc-code :language "bash" :code'
|
f' {c1})'
|
||||||
' "HTTP/1.1 200 OK\\n'
|
|
||||||
'Content-Type: text/sx\\n'
|
|
||||||
'SX-Css-Hash: a1b2c3d4\\n\\n'
|
|
||||||
'(div :class \\"p-4\\"\\n'
|
|
||||||
' (~card :title \\"Hello\\"))"))'
|
|
||||||
' (~doc-section :title "Request lifecycle" :id "lifecycle"'
|
' (~doc-section :title "Request lifecycle" :id "lifecycle"'
|
||||||
' (p :class "text-stone-600"'
|
' (p :class "text-stone-600"'
|
||||||
' "1. User interacts with an element that has sx-get/sx-post/etc.")'
|
' "1. User interacts with an element that has sx-get/sx-post/etc.")'
|
||||||
@@ -578,16 +694,16 @@ def _protocol_wire_format_sx() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _protocol_fragments_sx() -> str:
|
def _protocol_fragments_sx() -> str:
|
||||||
|
c1 = _code('(frag "blog" "link-card" :slug "hello")')
|
||||||
return (
|
return (
|
||||||
'(~doc-page :title "Cross-Service Fragments"'
|
f'(~doc-page :title "Cross-Service Fragments"'
|
||||||
' (~doc-section :title "Fragment protocol" :id "protocol"'
|
f' (~doc-section :title "Fragment protocol" :id "protocol"'
|
||||||
' (p :class "text-stone-600"'
|
f' (p :class "text-stone-600"'
|
||||||
' "Rose Ash runs as independent microservices. Each service can expose HTML or sx fragments '
|
f' "Rose Ash runs as independent microservices. Each service can expose HTML or sx fragments '
|
||||||
'that other services compose into their pages. Fragment endpoints return text/sx or text/html.")'
|
f'that other services compose into their pages. Fragment endpoints return text/sx or text/html.")'
|
||||||
' (p :class "text-stone-600"'
|
f' (p :class "text-stone-600"'
|
||||||
' "The frag resolver is an I/O primitive in the render tree:") '
|
f' "The frag resolver is an I/O primitive in the render tree:")'
|
||||||
' (~doc-code :language "lisp" :code'
|
f' {c1})'
|
||||||
' "(frag \\"blog\\" \\"link-card\\" :slug \\"hello\\")"))'
|
|
||||||
' (~doc-section :title "SxExpr wrapping" :id "wrapping"'
|
' (~doc-section :title "SxExpr wrapping" :id "wrapping"'
|
||||||
' (p :class "text-stone-600"'
|
' (p :class "text-stone-600"'
|
||||||
' "When a fragment returns text/sx, the response is wrapped in an SxExpr and embedded directly '
|
' "When a fragment returns text/sx, the response is wrapped in an SxExpr and embedded directly '
|
||||||
@@ -700,9 +816,10 @@ async def render_examples_page_sx(ctx: dict, slug: str) -> str:
|
|||||||
current = next((label for label, href in EXAMPLES_NAV if href.endswith(slug)), None)
|
current = next((label for label, href in EXAMPLES_NAV if href.endswith(slug)), None)
|
||||||
main_nav = _main_nav_sx("Examples")
|
main_nav = _main_nav_sx("Examples")
|
||||||
sub_nav = _examples_nav_sx(current)
|
sub_nav = _examples_nav_sx(current)
|
||||||
hdr = _section_header_stack_sx(ctx, main_nav, sub_nav, "Examples", "/examples/click-to-load")
|
hdr = _section_header_stack_sx(ctx, main_nav, sub_nav, "Examples", "/examples/click-to-load",
|
||||||
|
selected=current or "")
|
||||||
content = _examples_content_sx(slug)
|
content = _examples_content_sx(slug)
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content)
|
return _full_page(ctx, header_rows=hdr, content=content)
|
||||||
|
|
||||||
|
|
||||||
def _examples_content_sx(slug: str) -> str:
|
def _examples_content_sx(slug: str) -> str:
|
||||||
@@ -718,60 +835,66 @@ def _examples_content_sx(slug: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _example_click_to_load_sx() -> str:
|
def _example_click_to_load_sx() -> str:
|
||||||
|
c1 = _example_code('(button\n'
|
||||||
|
' :sx-get "/examples/api/click"\n'
|
||||||
|
' :sx-target "#click-result"\n'
|
||||||
|
' :sx-swap "innerHTML"\n'
|
||||||
|
' "Load content")')
|
||||||
return (
|
return (
|
||||||
'(~doc-page :title "Click to Load"'
|
f'(~doc-page :title "Click to Load"'
|
||||||
' (p :class "text-stone-600 mb-6"'
|
f' (p :class "text-stone-600 mb-6"'
|
||||||
' "The simplest sx interaction: click a button, fetch content from the server, swap it in.")'
|
f' "The simplest sx interaction: click a button, fetch content from the server, swap it in.")'
|
||||||
' (~example-card :title "Demo"'
|
f' (~example-card :title "Demo"'
|
||||||
' :description "Click the button to load server-rendered content."'
|
f' :description "Click the button to load server-rendered content."'
|
||||||
' (~example-demo (~click-to-load-demo)))'
|
f' (~example-demo (~click-to-load-demo)))'
|
||||||
' (~example-source :code'
|
f' {c1})'
|
||||||
' "(button\\n'
|
|
||||||
' :sx-get \\"/examples/api/click\\"\\n'
|
|
||||||
' :sx-target \\"#click-result\\"\\n'
|
|
||||||
' :sx-swap \\"innerHTML\\"\\n'
|
|
||||||
' \\"Load content\\")"))'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _example_form_submission_sx() -> str:
|
def _example_form_submission_sx() -> str:
|
||||||
|
c1 = _example_code('(form\n'
|
||||||
|
' :sx-post "/examples/api/form"\n'
|
||||||
|
' :sx-target "#form-result"\n'
|
||||||
|
' :sx-swap "innerHTML"\n'
|
||||||
|
' (input :type "text" :name "name")\n'
|
||||||
|
' (button :type "submit" "Submit"))')
|
||||||
return (
|
return (
|
||||||
'(~doc-page :title "Form Submission"'
|
f'(~doc-page :title "Form Submission"'
|
||||||
' (p :class "text-stone-600 mb-6"'
|
f' (p :class "text-stone-600 mb-6"'
|
||||||
' "Forms with sx-post submit via AJAX and swap the response into a target.")'
|
f' "Forms with sx-post submit via AJAX and swap the response into a target.")'
|
||||||
' (~example-card :title "Demo"'
|
f' (~example-card :title "Demo"'
|
||||||
' :description "Enter a name and submit."'
|
f' :description "Enter a name and submit."'
|
||||||
' (~example-demo (~form-demo)))'
|
f' (~example-demo (~form-demo)))'
|
||||||
' (~example-source :code'
|
f' {c1})'
|
||||||
' "(form\\n'
|
|
||||||
' :sx-post \\"/examples/api/form\\"\\n'
|
|
||||||
' :sx-target \\"#form-result\\"\\n'
|
|
||||||
' :sx-swap \\"innerHTML\\"\\n'
|
|
||||||
' (input :type \\"text\\" :name \\"name\\")\\n'
|
|
||||||
' (button :type \\"submit\\" \\"Submit\\"))"))'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _example_polling_sx() -> str:
|
def _example_polling_sx() -> str:
|
||||||
|
c1 = _example_code('(div\n'
|
||||||
|
' :sx-get "/examples/api/poll"\n'
|
||||||
|
' :sx-trigger "load, every 2s"\n'
|
||||||
|
' :sx-swap "innerHTML"\n'
|
||||||
|
' "Loading...")')
|
||||||
return (
|
return (
|
||||||
'(~doc-page :title "Polling"'
|
f'(~doc-page :title "Polling"'
|
||||||
' (p :class "text-stone-600 mb-6"'
|
f' (p :class "text-stone-600 mb-6"'
|
||||||
' "Use sx-trigger with \\"every\\\" to poll the server at regular intervals.")'
|
f' "Use sx-trigger with \\"every\\" to poll the server at regular intervals.")'
|
||||||
' (~example-card :title "Demo"'
|
f' (~example-card :title "Demo"'
|
||||||
' :description "This div polls the server every 2 seconds."'
|
f' :description "This div polls the server every 2 seconds."'
|
||||||
' (~example-demo (~polling-demo)))'
|
f' (~example-demo (~polling-demo)))'
|
||||||
' (~example-source :code'
|
f' {c1})'
|
||||||
' "(div\\n'
|
|
||||||
' :sx-get \\"/examples/api/poll\\"\\n'
|
|
||||||
' :sx-trigger \\"load, every 2s\\"\\n'
|
|
||||||
' :sx-swap \\"innerHTML\\"\\n'
|
|
||||||
' \\"Loading...\\")"))'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _example_delete_row_sx() -> str:
|
def _example_delete_row_sx() -> str:
|
||||||
from content.pages import DELETE_DEMO_ITEMS
|
from content.pages import DELETE_DEMO_ITEMS
|
||||||
items_sx = " ".join(f'(list "{id}" "{name}")' for id, name in DELETE_DEMO_ITEMS)
|
items_sx = " ".join(f'(list "{id}" "{name}")' for id, name in DELETE_DEMO_ITEMS)
|
||||||
|
c1 = _example_code('(button\n'
|
||||||
|
' :sx-delete "/api/delete/1"\n'
|
||||||
|
' :sx-target "#row-1"\n'
|
||||||
|
' :sx-swap "outerHTML"\n'
|
||||||
|
' :sx-confirm "Delete this item?"\n'
|
||||||
|
' "delete")')
|
||||||
return (
|
return (
|
||||||
f'(~doc-page :title "Delete Row"'
|
f'(~doc-page :title "Delete Row"'
|
||||||
f' (p :class "text-stone-600 mb-6"'
|
f' (p :class "text-stone-600 mb-6"'
|
||||||
@@ -779,53 +902,47 @@ def _example_delete_row_sx() -> str:
|
|||||||
f' (~example-card :title "Demo"'
|
f' (~example-card :title "Demo"'
|
||||||
f' :description "Click delete to remove a row. Uses sx-confirm for confirmation."'
|
f' :description "Click delete to remove a row. Uses sx-confirm for confirmation."'
|
||||||
f' (~example-demo (~delete-demo :items (list {items_sx}))))'
|
f' (~example-demo (~delete-demo :items (list {items_sx}))))'
|
||||||
f' (~example-source :code'
|
f' {c1})'
|
||||||
f' "(button\\n'
|
|
||||||
f' :sx-delete \\"/api/delete/1\\"\\n'
|
|
||||||
f' :sx-target \\"#row-1\\"\\n'
|
|
||||||
f' :sx-swap \\"outerHTML\\"\\n'
|
|
||||||
f' :sx-confirm \\"Delete this item?\\"\\n'
|
|
||||||
f' \\"delete\\")"))'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _example_inline_edit_sx() -> str:
|
def _example_inline_edit_sx() -> str:
|
||||||
|
c1 = _example_code(';; View mode\n'
|
||||||
|
'(button :sx-get "/api/edit?value=text"\n'
|
||||||
|
' :sx-target "#edit-target" :sx-swap "innerHTML"\n'
|
||||||
|
' "edit")\n\n'
|
||||||
|
';; Edit mode (returned by server)\n'
|
||||||
|
'(form :sx-post "/api/edit"\n'
|
||||||
|
' :sx-target "#edit-target" :sx-swap "innerHTML"\n'
|
||||||
|
' (input :type "text" :name "value")\n'
|
||||||
|
' (button :type "submit" "save"))')
|
||||||
return (
|
return (
|
||||||
'(~doc-page :title "Inline Edit"'
|
f'(~doc-page :title "Inline Edit"'
|
||||||
' (p :class "text-stone-600 mb-6"'
|
f' (p :class "text-stone-600 mb-6"'
|
||||||
' "Click edit to swap a display view for an edit form. Save swaps back.")'
|
f' "Click edit to swap a display view for an edit form. Save swaps back.")'
|
||||||
' (~example-card :title "Demo"'
|
f' (~example-card :title "Demo"'
|
||||||
' :description "Click edit, modify the text, save or cancel."'
|
f' :description "Click edit, modify the text, save or cancel."'
|
||||||
' (~example-demo (~inline-edit-demo)))'
|
f' (~example-demo (~inline-edit-demo)))'
|
||||||
' (~example-source :code'
|
f' {c1})'
|
||||||
' ";; View mode\\n'
|
|
||||||
'(button :sx-get \\"/api/edit?value=text\\"\\n'
|
|
||||||
' :sx-target \\"#edit-target\\" :sx-swap \\"innerHTML\\"\\n'
|
|
||||||
' \\"edit\\")\\n\\n'
|
|
||||||
';; Edit mode (returned by server)\\n'
|
|
||||||
'(form :sx-post \\"/api/edit\\"\\n'
|
|
||||||
' :sx-target \\"#edit-target\\" :sx-swap \\"innerHTML\\"\\n'
|
|
||||||
' (input :type \\"text\\" :name \\"value\\")\\n'
|
|
||||||
' (button :type \\"submit\\" \\"save\\"))"))'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _example_oob_swaps_sx() -> str:
|
def _example_oob_swaps_sx() -> str:
|
||||||
|
c1 = _example_code(';; Response body updates the target (Box A)\n'
|
||||||
|
';; OOB element updates Box B by ID\n\n'
|
||||||
|
'(<>\n'
|
||||||
|
' (div :class "text-center"\n'
|
||||||
|
' (p "Box A updated!")))\n'
|
||||||
|
' (div :id "oob-box-b" :sx-swap-oob "innerHTML"\n'
|
||||||
|
' (p "Box B updated via OOB!")))')
|
||||||
return (
|
return (
|
||||||
'(~doc-page :title "Out-of-Band Swaps"'
|
f'(~doc-page :title "Out-of-Band Swaps"'
|
||||||
' (p :class "text-stone-600 mb-6"'
|
f' (p :class "text-stone-600 mb-6"'
|
||||||
' "sx-swap-oob lets a single response update multiple elements anywhere in the DOM.")'
|
f' "sx-swap-oob lets a single response update multiple elements anywhere in the DOM.")'
|
||||||
' (~example-card :title "Demo"'
|
f' (~example-card :title "Demo"'
|
||||||
' :description "One request updates both Box A (via sx-target) and Box B (via sx-swap-oob)."'
|
f' :description "One request updates both Box A (via sx-target) and Box B (via sx-swap-oob)."'
|
||||||
' (~example-demo (~oob-demo)))'
|
f' (~example-demo (~oob-demo)))'
|
||||||
' (~example-source :code'
|
f' {c1})'
|
||||||
' ";; Response body updates the target (Box A)\\n'
|
|
||||||
';; OOB element updates Box B by ID\\n\\n'
|
|
||||||
'(<>\\n'
|
|
||||||
' (div :class \\"text-center\\"\\n'
|
|
||||||
' (p \\"Box A updated!\\")))\\n'
|
|
||||||
' (div :id \\"oob-box-b\\" :sx-swap-oob \\"innerHTML\\"\\n'
|
|
||||||
' (p \\"Box B updated via OOB!\\"))))"))'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -839,9 +956,10 @@ async def render_essay_page_sx(ctx: dict, slug: str) -> str:
|
|||||||
current = next((label for label, href in ESSAYS_NAV if href.endswith(slug)), None)
|
current = next((label for label, href in ESSAYS_NAV if href.endswith(slug)), None)
|
||||||
main_nav = _main_nav_sx("Essays")
|
main_nav = _main_nav_sx("Essays")
|
||||||
sub_nav = _essays_nav_sx(current)
|
sub_nav = _essays_nav_sx(current)
|
||||||
hdr = _section_header_stack_sx(ctx, main_nav, sub_nav, "Essays", "/essays/sx-sucks")
|
hdr = _section_header_stack_sx(ctx, main_nav, sub_nav, "Essays", "/essays/sx-sucks",
|
||||||
|
selected=current or "")
|
||||||
content = _essay_content_sx(slug)
|
content = _essay_content_sx(slug)
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content)
|
return _full_page(ctx, header_rows=hdr, content=content)
|
||||||
|
|
||||||
|
|
||||||
def _essay_content_sx(slug: str) -> str:
|
def _essay_content_sx(slug: str) -> str:
|
||||||
@@ -878,7 +996,7 @@ def _essay_sx_sucks() -> str:
|
|||||||
'sx looked at this landscape and said: you know what this needs? A Lisp dialect. '
|
'sx looked at this landscape and said: you know what this needs? A Lisp dialect. '
|
||||||
'For HTML. Over HTTP.")'
|
'For HTML. Over HTTP.")'
|
||||||
f' {p}'
|
f' {p}'
|
||||||
' "Nobody was asking for this. The zero GitHub stars confirm it."))'
|
' "Nobody was asking for this. The zero GitHub stars confirm it. It is not even on GitHub."))'
|
||||||
|
|
||||||
' (~doc-section :title "The author has never written a line of LISP" :id "no-lisp"'
|
' (~doc-section :title "The author has never written a line of LISP" :id "no-lisp"'
|
||||||
f' {p}'
|
f' {p}'
|
||||||
@@ -898,6 +1016,11 @@ def _essay_sx_sucks() -> str:
|
|||||||
'This is not artisanal hand-crafted software. This is the software equivalent of '
|
'This is not artisanal hand-crafted software. This is the software equivalent of '
|
||||||
'a microwave dinner presented on a nice plate.")'
|
'a microwave dinner presented on a nice plate.")'
|
||||||
f' {p}'
|
f' {p}'
|
||||||
|
' "He adds features by typing stuff like '
|
||||||
|
'\\"is there rom for macros within sx.js? what benefits m,ight that bring?\\", '
|
||||||
|
'skim-reading the response, and then entering \\"crack on then!\\" '
|
||||||
|
'This is not software engineering. This is improv comedy with a compiler.")'
|
||||||
|
f' {p}'
|
||||||
' "Is that bad? Maybe. Is it honest? Yes. Is this paragraph also AI-generated? '
|
' "Is that bad? Maybe. Is it honest? Yes. Is this paragraph also AI-generated? '
|
||||||
'You will never know."))'
|
'You will never know."))'
|
||||||
|
|
||||||
@@ -1029,14 +1152,19 @@ def _essay_on_demand_css() -> str:
|
|||||||
|
|
||||||
def home_content_sx() -> str:
|
def home_content_sx() -> str:
|
||||||
"""Home page content as sx wire format."""
|
"""Home page content as sx wire format."""
|
||||||
|
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 (
|
return (
|
||||||
'(section :id "main-panel"'
|
f'(section :id "main-panel"'
|
||||||
' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'
|
f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'
|
||||||
' (div :id "main-content"'
|
f' (div :id "main-content"'
|
||||||
' (~sx-hero)'
|
f' (~sx-hero {hero_code})'
|
||||||
' (~sx-philosophy)'
|
f' (~sx-philosophy)'
|
||||||
' (~sx-how-it-works)'
|
f' (~sx-how-it-works)'
|
||||||
' (~sx-credits)))'
|
f' (~sx-credits)))'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user