diff --git a/modules/propresenter-timers/client.js b/modules/propresenter-timers/client.js index 7d81938..ec5f392 100644 --- a/modules/propresenter-timers/client.js +++ b/modules/propresenter-timers/client.js @@ -64,6 +64,7 @@ const LTC_KEY = 'ltc:ltc'; the same values module.json gives the full "All timers" schema. */ const DEFAULTS = { showNames: true, + groupBySource: false, showHeadings: true, showStatus: true, showDetail: true, @@ -391,7 +392,18 @@ export default function create({ root, moduleApi }) { if (!items.length && source.timers.length) continue; groups.push({ id: source.id, source, items }); } - return groups; + if (pref('groupBySource') === true) return groups; + // Grouping off: every timer in one grid, filled row by row. A source in + // trouble is still named, in one line above the cards. + const trouble = groups.filter((g) => g.source && g.source.status !== 'ok'); + return [{ + id: 'all', + source: null, + flat: true, + items: groups.flatMap((g) => g.items), + status: trouble.length ? 'error' : 'ok', + message: trouble.map((g) => `${g.source.label}: ${g.source.message || 'unavailable'}`).join(' · '), + }]; } /* ── cards ──────────────────────────────────────────────────────── */ @@ -677,7 +689,8 @@ export default function create({ root, moduleApi }) { setText(card.detailEl, detailText); card.detailEl.hidden = !detailText || !showDetail; const kindCls = source.kind === 'ltc' ? ' tm-card-ltc' : source.kind === 'clock' ? ' tm-card-clock' : ''; - setCardClass(card, `tm-card is-${tone}${extra}${kindCls}`); + const stale = source.status === 'error' ? ' is-stale' : ''; + setCardClass(card, `tm-card is-${tone}${extra}${kindCls}${stale}`); } if (charsChanged) fit(); } @@ -748,7 +761,8 @@ export default function create({ root, moduleApi }) { wrap.classList.toggle('tm-mono', mono); needFit = true; } - const showHeadings = !solo && pref('showHeadings') !== false; + // Headings belong to sections; without grouping there is one grid and no heading. + const showHeadings = !solo && pref('groupBySource') === true && pref('showHeadings') !== false; wrap.classList.toggle('tm-headings', showHeadings); for (const [cls, on] of [ ['tm-nostatus', pref('showStatus') === false], @@ -781,18 +795,24 @@ export default function create({ root, moduleApi }) { structureChanged = true; } const source = g.source; - const status = source ? source.status : 'ok'; - const message = source ? source.message : ''; + const status = g.status ?? (source ? source.status : 'ok'); + const message = g.message ?? (source ? source.message : ''); ge.headEl.hidden = !showHeadings; - setText(ge.titleEl, source ? source.label : 'Timer'); + setText(ge.titleEl, source ? source.label : 'Timers'); setText(ge.noteEl, status !== 'ok' ? message : g.items.length ? '' : 'no timers'); // Without headings the source's trouble still has to be said somewhere. const emptyText = status !== 'ok' ? message : 'No timers'; const showEmpty = !showHeadings && (status !== 'ok' || !g.items.length); ge.emptyEl.hidden = !showEmpty; setText(ge.emptyEl, showEmpty ? emptyText : ''); - ge.el.classList.toggle('is-down', status !== 'ok'); - ge.el.classList.toggle('is-stale', status === 'error' && g.items.length > 0); + ge.emptyEl.dataset.tone = showEmpty && status !== 'ok' ? 'warn' : ''; + // In the flat grid a card dims for its own source (paint), not the whole grid. + ge.el.classList.toggle('is-down', !g.flat && status !== 'ok'); + ge.el.classList.toggle('is-stale', !g.flat && status === 'error' && g.items.length > 0); + if (ge.gridEl.classList.contains('is-flow') !== Boolean(g.flat)) { + ge.gridEl.classList.toggle('is-flow', Boolean(g.flat)); + structureChanged = true; + } const els = []; for (const item of g.items) { @@ -920,6 +940,7 @@ export default function create({ root, moduleApi }) { if (!best) return; for (const g of groups) { g.gridEl.style.gridTemplateColumns = `repeat(${best.cols}, 1fr)`; + g.gridEl.style.setProperty('--tm-cols', String(best.cols)); // Space is shared in proportion to each group's rows, so a card is // the same height under every heading. g.el.style.flexGrow = String(Math.max(1, Math.ceil(g.count / best.cols))); diff --git a/modules/propresenter-timers/module.json b/modules/propresenter-timers/module.json index e236599..683de2b 100644 --- a/modules/propresenter-timers/module.json +++ b/modules/propresenter-timers/module.json @@ -156,11 +156,18 @@ "default": false, "group": "Show" }, + "groupBySource": { + "type": "switch", + "label": "Group by source", + "default": false, + "group": "Show" + }, "showHeadings": { "type": "switch", "label": "Source headings", "default": true, - "group": "Show" + "group": "Show", + "showWhen": "groupBySource" }, "clock24h": { "type": "switch", diff --git a/modules/propresenter-timers/style.css b/modules/propresenter-timers/style.css index c951199..a216138 100644 --- a/modules/propresenter-timers/style.css +++ b/modules/propresenter-timers/style.css @@ -39,6 +39,7 @@ visually, per group: the LTC card keeps ticking when only ProPresenter is unreachable. When the ProdDash feed is down everything is old. */ [data-module="propresenter-timers"] .tm-group.is-stale .tm-card, +[data-module="propresenter-timers"] .tm-card.is-stale, [data-module="propresenter-timers"] .tm-wrap.is-feed-down .tm-card { opacity: 0.45; } [data-module="propresenter-timers"] .tm-hint { @@ -126,6 +127,21 @@ } [data-module="propresenter-timers"] .tm-grid:empty { display: none; } +/* Grouping off: one grid for every timer, filled row by row. A wrapping + flex row instead of a CSS grid, so the last row's cards widen to use the + whole width — nothing is left empty. Rows share the height equally. */ +[data-module="propresenter-timers"] .tm-grid.is-flow { + display: flex; + flex-wrap: wrap; + align-content: stretch; + align-items: stretch; +} +[data-module="propresenter-timers"] .tm-grid.is-flow .tm-card { + flex: 1 1 calc((100% - var(--tm-gap, 8px) * (var(--tm-cols, 1) - 1)) / var(--tm-cols, 1)); + min-width: 0; +} +[data-module="propresenter-timers"] .tm-empty[data-tone="warn"] { color: var(--tm-warn); } + /* Compact cards (short tiles) and "Status lines" off: the lines under the digits go, the colour stays. A note that explains a missing value (no LTC input, a solo timer no longer offered) must never be hidden. */