Files
gmtool/src/components/break-timer.js
T
slaguru666andClaude Opus 4.8 73a7ea72ad 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>
2026-07-20 23:00:17 +01:00

53 lines
2.3 KiB
JavaScript

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);