diff --git a/app/renderer/App.tsx b/app/renderer/App.tsx index e864c7ef..9e4db6d2 100644 --- a/app/renderer/App.tsx +++ b/app/renderer/App.tsx @@ -15,7 +15,7 @@ import { Models } from './sections/Models' import { Sessions } from './sections/Sessions' import { Compare } from './sections/Compare' import { Plans } from './sections/Plans' -import { Settings } from './sections/Settings' +import { Settings, type SettingsPane } from './sections/Settings' import { SpendContent } from './sections/Spend' import type { DateRange, MenubarPayload, Period } from './lib/types' @@ -65,6 +65,7 @@ function refreshedLabel(lastSuccessAt: number | null, loading: boolean, now: num export function App() { const [section, setSection] = useState
('overview') + const [settingsPane, setSettingsPane] = useState('general') const [period, setPeriod] = useState('30days') const [provider, setProvider] = useState('all') const [detectedProviders, setDetectedProviders] = useState([]) @@ -107,18 +108,23 @@ export function App() { setRefreshToken(token => token + 1) }, [refreshOverview]) + const navigate = useCallback((next: Section, pane: SettingsPane = 'general') => { + setSettingsPane(pane) + setSection(next) + }, []) + useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if (!event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return const key = event.key.toLowerCase() - if (key === '1') setSection('overview') - else if (key === '2') setSection('sessions') - else if (key === '3') setSection('spend') - else if (key === '4') setSection('optimize') - else if (key === '5') setSection('models') - else if (key === '6') setSection('compare') - else if (key === '7') setSection('plans') - else if (key === ',') setSection('settings') + if (key === '1') navigate('overview') + else if (key === '2') navigate('sessions') + else if (key === '3') navigate('spend') + else if (key === '4') navigate('optimize') + else if (key === '5') navigate('models') + else if (key === '6') navigate('compare') + else if (key === '7') navigate('plans') + else if (key === ',') navigate('settings') else if (key === 'r') refreshVisible() else return event.preventDefault() @@ -126,7 +132,7 @@ export function App() { document.addEventListener('keydown', onKeyDown) return () => document.removeEventListener('keydown', onKeyDown) - }, [refreshVisible]) + }, [refreshVisible, navigate]) const onPeriodChange = (value: string) => { if (isPeriod(value)) { @@ -144,13 +150,13 @@ export function App() { return ( - } /> + } />
{section === 'plans' ? ( - + ) : section === 'settings' ? ( - + ) : ( <>
{section === 'overview' ? ( - + ) : section === 'sessions' ? ( ) : section === 'spend' ? ( @@ -175,7 +181,7 @@ export function App() { ) : section === 'optimize' ? ( ) : section === 'models' ? ( - + ) : section === 'compare' ? ( ) : ( diff --git a/app/renderer/components/Sankey.tsx b/app/renderer/components/Sankey.tsx index 5b4fb449..0753b596 100644 --- a/app/renderer/components/Sankey.tsx +++ b/app/renderer/components/Sankey.tsx @@ -1,5 +1,5 @@ import { formatUsd } from '../lib/format' -import { isOtherNode, seriesHexForModel } from '../lib/modelSeries' +import { isOtherNode, seriesColorForModel } from '../lib/modelSeries' import type { SpendFlow, SpendFlowNode } from '../lib/types' type LayoutNode = SpendFlowNode & { @@ -98,12 +98,12 @@ export function Sankey({ flow }: { flow: SpendFlow }) { ))} {models.map(node => ( - + {node.displayLabel} · {formatUsd(node.cost)} ))} {projects.map(node => ( - + {node.displayLabel} · {formatUsd(node.cost)} ))} @@ -124,7 +124,7 @@ function layoutNodes(nodes: SpendFlowNode[], x: number, modelSide: boolean): Lay return nodes.map((node, i) => { const h = Math.max(2, inflated[i] * scale) const neutral = isOtherNode(node.id) || isOtherNode(node.label) - const fill = modelSide && !neutral ? seriesHexForModel(node.label || node.id) : neutral ? '#5F6780' : '#3A4258' + const fill = modelSide && !neutral ? seriesColorForModel(node.label || node.id) : neutral ? 'var(--s-other)' : 'var(--mut2)' const displayLabel = modelSide ? modelDisplayLabel(node.label || node.id) : projectDisplayLabel(node.label || node.id) const laidOut = { ...node, x, y, h, fill, displayLabel } y += h + GAP diff --git a/app/renderer/components/StackedBars.tsx b/app/renderer/components/StackedBars.tsx index 6fec1547..e8b5169c 100644 --- a/app/renderer/components/StackedBars.tsx +++ b/app/renderer/components/StackedBars.tsx @@ -1,5 +1,5 @@ import { formatUsd } from '../lib/format' -import { SERIES_HEX, SERIES_LABELS, type SeriesKey, seriesClassForModel, seriesKeyForModel } from '../lib/modelSeries' +import { SERIES_LABELS, type SeriesKey, seriesClassForKey, seriesClassForModel, seriesKeyForModel } from '../lib/modelSeries' import type { DailyHistoryEntry } from '../lib/types' export function StackedBars({ daily }: { daily: DailyHistoryEntry[] }) { @@ -37,7 +37,7 @@ export function StackedBars({ daily }: { daily: DailyHistoryEntry[] }) {
{legendSeries.map(series => ( - + {SERIES_LABELS[series]} ))} diff --git a/app/renderer/lib/modelSeries.ts b/app/renderer/lib/modelSeries.ts index 7fe2cabc..57af4305 100644 --- a/app/renderer/lib/modelSeries.ts +++ b/app/renderer/lib/modelSeries.ts @@ -17,11 +17,11 @@ export const SERIES_LABELS: Record = { } const SERIES_CSS_VAR: Record = { - opus: 'var(--blue)', - sonnet: 'var(--purple)', - haiku: 'var(--lav)', - gpt: 'var(--cyan)', - other: 'var(--t3)', + opus: 'var(--s-opus)', + sonnet: 'var(--s-sonnet)', + haiku: 'var(--s-haiku)', + gpt: 'var(--s-gpt)', + other: 'var(--s-other)', } const SERIES_CLASS: Record = { @@ -49,6 +49,10 @@ export function seriesClassForModel(model?: string): string { return SERIES_CLASS[seriesKeyForModel(model)] } +export function seriesClassForKey(series: SeriesKey): string { + return SERIES_CLASS[series] +} + export function seriesHexForModel(model?: string): string { return SERIES_HEX[seriesKeyForModel(model)] } diff --git a/app/renderer/sections/Models.test.tsx b/app/renderer/sections/Models.test.tsx index 02eed528..5476883b 100644 --- a/app/renderer/sections/Models.test.tsx +++ b/app/renderer/sections/Models.test.tsx @@ -124,8 +124,8 @@ describe('Models', () => { expect(screen.getByText('$35.10')).toHaveClass('pos') const dots = [...container.querySelectorAll('.mdot')] - expect(dots[0]).toHaveAttribute('style', expect.stringContaining('var(--blue)')) - expect(dots[1]).toHaveAttribute('style', expect.stringContaining('var(--cyan)')) + expect(dots[0]).toHaveAttribute('style', expect.stringContaining('var(--s-opus)')) + expect(dots[1]).toHaveAttribute('style', expect.stringContaining('var(--s-gpt)')) }) it('renders codex rows with credits and real cost as priced', async () => { diff --git a/app/renderer/sections/Models.tsx b/app/renderer/sections/Models.tsx index 4f75568a..6e51ca53 100644 --- a/app/renderer/sections/Models.tsx +++ b/app/renderer/sections/Models.tsx @@ -4,6 +4,7 @@ import { CliErrorPanel } from '../components/CliErrorPanel' import { seriesColorForModel } from '../components/ListRow' import { Panel } from '../components/Panel' import { SegTabs } from '../components/SegTabs' +import type { Section } from '../components/Sidebar' import { usePolled } from '../hooks/usePolled' import { formatUsd } from '../lib/format' import { codeburn } from '../lib/ipc' @@ -38,11 +39,13 @@ export function Models({ provider, range = null, refreshToken = 0, + onNavigate, }: { period: Period provider: string range?: DateRange | null refreshToken?: number + onNavigate?: (section: Section) => void }) { const [lens, setLens] = useState('model') const byTask = lens === 'task' @@ -64,9 +67,9 @@ export function Models({ <>
setLens(value as ModelsLens)} /> - +
{report.data.length ? ( diff --git a/app/renderer/sections/Overview.test.tsx b/app/renderer/sections/Overview.test.tsx index 2cea2266..e5d707d7 100644 --- a/app/renderer/sections/Overview.test.tsx +++ b/app/renderer/sections/Overview.test.tsx @@ -245,8 +245,11 @@ describe('Overview', () => { expect(container.querySelector('.ov-spark')).not.toBeInTheDocument() expect(screen.getByText('82%')).toBeInTheDocument() - expect(screen.getByText('$84.20')).toBeInTheDocument() - expect(screen.getByText('from 11 applied fixes')).toBeInTheDocument() + // Scope to the KPI card (kpis, declared above): month-to-date spend can + // equal the saved figure on some dates, so an unscoped getByText('$84.20') + // would match two cards. + expect(within(kpis).getByText('$84.20')).toBeInTheDocument() + expect(within(kpis).getByText('from 11 applied fixes')).toBeInTheDocument() const statsCard = screen.getByText('Month to date').closest('.ov-stats3') expect(statsCard).toHaveClass('ov-card') expect(statsCard?.querySelector(':scope > .ov-fuel')).toBeInTheDocument() @@ -347,12 +350,12 @@ describe('Overview', () => { // (project|date|calls|cost) → claude-opus-4 → blue dot + model in sub-line. expect(await screen.findByText('Jul 8 · claude-opus-4 · 41 calls')).toBeInTheDocument() const firstDot = container.querySelectorAll('.li')[0].querySelector('.mdot') - expect(firstDot?.getAttribute('style')).toContain('var(--blue)') + expect(firstDot?.getAttribute('style')).toContain('var(--s-opus)') // pairing-svc has no matching project → neutral dot, model omitted from sub. expect(screen.getByText('Jul 7 · 33 calls')).toBeInTheDocument() const secondDot = container.querySelectorAll('.li')[1].querySelector('.mdot') - expect(secondDot?.getAttribute('style')).toContain('var(--t3)') + expect(secondDot?.getAttribute('style')).toContain('var(--s-other)') }) it('disambiguates two same-project/same-day/same-calls sessions by cost', async () => { @@ -406,8 +409,8 @@ describe('Overview', () => { expect(await screen.findByText('Jul 8 · claude-opus-4 · 20 calls')).toBeInTheDocument() expect(screen.getByText('Jul 8 · claude-haiku-4 · 20 calls')).toBeInTheDocument() const dots = container.querySelectorAll('.li .mdot') - expect(dots[0].getAttribute('style')).toContain('var(--blue)') // $10 → opus - expect(dots[1].getAttribute('style')).toContain('var(--lav)') // $2 → haiku + expect(dots[0].getAttribute('style')).toContain('var(--s-opus)') // $10 → opus + expect(dots[1].getAttribute('style')).toContain('var(--s-haiku)') // $2 → haiku }) it('shows the first-run locate-CLI state when the binary is missing', async () => { diff --git a/app/renderer/sections/Plans.tsx b/app/renderer/sections/Plans.tsx index ca6e731e..814a7fa4 100644 --- a/app/renderer/sections/Plans.tsx +++ b/app/renderer/sections/Plans.tsx @@ -1,9 +1,11 @@ import { CliErrorPanel } from '../components/CliErrorPanel' import { Panel } from '../components/Panel' +import type { Section } from '../components/Sidebar' import { usePolled } from '../hooks/usePolled' import { formatUsd } from '../lib/format' import { codeburn } from '../lib/ipc' import type { JsonPlanSummary, Period, PlanId, PlanProvider, StatusJson } from '../lib/types' +import type { SettingsPane } from './Settings' const PROVIDER_ORDER: PlanProvider[] = ['all', 'claude', 'codex', 'cursor', 'grok'] const MS_PER_DAY = 24 * 60 * 60 * 1000 @@ -76,7 +78,7 @@ function planSummaries(status: StatusJson): JsonPlanSummary[] { return status.plan ? [status.plan] : [] } -export function Plans({ period, refreshToken = 0 }: { period: Period; refreshToken?: number }) { +export function Plans({ period, refreshToken = 0, onNavigate }: { period: Period; refreshToken?: number; onNavigate?: (section: Section, pane?: SettingsPane) => void }) { const report = usePolled(() => codeburn.getPlans(period), [period, refreshToken]) const plans = report.data ? planSummaries(report.data) : [] const cycle = cycleLabels(plans[0]) @@ -87,10 +89,10 @@ export function Plans({ period, refreshToken = 0 }: { period: Period; refreshTok
Plans
{cycle ? {cycle.caption} : Cycle unavailable}
-
{cycle ? cycle.pop : 'Cycle unavailable'}
- + {cycle ? cycle.pop : 'Cycle unavailable'} +
{renderBody(report.data, report.error, plans)}
diff --git a/app/renderer/sections/Settings.tsx b/app/renderer/sections/Settings.tsx index 11bee576..ddf4115d 100644 --- a/app/renderer/sections/Settings.tsx +++ b/app/renderer/sections/Settings.tsx @@ -9,7 +9,8 @@ import { formatUsd } from '../lib/format' import { codeburn } from '../lib/ipc' import type { ActionResult, AliasRow, CliError, CombinedUsage, DeviceScanResult, Identity, JsonPlanSummary, MenubarPayload, Period, PlanId, PlanProvider, ShareStatus, StatusJson } from '../lib/types' -type Pane = 'general' | 'providers' | 'aliases' | 'plans' | 'devices' | 'export' | 'privacy' +export type SettingsPane = 'general' | 'providers' | 'aliases' | 'plans' | 'devices' | 'export' | 'privacy' +type Pane = SettingsPane type Theme = 'system' | 'light' | 'dark' type PlanPreset = { id: Exclude; label: string; provider: Exclude } @@ -55,8 +56,8 @@ function shortFingerprint(fingerprint: string): string { return `${parts[0]}:${parts[1]}:…:${parts[parts.length - 1]}` } -export function Settings({ period, refreshToken = 0, onNavigate }: { period: Period; refreshToken?: number; onNavigate?: (section: Section) => void }) { - const [pane, setPane] = useState('general') +export function Settings({ period, refreshToken = 0, onNavigate, initialPane }: { period: Period; refreshToken?: number; onNavigate?: (section: Section) => void; initialPane?: SettingsPane }) { + const [pane, setPane] = useState(initialPane ?? 'general') return ( <> @@ -271,7 +272,7 @@ function PrivacyClaim({ title, detail, icon }: { title: string; detail: string; function ThisDevicePanel({ identity, shareStatus }: { identity: ReturnType>; shareStatus: ReturnType> }) { const status = shareStatus.data ? {shareStatus.data.sharing ? 'Visible' : 'Not sharing'} : null - return {identity.data ?
{identity.data.name}Local device name: {identity.data.name}{identity.data.fingerprint}
: identity.error ? :

Reading this device identity…

}{shareStatus.error && }
+ return {identity.data ?
{identity.data.name}Local device name: {identity.data.name}{identity.data.fingerprint}
: identity.error ? :

Reading this device identity…

}{shareStatus.error && }
} function DiscoveredPanel({ scan }: { scan: ReturnType> }) { @@ -294,6 +295,6 @@ function PairedPanel({ devices, period, onRefresh }: { devices: ReturnType{display.title}

} + if (error.kind === 'not-found') { const display = cliErrorDisplay(error); return

{display.title}

} return } diff --git a/app/renderer/sections/Spend.test.tsx b/app/renderer/sections/Spend.test.tsx index 9192d8d7..e1eeea79 100644 --- a/app/renderer/sections/Spend.test.tsx +++ b/app/renderer/sections/Spend.test.tsx @@ -280,8 +280,8 @@ describe('Spend', () => { const opusRibbon = container.querySelector('[data-testid="sankey-ribbon"][data-model="claude-opus-4-20260701"]') expect(opusRibbon?.getAttribute('stroke')).toBe('url(#sankey-claude-opus-4-20260701)') const opusStop = container.querySelector('linearGradient[id="sankey-claude-opus-4-20260701"] stop') - expect(opusStop?.getAttribute('stop-color')).toBe('#5B8CFF') + expect(opusStop?.getAttribute('stop-color')).toBe('var(--s-opus)') const otherNode = container.querySelector('[data-testid="sankey-node"][data-node-id="__other__"]') - expect(otherNode?.getAttribute('fill')).toBe('#5F6780') + expect(otherNode?.getAttribute('fill')).toBe('var(--s-other)') }) }) diff --git a/app/renderer/styles/indigo.css b/app/renderer/styles/indigo.css index 110bc3b5..76cdc5e6 100644 --- a/app/renderer/styles/indigo.css +++ b/app/renderer/styles/indigo.css @@ -141,7 +141,7 @@ h2 { font-size: 20px; font-weight: 650; letter-spacing: -.015em; margin: 0 0 6px .li .lx span.bad { color: var(--red); } .li .val { font-family: var(--mono); font-size: 12px; font-weight: 500; font-variant-numeric: tabular-nums; } .li .val.ok { color: var(--mint); } -.li .chev { color: #333A50; } +.li .chev { color: var(--mut2); } .mdot { width: 8px; height: 8px; border-radius: 2.5px; flex: 0 0 auto; } .hint { display: flex; gap: 14px; padding: 8px 18px; border-top: 1px solid var(--hair); font-size: 10.5px; color: var(--t3); } diff --git a/app/renderer/styles/plain.css b/app/renderer/styles/plain.css index f54b87bb..3097ff34 100644 --- a/app/renderer/styles/plain.css +++ b/app/renderer/styles/plain.css @@ -16,6 +16,12 @@ --purple: #8a93a3; --lav: #9ca4b1; --cyan: #606a79; + --s-opus: #2a78d6; + --s-sonnet: #e87ba4; + --s-haiku: #eda100; + --s-gpt: #008300; + --s-other: #8b93a1; + --card-shadow: 0 1px 2px rgba(20,24,33,.05), 0 1px 3px rgba(20,24,33,.06); --accent: #e8590c; --bg: #f5f6f8; --line: #e6e8ec; @@ -34,6 +40,11 @@ --tip-ink: #ffffff; --grad: var(--accent); --grad-bar: var(--bar-hi); + --t1: var(--ink); + --t2: var(--mut); + --t3: var(--mut2); + --hair: var(--line); + --hair2: var(--line2); } @media (prefers-color-scheme: dark) { @@ -54,6 +65,12 @@ --purple: #69717e; --lav: #9299a5; --cyan: #5d6571; + --s-opus: #3987e5; + --s-sonnet: #d55181; + --s-haiku: #c98500; + --s-gpt: #008300; + --s-other: #6b7280; + --card-shadow: 0 1px 2px rgba(0,0,0,.24), 0 1px 3px rgba(0,0,0,.30); --accent: #f2701c; --bg: #0e1013; --line: #282c33; @@ -79,6 +96,8 @@ --hair: #262a31; --hair2: #1e2127; --t1: #e6e8ec; --t2: #9aa2ae; --t3: #6b7280; --mint: #37b24d; --amber: #f2a60c; --red: #ff6b6b; --blue: #7d8592; --purple: #69717e; --lav: #9299a5; --cyan: #5d6571; + --s-opus: #3987e5; --s-sonnet: #d55181; --s-haiku: #c98500; --s-gpt: #008300; --s-other: #6b7280; + --card-shadow: 0 1px 2px rgba(0,0,0,.24), 0 1px 3px rgba(0,0,0,.30); --accent: #f2701c; --bg: #0e1013; --line: #282c33; --line2: #20242a; --ink: #e8eaee; --mut: #959ca8; --mut2: #5f6773; --ok: #3ecf8e; --warn: #e8b93e; --bad: #f26d6d; --bar: #2b3038; --bar-hi: #c6ccd6; @@ -92,6 +111,8 @@ --hair: #e3e5ea; --hair2: #eef0f3; --t1: #1a1d23; --t2: #5b6370; --t3: #8b93a1; --mint: #2f9e44; --amber: #e8850c; --red: #e03131; --blue: #727b89; --purple: #8a93a3; --lav: #9ca4b1; --cyan: #606a79; + --s-opus: #2a78d6; --s-sonnet: #e87ba4; --s-haiku: #eda100; --s-gpt: #008300; --s-other: #8b93a1; + --card-shadow: 0 1px 2px rgba(20,24,33,.05), 0 1px 3px rgba(20,24,33,.06); --accent: #e8590c; --bg: #f5f6f8; --line: #e6e8ec; --line2: #eef0f3; --ink: #181b20; --mut: #697181; --mut2: #a2a9b5; --ok: #12805c; --warn: #a76a00; --bad: #c2382f; --bar: #d7dbe1; --bar-hi: #3a4150; @@ -103,7 +124,7 @@ html, body, #root { height: 100%; margin: 0; } body { overflow: hidden; background: var(--bg); color: var(--ink); } .win { height: 100%; min-height: 0; border-radius: 0; background: var(--canvas); box-shadow: none; } .ct { min-height: 0; } -.body { overflow-y: auto; min-height: 0; align-items: center; } +.body { overflow-y: auto; min-height: 0; align-items: center; padding: 18px 20px; gap: 16px; } /* Children keep their natural height (don't flex-shrink to fit) so the pane scrolls instead of squishing; capped width keeps content from stretching. */ .body > * { flex-shrink: 0; width: 100%; max-width: 1180px; } @@ -143,6 +164,7 @@ body { overflow: hidden; background: var(--bg); color: var(--ink); } .ni.on::before { background: var(--accent); } .seg { background: var(--bg); border: 1px solid var(--hair); } .seg .on { background: var(--panel); box-shadow: inset 0 -2px 0 var(--accent); } +.seg span:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; } .pop-wrap, .calendar-wrap { position: relative; } .pop { background: var(--panel); color: var(--t1); cursor: pointer; user-select: none; } .pop-menu { @@ -173,6 +195,7 @@ body { overflow: hidden; background: var(--bg); color: var(--ink); } } .calendar-nav:hover:not(:disabled) { border-color: var(--line); background: var(--hover); color: var(--t1); } .calendar-nav:disabled { color: var(--mut2); cursor: default; opacity: .45; } +.calendar-nav:focus-visible { outline: 1px solid var(--accent); outline-offset: -1px; } .calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; user-select: none; } .calendar-weekday { padding: 3px 0 5px; color: var(--t3); text-align: center; font-size: 9px; text-transform: uppercase; } .calendar-day { @@ -184,9 +207,11 @@ body { overflow: hidden; background: var(--bg); color: var(--ink); } .calendar-day.in-range { background: color-mix(in srgb, var(--accent) 12%, var(--panel)); color: var(--t1); border-radius: 2px; } .calendar-day.endpoint { background: var(--accent); color: #fff; border-radius: 6px; font-weight: 600; } .calendar-day:disabled { color: var(--mut2); opacity: .38; cursor: default; } +.calendar-day:focus-visible { outline: 1px solid var(--accent); outline-offset: -1px; } .btn-p { background: var(--accent); box-shadow: none; } .btn-s { background: var(--panel); } -.panel { border-color: var(--line); box-shadow: none; } +.btnp:focus-visible, .btn:focus-visible, .btn-s:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; } +.panel { border-color: var(--line); box-shadow: var(--card-shadow); } .error-boundary { display: flex; padding: 20px; } .error-card { max-width: 760px; padding: 16px 18px; } .error-title { margin: 0 0 8px; font-size: 14px; font-weight: 620; color: var(--ink); } @@ -206,7 +231,7 @@ body { overflow: hidden; background: var(--bg); color: var(--ink); } .set-h { font-size: 15px; font-weight: 640; margin: 0; } .set-sub { color: var(--mut); font-size: 12px; margin: 4px 0 0; line-height: 1.5; } .set-cap { color: var(--mut2); font-size: 11px; line-height: 1.6; margin: 0; } -.card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; } +.card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; box-shadow: var(--card-shadow); } .about-sec { padding: 14px 18px; border-bottom: 1px solid var(--line2); } .about-sec.set-last-sec { border-bottom: 0; } .about-sec-h { font-size: 10.5px; font-weight: 600; color: var(--mut2); text-transform: uppercase; letter-spacing: .05em; margin-bottom: 8px; } @@ -224,7 +249,7 @@ body { overflow: hidden; background: var(--bg); color: var(--ink); } .set-alias-ar { color: var(--mut2); text-align: center; } .set-alias-to { color: var(--mut); } .set-alias-empty { padding-bottom: 9px; } -.set-input { border: 1px solid var(--line); border-radius: 6px; padding: 4px 9px; font: inherit; font-size: 11.5px; background: var(--bg); color: var(--ink); min-width: 0; } +.set-input { border: 1px solid var(--line); border-radius: 7px; padding: 6px 10px; font: inherit; font-size: 11.5px; background: var(--panel); color: var(--ink); min-width: 0; } .set-input:focus-visible { border-color: var(--accent); outline: 1px solid var(--accent); } .btnp { font-size: 11.5px; font-weight: 540; color: var(--ink); border: 1px solid var(--line); border-radius: 7px; padding: 5px 11px; background: var(--panel); cursor: pointer; } .btnp-primary { background: var(--accent); color: #fff; border-color: var(--accent); } @@ -249,6 +274,7 @@ body { overflow: hidden; background: var(--bg); color: var(--ink); } .set-p .seg button { border: 0; border-right: 1px solid var(--line2); background: transparent; padding: 5px 11px; font: inherit; font-size: 12px; color: var(--mut); cursor: pointer; } .set-p .seg button:last-child { border-right: 0; } .set-p .seg button.on { background: var(--panel); color: var(--ink); font-weight: 560; box-shadow: inset 0 -2px 0 var(--accent); } +.set-p .seg button:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; } @media (max-width: 700px) { .set-rail { flex-basis: 58px; padding-inline: 8px; } @@ -259,18 +285,24 @@ body { overflow: hidden; background: var(--bg); color: var(--ink); } .set-alias { grid-template-columns: minmax(0, 1fr) 12px minmax(0, 1fr); } .set-alias .btnp { grid-column: 3; justify-self: end; } } -.error-stack { max-height: 320px; margin: 0 0 12px; padding: 10px 12px; overflow: auto; border: 1px solid var(--line2); border-radius: 8px; background: var(--phead); color: var(--mut); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; line-height: 1.5; white-space: pre; } +.error-stack { max-height: 320px; margin: 0 0 12px; padding: 10px 12px; overflow: auto; border: 1px solid var(--line); border-radius: 8px; background: var(--phead); color: var(--mut); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; line-height: 1.5; white-space: pre; } .phead { border-color: var(--line2); } .phead .r.link, a { color: var(--accent); } +.alias { color: var(--accent); } .bars .b, .bars .c.hi .b, .bars .c.hi2 .b { background: var(--bar); box-shadow: none; } .bars .c.hi .b { background: var(--bar-hi); } .bars .c.hi2 .b { background: color-mix(in srgb, var(--bar-hi) 70%, var(--bar)); } +.s-opus { background: var(--s-opus); } +.s-son { background: var(--s-sonnet); } +.s-hai { background: var(--s-haiku); } +.s-gpt { background: var(--s-gpt); } +.s-other { background: var(--s-other); } .track i, .tglon { background: var(--accent); box-shadow: none; } .track i.over { background: var(--bad); } .track i.mut { background: var(--bar); } .ov-dashboard { display: grid; width: 100%; gap: 12px; } -.ov-kpis { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); overflow: hidden; background: var(--panel); border: 1px solid var(--line); border-radius: 10px; } +.ov-kpis { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); overflow: hidden; background: var(--panel); border: 1px solid var(--line); border-radius: 10px; box-shadow: var(--card-shadow); } .ov-kpi { position: relative; display: flex; min-width: 0; flex-direction: column; justify-content: center; gap: 4px; min-height: 64px; padding: 9px 13px; border-right: 1px solid var(--line2); } .ov-kpi:last-child { border-right: 0; } .ov-kpi > span { color: var(--mut); font-size: 11px; font-weight: 520; } @@ -280,7 +312,7 @@ body { overflow: hidden; background: var(--bg); color: var(--ink); } .ov-kpi-primary > span, .ov-kpi-primary > strong { color: var(--accent); } .ov-kpi-saved > strong { color: var(--ok); } .ov-hero-row { display: grid; grid-template-columns: minmax(0, 1fr) 280px 196px; gap: 12px; } -.ov-card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; } +.ov-card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; box-shadow: var(--card-shadow); } .ov-hero { display: flex; flex-direction: column; gap: 6px; padding: 14px 16px; } .ov-label { color: var(--mut); font-size: 11.5px; font-weight: 500; } .ov-hero-top, .ov-fuel-head { display: flex; align-items: center; justify-content: space-between; } @@ -326,6 +358,7 @@ body { overflow: hidden; background: var(--bg); color: var(--ink); } .ov-widget-caption { margin: 8px 0 0; color: var(--mut2); font-size: 10.5px; line-height: 1.4; } .ov-fuel { display: flex; flex-direction: column; padding: 14px 16px; min-height: 0; } .ov-link { color: var(--accent); text-decoration: none; font-size: 11.5px; font-weight: 540; border: 0; background: none; padding: 0; cursor: pointer; } +.ov-link:hover, .ov-link:focus-visible, .ov-coach-cta:hover, .ov-coach-cta:focus-visible { text-decoration: underline; } .ov-ring-wrap { position: relative; width: 98px; height: 98px; margin: 8px auto 2px; } .ov-ring { width: 98px; height: 98px; transform: rotate(-90deg); } .ov-ring circle { fill: none; stroke-width: 8; stroke-linecap: round; } @@ -382,7 +415,7 @@ body { overflow: hidden; background: var(--bg); color: var(--ink); } .ov-analytics-row > :only-child { grid-column: 1 / -1; } .ov-body-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; gap: 12px; } .ov-main-column, .ov-side-column { display: grid; min-width: 0; gap: 12px; } -.ov-outcome-metrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); border: 1px solid var(--line2); border-radius: 7px; } +.ov-outcome-metrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); border: 1px solid var(--line); border-radius: 7px; } .ov-outcome-metrics > div { display: flex; min-width: 0; flex-direction: column; gap: 5px; padding: 9px 10px; border-right: 1px solid var(--line2); } .ov-outcome-metrics > div:last-child { border-right: 0; } .ov-outcome-metrics span { color: var(--mut); font-size: 10px; line-height: 1.25; } @@ -407,6 +440,7 @@ body { overflow: hidden; background: var(--bg); color: var(--ink); } .chart::before { content: ''; position: absolute; right: 0; bottom: 36px; left: 0; height: 1px; background: var(--line2); box-shadow: 0 -37px var(--line2), 0 -74px var(--line2); pointer-events: none; } .chart .col { flex: 1; background: var(--bar); border-radius: 3px 3px 0 0; min-height: 3px; transition: background .14s ease; border: 0; padding: 0; } .chart .col.hi { background: var(--bar-hi); } .chart .col:hover { background: var(--accent); } +.chart .col:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; } .chart-tip { position: fixed; pointer-events: none; max-width: calc(100vw - 16px); opacity: 0; background: var(--tip-bg); color: var(--tip-ink); border: 1px solid color-mix(in srgb, var(--tip-ink) 14%, transparent); border-radius: 7px; padding: 7px 10px; line-height: 1.35; white-space: nowrap; box-shadow: 0 6px 20px rgba(0,0,0,.22); transition: opacity .1s ease; z-index: 1000; } .chart-tip.on { opacity: 1; } .chart-tip-d { color: inherit; opacity: .62; font-size: 10px; } @@ -469,7 +503,7 @@ body { overflow: hidden; background: var(--bg); color: var(--ink); } .sessions-empty { display: flex; align-items: center; gap: 10px; padding: 14px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); } .sessions-clear { padding: 0; border: 0; background: transparent; color: var(--mut); font: inherit; font-size: 11.5px; cursor: pointer; } .sessions-clear:hover, .sessions-clear:focus-visible { color: var(--accent); text-decoration: underline; outline: none; } -.session-list { border: 1px solid var(--line); border-radius: 10px; background: var(--panel); } +.session-list { border: 1px solid var(--line); border-radius: 10px; background: var(--panel); box-shadow: var(--card-shadow); } .session-provider { display: contents; } .provider-h { position: sticky; top: 0; z-index: 2; display: flex; align-items: center; gap: 8px; padding: 11px 14px 6px; background: var(--panel); color: var(--mut2); font-size: 10.5px; font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: .05em; text-transform: uppercase; } .session-row { display: flex; width: 100%; align-items: center; gap: 18px; padding: 11px 14px; border: 0; border-top: 1px solid var(--line2); background: transparent; color: var(--ink); font: inherit; text-align: left; cursor: pointer; } @@ -477,7 +511,7 @@ body { overflow: hidden; background: var(--bg); color: var(--ink); } .session-primary { min-width: 0; flex: 1; } .session-title { display: block; overflow: hidden; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; } .session-project { display: block; overflow: hidden; margin-top: 2px; color: var(--mut2); font-family: var(--mono); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } -.session-meta { display: grid; grid-template-columns: 64px minmax(110px, 1fr) 58px 70px 76px; align-items: center; gap: 10px; color: var(--mut); font-size: 11.5px; font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; } +.session-meta { display: grid; grid-template-columns: 64px minmax(110px, 1fr) 58px 70px 76px; align-items: center; gap: 10px; color: var(--mut); font-family: var(--mono); font-size: 11.5px; font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; } .session-meta span:nth-child(2) { overflow: hidden; text-align: left; text-overflow: ellipsis; } .session-when { min-width: 64px; color: var(--mut2); font-variant-numeric: tabular-nums; text-align: left; } .sessions-more { width: 100%; padding: 9px 12px; border: 1px solid var(--line); border-radius: 7px; background: var(--panel); color: var(--mut); font: inherit; font-size: 11.5px; font-variant-numeric: tabular-nums; cursor: pointer; } @@ -489,7 +523,7 @@ body { overflow: hidden; background: var(--bg); color: var(--ink); } .detail-head { padding: 14px 15px; } .detail-head h3 { margin: 0 0 6px; font-size: 15px; font-weight: 620; } .detail-line { color: var(--mut); font-size: 12px; font-variant-numeric: tabular-nums; } -.stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; } +.stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; } .stat .v { font-size: 24px; font-weight: 640; font-variant-numeric: tabular-nums; letter-spacing: -.03em; line-height: 1.1; } .stat .d { margin-top: 4px; color: var(--mut2); font-size: 10.5px; font-variant-numeric: tabular-nums; } .session-detail-notes { display: flex; flex-wrap: wrap; gap: 8px 24px; color: var(--mut); font-size: 11.5px; font-variant-numeric: tabular-nums; } @@ -501,9 +535,9 @@ select.cmp-select:focus-visible { border-color: var(--accent); outline: 1px soli .cmp-vs { color: var(--mut2); font-size: 10.5px; font-weight: 600; text-transform: uppercase; } .cmp-body { display: flex; width: 100%; flex-direction: column; gap: 14px; } .cmp-pair { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; } -.cmp-card { min-width: 0; overflow: hidden; border-radius: 8px; } +.cmp-card { min-width: 0; overflow: hidden; box-shadow: var(--card-shadow); } .cmp-head { display: flex; min-height: 42px; align-items: center; padding: 0 14px; border-bottom: 1px solid var(--line2); background: var(--phead); } -.cmp-head h3 { margin: 0; font-size: 12.5px; font-weight: 620; letter-spacing: -.005em; } +.cmp-head h3 { margin: 0; font-size: 11.5px; font-weight: 560; color: var(--t2); letter-spacing: -.005em; } .cmp-head-note { margin-left: auto; color: var(--mut2); font-size: 10px; } .cmp-metrics { padding: 4px 14px 7px; } .cmp-metric-head, .cmp-metric { display: grid; grid-template-columns: minmax(120px, 1fr) minmax(74px, .56fr) minmax(74px, .56fr); align-items: center; column-gap: 12px; }