Refactor SX templates: shared components, Python migration, cleanup
- Extract shared components (empty-state, delete-btn, sentinel, crud-*, view-toggle, img-or-placeholder, avatar, sumup-settings-form, auth forms, order tables/detail/checkout) - Migrate all Python sx_call() callers to use shared components directly - Remove 55+ thin wrapper defcomps from domain .sx files - Remove trivial passthrough wrappers (blog-header-label, market-card-text, etc) - Unify duplicate auth flows (account + federation) into shared/sx/templates/auth.sx - Unify duplicate order views (cart + orders) into shared/sx/templates/orders.sx - Disable static file caching in dev (SEND_FILE_MAX_AGE_DEFAULT=0) - Add SX response validation and debug headers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,31 +1,5 @@
|
||||
;; Auth components (login, check email, choose username)
|
||||
|
||||
(defcomp ~federation-error-banner (&key error)
|
||||
(div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4" error))
|
||||
|
||||
(defcomp ~federation-login-form (&key error action csrf email)
|
||||
(div :class "py-8 max-w-md mx-auto"
|
||||
(h1 :class "text-2xl font-bold mb-6" "Sign in")
|
||||
error
|
||||
(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"))))
|
||||
|
||||
(defcomp ~federation-check-email-error (&key error)
|
||||
(div :class "bg-yellow-50 border border-yellow-200 text-yellow-700 p-3 rounded mt-4" error))
|
||||
|
||||
(defcomp ~federation-check-email (&key email error)
|
||||
(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 email) ".")
|
||||
(p :class "text-stone-500 text-sm" "Click the link in the email to sign in. The link expires in 15 minutes.")
|
||||
error))
|
||||
;; Auth components (choose username — federation-specific)
|
||||
;; Login and check-email components are shared: see shared/sx/templates/auth.sx
|
||||
|
||||
(defcomp ~federation-choose-username (&key domain error csrf username check-url)
|
||||
(div :class "py-8 max-w-md mx-auto"
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
preview
|
||||
(div :class "text-xs text-stone-400 mt-1" time)))))
|
||||
|
||||
(defcomp ~federation-notifications-empty ()
|
||||
(p :class "text-stone-500" "No notifications yet."))
|
||||
|
||||
(defcomp ~federation-notifications-list (&key items)
|
||||
(div :class "space-y-2" items))
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
;; Search and actor card components
|
||||
|
||||
;; Aliases — delegate to shared ~avatar
|
||||
(defcomp ~federation-actor-avatar-img (&key src cls)
|
||||
(img :src src :alt "" :class cls))
|
||||
(~avatar :src src :cls cls))
|
||||
|
||||
(defcomp ~federation-actor-avatar-placeholder (&key cls initial)
|
||||
(div :class cls initial))
|
||||
(~avatar :cls cls :initial initial))
|
||||
|
||||
(defcomp ~federation-actor-name-link (&key href name)
|
||||
(a :href href :class "font-semibold text-stone-900 hover:underline" name))
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
(nav :class "flex gap-3 text-sm items-center"
|
||||
(a :href url :class "px-2 py-1 rounded hover:bg-stone-200 font-bold" "Choose username")))
|
||||
|
||||
(defcomp ~federation-nav-link (&key href cls label)
|
||||
(a :href href :class cls label))
|
||||
|
||||
(defcomp ~federation-nav-notification-link (&key href cls count-url)
|
||||
(a :href href :class cls "Notifications"
|
||||
(span :sx-get count-url :sx-trigger "load, every 30s" :sx-swap "innerHTML"
|
||||
@@ -21,35 +18,29 @@
|
||||
(div :id "social-row" :class "flex flex-col items-center md:flex-row justify-center md:justify-between w-full p-1 bg-sky-400"
|
||||
(div :class "w-full flex flex-row items-center gap-2 flex-wrap" nav)))
|
||||
|
||||
(defcomp ~federation-header-child (&key inner)
|
||||
(div :id "root-header-child" :class "flex flex-col w-full items-center" inner))
|
||||
|
||||
;; --- Post card ---
|
||||
|
||||
(defcomp ~federation-boost-label (&key name)
|
||||
(div :class "text-sm text-stone-500 mb-2" "Boosted by " name))
|
||||
|
||||
;; Aliases — delegate to shared ~avatar
|
||||
(defcomp ~federation-avatar-img (&key src cls)
|
||||
(img :src src :alt "" :class cls))
|
||||
(~avatar :src src :cls cls))
|
||||
|
||||
(defcomp ~federation-avatar-placeholder (&key cls initial)
|
||||
(div :class cls initial))
|
||||
(~avatar :cls cls :initial initial))
|
||||
|
||||
(defcomp ~federation-content-cw (&key summary content)
|
||||
(details :class "mt-2"
|
||||
(summary :class "text-stone-500 cursor-pointer" "CW: " (~rich-text :html summary))
|
||||
(defcomp ~federation-content (&key content summary)
|
||||
(if summary
|
||||
(details :class "mt-2"
|
||||
(summary :class "text-stone-500 cursor-pointer" "CW: " (~rich-text :html summary))
|
||||
(div :class "mt-2 prose prose-sm prose-stone max-w-none" (~rich-text :html content)))
|
||||
(div :class "mt-2 prose prose-sm prose-stone max-w-none" (~rich-text :html content))))
|
||||
|
||||
(defcomp ~federation-content-plain (&key content)
|
||||
(div :class "mt-2 prose prose-sm prose-stone max-w-none" (~rich-text :html content)))
|
||||
|
||||
(defcomp ~federation-original-link (&key url)
|
||||
(a :href url :target "_blank" :rel "noopener"
|
||||
:class "text-sm text-stone-400 hover:underline mt-1 inline-block" "original"))
|
||||
|
||||
(defcomp ~federation-interactions-wrap (&key id buttons)
|
||||
(div :id id buttons))
|
||||
|
||||
(defcomp ~federation-post-card (&key boost avatar actor-name actor-username domain time content original interactions)
|
||||
(article :class "bg-white rounded-lg shadow-sm border border-stone-200 p-4 mb-4"
|
||||
boost
|
||||
|
||||
@@ -11,6 +11,7 @@ from typing import Any
|
||||
from markupsafe import escape
|
||||
|
||||
from shared.sx.jinja_bridge import load_service_components
|
||||
from shared.sx.parser import serialize
|
||||
from shared.sx.helpers import (
|
||||
sx_call, SxExpr,
|
||||
root_header_sx, full_page_sx, header_child_sx,
|
||||
@@ -45,12 +46,8 @@ def _social_nav_sx(actor: Any) -> str:
|
||||
for endpoint, label in links:
|
||||
href = url_for(endpoint)
|
||||
bold = " font-bold" if request.path == href else ""
|
||||
parts.append(sx_call(
|
||||
"federation-nav-link",
|
||||
href=href,
|
||||
cls=f"px-2 py-1 rounded hover:bg-stone-200{bold}",
|
||||
label=label,
|
||||
))
|
||||
cls = f"px-2 py-1 rounded hover:bg-stone-200{bold}"
|
||||
parts.append(f'(a :href {serialize(href)} :class {serialize(cls)} {serialize(label)})')
|
||||
|
||||
# Notifications with live badge
|
||||
notif_url = url_for("social.notifications")
|
||||
@@ -65,12 +62,7 @@ def _social_nav_sx(actor: Any) -> str:
|
||||
|
||||
# Profile link
|
||||
profile_url = url_for("activitypub.actor_profile", username=actor.preferred_username)
|
||||
parts.append(sx_call(
|
||||
"federation-nav-link",
|
||||
href=profile_url,
|
||||
cls="px-2 py-1 rounded hover:bg-stone-200",
|
||||
label=f"@{actor.preferred_username}",
|
||||
))
|
||||
parts.append(f'(a :href {serialize(profile_url)} :class "px-2 py-1 rounded hover:bg-stone-200" {serialize("@" + actor.preferred_username)})')
|
||||
|
||||
items_sx = "(<> " + " ".join(parts) + ")"
|
||||
return sx_call("federation-nav-bar", items=SxExpr(items_sx))
|
||||
@@ -171,26 +163,23 @@ def _post_card_sx(item: Any, actor: Any) -> str:
|
||||
"federation-boost-label", name=str(escape(boosted_by)),
|
||||
) if boosted_by else ""
|
||||
|
||||
if actor_icon:
|
||||
avatar = sx_call("federation-avatar-img", src=actor_icon, cls="w-10 h-10 rounded-full")
|
||||
else:
|
||||
initial = actor_name[0].upper() if actor_name else "?"
|
||||
avatar = sx_call(
|
||||
"federation-avatar-placeholder",
|
||||
cls="w-10 h-10 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-sm",
|
||||
initial=initial,
|
||||
)
|
||||
initial = actor_name[0].upper() if (not actor_icon and actor_name) else "?"
|
||||
avatar = sx_call(
|
||||
"avatar", src=actor_icon or None,
|
||||
cls="w-10 h-10 rounded-full" if actor_icon else "w-10 h-10 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-sm",
|
||||
initial=None if actor_icon else initial,
|
||||
)
|
||||
|
||||
domain_str = f"@{escape(actor_domain)}" if actor_domain else ""
|
||||
time_str = published.strftime("%b %d, %H:%M") if published else ""
|
||||
|
||||
if summary:
|
||||
content_sx = sx_call(
|
||||
"federation-content-cw",
|
||||
summary=str(escape(summary)), content=content,
|
||||
"federation-content",
|
||||
content=content, summary=str(escape(summary)),
|
||||
)
|
||||
else:
|
||||
content_sx = sx_call("federation-content-plain", content=content)
|
||||
content_sx = sx_call("federation-content", content=content)
|
||||
|
||||
original_sx = ""
|
||||
if url and post_type == "remote":
|
||||
@@ -200,11 +189,7 @@ def _post_card_sx(item: Any, actor: Any) -> str:
|
||||
if actor:
|
||||
oid = getattr(item, "object_id", "") or ""
|
||||
safe_id = oid.replace("/", "_").replace(":", "_")
|
||||
interactions_sx = sx_call(
|
||||
"federation-interactions-wrap",
|
||||
id=f"interactions-{safe_id}",
|
||||
buttons=SxExpr(_interaction_buttons_sx(item, actor)),
|
||||
)
|
||||
interactions_sx = f'(div :id {serialize(f"interactions-{safe_id}")} {_interaction_buttons_sx(item, actor)})'
|
||||
|
||||
return sx_call(
|
||||
"federation-post-card",
|
||||
@@ -263,15 +248,12 @@ def _actor_card_sx(a: Any, actor: Any, followed_urls: set,
|
||||
|
||||
safe_id = actor_url.replace("/", "_").replace(":", "_")
|
||||
|
||||
if icon_url:
|
||||
avatar = sx_call("federation-actor-avatar-img", src=icon_url, cls="w-12 h-12 rounded-full")
|
||||
else:
|
||||
initial = (display_name or username)[0].upper() if (display_name or username) else "?"
|
||||
avatar = sx_call(
|
||||
"federation-actor-avatar-placeholder",
|
||||
cls="w-12 h-12 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold",
|
||||
initial=initial,
|
||||
)
|
||||
initial = (display_name or username)[0].upper() if (not icon_url and (display_name or username)) else "?"
|
||||
avatar = sx_call(
|
||||
"avatar", src=icon_url or None,
|
||||
cls="w-12 h-12 rounded-full" if icon_url else "w-12 h-12 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold",
|
||||
initial=None if icon_url else initial,
|
||||
)
|
||||
|
||||
# Name link
|
||||
if (list_type in ("following", "search")) and aid:
|
||||
@@ -359,15 +341,12 @@ def _notification_sx(notif: Any) -> str:
|
||||
|
||||
border = " border-l-4 border-l-stone-400" if not read else ""
|
||||
|
||||
if from_icon:
|
||||
avatar = sx_call("federation-avatar-img", src=from_icon, cls="w-8 h-8 rounded-full")
|
||||
else:
|
||||
initial = from_name[0].upper() if from_name else "?"
|
||||
avatar = sx_call(
|
||||
"federation-avatar-placeholder",
|
||||
cls="w-8 h-8 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-xs",
|
||||
initial=initial,
|
||||
)
|
||||
initial = from_name[0].upper() if (not from_icon and from_name) else "?"
|
||||
avatar = sx_call(
|
||||
"avatar", src=from_icon or None,
|
||||
cls="w-8 h-8 rounded-full" if from_icon else "w-8 h-8 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-xs",
|
||||
initial=None if from_icon else initial,
|
||||
)
|
||||
|
||||
domain_str = f"@{escape(from_domain)}" if from_domain else ""
|
||||
|
||||
@@ -423,12 +402,12 @@ async def render_login_page(ctx: dict) -> str:
|
||||
action = url_for("auth.start_login")
|
||||
csrf = generate_csrf_token()
|
||||
|
||||
error_sx = sx_call("federation-error-banner", error=error) if error else ""
|
||||
error_sx = sx_call("auth-error-banner", error=error) if error else ""
|
||||
|
||||
content = sx_call(
|
||||
"federation-login-form",
|
||||
"auth-login-form",
|
||||
error=SxExpr(error_sx) if error_sx else None,
|
||||
action=action, csrf=csrf,
|
||||
action=action, csrf_token=csrf,
|
||||
email=str(escape(email)),
|
||||
)
|
||||
|
||||
@@ -442,11 +421,11 @@ async def render_check_email_page(ctx: dict) -> str:
|
||||
email_error = ctx.get("email_error")
|
||||
|
||||
error_sx = sx_call(
|
||||
"federation-check-email-error", error=str(escape(email_error)),
|
||||
"auth-check-email-error", error=str(escape(email_error)),
|
||||
) if email_error else ""
|
||||
|
||||
content = sx_call(
|
||||
"federation-check-email",
|
||||
"auth-check-email",
|
||||
email=str(escape(email)),
|
||||
error=SxExpr(error_sx) if error_sx else None,
|
||||
)
|
||||
@@ -622,15 +601,12 @@ async def render_actor_timeline_page(ctx: dict, remote_actor: Any, items: list,
|
||||
summary = getattr(remote_actor, "summary", None)
|
||||
actor_url = getattr(remote_actor, "actor_url", "")
|
||||
|
||||
if icon_url:
|
||||
avatar = sx_call("federation-avatar-img", src=icon_url, cls="w-16 h-16 rounded-full")
|
||||
else:
|
||||
initial = display_name[0].upper() if display_name else "?"
|
||||
avatar = sx_call(
|
||||
"federation-avatar-placeholder",
|
||||
cls="w-16 h-16 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-xl",
|
||||
initial=initial,
|
||||
)
|
||||
initial = display_name[0].upper() if (not icon_url and display_name) else "?"
|
||||
avatar = sx_call(
|
||||
"avatar", src=icon_url or None,
|
||||
cls="w-16 h-16 rounded-full" if icon_url else "w-16 h-16 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-xl",
|
||||
initial=None if icon_url else initial,
|
||||
)
|
||||
|
||||
summary_sx = sx_call("federation-profile-summary", summary=summary) if summary else ""
|
||||
|
||||
@@ -687,7 +663,8 @@ async def render_notifications_page(ctx: dict, notifications: list,
|
||||
actor: Any) -> str:
|
||||
"""Full page: notifications."""
|
||||
if not notifications:
|
||||
notif_sx = sx_call("federation-notifications-empty")
|
||||
notif_sx = sx_call("empty-state", message="No notifications yet.",
|
||||
cls="text-stone-500")
|
||||
else:
|
||||
items_sx = "(<> " + " ".join(_notification_sx(n) for n in notifications) + ")"
|
||||
notif_sx = sx_call(
|
||||
@@ -717,7 +694,7 @@ async def render_choose_username_page(ctx: dict) -> str:
|
||||
check_url = url_for("identity.check_username")
|
||||
actor = ctx.get("actor")
|
||||
|
||||
error_sx = sx_call("federation-error-banner", error=error) if error else ""
|
||||
error_sx = sx_call("auth-error-banner", error=error) if error else ""
|
||||
|
||||
content = sx_call(
|
||||
"federation-choose-username",
|
||||
|
||||
Reference in New Issue
Block a user