Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions dash/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
66 changes: 61 additions & 5 deletions dash/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -459,14 +495,33 @@ 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 ?? ''

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)}
Expand All @@ -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>
Expand Down Expand Up @@ -550,6 +605,7 @@ export function App() {
</select>
</>
)}
<ThemeToggle />
</div>
</header>

Expand Down Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion dash/src/components/ContextExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
)}
Expand Down
2 changes: 1 addition & 1 deletion dash/src/components/DeviceSearchModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
4 changes: 2 additions & 2 deletions dash/src/components/UsageChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 */}
Expand Down Expand Up @@ -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}
Expand Down
51 changes: 51 additions & 0 deletions dash/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
--primary-foreground: #ffffff;
--ring: #1f8a5b;
--positive: #1f8a5b;
--brand: #e8553a;

--chart-1: #1f8a5b;
--chart-2: #4fd394;
Expand All @@ -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);
Expand All @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions dash/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> = {
Expand Down