diff --git a/apps/arc-web/app/app.css b/apps/arc-web/app/app.css index 63818aa80..48ac2dc41 100644 --- a/apps/arc-web/app/app.css +++ b/apps/arc-web/app/app.css @@ -24,17 +24,103 @@ --color-mint: #5AC8A8; --color-amber: #F0A45B; --color-coral: #E86B6B; + + /* ── Semantic tokens (dark defaults) ── */ + --color-page: #0F1729; + --color-panel: #252C3D; + --color-panel-alt: #1a2133; + --color-overlay: rgba(255, 255, 255, 0.04); + --color-overlay-strong: rgba(255, 255, 255, 0.10); + --color-fg: #ffffff; + --color-fg-2: #E8EDF3; + --color-fg-3: #A8B5C5; + --color-fg-muted: #4B5768; + --color-line: rgba(255, 255, 255, 0.06); + --color-line-strong: rgba(255, 255, 255, 0.12); + --color-focus: rgba(103, 178, 215, 0.4); +} + +/* ── Light theme overrides ── */ + +.light { + /* Semantic tokens */ + --color-page: #ffffff; + --color-panel: #f8fafc; + --color-panel-alt: #f1f5f9; + --color-overlay: rgba(0, 0, 0, 0.04); + --color-overlay-strong: rgba(0, 0, 0, 0.10); + --color-fg: #0f172a; + --color-fg-2: #1e293b; + --color-fg-3: #475569; + --color-fg-muted: #94a3b8; + --color-line: rgba(0, 0, 0, 0.08); + --color-line-strong: rgba(0, 0, 0, 0.15); + --color-focus: rgba(14, 116, 144, 0.5); + + /* Accent — darkened for WCAG AA on white */ + --color-mint: #0d9488; + --color-amber: #d97706; + --color-coral: #dc2626; + --color-teal-300: #0891b2; + --color-teal-500: #0e7490; + + /* @pierre/diffs — light surfaces */ + --diffs-bg-buffer-override: #ffffff; + --diffs-bg-context-override: #f8fafc; + --diffs-bg-hover-override: #f1f5f9; + --diffs-bg-separator-override: #e2e8f0; + --diffs-fg-number-override: #94a3b8; + --diffs-addition-color-override: #0d9488; + --diffs-fg-number-addition-override: rgba(13, 148, 136, 0.5); + --diffs-deletion-color-override: #dc2626; + --diffs-fg-number-deletion-override: rgba(220, 38, 38, 0.5); + + /* Chart variables */ + --chart-gridline: rgba(0, 0, 0, 0.06); + --chart-axis-label: rgba(71, 85, 105, 1); +} + +/* ── Chart variables (dark defaults) ── */ + +:root { + --chart-gridline: rgba(255, 255, 255, 0.04); + --chart-axis-label: rgba(75, 87, 104, 1); } /* ── Background atmosphere ── */ .bg-atmosphere { - background-color: var(--color-navy-950); + background-color: var(--color-page); background-image: radial-gradient(ellipse 120% 60% at 15% 5%, rgba(53, 127, 158, 0.12) 0%, transparent 50%), radial-gradient(ellipse 80% 50% at 85% 90%, rgba(90, 200, 168, 0.07) 0%, transparent 45%); background-attachment: fixed; } + +.light .bg-atmosphere, +.light.bg-atmosphere { + background-image: + radial-gradient(ellipse 120% 60% at 15% 5%, rgba(14, 116, 144, 0.06) 0%, transparent 50%), + radial-gradient(ellipse 80% 50% at 85% 90%, rgba(13, 148, 136, 0.04) 0%, transparent 45%); +} + +.bg-atmosphere::before { + content: ""; + position: fixed; + inset: 0; + z-index: 1; + pointer-events: none; + opacity: 0.07; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); + background-repeat: repeat; + background-size: 200px 200px; +} + +.light .bg-atmosphere::before, +.light.bg-atmosphere::before { + opacity: 0.03; +} + /* ── @pierre/diffs brand overrides ── */ :root { @@ -60,15 +146,3 @@ --diffs-deletion-color-override: #E86B6B; --diffs-fg-number-deletion-override: rgba(232, 107, 107, 0.5); } - -.bg-atmosphere::before { - content: ""; - position: fixed; - inset: 0; - z-index: 1; - pointer-events: none; - opacity: 0.07; - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); - background-repeat: repeat; - background-size: 200px 200px; -} diff --git a/apps/arc-web/app/components/collapsible-file.tsx b/apps/arc-web/app/components/collapsible-file.tsx index d3b293591..453e9a99d 100644 --- a/apps/arc-web/app/components/collapsible-file.tsx +++ b/apps/arc-web/app/components/collapsible-file.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { ChevronRightIcon } from "@heroicons/react/20/solid"; import type { FileContents } from "@pierre/diffs"; import { File } from "@pierre/diffs/react"; +import { useTheme } from "../lib/theme"; export function CollapsibleFile({ file, @@ -11,32 +12,33 @@ export function CollapsibleFile({ defaultOpen?: boolean; }) { const [open, setOpen] = useState(defaultOpen); + const { theme } = useTheme(); const lines = file.contents.split("\n"); const lineCount = lines.length; const loc = lines.filter((l) => l.trim().length > 0).length; return ( -
+
-
+
diff --git a/apps/arc-web/app/data/register-dot-language.ts b/apps/arc-web/app/data/register-dot-language.ts index 6b2876ec9..f756065a6 100644 --- a/apps/arc-web/app/data/register-dot-language.ts +++ b/apps/arc-web/app/data/register-dot-language.ts @@ -7,7 +7,7 @@ import { dotLanguage } from "./dot-grammar"; export async function registerDotLanguage(): Promise { const highlighter = await getSharedHighlighter({ - themes: ["pierre-dark"], + themes: ["pierre-dark", "pierre-light"], langs: [], }); attachResolvedLanguages( diff --git a/apps/arc-web/app/data/verifications.ts b/apps/arc-web/app/data/verifications.ts index d1a6ad6cd..4b25f709a 100644 --- a/apps/arc-web/app/data/verifications.ts +++ b/apps/arc-web/app/data/verifications.ts @@ -33,10 +33,10 @@ export const statusConfig = { }, na: { label: "N/A", - color: "text-navy-600", - bg: "bg-white/[0.04]", - dot: "bg-navy-600", - border: "border-l-navy-600/50", + color: "text-fg-muted", + bg: "bg-overlay", + dot: "bg-fg-muted", + border: "border-l-fg-muted/50", }, } as const satisfies Record< VerificationStatus, @@ -157,7 +157,7 @@ export interface CriterionPerformance { export const modeConfig = { active: { label: "Active", color: "text-mint", bg: "bg-mint/10" }, evaluate: { label: "Evaluate", color: "text-amber", bg: "bg-amber/10" }, - disabled: { label: "Disabled", color: "text-navy-600", bg: "bg-white/[0.04]" }, + disabled: { label: "Disabled", color: "text-fg-muted", bg: "bg-overlay" }, } as const satisfies Record< VerificationMode, { label: string; color: string; bg: string } diff --git a/apps/arc-web/app/layouts/app-shell.tsx b/apps/arc-web/app/layouts/app-shell.tsx index b01dab917..d7d7d31d0 100644 --- a/apps/arc-web/app/layouts/app-shell.tsx +++ b/apps/arc-web/app/layouts/app-shell.tsx @@ -13,12 +13,15 @@ import { CheckBadgeIcon, Cog6ToothIcon, LightBulbIcon, + MoonIcon, PlayIcon, RectangleStackIcon, SparklesIcon, + SunIcon, XMarkIcon, } from "@heroicons/react/24/outline"; import { Link, Outlet, useLocation, useMatches } from "react-router"; +import { useTheme } from "../lib/theme"; const user = { name: "Tom Cook", @@ -46,6 +49,7 @@ function classNames(...classes: Array) { export default function AppShell() { const { pathname } = useLocation(); const matches = useMatches(); + const { theme, toggle } = useTheme(); const currentNav = navigation.find((item) => pathname.startsWith(item.href)); const title = currentNav?.name ?? ""; const lastMatch = matches[matches.length - 1]; @@ -55,9 +59,11 @@ export default function AppShell() { const wide = matches.some((m) => (m.handle as { wide?: boolean } | undefined)?.wide); const maxWidth = wide ? "" : "max-w-5xl"; + const ThemeIcon = theme === "dark" ? SunIcon : MoonIcon; + return (
- +
@@ -77,8 +83,8 @@ export default function AppShell() { aria-current={current ? "page" : undefined} className={classNames( current - ? "bg-navy-950/50 text-white" - : "text-ice-300 hover:bg-white/5 hover:text-white", + ? "bg-page/50 text-fg" + : "text-fg-3 hover:bg-overlay hover:text-fg", "inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium", )} > @@ -91,7 +97,16 @@ export default function AppShell() {
-
+
+ @@ -99,19 +114,19 @@ export default function AppShell() { {userNavigation.map((item) => ( {item.name} @@ -122,7 +137,7 @@ export default function AppShell() {
- + Open main menu @@ -161,23 +176,32 @@ export default function AppShell() { ); })}
-
+
-
+
{user.name}
-
+
{user.email}
+
{userNavigation.map((item) => ( @@ -185,7 +209,7 @@ export default function AppShell() { key={item.name} as="a" href={item.href} - className="block rounded-md px-3 py-2 text-base font-medium text-navy-600 hover:bg-white/5 hover:text-white" + className="block rounded-md px-3 py-2 text-base font-medium text-fg-muted hover:bg-overlay hover:text-fg" > {item.name} @@ -196,10 +220,10 @@ export default function AppShell() { {!hideHeader && ( -
+
-

{title}

+

{title}

{headerExtra &&
{headerExtra}
}
diff --git a/apps/arc-web/app/lib/graph-theme.ts b/apps/arc-web/app/lib/graph-theme.ts new file mode 100644 index 000000000..06c71e4eb --- /dev/null +++ b/apps/arc-web/app/lib/graph-theme.ts @@ -0,0 +1,70 @@ +import type { Theme } from "./theme"; + +interface GraphTheme { + fontcolor: string; + edgeColor: string; + nodeFill: string; + nodeText: string; + startFill: string; + startBorder: string; + startText: string; + gateFill: string; + gateBorder: string; + gateText: string; + completedFill: string; + completedBorder: string; + completedText: string; + runningFill: string; + runningBorder: string; + runningText: string; + runningPulseFill: string; + runningPulseStroke: string; +} + +const dark: GraphTheme = { + fontcolor: "#5a7a94", + edgeColor: "#2a3f52", + nodeFill: "#1a2b3c", + nodeText: "#c6d4e0", + startFill: "#0d4f4f", + startBorder: "#14b8a6", + startText: "#5eead4", + gateFill: "#1a2030", + gateBorder: "#f59e0b", + gateText: "#fbbf24", + completedFill: "#0a2a20", + completedBorder: "#34d399", + completedText: "#6ee7b7", + runningFill: "#0d3a3a", + runningBorder: "#14b8a6", + runningText: "#5eead4", + runningPulseFill: "#134e4a", + runningPulseStroke: "#5eead4", +}; + +const light: GraphTheme = { + fontcolor: "#475569", + edgeColor: "#cbd5e1", + nodeFill: "#f1f5f9", + nodeText: "#1e293b", + startFill: "#ccfbf1", + startBorder: "#0d9488", + startText: "#0d9488", + gateFill: "#fef3c7", + gateBorder: "#d97706", + gateText: "#d97706", + completedFill: "#d1fae5", + completedBorder: "#059669", + completedText: "#059669", + runningFill: "#ccfbf1", + runningBorder: "#0d9488", + runningText: "#0d9488", + runningPulseFill: "#99f6e4", + runningPulseStroke: "#0d9488", +}; + +export function getGraphTheme(theme: Theme): GraphTheme { + return theme === "dark" ? dark : light; +} + +export type { GraphTheme }; diff --git a/apps/arc-web/app/lib/theme.tsx b/apps/arc-web/app/lib/theme.tsx new file mode 100644 index 000000000..6c760022f --- /dev/null +++ b/apps/arc-web/app/lib/theme.tsx @@ -0,0 +1,53 @@ +import { createContext, useCallback, useContext, useEffect, useState } from "react"; + +type Theme = "light" | "dark"; + +const STORAGE_KEY = "arc-theme"; + +function getInitialTheme(): Theme { + if (typeof window === "undefined") return "dark"; + const stored = localStorage.getItem(STORAGE_KEY); + if (stored === "light" || stored === "dark") return stored; + return window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light"; +} + +function applyThemeClass(theme: Theme) { + const root = document.documentElement; + root.classList.remove("light", "dark"); + root.classList.add(theme); +} + +const ThemeContext = createContext<{ + theme: Theme; + toggle: () => void; +}>({ theme: "dark", toggle: () => {} }); + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + const [theme, setTheme] = useState(getInitialTheme); + + useEffect(() => { + applyThemeClass(theme); + }, [theme]); + + const toggle = useCallback(() => { + setTheme((prev) => { + const next = prev === "dark" ? "light" : "dark"; + localStorage.setItem(STORAGE_KEY, next); + return next; + }); + }, []); + + return ( + + {children} + + ); +} + +export function useTheme() { + return useContext(ThemeContext); +} + +export type { Theme }; diff --git a/apps/arc-web/app/root.tsx b/apps/arc-web/app/root.tsx index 7dcf3d245..aad700262 100644 --- a/apps/arc-web/app/root.tsx +++ b/apps/arc-web/app/root.tsx @@ -8,8 +8,11 @@ import { } from "react-router"; import type { Route } from "./+types/root"; +import { ThemeProvider } from "./lib/theme"; import "./app.css"; +const themeScript = `(function(){try{var t=localStorage.getItem("arc-theme");if(t!=="light"&&t!=="dark")t=window.matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light";document.documentElement.classList.add(t)}catch(e){document.documentElement.classList.add("dark")}})()`; + export const links: Route.LinksFunction = () => [ { rel: "icon", href: "/favicon.svg", type: "image/svg+xml" }, { rel: "icon", href: "/favicon.ico", sizes: "32x32" }, @@ -30,6 +33,7 @@ export function Layout({ children }: { children: React.ReactNode }) { return ( +