feat: dashboard theme overhaul — light content area with branded teal sidebar

Complete frontend visual redesign using certctl logo color palette:
- Deep teal sidebar (#0c2e25) with prominent centered logo (64px in white pill)
- Light content area (#f0f4f8) with white cards and visible borders
- Brand colors from logo: teal (#2ea88f), blue (#3b7dd8), orange (#e8873a), green (#4ebe6e)
- Inter + JetBrains Mono typography, colored stat card top borders
- All 17 pages + 7 components updated (25 files, ~700 lines changed)
- 15 new dashboard screenshots replacing old dark theme screenshots
- Prometheus metrics e2e test added, integration test mock fixes
- Docs updated: architecture.md theme description, testing-guide.md DNS-PERSIST-01 coverage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shankar0123
2026-03-26 23:27:42 -04:00
parent 8380cb7946
commit 50c520e1ff
48 changed files with 699 additions and 519 deletions
+3 -3
View File
@@ -7,10 +7,10 @@ export default function AuthGate({ children }: { children: ReactNode }) {
if (loading) {
return (
<div className="min-h-screen bg-slate-900 flex items-center justify-center">
<div className="min-h-screen bg-page flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold text-blue-400 mb-2">certctl</h1>
<p className="text-sm text-slate-400">Connecting...</p>
<h1 className="text-2xl font-bold text-brand-500 mb-2">certctl</h1>
<p className="text-sm text-ink-muted">Connecting...</p>
</div>
</div>
);
+8 -8
View File
@@ -20,7 +20,7 @@ interface DataTableProps<T> {
export default function DataTable<T>({ columns, data, onRowClick, emptyMessage, isLoading, keyField = 'id', selectable, selectedKeys, onSelectionChange }: DataTableProps<T>) {
if (isLoading) {
return (
<div className="flex items-center justify-center py-16 text-slate-400">
<div className="flex items-center justify-center py-16 text-ink-muted">
<svg className="animate-spin h-5 w-5 mr-3" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
@@ -32,7 +32,7 @@ export default function DataTable<T>({ columns, data, onRowClick, emptyMessage,
if (!data.length) {
return (
<div className="flex items-center justify-center py-16 text-slate-500">
<div className="flex items-center justify-center py-16 text-ink-faint">
{emptyMessage || 'No data found'}
</div>
);
@@ -62,19 +62,19 @@ export default function DataTable<T>({ columns, data, onRowClick, emptyMessage,
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b-2 border-slate-700">
<tr className="border-b-2 border-surface-border bg-surface-muted">
{selectable && (
<th className="px-3 py-3 w-10">
<input
type="checkbox"
checked={allSelected || false}
onChange={toggleAll}
className="rounded border-slate-600 bg-slate-900 text-blue-600 focus:ring-blue-500 focus:ring-offset-0 cursor-pointer"
className="rounded border-surface-border bg-white text-brand-500 focus:ring-brand-500 focus:ring-offset-0 cursor-pointer"
/>
</th>
)}
{columns.map(col => (
<th key={col.key} className={`px-4 py-3 text-left text-xs font-semibold text-slate-400 uppercase tracking-wider ${col.className || ''}`}>
<th key={col.key} className={`px-4 py-3 text-left text-xs font-semibold text-ink-muted uppercase tracking-wider ${col.className || ''}`}>
{col.label}
</th>
))}
@@ -88,7 +88,7 @@ export default function DataTable<T>({ columns, data, onRowClick, emptyMessage,
<tr
key={rowKey}
onClick={() => onRowClick?.(item)}
className={`border-b border-slate-700/50 transition-colors hover:bg-blue-500/5 ${onRowClick ? 'cursor-pointer' : ''} ${isSelected ? 'bg-blue-500/10' : ''}`}
className={`border-b border-surface-border/50 transition-colors hover:bg-surface-muted ${onRowClick ? 'cursor-pointer' : ''} ${isSelected ? 'bg-brand-50' : ''}`}
>
{selectable && (
<td className="px-3 py-3 w-10">
@@ -97,12 +97,12 @@ export default function DataTable<T>({ columns, data, onRowClick, emptyMessage,
checked={isSelected || false}
onChange={(e) => { e.stopPropagation(); toggleOne(rowKey); }}
onClick={(e) => e.stopPropagation()}
className="rounded border-slate-600 bg-slate-900 text-blue-600 focus:ring-blue-500 focus:ring-offset-0 cursor-pointer"
className="rounded border-surface-border bg-white text-brand-500 focus:ring-brand-500 focus:ring-offset-0 cursor-pointer"
/>
</td>
)}
{columns.map(col => (
<td key={col.key} className={`px-4 py-3 ${col.className || ''}`}>
<td key={col.key} className={`px-4 py-3 text-ink ${col.className || ''}`}>
{col.render(item)}
</td>
))}
+4 -4
View File
@@ -26,10 +26,10 @@ export default class ErrorBoundary extends Component<Props, State> {
render() {
if (this.state.hasError) {
return (
<div className="flex items-center justify-center min-h-screen bg-slate-900">
<div className="flex items-center justify-center min-h-screen bg-page">
<div className="text-center p-8">
<h1 className="text-xl font-semibold text-red-400 mb-2">Something went wrong</h1>
<p className="text-sm text-slate-400 mb-4">
<h1 className="text-xl font-semibold text-red-700 mb-2">Something went wrong</h1>
<p className="text-sm text-ink-muted mb-4">
{this.state.error?.message || 'An unexpected error occurred'}
</p>
<button
@@ -37,7 +37,7 @@ export default class ErrorBoundary extends Component<Props, State> {
this.setState({ hasError: false, error: null });
window.location.reload();
}}
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-500"
className="px-4 py-2 bg-brand-500 text-white rounded text-sm hover:bg-brand-600"
>
Reload Page
</button>
+4 -4
View File
@@ -5,12 +5,12 @@ interface ErrorStateProps {
export default function ErrorState({ error, onRetry }: ErrorStateProps) {
return (
<div className="flex flex-col items-center justify-center py-16 text-slate-400">
<svg className="w-12 h-12 text-red-400 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<div className="flex flex-col items-center justify-center py-16 text-ink-muted">
<svg className="w-12 h-12 text-red-700 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
<p className="text-sm mb-2">Failed to load data</p>
<p className="text-xs text-slate-500 mb-4">{error.message}</p>
<p className="text-sm mb-2 text-ink">Failed to load data</p>
<p className="text-xs text-ink-faint mb-4">{error.message}</p>
{onRetry && (
<button onClick={onRetry} className="btn btn-primary text-xs">
Retry
+24 -15
View File
@@ -1,5 +1,6 @@
import { NavLink, Outlet } from 'react-router-dom';
import { useAuth } from './AuthProvider';
import logo from '../assets/certctl-logo.png';
const nav = [
{ to: '/', label: 'Dashboard', icon: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0h4' },
@@ -21,7 +22,7 @@ const nav = [
function Icon({ d }: { d: string }) {
return (
<svg className="w-5 h-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<svg className="w-[18px] h-[18px] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d={d} />
</svg>
);
@@ -32,23 +33,30 @@ export default function Layout() {
return (
<div className="flex h-screen overflow-hidden">
{/* Sidebar */}
<aside className="w-64 bg-slate-800 border-r border-slate-700 flex flex-col">
<div className="p-6 border-b border-slate-700">
<h1 className="text-xl font-bold text-blue-400">certctl</h1>
<p className="text-xs text-slate-400 uppercase tracking-wider mt-1">Certificate Control Plane</p>
{/* Sidebar — deep teal from logo */}
<aside className="w-60 bg-sidebar flex flex-col shadow-xl">
{/* Logo — large and prominent */}
<div className="px-4 pt-5 pb-4 flex flex-col items-center gap-2">
<div className="bg-white rounded-xl p-2 shadow-lg">
<img src={logo} alt="certctl" className="h-16 w-16" />
</div>
<div className="text-center">
<h1 className="text-lg font-bold text-white tracking-tight">certctl</h1>
<p className="text-[10px] text-brand-300 uppercase tracking-[0.2em]">Control Plane</p>
</div>
</div>
<nav className="flex-1 p-4 space-y-1 overflow-y-auto">
<nav className="flex-1 py-2 px-3 space-y-0.5 overflow-y-auto">
{nav.map(item => (
<NavLink
key={item.to}
to={item.to}
end={item.to === '/'}
className={({ isActive }) =>
`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors ${
`flex items-center gap-3 px-3 py-2 text-[13px] rounded transition-all duration-150 ${
isActive
? 'bg-blue-600 text-white'
: 'text-slate-400 hover:bg-slate-700 hover:text-slate-200'
? 'bg-white/15 text-white font-semibold shadow-sm'
: 'text-sidebar-text hover:text-white hover:bg-white/10'
}`
}
>
@@ -57,12 +65,13 @@ export default function Layout() {
</NavLink>
))}
</nav>
<div className="p-4 border-t border-slate-700 flex items-center justify-between">
<span className="text-xs text-slate-500">certctl v1.0-dev</span>
<div className="px-5 py-3 border-t border-white/10 flex items-center justify-between">
<span className="text-[10px] text-brand-300/60 font-mono">v2.0.2</span>
{authRequired && (
<button
onClick={logout}
className="text-xs text-slate-500 hover:text-slate-300 transition-colors"
className="text-xs text-sidebar-text hover:text-white transition-colors"
title="Sign out"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
@@ -73,8 +82,8 @@ export default function Layout() {
</div>
</aside>
{/* Main content */}
<main className="flex-1 flex flex-col overflow-hidden">
{/* Main content — light background */}
<main className="flex-1 flex flex-col overflow-hidden bg-page">
<Outlet />
</main>
</div>
+3 -3
View File
@@ -6,10 +6,10 @@ interface PageHeaderProps {
export default function PageHeader({ title, subtitle, action }: PageHeaderProps) {
return (
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-700 bg-slate-800">
<div className="flex items-center justify-between px-6 py-4 border-b border-surface-border bg-surface">
<div>
<h2 className="text-lg font-semibold">{title}</h2>
{subtitle && <p className="text-sm text-slate-400 mt-0.5">{subtitle}</p>}
<h2 className="text-lg font-semibold text-ink">{title}</h2>
{subtitle && <p className="text-sm text-ink-muted mt-0.5">{subtitle}</p>}
</div>
{action}
</div>
+3
View File
@@ -1,4 +1,5 @@
const statusStyles: Record<string, string> = {
// Certificate statuses
Active: 'badge-success',
Expiring: 'badge-warning',
Expired: 'badge-danger',
@@ -8,6 +9,8 @@ const statusStyles: Record<string, string> = {
Revoked: 'badge-danger',
// Job statuses
Pending: 'badge-info',
AwaitingCSR: 'badge-info',
AwaitingApproval: 'badge-info',
Running: 'badge-warning',
Completed: 'badge-success',
Failed: 'badge-danger',