From 8cfa12de6b84469b406a01816debbfe34bfdecb9 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 28 Feb 2026 20:40:03 +0000 Subject: [PATCH] Eliminate post sub-admin rows, highlight active nav on admin row Remove the separate sub-admin header rows (data, entries, edit, settings) that caused duplicate/stale rows on HTMX navigation and font styling breaks. Instead, pass selected= to the admin row to highlight the active nav item via aria-selected styling. External nav items (calendars, markets, payments) also gain is-selected and select-colours support. Co-Authored-By: Claude Opus 4.6 --- blog/sexp/header.sexpr | 8 ++- blog/sexp/sexp_components.py | 104 +++++++---------------------------- 2 files changed, 27 insertions(+), 85 deletions(-) diff --git a/blog/sexp/header.sexpr b/blog/sexp/header.sexpr index 6debebe..c5554b6 100644 --- a/blog/sexp/header.sexpr +++ b/blog/sexp/header.sexpr @@ -10,8 +10,12 @@ (defcomp ~blog-admin-label () (<> (i :class "fa fa-shield-halved" :aria-hidden "true") " admin")) -(defcomp ~blog-admin-nav-item (&key href nav-btn-class label) - (div :class "relative nav-group" (a :href href :class nav-btn-class label))) +(defcomp ~blog-admin-nav-item (&key href nav-btn-class label is-selected select-colours) + (div :class "relative nav-group" + (a :href href + :aria-selected (when is-selected "true") + :class (str (or nav-btn-class "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3") " " (or select-colours "")) + label))) (defcomp ~blog-sub-settings-label (&key icon label) (<> (i :class icon :aria-hidden "true") " " label)) diff --git a/blog/sexp/sexp_components.py b/blog/sexp/sexp_components.py index 7b85bb9..2b3b8d3 100644 --- a/blog/sexp/sexp_components.py +++ b/blog/sexp/sexp_components.py @@ -86,7 +86,7 @@ def _post_header_html(ctx: dict, *, oob: bool = False) -> str: # Post admin header # --------------------------------------------------------------------------- -def _post_admin_header_html(ctx: dict, *, oob: bool = False) -> str: +def _post_admin_header_html(ctx: dict, *, oob: bool = False, selected: str = "") -> str: """Post admin header row with admin icon and nav links.""" from quart import url_for as qurl @@ -100,7 +100,7 @@ def _post_admin_header_html(ctx: dict, *, oob: bool = False) -> str: admin_href = qurl("blog.post.admin.admin", slug=slug) label_html = render("blog-admin-label") - nav_html = _post_admin_nav_html(ctx) + nav_html = _post_admin_nav_html(ctx, selected=selected) return render("menu-row", id="post-admin-row", level=2, @@ -109,7 +109,7 @@ def _post_admin_header_html(ctx: dict, *, oob: bool = False) -> str: ) -def _post_admin_nav_html(ctx: dict) -> str: +def _post_admin_nav_html(ctx: dict, *, selected: str = "") -> str: """Post admin desktop nav: calendars, markets, payments, entries, data, edit, settings.""" from quart import url_for as qurl @@ -136,6 +136,7 @@ def _post_admin_nav_html(ctx: dict) -> str: href = url_fn(path) parts.append(render("blog-admin-nav-item", href=href, nav_btn_class=nav_btn, label=label, + is_selected=(label == selected), select_colours=select_colours, )) # HTMX links @@ -148,6 +149,7 @@ def _post_admin_nav_html(ctx: dict) -> str: href = qurl(endpoint, slug=slug) parts.append(render("nav-link", href=href, label=label, select_colours=select_colours, + is_selected=(label == selected), )) return "".join(parts) @@ -1415,32 +1417,16 @@ async def render_post_admin_oob(ctx: dict) -> str: async def render_post_data_page(ctx: dict) -> str: root_hdr = root_header_html(ctx) post_hdr = _post_header_html(ctx) - admin_hdr = _post_admin_header_html(ctx) - from quart import url_for as qurl - slug = (ctx.get("post") or {}).get("slug", "") - data_hdr = _post_sub_admin_header_html( - "post_data-row", "post_data-header-child", - qurl("blog.post.admin.data", slug=slug), - "database", "data", ctx, - ) - header_rows = root_hdr + post_hdr + admin_hdr + data_hdr + admin_hdr = _post_admin_header_html(ctx, selected="data") + header_rows = root_hdr + post_hdr + admin_hdr content = ctx.get("data_html", "") return full_page(ctx, header_rows_html=header_rows, content_html=content) async def render_post_data_oob(ctx: dict) -> str: - admin_hdr_oob = _post_admin_header_html(ctx, oob=True) - from quart import url_for as qurl - slug = (ctx.get("post") or {}).get("slug", "") - data_hdr = _post_sub_admin_header_html( - "post_data-row", "post_data-header-child", - qurl("blog.post.admin.data", slug=slug), - "database", "data", ctx, - ) - data_oob = _oob_header_html("post-admin-header-child", "post_data-header-child", - data_hdr) + admin_hdr_oob = _post_admin_header_html(ctx, oob=True, selected="data") content = ctx.get("data_html", "") - return oob_page(ctx, oobs_html=admin_hdr_oob + data_oob, content_html=content) + return oob_page(ctx, oobs_html=admin_hdr_oob, content_html=content) # ---- Post entries ---- @@ -1448,32 +1434,16 @@ async def render_post_data_oob(ctx: dict) -> str: async def render_post_entries_page(ctx: dict) -> str: root_hdr = root_header_html(ctx) post_hdr = _post_header_html(ctx) - admin_hdr = _post_admin_header_html(ctx) - from quart import url_for as qurl - slug = (ctx.get("post") or {}).get("slug", "") - entries_hdr = _post_sub_admin_header_html( - "post_entries-row", "post_entries-header-child", - qurl("blog.post.admin.entries", slug=slug), - "clock", "entries", ctx, - ) - header_rows = root_hdr + post_hdr + admin_hdr + entries_hdr + admin_hdr = _post_admin_header_html(ctx, selected="entries") + header_rows = root_hdr + post_hdr + admin_hdr content = ctx.get("entries_html", "") return full_page(ctx, header_rows_html=header_rows, content_html=content) async def render_post_entries_oob(ctx: dict) -> str: - admin_hdr_oob = _post_admin_header_html(ctx, oob=True) - from quart import url_for as qurl - slug = (ctx.get("post") or {}).get("slug", "") - entries_hdr = _post_sub_admin_header_html( - "post_entries-row", "post_entries-header-child", - qurl("blog.post.admin.entries", slug=slug), - "clock", "entries", ctx, - ) - entries_oob = _oob_header_html("post-admin-header-child", "post_entries-header-child", - entries_hdr) + admin_hdr_oob = _post_admin_header_html(ctx, oob=True, selected="entries") content = ctx.get("entries_html", "") - return oob_page(ctx, oobs_html=admin_hdr_oob + entries_oob, content_html=content) + return oob_page(ctx, oobs_html=admin_hdr_oob, content_html=content) # ---- Post edit ---- @@ -1481,15 +1451,8 @@ async def render_post_entries_oob(ctx: dict) -> str: async def render_post_edit_page(ctx: dict) -> str: root_hdr = root_header_html(ctx) post_hdr = _post_header_html(ctx) - admin_hdr = _post_admin_header_html(ctx) - from quart import url_for as qurl - slug = (ctx.get("post") or {}).get("slug", "") - edit_hdr = _post_sub_admin_header_html( - "post_edit-row", "post_edit-header-child", - qurl("blog.post.admin.edit", slug=slug), - "pen-to-square", "edit", ctx, - ) - header_rows = root_hdr + post_hdr + admin_hdr + edit_hdr + admin_hdr = _post_admin_header_html(ctx, selected="edit") + header_rows = root_hdr + post_hdr + admin_hdr content = ctx.get("edit_html", "") body_end = ctx.get("body_end_html", "") return full_page(ctx, header_rows_html=header_rows, content_html=content, @@ -1497,18 +1460,9 @@ async def render_post_edit_page(ctx: dict) -> str: async def render_post_edit_oob(ctx: dict) -> str: - admin_hdr_oob = _post_admin_header_html(ctx, oob=True) - from quart import url_for as qurl - slug = (ctx.get("post") or {}).get("slug", "") - edit_hdr = _post_sub_admin_header_html( - "post_edit-row", "post_edit-header-child", - qurl("blog.post.admin.edit", slug=slug), - "pen-to-square", "edit", ctx, - ) - edit_oob = _oob_header_html("post-admin-header-child", "post_edit-header-child", - edit_hdr) + admin_hdr_oob = _post_admin_header_html(ctx, oob=True, selected="edit") content = ctx.get("edit_html", "") - return oob_page(ctx, oobs_html=admin_hdr_oob + edit_oob, content_html=content) + return oob_page(ctx, oobs_html=admin_hdr_oob, content_html=content) # ---- Post settings ---- @@ -1516,32 +1470,16 @@ async def render_post_edit_oob(ctx: dict) -> str: async def render_post_settings_page(ctx: dict) -> str: root_hdr = root_header_html(ctx) post_hdr = _post_header_html(ctx) - admin_hdr = _post_admin_header_html(ctx) - from quart import url_for as qurl - slug = (ctx.get("post") or {}).get("slug", "") - settings_hdr = _post_sub_admin_header_html( - "post_settings-row", "post_settings-header-child", - qurl("blog.post.admin.settings", slug=slug), - "cog", "settings", ctx, - ) - header_rows = root_hdr + post_hdr + admin_hdr + settings_hdr + admin_hdr = _post_admin_header_html(ctx, selected="settings") + header_rows = root_hdr + post_hdr + admin_hdr content = ctx.get("settings_html", "") return full_page(ctx, header_rows_html=header_rows, content_html=content) async def render_post_settings_oob(ctx: dict) -> str: - admin_hdr_oob = _post_admin_header_html(ctx, oob=True) - from quart import url_for as qurl - slug = (ctx.get("post") or {}).get("slug", "") - settings_hdr = _post_sub_admin_header_html( - "post_settings-row", "post_settings-header-child", - qurl("blog.post.admin.settings", slug=slug), - "cog", "settings", ctx, - ) - settings_oob = _oob_header_html("post-admin-header-child", "post_settings-header-child", - settings_hdr) + admin_hdr_oob = _post_admin_header_html(ctx, oob=True, selected="settings") content = ctx.get("settings_html", "") - return oob_page(ctx, oobs_html=admin_hdr_oob + settings_oob, content_html=content) + return oob_page(ctx, oobs_html=admin_hdr_oob, content_html=content) # ---- Settings home ----