155 lines
5.3 KiB
Python
155 lines
5.3 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import htpy as h
|
||
|
|
from htpy import Node, Renderable
|
||
|
|
|
||
|
|
|
||
|
|
def base_layout(*, page_title: str, stylesheet_href: str, content: Node) -> Renderable:
|
||
|
|
return h.html(lang="en", class_="h-full bg-slate-100")[
|
||
|
|
h.head[
|
||
|
|
h.meta(charset="utf-8"),
|
||
|
|
h.meta(name="viewport", content="width=device-width, initial-scale=1"),
|
||
|
|
h.title[page_title],
|
||
|
|
h.link(rel="stylesheet", href=stylesheet_href),
|
||
|
|
],
|
||
|
|
h.body(
|
||
|
|
class_="h-full bg-linear-to-br from-stone-100 via-amber-50 to-orange-100 text-slate-900"
|
||
|
|
)[content],
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def nav_link(
|
||
|
|
*, label: 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 "
|
||
|
|
+ (
|
||
|
|
"bg-white text-slate-950 shadow-sm ring-1 ring-white/10"
|
||
|
|
if active
|
||
|
|
else "text-slate-300 hover:bg-white/5 hover:text-white"
|
||
|
|
)
|
||
|
|
)
|
||
|
|
badge_class = "rounded-full px-2 py-0.5 text-[11px] font-semibold " + (
|
||
|
|
"bg-amber-200 text-amber-950" if active else "bg-slate-800 text-slate-300"
|
||
|
|
)
|
||
|
|
|
||
|
|
return h.a(href="#", class_=link_class)[
|
||
|
|
h.span[label],
|
||
|
|
badge and h.span(class_=badge_class)[badge],
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
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"
|
||
|
|
)[
|
||
|
|
h.dt(class_="text-sm font-medium text-slate-500")[label],
|
||
|
|
h.dd(class_="mt-3 text-3xl font-semibold tracking-tight text-slate-950")[value],
|
||
|
|
h.p(class_="mt-2 text-sm text-slate-600")[detail],
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def input_field(
|
||
|
|
*,
|
||
|
|
label: str,
|
||
|
|
field_id: str,
|
||
|
|
value: str = "",
|
||
|
|
placeholder: str = "",
|
||
|
|
help_text: str | None = None,
|
||
|
|
) -> Renderable:
|
||
|
|
return h.div[
|
||
|
|
h.label(for_=field_id, class_="block text-sm font-medium text-slate-900")[
|
||
|
|
label
|
||
|
|
],
|
||
|
|
h.input(
|
||
|
|
id=field_id,
|
||
|
|
name=field_id,
|
||
|
|
type="text",
|
||
|
|
value=value,
|
||
|
|
placeholder=placeholder,
|
||
|
|
class_="mt-2 block w-full rounded-2xl border-0 bg-white px-3.5 py-2.5 text-sm text-slate-900 shadow-sm ring-1 ring-slate-200 placeholder:text-slate-400 focus:outline-hidden focus:ring-2 focus:ring-amber-500",
|
||
|
|
),
|
||
|
|
help_text and h.p(class_="mt-2 text-xs text-slate-500")[help_text],
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def select_field(
|
||
|
|
*,
|
||
|
|
label: str,
|
||
|
|
field_id: str,
|
||
|
|
options: tuple[str, ...],
|
||
|
|
selected: str,
|
||
|
|
help_text: str | None = None,
|
||
|
|
) -> Renderable:
|
||
|
|
return h.div[
|
||
|
|
h.label(for_=field_id, class_="block text-sm font-medium text-slate-900")[
|
||
|
|
label
|
||
|
|
],
|
||
|
|
h.select(
|
||
|
|
id=field_id,
|
||
|
|
name=field_id,
|
||
|
|
class_="mt-2 block w-full rounded-2xl border-0 bg-white px-3.5 py-2.5 text-sm text-slate-900 shadow-sm ring-1 ring-slate-200 focus:outline-hidden focus:ring-2 focus:ring-amber-500",
|
||
|
|
)[
|
||
|
|
(
|
||
|
|
h.option(value=option, selected=option == selected)[option]
|
||
|
|
for option in options
|
||
|
|
)
|
||
|
|
],
|
||
|
|
help_text and h.p(class_="mt-2 text-xs text-slate-500")[help_text],
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def textarea_field(
|
||
|
|
*, label: str, field_id: str, value: str, rows: str = "4"
|
||
|
|
) -> Renderable:
|
||
|
|
return h.div[
|
||
|
|
h.label(for_=field_id, class_="block text-sm font-medium text-slate-900")[
|
||
|
|
label
|
||
|
|
],
|
||
|
|
h.textarea(
|
||
|
|
id=field_id,
|
||
|
|
name=field_id,
|
||
|
|
rows=rows,
|
||
|
|
class_="mt-2 block w-full rounded-2xl border-0 bg-white px-3.5 py-2.5 text-sm text-slate-900 shadow-sm ring-1 ring-slate-200 placeholder:text-slate-400 focus:outline-hidden focus:ring-2 focus:ring-amber-500",
|
||
|
|
)[value],
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
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")
|
||
|
|
)
|
||
|
|
|
||
|
|
return h.div(class_="rounded-3xl bg-stone-50 p-4 ring-1 ring-slate-200")[
|
||
|
|
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"),
|
||
|
|
],
|
||
|
|
],
|
||
|
|
]
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def status_badge(*, label: str, tone: str) -> Renderable:
|
||
|
|
tones = {
|
||
|
|
"running": "bg-emerald-100 text-emerald-800",
|
||
|
|
"scheduled": "bg-sky-100 text-sky-800",
|
||
|
|
"idle": "bg-slate-200 text-slate-700",
|
||
|
|
"failed": "bg-rose-100 text-rose-800",
|
||
|
|
"done": "bg-amber-100 text-amber-800",
|
||
|
|
}
|
||
|
|
return h.span(
|
||
|
|
class_=f"inline-flex rounded-full px-2.5 py-1 text-xs font-semibold {tones[tone]}"
|
||
|
|
)[label]
|