mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-04 05:41:29 +00:00
feat(dash): dark mode with pre-paint bootstrap and themed charts
This commit is contained in:
parent
fd4ede2bb3
commit
6fca845098
7 changed files with 133 additions and 13 deletions
|
|
@ -5,6 +5,19 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/png" href="/codeburn-logo.png" />
|
||||
<title>CodeBurn - Local Dashboard</title>
|
||||
<script>
|
||||
// Apply the saved theme before first paint so the dashboard never flashes light.
|
||||
(function () {
|
||||
var saved = null
|
||||
try {
|
||||
saved = localStorage.getItem('codeburn-theme')
|
||||
} catch (e) {
|
||||
// storage disabled (some embeds/webviews): fall back to OS theme
|
||||
}
|
||||
var dark = saved === 'dark' || (saved !== 'light' && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
if (dark) document.documentElement.classList.add('dark')
|
||||
})()
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
aria-label={dark ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
title={dark ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border bg-card text-tertiary-foreground transition-colors hover:bg-interactive-secondary hover:text-foreground max-md:h-9 max-md:w-9 max-md:shrink-0"
|
||||
>
|
||||
{dark ? (
|
||||
<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M14 8.53A6 6 0 1 1 7.47 2 4.67 4.67 0 0 0 14 8.53Z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round">
|
||||
<circle cx="8" cy="8" r="3.1" />
|
||||
<path d="M8 1.8v1.7M8 12.5v1.7M1.8 8h1.7M12.5 8h1.7M3.5 3.5l1.2 1.2M11.3 11.3l1.2 1.2M12.5 3.5l-1.2 1.2M4.7 11.3l-1.2 1.2" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function App() {
|
||||
const [page, setPage] = useState<'usage' | 'context'>('usage')
|
||||
const [period, setPeriod] = useState<Period>('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 (
|
||||
<div className="min-h-screen bg-outer-background p-2.5 max-md:min-h-[100dvh]">
|
||||
<div className="flex h-[calc(100vh-20px)] flex-col gap-2.5 max-md:h-[calc(100dvh-20px)]">
|
||||
<header className="flex h-12 shrink-0 items-center gap-4 rounded-md border border-border bg-card px-5 shadow-[0_2px_8px_rgba(0,0,0,0.03)] max-md:gap-3 max-md:px-3">
|
||||
<header className="flex h-12 shrink-0 items-center gap-4 rounded-md border border-border bg-card px-5 shadow-[0_2px_8px_rgba(0,0,0,0.03)] dark:shadow-[0_2px_8px_rgba(0,0,0,0.5)] max-md:gap-3 max-md:px-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
|
|
@ -482,7 +537,7 @@ export function App() {
|
|||
<div className="flex items-center gap-2 max-md:shrink-0">
|
||||
<img src="/codeburn-logo.png" alt="CodeBurn" className="h-6 w-6" />
|
||||
<span className="text-lg font-semibold tracking-[-0.02em] text-foreground">
|
||||
Code<span className="text-[#e8553a]">Burn</span>
|
||||
Code<span className="text-brand">Burn</span>
|
||||
</span>
|
||||
<span className="ml-1 text-[11px] font-light uppercase tracking-[0.14em] text-tertiary-foreground max-sm:hidden">usage</span>
|
||||
</div>
|
||||
|
|
@ -550,6 +605,7 @@ export function App() {
|
|||
</select>
|
||||
</>
|
||||
)}
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
@ -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
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ function SessionDetails({ provider, id }: { provider: ContextProvider; id: strin
|
|||
<span className="tabular-nums">{pct}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 overflow-hidden rounded-full bg-interactive-secondary">
|
||||
<div className={cn('h-full rounded-full', pct >= 80 ? 'bg-[#c8541f]' : 'bg-primary')} style={{ width: `${pct}%` }} />
|
||||
<div className={cn('h-full rounded-full', pct >= 80 ? 'bg-chart-5' : 'bg-primary')} style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export function DeviceSearchModal({ onClose, onPaired }: { onClose: () => void;
|
|||
)}
|
||||
|
||||
{status && <p className="mt-3 text-xs text-tertiary-foreground">{status}</p>}
|
||||
{error && <p className="mt-3 text-xs text-[#b5403a]">{error}</p>}
|
||||
{error && <p className="mt-3 text-xs text-chart-8">{error}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ function makeTooltip(labels: Record<string, string>, 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 (
|
||||
<div className="rounded-lg border border-border bg-popover px-3 py-2 text-xs shadow-xl ring-1 ring-black/5">
|
||||
<div className="rounded-lg border border-border bg-popover px-3 py-2 text-xs shadow-xl ring-1 ring-border">
|
||||
<div className="mb-1.5 font-medium text-foreground">{formatPeriod(String(lbl))}</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{/* 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}
|
||||
/>
|
||||
<Tooltip cursor={{ fill: 'rgba(0,0,0,0.04)' }} content={<Tip />} />
|
||||
<Tooltip cursor={{ fill: 'var(--chart-hover-cursor)' }} content={<Tip />} />
|
||||
{series.map((s, i) => (
|
||||
<Bar
|
||||
key={s.key}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
--primary-foreground: #ffffff;
|
||||
--ring: #1f8a5b;
|
||||
--positive: #1f8a5b;
|
||||
--brand: #e8553a;
|
||||
|
||||
--chart-1: #1f8a5b;
|
||||
--chart-2: #4fd394;
|
||||
|
|
@ -47,10 +48,59 @@
|
|||
--chart-9: #3f8f6b;
|
||||
--chart-10: #a98b4f;
|
||||
--chart-grid-stroke: rgba(23, 27, 32, 0.07);
|
||||
--chart-hover-cursor: rgba(0, 0, 0, 0.04);
|
||||
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
/*
|
||||
* Dark surface: reuses the Electron app's dark tokens (app/renderer/styles/
|
||||
* plain.css) so the dashboard and the desktop shell stay visually consistent.
|
||||
* Chart colors are re-derived for dark-background contrast. The .dark class is
|
||||
* applied to <html> 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);
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue