diff --git a/CHANGELOG.md b/CHANGELOG.md index f4a5058..b413ba2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,27 @@ All notable changes to TokenFlow are recorded here. Versions follow [semantic versioning](https://semver.org/spec/v2.0.0.html). +## 1.3.4 — 2026-09-07 + +Interface fixes only; nothing about how TokenFlow reads or prices usage changes. Every one of +the 21 views was rendered in the three themes at 1440, 1000, 700 and 380 pixels wide and probed +for text that leaves its card or a page that scrolls sideways. None remains. + +### Fixed + +- **The Data health grade fits its card.** "Excellent" was set at the hero figure size, which is + tuned for numbers, and at that size the word is wider than one card of the grid, so it read + "Excelle" with the rest cut off. A hero value is now sized to the card it sits in, between a + readable floor and the hero size, and the grade card takes a double-width slot so the word + shows at full size on anything wider than a phone. +- **The Cost and Peaks heroes no longer clip on narrower windows.** The same rule catches + "$364.52" and "53.3M", which ran 5 to 57 pixels past the card edge: in the Editorial theme + even at 1440 pixels, and in every theme below 1000. +- **The Overview no longer scrolls sideways on a phone.** Its two-up chart grid asked for columns + at least 420 pixels wide, so on a 380-pixel screen the page was 64 pixels wider than the + viewport. The minimum now yields to the width available. +- **Branch names in the Live receipts list wrap** instead of spilling out of their column. + ## 1.3.3 — 2026-09-07 No change to how TokenFlow reads or prices anything. This release exists so the GitHub Action diff --git a/action/README.md b/action/README.md index 33c95f4..fe14fd6 100644 --- a/action/README.md +++ b/action/README.md @@ -34,7 +34,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 # notes live outside the default shallow fetch - - uses: vimoxshah/tokenflow@v1.3.3 + - uses: vimoxshah/tokenflow@v1.3.4 # or, dogfooding this repo's own checkout: uses: ./ with: token: ${{ secrets.GITHUB_TOKEN }} # default; only pass to override diff --git a/docs/receipts-on-github.md b/docs/receipts-on-github.md index 3af85b0..1cd0855 100644 --- a/docs/receipts-on-github.md +++ b/docs/receipts-on-github.md @@ -44,7 +44,7 @@ network call. - uses: actions/checkout@v4 with: fetch-depth: 0 # notes live outside the default shallow fetch - - uses: vimoxshah/tokenflow@v1.3.3 + - uses: vimoxshah/tokenflow@v1.3.4 with: max-usd: '50' max-usd-per-100-lines: '5' diff --git a/package.json b/package.json index 79cdf65..44fd017 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vimoxshah/tokenflow", - "version": "1.3.3", + "version": "1.3.4", "description": "Local-first, provider-agnostic analytics for the AI tokens you actually spend. Zero dependencies, nothing leaves your machine.", "type": "module", "license": "MIT", diff --git a/src/ui/app.js b/src/ui/app.js index e242371..e9f6fc5 100644 --- a/src/ui/app.js +++ b/src/ui/app.js @@ -1172,13 +1172,13 @@ function btn(label, onClick, cls = '', id = null) { function kpi(label, value, sub, opt = {}) { const c = el('div', { class: 'card' + (opt.hero ? ' hero-card' : '') }); - const k = el('div', { class: 'kpi' + (opt.onClick ? ' clickable' : '') }); + const k = el('div', { class: 'kpi' + (opt.hero ? ' kpi-hero' : '') + (opt.onClick ? ' clickable' : '') }); k.appendChild(el('span', { class: 'k-label' }, [ document.createTextNode(label), opt.badge ? el('span', { class: 'badge ' + (opt.badgeKind || ''), text: opt.badge, title: opt.badgeTitle || '' }) : null, ])); k.appendChild(el('span', { - class: 'k-value' + (opt.hero ? ' hero' : '') + (opt.str ? ' str' : ''), + class: 'k-value' + (opt.hero ? ' hero' : '') + (opt.str ? ' str' : '') + (opt.word ? ' word' : ''), text: value, })); if (sub) k.appendChild(el('span', { class: 'k-sub' }, [typeof sub === 'string' ? document.createTextNode(sub) : sub])); @@ -1240,7 +1240,7 @@ function viewOverview() { root.appendChild(compositionCard()); root.appendChild(providerDailyCard()); - const two = el('div', { class: 'grid', style: 'grid-template-columns:repeat(auto-fit,minmax(420px,1fr))' }); + const two = el('div', { class: 'grid', style: 'grid-template-columns:repeat(auto-fit,minmax(min(420px,100%),1fr))' }); two.appendChild(shareCard('provider', 'Provider distribution', v.dimensions.providers, S.colors.provider, 'provider')); two.appendChild(shareCard('model', 'Model distribution', v.dimensions.models, S.colors.model, 'model')); two.appendChild(interfaceCard()); @@ -2593,7 +2593,9 @@ function viewHealth() { root.appendChild(sectionTitle('Data health')); const cards = el('div', { class: 'cards' }); - cards.appendChild(kpi('Data health', h.grade, `${pct(h.missingTokenFieldRate)} of token fields not reported`, { hero: true })); + const gradeCard = kpi('Data health', h.grade, `${pct(h.missingTokenFieldRate)} of token fields not reported`, { hero: true, word: true }); + gradeCard.classList.add('wide'); + cards.appendChild(gradeCard); cards.appendChild(kpi('Records', int(h.records), `${int(h.sourceFiles)} source files tracked`)); cards.appendChild(kpi('Date coverage', h.coverage.from ? `${shortDate(h.coverage.from)} → ${shortDate(h.coverage.to)}` : '—', `${h.coverage.from ? daysBetween(h.coverage.from, h.coverage.to) + 1 : 0} days`)); cards.appendChild(kpi('Providers', int(h.providers), `${int(h.models)} models · ${int(h.clients)} clients`)); diff --git a/src/ui/styles.css b/src/ui/styles.css index b1e6488..9b2b677 100644 --- a/src/ui/styles.css +++ b/src/ui/styles.css @@ -383,8 +383,23 @@ label.fld { display: flex; flex-direction: column; gap: 4px; font-size: 11px; co letter-spacing: var(--tracking-h); line-height: var(--lh-figure); font-family: var(--figure); font-variant-numeric: tabular-nums; } -/* The one hero number on a view: bigger, and tighter still as type grows. */ -.kpi .k-value.hero { font-size: calc(var(--fs-hero) * var(--fig-scale)); letter-spacing: calc(var(--tracking-h) - .01em); line-height: var(--lh-hero); } +/* The one hero number on a view: bigger, and tighter still as type grows. + A hero cannot wrap, and at the full size a 7-character figure ("$364.52") + or the word "Excellent" ran past the edge of a narrow card. So the hero is + sized to the card it sits in, between a readable floor and the hero size: + 19% of the column's width fits the longest figure the formatters emit in the + widest of the three figure fonts, 17% fits the longest grade. The inner kpi + column is the container, not the card, so grid sizing is untouched. A wide + card reaches the hero size; a narrow one shrinks instead of clipping. */ +.kpi.kpi-hero { container-type: inline-size; } +.kpi .k-value.hero { + --hero-cap: calc(var(--fs-hero) * var(--fig-scale)); --hero-k: 19cqi; + font-size: var(--hero-cap); letter-spacing: calc(var(--tracking-h) - .01em); line-height: var(--lh-hero); +} +.kpi .k-value.hero.word { --hero-k: 17cqi; } +@supports (font-size: 1cqi) { + .kpi .k-value.hero, .kpi .k-value.hero.word { font-size: clamp(calc(20px * var(--fig-scale)), var(--hero-k), var(--hero-cap)); } +} /* A long identifier is not a hero figure: keep it readable and let it wrap. */ .kpi .k-value.str { font-size: 17px; font-weight: 600; line-height: 1.25; overflow-wrap: anywhere; } .card.wide { grid-column: span 2; } @@ -669,9 +684,14 @@ dialog h2 { margin: 0; font-size: 15px; } @media (max-width: 860px) { .kpi .k-value { font-size: 21px; } - .kpi .k-value.hero { font-size: 34px; } + .kpi .k-value.hero { --hero-cap: 34px; } .bar-row { grid-template-columns: 96px 1fr 62px; } } +/* Below this the card grid has one column, and a card spanning two would open + an implicit column off the right edge of the page. */ +@media (max-width: 480px) { + .card.wide { grid-column: auto; } +} @media print { header.top { position: static; } diff --git a/src/ui/styles/live.css b/src/ui/styles/live.css index 06a4ae4..1856496 100644 --- a/src/ui/styles/live.css +++ b/src/ui/styles/live.css @@ -68,7 +68,7 @@ } .live-receipt:first-child { border-top: 0; } .live-receipt-repo { font-weight: 600; overflow-wrap: anywhere; } -.live-receipt-where p, .live-receipt-where .hint { margin: 0; } +.live-receipt-where p, .live-receipt-where .hint { margin: 0; overflow-wrap: anywhere; } .live-receipt-cost { font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; } .live-receipt-n { font-variant-numeric: tabular-nums; text-align: right; margin: 0; } .live-receipt-total { border-top: 1px solid var(--border-strong); font-weight: 600; }