+
{#each section.items as item (item.unit.id)}
+ *
+ * How it works:
+ * 1. After each render (via tick()), for every .result-tile in the container:
+ * - Measure available inner width (clientWidth − horizontal padding)
+ * - Measure rendered text width of .result-value and .result-label using
+ * an off-screen span with matching font properties
+ * - Compute scale = availableWidth / max(valueWidth, labelWidth)
+ * 2. Take minimum scale across all tiles (tightest tile wins)
+ * 3. Clamp: min 0.6rem (floor), max = base font size (never inflate)
+ * 4. Set --card-font-size on the container
+ * 5. For any element still overflowing at the floor, add ellipsis + title
+ *
+ * ResizeObserver on the container re-runs measurement, debounced 50 ms.
+ */
+
+import { tick } from 'svelte';
+import { browser } from '$app/environment';
+
+export interface UniformFontSizeParams {
+ /** Pass the current results array; any change triggers re-measurement. */
+ results: unknown;
+}
+
+// Base font size we scale from (1rem). Expressed in px at measure time.
+const BASE_REM = 1; // rem
+const FLOOR_REM = 0.6; // rem
+const DEBOUNCE_MS = 50;
+
+/** Return the computed horizontal padding (left + right) of an element in px. */
+function horizontalPadding(el: HTMLElement): number {
+ const style = window.getComputedStyle(el);
+ return parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
+}
+
+/** Measure the rendered pixel width of `text` at `fontSize`px using a hidden span. */
+function measureText(
+ text: string,
+ fontSize: number,
+ fontWeight: string,
+ fontFamily: string,
+ span: HTMLSpanElement
+): number {
+ span.style.fontSize = `${fontSize}px`;
+ span.style.fontWeight = fontWeight;
+ span.style.fontFamily = fontFamily;
+ span.textContent = text;
+ return span.getBoundingClientRect().width;
+}
+
+/** Get root font size in px (for rem→px conversion). */
+function getRootFontSizePx(): number {
+ return parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
+}
+
+export function uniformFontSize(
+ node: HTMLElement,
+ params: UniformFontSizeParams
+): { update: (p: UniformFontSizeParams) => void; destroy: () => void } {
+ if (!browser) {
+ return { update: () => {}, destroy: () => {} };
+ }
+
+ // Off-screen measurement span — created once, reused, destroyed on action teardown.
+ const measureSpan = document.createElement('span');
+ measureSpan.style.cssText =
+ 'position:fixed;top:-9999px;left:-9999px;visibility:hidden;white-space:nowrap;pointer-events:none;';
+ document.body.appendChild(measureSpan);
+
+ let debounceTimer: ReturnType | null = null;
+ let observer: ResizeObserver | null = null;
+
+ async function measure() {
+ // Wait for Svelte to flush DOM updates.
+ await tick();
+
+ const tiles = Array.from(node.querySelectorAll('.result-tile'));
+ if (tiles.length === 0) return;
+
+ const rootPx = getRootFontSizePx();
+ const basePx = BASE_REM * rootPx;
+ const floorPx = FLOOR_REM * rootPx;
+
+ let minScale = 1; // never inflate above base
+
+ for (const tile of tiles) {
+ const availableWidth = tile.clientWidth - horizontalPadding(tile);
+ if (availableWidth <= 0) continue;
+
+ const valueEl = tile.querySelector('.result-value');
+ const labelEl = tile.querySelector('.result-label');
+ if (!valueEl || !labelEl) continue;
+
+ // Get font properties from the live elements (inherits CSS vars etc.)
+ const valueStyle = window.getComputedStyle(valueEl);
+ const labelStyle = window.getComputedStyle(labelEl);
+
+ const valueText = valueEl.textContent ?? '';
+ const labelText = labelEl.textContent ?? '';
+
+ const valueWidth = measureText(
+ valueText,
+ basePx,
+ valueStyle.fontWeight,
+ valueStyle.fontFamily,
+ measureSpan
+ );
+ const labelWidth = measureText(
+ labelText,
+ basePx,
+ labelStyle.fontWeight,
+ labelStyle.fontFamily,
+ measureSpan
+ );
+
+ const maxTextWidth = Math.max(valueWidth, labelWidth);
+ if (maxTextWidth <= 0) continue;
+
+ const scale = availableWidth / maxTextWidth;
+ if (scale < minScale) minScale = scale;
+ }
+
+ // Compute final font size in rem, clamped to floor.
+ const computedPx = Math.max(floorPx, basePx * minScale);
+ const computedRem = computedPx / rootPx;
+
+ node.style.setProperty('--card-font-size', `${computedRem.toFixed(4)}rem`);
+
+ // Apply truncation on elements that still overflow at the floor.
+ if (computedPx <= floorPx + 0.01) {
+ for (const tile of tiles) {
+ for (const selector of ['.result-value', '.result-label']) {
+ const el = tile.querySelector(selector);
+ if (!el) continue;
+ if (el.scrollWidth > el.clientWidth) {
+ el.style.overflow = 'hidden';
+ el.style.whiteSpace = 'nowrap';
+ el.style.textOverflow = 'ellipsis';
+ el.title = el.textContent ?? '';
+ }
+ }
+ }
+ }
+ }
+
+ function scheduleMeasure() {
+ if (debounceTimer !== null) clearTimeout(debounceTimer);
+ debounceTimer = setTimeout(() => {
+ debounceTimer = null;
+ measure();
+ }, DEBOUNCE_MS);
+ }
+
+ // Initial measurement (after first render).
+ measure();
+
+ // ResizeObserver — single observer on the grid container.
+ observer = new ResizeObserver(scheduleMeasure);
+ observer.observe(node);
+
+ return {
+ update(_newParams: UniformFontSizeParams) {
+ // Called by Svelte whenever the `results` prop changes — re-measure.
+ measure();
+ },
+ destroy() {
+ observer?.disconnect();
+ observer = null;
+ if (debounceTimer !== null) {
+ clearTimeout(debounceTimer);
+ debounceTimer = null;
+ }
+ measureSpan.remove();
+ }
+ };
+}