separeate pages

This commit is contained in:
Abel Luck 2026-03-30 13:11:37 +02:00
parent 3fc999a69b
commit 9e826fcee8
9 changed files with 1376 additions and 924 deletions

View file

@ -19,7 +19,7 @@ def base_layout(*, page_title: str, stylesheet_href: str, content: Node) -> Rend
def nav_link(
*, label: str, active: bool = False, badge: str | None = None
*, label: str, href: str, active: bool = False, badge: str | None = None
) -> Renderable:
link_class = (
"group flex items-center justify-between rounded-xl px-3 py-2 text-sm font-medium transition "
@ -33,12 +33,228 @@ def nav_link(
"bg-amber-200 text-amber-950" if active else "bg-slate-800 text-slate-300"
)
return h.a(href="#", class_=link_class)[
return h.a(href=href, class_=link_class)[
h.span[label],
badge and h.span(class_=badge_class)[badge],
]
def admin_sidebar(*, current_path: str) -> Renderable:
return h.aside(
class_="relative overflow-hidden bg-slate-950 px-6 py-8 text-white lg:min-h-screen"
)[
h.div(
class_="absolute inset-x-0 top-0 h-40 bg-radial from-amber-400/25 via-amber-400/10 to-transparent"
),
h.div(class_="relative flex h-full flex-col")[
h.div(class_="flex items-center gap-3")[
h.div(
class_="flex size-11 items-center justify-center rounded-2xl bg-amber-400 text-base font-black text-slate-950"
)["AR"],
h.div[
h.p(
class_="text-xs font-semibold uppercase tracking-[0.24em] text-amber-300"
)["Republisher"],
h.p(class_="text-sm text-slate-300")["Admin spike"],
],
],
h.nav(class_="mt-10 space-y-2")[
nav_link(
label="Dashboard",
href="/",
active=current_path == "/",
badge="Live",
),
nav_link(
label="Sources",
href="/sources",
active=current_path.startswith("/sources"),
badge="12",
),
nav_link(
label="Runs",
href="/runs",
active=current_path.startswith("/runs")
or current_path.startswith("/job/"),
badge="3",
),
],
h.div(class_="mt-10 rounded-3xl border border-white/10 bg-white/5 p-5")[
h.p(
class_="text-xs font-semibold uppercase tracking-[0.22em] text-amber-300"
)["Operator notes"],
h.p(class_="mt-3 text-sm leading-6 text-slate-300")[
"Single-operator control plane over Tailscale. The current pass is layout only, but it follows the new full-page Datastar render pattern."
],
],
h.div(class_="mt-auto rounded-3xl bg-white/5 p-5 ring-1 ring-white/10")[
h.p(class_="text-sm font-semibold text-white")["Output root"],
h.p(class_="mt-2 font-mono text-sm text-slate-300")["/srv/anynews/out"],
h.p(class_="mt-4 text-xs uppercase tracking-[0.22em] text-slate-400")[
"Trusted network only"
],
],
],
]
def header_action_link(*, href: str, label: str) -> Renderable:
return h.a(
href=href,
class_="inline-flex items-center rounded-full bg-amber-400 px-4 py-2.5 text-sm font-semibold text-slate-950 shadow-sm transition hover:bg-amber-300",
)[label]
def header_secondary_link(*, href: str, label: str) -> Renderable:
return h.a(
href=href,
class_="inline-flex items-center rounded-full border border-white/15 bg-white/5 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-white/10",
)[label]
def muted_action_link(*, href: str, label: str) -> Renderable:
return h.a(
href=href,
class_="inline-flex items-center rounded-full border border-slate-200 bg-white px-3.5 py-2 text-sm font-semibold text-slate-700 shadow-sm transition hover:bg-slate-50",
)[label]
def inline_link(*, href: str, label: str, tone: str = "default") -> Renderable:
classes = {
"default": "text-slate-700 hover:text-slate-950",
"amber": "text-amber-700 hover:text-amber-800",
"rose": "text-rose-700 hover:text-rose-800",
}
return h.a(
href=href,
class_=f"inline-flex items-center whitespace-nowrap text-sm font-semibold {classes[tone]}",
)[label]
def inline_button(
*, label: str, tone: str = "default", disabled: bool = False
) -> Renderable:
classes = {
"default": "bg-stone-100 text-slate-700 hover:bg-stone-200",
"danger": "bg-rose-50 text-rose-700 hover:bg-rose-100",
"success": "bg-emerald-100 text-emerald-800 hover:bg-emerald-200",
}
class_name = (
"cursor-not-allowed bg-slate-100 text-slate-400" if disabled else classes[tone]
)
return h.button(
type="button",
disabled=disabled,
class_=f"inline-flex items-center whitespace-nowrap rounded-full px-3 py-1.5 text-sm font-semibold transition {class_name}",
)[label]
def page_shell(
*,
current_path: str,
eyebrow: str,
title: str,
description: str,
actions: Node | None = None,
content: Node,
) -> Renderable:
return h.main(
id="morph",
class_="min-h-screen lg:grid lg:grid-cols-[18rem_minmax(0,1fr)]",
)[
admin_sidebar(current_path=current_path),
h.div(class_="px-4 py-4 sm:px-5 lg:px-6 lg:py-5")[
h.div(class_="mx-auto max-w-7xl space-y-5")[
h.section[
h.div(
class_="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
)[
h.div(class_="max-w-3xl")[
h.h1(
class_="text-3xl font-semibold tracking-tight text-slate-950"
)[title],
(
description
and h.p(class_="mt-1 text-sm text-slate-600")[
description
]
),
],
actions and h.div(class_="flex flex-wrap gap-2")[actions],
]
],
content,
]
],
]
def section_card(*, content: Node) -> Renderable:
return h.section(class_="space-y-4")[content]
def table_section(
*,
eyebrow: str | None = None,
title: str,
subtitle: str,
headers: tuple[str, ...],
rows: tuple[tuple[Node, ...], ...],
actions: Node | None = None,
) -> Renderable:
def render_row(row: tuple[Node, ...]) -> Renderable:
first_cell, *other_cells = row
return h.tr(class_="align-top")[
h.td(class_="py-4 pr-6 pl-4 text-sm font-medium text-slate-950 sm:pl-6")[
first_cell
],
(
h.td(
class_="px-3 py-4 align-top text-sm whitespace-nowrap text-slate-600"
)[cell]
for cell in other_cells
),
]
return h.section[
h.div(class_="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between")[
h.div[
eyebrow
and h.p(
class_="text-xs font-semibold uppercase tracking-[0.22em] text-amber-600"
)[eyebrow],
h.h2(class_="mt-1 text-xl font-semibold text-slate-950")[title],
h.p(class_="mt-1 text-sm text-slate-600")[subtitle],
],
actions,
],
h.div(
class_="mt-3 overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-slate-200"
)[
h.div(class_="overflow-x-auto")[
h.table(
class_="relative w-full min-w-[72rem] divide-y divide-slate-200 table-auto"
)[
h.thead(class_="bg-stone-50")[
h.tr[
(
h.th(
scope="col",
class_="px-3 py-2.5 text-left text-xs font-semibold uppercase tracking-[0.18em] whitespace-nowrap text-slate-500 first:pl-4 sm:first:pl-6",
)[header]
for header in headers
)
]
],
h.tbody(class_="divide-y divide-slate-200 bg-white")[
(render_row(row) for row in rows)
],
]
]
],
]
def stat_card(*, label: str, value: str, detail: str) -> Renderable:
return h.div(
class_="rounded-3xl bg-white/85 p-5 shadow-sm ring-1 ring-slate-200 backdrop-blur"
@ -115,26 +331,46 @@ def textarea_field(
]
def toggle_field(*, label: str, description: str, checked: bool = False) -> Renderable:
wrapper_class = (
"group relative inline-flex w-11 shrink-0 rounded-full p-0.5 outline-offset-2 outline-amber-500 transition "
+ ("bg-amber-500" if checked else "bg-slate-200")
)
knob_class = (
"size-5 rounded-full bg-white shadow-xs ring-1 ring-slate-900/5 transition-transform "
+ ("translate-x-5" if checked else "translate-x-0")
)
def toggle_field(
*,
label: str,
description: str,
signal_name: str,
checked: bool = False,
) -> Renderable:
signal_value = str(checked).lower()
return h.div(class_="rounded-3xl bg-stone-50 p-4 ring-1 ring-slate-200")[
return h.div(
{"data-signals__ifmissing": f"{{{signal_name}: {signal_value}}}"},
class_="rounded-3xl bg-white p-4 shadow-sm",
)[
h.div(class_="flex items-start justify-between gap-4")[
h.div[
h.h3(class_="text-sm font-semibold text-slate-900")[label],
h.p(class_="mt-1 text-sm text-slate-600")[description],
],
h.label(class_="mt-0.5 cursor-pointer")[
h.div(class_=wrapper_class)[
h.span(class_=knob_class),
h.input(type="checkbox", checked=checked, class_="sr-only"),
h.div(
{
"data-class:bg-amber-500": f"${signal_name}",
"data-class:bg-slate-200": f"!${signal_name}",
},
class_="group relative inline-flex w-11 shrink-0 rounded-full bg-slate-200 p-0.5 outline-offset-2 outline-amber-500 transition",
)[
h.span(
{
"data-class:translate-x-5": f"${signal_name}",
"data-class:translate-x-0": f"!${signal_name}",
},
class_="size-5 translate-x-0 rounded-full bg-white shadow-xs ring-1 ring-slate-900/5 transition-transform",
),
h.input(
{"data-bind": signal_name},
type="checkbox",
name=signal_name,
checked=checked,
class_="sr-only",
),
],
],
]