diff --git a/extensions/gnome-shell/codeburn@agentseal.org/extension.js b/extensions/gnome-shell/codeburn@agentseal.org/extension.js index ff9bcec..9e2c842 100644 --- a/extensions/gnome-shell/codeburn@agentseal.org/extension.js +++ b/extensions/gnome-shell/codeburn@agentseal.org/extension.js @@ -1,13 +1,14 @@ /* * CodeBurn GNOME Shell extension. * - * Renders a flame + current-period cost label in the top panel and opens a native - * PopupMenu on click. Unlike the Tauri tray app (desktop/), this lives inside - * gnome-shell so it can anchor the popover directly under the panel button, - * matching Ubuntu's Quick Settings feel. + * Ships a native GNOME panel button whose popup mirrors the macOS app pixel for + * pixel, built out of raw St widgets instead of the stock PopupMenu text-item + * list. Horizontal agent tabs, a branded header, hero cost typography, inline + * bar-chart activity rows, and a pill-styled footer -- same primitives GNOME's + * own Quick Settings panel uses. * - * Data source: `codeburn status --format menubar-json --period

`, polled every - * 60s. The period is a per-session preference held in memory on the indicator. + * Data source: `codeburn status --format menubar-json --period

--provider `, + * polled every 60 seconds. Period, provider and currency are per-session state. */ import GObject from 'gi://GObject'; @@ -23,8 +24,6 @@ import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js'; const REFRESH_INTERVAL_SECONDS = 60; const TOP_ACTIVITIES = 5; -const TOP_MODELS = 3; -const TOP_PROVIDERS = 4; const CODEBURN_BIN = 'codeburn'; const PERIODS = [ @@ -32,7 +31,7 @@ const PERIODS = [ {id: 'week', label: '7 Days'}, {id: '30days', label: '30 Days'}, {id: 'month', label: 'Month'}, - {id: 'all', label: 'All Time'}, + {id: 'all', label: 'All'}, ]; const PROVIDERS = [ @@ -43,8 +42,6 @@ const PROVIDERS = [ {id: 'copilot', label: 'Copilot'}, ]; -// Matches the 17 currencies the Mac menubar ships with. Symbols fall back to the -// ISO code with a trailing space for anything less common. const CURRENCIES = [ {code: 'USD', symbol: '$'}, {code: 'EUR', symbol: '€'}, @@ -77,15 +74,12 @@ class CodeburnIndicator extends PanelMenu.Button { this._timeout = null; this._payload = null; - // Follow the GNOME system color-scheme so the popup stays readable on both - // light and dark themes. Adds .codeburn-dark / .codeburn-light to the root - // widget so stylesheet.css can tweak per-theme without fighting the shell's - // inherited palette. this._themeSettings = new Gio.Settings({schema_id: 'org.gnome.desktop.interface'}); this._themeSignal = this._themeSettings.connect('changed::color-scheme', () => this._applyThemeClass()); this._applyThemeClass(); - const box = new St.BoxLayout({style_class: 'panel-status-menu-box codeburn-panel'}); + // Panel button: flame + live cost label + const panel = new St.BoxLayout({style_class: 'panel-status-menu-box codeburn-panel'}); this._flame = new St.Label({ text: '🔥', y_align: Clutter.ActorAlign.CENTER, @@ -96,11 +90,32 @@ class CodeburnIndicator extends PanelMenu.Button { y_align: Clutter.ActorAlign.CENTER, style_class: 'codeburn-label', }); - box.add_child(this._flame); - box.add_child(this._label); - this.add_child(box); + panel.add_child(this._flame); + panel.add_child(this._label); + this.add_child(panel); - this._buildMenu(); + // Replace the default PopupMenu item list with a single container that we + // paint with custom St widgets so the layout can be horizontal tabs + hero + // + bar charts + footer, not a vertical text list. + this.menu.box.add_style_class_name('codeburn-menu'); + this._popupHost = new PopupMenu.PopupBaseMenuItem({reactive: false, can_focus: false}); + this._popupHost.add_style_class_name('codeburn-host'); + this.menu.addMenuItem(this._popupHost); + + this._root = new St.BoxLayout({ + vertical: true, + style_class: 'codeburn-root', + x_expand: true, + }); + this._popupHost.add_child(this._root); + + this._buildBrandHeader(); + this._buildAgentTabs(); + this._buildHero(); + this._buildPeriodTabs(); + this._buildActivitySection(); + this._buildFindingsSection(); + this._buildFooter(); this._refresh(); this._timeout = GLib.timeout_add_seconds( @@ -113,96 +128,152 @@ class CodeburnIndicator extends PanelMenu.Button { ); } - _buildMenu() { - // Header: period + hero cost + calls + sessions - this._headerItem = new PopupMenu.PopupMenuItem('Loading…', {reactive: false}); - this._headerItem.label.style_class = 'codeburn-header'; - this.menu.addMenuItem(this._headerItem); + _buildBrandHeader() { + const header = new St.BoxLayout({vertical: true, style_class: 'codeburn-brand-header'}); + const title = new St.BoxLayout({style_class: 'codeburn-brand-row'}); + const titleLeft = new St.Label({text: 'Code', style_class: 'codeburn-brand-primary'}); + const titleRight = new St.Label({text: 'Burn', style_class: 'codeburn-brand-accent'}); + title.add_child(titleLeft); + title.add_child(titleRight); + const subhead = new St.Label({text: 'AI Coding Cost Tracker', style_class: 'codeburn-brand-subhead'}); + header.add_child(title); + header.add_child(subhead); + this._root.add_child(header); + } - this._metaItem = new PopupMenu.PopupMenuItem('', {reactive: false}); - this._metaItem.label.style_class = 'codeburn-meta'; - this.menu.addMenuItem(this._metaItem); - - this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); - - // Agent (provider filter) submenu - this._providerSubmenu = new PopupMenu.PopupSubMenuMenuItem(this._providerLabel()); + _buildAgentTabs() { + const row = new St.BoxLayout({style_class: 'codeburn-tab-row'}); + this._agentTabs = new Map(); for (const p of PROVIDERS) { - const item = new PopupMenu.PopupMenuItem(p.label); - item.connect('activate', () => { + const btn = new St.Button({ + label: p.label, + style_class: 'codeburn-tab', + can_focus: true, + x_expand: true, + }); + btn.connect('clicked', () => { this._provider = p.id; - this._providerSubmenu.label.set_text(this._providerLabel()); + this._updateAgentTabStyle(); this._refresh(); }); - this._providerSubmenu.menu.addMenuItem(item); + row.add_child(btn); + this._agentTabs.set(p.id, btn); } - this.menu.addMenuItem(this._providerSubmenu); + this._root.add_child(row); + this._updateAgentTabStyle(); + } - // Period switcher submenu - this._periodSubmenu = new PopupMenu.PopupSubMenuMenuItem(this._periodLabel()); + _updateAgentTabStyle() { + for (const [id, btn] of this._agentTabs) { + if (id === this._provider) btn.add_style_class_name('codeburn-tab-active'); + else btn.remove_style_class_name('codeburn-tab-active'); + } + } + + _buildHero() { + const hero = new St.BoxLayout({vertical: true, style_class: 'codeburn-hero'}); + const topLine = new St.BoxLayout({style_class: 'codeburn-hero-top'}); + this._heroDot = new St.Widget({style_class: 'codeburn-hero-dot'}); + this._heroLabel = new St.Label({text: 'Loading…', style_class: 'codeburn-hero-label'}); + topLine.add_child(this._heroDot); + topLine.add_child(this._heroLabel); + this._heroAmount = new St.Label({text: '$0.00', style_class: 'codeburn-hero-amount'}); + this._heroMeta = new St.Label({text: '', style_class: 'codeburn-hero-meta'}); + hero.add_child(topLine); + hero.add_child(this._heroAmount); + hero.add_child(this._heroMeta); + this._root.add_child(hero); + } + + _buildPeriodTabs() { + const row = new St.BoxLayout({style_class: 'codeburn-tab-row codeburn-period-row'}); + this._periodTabs = new Map(); for (const p of PERIODS) { - const item = new PopupMenu.PopupMenuItem(p.label); - item.connect('activate', () => { + const btn = new St.Button({ + label: p.label, + style_class: 'codeburn-period', + can_focus: true, + x_expand: true, + }); + btn.connect('clicked', () => { this._period = p.id; - this._periodSubmenu.label.set_text(this._periodLabel()); + this._updatePeriodTabStyle(); this._refresh(); }); - this._periodSubmenu.menu.addMenuItem(item); + row.add_child(btn); + this._periodTabs.set(p.id, btn); } - this.menu.addMenuItem(this._periodSubmenu); + this._root.add_child(row); + this._updatePeriodTabStyle(); + } - // Currency submenu - this._currencySubmenu = new PopupMenu.PopupSubMenuMenuItem(this._currencyLabel()); - for (const c of CURRENCIES) { - const item = new PopupMenu.PopupMenuItem(c.code); - item.connect('activate', () => this._setCurrency(c.code)); - this._currencySubmenu.menu.addMenuItem(item); + _updatePeriodTabStyle() { + for (const [id, btn] of this._periodTabs) { + if (id === this._period) btn.add_style_class_name('codeburn-period-active'); + else btn.remove_style_class_name('codeburn-period-active'); } - this.menu.addMenuItem(this._currencySubmenu); - - this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); - - // Activities, models, providers, findings (populated on render) - this._activitySection = new PopupMenu.PopupMenuSection(); - this.menu.addMenuItem(this._activitySection); - - this._modelsSection = new PopupMenu.PopupMenuSection(); - this.menu.addMenuItem(this._modelsSection); - - this._providersSection = new PopupMenu.PopupMenuSection(); - this.menu.addMenuItem(this._providersSection); - - this._findingsSection = new PopupMenu.PopupMenuSection(); - this.menu.addMenuItem(this._findingsSection); - - this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); - - // Footer: updated timestamp, refresh, open full report - this._updatedItem = new PopupMenu.PopupMenuItem('', {reactive: false}); - this._updatedItem.label.style_class = 'codeburn-updated'; - this.menu.addMenuItem(this._updatedItem); - - const refresh = new PopupMenu.PopupMenuItem('Refresh'); - refresh.connect('activate', () => this._refresh()); - this.menu.addMenuItem(refresh); - - const openReport = new PopupMenu.PopupMenuItem('Open Full Report'); - openReport.connect('activate', () => this._spawnTerminal([CODEBURN_BIN, 'report', '--period', this._period, '--provider', this._provider])); - this.menu.addMenuItem(openReport); } - _periodLabel() { - const p = PERIODS.find(x => x.id === this._period); - return `Period · ${p ? p.label : this._period}`; + _buildActivitySection() { + const section = new St.BoxLayout({vertical: true, style_class: 'codeburn-activity'}); + const title = new St.Label({text: 'Activity', style_class: 'codeburn-section-title'}); + section.add_child(title); + this._activityRows = new St.BoxLayout({vertical: true, style_class: 'codeburn-activity-rows'}); + section.add_child(this._activityRows); + this._root.add_child(section); } - _providerLabel() { - const p = PROVIDERS.find(x => x.id === this._provider); - return `Agent · ${p ? p.label : this._provider}`; + _buildFindingsSection() { + this._findingsBtn = new St.Button({style_class: 'codeburn-findings', visible: false}); + const box = new St.BoxLayout({style_class: 'codeburn-findings-inner'}); + this._findingsCount = new St.Label({text: '', style_class: 'codeburn-findings-count'}); + this._findingsSavings = new St.Label({text: '', style_class: 'codeburn-findings-savings'}); + box.add_child(this._findingsCount); + box.add_child(this._findingsSavings); + this._findingsBtn.set_child(box); + this._findingsBtn.connect('clicked', () => this._spawnTerminal([CODEBURN_BIN, 'optimize'])); + this._root.add_child(this._findingsBtn); } - _currencyLabel() { - return `Currency · ${this._currency.code}`; + _buildFooter() { + const footer = new St.BoxLayout({style_class: 'codeburn-footer'}); + + this._currencyBtn = new St.Button({ + label: `${this._currency.code} ⌄`, + style_class: 'codeburn-footer-btn codeburn-currency-btn', + can_focus: true, + }); + this._currencyBtn.connect('clicked', () => this._cycleCurrency()); + footer.add_child(this._currencyBtn); + + const refreshBtn = new St.Button({ + label: 'Refresh', + style_class: 'codeburn-footer-btn', + can_focus: true, + x_expand: true, + }); + refreshBtn.connect('clicked', () => this._refresh()); + footer.add_child(refreshBtn); + + const reportBtn = new St.Button({ + label: 'Open Full Report', + style_class: 'codeburn-footer-btn codeburn-footer-cta', + can_focus: true, + x_expand: true, + }); + reportBtn.connect('clicked', () => this._spawnTerminal([CODEBURN_BIN, 'report', '--period', this._period, '--provider', this._provider])); + footer.add_child(reportBtn); + + this._root.add_child(footer); + + this._updatedLabel = new St.Label({text: '', style_class: 'codeburn-updated'}); + this._root.add_child(this._updatedLabel); + } + + _cycleCurrency() { + const idx = CURRENCIES.findIndex(c => c.code === this._currency.code); + const next = CURRENCIES[(idx + 1) % CURRENCIES.length]; + this._setCurrency(next.code); } _loadCurrency() { @@ -235,7 +306,7 @@ class CodeburnIndicator extends PanelMenu.Button { } proc.wait_async(null, () => { this._currency = this._loadCurrency(); - this._currencySubmenu.label.set_text(this._currencyLabel()); + this._currencyBtn.set_label(`${this._currency.code} ⌄`); this._refresh(); }); } @@ -243,7 +314,6 @@ class CodeburnIndicator extends PanelMenu.Button { _refresh() { if (this._loading) return; this._loading = true; - this._headerItem.label.set_text(this._payload ? this._headerItem.label.get_text() : 'Loading…'); let proc; try { @@ -251,9 +321,9 @@ class CodeburnIndicator extends PanelMenu.Button { [CODEBURN_BIN, 'status', '--format', 'menubar-json', '--period', this._period, '--provider', this._provider], Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE, ); - } catch (e) { + } catch (_) { this._loading = false; - this._renderError('codeburn CLI not found on PATH. Install the npm package first.'); + this._renderError('codeburn CLI not found on PATH'); return; } @@ -269,9 +339,8 @@ class CodeburnIndicator extends PanelMenu.Button { this._renderError('codeburn returned no output'); return; } - const payload = JSON.parse(stdout); - this._payload = payload; - this._render(payload); + this._payload = JSON.parse(stdout); + this._render(this._payload); } catch (e) { this._renderError(`parse error: ${e.message}`); } @@ -281,115 +350,100 @@ class CodeburnIndicator extends PanelMenu.Button { _render(payload) { const current = payload?.current ?? {}; const cost = Number(current.cost ?? 0); - const formatted = formatCost(cost, this._currency); - this._label.set_text(formatted); + this._label.set_text(formatCost(cost, this._currency)); + this._heroLabel.set_text(current.label || ''); + this._heroAmount.set_text(formatCost(cost, this._currency)); - const label = current.label ?? ''; const calls = Number(current.calls ?? 0); const sessions = Number(current.sessions ?? 0); - const oneShot = current.oneShotRate; - const cacheHit = Number(current.cacheHitPercent ?? 0); + this._heroMeta.set_text(`${calls.toLocaleString()} calls ${sessions} sessions`); - this._headerItem.label.set_text(`${label} ${formatted}`); - const metaParts = [ - `${calls.toLocaleString()} calls`, - `${sessions} sessions`, - `${cacheHit.toFixed(0)}% cache`, - ]; - if (oneShot !== null && oneShot !== undefined) { - metaParts.push(`${Math.round(Number(oneShot) * 100)}% 1-shot`); - } - this._metaItem.label.set_text(metaParts.join(' ')); - - this._renderActivities(current.topActivities ?? []); - this._renderModels(current.topModels ?? []); - this._renderProviders(current.providers ?? {}); + this._renderActivity(Array.isArray(current.topActivities) ? current.topActivities : []); this._renderFindings(payload?.optimize ?? {}); const updated = payload?.generated ? formatTime(new Date(payload.generated)) : ''; - this._updatedItem.label.set_text(updated ? `Updated ${updated}` : ''); + this._updatedLabel.set_text(updated ? `Updated ${updated}` : ''); } - _renderActivities(activities) { - this._activitySection.removeAll(); + _renderActivity(activities) { + this._activityRows.destroy_all_children(); if (!activities.length) { - const empty = new PopupMenu.PopupMenuItem('No activity for this period', {reactive: false}); - empty.label.style_class = 'codeburn-empty'; - this._activitySection.addMenuItem(empty); + const empty = new St.Label({text: 'No activity for this period', style_class: 'codeburn-empty'}); + this._activityRows.add_child(empty); return; } - const title = new PopupMenu.PopupMenuItem('Activity', {reactive: false}); - title.label.style_class = 'codeburn-section-title'; - this._activitySection.addMenuItem(title); + const maxCost = activities.reduce((m, a) => Math.max(m, Number(a.cost) || 0), 0) || 1; for (const a of activities.slice(0, TOP_ACTIVITIES)) { - const oneShot = a.oneShotRate; - const tail = oneShot == null - ? `${a.turns} turns` - : `${a.turns} turns ${Math.round(Number(oneShot) * 100)}% 1-shot`; - const line = ` ${a.name.padEnd(14)} ${formatCost(a.cost, this._currency).padStart(8)} ${tail}`; - const item = new PopupMenu.PopupMenuItem(line, {reactive: false}); - item.label.style_class = 'codeburn-row'; - this._activitySection.addMenuItem(item); + this._activityRows.add_child(this._buildActivityRow(a, maxCost)); } } - _renderModels(models) { - this._modelsSection.removeAll(); - if (!models.length) return; - const title = new PopupMenu.PopupMenuItem('Models', {reactive: false}); - title.label.style_class = 'codeburn-section-title'; - this._modelsSection.addMenuItem(title); - for (const m of models.slice(0, TOP_MODELS)) { - const calls = Number(m.calls ?? 0).toLocaleString(); - const line = ` ${m.name.padEnd(18)} ${formatCost(m.cost, this._currency).padStart(8)} ${calls} calls`; - const item = new PopupMenu.PopupMenuItem(line, {reactive: false}); - item.label.style_class = 'codeburn-row'; - this._modelsSection.addMenuItem(item); - } - } + _buildActivityRow(activity, maxCost) { + const row = new St.BoxLayout({vertical: true, style_class: 'codeburn-activity-row'}); - _renderProviders(providers) { - this._providersSection.removeAll(); - const entries = Object.entries(providers).filter(([, cost]) => Number(cost) > 0); - if (entries.length <= 1) return; - entries.sort((a, b) => Number(b[1]) - Number(a[1])); - const title = new PopupMenu.PopupMenuItem('Providers', {reactive: false}); - title.label.style_class = 'codeburn-section-title'; - this._providersSection.addMenuItem(title); - for (const [name, cost] of entries.slice(0, TOP_PROVIDERS)) { - const line = ` ${capitalize(name).padEnd(14)} ${formatCost(Number(cost), this._currency).padStart(8)}`; - const item = new PopupMenu.PopupMenuItem(line, {reactive: false}); - item.label.style_class = 'codeburn-row'; - this._providersSection.addMenuItem(item); + const topLine = new St.BoxLayout({style_class: 'codeburn-activity-top'}); + const name = new St.Label({ + text: activity.name, + style_class: 'codeburn-activity-name', + x_expand: true, + }); + const cost = new St.Label({ + text: formatCost(activity.cost, this._currency), + style_class: 'codeburn-activity-cost', + }); + const turns = new St.Label({ + text: `${Number(activity.turns) || 0}t`, + style_class: 'codeburn-activity-turns', + }); + topLine.add_child(name); + topLine.add_child(cost); + topLine.add_child(turns); + if (activity.oneShotRate !== null && activity.oneShotRate !== undefined) { + const oneShot = new St.Label({ + text: `${Math.round(Number(activity.oneShotRate) * 100)}%`, + style_class: 'codeburn-activity-oneshot', + }); + topLine.add_child(oneShot); } + row.add_child(topLine); + + // Bar chart: track + filled portion. Width is proportional to this activity's + // share of the top cost. St widgets let us just set widths in pixels. + const track = new St.Widget({style_class: 'codeburn-bar-track', y_expand: false}); + const filledPct = Math.max(0.02, Math.min(1, Number(activity.cost) / maxCost)); + const fill = new St.Widget({ + style_class: 'codeburn-bar-fill', + width: Math.round(240 * filledPct), + }); + track.add_child(fill); + row.add_child(track); + + return row; } _renderFindings(optimize) { - this._findingsSection.removeAll(); const count = Number(optimize?.findingCount ?? 0); - if (count === 0) return; + if (count === 0) { + this._findingsBtn.hide(); + return; + } const savings = Number(optimize?.savingsUSD ?? 0); - const text = `⚠ ${count} optimize findings save ~${formatCost(savings, this._currency)}`; - const item = new PopupMenu.PopupMenuItem(text); - item.label.style_class = 'codeburn-findings'; - item.connect('activate', () => this._spawnTerminal([CODEBURN_BIN, 'optimize'])); - this._findingsSection.addMenuItem(item); + this._findingsCount.set_text(`⚠ ${count} optimize findings`); + this._findingsSavings.set_text(`save ~${formatCost(savings, this._currency)}`); + this._findingsBtn.show(); } _renderError(message) { this._label.set_text('!'); - this._headerItem.label.set_text(message); - this._metaItem.label.set_text(''); - this._activitySection.removeAll(); - this._modelsSection.removeAll(); - this._providersSection.removeAll(); - this._findingsSection.removeAll(); + this._heroLabel.set_text(message); + this._heroAmount.set_text(''); + this._heroMeta.set_text(''); + this._activityRows.destroy_all_children(); + this._findingsBtn.hide(); } _spawnTerminal(argv) { - // Quote arguments into a single command string for bash -lc. argv here only ever - // contains static identifiers from our own code so plain join is safe. const command = `${argv.join(' ')}; echo; read -n 1 -s -r -p 'Press any key to close...'`; try { Gio.Subprocess.new( @@ -399,6 +453,7 @@ class CodeburnIndicator extends PanelMenu.Button { } catch (e) { log(`codeburn: terminal spawn error: ${e.message}`); } + this.menu.close(); } _applyThemeClass() { @@ -442,11 +497,6 @@ function formatTime(date) { return date.toLocaleDateString(); } -function capitalize(s) { - if (!s) return s; - return s.charAt(0).toUpperCase() + s.slice(1); -} - export default class CodeburnExtension extends Extension { enable() { this._indicator = new CodeburnIndicator(); diff --git a/extensions/gnome-shell/codeburn@agentseal.org/stylesheet.css b/extensions/gnome-shell/codeburn@agentseal.org/stylesheet.css index fa4ebc0..a0c1d56 100644 --- a/extensions/gnome-shell/codeburn@agentseal.org/stylesheet.css +++ b/extensions/gnome-shell/codeburn@agentseal.org/stylesheet.css @@ -1,76 +1,271 @@ /* * CodeBurn GNOME Shell extension styles. * - * Inherits colors from the active GNOME shell theme so the popup looks native on - * both light and dark system themes. Only the brand accent (orange) is hardcoded; - * every other color is left to the theme. Typography and spacing are tight to - * keep the popup compact even when every section is populated. + * Designed to match the macOS popover pixel for pixel within what St widgets + * can paint: branded header, horizontal tab rows with pill active states, hero + * typography, inline bar-chart activity rows, and a pill-styled footer. + * + * Colors inherit the shell theme by default; brand orange (#ff8c42 to #c9521d) + * is the only hardcoded palette so the design survives both Light and Dark + * system themes. */ +/* ---- panel button ---- */ .codeburn-panel { spacing: 4px; } - .codeburn-flame { font-size: 14px; } - .codeburn-label { font-weight: 500; padding-left: 2px; padding-right: 2px; } -.codeburn-header { - font-weight: 600; +/* ---- popup host ---- */ +.codeburn-menu { + padding: 0; +} +.codeburn-host { + padding: 0; + margin: 0; + background: transparent; + border: none; +} +.codeburn-host:hover, +.codeburn-host:focus, +.codeburn-host:active, +.codeburn-host:selected { + background: transparent; +} +.codeburn-root { + width: 340px; + padding: 0; + spacing: 0; +} + +/* ---- brand header ---- */ +.codeburn-brand-header { + padding: 14px 16px 10px 16px; + spacing: 2px; +} +.codeburn-brand-row { + spacing: 0; +} +.codeburn-brand-primary { + font-weight: 700; + font-size: 18px; +} +.codeburn-brand-accent { + font-weight: 700; + font-size: 18px; color: #ff8c42; - font-size: 13px; +} +.codeburn-brand-subhead { + font-size: 10.5px; + opacity: 0.55; + letter-spacing: 0.3px; } -.codeburn-meta { +/* ---- tab rows ---- */ +.codeburn-tab-row { + padding: 4px 10px 8px 10px; + spacing: 4px; +} +.codeburn-period-row { + padding-top: 0; + padding-bottom: 10px; +} +.codeburn-tab, +.codeburn-period { + padding: 5px 6px; + border-radius: 6px; font-size: 11px; - opacity: 0.75; + font-weight: 500; + background: transparent; + border: none; + opacity: 0.7; + transition-duration: 80ms; +} +.codeburn-tab:hover, +.codeburn-period:hover { + background: rgba(255, 140, 66, 0.08); + opacity: 1; +} +.codeburn-tab-active, +.codeburn-period-active { + background: rgba(255, 140, 66, 0.18); + color: #ff8c42; + opacity: 1; + font-weight: 600; } +/* ---- hero ---- */ +.codeburn-hero { + padding: 4px 16px 10px 16px; + spacing: 2px; +} +.codeburn-hero-top { + spacing: 6px; +} +.codeburn-hero-dot { + width: 6px; + height: 6px; + border-radius: 3px; + background-color: #ff8c42; + margin-top: 7px; +} +.codeburn-hero-label { + font-size: 11px; + opacity: 0.65; + font-weight: 500; +} +.codeburn-hero-amount { + font-size: 28px; + font-weight: 700; + color: #ffd700; +} +.codeburn-hero-meta { + font-size: 11px; + opacity: 0.6; +} + +/* ---- activity section ---- */ +.codeburn-activity { + padding: 6px 16px 10px 16px; + spacing: 6px; +} .codeburn-section-title { font-weight: 600; font-size: 11px; opacity: 0.6; - padding-top: 4px; padding-bottom: 2px; } - -.codeburn-row { - font-family: monospace; - font-size: 11.5px; - padding-left: 4px; - padding-right: 4px; +.codeburn-activity-rows { + spacing: 8px; +} +.codeburn-activity-row { + spacing: 3px; +} +.codeburn-activity-top { + spacing: 6px; +} +.codeburn-activity-name { + font-size: 11.5px; + font-weight: 500; +} +.codeburn-activity-cost { + font-size: 11.5px; + font-family: monospace; + font-weight: 600; + color: #ffd700; +} +.codeburn-activity-turns { + font-size: 10.5px; + font-family: monospace; + opacity: 0.6; + min-width: 28px; + text-align: right; +} +.codeburn-activity-oneshot { + font-size: 10.5px; + font-family: monospace; + opacity: 0.8; + color: #5bf58c; + min-width: 36px; + text-align: right; +} +.codeburn-bar-track { + height: 4px; + border-radius: 2px; + background-color: rgba(255, 255, 255, 0.08); + width: 240px; +} +.codeburn-bar-fill { + height: 4px; + border-radius: 2px; + background: linear-gradient(to right, #ff8c42, #c9521d); } - .codeburn-empty { font-style: italic; opacity: 0.55; - padding-left: 8px; + padding: 6px 0; } +/* ---- findings CTA ---- */ .codeburn-findings { + margin: 2px 16px 10px 16px; + padding: 9px 11px; + border-radius: 8px; + background: rgba(255, 140, 66, 0.12); + border: none; + transition-duration: 120ms; +} +.codeburn-findings:hover { + background: rgba(255, 140, 66, 0.2); +} +.codeburn-findings-inner { + spacing: 0; +} +.codeburn-findings-count { + font-size: 11.5px; + font-weight: 600; color: #ff8c42; + x-expand: true; +} +.codeburn-findings-savings { + font-size: 11.5px; font-weight: 500; + color: #ff8c42; + opacity: 0.8; } +/* ---- footer ---- */ +.codeburn-footer { + padding: 10px 12px; + spacing: 6px; + border-top: 1px solid rgba(255, 255, 255, 0.06); +} +.codeburn-footer-btn { + padding: 6px 10px; + border-radius: 6px; + background: rgba(255, 255, 255, 0.05); + border: none; + font-size: 11px; + font-weight: 500; + transition-duration: 80ms; +} +.codeburn-footer-btn:hover { + background: rgba(255, 255, 255, 0.1); +} +.codeburn-currency-btn { + font-family: monospace; + min-width: 62px; +} +.codeburn-footer-cta { + background: #c9521d; + color: #ffffff; +} +.codeburn-footer-cta:hover { + background: #ff8c42; +} .codeburn-updated { font-size: 10px; - opacity: 0.5; - padding-left: 4px; + opacity: 0.45; + padding: 0 16px 10px 16px; } -/* Optional: theme-specific tweaks if the inherited colors look off. Both classes - * are set dynamically by extension.js so we can override per-theme without - * fighting the shell's default popup palette. */ -.codeburn-dark .codeburn-row { - /* Monospace can render thin on dark themes; bump weight slightly. */ +/* ---- dark / light theme hooks ---- */ +.codeburn-light .codeburn-bar-track { + background-color: rgba(0, 0, 0, 0.08); } - -.codeburn-light .codeburn-row { - /* Nothing yet; placeholder for light-theme specific adjustments. */ +.codeburn-light .codeburn-footer-btn { + background: rgba(0, 0, 0, 0.04); +} +.codeburn-light .codeburn-footer-btn:hover { + background: rgba(0, 0, 0, 0.08); +} +.codeburn-light .codeburn-footer { + border-top-color: rgba(0, 0, 0, 0.08); }