diff --git a/dash/index.html b/dash/index.html index 978f7b9..7023109 100644 --- a/dash/index.html +++ b/dash/index.html @@ -5,6 +5,19 @@ CodeBurn - Local Dashboard +
diff --git a/dash/src/App.tsx b/dash/src/App.tsx index 6b1e5a7..2711761 100644 --- a/dash/src/App.tsx +++ b/dash/src/App.tsx @@ -77,7 +77,7 @@ function DeviceView({ payload, isRemote, unit }: { payload?: Payload; isRemote: const c = payload?.current // Cache cards read the period-scoped `current` totals, matching Cost/Calls/ // Tokens. `history.daily` is the 365-day backfill that feeds the trend chart - // only; summing it here over-counted the cards for shorter periods (#583). + // only; summing it here over-counted the cards for shorter periods (issue 583). const cacheWrite = c?.cacheWriteTokens ?? 0 const cacheRead = c?.cacheReadTokens ?? 0 const toolBars: BarItem[] = c @@ -290,7 +290,7 @@ function CombinedView({ devices, unit }: { devices: DeviceUsage[]; unit: Unit }) if (!c) continue inTok += c.inputTokens outTok += c.outputTokens - // Period-scoped per device (was summing each device's 365-day backfill, #583). + // Period-scoped per device (was summing each device's 365-day backfill, issue 583). // `?? 0` mirrors DeviceView and guards the un-normalized bootstrap payload, // where an older peer may not carry these fields yet (avoids NaN). cacheWrite += c.cacheWriteTokens ?? 0 @@ -376,6 +376,42 @@ function CombinedView({ devices, unit }: { devices: DeviceUsage[]; unit: Unit }) ) } +// Theme toggle: mirrors the .dark class set by the index.html pre-paint script +// and persists the choice to the same localStorage key. +function ThemeToggle() { + const [dark, setDark] = useState(() => document.documentElement.classList.contains('dark')) + const toggle = () => { + const next = !dark + setDark(next) + document.documentElement.classList.toggle('dark', next) + try { + localStorage.setItem('codeburn-theme', next ? 'dark' : 'light') + } catch { + // storage disabled (some embeds/webviews): persist nothing, OS theme wins next load + } + } + return ( + + ) +} + export function App() { const [page, setPage] = useState<'usage' | 'context'>('usage') const [period, setPeriod] = useState('today') @@ -459,6 +495,25 @@ export function App() { if (provider !== 'all' && c0 && !providerOptions.includes(provider)) setProvider('all') }, [provider, providerOptions, c0]) + // Follow the OS theme live while the user has no explicit preference, so + // flipping the system theme updates the dashboard without a reload. + useEffect(() => { + const mql = window.matchMedia('(prefers-color-scheme: dark)') + const apply = () => { + let saved: string | null = null + try { + saved = localStorage.getItem('codeburn-theme') + } catch { + // storage disabled: OS theme only + } + if (saved !== 'dark' && saved !== 'light') { + document.documentElement.classList.toggle('dark', mql.matches) + } + } + mql.addEventListener('change', apply) + return () => mql.removeEventListener('change', apply) + }, []) + const showCombined = multi && view === 'all' const viewTitle = showCombined ? 'All devices' : (primary ? primary.name + (primary.local ? ' · this Mac' : '') : 'Loading…') const label = local?.payload?.current?.label ?? '' @@ -466,7 +521,7 @@ export function App() { return (
-
+
@@ -638,7 +694,7 @@ export function App() { type="checkbox" checked={shareInfo.always} onChange={() => void toggleAlways()} - className="h-3.5 w-3.5 accent-[#1f8a5b]" + className="h-3.5 w-3.5 accent-primary" /> Keep sharing always diff --git a/dash/src/components/ContextExplorer.tsx b/dash/src/components/ContextExplorer.tsx index 2fa67af..7538fd5 100644 --- a/dash/src/components/ContextExplorer.tsx +++ b/dash/src/components/ContextExplorer.tsx @@ -106,7 +106,7 @@ function SessionDetails({ provider, id }: { provider: ContextProvider; id: strin {pct}%
-
= 80 ? 'bg-[#c8541f]' : 'bg-primary')} style={{ width: `${pct}%` }} /> +
= 80 ? 'bg-chart-5' : 'bg-primary')} style={{ width: `${pct}%` }} />
)} diff --git a/dash/src/components/DeviceSearchModal.tsx b/dash/src/components/DeviceSearchModal.tsx index 8181c37..6eacb3b 100644 --- a/dash/src/components/DeviceSearchModal.tsx +++ b/dash/src/components/DeviceSearchModal.tsx @@ -115,7 +115,7 @@ export function DeviceSearchModal({ onClose, onPaired }: { onClose: () => void; )} {status &&

{status}

} - {error &&

{error}

} + {error &&

{error}

}
diff --git a/dash/src/components/UsageChart.tsx b/dash/src/components/UsageChart.tsx index 1c1ce1b..926409e 100644 --- a/dash/src/components/UsageChart.tsx +++ b/dash/src/components/UsageChart.tsx @@ -26,7 +26,7 @@ function makeTooltip(labels: Record, fmt: (n: number) => string, // eslint-disable-next-line @typescript-eslint/no-explicit-any const total = items.reduce((s: number, p: any) => s + p.value, 0) return ( -
+
{formatPeriod(String(lbl))}
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} @@ -252,7 +252,7 @@ function StackedBars({ tick={{ fontSize: 11, fill: 'var(--color-tertiary-foreground)' }} tickFormatter={axisFmt} /> - } /> + } /> {series.map((s, i) => ( by index.html before first paint and toggled in the header. + */ +.dark { + color-scheme: dark; + + --background: #0e1013; + --outer-background: #0e1013; + --foreground: #e8eaee; + --card: #16181d; + --card-foreground: #e8eaee; + --popover: #16181d; + --popover-foreground: #e8eaee; + --muted: #1a1d22; + --muted-foreground: #959ca8; + --tertiary-foreground: #8b93a1; + --heading: #7aa86f; + --border: #282c33; + --input: #282c33; + --interactive-secondary: rgba(255, 255, 255, 0.06); + --interactive-secondary-hover: rgba(255, 255, 255, 0.1); + --active-primary: #2a2f38; + --accent: #1a1d22; + --accent-foreground: #e8eaee; + --subtle: #8b93a1; + --primary: #3ecf8e; + --primary-foreground: #0e1013; + --ring: #3ecf8e; + --positive: #3ecf8e; + --brand: #f2701c; + + --chart-1: #3ecf8e; + --chart-2: #7ce0b0; + --chart-3: #2f9e6e; + --chart-4: #e8b93e; + --chart-5: #f2701c; + --chart-6: #5b9bef; + --chart-7: #8bb6a0; + --chart-8: #f26d6d; + --chart-9: #4fbf93; + --chart-10: #d5b26a; + --chart-grid-stroke: rgba(255, 255, 255, 0.08); + --chart-hover-cursor: rgba(255, 255, 255, 0.06); +} + @theme inline { --color-background: var(--background); --color-outer-background: var(--outer-background); @@ -75,6 +125,7 @@ --color-primary-foreground: var(--primary-foreground); --color-ring: var(--ring); --color-positive: var(--positive); + --color-brand: var(--brand); --color-chart-1: var(--chart-1); --color-chart-2: var(--chart-2); diff --git a/dash/src/lib/utils.ts b/dash/src/lib/utils.ts index 95f28c1..1a6de0d 100644 --- a/dash/src/lib/utils.ts +++ b/dash/src/lib/utils.ts @@ -36,11 +36,11 @@ export function compactUsd(n: number): string { return sign + '$' + Math.round(a) } -// Forest green -> gold -> terracotta ramp for stacked series (mirrors the -// --chart-* tokens). Warm and on-brand, distinct enough to read when stacked. +// Forest green -> gold -> terracotta ramp for stacked series. Referenced as CSS +// custom properties so the palette follows the active theme (light or dark). export const CHART_COLORS = [ - '#1f8a5b', '#4fd394', '#2c5242', '#d99a3c', '#c8541f', - '#2f5fd0', '#7aa86f', '#b5403a', '#3f8f6b', '#a98b4f', + 'var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)', 'var(--chart-5)', + 'var(--chart-6)', 'var(--chart-7)', 'var(--chart-8)', 'var(--chart-9)', 'var(--chart-10)', ] const MODEL_LABELS: Record = {