diff --git a/package.json b/package.json index 5a7aff3..c380afa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ration", - "version": "0.1.0", + "version": "0.1.1", "private": true, "description": "Ration — AI Quota Tracker. A browser extension showing remaining quota across your AI subscriptions in one glance.", "type": "module", diff --git a/public/manifest.json b/public/manifest.json index fcdca68..e129208 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Ration — AI Quota Tracker", - "version": "0.1.0", + "version": "0.1.1", "description": "One glance at remaining quota across your AI subscriptions. No accounts, no telemetry, no credential access.", "minimum_chrome_version": "120", "permissions": ["storage", "alarms"], @@ -16,7 +16,7 @@ }, "action": { "default_popup": "popup.html", - "default_title": "Ration", + "default_title": "Ration — AI Quota Tracker", "default_icon": { "16": "icons/icon16.png", "32": "icons/icon32.png" diff --git a/public/popup.css b/public/popup.css index a9f099e..a0570ef 100644 --- a/public/popup.css +++ b/public/popup.css @@ -35,7 +35,7 @@ } body { - width: 340px; + width: 356px; margin: 0; background: var(--bg); color: var(--fg); @@ -44,9 +44,9 @@ body { #header { display: flex; - align-items: baseline; + align-items: center; justify-content: space-between; - padding: 10px 14px 8px; + padding: 8px 14px; border-bottom: 1px solid var(--border); } @@ -54,6 +54,19 @@ body { margin: 0; font-size: 14px; font-weight: 600; + white-space: nowrap; +} + +#subtitle { + color: var(--fg-muted); + font-size: 11px; + font-weight: 400; +} + +#header-right { + display: flex; + align-items: center; + gap: 6px; } #age { @@ -61,6 +74,34 @@ body { font-size: 11px; } +#refresh { + border: 0; + padding: 2px 4px; + background: none; + color: var(--fg-muted); + font: inherit; + font-size: 14px; + line-height: 1; + cursor: pointer; + border-radius: 4px; +} + +#refresh:hover { + color: var(--accent); + background: var(--card); +} + +#refresh.spinning { + animation: spin 0.8s linear infinite; + pointer-events: none; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + #cards { padding: 6px 10px; } @@ -123,12 +164,20 @@ body { .lane { display: grid; - grid-template-columns: minmax(72px, auto) 1fr auto; + grid-template-columns: minmax(72px, auto) 1fr auto auto; align-items: center; gap: 8px; margin: 6px 0 0 20px; } +.lane-pct { + color: var(--fg-muted); + font-size: 11px; + font-variant-numeric: tabular-nums; + min-width: 30px; + text-align: right; +} + .lane-label { color: var(--fg-muted); overflow: hidden; @@ -208,6 +257,15 @@ body { text-decoration: underline; } +#footer-right { + display: flex; + gap: 10px; +} + +#issues { + color: var(--fg-muted); +} + #repo { color: var(--fg-muted); text-decoration: none; diff --git a/public/popup.html b/public/popup.html index 9c17184..79953bb 100644 --- a/public/popup.html +++ b/public/popup.html @@ -7,21 +7,34 @@
diff --git a/src/popup/main.ts b/src/popup/main.ts index f071516..1afdf58 100644 --- a/src/popup/main.ts +++ b/src/popup/main.ts @@ -15,6 +15,8 @@ import type { Msg, ProviderAdapter, ProviderSnapshot, Settings } from '../types' const cardsEl = document.getElementById('cards') as HTMLElement; const ageEl = document.getElementById('age') as HTMLElement; const wipeEl = document.getElementById('wipe') as HTMLButtonElement; +const refreshEl = document.getElementById('refresh') as HTMLButtonElement; +const repoEl = document.getElementById('repo') as HTMLAnchorElement; const send = (msg: Msg): Promise => chrome.runtime.sendMessage(msg); @@ -69,8 +71,13 @@ function renderOkCard(adapter: ProviderAdapter, snap: ProviderSnapshot, now: Dat bar.title = `${Math.round(lane.headroomPct)}% left`; bar.append(fill); row.append(bar); + row.append(el('span', 'lane-pct', `${Math.round(lane.headroomPct)}%`)); const countdown = formatCountdown(lane.resetsAt, now); - row.append(el('span', 'lane-reset', countdown ? `resets ${countdown}` : '')); + const reset = el('span', 'lane-reset', countdown ? `resets ${countdown}` : ''); + if (lane.resetsAt && countdown) { + reset.title = `Resets at ${new Date(lane.resetsAt).toLocaleString()}`; + } + row.append(reset); card.append(row); } return card; @@ -192,6 +199,17 @@ wipeEl.addEventListener('click', () => { void send({ type: 'wipeAll' }).then(render); }); +refreshEl.addEventListener('click', () => { + // The worker still applies its politeness gates (60s/provider floor, + // backoff) — the button just asks now instead of waiting for the alarm. + refreshEl.classList.add('spinning'); + void send({ type: 'refresh' }) + .then(render) + .finally(() => refreshEl.classList.remove('spinning')); +}); + +repoEl.textContent = `v${chrome.runtime.getManifest().version}`; + chrome.storage.onChanged.addListener((_changes, area) => { if (area === 'local') void render(); });