|
53 | 53 | } |
54 | 54 | .tile .sub { color: var(--ink-muted); font-size: 12px; font-size: clamp(11px, 5.5cqi, 13px); } |
55 | 55 | .tile svg { display: block; width: 100%; height: clamp(34px, 20cqi, 56px); } |
| 56 | + /* Plain label+number tiles: let the number own the leftover space and sit |
| 57 | + centered in it (fitValues() grows the font to fill), so it never strands at |
| 58 | + the bottom. Tiles with a sub-line or sparkline keep their stacked layout. */ |
| 59 | + .tile:not(:has(.sub)):not(:has(svg)) .value { |
| 60 | + flex: 1 1 auto; min-height: 0; display: flex; align-items: center; |
| 61 | + } |
56 | 62 |
|
57 | 63 | .card { background: var(--surface-1); border: 1px solid var(--border); border-radius: 10px; overflow-x: auto; } |
58 | 64 | .pager { display: flex; align-items: center; padding: 8px 12px; border-top: 1px solid var(--grid); |
@@ -246,19 +252,37 @@ <h3>Response <button class="copy" data-copy="d-resp">copy</button></h3> |
246 | 252 |
|
247 | 253 | function esc(s) { return String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); } |
248 | 254 |
|
249 | | -// Size each stat number as big as fits its tile: the CSS aims high, this shrinks |
250 | | -// only the values whose text is wider than the tile (long token counts, dollar |
251 | | -// amounts) so short numbers stay huge and nothing clips. |
| 255 | +// Size each stat number to fill its tile. Plain label+number tiles grow the |
| 256 | +// number to fill the leftover width AND height (so it fills the gap on tall |
| 257 | +// tiles); tiles that carry sub-text or a sparkline only shrink long numbers to |
| 258 | +// fit the width, keeping their existing layout. |
252 | 259 | function fitValues() { |
| 260 | + const range = document.createRange(); |
253 | 261 | for (const el of document.querySelectorAll(".tile .value")) { |
254 | | - el.style.fontSize = ""; // reset to the CSS-driven size |
255 | | - const avail = el.clientWidth; |
256 | | - if (!avail) continue; |
257 | | - const w = el.scrollWidth; // full text width (white-space:nowrap) |
258 | | - if (w > avail) { |
259 | | - const base = parseFloat(getComputedStyle(el).fontSize); |
260 | | - el.style.fontSize = Math.max(16, Math.floor(base * (avail / w) * 0.97)) + "px"; // 0.97 = sub-pixel safety |
| 262 | + el.style.fontSize = ""; // reset to the CSS-driven size |
| 263 | + const availW = el.clientWidth; |
| 264 | + if (!availW || !el.firstChild) continue; |
| 265 | + // Measure the actual text box — the value is a stretched flex item, so its own |
| 266 | + // scrollWidth is the tile width, not the glyphs'. A Range gives the real size. |
| 267 | + range.selectNodeContents(el); |
| 268 | + const r = range.getBoundingClientRect(); |
| 269 | + if (!r.width || !r.height) continue; |
| 270 | + const base = parseFloat(getComputedStyle(el).fontSize); |
| 271 | + const tile = el.closest(".tile"); |
| 272 | + const fillH = !tile.querySelector(".sub") && !tile.querySelector("svg"); |
| 273 | + let ratio = availW / r.width; // fit the width |
| 274 | + if (fillH) { |
| 275 | + // Leftover vertical space = tile content height minus the other rows (label); |
| 276 | + // let the number grow to fill it so it doesn't strand at the bottom. |
| 277 | + const cs = getComputedStyle(tile); |
| 278 | + let used = parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom); |
| 279 | + for (const c of tile.children) if (c !== el) used += c.offsetHeight; |
| 280 | + const availH = tile.clientHeight - used; |
| 281 | + if (availH > 0) ratio = Math.min(ratio, availH / r.height); |
| 282 | + } else { |
| 283 | + ratio = Math.min(1, ratio); // sub/sparkline tiles: shrink only, never grow |
261 | 284 | } |
| 285 | + el.style.fontSize = Math.max(16, Math.floor(base * ratio * 0.92)) + "px"; // 0.92 = breathing room |
262 | 286 | } |
263 | 287 | } |
264 | 288 | let fitScheduled = false; |
|
0 commit comments