Enable cross-subdomain htmx and purify layout to sexp
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m15s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m15s
- Disable htmx selfRequestsOnly, add CORS headers for *.rose-ash.com - Remove same-origin guards from ~menu-row and ~nav-link htmx attrs - Convert ~app-layout from string-concatenated HTML to pure sexp tree - Extract ~app-head component, replace ~app-shell with inline structure - Convert hamburger SVG from Python HTML constant to ~hamburger sexp component - Fix cross-domain fragment URLs (events_url, market_url) - Fix starts-with? primitive to handle nil values - Fix duplicate admin menu rows on OOB swaps - Add calendar admin nav links (slots, description) - Convert slots page from Jinja to sexp rendering - Disable page caching in development mode - Backfill migration to clean orphaned container_relations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -86,19 +86,21 @@ def _post_header_html(ctx: dict, *, oob: bool = False) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Admin link
|
# Admin link
|
||||||
from quart import url_for as qurl, g
|
from quart import url_for as qurl, g, request
|
||||||
rights = ctx.get("rights") or {}
|
rights = ctx.get("rights") or {}
|
||||||
has_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False)
|
has_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False)
|
||||||
if has_admin:
|
if has_admin:
|
||||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
|
||||||
select_colours = ctx.get("select_colours", "")
|
select_colours = ctx.get("select_colours", "")
|
||||||
styles = ctx.get("styles") or {}
|
styles = ctx.get("styles") or {}
|
||||||
nav_btn = styles.get("nav_button", "") if isinstance(styles, dict) else getattr(styles, "nav_button", "")
|
nav_btn = styles.get("nav_button", "") if isinstance(styles, dict) else getattr(styles, "nav_button", "")
|
||||||
admin_href = qurl("blog.post.admin.admin", slug=slug)
|
admin_href = qurl("blog.post.admin.admin", slug=slug)
|
||||||
|
is_admin_page = "/admin" in request.path
|
||||||
|
sel_attr = ' aria-selected="true"' if is_admin_page else ''
|
||||||
nav_parts.append(
|
nav_parts.append(
|
||||||
f'<div class="relative nav-group"><a href="{admin_href}"'
|
f'<div class="relative nav-group"><a href="{admin_href}"'
|
||||||
f' hx-get="{admin_href}" hx-target="#main-panel" hx-select="{hx_select}"'
|
f' hx-get="{admin_href}" hx-target="#main-panel" hx-select="#main-panel"'
|
||||||
f' hx-swap="outerHTML" hx-push-url="true"'
|
f' hx-swap="outerHTML" hx-push-url="true"'
|
||||||
|
f'{sel_attr}'
|
||||||
f' class="{nav_btn} {select_colours}">'
|
f' class="{nav_btn} {select_colours}">'
|
||||||
f'<i class="fa fa-cog" aria-hidden="true"></i></a></div>'
|
f'<i class="fa fa-cog" aria-hidden="true"></i></a></div>'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -77,9 +77,23 @@ def register():
|
|||||||
|
|
||||||
@bp.context_processor
|
@bp.context_processor
|
||||||
async def inject_root():
|
async def inject_root():
|
||||||
|
from shared.infrastructure.fragments import fetch_fragment
|
||||||
|
|
||||||
|
container_nav_html = ""
|
||||||
|
post_data = getattr(g, "post_data", None)
|
||||||
|
if post_data:
|
||||||
|
post_id = post_data["post"]["id"]
|
||||||
|
post_slug = post_data["post"]["slug"]
|
||||||
|
container_nav_html = await fetch_fragment("relations", "container-nav", params={
|
||||||
|
"container_type": "page",
|
||||||
|
"container_id": str(post_id),
|
||||||
|
"post_slug": post_slug,
|
||||||
|
"exclude": "page->calendar",
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"calendar": getattr(g, "calendar", None),
|
"calendar": getattr(g, "calendar", None),
|
||||||
|
"container_nav_html": container_nav_html,
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------- Pages ----------
|
# ---------- Pages ----------
|
||||||
|
|||||||
@@ -44,16 +44,14 @@ def register():
|
|||||||
|
|
||||||
@bp.get("/")
|
@bp.get("/")
|
||||||
async def get(**kwargs):
|
async def get(**kwargs):
|
||||||
|
from shared.sexp.page import get_template_context
|
||||||
|
from sexp.sexp_components import render_slots_page, render_slots_oob
|
||||||
|
|
||||||
|
tctx = await get_template_context()
|
||||||
if not is_htmx_request():
|
if not is_htmx_request():
|
||||||
# Normal browser request: full page with layout
|
html = await render_slots_page(tctx)
|
||||||
html = await render_template(
|
|
||||||
"_types/slots/index.html",
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
|
html = await render_slots_oob(tctx)
|
||||||
html = await render_template(
|
|
||||||
"_types/slots/_oob_elements.html",
|
|
||||||
)
|
|
||||||
return await make_response(html)
|
return await make_response(html)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -79,34 +79,31 @@ def _post_header_html(ctx: dict, *, oob: bool = False) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _post_nav_html(ctx: dict) -> str:
|
def _post_nav_html(ctx: dict) -> str:
|
||||||
"""Post desktop nav: calendar links + admin gear."""
|
"""Post desktop nav: calendar links + container nav (markets, etc.)."""
|
||||||
from quart import url_for
|
from quart import url_for, g
|
||||||
|
|
||||||
calendars = ctx.get("calendars") or []
|
calendars = ctx.get("calendars") or []
|
||||||
rights = ctx.get("rights") or {}
|
|
||||||
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
|
|
||||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
|
||||||
select_colours = ctx.get("select_colours", "")
|
select_colours = ctx.get("select_colours", "")
|
||||||
post = ctx.get("post") or {}
|
current_cal_slug = getattr(g, "calendar_slug", None)
|
||||||
slug = post.get("slug", "")
|
|
||||||
|
|
||||||
parts = []
|
parts = []
|
||||||
for cal in calendars:
|
for cal in calendars:
|
||||||
cal_slug = getattr(cal, "slug", "") if hasattr(cal, "slug") else cal.get("slug", "")
|
cal_slug = getattr(cal, "slug", "") if hasattr(cal, "slug") else cal.get("slug", "")
|
||||||
cal_name = getattr(cal, "name", "") if hasattr(cal, "name") else cal.get("name", "")
|
cal_name = getattr(cal, "name", "") if hasattr(cal, "name") else cal.get("name", "")
|
||||||
href = url_for("calendars.calendar.get", calendar_slug=cal_slug)
|
href = url_for("calendars.calendar.get", calendar_slug=cal_slug)
|
||||||
|
is_sel = (cal_slug == current_cal_slug)
|
||||||
parts.append(sexp(
|
parts.append(sexp(
|
||||||
'(~nav-link :href h :icon "fa fa-calendar" :label l :select-colours sc)',
|
'(~nav-link :href h :icon "fa fa-calendar" :label l :select-colours sc :is-selected sel)',
|
||||||
h=href,
|
h=href,
|
||||||
l=cal_name,
|
l=cal_name,
|
||||||
sc=select_colours,
|
sc=select_colours,
|
||||||
|
sel=is_sel,
|
||||||
))
|
))
|
||||||
if is_admin:
|
# Container nav fragments (markets, etc.)
|
||||||
admin_href = call_url(ctx, "blog_url", f"/{slug}/admin/")
|
container_nav = ctx.get("container_nav_html", "")
|
||||||
parts.append(
|
if container_nav:
|
||||||
f'<a href="{admin_href}" class="flex items-center gap-2 px-3 py-2 rounded">'
|
parts.append(container_nav)
|
||||||
f'<i class="fa fa-cog" aria-hidden="true"></i></a>'
|
|
||||||
)
|
|
||||||
return "".join(parts)
|
return "".join(parts)
|
||||||
|
|
||||||
|
|
||||||
@@ -114,21 +111,6 @@ def _post_nav_html(ctx: dict) -> str:
|
|||||||
# Post admin header
|
# Post admin header
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _post_admin_header_html(ctx: dict, *, oob: bool = False) -> str:
|
|
||||||
"""Build the post-admin-level header row."""
|
|
||||||
post = ctx.get("post") or {}
|
|
||||||
slug = post.get("slug", "")
|
|
||||||
link_href = call_url(ctx, "blog_url", f"/{slug}/admin/")
|
|
||||||
|
|
||||||
return sexp(
|
|
||||||
'(~menu-row :id "post-admin-row" :level 2'
|
|
||||||
' :link-href lh :link-label "admin" :icon "fa fa-cog"'
|
|
||||||
' :child-id "post-admin-header-child" :oob oob)',
|
|
||||||
lh=link_href,
|
|
||||||
oob=oob,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Calendars header
|
# Calendars header
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -347,11 +329,30 @@ def _day_admin_header_html(ctx: dict, *, oob: bool = False) -> str:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _calendar_admin_header_html(ctx: dict, *, oob: bool = False) -> str:
|
def _calendar_admin_header_html(ctx: dict, *, oob: bool = False) -> str:
|
||||||
"""Build calendar admin header row."""
|
"""Build calendar admin header row with nav links."""
|
||||||
|
from quart import url_for
|
||||||
|
calendar = ctx.get("calendar")
|
||||||
|
cal_slug = getattr(calendar, "slug", "") if calendar else ""
|
||||||
|
select_colours = ctx.get("select_colours", "")
|
||||||
|
|
||||||
|
nav_parts = []
|
||||||
|
if cal_slug:
|
||||||
|
for endpoint, label in [
|
||||||
|
("calendars.calendar.slots.get", "slots"),
|
||||||
|
("calendars.calendar.admin.calendar_description_edit", "description"),
|
||||||
|
]:
|
||||||
|
href = url_for(endpoint, calendar_slug=cal_slug)
|
||||||
|
nav_parts.append(sexp(
|
||||||
|
'(~nav-link :href h :label l :select-colours sc)',
|
||||||
|
h=href, l=label, sc=select_colours,
|
||||||
|
))
|
||||||
|
|
||||||
|
nav_html = "".join(nav_parts)
|
||||||
return sexp(
|
return sexp(
|
||||||
'(~menu-row :id "calendar-admin-row" :level 4'
|
'(~menu-row :id "calendar-admin-row" :level 4'
|
||||||
' :link-label "admin" :icon "fa fa-cog"'
|
' :link-label "admin" :icon "fa fa-cog"'
|
||||||
' :child-id "calendar-admin-header-child" :oob oob)',
|
' :nav-html nh :child-id "calendar-admin-header-child" :oob oob)',
|
||||||
|
nh=nav_html,
|
||||||
oob=oob,
|
oob=oob,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1659,7 +1660,7 @@ async def render_calendars_page(ctx: dict) -> str:
|
|||||||
"""Full page: calendars listing."""
|
"""Full page: calendars listing."""
|
||||||
content = _calendars_main_panel_html(ctx)
|
content = _calendars_main_panel_html(ctx)
|
||||||
hdr = root_header_html(ctx)
|
hdr = root_header_html(ctx)
|
||||||
child = _post_header_html(ctx) + _post_admin_header_html(ctx) + _calendars_header_html(ctx)
|
child = _post_header_html(ctx) + _calendars_header_html(ctx)
|
||||||
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
||||||
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
||||||
|
|
||||||
@@ -1667,8 +1668,8 @@ async def render_calendars_page(ctx: dict) -> str:
|
|||||||
async def render_calendars_oob(ctx: dict) -> str:
|
async def render_calendars_oob(ctx: dict) -> str:
|
||||||
"""OOB response: calendars listing."""
|
"""OOB response: calendars listing."""
|
||||||
content = _calendars_main_panel_html(ctx)
|
content = _calendars_main_panel_html(ctx)
|
||||||
oobs = _post_admin_header_html(ctx, oob=True)
|
oobs = _post_header_html(ctx, oob=True)
|
||||||
oobs += _oob_header_html("post-admin-header-child", "calendars-header-child",
|
oobs += _oob_header_html("post-header-child", "calendars-header-child",
|
||||||
_calendars_header_html(ctx))
|
_calendars_header_html(ctx))
|
||||||
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
||||||
|
|
||||||
@@ -1681,7 +1682,7 @@ async def render_calendar_page(ctx: dict) -> str:
|
|||||||
"""Full page: calendar month view."""
|
"""Full page: calendar month view."""
|
||||||
content = _calendar_main_panel_html(ctx)
|
content = _calendar_main_panel_html(ctx)
|
||||||
hdr = root_header_html(ctx)
|
hdr = root_header_html(ctx)
|
||||||
child = _post_header_html(ctx) + _post_admin_header_html(ctx) + _calendar_header_html(ctx)
|
child = _post_header_html(ctx) + _calendar_header_html(ctx)
|
||||||
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
||||||
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
||||||
|
|
||||||
@@ -1703,7 +1704,7 @@ async def render_day_page(ctx: dict) -> str:
|
|||||||
"""Full page: day detail."""
|
"""Full page: day detail."""
|
||||||
content = _day_main_panel_html(ctx)
|
content = _day_main_panel_html(ctx)
|
||||||
hdr = root_header_html(ctx)
|
hdr = root_header_html(ctx)
|
||||||
child = (_post_header_html(ctx) + _post_admin_header_html(ctx)
|
child = (_post_header_html(ctx)
|
||||||
+ _calendar_header_html(ctx) + _day_header_html(ctx))
|
+ _calendar_header_html(ctx) + _day_header_html(ctx))
|
||||||
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
||||||
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
||||||
@@ -1726,7 +1727,7 @@ async def render_day_admin_page(ctx: dict) -> str:
|
|||||||
"""Full page: day admin."""
|
"""Full page: day admin."""
|
||||||
content = _day_admin_main_panel_html(ctx)
|
content = _day_admin_main_panel_html(ctx)
|
||||||
hdr = root_header_html(ctx)
|
hdr = root_header_html(ctx)
|
||||||
child = (_post_header_html(ctx) + _post_admin_header_html(ctx)
|
child = (_post_header_html(ctx)
|
||||||
+ _calendar_header_html(ctx) + _day_header_html(ctx)
|
+ _calendar_header_html(ctx) + _day_header_html(ctx)
|
||||||
+ _day_admin_header_html(ctx))
|
+ _day_admin_header_html(ctx))
|
||||||
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
||||||
@@ -1750,7 +1751,7 @@ async def render_calendar_admin_page(ctx: dict) -> str:
|
|||||||
"""Full page: calendar admin."""
|
"""Full page: calendar admin."""
|
||||||
content = _calendar_admin_main_panel_html(ctx)
|
content = _calendar_admin_main_panel_html(ctx)
|
||||||
hdr = root_header_html(ctx)
|
hdr = root_header_html(ctx)
|
||||||
child = (_post_header_html(ctx) + _post_admin_header_html(ctx)
|
child = (_post_header_html(ctx)
|
||||||
+ _calendar_header_html(ctx) + _calendar_admin_header_html(ctx))
|
+ _calendar_header_html(ctx) + _calendar_admin_header_html(ctx))
|
||||||
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
||||||
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
||||||
@@ -1765,6 +1766,32 @@ async def render_calendar_admin_oob(ctx: dict) -> str:
|
|||||||
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Slots
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def render_slots_page(ctx: dict) -> str:
|
||||||
|
"""Full page: slots listing."""
|
||||||
|
from quart import g
|
||||||
|
slots = ctx.get("slots") or []
|
||||||
|
calendar = ctx.get("calendar")
|
||||||
|
content = render_slots_table(slots, calendar)
|
||||||
|
hdr = root_header_html(ctx)
|
||||||
|
child = (_post_header_html(ctx)
|
||||||
|
+ _calendar_header_html(ctx) + _calendar_admin_header_html(ctx))
|
||||||
|
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
||||||
|
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
||||||
|
|
||||||
|
|
||||||
|
async def render_slots_oob(ctx: dict) -> str:
|
||||||
|
"""OOB response: slots listing."""
|
||||||
|
slots = ctx.get("slots") or []
|
||||||
|
calendar = ctx.get("calendar")
|
||||||
|
content = render_slots_table(slots, calendar)
|
||||||
|
oobs = _calendar_admin_header_html(ctx, oob=True)
|
||||||
|
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Tickets
|
# Tickets
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1820,7 +1847,7 @@ async def render_markets_page(ctx: dict) -> str:
|
|||||||
"""Full page: markets listing."""
|
"""Full page: markets listing."""
|
||||||
content = _markets_main_panel_html(ctx)
|
content = _markets_main_panel_html(ctx)
|
||||||
hdr = root_header_html(ctx)
|
hdr = root_header_html(ctx)
|
||||||
child = _post_header_html(ctx) + _post_admin_header_html(ctx) + _markets_header_html(ctx)
|
child = _post_header_html(ctx) + _markets_header_html(ctx)
|
||||||
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
||||||
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
||||||
|
|
||||||
@@ -1828,8 +1855,8 @@ async def render_markets_page(ctx: dict) -> str:
|
|||||||
async def render_markets_oob(ctx: dict) -> str:
|
async def render_markets_oob(ctx: dict) -> str:
|
||||||
"""OOB response: markets listing."""
|
"""OOB response: markets listing."""
|
||||||
content = _markets_main_panel_html(ctx)
|
content = _markets_main_panel_html(ctx)
|
||||||
oobs = _post_admin_header_html(ctx, oob=True)
|
oobs = _post_header_html(ctx, oob=True)
|
||||||
oobs += _oob_header_html("post-admin-header-child", "markets-header-child",
|
oobs += _oob_header_html("post-header-child", "markets-header-child",
|
||||||
_markets_header_html(ctx))
|
_markets_header_html(ctx))
|
||||||
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
||||||
|
|
||||||
@@ -1842,7 +1869,7 @@ async def render_payments_page(ctx: dict) -> str:
|
|||||||
"""Full page: payments admin."""
|
"""Full page: payments admin."""
|
||||||
content = _payments_main_panel_html(ctx)
|
content = _payments_main_panel_html(ctx)
|
||||||
hdr = root_header_html(ctx)
|
hdr = root_header_html(ctx)
|
||||||
child = _post_header_html(ctx) + _post_admin_header_html(ctx) + _payments_header_html(ctx)
|
child = _post_header_html(ctx) + _payments_header_html(ctx)
|
||||||
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
||||||
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
||||||
|
|
||||||
@@ -1850,8 +1877,8 @@ async def render_payments_page(ctx: dict) -> str:
|
|||||||
async def render_payments_oob(ctx: dict) -> str:
|
async def render_payments_oob(ctx: dict) -> str:
|
||||||
"""OOB response: payments admin."""
|
"""OOB response: payments admin."""
|
||||||
content = _payments_main_panel_html(ctx)
|
content = _payments_main_panel_html(ctx)
|
||||||
oobs = _post_admin_header_html(ctx, oob=True)
|
oobs = _post_header_html(ctx, oob=True)
|
||||||
oobs += _oob_header_html("post-admin-header-child", "payments-header-child",
|
oobs += _oob_header_html("post-header-child", "payments-header-child",
|
||||||
_payments_header_html(ctx))
|
_payments_header_html(ctx))
|
||||||
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
||||||
|
|
||||||
@@ -2322,7 +2349,7 @@ async def render_entry_page(ctx: dict) -> str:
|
|||||||
"""Full page: entry detail."""
|
"""Full page: entry detail."""
|
||||||
content = _entry_main_panel_html(ctx)
|
content = _entry_main_panel_html(ctx)
|
||||||
hdr = root_header_html(ctx)
|
hdr = root_header_html(ctx)
|
||||||
child = (_post_header_html(ctx) + _post_admin_header_html(ctx)
|
child = (_post_header_html(ctx)
|
||||||
+ _calendar_header_html(ctx) + _day_header_html(ctx)
|
+ _calendar_header_html(ctx) + _day_header_html(ctx)
|
||||||
+ _entry_header_html(ctx))
|
+ _entry_header_html(ctx))
|
||||||
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from shared.sexp.helpers import (
|
|||||||
call_url, get_asset_url, root_header_html,
|
call_url, get_asset_url, root_header_html,
|
||||||
search_mobile_html, search_desktop_html, full_page, oob_page,
|
search_mobile_html, search_desktop_html, full_page, oob_page,
|
||||||
)
|
)
|
||||||
from shared.sexp.page import HAMBURGER_HTML
|
|
||||||
from shared.infrastructure.urls import market_product_url, cart_url
|
from shared.infrastructure.urls import market_product_url, cart_url
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""Backfill label + metadata for calendar relations.
|
||||||
|
|
||||||
|
Removes calendar relations with no metadata slug and no label,
|
||||||
|
which are either orphaned or were created before metadata was tracked.
|
||||||
|
|
||||||
|
Revision ID: relations_0003
|
||||||
|
Revises: relations_0002
|
||||||
|
Create Date: 2026-02-28
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = "relations_0003"
|
||||||
|
down_revision = "relations_0002"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.execute("""
|
||||||
|
DELETE FROM container_relations
|
||||||
|
WHERE child_type IN ('calendar', 'market')
|
||||||
|
AND (metadata IS NULL OR metadata->>'slug' IS NULL)
|
||||||
|
AND label IS NULL
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
@@ -43,6 +43,12 @@ def register():
|
|||||||
from shared.sexp.jinja_bridge import sexp as render_sexp
|
from shared.sexp.jinja_bridge import sexp as render_sexp
|
||||||
from shared.sexp.relations import relations_from
|
from shared.sexp.relations import relations_from
|
||||||
from shared.services.relationships import get_children
|
from shared.services.relationships import get_children
|
||||||
|
from shared.infrastructure.urls import events_url, market_url
|
||||||
|
|
||||||
|
_SERVICE_URL = {
|
||||||
|
"calendar": events_url,
|
||||||
|
"market": market_url,
|
||||||
|
}
|
||||||
|
|
||||||
container_type = request.args.get("container_type", "page")
|
container_type = request.args.get("container_type", "page")
|
||||||
container_id = int(request.args.get("container_id", 0))
|
container_id = int(request.args.get("container_id", 0))
|
||||||
@@ -70,7 +76,17 @@ def register():
|
|||||||
)
|
)
|
||||||
for child in children:
|
for child in children:
|
||||||
slug = (child.metadata_ or {}).get("slug", "")
|
slug = (child.metadata_ or {}).get("slug", "")
|
||||||
href = f"/{post_slug}/{slug}/" if post_slug else f"/{slug}/"
|
if not slug:
|
||||||
|
continue
|
||||||
|
nav_label = defn.nav_label or ""
|
||||||
|
if post_slug and nav_label:
|
||||||
|
path = f"/{post_slug}/{nav_label}/{slug}/"
|
||||||
|
elif post_slug:
|
||||||
|
path = f"/{post_slug}/{slug}/"
|
||||||
|
else:
|
||||||
|
path = f"/{slug}/"
|
||||||
|
url_fn = _SERVICE_URL.get(defn.to_type)
|
||||||
|
href = url_fn(path) if url_fn else path
|
||||||
parts.append(render_sexp(
|
parts.append(render_sexp(
|
||||||
'(~relation-nav :href href :name name :icon icon :nav-class nav-class :relation-type relation-type)',
|
'(~relation-nav :href href :name name :icon icon :nav-class nav-class :relation-type relation-type)',
|
||||||
href=href,
|
href=href,
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ def cache_page(
|
|||||||
async def wrapper(*args, **kwargs):
|
async def wrapper(*args, **kwargs):
|
||||||
r = get_redis()
|
r = get_redis()
|
||||||
|
|
||||||
if not r or request.method != "GET":
|
if not r or request.method != "GET" or current_app.config.get("NO_PAGE_CACHE"):
|
||||||
return await view(*args, **kwargs)
|
return await view(*args, **kwargs)
|
||||||
uid = get_user_id()
|
uid = get_user_id()
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<link rel="stylesheet" type="text/css" href="{{asset_url('styles/blog-content.css')}}">
|
<link rel="stylesheet" type="text/css" href="{{asset_url('styles/blog-content.css')}}">
|
||||||
|
|
||||||
<script src="https://unpkg.com/htmx.org@2.0.8"></script>
|
<script src="https://unpkg.com/htmx.org@2.0.8"></script>
|
||||||
|
<meta name="htmx-config" content='{"selfRequestsOnly":false}'>
|
||||||
<script src="https://unpkg.com/hyperscript.org@0.9.12"></script>
|
<script src="https://unpkg.com/hyperscript.org@0.9.12"></script>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<link rel="stylesheet" href="{{asset_url('fontawesome/css/all.min.css')}}">
|
<link rel="stylesheet" href="{{asset_url('fontawesome/css/all.min.css')}}">
|
||||||
|
|||||||
@@ -102,6 +102,10 @@ def create_base_app(
|
|||||||
# Cache app prefix for key namespacing
|
# Cache app prefix for key namespacing
|
||||||
app.config["CACHE_APP_PREFIX"] = name
|
app.config["CACHE_APP_PREFIX"] = name
|
||||||
|
|
||||||
|
# Disable page caching in development
|
||||||
|
env = os.getenv("ENVIRONMENT", "development")
|
||||||
|
app.config["NO_PAGE_CACHE"] = env not in ("production", "staging")
|
||||||
|
|
||||||
# --- infrastructure ---
|
# --- infrastructure ---
|
||||||
register_middleware(app)
|
register_middleware(app)
|
||||||
register_db(app)
|
register_db(app)
|
||||||
@@ -276,6 +280,15 @@ def create_base_app(
|
|||||||
response.delete_cookie("blog_session", domain=".rose-ash.com", path="/")
|
response.delete_cookie("blog_session", domain=".rose-ash.com", path="/")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@app.after_request
|
||||||
|
async def _cors_for_subdomains(response):
|
||||||
|
origin = request.headers.get("Origin", "")
|
||||||
|
if origin.endswith(".rose-ash.com") or origin.endswith(".localhost"):
|
||||||
|
response.headers["Access-Control-Allow-Origin"] = origin
|
||||||
|
response.headers["Access-Control-Allow-Credentials"] = "true"
|
||||||
|
response.headers["Access-Control-Allow-Headers"] = "HX-Request, HX-Target, HX-Current-URL, HX-Trigger, HX-Boosted, Content-Type"
|
||||||
|
return response
|
||||||
|
|
||||||
@app.after_request
|
@app.after_request
|
||||||
async def _add_hx_preserve_search_header(response):
|
async def _add_hx_preserve_search_header(response):
|
||||||
value = request.headers.get("X-Search")
|
value = request.headers.get("X-Search")
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .jinja_bridge import sexp
|
from .jinja_bridge import sexp
|
||||||
from .page import HAMBURGER_HTML, SEARCH_HEADERS_MOBILE, SEARCH_HEADERS_DESKTOP
|
from .page import SEARCH_HEADERS_MOBILE, SEARCH_HEADERS_DESKTOP
|
||||||
|
|
||||||
|
|
||||||
def call_url(ctx: dict, key: str, path: str = "/") -> str:
|
def call_url(ctx: dict, key: str, path: str = "/") -> str:
|
||||||
@@ -34,14 +34,13 @@ def root_header_html(ctx: dict, *, oob: bool = False) -> str:
|
|||||||
return sexp(
|
return sexp(
|
||||||
'(~header-row :cart-mini-html cmi :blog-url bu :site-title st'
|
'(~header-row :cart-mini-html cmi :blog-url bu :site-title st'
|
||||||
' :nav-tree-html nth :auth-menu-html amh :nav-panel-html nph'
|
' :nav-tree-html nth :auth-menu-html amh :nav-panel-html nph'
|
||||||
' :hamburger-html hh :oob oob)',
|
' :oob oob)',
|
||||||
cmi=ctx.get("cart_mini_html", ""),
|
cmi=ctx.get("cart_mini_html", ""),
|
||||||
bu=call_url(ctx, "blog_url", ""),
|
bu=call_url(ctx, "blog_url", ""),
|
||||||
st=ctx.get("base_title", ""),
|
st=ctx.get("base_title", ""),
|
||||||
nth=ctx.get("nav_tree_html", ""),
|
nth=ctx.get("nav_tree_html", ""),
|
||||||
amh=ctx.get("auth_menu_html", ""),
|
amh=ctx.get("auth_menu_html", ""),
|
||||||
nph=ctx.get("nav_panel_html", ""),
|
nph=ctx.get("nav_panel_html", ""),
|
||||||
hh=HAMBURGER_HTML,
|
|
||||||
oob=oob,
|
oob=oob,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -30,19 +30,6 @@ from typing import Any
|
|||||||
|
|
||||||
from .jinja_bridge import sexp
|
from .jinja_bridge import sexp
|
||||||
|
|
||||||
# HTML constants used by layout components — kept here to avoid
|
|
||||||
# s-expression parser issues with embedded quotes in SVG.
|
|
||||||
HAMBURGER_HTML = (
|
|
||||||
'<div class="md:hidden bg-stone-200 rounded">'
|
|
||||||
'<svg class="h-12 w-12 transition-transform group-open/root:hidden block self-start"'
|
|
||||||
' viewBox="0 0 24 24" fill="none" stroke="currentColor">'
|
|
||||||
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"'
|
|
||||||
' d="M4 6h16M4 12h16M4 18h16"/></svg>'
|
|
||||||
'<svg aria-hidden="true" viewBox="0 0 24 24"'
|
|
||||||
' class="w-12 h-12 rotate-180 transition-transform group-open/root:block hidden self-start">'
|
|
||||||
'<path d="M6 9l6 6 6-6" fill="currentColor"/></svg></div>'
|
|
||||||
)
|
|
||||||
|
|
||||||
SEARCH_HEADERS_MOBILE = '{"X-Origin":"search-mobile","X-Search":"true"}'
|
SEARCH_HEADERS_MOBILE = '{"X-Origin":"search-mobile","X-Search":"true"}'
|
||||||
SEARCH_HEADERS_DESKTOP = '{"X-Origin":"search-desktop","X-Search":"true"}'
|
SEARCH_HEADERS_DESKTOP = '{"X-Origin":"search-desktop","X-Search":"true"}'
|
||||||
|
|
||||||
@@ -88,6 +75,11 @@ async def get_template_context(**kwargs: Any) -> dict[str, Any]:
|
|||||||
if key not in ctx:
|
if key not in ctx:
|
||||||
ctx[key] = val
|
ctx[key] = val
|
||||||
|
|
||||||
|
# Expose request-scoped values that sexp components need
|
||||||
|
from quart import g
|
||||||
|
if "rights" not in ctx:
|
||||||
|
ctx["rights"] = getattr(g, "rights", {})
|
||||||
|
|
||||||
ctx.update(kwargs)
|
ctx.update(kwargs)
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|||||||
@@ -255,7 +255,9 @@ def prim_join(sep: str, coll: list) -> str:
|
|||||||
return sep.join(str(x) for x in coll)
|
return sep.join(str(x) for x in coll)
|
||||||
|
|
||||||
@register_primitive("starts-with?")
|
@register_primitive("starts-with?")
|
||||||
def prim_starts_with(s: str, prefix: str) -> bool:
|
def prim_starts_with(s, prefix: str) -> bool:
|
||||||
|
if not isinstance(s, str):
|
||||||
|
return False
|
||||||
return s.startswith(prefix)
|
return s.startswith(prefix)
|
||||||
|
|
||||||
@register_primitive("ends-with?")
|
@register_primitive("ends-with?")
|
||||||
|
|||||||
@@ -1,89 +1,76 @@
|
|||||||
(defcomp ~app-shell (&key title asset-url meta-html body-html body-end-html)
|
(defcomp ~app-head (&key title asset-url meta-html)
|
||||||
(<>
|
(head
|
||||||
(raw! "<!doctype html>")
|
(meta :charset "utf-8")
|
||||||
(html :lang "en"
|
(meta :name "viewport" :content "width=device-width, initial-scale=1")
|
||||||
(head
|
(meta :name "robots" :content "index,follow")
|
||||||
(meta :charset "utf-8")
|
(meta :name "theme-color" :content "#ffffff")
|
||||||
(meta :name "viewport" :content "width=device-width, initial-scale=1")
|
(title title)
|
||||||
(meta :name "robots" :content "index,follow")
|
(when meta-html (raw! meta-html))
|
||||||
(meta :name "theme-color" :content "#ffffff")
|
(style "@media (min-width: 768px) { .js-mobile-sentinel { display:none !important; } }")
|
||||||
(title title)
|
(link :rel "stylesheet" :type "text/css" :href (str asset-url "/styles/basics.css"))
|
||||||
(when meta-html (raw! meta-html))
|
(link :rel "stylesheet" :type "text/css" :href (str asset-url "/styles/cards.css"))
|
||||||
(style "@media (min-width: 768px) { .js-mobile-sentinel { display:none !important; } }")
|
(link :rel "stylesheet" :type "text/css" :href (str asset-url "/styles/blog-content.css"))
|
||||||
(link :rel "stylesheet" :type "text/css" :href (str asset-url "/styles/basics.css"))
|
(script :src "https://unpkg.com/htmx.org@2.0.8")
|
||||||
(link :rel "stylesheet" :type "text/css" :href (str asset-url "/styles/cards.css"))
|
(meta :name "htmx-config" :content "{\"selfRequestsOnly\":false}")
|
||||||
(link :rel "stylesheet" :type "text/css" :href (str asset-url "/styles/blog-content.css"))
|
(script :src "https://unpkg.com/hyperscript.org@0.9.12")
|
||||||
(script :src "https://unpkg.com/htmx.org@2.0.8")
|
(script :src "https://cdn.tailwindcss.com")
|
||||||
(script :src "https://unpkg.com/hyperscript.org@0.9.12")
|
(link :rel "stylesheet" :href (str asset-url "/fontawesome/css/all.min.css"))
|
||||||
(script :src "https://cdn.tailwindcss.com")
|
(link :rel "stylesheet" :href (str asset-url "/fontawesome/css/v4-shims.min.css"))
|
||||||
(link :rel "stylesheet" :href (str asset-url "/fontawesome/css/all.min.css"))
|
(link :href "https://unpkg.com/prismjs/themes/prism.css" :rel "stylesheet")
|
||||||
(link :rel "stylesheet" :href (str asset-url "/fontawesome/css/v4-shims.min.css"))
|
(script :src "https://unpkg.com/prismjs/prism.js")
|
||||||
(link :href "https://unpkg.com/prismjs/themes/prism.css" :rel "stylesheet")
|
(script :src "https://unpkg.com/prismjs/components/prism-javascript.min.js")
|
||||||
(script :src "https://unpkg.com/prismjs/prism.js")
|
(script :src "https://unpkg.com/prismjs/components/prism-python.min.js")
|
||||||
(script :src "https://unpkg.com/prismjs/components/prism-javascript.min.js")
|
(script :src "https://unpkg.com/prismjs/components/prism-bash.min.js")
|
||||||
(script :src "https://unpkg.com/prismjs/components/prism-python.min.js")
|
(script :src "https://cdn.jsdelivr.net/npm/sweetalert2@11")
|
||||||
(script :src "https://unpkg.com/prismjs/components/prism-bash.min.js")
|
(script "if(matchMedia('(hover:hover) and (pointer:fine)').matches){document.documentElement.classList.add('hover-capable')}")
|
||||||
(script :src "https://cdn.jsdelivr.net/npm/sweetalert2@11")
|
(script "document.addEventListener('click',function(e){var t=e.target.closest('[data-close-details]');if(!t)return;var d=t.closest('details');if(d)d.removeAttribute('open')})")
|
||||||
(script "if(matchMedia('(hover:hover) and (pointer:fine)').matches){document.documentElement.classList.add('hover-capable')}")
|
(style
|
||||||
(script "document.addEventListener('click',function(e){var t=e.target.closest('[data-close-details]');if(!t)return;var d=t.closest('details');if(d)d.removeAttribute('open')})")
|
"details[data-toggle-group=\"mobile-panels\"]>summary{list-style:none}"
|
||||||
(style
|
"details[data-toggle-group=\"mobile-panels\"]>summary::-webkit-details-marker{display:none}"
|
||||||
"details[data-toggle-group=\"mobile-panels\"]>summary{list-style:none}"
|
"@media(min-width:768px){.nav-group:focus-within .submenu,.nav-group:hover .submenu{display:block}}"
|
||||||
"details[data-toggle-group=\"mobile-panels\"]>summary::-webkit-details-marker{display:none}"
|
"img{max-width:100%;height:auto}"
|
||||||
"@media(min-width:768px){.nav-group:focus-within .submenu,.nav-group:hover .submenu{display:block}}"
|
".clamp-2{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}"
|
||||||
"img{max-width:100%;height:auto}"
|
".clamp-3{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}"
|
||||||
".clamp-2{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}"
|
".no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}"
|
||||||
".clamp-3{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}"
|
"details.group{overflow:hidden}details.group>summary{list-style:none}details.group>summary::-webkit-details-marker{display:none}"
|
||||||
".no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}"
|
".htmx-indicator{display:none}.htmx-request .htmx-indicator{display:inline-flex}"
|
||||||
"details.group{overflow:hidden}details.group>summary{list-style:none}details.group>summary::-webkit-details-marker{display:none}"
|
".js-wrap.open .js-pop{display:block}.js-wrap.open .js-backdrop{display:block}")))
|
||||||
".htmx-indicator{display:none}.htmx-request .htmx-indicator{display:inline-flex}"
|
|
||||||
".js-wrap.open .js-pop{display:block}.js-wrap.open .js-backdrop{display:block}"))
|
|
||||||
(body :class "bg-stone-50 text-stone-900"
|
|
||||||
(raw! body-html)
|
|
||||||
(when body-end-html (raw! body-end-html))
|
|
||||||
(script :src (str asset-url "/scripts/body.js"))))))
|
|
||||||
|
|
||||||
(defcomp ~app-layout (&key title asset-url meta-html menu-colour
|
(defcomp ~app-layout (&key title asset-url meta-html menu-colour
|
||||||
header-rows-html menu-html
|
header-rows-html menu-html
|
||||||
filter-html aside-html content-html
|
filter-html aside-html content-html
|
||||||
body-end-html)
|
body-end-html)
|
||||||
(let* ((colour (or menu-colour "sky")))
|
(let* ((colour (or menu-colour "sky")))
|
||||||
(~app-shell :title (or title "Rose Ash") :asset-url asset-url
|
(<>
|
||||||
:meta-html meta-html :body-end-html body-end-html
|
(raw! "<!doctype html>")
|
||||||
:body-html (str
|
(html :lang "en"
|
||||||
"<div class=\"max-w-screen-2xl mx-auto py-1 px-1\">"
|
(~app-head :title (or title "Rose Ash") :asset-url asset-url :meta-html meta-html)
|
||||||
"<div class=\"w-full\">"
|
(body :class "bg-stone-50 text-stone-900"
|
||||||
"<details class=\"group/root p-2\" data-toggle-group=\"mobile-panels\">"
|
(div :class "max-w-screen-2xl mx-auto py-1 px-1"
|
||||||
"<summary>"
|
(div :class "w-full"
|
||||||
"<header class=\"z-50\">"
|
(details :class "group/root p-2" :data-toggle-group "mobile-panels"
|
||||||
"<div id=\"root-header-summary\" class=\"flex items-start gap-2 p-1 bg-" colour "-500\">"
|
(summary
|
||||||
"<div class=\"flex flex-col w-full items-center\">"
|
(header :class "z-50"
|
||||||
header-rows-html
|
(div :id "root-header-summary"
|
||||||
"</div>"
|
:class (str "flex items-start gap-2 p-1 bg-" colour "-500")
|
||||||
"</div>"
|
(div :class "flex flex-col w-full items-center"
|
||||||
"</header>"
|
(when header-rows-html (raw! header-rows-html))))))
|
||||||
"</summary>"
|
(div :id "root-menu" :hx-swap-oob "outerHTML" :class "md:hidden"
|
||||||
"<div id=\"root-menu\" hx-swap-oob=\"outerHTML\" class=\"md:hidden\">"
|
(when menu-html (raw! menu-html)))))
|
||||||
(or menu-html "")
|
(div :id "filter"
|
||||||
"</div>"
|
(when filter-html (raw! filter-html)))
|
||||||
"</details>"
|
(main :id "root-panel" :class "max-w-full"
|
||||||
"</div>"
|
(div :class "md:min-h-0"
|
||||||
"<div id=\"filter\">"
|
(div :class "flex flex-row md:h-full md:min-h-0"
|
||||||
(or filter-html "")
|
(aside :id "aside"
|
||||||
"</div>"
|
:class "hidden md:flex md:flex-col max-w-xs md:h-full md:min-h-0 mr-3"
|
||||||
"<main id=\"root-panel\" class=\"max-w-full\">"
|
(when aside-html (raw! aside-html)))
|
||||||
"<div class=\"md:min-h-0\">"
|
(section :id "main-panel"
|
||||||
"<div class=\"flex flex-row md:h-full md:min-h-0\">"
|
:class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"
|
||||||
"<aside id=\"aside\" class=\"hidden md:flex md:flex-col max-w-xs md:h-full md:min-h-0 mr-3\">"
|
(when content-html (raw! content-html))
|
||||||
(or aside-html "")
|
(div :class "pb-8"))))))
|
||||||
"</aside>"
|
(when body-end-html (raw! body-end-html))
|
||||||
"<section id=\"main-panel\" class=\"flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport\">"
|
(script :src (str asset-url "/scripts/body.js")))))))
|
||||||
(or content-html "")
|
|
||||||
"<div class=\"pb-8\"></div>"
|
|
||||||
"</section>"
|
|
||||||
"</div>"
|
|
||||||
"</div>"
|
|
||||||
"</main>"
|
|
||||||
"</div>"))))
|
|
||||||
|
|
||||||
(defcomp ~oob-response (&key oobs-html filter-html aside-html menu-html content-html)
|
(defcomp ~oob-response (&key oobs-html filter-html aside-html menu-html content-html)
|
||||||
(<>
|
(<>
|
||||||
@@ -99,9 +86,19 @@
|
|||||||
:class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"
|
:class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"
|
||||||
(when content-html (raw! content-html)))))
|
(when content-html (raw! content-html)))))
|
||||||
|
|
||||||
|
(defcomp ~hamburger ()
|
||||||
|
(div :class "md:hidden bg-stone-200 rounded"
|
||||||
|
(svg :class "h-12 w-12 transition-transform group-open/root:hidden block self-start"
|
||||||
|
:viewBox "0 0 24 24" :fill "none" :stroke "currentColor"
|
||||||
|
(path :stroke-linecap "round" :stroke-linejoin "round" :stroke-width "2"
|
||||||
|
:d "M4 6h16M4 12h16M4 18h16"))
|
||||||
|
(svg :aria-hidden "true" :viewBox "0 0 24 24"
|
||||||
|
:class "w-12 h-12 rotate-180 transition-transform group-open/root:block hidden self-start"
|
||||||
|
(path :d "M6 9l6 6 6-6" :fill "currentColor"))))
|
||||||
|
|
||||||
(defcomp ~header-row (&key cart-mini-html blog-url site-title
|
(defcomp ~header-row (&key cart-mini-html blog-url site-title
|
||||||
nav-tree-html auth-menu-html nav-panel-html
|
nav-tree-html auth-menu-html nav-panel-html
|
||||||
settings-url is-admin oob hamburger-html)
|
settings-url is-admin oob)
|
||||||
(<>
|
(<>
|
||||||
(div :id "root-row"
|
(div :id "root-row"
|
||||||
:hx-swap-oob (if oob "outerHTML" nil)
|
:hx-swap-oob (if oob "outerHTML" nil)
|
||||||
@@ -118,7 +115,7 @@
|
|||||||
(when (and is-admin settings-url)
|
(when (and is-admin settings-url)
|
||||||
(a :href settings-url :class "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3"
|
(a :href settings-url :class "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3"
|
||||||
(i :class "fa fa-cog" :aria-hidden "true"))))
|
(i :class "fa fa-cog" :aria-hidden "true"))))
|
||||||
(when hamburger-html (raw! hamburger-html))))
|
(~hamburger)))
|
||||||
(div :class "block md:hidden text-md font-bold"
|
(div :class "block md:hidden text-md font-bold"
|
||||||
(when auth-menu-html (raw! auth-menu-html)))))
|
(when auth-menu-html (raw! auth-menu-html)))))
|
||||||
|
|
||||||
@@ -145,11 +142,11 @@
|
|||||||
(when nav-html
|
(when nav-html
|
||||||
(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"
|
||||||
(raw! nav-html))))
|
(raw! nav-html))))
|
||||||
(when child-id
|
(when (and child-id (not oob))
|
||||||
(div :id child-id :class "flex flex-col w-full items-center"
|
(div :id child-id :class "flex flex-col w-full items-center"
|
||||||
(when child-html (raw! child-html)))))))
|
(when child-html (raw! child-html)))))))
|
||||||
|
|
||||||
(defcomp ~nav-link (&key href hx-select label icon aclass select-colours)
|
(defcomp ~nav-link (&key href hx-select label icon aclass select-colours is-selected)
|
||||||
(div :class "relative nav-group"
|
(div :class "relative nav-group"
|
||||||
(a :href href
|
(a :href href
|
||||||
:hx-get href
|
:hx-get href
|
||||||
@@ -157,6 +154,7 @@
|
|||||||
:hx-select (or hx-select "#main-panel")
|
:hx-select (or hx-select "#main-panel")
|
||||||
:hx-swap "outerHTML"
|
:hx-swap "outerHTML"
|
||||||
:hx-push-url "true"
|
:hx-push-url "true"
|
||||||
|
:aria-selected (when is-selected "true")
|
||||||
:class (or aclass
|
:class (or aclass
|
||||||
(str "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3 "
|
(str "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3 "
|
||||||
(or select-colours "")))
|
(or select-colours "")))
|
||||||
|
|||||||
Reference in New Issue
Block a user