Timers: the gear chooses what a card shows, not which timers; a 24-hour switch; a date that fits

Which timers appear is the header's Timers ▾ menu's job, so the gear's
source switches (ProPresenter / LTC / module timers, the Clock on/off)
are gone. In their place the Show group offers the card's elements —
Names, Status lines, Detail lines, Seconds, Leading zeros, Source
headings; without seconds a countdown reads in whole minutes, rounded
up. The clock's format select is a 24-hour switch (an older layout's
"12h" choice is honoured); the clock card has no CLOCK label; Short day
and Short month appear once Date is on, and the date fits its card — one
line when it can, else a break after the day and a size that fits the
longer half. Solo tiles get the same element switches.

Shell: the settings window's grab bar is the accent colour and the gear
that opened it wears the same colour while it is open; showWhen works in
a tile's gear as it does on the admin page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Waters Church
2026-09-15 09:53:41 -04:00
co-authored by Claude Fable 5.1
parent 1db6c2c89f
commit efbcf960dd
7 changed files with 319 additions and 95 deletions
+27 -3
View File
@@ -286,7 +286,7 @@ function buildTile(tile) {
gear.innerHTML = GEAR_SVG;
gear.addEventListener('click', (e) => {
e.stopPropagation();
toggleSettingsPopover(tile, el);
toggleSettingsPopover(tile, el, gear);
});
head.appendChild(gear);
}
@@ -1108,6 +1108,7 @@ function tileSchema(tile, man) {
function closeSettingsPopovers() {
document.querySelectorAll('.tile-settings').forEach((p) => {
p._cleanup?.();
p._button?.classList.remove('is-open');
p.remove();
});
}
@@ -1143,7 +1144,8 @@ function clampPopover(pop) {
pop.style.top = Math.min(Math.max(pad, pop.offsetTop), maxTop) + 'px';
}
function toggleSettingsPopover(tile, el) {
/** Open (or close) the settings window for `tile`, beside its element `el`; `button` is the gear that asked. */
function toggleSettingsPopover(tile, el, button = null) {
const existing = document.querySelector(`.tile-settings[data-tile-id="${tile.id}"]`);
closeSettingsPopovers();
if (existing) return;
@@ -1156,6 +1158,11 @@ function toggleSettingsPopover(tile, el) {
pop.dataset.tileId = tile.id;
pop.setAttribute('role', 'dialog');
pop.setAttribute('aria-label', `${tile.title || man.name} settings`);
// The gear that opened it wears the window's colour while it is open.
if (button) {
button.classList.add('is-open');
pop._button = button;
}
// A slim bar to move it by — nothing else. No title (the window sits beside
// the tile it belongs to) and no close button: a click anywhere else, or
@@ -1168,6 +1175,7 @@ function toggleSettingsPopover(tile, el) {
pop.append(bar, body);
const inputs = new Map();
const shownWhen = []; // fields that appear only while another has a given value
/* Display choices apply as they change and save with the layout — nothing
to confirm, so no Apply. Typing is debounced because applying means
@@ -1446,6 +1454,8 @@ function toggleSettingsPopover(tile, el) {
field.appendChild(help);
}
if (typeof spec?.showWhen === 'string' && spec.showWhen) shownWhen.push({ field, key: spec.showWhen });
if (current && switchInput && key === current.toggleKey) {
// The group's on/off lives in its title row, never in the body, and a
// click on it must not fold a <details>.
@@ -1461,7 +1471,21 @@ function toggleSettingsPopover(tile, el) {
}
}
// Nothing to press: click away, press Escape, the ✕, or the gear again.
// "Short day" appears once "Date" is on: a field with showWhen:"K" follows
// K's switch, live — the same rule the admin page applies.
if (shownWhen.length) {
const refresh = () => {
for (const rule of shownWhen) {
const read = inputs.get(rule.key);
rule.field.hidden = read ? !read() : false;
}
};
refresh();
body.addEventListener('change', refresh);
body.addEventListener('input', refresh);
}
// Nothing to press: click away, press Escape, or the gear again.
// (The gear's own click stops propagating, so it toggles rather than
// closing and reopening.)
const closeOnClick = (e) => { if (!pop.contains(e.target)) closeSettingsPopovers(); };