Externalize sexp to .sexpr files + render() API
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m20s

Replace all 676 inline sexp() string calls across 7 services with
render(component_name, **kwargs) calls backed by 46 external .sexpr
component definition files (587 defcomps total).

- Add render() function to shared/sexp/jinja_bridge.py
- Add load_service_components() helper and update load_sexp_dir() for *.sexpr
- Update parser keyword regex to support HTMX hx-on::event syntax
- Convert remaining inline HTML in route files to render() calls
- Add shared/sexp/templates/misc.sexp for cross-service utility components

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 16:14:58 +00:00
parent f4c2f4b6b8
commit f9d9697c67
64 changed files with 5041 additions and 4051 deletions

58
account/sexp/auth.sexpr Normal file
View File

@@ -0,0 +1,58 @@
;; Auth page components (login, device, check email)
(defcomp ~account-login-error (&key error)
(when error
(div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4"
(raw! error))))
(defcomp ~account-login-form (&key error-html action csrf-token email)
(div :class "py-8 max-w-md mx-auto"
(h1 :class "text-2xl font-bold mb-6" "Sign in")
(raw! error-html)
(form :method "post" :action action :class "space-y-4"
(input :type "hidden" :name "csrf_token" :value csrf-token)
(div
(label :for "email" :class "block text-sm font-medium mb-1" "Email address")
(input :type "email" :name "email" :id "email" :value email :required true :autofocus true
:class "w-full border border-stone-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-stone-500"))
(button :type "submit"
:class "w-full bg-stone-800 text-white py-2 px-4 rounded hover:bg-stone-700 transition"
"Send magic link"))))
(defcomp ~account-device-error (&key error)
(when error
(div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4"
(raw! error))))
(defcomp ~account-device-form (&key error-html action csrf-token code)
(div :class "py-8 max-w-md mx-auto"
(h1 :class "text-2xl font-bold mb-6" "Authorize device")
(p :class "text-stone-600 mb-4" "Enter the code shown in your terminal to sign in.")
(raw! error-html)
(form :method "post" :action action :class "space-y-4"
(input :type "hidden" :name "csrf_token" :value csrf-token)
(div
(label :for "code" :class "block text-sm font-medium mb-1" "Device code")
(input :type "text" :name "code" :id "code" :value code :placeholder "XXXX-XXXX"
:required true :autofocus true :maxlength "9" :autocomplete "off" :spellcheck "false"
:class "w-full border border-stone-300 rounded px-3 py-3 text-center text-2xl tracking-widest font-mono uppercase focus:outline-none focus:ring-2 focus:ring-stone-500"))
(button :type "submit"
:class "w-full bg-stone-800 text-white py-2 px-4 rounded hover:bg-stone-700 transition"
"Authorize"))))
(defcomp ~account-device-approved ()
(div :class "py-8 max-w-md mx-auto text-center"
(h1 :class "text-2xl font-bold mb-4" "Device authorized")
(p :class "text-stone-600" "You can close this window and return to your terminal.")))
(defcomp ~account-check-email-error (&key error)
(when error
(div :class "bg-yellow-50 border border-yellow-200 text-yellow-700 p-3 rounded mt-4"
(raw! error))))
(defcomp ~account-check-email (&key email error-html)
(div :class "py-8 max-w-md mx-auto text-center"
(h1 :class "text-2xl font-bold mb-4" "Check your email")
(p :class "text-stone-600 mb-2" "We sent a sign-in link to " (strong (raw! email)) ".")
(p :class "text-stone-500 text-sm" "Click the link in the email to sign in. The link expires in 15 minutes.")
(raw! error-html)))

View File

@@ -0,0 +1,48 @@
;; Account dashboard components
(defcomp ~account-error-banner (&key error)
(when error
(div :class "rounded-lg border border-red-200 bg-red-50 text-red-800 px-4 py-3 text-sm"
(raw! error))))
(defcomp ~account-user-email (&key email)
(when email
(p :class "text-sm text-stone-500 mt-1" (raw! email))))
(defcomp ~account-user-name (&key name)
(when name
(p :class "text-sm text-stone-600" (raw! name))))
(defcomp ~account-logout-form (&key csrf-token)
(form :action "/auth/logout/" :method "post"
(input :type "hidden" :name "csrf_token" :value csrf-token)
(button :type "submit"
:class "inline-flex items-center gap-2 rounded-full border border-stone-300 px-4 py-2 text-sm font-medium text-stone-700 hover:bg-stone-50 transition"
(i :class "fa-solid fa-right-from-bracket text-xs") " Sign out")))
(defcomp ~account-label-item (&key name)
(span :class "inline-flex items-center rounded-full border border-stone-200 px-3 py-1 text-xs font-medium bg-white/60"
(raw! name)))
(defcomp ~account-labels-section (&key items-html)
(when items-html
(div
(h2 :class "text-base font-semibold tracking-tight mb-3" "Labels")
(div :class "flex flex-wrap gap-2" (raw! items-html)))))
(defcomp ~account-main-panel (&key error-html email-html name-html logout-html labels-html)
(div :class "w-full max-w-3xl mx-auto px-4 py-6"
(div :class "bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-8"
(raw! error-html)
(div :class "flex items-center justify-between"
(div
(h1 :class "text-xl font-semibold tracking-tight" "Account")
(raw! email-html)
(raw! name-html))
(raw! logout-html))
(raw! labels-html))))
;; Header child wrapper
(defcomp ~account-header-child (&key inner-html)
(div :id "root-header-child" :class "flex flex-col w-full items-center"
(raw! inner-html)))

View File

@@ -0,0 +1,37 @@
;; Newsletter management components
(defcomp ~account-newsletter-desc (&key description)
(when description
(p :class "text-xs text-stone-500 mt-0.5 truncate" (raw! description))))
(defcomp ~account-newsletter-toggle (&key id url hdrs target cls checked knob-cls)
(div :id id :class "flex items-center"
(button :hx-post url :hx-headers hdrs :hx-target target :hx-swap "outerHTML"
:class cls :role "switch" :aria-checked checked
(span :class knob-cls))))
(defcomp ~account-newsletter-toggle-off (&key id url hdrs target)
(div :id id :class "flex items-center"
(button :hx-post url :hx-headers hdrs :hx-target target :hx-swap "outerHTML"
:class "relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 bg-stone-300"
:role "switch" :aria-checked "false"
(span :class "inline-block h-4 w-4 rounded-full bg-white shadow transform transition-transform translate-x-1"))))
(defcomp ~account-newsletter-item (&key name desc-html toggle-html)
(div :class "flex items-center justify-between py-4 first:pt-0 last:pb-0"
(div :class "min-w-0 flex-1"
(p :class "text-sm font-medium text-stone-800" (raw! name))
(raw! desc-html))
(div :class "ml-4 flex-shrink-0" (raw! toggle-html))))
(defcomp ~account-newsletter-list (&key items-html)
(div :class "divide-y divide-stone-100" (raw! items-html)))
(defcomp ~account-newsletter-empty ()
(p :class "text-sm text-stone-500" "No newsletters available."))
(defcomp ~account-newsletters-panel (&key list-html)
(div :class "w-full max-w-3xl mx-auto px-4 py-6"
(div :class "bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-6"
(h1 :class "text-xl font-semibold tracking-tight" "Newsletters")
(raw! list-html))))

View File

@@ -6,14 +6,18 @@ auth pages. Called from route handlers in place of ``render_template()``.
"""
from __future__ import annotations
import os
from typing import Any
from shared.sexp.jinja_bridge import sexp
from shared.sexp.jinja_bridge import render, load_service_components
from shared.sexp.helpers import (
call_url, root_header_html, search_desktop_html,
search_mobile_html, full_page, oob_page,
)
# Load account-specific .sexpr components at import time
load_service_components(os.path.dirname(os.path.dirname(__file__)))
# ---------------------------------------------------------------------------
# Header helpers
@@ -21,10 +25,11 @@ from shared.sexp.helpers import (
def _auth_nav_html(ctx: dict) -> str:
"""Auth section desktop nav items."""
html = sexp(
'(~nav-link :href h :label "newsletters" :select-colours sc)',
h=call_url(ctx, "account_url", "/newsletters/"),
sc=ctx.get("select_colours", ""),
html = render(
"nav-link",
href=call_url(ctx, "account_url", "/newsletters/"),
label="newsletters",
select_colours=ctx.get("select_colours", ""),
)
account_nav_html = ctx.get("account_nav_html", "")
if account_nav_html:
@@ -34,22 +39,23 @@ def _auth_nav_html(ctx: dict) -> str:
def _auth_header_html(ctx: dict, *, oob: bool = False) -> str:
"""Build the account section header row."""
return sexp(
'(~menu-row :id "auth-row" :level 1 :colour "sky"'
' :link-href lh :link-label "account" :icon "fa-solid fa-user"'
' :nav-html nh :child-id "auth-header-child" :oob oob)',
lh=call_url(ctx, "account_url", "/"),
nh=_auth_nav_html(ctx),
oob=oob,
return render(
"menu-row",
id="auth-row", level=1, colour="sky",
link_href=call_url(ctx, "account_url", "/"),
link_label="account", icon="fa-solid fa-user",
nav_html=_auth_nav_html(ctx),
child_id="auth-header-child", oob=oob,
)
def _auth_nav_mobile_html(ctx: dict) -> str:
"""Mobile nav menu for auth section."""
html = sexp(
'(~nav-link :href h :label "newsletters" :select-colours sc)',
h=call_url(ctx, "account_url", "/newsletters/"),
sc=ctx.get("select_colours", ""),
html = render(
"nav-link",
href=call_url(ctx, "account_url", "/newsletters/"),
label="newsletters",
select_colours=ctx.get("select_colours", ""),
)
account_nav_html = ctx.get("account_nav_html", "")
if account_nav_html:
@@ -69,58 +75,30 @@ def _account_main_panel_html(ctx: dict) -> str:
user = getattr(g, "user", None)
error = ctx.get("error", "")
error_html = sexp(
'(div :class "rounded-lg border border-red-200 bg-red-50 text-red-800 px-4 py-3 text-sm" (raw! e))',
e=error,
) if error else ""
error_html = render("account-error-banner", error=error) if error else ""
user_email_html = ""
user_name_html = ""
if user:
user_email_html = sexp(
'(p :class "text-sm text-stone-500 mt-1" (raw! e))',
e=user.email,
)
user_email_html = render("account-user-email", email=user.email)
if user.name:
user_name_html = sexp(
'(p :class "text-sm text-stone-600" (raw! n))',
n=user.name,
)
user_name_html = render("account-user-name", name=user.name)
logout_html = sexp(
'(form :action "/auth/logout/" :method "post"'
' (input :type "hidden" :name "csrf_token" :value csrf)'
' (button :type "submit"'
' :class "inline-flex items-center gap-2 rounded-full border border-stone-300 px-4 py-2 text-sm font-medium text-stone-700 hover:bg-stone-50 transition"'
' (i :class "fa-solid fa-right-from-bracket text-xs") " Sign out"))',
csrf=generate_csrf_token(),
)
logout_html = render("account-logout-form", csrf_token=generate_csrf_token())
labels_html = ""
if user and hasattr(user, "labels") and user.labels:
label_items = "".join(
sexp(
'(span :class "inline-flex items-center rounded-full border border-stone-200 px-3 py-1 text-xs font-medium bg-white/60" (raw! n))',
n=label.name,
)
render("account-label-item", name=label.name)
for label in user.labels
)
labels_html = sexp(
'(div (h2 :class "text-base font-semibold tracking-tight mb-3" "Labels")'
' (div :class "flex flex-wrap gap-2" (raw! items)))',
items=label_items,
)
labels_html = render("account-labels-section", items_html=label_items)
return sexp(
'(div :class "w-full max-w-3xl mx-auto px-4 py-6"'
' (div :class "bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-8"'
' (raw! err)'
' (div :class "flex items-center justify-between"'
' (div (h1 :class "text-xl font-semibold tracking-tight" "Account") (raw! email) (raw! name))'
' (raw! logout))'
' (raw! labels)))',
err=error_html, email=user_email_html, name=user_name_html,
logout=logout_html, labels=labels_html,
return render(
"account-main-panel",
error_html=error_html, email_html=user_email_html,
name_html=user_name_html, logout_html=logout_html,
labels_html=labels_html,
)
@@ -140,31 +118,24 @@ def _newsletter_toggle_html(un: Any, account_url_fn: Any, csrf_token: str) -> st
bg = "bg-stone-300"
translate = "translate-x-1"
checked = "false"
return sexp(
'(div :id id :class "flex items-center"'
' (button :hx-post url :hx-headers hdrs :hx-target tgt :hx-swap "outerHTML"'
' :class cls :role "switch" :aria-checked checked'
' (span :class knob)))',
return render(
"account-newsletter-toggle",
id=f"nl-{nid}", url=toggle_url,
hdrs=f'{{"X-CSRFToken": "{csrf_token}"}}',
tgt=f"#nl-{nid}",
target=f"#nl-{nid}",
cls=f"relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 {bg}",
checked=checked,
knob=f"inline-block h-4 w-4 rounded-full bg-white shadow transform transition-transform {translate}",
knob_cls=f"inline-block h-4 w-4 rounded-full bg-white shadow transform transition-transform {translate}",
)
def _newsletter_toggle_off_html(nid: int, toggle_url: str, csrf_token: str) -> str:
"""Render an unsubscribed newsletter toggle (no subscription record yet)."""
return sexp(
'(div :id id :class "flex items-center"'
' (button :hx-post url :hx-headers hdrs :hx-target tgt :hx-swap "outerHTML"'
' :class "relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 bg-stone-300"'
' :role "switch" :aria-checked "false"'
' (span :class "inline-block h-4 w-4 rounded-full bg-white shadow transform transition-transform translate-x-1")))',
return render(
"account-newsletter-toggle-off",
id=f"nl-{nid}", url=toggle_url,
hdrs=f'{{"X-CSRFToken": "{csrf_token}"}}',
tgt=f"#nl-{nid}",
target=f"#nl-{nid}",
)
@@ -181,9 +152,8 @@ def _newsletters_panel_html(ctx: dict, newsletter_list: list) -> str:
nl = item["newsletter"]
un = item.get("un")
desc_html = sexp(
'(p :class "text-xs text-stone-500 mt-0.5 truncate" (raw! d))',
d=nl.description,
desc_html = render(
"account-newsletter-desc", description=nl.description
) if nl.description else ""
if un:
@@ -192,28 +162,18 @@ def _newsletters_panel_html(ctx: dict, newsletter_list: list) -> str:
toggle_url = account_url_fn(f"/newsletter/{nl.id}/toggle/")
toggle = _newsletter_toggle_off_html(nl.id, toggle_url, csrf)
items.append(sexp(
'(div :class "flex items-center justify-between py-4 first:pt-0 last:pb-0"'
' (div :class "min-w-0 flex-1"'
' (p :class "text-sm font-medium text-stone-800" (raw! name))'
' (raw! desc))'
' (div :class "ml-4 flex-shrink-0" (raw! toggle)))',
name=nl.name, desc=desc_html, toggle=toggle,
items.append(render(
"account-newsletter-item",
name=nl.name, desc_html=desc_html, toggle_html=toggle,
))
list_html = sexp(
'(div :class "divide-y divide-stone-100" (raw! items))',
items="".join(items),
list_html = render(
"account-newsletter-list",
items_html="".join(items),
)
else:
list_html = sexp('(p :class "text-sm text-stone-500" "No newsletters available.")')
list_html = render("account-newsletter-empty")
return sexp(
'(div :class "w-full max-w-3xl mx-auto px-4 py-6"'
' (div :class "bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-6"'
' (h1 :class "text-xl font-semibold tracking-tight" "Newsletters")'
' (raw! list)))',
list=list_html,
)
return render("account-newsletters-panel", list_html=list_html)
# ---------------------------------------------------------------------------
@@ -229,26 +189,12 @@ def _login_page_content(ctx: dict) -> str:
email = ctx.get("email", "")
action = url_for("auth.start_login")
error_html = sexp(
'(div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4" (raw! e))',
e=error,
) if error else ""
error_html = render("account-login-error", error=error) if error else ""
return sexp(
'(div :class "py-8 max-w-md mx-auto"'
' (h1 :class "text-2xl font-bold mb-6" "Sign in")'
' (raw! err)'
' (form :method "post" :action action :class "space-y-4"'
' (input :type "hidden" :name "csrf_token" :value csrf)'
' (div'
' (label :for "email" :class "block text-sm font-medium mb-1" "Email address")'
' (input :type "email" :name "email" :id "email" :value email :required true :autofocus true'
' :class "w-full border border-stone-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-stone-500"))'
' (button :type "submit"'
' :class "w-full bg-stone-800 text-white py-2 px-4 rounded hover:bg-stone-700 transition"'
' "Send magic link")))',
err=error_html, action=action,
csrf=generate_csrf_token(), email=email,
return render(
"account-login-form",
error_html=error_html, action=action,
csrf_token=generate_csrf_token(), email=email,
)
@@ -261,38 +207,18 @@ def _device_page_content(ctx: dict) -> str:
code = ctx.get("code", "")
action = url_for("auth.device_submit")
error_html = sexp(
'(div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4" (raw! e))',
e=error,
) if error else ""
error_html = render("account-device-error", error=error) if error else ""
return sexp(
'(div :class "py-8 max-w-md mx-auto"'
' (h1 :class "text-2xl font-bold mb-6" "Authorize device")'
' (p :class "text-stone-600 mb-4" "Enter the code shown in your terminal to sign in.")'
' (raw! err)'
' (form :method "post" :action action :class "space-y-4"'
' (input :type "hidden" :name "csrf_token" :value csrf)'
' (div'
' (label :for "code" :class "block text-sm font-medium mb-1" "Device code")'
' (input :type "text" :name "code" :id "code" :value code :placeholder "XXXX-XXXX"'
' :required true :autofocus true :maxlength "9" :autocomplete "off" :spellcheck "false"'
' :class "w-full border border-stone-300 rounded px-3 py-3 text-center text-2xl tracking-widest font-mono uppercase focus:outline-none focus:ring-2 focus:ring-stone-500"))'
' (button :type "submit"'
' :class "w-full bg-stone-800 text-white py-2 px-4 rounded hover:bg-stone-700 transition"'
' "Authorize")))',
err=error_html, action=action,
csrf=generate_csrf_token(), code=code,
return render(
"account-device-form",
error_html=error_html, action=action,
csrf_token=generate_csrf_token(), code=code,
)
def _device_approved_content() -> str:
"""Device approved success content."""
return sexp(
'(div :class "py-8 max-w-md mx-auto text-center"'
' (h1 :class "text-2xl font-bold mb-4" "Device authorized")'
' (p :class "text-stone-600" "You can close this window and return to your terminal."))',
)
return render("account-device-approved")
# ---------------------------------------------------------------------------
@@ -304,10 +230,7 @@ async def render_account_page(ctx: dict) -> str:
main = _account_main_panel_html(ctx)
hdr = root_header_html(ctx)
hdr += sexp(
'(div :id "root-header-child" :class "flex flex-col w-full items-center" (raw! a))',
a=_auth_header_html(ctx),
)
hdr += render("account-header-child", inner_html=_auth_header_html(ctx))
return full_page(ctx, header_rows_html=hdr,
content_html=main,
@@ -337,10 +260,7 @@ async def render_newsletters_page(ctx: dict, newsletter_list: list) -> str:
main = _newsletters_panel_html(ctx, newsletter_list)
hdr = root_header_html(ctx)
hdr += sexp(
'(div :id "root-header-child" :class "flex flex-col w-full items-center" (raw! a))',
a=_auth_header_html(ctx),
)
hdr += render("account-header-child", inner_html=_auth_header_html(ctx))
return full_page(ctx, header_rows_html=hdr,
content_html=main,
@@ -368,10 +288,7 @@ async def render_newsletters_oob(ctx: dict, newsletter_list: list) -> str:
async def render_fragment_page(ctx: dict, page_fragment_html: str) -> str:
"""Full page: fragment-provided content."""
hdr = root_header_html(ctx)
hdr += sexp(
'(div :id "root-header-child" :class "flex flex-col w-full items-center" (raw! a))',
a=_auth_header_html(ctx),
)
hdr += render("account-header-child", inner_html=_auth_header_html(ctx))
return full_page(ctx, header_rows_html=hdr,
content_html=page_fragment_html,
@@ -426,18 +343,13 @@ def _check_email_content(email: str, email_error: str | None = None) -> str:
"""Check email confirmation content."""
from markupsafe import escape
error_html = sexp(
'(div :class "bg-yellow-50 border border-yellow-200 text-yellow-700 p-3 rounded mt-4" (raw! e))',
e=str(escape(email_error)),
error_html = render(
"account-check-email-error", error=str(escape(email_error))
) if email_error else ""
return sexp(
'(div :class "py-8 max-w-md mx-auto text-center"'
' (h1 :class "text-2xl font-bold mb-4" "Check your email")'
' (p :class "text-stone-600 mb-2" "We sent a sign-in link to " (strong (raw! email)) ".")'
' (p :class "text-stone-500 text-sm" "Click the link in the email to sign in. The link expires in 15 minutes.")'
' (raw! err))',
email=str(escape(email)), err=error_html,
return render(
"account-check-email",
email=str(escape(email)), error_html=error_html,
)
@@ -468,7 +380,6 @@ def render_newsletter_toggle(un) -> str:
from quart import g
account_url_fn = getattr(g, "_account_url", None)
if account_url_fn is None:
# Fallback: construct URL directly
from shared.infrastructure.urls import account_url
account_url_fn = account_url
return _newsletter_toggle_html(un, account_url_fn, generate_csrf_token())