From 1db6c2c89fe70d7b45e00fd89c57c1c350a01745 Mon Sep 17 00:00:00 2001 From: Waters Church Date: Tue, 15 Sep 2026 09:05:56 -0400 Subject: [PATCH] Timers: "to 9:30a" under a countdown-to-time, smaller status lines, titles that fit A ProPresenter count_down_to_time timer's detail line reads "to 9:30a" (or "to 21:30" on a 24-hour clock) instead of a sentence. The status and detail lines under the digits are drawn at 0.8 of the title's size, and fit() budgets them at that share. A title that would be cut off shrinks to fit its card (measured off-screen, down to 8 px) instead of ending in an ellipsis; a changed label re-fits. Co-Authored-By: Claude Fable 5.1 --- modules/propresenter-timers/client.js | 30 +++++++++++++++++++++++++-- modules/propresenter-timers/server.js | 6 +++--- modules/propresenter-timers/style.css | 2 +- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/modules/propresenter-timers/client.js b/modules/propresenter-timers/client.js index 2be54b9..111c567 100644 --- a/modules/propresenter-timers/client.js +++ b/modules/propresenter-timers/client.js @@ -428,7 +428,11 @@ export default function create({ root, moduleApi }) { } else if (!isLtc && badge) { badge.remove(); } - setText(card.labelEl, item.missing ? (lastSoloLabel || 'Timer') : item.timer.label); + const label = item.missing ? (lastSoloLabel || 'Timer') : item.timer.label; + if (card.labelEl.textContent !== label) { + setText(card.labelEl, label); + needFit = true; // the title's size follows its length + } } function updateMissingCard(card) { @@ -772,6 +776,27 @@ export default function create({ root, moduleApi }) { /* ── sizing: the column count that gives the biggest digits ─────── */ + /** Status and detail lines are drawn at this share of the label's size (style.css agrees). */ + const LINE_SCALE = 0.8; + + /* The title fits its card: measured off-screen, it shrinks (down to a + floor) before the card would cut it off. Uppercase and letter-spaced + like the CSS draws it. */ + const measurer = document.createElement('canvas').getContext('2d'); + let labelFamily = ''; + function labelWidth(text, px) { + if (!labelFamily) labelFamily = getComputedStyle(root).fontFamily || 'sans-serif'; + measurer.font = `700 ${px}px ${labelFamily}`; + const t = String(text || '').toUpperCase(); + return measurer.measureText(t).width + 0.08 * px * Math.max(0, t.length - 1); + } + function fitLabel(card, meta, avail) { + const room = Math.max(24, avail - (card.nameEl.querySelector('.tm-ltc-badge') ? 34 : 0)); + const natural = labelWidth(card.labelEl.textContent, meta); + const px = natural > room ? Math.max(8, Math.floor((meta * room) / natural)) : meta; + card.labelEl.style.fontSize = px === meta ? '' : `${px}px`; + } + function fit() { const list = [...cards.values()]; const n = list.length; @@ -803,7 +828,7 @@ export default function create({ root, moduleApi }) { const ch = (H - PAD * 2 - headings * HEAD_H - GAP * (rows - 1) - (headings ? (headings - 1) * GAP : 0)) / rows - CARD_PAD; if (cw <= 0 || ch <= 0) continue; const meta = Math.max(9, Math.min(22, Math.floor(ch * 0.16))); - const fullBudget = ch - (1 + metaLines) * 1.3 * meta; + const fullBudget = ch - 1.3 * meta - metaLines * 1.3 * meta * LINE_SCALE; const compactBudget = ch - 1.3 * meta; let full = Infinity; let compact = Infinity; @@ -831,6 +856,7 @@ export default function create({ root, moduleApi }) { for (const c of list) { const px = Math.max(11, Math.floor(Math.min((best.cw * 1.55) / c.chars, best.budget))); c.timeEl.style.fontSize = px + 'px'; + fitLabel(c, best.meta, best.cw); } } diff --git a/modules/propresenter-timers/server.js b/modules/propresenter-timers/server.js index 7ec8414..741f456 100644 --- a/modules/propresenter-timers/server.js +++ b/modules/propresenter-timers/server.js @@ -171,14 +171,14 @@ function fmtMs(ms) { return h ? `${h}:${pad(m)}:${pad(sec)}` : `${m}:${pad(sec)}`; } -/** ProPresenter's count_down_to_time target: seconds since midnight + am/pm/24h. */ +/** ProPresenter's count_down_to_time target, short: "9:30a", "12:05p", or "21:30" for a 24-hour clock. */ function timeOfDayText(seconds, period) { const s = Number(seconds); if (!Number.isFinite(s)) return ''; const h = Math.floor(s / 3600) % 24; const m = Math.floor((s % 3600) / 60); const p = String(period || '').toLowerCase(); - if (p === 'am' || p === 'pm') return `${h % 12 || 12}:${String(m).padStart(2, '0')} ${p}`; + if (p === 'am' || p === 'pm') return `${h % 12 || 12}:${String(m).padStart(2, '0')}${p[0]}`; return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; } @@ -278,7 +278,7 @@ function createProPresenterSource({ config, log }) { } if (entry.count_down_to_time) { const at = timeOfDayText(entry.count_down_to_time.time_of_day, entry.count_down_to_time.period); - return { type: 'countdown_to_time', targetMs: null, detail: at ? `Counts down to ${at}` : 'Counts down to a time of day' }; + return { type: 'countdown_to_time', targetMs: null, detail: at ? `to ${at}` : 'to a time of day' }; } if (entry.elapsed) { const end = Number(entry.elapsed.end_time); diff --git a/modules/propresenter-timers/style.css b/modules/propresenter-timers/style.css index 9a9c8a4..d3a0c6b 100644 --- a/modules/propresenter-timers/style.css +++ b/modules/propresenter-timers/style.css @@ -196,7 +196,7 @@ [data-module="propresenter-timers"] .tm-detail { flex: none; max-width: 100%; - font-size: var(--tm-meta-size); + font-size: calc(var(--tm-meta-size) * 0.8); /* smaller than the title; fit() budgets the same share */ font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;