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(); };
+9 -6
View File
@@ -306,6 +306,8 @@ body.menu-hidden #topbar {
.tile-btn:hover { background: var(--border); color: var(--text); }
.tile-btn.close:hover { color: var(--danger); }
.tile-btn.active { color: var(--accent); }
.tile-btn.is-open,
.tile-btn.is-open:hover { background: var(--accent); color: var(--accent-text); }
.tile-btn svg { width: 13px; height: 13px; display: block; }
/* controls a module adds to its own title bar (moduleApi.header) */
@@ -554,15 +556,15 @@ body.fullscreen #grid { padding-bottom: 0; }
border-radius: 10px;
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.55);
}
/* the grab bar: a slim strip with a grip mark, nothing to read or press */
/* the grab bar: a slim accent strip with a grip mark, nothing to read or
press — the gear that opened the window wears the same colour meanwhile */
.tile-settings-bar {
flex: none;
height: 14px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.04);
border-bottom: 1px solid var(--border);
background: var(--accent);
cursor: grab;
user-select: none;
-webkit-user-select: none;
@@ -573,11 +575,12 @@ body.fullscreen #grid { padding-bottom: 0; }
width: 34px;
height: 4px;
border-radius: 2px;
background: var(--muted);
opacity: 0.55;
background: var(--accent-text);
opacity: 0.7;
transition: opacity 0.12s;
}
.tile-settings-bar:hover::before { opacity: 0.9; }
.tile-settings-bar:hover::before { opacity: 1; }
.tile-settings .field[hidden] { display: none; }
.tile-settings-bar:active { cursor: grabbing; }
.tile-settings-body {
flex: 1;