From 9c60d11c2c49b206f8c8d105e6d57998d4235b6e Mon Sep 17 00:00:00 2001 From: fuomag9 <1580624+fuomag9@users.noreply.github.com> Date: Sun, 22 Mar 2026 22:17:56 +0100 Subject: [PATCH] feat: improve UI contrast, dark mode, dialog sizing, color coherence, and add table sorting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix dialog scrollability (flex layout + max-h-[90dvh]) and increase L4 dialog to lg width - Add styled enable card to L4 dialog matching proxy host pattern - Unify section colors across proxy host and L4 dialogs (cyan=LB, emerald=DNS, violet=upstream DNS, rose=geo, amber=mTLS) - Improve light mode contrast: muted-foreground oklch 0.552→0.502, remove opacity modifiers on secondary text - Improve dark mode: boost muted-foreground to 0.85, increase border opacity 10%→16%, input 15%→20% - Add bg-card to DataTable wrapper and bg-muted/40 to table headers for surface hierarchy - Add semantic badge variants (success, warning, info, muted) and StatusChip dark mode fix - Add server-side sortable columns to Proxy Hosts and L4 Proxy Hosts (name, upstream, status, protocol, listen) - Add sortKey to DataTable Column type with clickable sort headers (ArrowUp/Down indicators, URL param driven) - Fix E2E test selectors for shadcn UI (label associations, combobox roles, dropdown menus, mobile drawer) - Add htmlFor/id to proxy host form fields and aria-labels to select triggers for accessibility - Add sorting E2E tests for both proxy host pages Co-Authored-By: Claude Opus 4.6 (1M context) --- app/(dashboard)/OverviewClient.tsx | 178 ++- .../access-lists/AccessListsClient.tsx | 281 ++-- app/(dashboard)/analytics/AnalyticsClient.tsx | 4 +- .../certificates/CertificatesClient.tsx | 70 +- .../certificates/components/AcmeTab.tsx | 53 +- .../certificates/components/CaTab.tsx | 82 +- .../components/ImportCertDrawer.tsx | 1 + .../certificates/components/ImportedTab.tsx | 61 +- .../certificates/components/RelativeTime.tsx | 35 +- .../components/StatusSummaryBar.tsx | 93 +- .../l4-proxy-hosts/L4ProxyHostsClient.tsx | 85 +- app/(dashboard)/l4-proxy-hosts/page.tsx | 9 +- .../proxy-hosts/ProxyHostsClient.tsx | 94 +- app/(dashboard)/proxy-hosts/page.tsx | 9 +- app/(dashboard)/settings/SettingsClient.tsx | 1146 +++++++++-------- app/(dashboard)/waf/WafEventsClient.tsx | 6 +- app/globals.css | 18 +- .../l4-proxy-hosts/L4HostDialogs.tsx | 128 +- .../proxy-hosts/DnsResolverFields.tsx | 2 +- src/components/proxy-hosts/GeoBlockFields.tsx | 4 +- src/components/proxy-hosts/HostDialogs.tsx | 25 +- .../proxy-hosts/LoadBalancerFields.tsx | 2 +- src/components/proxy-hosts/MtlsConfig.tsx | 4 +- src/components/proxy-hosts/RewriteFields.tsx | 3 +- .../UpstreamDnsResolutionFields.tsx | 2 +- src/components/ui/AppDialog.tsx | 2 +- src/components/ui/DataTable.tsx | 54 +- src/components/ui/StatusChip.tsx | 2 +- src/components/ui/badge.tsx | 8 + src/components/ui/dialog.tsx | 2 +- src/components/ui/table.tsx | 2 +- src/lib/models/l4-proxy-hosts.ts | 24 +- src/lib/models/proxy-hosts.ts | 23 +- tests/e2e/audit-log.spec.ts | 2 +- tests/e2e/functional/l4-proxy-routing.spec.ts | 8 +- .../functional/path-prefix-rewrite.spec.ts | 2 +- tests/e2e/functional/proxy-routing.spec.ts | 6 +- .../e2e/functional/redirects-advanced.spec.ts | 2 +- tests/e2e/functional/redirects.spec.ts | 2 +- tests/e2e/functional/ssl-redirect.spec.ts | 2 +- tests/e2e/l4-proxy-hosts.spec.ts | 52 +- tests/e2e/mobile/mobile-layout.spec.ts | 29 +- tests/e2e/proxy-hosts.spec.ts | 31 +- tests/helpers/l4-proxy-api.ts | 6 +- tests/helpers/proxy-api.ts | 14 +- 45 files changed, 1616 insertions(+), 1052 deletions(-) diff --git a/app/(dashboard)/OverviewClient.tsx b/app/(dashboard)/OverviewClient.tsx index f0dc99c5..8b21755a 100644 --- a/app/(dashboard)/OverviewClient.tsx +++ b/app/(dashboard)/OverviewClient.tsx @@ -1,10 +1,9 @@ "use client"; import Link from "next/link"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { BarChart2 } from "lucide-react"; +import { Card, CardContent } from "@/components/ui/card"; +import { BarChart2, Activity } from "lucide-react"; import { ReactNode } from "react"; -import { Separator } from "@/components/ui/separator"; type StatCard = { label: string; @@ -23,6 +22,36 @@ type TrafficSummary = { blockedPercent: number; } | null; +// Per-position accent colors for stat cards (proxy hosts, certs, access lists, traffic) +const CARD_ACCENTS = [ + { border: "border-l-violet-500", icon: "border-violet-500/30 bg-violet-500/10 text-violet-500", count: "text-violet-600 dark:text-violet-400" }, + { border: "border-l-emerald-500", icon: "border-emerald-500/30 bg-emerald-500/10 text-emerald-500", count: "text-emerald-600 dark:text-emerald-400" }, + { border: "border-l-amber-500", icon: "border-amber-500/30 bg-amber-500/10 text-amber-500", count: "text-amber-600 dark:text-amber-400" }, +]; + +const TRAFFIC_ACCENT = { + border: "border-l-cyan-500", + icon: "border-cyan-500/30 bg-cyan-500/10 text-cyan-500", + count: "text-cyan-600 dark:text-cyan-400", +}; + +function getEventDotColor(summary: string): string { + const lower = summary.toLowerCase(); + if (lower.startsWith("delete") || lower.startsWith("remove")) return "bg-rose-500 shadow-[0_0_6px_rgba(244,63,94,0.5)]"; + if (lower.startsWith("create") || lower.startsWith("add")) return "bg-emerald-500 shadow-[0_0_6px_rgba(16,185,129,0.5)]"; + return "bg-primary shadow-[0_0_6px_var(--primary)]"; +} + +function formatRelativeTime(iso: string): string { + const diff = Date.now() - new Date(iso).getTime(); + const mins = Math.floor(diff / 60000); + if (mins < 1) return "just now"; + if (mins < 60) return `${mins}m ago`; + const hrs = Math.floor(mins / 60); + if (hrs < 24) return `${hrs}h ago`; + return new Date(iso).toLocaleDateString(); +} + export default function OverviewClient({ userName, stats, @@ -36,89 +65,108 @@ export default function OverviewClient({ }) { return (
+ {/* Welcome header */}

- Welcome back, {userName} + Welcome back, {userName}

Everything you need to orchestrate Caddy proxies, certificates, and secure edge services.

+ {/* Stat grid */}
- {stats.map((stat) => ( - - - - - {stat.label} - -
- {stat.icon} -
-
- -
{stat.count}
-
-
- - ))} + {stats.map((stat, i) => { + const accent = CARD_ACCENTS[i % CARD_ACCENTS.length]; + return ( + + + +
+
+ {stat.icon} +
+ + {stat.count} + +
+

{stat.label}

+
+
+ + ); + })} {/* Traffic (24h) card */} - - - - - Traffic (24h) - -
- + + + +
+
+ +
+ + {trafficSummary ? trafficSummary.totalRequests.toLocaleString() : "—"} +
- - - {trafficSummary ? ( - <> -
- {trafficSummary.totalRequests.toLocaleString()} +

Traffic (24h)

+ {trafficSummary && trafficSummary.totalRequests > 0 && ( +
+
+ Blocked + 0 ? "text-rose-500" : "text-muted-foreground"}`}> + {trafficSummary.blockedPercent}% +
- {trafficSummary.totalRequests > 0 && ( -

0 ? "text-destructive" : "text-muted-foreground"}`}> - {trafficSummary.blockedPercent}% blocked -

- )} - - ) : ( -
+
+
+
+
)}
- - - Recent Activity - - - {recentEvents.length === 0 ? ( -

No activity recorded yet.

- ) : ( -
- {recentEvents.map((event, index) => ( -
- {index > 0 && } -
- {event.summary} - - {new Date(event.created_at).toLocaleString()} + {/* Recent Activity */} +
+
+
+ +
+

Recent Activity

+
+ + + + {recentEvents.length === 0 ? ( +

No activity recorded yet.

+ ) : ( +
+ {/* Vertical timeline line */} +
+ {recentEvents.map((event, index) => ( +
+ {/* Dot */} +
+ {event.summary} + + {formatRelativeTime(event.created_at)}
-
- ))} -
- )} - - + ))} +
+ )} +
+
+
); } diff --git a/app/(dashboard)/access-lists/AccessListsClient.tsx b/app/(dashboard)/access-lists/AccessListsClient.tsx index 170612c1..67771cec 100644 --- a/app/(dashboard)/access-lists/AccessListsClient.tsx +++ b/app/(dashboard)/access-lists/AccessListsClient.tsx @@ -1,6 +1,6 @@ "use client"; -import { Trash2 } from "lucide-react"; +import { Shield, Trash2, UserPlus, Plus } from "lucide-react"; import { PageHeader } from "@/components/ui/PageHeader"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; @@ -22,6 +22,44 @@ type Props = { pagination: { total: number; page: number; perPage: number }; }; +// Cycling accent colors per card +const ACCENT_COLORS = [ + { + border: "border-l-violet-500", + icon: "border-violet-500/30 bg-violet-500/10 text-violet-500", + countBadge: "border-violet-500/30 bg-violet-500/10 text-violet-600 dark:text-violet-400", + avatar: "bg-violet-500/15 text-violet-600 dark:text-violet-400", + }, + { + border: "border-l-cyan-500", + icon: "border-cyan-500/30 bg-cyan-500/10 text-cyan-500", + countBadge: "border-cyan-500/30 bg-cyan-500/10 text-cyan-600 dark:text-cyan-400", + avatar: "bg-cyan-500/15 text-cyan-600 dark:text-cyan-400", + }, + { + border: "border-l-emerald-500", + icon: "border-emerald-500/30 bg-emerald-500/10 text-emerald-500", + countBadge: "border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400", + avatar: "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400", + }, + { + border: "border-l-amber-500", + icon: "border-amber-500/30 bg-amber-500/10 text-amber-500", + countBadge: "border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400", + avatar: "bg-amber-500/15 text-amber-600 dark:text-amber-400", + }, + { + border: "border-l-rose-500", + icon: "border-rose-500/30 bg-rose-500/10 text-rose-500", + countBadge: "border-rose-500/30 bg-rose-500/10 text-rose-600 dark:text-rose-400", + avatar: "bg-rose-500/15 text-rose-600 dark:text-rose-400", + }, +]; + +function getInitials(username: string): string { + return username.slice(0, 2).toUpperCase(); +} + export default function AccessListsClient({ lists, pagination }: Props) { const router = useRouter(); const pathname = usePathname(); @@ -42,89 +80,124 @@ export default function AccessListsClient({ lists, pagination }: Props) { />
- {lists.map((list) => ( - - -
updateAccessListAction(list.id, formData)} className="flex flex-col gap-3"> -

Access List

-
- - -
-
- -