refactor(app): give formatTime a home — a leaf format.js, one fewer host hook (R3a) (#895)

static/js/format.js (17). One function. Retires the formatTime hook: 12 -> 11.

WHY A MODULE FOR ONE FUNCTION. formatTime was a host hook — loops.js and
section-practice.js both reached back through the seam for it. It is ALSO, by pure
accident of who calls it, inside the dependency closure of the library carve that comes
next. Leaving it there would have made loops.js and section-practice.js import the
LIBRARY in order to format a timestamp — nonsense, and a cycle waiting to happen.

Same rule as the transport carve: a hook is a cycle you agreed to live with; an import is
a dependency you actually have. formatTime has a real owner. It just isn't app.js, and it
certainly isn't the library. Give it a home and both consumers import it directly.

A leaf on purpose. Anything else that turns out to be a shared pure formatter belongs
here too; nothing does yet (I checked — formatBadge, _safeImageUrl and _fetchJsonOrThrow
have no callers outside the library), so nothing else is here.

node 1040/1040, host contract 2/2, ESLint 0.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Byron Gamatos
2026-07-11 23:27:17 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 8bec8d2466
commit 09f7e450a5
4 changed files with 24 additions and 6 deletions
+4 -3
View File
@@ -20,6 +20,7 @@
// fails CI if the hooks used here and the hooks app.js wires ever drift apart.
import { esc, uiPrompt } from './dom.js';
import { _audioSeek, _audioTime } from './transport.js';
import { formatTime } from './format.js';
import { host } from './host.js';
import {
_setSectionPracticeMode,
@@ -167,11 +168,11 @@ export function updateLoopUI() {
const label = document.getElementById('loop-label');
const hasLoop = loopA !== null && loopB !== null;
if (hasLoop) {
label.textContent = `${host.formatTime(loopA)}${host.formatTime(loopB)}`;
label.textContent = `${formatTime(loopA)}${formatTime(loopB)}`;
document.getElementById('btn-loop-clear').classList.remove('hidden');
document.getElementById('btn-loop-save').classList.remove('hidden');
} else if (loopA !== null) {
label.textContent = `${host.formatTime(loopA)} → ?`;
label.textContent = `${formatTime(loopA)} → ?`;
document.getElementById('btn-loop-clear').classList.add('hidden');
document.getElementById('btn-loop-save').classList.add('hidden');
} else {
@@ -190,7 +191,7 @@ export async function loadSavedLoops() {
sel.innerHTML = '<option value="">Saved Loops</option>';
for (const l of loops) {
sel.innerHTML += `<option value="${l.id}" data-start="${l.start}" data-end="${l.end}">${esc(l.name)} (${host.formatTime(l.start)}${host.formatTime(l.end)})</option>`;
sel.innerHTML += `<option value="${l.id}" data-start="${l.start}" data-end="${l.end}">${esc(l.name)} (${formatTime(l.start)}${formatTime(l.end)})</option>`;
}
if (loops.length > 0) {
sel.classList.remove('hidden');