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 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/(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} /> ) { return ( - - - - - - -
{children}
-
-
-
-
+ + + + + + + +
{children}
+
+
+
+
+
+ ); } 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 ( - +
{ 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 )} 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/lib/exportImage.ts b/src/lib/exportImage.ts index 7c4ff49..68aa842 100644 --- a/src/lib/exportImage.ts +++ b/src/lib/exportImage.ts @@ -75,6 +75,7 @@ function applyExportDirectivesInPlace(root: HTMLElement) { applyStyleOverride(element, "max-width", "none", "important", overrides); applyStyleOverride(element, "height", "auto", "important", overrides); applyStyleOverride(element, "overflow", "visible", "important", overrides); + applyStyleOverride(element, "min-width", "fit-content", "important", overrides); }); visible.forEach((element) => { diff --git a/src/styles/globals.css b/src/styles/globals.css index 7498049..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); @@ -56,83 +63,43 @@ --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; - --color-accent: rgb(var(--color-accent-rgb)); - - /* Surfaces */ - --surface-0: #0b0f14; - --surface-1: #121821; - --surface-2: #18212c; - --glass-alpha: 0.06; - - /* 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); - - /* 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 */ +/* Light Theme */ +:root[data-theme='light'] { + /* Accent - Blue 600 for contrast against white */ + --color-accent-rgb: 37, 99, 235; --color-accent: rgb(var(--color-accent-rgb)); - - --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); - + --color-accent-muted: rgba(var(--color-accent-rgb), 0.8); + --color-accent-dim: rgba(var(--color-accent-rgb), 0.1); --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; + /* Surfaces - Clean white and slate grays */ + --surface-0: #ffffff; + --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: rgba(var(--border-rgb, 255,255,255), 0.10); - --color-border-light: rgba(var(--border-rgb, 255,255,255), 0.18); + --color-border: #cbd5e1; + /* Slate 300 - better definition */ + --color-border-light: #94a3b8; + /* Slate 400 */ + + /* 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 */ - --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 +107,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 +154,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 +193,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