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:
2026-10-16 06:29:21 +01:00
co-authored by Claude Opus 5.5
parent e50b0405b0
commit cad1d32ae4
291 changed files with 6436 additions and 6612 deletions
@@ -0,0 +1,123 @@
import { useState } from "react";
import { Plus } from "lucide-react";
import Modal from "@/src/components/twui/elements/Modal";
import Button from "@/src/components/twui/layout/Button";
import H3 from "@/src/components/twui/layout/H3";
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 InlineAlert from "../inline-alert";
import ClientRuleFields from "./client-rule-fields";
import buildClientRuleDrafts from "./build-client-rule-drafts";
import { createClientRuleDraft, type ClientRuleDraft } from "./client-rule-draft";
export type ClientRuleModalAddResult = { success: boolean; msg?: string };
type Props = {
onAdd: ({ rules }: { rules: ClientRuleDraft[] }) => Promise<ClientRuleModalAddResult>;
disabled?: boolean;
};
/** Validates a rule and hands it to `onAdd`. Closes only on success. */
export default function ClientRuleModal({ onAdd, disabled }: Props) {
const [open, setOpen] = useState(false);
const [draft, setDraft] = useState<ClientRuleDraft>(createClientRuleDraft());
const [error, setError] = useState<string>();
const [saving, setSaving] = useState(false);
function reset() {
setDraft(createClientRuleDraft());
setError(undefined);
}
async function handleAdd() {
const built = buildClientRuleDrafts({ draft });
if (!built.success) {
setError(built.msg);
return;
}
setSaving(true);
const res = await onAdd({ rules: built.drafts });
setSaving(false);
if (!res.success) {
setError(res.msg || "Couldn't add the rule");
return;
}
reset();
setOpen(false);
}
return (
<Modal
open={open}
setOpen={(value) => {
setOpen(value);
if (value === false) reset();
}}
className="max-w-lg p-5"
target={
<Button
title="Add rule"
type="button"
variant="outlined"
color="gray"
size="small"
disabled={disabled}
>
<Plus size={14} />
Add rule
</Button>
}
>
<Stack className="w-full items-stretch gap-5">
<Stack className="gap-1 pr-8">
<H3 className="text-lg!">Add access rule</H3>
<P className="text-[15px]">
Allow this client to reach a destination. Leave ports empty for any port.
</P>
</Stack>
{error ? <InlineAlert tone="error" msg={error} /> : null}
<ClientRuleFields
key={draft.local_id}
draft={draft}
setDraft={(next) => {
setDraft(next);
setError(undefined);
}}
onEnter={handleAdd}
/>
<Row className="w-full justify-end gap-2 pt-1">
<Button
title="Cancel"
type="button"
variant="outlined"
color="gray"
size="small"
onClick={() => {
reset();
setOpen(false);
}}
>
Cancel
</Button>
<Button
title="Add rule"
type="button"
size="small"
loading={saving}
onClick={handleAdd}
>
Add rule
</Button>
</Row>
</Stack>
</Modal>
);
}