Intent. Every door has to say how long you have — "4h 12m left", "lapsed 20m ago", "settled on 14 Sep" — so say it once, in one pure module, and render it in a slot whose width never changes.
Done looks like
[ ] src/when.ts, pure, now injected, no dependency and no database: remaining(until, now) ("4h 12m left", "lapsed"), relative(at, now) ("20m ago", "just now"), and absolute(at) (a UTC date a person can read, plus the ISO string).
[ ] One vocabulary, fixed: units are d h m s, at most two of them, largest first; never "in 0 minutes"; a past time never renders as a future one. src/when.test.ts is a table of inputs to expected strings and covers the boundaries — 0s, 59s, 60s, 90m, 23h59m, 24h, 8d — and a negative duration.
[ ] timeTag(at, now) returns the markup every page uses: <time datetime="…" title="…">4h 12m left</time>, escaped here, with a class the CSS knows.
[ ] static/app.css gives that class a fixed width in ch so a ticking countdown never reflows the row around it, and the numbers are tabular.
[ ] static/live.js re-renders every countdown from its datetime attribute once a second — one interval for the whole page, not one per element, paused when the tab is hidden, no request of any kind. With JavaScript off the server-rendered text is correct at paint and simply grows stale.
[ ] No locale guessing and no Intl on the server: the server renders UTC; the browser may put the local absolute time in the title.
[ ] The module is used by nobody yet and that is fine — #11, #12, #14 and #16 all import it rather than writing their own, which is why it lands first.
Risks Rendering relative time on the server and again in the browser is how the two disagree — the datetime attribute is the single source, and the text is derived from it in both places. A per-element timer on a table with six seats and a hundred acts is the app's only per-second work; keep it to one.
Brief that fits @hearth — honest about what really runs, tested against a clock it is handed. (A person assigns.)
Intent. Every door has to say how long you have — "4h 12m left", "lapsed 20m ago", "settled on 14 Sep" — so say it once, in one pure module, and render it in a slot whose width never changes.
Done looks like
src/when.ts, pure,nowinjected, no dependency and no database:remaining(until, now)("4h 12m left", "lapsed"),relative(at, now)("20m ago", "just now"), andabsolute(at)(a UTC date a person can read, plus the ISO string).d h m s, at most two of them, largest first; never "in 0 minutes"; a past time never renders as a future one.src/when.test.tsis a table of inputs to expected strings and covers the boundaries — 0s, 59s, 60s, 90m, 23h59m, 24h, 8d — and a negative duration.timeTag(at, now)returns the markup every page uses:<time datetime="…" title="…">4h 12m left</time>, escaped here, with a class the CSS knows.static/app.cssgives that class a fixed width inchso a ticking countdown never reflows the row around it, and the numbers are tabular.static/live.jsre-renders every countdown from itsdatetimeattribute once a second — one interval for the whole page, not one per element, paused when the tab is hidden, no request of any kind. With JavaScript off the server-rendered text is correct at paint and simply grows stale.Intlon the server: the server renders UTC; the browser may put the local absolute time in thetitle.Files
src/when.ts(new),src/when.test.ts(new),static/app.css,static/live.js,src/server.test.ts.Risks Rendering relative time on the server and again in the browser is how the two disagree — the
datetimeattribute is the single source, and the text is derived from it in both places. A per-element timer on a table with six seats and a hundred acts is the app's only per-second work; keep it to one.Brief that fits @hearth — honest about what really runs, tested against a clock it is handed. (A person assigns.)