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,30 @@
|
||||
type Params = {
|
||||
is_dark: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* CSS for the TinyMCE content iframe. The iframe is its own document, so it
|
||||
* doesn't see the app's tokens. Its `color-scheme` must match the page's:
|
||||
* when they differ the browser paints the iframe with an opaque white canvas,
|
||||
* which made dark-mode text white on white.
|
||||
*/
|
||||
export default function buildRteContentStyle({ is_dark }: Params) {
|
||||
const background = is_dark ? "#0f0f11" : "#ffffff";
|
||||
const color = is_dark ? "#fafafa" : "#18181b";
|
||||
const placeholder = is_dark ? "#71717a" : "#a1a1aa";
|
||||
const link = is_dark ? "#ef7a7e" : "#89161b";
|
||||
|
||||
return `
|
||||
:root { color-scheme: ${is_dark ? "dark" : "light"}; }
|
||||
body {
|
||||
font-family: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
background-color: ${background};
|
||||
color: ${color};
|
||||
margin: 12px 14px;
|
||||
}
|
||||
a { color: ${link}; }
|
||||
.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before { color: ${placeholder}; }
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { ThemeMode } from "@/src/dict/theme-dict";
|
||||
|
||||
type Params = {
|
||||
mode: ThemeMode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles the `.dark` class on `<html>` for the given mode. Only touches that
|
||||
* one class so other classes on the element survive.
|
||||
*/
|
||||
export default function applyTheme({ mode }: Params) {
|
||||
const dark =
|
||||
mode == "dark" ||
|
||||
(mode == "system" &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||
|
||||
document.documentElement.classList.toggle("dark", dark);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import {
|
||||
ThemeCookieMaxAge,
|
||||
ThemeCookieName,
|
||||
type ThemeMode,
|
||||
} from "@/src/dict/theme-dict";
|
||||
|
||||
type Params = {
|
||||
mode: ThemeMode;
|
||||
};
|
||||
|
||||
export default function setThemeCookie({ mode }: Params) {
|
||||
document.cookie = `${ThemeCookieName}=${mode}; Path=/; SameSite=Lax; Max-Age=${ThemeCookieMaxAge}`;
|
||||
}
|
||||
Reference in New Issue
Block a user