From 1b66bf94208f7ce68202f149760657b9c95105c1 Mon Sep 17 00:00:00 2001 From: initstring <26131150+initstring@users.noreply.github.com> Date: Thu, 25 Dec 2025 21:03:22 +1100 Subject: [PATCH 1/7] Migrate to simple light/dark mode options --- package-lock.json | 11 ++ package.json | 1 + src/app/layout.tsx | 30 ++--- src/features/shared/layout/theme-toggle.tsx | 91 ++++--------- src/styles/globals.css | 138 +++++++------------- 5 files changed, 99 insertions(+), 172 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3a8da08..22278e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "lucide-react": "^0.555.0", "next": "^15.5.7", "next-auth": "5.0.0-beta.30", + "next-themes": "^0.4.6", "pino": "^9.14.0", "react": "~19.2.1", "react-dom": "~19.2.1", @@ -9262,6 +9263,16 @@ } } }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", diff --git a/package.json b/package.json index b95a895..f2038d4 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "lucide-react": "^0.555.0", "next": "^15.5.7", "next-auth": "5.0.0-beta.30", + "next-themes": "^0.4.6", "pino": "^9.14.0", "react": "~19.2.1", "react-dom": "~19.2.1", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 11a0aae..eca1aad 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,7 +1,6 @@ import "@/styles/globals.css"; import { type Metadata } from "next"; -import { Geist } from "next/font/google"; import { SessionProvider } from "next-auth/react"; import { TRPCReactProvider } from "@/trpc/react"; @@ -14,27 +13,28 @@ export const metadata: Metadata = { icons: [{ rel: "icon", url: "/favicon.ico" }], }; -const geist = Geist({ - subsets: ["latin"], - variable: "--font-geist-sans", -}); + +import { ThemeProvider } from "next-themes"; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { return ( - + - - - - -
{children}
-
-
-
-
+ + + + + +
{children}
+
+
+
+
+
+ ); } diff --git a/src/features/shared/layout/theme-toggle.tsx b/src/features/shared/layout/theme-toggle.tsx index a548959..44c0e39 100644 --- a/src/features/shared/layout/theme-toggle.tsx +++ b/src/features/shared/layout/theme-toggle.tsx @@ -1,81 +1,38 @@ "use client"; import { useEffect, useState } from "react"; +import { useTheme } from "next-themes"; import { Button } from "@/components/ui/button"; - -type ThemeKey = "theme-modern-teal" | "theme-modern-blue" | "theme-modern-ember"; - -const THEMES: { key: ThemeKey; label: string }[] = [ - { key: "theme-modern-teal", label: "Teal" }, - { key: "theme-modern-blue", label: "Blue" }, - { key: "theme-modern-ember", label: "Ember" }, -]; +import { Sun, Moon } from "lucide-react"; export function ThemeToggle({ variant = "full" as "full" | "compact" }: { variant?: "full" | "compact" }) { - const [theme, setTheme] = useState("theme-modern-teal"); - - // Apply theme to and persist - const apply = (key: ThemeKey) => { - setTheme(key); - if (typeof document !== 'undefined') { - const root = document.documentElement; - ["theme-modern","theme-modern-teal","theme-modern-blue","theme-modern-ember"].forEach(c => root.classList.remove(c)); - root.classList.add(key); - } - if (typeof localStorage !== 'undefined') { - localStorage.setItem('rtap.theme', key); - } - }; + const { theme, setTheme } = useTheme(); + const [mounted, setMounted] = useState(false); - // Load stored theme on mount and apply immediately useEffect(() => { - const stored = (typeof window !== 'undefined' && (localStorage.getItem('rtap.theme') as ThemeKey | null)) ?? null; - const initial = stored && THEMES.some(t => t.key === stored) ? stored : "theme-modern-teal"; - apply(initial); + setMounted(true); }, []); - if (variant === "compact") { - // A tiny button that cycles themes on click - const nextTheme = () => { - if (THEMES.length === 0) return; - const idxRaw = THEMES.findIndex(t => t.key === theme); - const idx = idxRaw >= 0 ? idxRaw : 0; - const next = THEMES[(idx + 1) % THEMES.length]; - if (next) apply(next.key); - }; - return ( - - ); - } + if (!mounted) return null; + + const toggleTheme = () => { + setTheme(theme === "light" ? "dark" : "light"); + }; return ( -
- {THEMES.map((t, i) => { - const selected = theme === t.key; - return ( - - ); - })} -
+ ); } diff --git a/src/styles/globals.css b/src/styles/globals.css index 7498049..021ddcb 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -56,83 +56,36 @@ --shadow-glow-error: 0 0 8px rgba(255, 68, 68, 0.2); } -/* Modern theme tokens (opt-in) - Teal */ -:root.theme-modern-teal { - /* Accent */ - --color-accent-rgb: 59, 201, 186; +/* Light Theme */ +:root[data-theme='light'] { + /* Accent - Darker green for contrast against white */ + --color-accent-rgb: 0, 150, 40; --color-accent: rgb(var(--color-accent-rgb)); + --color-accent-muted: rgba(var(--color-accent-rgb), 0.8); + --color-accent-dim: rgba(var(--color-accent-rgb), 0.1); + --ring: var(--color-accent); /* Surfaces */ - --surface-0: #0b0f14; - --surface-1: #121821; - --surface-2: #18212c; - --glass-alpha: 0.06; + --surface-0: #ffffff; + --surface-1: #f4f6f8; + --surface-2: #e2e8f0; + --glass-alpha: 0.8; - /* Map legacy tokens to modern surfaces for easy adoption */ --color-surface: var(--surface-0); --color-surface-elevated: var(--surface-1); - --color-border: rgba(var(--border-rgb, 255,255,255), 0.10); - --color-border-light: rgba(var(--border-rgb, 255,255,255), 0.18); + --color-border: #e2e8f0; + --color-border-light: #cbd5e1; /* Text */ - --text-primary: #e6e9ee; - --text-secondary: #b4bdca; - --text-muted: #8893a1; - --color-text-primary: var(--text-primary); - --color-text-secondary: var(--text-secondary); - --color-text-muted: var(--text-muted); - - --ring: var(--color-accent); -} - -/* Modern theme - Electric Blue */ -:root.theme-modern-blue { - --color-accent-rgb: 90, 177, 255; /* #5AB1FF */ - --color-accent: rgb(var(--color-accent-rgb)); + --color-text-primary: #0f172a; + --color-text-secondary: #475569; + --color-text-muted: #64748b; - --surface-0: #0b0f14; - --surface-1: #121821; - --surface-2: #17202b; - --glass-alpha: 0.06; - - --color-surface: var(--surface-0); - --color-surface-elevated: var(--surface-1); - --color-border: rgba(var(--border-rgb, 255,255,255), 0.10); - --color-border-light: rgba(var(--border-rgb, 255,255,255), 0.18); - - --text-primary: #e6eaf0; - --text-secondary: #b8c4d4; - --text-muted: #90a0b5; - --color-text-primary: var(--text-primary); - --color-text-secondary: var(--text-secondary); - --color-text-muted: var(--text-muted); - - --ring: var(--color-accent); -} - -/* Modern theme - Ember */ -:root.theme-modern-ember { - --color-accent-rgb: 255, 107, 90; /* #FF6B5A */ - --color-accent: rgb(var(--color-accent-rgb)); - - --surface-0: #0e0f13; - --surface-1: #14161b; - --surface-2: #191c22; - --glass-alpha: 0.06; - - --color-surface: var(--surface-0); - --color-surface-elevated: var(--surface-1); - --color-border: rgba(var(--border-rgb, 255,255,255), 0.10); - --color-border-light: rgba(var(--border-rgb, 255,255,255), 0.18); - - --text-primary: #e9ecf1; - --text-secondary: #c9ced6; - --text-muted: #9aa3af; - --color-text-primary: var(--text-primary); - --color-text-secondary: var(--text-secondary); - --color-text-muted: var(--text-muted); - - --ring: var(--color-accent); + /* Shadows */ + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + --shadow-glow: 0 0 10px rgba(var(--color-accent-rgb), 0.2); } /* Base styles */ @@ -140,8 +93,15 @@ border-color: var(--color-border); } -html { background-color: var(--color-surface); color: var(--color-text-primary); } -body { background: linear-gradient(135deg, var(--surface-0) 0%, var(--surface-1) 100%); min-height: 100vh; } +html { + background-color: var(--color-surface); + color: var(--color-text-primary); +} + +body { + background: linear-gradient(135deg, var(--surface-0) 0%, var(--surface-1) 100%); + min-height: 100vh; +} /* Subtle glow effects */ .glow-accent { @@ -180,29 +140,27 @@ body { background: linear-gradient(135deg, var(--surface-0) 0%, var(--surface-1) opacity: 0.3; } -/* Glass utilities for the modern theme */ +/* Glass utilities */ .glass { - background-color: rgba(255,255,255, var(--glass-alpha, 0.04)); + background-color: rgba(255, 255, 255, var(--glass-alpha, 0.04)); -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); - border: 1px solid rgba(var(--border-rgb, 255,255,255), 0.12); -} - - -/* Status tokens (modern themes) */ -:root, -:root.theme-modern, -:root.theme-modern-teal, -:root.theme-modern-blue, -:root.theme-modern-ember { - --status-success-fg: #4ade80; /* green-400 */ - --status-success-bg: rgba(74,222,128,0.16); - --status-warn-fg: #fbbf24; /* amber-400 */ - --status-warn-bg: rgba(251,191,36,0.16); - --status-error-fg: #f87171; /* red-400 */ - --status-error-bg: rgba(248,113,113,0.16); + border: 1px solid rgba(var(--border-rgb, 255, 255, 255), 0.12); +} + +/* Status tokens */ +:root { + --status-success-fg: #4ade80; + /* green-400 */ + --status-success-bg: rgba(74, 222, 128, 0.16); + --status-warn-fg: #fbbf24; + /* amber-400 */ + --status-warn-bg: rgba(251, 191, 36, 0.16); + --status-error-fg: #f87171; + /* red-400 */ + --status-error-bg: rgba(248, 113, 113, 0.16); --status-info-fg: var(--color-accent, #5ab1ff); - --status-info-bg: rgba(var(--color-accent-rgb, 90,177,255), 0.16); + --status-info-bg: rgba(var(--color-accent-rgb, 90, 177, 255), 0.16); } /* Scrollbar styling */ @@ -221,4 +179,4 @@ body { background: linear-gradient(135deg, var(--surface-0) 0%, var(--surface-1) ::-webkit-scrollbar-thumb:hover { background: var(--color-accent); -} +} \ No newline at end of file From 393a2dadc18bdb3a5ca8127e20fdfce5f9e8197a Mon Sep 17 00:00:00 2001 From: initstring <26131150+initstring@users.noreply.github.com> Date: Thu, 25 Dec 2025 21:28:07 +1100 Subject: [PATCH 2/7] Switch to blue variant by default --- src/styles/globals.css | 50 +++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/src/styles/globals.css b/src/styles/globals.css index 021ddcb..13c71e7 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -5,27 +5,34 @@ "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* Accent */ - --color-accent-rgb: 0, 255, 65; + --color-accent-rgb: 59, 130, 246; + /* Blue 500 - Professional, Electric Blue */ --color-accent: rgb(var(--color-accent-rgb)); --color-accent-muted: rgba(var(--color-accent-rgb), 0.8); --color-accent-dim: rgba(var(--color-accent-rgb), 0.85); --ring: var(--color-accent); - /* Surfaces */ - --surface-0: #0d0f0d; - --surface-1: #111511; - --surface-2: #191d19; + /* Dark Mode Polish: Deeper blacks, neutral zinc */ + --surface-0: #09090b; + /* Zinc 950 */ + --surface-1: #18181b; + /* Zinc 900 */ + --surface-2: #27272a; + /* Zinc 800 */ --glass-alpha: 0.04; --color-surface: var(--surface-0); --color-surface-elevated: var(--surface-1); - --color-border: #2a2d2a; - --color-border-light: #353835; + --color-border: #27272a; + --color-border-light: #3f3f46; - /* Text */ - --color-text-primary: #e8f5e8; - --color-text-secondary: #b8c5b8; - --color-text-muted: #8a958a; + /* Text - Zinc scale for clear, neutral reading */ + --color-text-primary: #e4e4e7; + /* Zinc 200 */ + --color-text-secondary: #a1a1aa; + /* Zinc 400 */ + --color-text-muted: #71717a; + /* Zinc 500 */ /* Status */ --color-success: var(--status-success-fg); @@ -58,28 +65,35 @@ /* Light Theme */ :root[data-theme='light'] { - /* Accent - Darker green for contrast against white */ - --color-accent-rgb: 0, 150, 40; + /* Accent - Blue 600 for contrast against white */ + --color-accent-rgb: 37, 99, 235; --color-accent: rgb(var(--color-accent-rgb)); --color-accent-muted: rgba(var(--color-accent-rgb), 0.8); --color-accent-dim: rgba(var(--color-accent-rgb), 0.1); --ring: var(--color-accent); - /* Surfaces */ + /* Surfaces - Clean white and slate grays */ --surface-0: #ffffff; - --surface-1: #f4f6f8; + --surface-1: #f8fafc; + /* Slate 50 */ --surface-2: #e2e8f0; + /* Slate 200 */ --glass-alpha: 0.8; --color-surface: var(--surface-0); --color-surface-elevated: var(--surface-1); - --color-border: #e2e8f0; - --color-border-light: #cbd5e1; + --color-border: #cbd5e1; + /* Slate 300 - better definition */ + --color-border-light: #94a3b8; + /* Slate 400 */ - /* Text */ + /* Text - High contrast Slate */ --color-text-primary: #0f172a; + /* Slate 900 */ --color-text-secondary: #475569; + /* Slate 600 - much more readable than 400/500 */ --color-text-muted: #64748b; + /* Slate 500 */ /* Shadows */ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); From 31961c9fbaeffa458039613c87608df8e1b6551a Mon Sep 17 00:00:00 2001 From: initstring <26131150+initstring@users.noreply.github.com> Date: Thu, 25 Dec 2025 21:29:08 +1100 Subject: [PATCH 3/7] Fix PNG export width --- .../analytics/components/attack-matrix/attack-matrix.tsx | 2 +- src/lib/exportImage.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/features/analytics/components/attack-matrix/attack-matrix.tsx b/src/features/analytics/components/attack-matrix/attack-matrix.tsx index b25df76..41df526 100644 --- a/src/features/analytics/components/attack-matrix/attack-matrix.tsx +++ b/src/features/analytics/components/attack-matrix/attack-matrix.tsx @@ -67,7 +67,7 @@ export default function AttackMatrix() { }, [metrics, opsOnly, split, subIndex, usedSubs, subMetrics]); if (isLoading) return
Loading technique metrics...
; return ( - +
{ From 14449b877037374b30446bbe1a9550371db15a30 Mon Sep 17 00:00:00 2001 From: initstring <26131150+initstring@users.noreply.github.com> Date: Thu, 25 Dec 2025 21:31:22 +1100 Subject: [PATCH 4/7] Update STYLE --- docs/dev/STYLE.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/dev/STYLE.md b/docs/dev/STYLE.md index f4b7c9c..6d7f047 100644 --- a/docs/dev/STYLE.md +++ b/docs/dev/STYLE.md @@ -8,11 +8,12 @@ Structure and Naming (source of truth) - Path aliases: `@features/*`, `@server/*`, `@lib/*`, `@components/*`, and `@/*` are configured in `tsconfig.json` — prefer these over deep relative imports. - Naming: keep existing file names for now; future cleanup will normalize React component filenames to kebab-case and utilities to camelCase. Avoid renaming during structural moves. -A concise, enforceable guide to keep the UI polished, consistent, and performant. Themes are neutral, dense, and modern — not neon or blurry. +A concise, enforceable guide to keep the UI polished, consistent, and performant. Themes are neutral, dense, and modern — using a Professional Blue accent for clarity. Principles -- Purposeful contrast: dark, cool neutrals with a restrained accent. +- Purposeful contrast: deep neutral blacks (Zinc) or clean whites (Slate); restrained Blue accent. +- Binary System: Strict Light vs. Dark modes. No confusing sub-themes. - No blur by default: crisp 1px borders; use blur only on overlays if needed. - Compact density: smaller paddings, tight grids, minimal chrome. - Consistent states: same focus, hover, and success/failure treatments everywhere. @@ -20,7 +21,7 @@ Principles Canonical Surfaces & Cards -- Page surface: `--surface-0` (neutral background). +- Page surface: `--surface-0` (Zinc-950 or White). - List/content cards: `Card variant="default"` with 1px border. Hover = subtle border/ring, not darker fill. - Overlays/modals: `Card variant="elevated"` only, stronger surface and shadow. - Content inside modals: use neutral cards/containers; avoid nested elevated cards to prevent double-elevation. @@ -29,16 +30,18 @@ Canonical Surfaces & Cards Design Tokens (CSS variables) Define tokens once and reference them everywhere. Do not bake colors directly into components. -- Accent: `--color-accent-rgb`, `--color-accent` (for focus, selected, executed outlines). +- Accent: `--color-accent` (Blue 500/600). Used for focus, active states, and selection. - Surfaces: `--surface-0/1/2` mapped to `--color-surface` and `--color-surface-elevated`. -- Text: `--text-primary`, `--text-secondary`, `--text-muted`. -- Status: `--status-success/warn/error/info` fg/bg pairs. -- Borders/Focus: `--border-rgb`, `--ring`. +- Text: `--text-primary` (Zinc-200 / Slate-900), `--text-secondary`, `--text-muted`. +- Status: `--status-success/warn/error/info` fg/bg pairs. Distinct from Accent. +- Borders/Focus: `--border-rgb`, `--ring` (matches Accent). - Legacy matrix tokens were removed; use the accent and surface variables above. -Theme presets +Theme configuration -- `theme-modern-teal`, `theme-modern-blue`, `theme-modern-ember` applied on ``; LocalStorage-backed toggle on dashboard. +- Strict `light` and `dark` modes managed via `next-themes`. +- Toggle available in sidebar (Sun/Moon). +- Exports automatically inherit the active theme (WYSIWYG). Component Standards From e8753c9eb3479867705556633e049edd964c38d4 Mon Sep 17 00:00:00 2001 From: initstring <26131150+initstring@users.noreply.github.com> Date: Thu, 25 Dec 2025 22:17:28 +1100 Subject: [PATCH 5/7] feat: Integrate Geist font and apply its variable to the body element. --- src/app/layout.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index eca1aad..dd00267 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -14,14 +14,20 @@ export const metadata: Metadata = { }; +import { Geist } from "next/font/google"; import { ThemeProvider } from "next-themes"; +const geist = Geist({ + subsets: ["latin"], + variable: "--font-geist-sans", +}); + export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { return ( - + From f0f141ecb325b5301a9fded2694c018e0518cb73 Mon Sep 17 00:00:00 2001 From: initstring <26131150+initstring@users.noreply.github.com> Date: Thu, 25 Dec 2025 22:19:26 +1100 Subject: [PATCH 6/7] style: update Prevention Rate chart color from warning to accent. --- src/app/(protected-routes)/analytics/trends/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(protected-routes)/analytics/trends/page.tsx b/src/app/(protected-routes)/analytics/trends/page.tsx index 8abd387..01f299f 100644 --- a/src/app/(protected-routes)/analytics/trends/page.tsx +++ b/src/app/(protected-routes)/analytics/trends/page.tsx @@ -91,7 +91,7 @@ export default function TrendsPage() { dataKey="preventionRate" name="Prevention Rate" title="Prevention Rate Trends" - color="var(--color-warning)" + color="var(--color-accent)" icon={Shield} /> Date: Fri, 26 Dec 2025 01:46:59 +1100 Subject: [PATCH 7/7] style: Replace generic logo div with a Shield icon from lucide-react. --- src/features/shared/layout/sidebar-nav.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/features/shared/layout/sidebar-nav.tsx b/src/features/shared/layout/sidebar-nav.tsx index b55dbec..98a0a68 100644 --- a/src/features/shared/layout/sidebar-nav.tsx +++ b/src/features/shared/layout/sidebar-nav.tsx @@ -7,6 +7,7 @@ import { useSession } from "next-auth/react"; import { UserRole } from "@prisma/client"; // Theme toggle now lives inside the UserMenu dropdown import { useSidebar } from "@features/shared/layout/sidebar-context"; +import { Shield } from "lucide-react"; import { UserMenu } from "./user-menu"; interface NavItem { @@ -81,7 +82,7 @@ const navigation: NavItem[] = [ label: "Data", href: "/settings/data", }, - + ], }, ]; @@ -106,7 +107,7 @@ export function SidebarNav() { const isParentActive = (item: NavItem): boolean => { if (pathname === item.href) return true; if (item.children) { - return item.children.some(child => + return item.children.some(child => isParentActive(child) || pathname === child.href ); } @@ -132,11 +133,11 @@ export function SidebarNav() { className={` flex items-center justify-between px-3 py-2 text-sm rounded-[var(--radius-md)] transition-all duration-200 cursor-pointer ${depth > 0 ? 'ml-' + (depth * 4) : ''} - ${active - ? 'bg-[var(--color-accent)]/20 text-[var(--color-accent)] border-l-2 border-[var(--color-accent)] -ml-[2px]' + ${active + ? 'bg-[var(--color-accent)]/20 text-[var(--color-accent)] border-l-2 border-[var(--color-accent)] -ml-[2px]' : parentActive - ? 'text-[var(--color-text-primary)]' - : 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-surface-elevated)]' + ? 'text-[var(--color-text-primary)]' + : 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-surface-elevated)]' } `} onClick={() => { @@ -146,7 +147,7 @@ export function SidebarNav() { }} > {item.href ? ( - hasChildren && e.stopPropagation()} @@ -191,7 +192,7 @@ export function SidebarNav() { if (!session) return null; return ( -
-
+ {!isCollapsed && ( RTAP )}