Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/clock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export function initClock(): { start: () => void; stop: () => void } | null {
}

function start() {
tick();
const ms = msToNextSecond(new Date());
setTimeout(() => {
tick();
Expand Down
103 changes: 91 additions & 12 deletions src/components/timer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export function initTimer(_callbacks: TimerCallbacks = {}) {
const presets = parsePresets(localStorage.getItem(STORAGE_KEY));
let core = createTimerCore(presets);
let intervalId: number | null = null;
let beepIntervalId: number | null = null;
let isPanelOpen = false;
let activeInput: 'min' | 'sec' = 'min';

Expand Down Expand Up @@ -267,11 +268,14 @@ export function initTimer(_callbacks: TimerCallbacks = {}) {
document.body.classList.add('timer-finished');
playBeep();
let beepCount = 0;
const beepInterval = window.setInterval(() => {
beepIntervalId = window.setInterval(() => {
playBeep();
beepCount++;
if (beepCount >= 2) {
clearInterval(beepInterval);
if (beepIntervalId !== null) {
clearInterval(beepIntervalId);
beepIntervalId = null;
}
}
}, 700);
}
Expand Down Expand Up @@ -317,6 +321,10 @@ export function initTimer(_callbacks: TimerCallbacks = {}) {
clearInterval(intervalId);
intervalId = null;
}
if (beepIntervalId !== null) {
clearInterval(beepIntervalId);
beepIntervalId = null;
}
core = resetTimer(core);
setInputsFromSeconds(core.configuredDuration);
document.body.classList.remove('timer-finished');
Expand Down Expand Up @@ -351,18 +359,87 @@ export function initTimer(_callbacks: TimerCallbacks = {}) {

// ---------- Panel toggle ----------

// FLIP animation: smoothly animate #clock-container movement when the
// panel is added/removed (which re-centers the container in the body).
// The clock sliding up/down IS the open/close animation for the
// surrounding layout, so open and close feel symmetric.
function flipAnimateClockContainer(action: () => void) {
const container = document.getElementById('clock-container');
if (!container) {
action();
return;
}

// Lock body overflow so the page can't scroll while the height changes
const prevBodyOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';

// First: record the current position of #clock-container
const firstRect = container.getBoundingClientRect();

// Run the DOM mutation (open/close panel)
action();

// Last: measure the new position synchronously
const lastRect = container.getBoundingClientRect();

// Compute the delta and invert it with a transform
const dx = firstRect.left - lastRect.left;
const dy = firstRect.top - lastRect.top;
if (dx === 0 && dy === 0) {
document.body.style.overflow = prevBodyOverflow;
return;
}

container.style.transition = 'none';
container.style.transform = `translate(${dx}px, ${dy}px)`;

// Play: on the next frame, animate the transform back to identity
requestAnimationFrame(() => {
container.style.transition = 'transform 0.3s ease-out';
container.style.transform = '';
});

// Clean up inline styles after the transition completes
const onEnd = () => {
container.removeEventListener('transitionend', onEnd);
container.style.transition = '';
container.style.transform = '';
document.body.style.overflow = prevBodyOverflow;
};
container.addEventListener('transitionend', onEnd);

// Fallback in case transitionend doesn't fire
setTimeout(onEnd, 400);
}

function openPanel() {
isPanelOpen = true;
dom.panel.removeAttribute('hidden');
dom.toggleBtn.classList.add('active');
renderToggle();
flipAnimateClockContainer(() => {
isPanelOpen = true;
dom.panel.removeAttribute('hidden');
dom.toggleBtn.classList.add('active');
renderToggle();
});
}

function closePanel() {
isPanelOpen = false;
dom.panel.setAttribute('hidden', '');
dom.toggleBtn.classList.remove('active');
renderToggle();
// Mirror open exactly: hide the panel immediately and let the FLIP
// clock slide-up be the close animation. This avoids the lag of
// waiting for a separate CSS close animation on the panel.
flipAnimateClockContainer(() => {
isPanelOpen = false;
dom.toggleBtn.classList.remove('active');
dom.panel.classList.remove('closing');
dom.panel.setAttribute('hidden', '');
// If the timer had just finished, auto-reset it on close so the
// next time the panel opens, it's ready to start a new countdown
// at the configured duration (and silences any in-flight beeps).
if (core.state === 'finished') {
onReset();
} else {
renderToggle();
}
});
}

function togglePanel() {
Expand Down Expand Up @@ -410,15 +487,17 @@ export function initTimer(_callbacks: TimerCallbacks = {}) {
dom.presetAddBtn.addEventListener('click', onAddPreset);
document.addEventListener('click', onDocumentClick);
document.addEventListener('keydown', onDocumentKeydown);

closePanel();
}

function destroy() {
if (intervalId !== null) {
clearInterval(intervalId);
intervalId = null;
}
if (beepIntervalId !== null) {
clearInterval(beepIntervalId);
beepIntervalId = null;
}
document.removeEventListener('click', onDocumentClick);
document.removeEventListener('keydown', onDocumentKeydown);
}
Expand Down
20 changes: 16 additions & 4 deletions src/styles/animations.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,27 @@
}
}

/* Panel fade-in — timer panel opens */
@keyframes timer-fade-in {
/* Panel open — timer panel pops in */
@keyframes timer-panel-open {
from {
opacity: 0;
transform: translateY(-6px);
transform: translateY(-8px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0);
transform: translateY(0) scale(1);
}
}

/* Panel close — timer panel pops out */
@keyframes timer-panel-close {
from {
opacity: 1;
transform: translateY(0) scale(1);
}
to {
opacity: 0;
transform: translateY(-8px) scale(0.95);
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/styles/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ body {
background-color: var(--color-bg);
color: var(--color-primary);
font-family: var(--font-family);
overflow: hidden;
overflow-y: auto;
overflow-x: hidden;
}

/* ---------- Environment Marker ---------- */
Expand Down
1 change: 1 addition & 0 deletions src/styles/clock.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
align-items: center;
justify-content: center;
gap: var(--space-3xl);
margin-top: -8vh;
}

#clock {
Expand Down
8 changes: 7 additions & 1 deletion src/styles/timer.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
flex-direction: column;
align-items: center;
gap: var(--space-md);
position: relative;
}

#timer-toggle {
Expand Down Expand Up @@ -52,16 +53,21 @@
border: var(--border-width) solid var(--color-primary);
border-radius: var(--border-radius-lg);
background-color: var(--color-surface);
animation: timer-fade-in var(--transition-normal) ease-out;
box-sizing: border-box;
width: var(--panel-width);
box-shadow: var(--glow-subtle);
animation: timer-panel-open 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

#timer-panel[hidden] {
display: none;
}

#timer-panel.closing {
animation: timer-panel-close 0.2s ease-in both;
pointer-events: none;
}

/* ---------- Display ---------- */

#timer-display {
Expand Down
Loading