Redesign the frontend UI on twui
Rebuild every page on a shared set of twui-based primitives with a dense ops-console look, WireGuard red accent and light/dark/system theming. - Add design tokens (WireGuard red accent, zinc neutrals, theme-flipping semantic colours) and a no-flash theme cookie + toggle - Add shared components: page header, panel, sub-nav, data table with mobile card list, stat tile, status dot, key-value list, copy button, inline alert, empty state, form section/actions, confirm dialog and toast providers - New admin shell (sidebar, sticky header, mobile drawer) and a centered shell for auth and 404; strip the public template pages - Unify adding clients at /admin/clients/add with a host picker; tabbed client detail (Overview / Rules / Config & QR) with config copy/download - Re-run client setup after rule changes on the detail page so the firewall matches the DB - Split Setup into status/maintenance/firewall and Settings into profile/password subpages - Replace window.confirm/alert and page reloads with in-app dialogs, toasts and in-place list updates - Fix nested rule form submitting the client form, prefilled signup passwords, hidden submit buttons, fields not updating after "next free" lookups, and TinyMCE white-on-white in dark mode Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import H1 from "@/src/components/twui/layout/H1";
|
||||
import Link from "@/src/components/twui/layout/Link";
|
||||
import P from "@/src/components/twui/layout/P";
|
||||
import Row from "@/src/components/twui/layout/Row";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import type { ActionMenuItem } from "../action-menu/types";
|
||||
import PageHeaderActions from "./page-header-actions";
|
||||
|
||||
type Props = {
|
||||
title: ReactNode;
|
||||
description?: ReactNode;
|
||||
/** Inline status line under the title (status dots, mono values) */
|
||||
meta?: ReactNode;
|
||||
primary_action?: ActionMenuItem;
|
||||
actions?: ActionMenuItem[];
|
||||
back?: { href: string; title: string };
|
||||
/** Rendered under the title row, e.g. a `SubNav` */
|
||||
nav?: ReactNode;
|
||||
};
|
||||
|
||||
export default function PageHeader({
|
||||
title,
|
||||
description,
|
||||
meta,
|
||||
primary_action,
|
||||
actions,
|
||||
back,
|
||||
nav,
|
||||
}: Props) {
|
||||
return (
|
||||
<Stack className="w-full gap-3 px-4 sm:px-6 pt-6 pb-5">
|
||||
{back ? (
|
||||
<Link
|
||||
href={back.href}
|
||||
className="text-[15px] text-muted! hover:text-fg! no-underline!"
|
||||
>
|
||||
<Row className="gap-1">
|
||||
<ArrowLeft size={14} />
|
||||
{back.title}
|
||||
</Row>
|
||||
</Link>
|
||||
) : null}
|
||||
<Row className="w-full justify-between items-start gap-4 flex-nowrap">
|
||||
<Stack className="gap-1.5 min-w-0">
|
||||
<H1 className="break-words">{title}</H1>
|
||||
{description ? (
|
||||
typeof description == "string" ? (
|
||||
<P className="text-[15px] max-w-2xl">
|
||||
{description}
|
||||
</P>
|
||||
) : (
|
||||
description
|
||||
)
|
||||
) : null}
|
||||
{meta ? <Row className="gap-x-4 gap-y-1">{meta}</Row> : null}
|
||||
</Stack>
|
||||
<PageHeaderActions
|
||||
primary_action={primary_action}
|
||||
actions={actions}
|
||||
/>
|
||||
</Row>
|
||||
{nav ? <div className="w-full pt-2">{nav}</div> : null}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user