feat: break timer — pause the session clock so breaks don't poison drift
Add <break-timer> tray: pick a duration, Start break pauses the session clock (reusing clock.pause/resume), End break resumes it — so meal/comfort break time is excluded from elapsed and drift stays honest. The shell owns pause/resume + persistence (break store key); the rail shows a on-break badge and gains a coffee chip. fmtCountdown (M:SS) added to format.js. tests/components/break-timer.test.js + gm-shell-break.test.js (incl. the 30-play/20-break/10-play drift-honesty case) + fmtCountdown. 159 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
49e9dcda46
commit
73a7ea72ad
@@ -0,0 +1,52 @@
|
||||
import { fmtCountdown } from '../core/format.js';
|
||||
|
||||
// Break timer tray. Pauses the session clock for a meal/comfort break so the
|
||||
// break time never poisons drift. The shell owns the clock; this component just
|
||||
// picks a duration and emits start-break / end-break, and renders the countdown
|
||||
// fed back to it via `state` ({ onBreak, remainingMs }).
|
||||
const DURATIONS = [5, 10, 15, 20];
|
||||
|
||||
export class BreakTimer extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this._state = { onBreak: false, remainingMs: null };
|
||||
this._minutes = 10;
|
||||
}
|
||||
connectedCallback() { this.render(); }
|
||||
|
||||
set state(v) { this._state = v || { onBreak: false, remainingMs: null }; this.render(); }
|
||||
get state() { return this._state; }
|
||||
|
||||
render() {
|
||||
const { onBreak, remainingMs } = this._state;
|
||||
|
||||
if (onBreak) {
|
||||
const over = remainingMs != null && remainingMs < 0;
|
||||
const clock = remainingMs != null ? fmtCountdown(Math.abs(remainingMs)) : '';
|
||||
const line = remainingMs == null
|
||||
? 'On break — clock paused'
|
||||
: over ? `Over by ${clock} — resume when ready` : `Back in ${clock}`;
|
||||
this.innerHTML = `
|
||||
<div class="break-timer">
|
||||
<div class="break-status ${over ? 'over' : ''}" data-role="status">${line}</div>
|
||||
<button class="break-end" data-role="end">▶ End break — resume clock</button>
|
||||
</div>`;
|
||||
this.querySelector('[data-role=end]').addEventListener('click', () =>
|
||||
this.dispatchEvent(new CustomEvent('end-break', { bubbles: true })));
|
||||
return;
|
||||
}
|
||||
|
||||
const pills = DURATIONS.map((m) =>
|
||||
`<button class="break-dur ${m === this._minutes ? 'on' : ''}" data-min="${m}">${m}m</button>`).join('');
|
||||
this.innerHTML = `
|
||||
<div class="break-timer">
|
||||
<div class="break-durs" data-role="durs">${pills}</div>
|
||||
<button class="break-start" data-role="start">⏸ Start break — pause clock</button>
|
||||
</div>`;
|
||||
this.querySelectorAll('[data-min]').forEach((b) =>
|
||||
b.addEventListener('click', () => { this._minutes = Number(b.dataset.min); this.render(); }));
|
||||
this.querySelector('[data-role=start]').addEventListener('click', () =>
|
||||
this.dispatchEvent(new CustomEvent('start-break', { detail: { minutes: this._minutes }, bubbles: true })));
|
||||
}
|
||||
}
|
||||
customElements.define('break-timer', BreakTimer);
|
||||
Reference in New Issue
Block a user