From 2fc9352926fbea10da6440c005dcca1edd7161ed Mon Sep 17 00:00:00 2001 From: Chisel Date: Tue, 3 Feb 2026 17:20:23 +0000 Subject: [PATCH 01/21] chore(changelog): update changelog for version 1.0.27 --- changelog.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/changelog.js b/changelog.js index b8da734..a516651 100644 --- a/changelog.js +++ b/changelog.js @@ -1,7 +1,21 @@ // Version tracking -export const APP_VERSION = '1.0.26'; +export const APP_VERSION = '1.0.27'; export const CHANGELOG = { + '1.0.27': { + date: '2026-02-03', + changes: { + features: [ + 'Revised accelerate-time (skip-ahead) flow: skipped preset days are styled as past, hidden when not current, and correctly numbered when starting a new day with Wake Up', + 'Add keyboard shortcut for Accelerate Time', + ], + improvements: [ + 'Sound choices in settings are now applied correctly; sound loading is more reliable', + 'Nominations countdown resets when starting a new day; wake-up countdown shortened to 6 seconds', + 'Timer display no longer flickers when reloading', + ], + }, + }, '1.0.26': { date: '2026-02-03', changes: { From 978a45529df8f20dbadf0b0ca8122ca474b7e9c3 Mon Sep 17 00:00:00 2001 From: Chisel Date: Tue, 3 Feb 2026 14:49:01 +0000 Subject: [PATCH 02/21] chore: change accelerate time process --- index.html | 5 +++- script.js | 70 +++++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/index.html b/index.html index 400f13c..c7dc6c2 100644 --- a/index.html +++ b/index.html @@ -314,7 +314,10 @@
-
diff --git a/script.js b/script.js index 0ec9a71..3df50ee 100644 --- a/script.js +++ b/script.js @@ -282,6 +282,8 @@ const BUTTON_LABELS = { RESUME: '▶️ Resume Day', RESET: '🔄 Reset Day', ACCELERATE: '⏩ Accelerate Time', + ACCELERATE_CONFIRM: 'Confirm…', + ACCELERATE_TIME_FLIES: '{time flies}', START_DAY: (day) => `▶ Start Day ${day}`, FULLSCREEN: { ENTER: @@ -323,6 +325,7 @@ let currentInterval = normalInterval; let wakeUpTimeout = null; let isEndSoundPlaying = false; // New state variable let hasReset = false; // New state variable to track reset state +let accelerateConfirmTimeout = null; // Game pace multipliers const PACE_MULTIPLIERS = { @@ -633,7 +636,7 @@ document.addEventListener('DOMContentLoaded', async () => { playerCountInput = document.getElementById('playerCount'); travellerCountInput = document.getElementById('travellerCount'); accelerateBtn = document.getElementById('accelerateBtn'); - accelerateBtn.textContent = BUTTON_LABELS.ACCELERATE; + resetAccelerateButton(); accelerateBtn.disabled = true; // Accelerate button should be disabled initially minuteButtons = document.querySelectorAll('.minute-btn'); secondButtons = document.querySelectorAll('.second-btn'); @@ -745,24 +748,31 @@ document.addEventListener('DOMContentLoaded', async () => { } }); - // Add hold-to-activate for accelerate button - timerUtils.holdToActivate( - accelerateBtn, - 2000, // 2 seconds hold duration - (progress) => { - // Progress is now handled by CSS custom property - }, - () => { - // Reset button appearance and trigger acceleration - accelerateBtn.style.setProperty('--progress-width', '0%'); - if (!accelerateBtn.disabled) { - accelerateTime(); + // Accelerate button: click -> "Confirm…", second click within 5s triggers acceleration + accelerateBtn.addEventListener('click', () => { + if (accelerateBtn.disabled) return; + if (accelerateBtn.textContent === BUTTON_LABELS.ACCELERATE) { + accelerateBtn.textContent = BUTTON_LABELS.ACCELERATE_CONFIRM; + accelerateBtn.setAttribute( + 'aria-label', + 'Click again to accelerate time' + ); + if (accelerateConfirmTimeout) clearTimeout(accelerateConfirmTimeout); + accelerateConfirmTimeout = setTimeout(() => { + accelerateConfirmTimeout = null; + resetAccelerateButton(); + }, ACCELERATE_CONFIRM_SECONDS * 1000); + } else { + // Confirm: clear timeout, show "time flies", disable, then accelerate + if (accelerateConfirmTimeout) { + clearTimeout(accelerateConfirmTimeout); + accelerateConfirmTimeout = null; } + accelerateBtn.textContent = BUTTON_LABELS.ACCELERATE_TIME_FLIES; + accelerateBtn.disabled = true; + accelerateTime(); } - ); - - // Remove the click event listener for accelerate button since we're using hold now - accelerateBtn.removeEventListener('click', accelerateTime); + }); // Add event listeners startBtn.addEventListener('click', startTimer); @@ -1378,6 +1388,7 @@ function updateClocktowerPresets() { startCountdown(); startBtn.disabled = false; updateStartButtonText(BUTTON_LABELS.PAUSE); + resetAccelerateButton(); accelerateBtn.disabled = false; resetBtn.disabled = false; // Enable reset button when starting timer }); @@ -1623,7 +1634,9 @@ function updateDisplay() { } else if (isRunning) { updateStartButtonText(BUTTON_LABELS.PAUSE); startBtn.disabled = false; - accelerateBtn.disabled = false; + // Keep accelerate disabled during accelerated countdown ("time flies") + accelerateBtn.disabled = + accelerateBtn.textContent === BUTTON_LABELS.ACCELERATE_TIME_FLIES; resetBtn.disabled = false; // Enable reset while running } else if (timeLeft > 0 && !hasReset) { updateStartButtonText(BUTTON_LABELS.RESUME); @@ -1644,6 +1657,21 @@ function updateDisplay() { ); } +const ACCELERATE_CONFIRM_SECONDS = 5; + +function resetAccelerateButton() { + if (accelerateConfirmTimeout) { + clearTimeout(accelerateConfirmTimeout); + accelerateConfirmTimeout = null; + } + accelerateBtn.textContent = BUTTON_LABELS.ACCELERATE; + accelerateBtn.setAttribute( + 'aria-label', + 'Accelerate time (click then confirm)' + ); + accelerateBtn.style.setProperty('--progress-width', '0%'); +} + // Acceleration functionality function accelerateTime() { if (!isRunning || timeLeft <= 0) return; @@ -1686,7 +1714,7 @@ function accelerateTime() { } }, currentInterval); - // Disable accelerate button after use + // Disable accelerate button after use (do not reset label during accelerated countdown) accelerateBtn.disabled = true; } @@ -1751,6 +1779,7 @@ function resetTimer() { // Reset button states startBtn.disabled = false; updateStartButtonText(BUTTON_LABELS.WAKE_UP); + resetAccelerateButton(); accelerateBtn.disabled = true; resetBtn.disabled = true; @@ -1917,6 +1946,7 @@ function playWakeUpSound() { startCountdown(); startBtn.disabled = false; updateStartButtonText(BUTTON_LABELS.PAUSE); + resetAccelerateButton(); accelerateBtn.disabled = false; resetBtn.disabled = false; } @@ -1991,6 +2021,7 @@ function startNewGame() { // Set button to Wake Up state startBtn.disabled = false; updateStartButtonText(BUTTON_LABELS.WAKE_UP); + resetAccelerateButton(); accelerateBtn.disabled = true; // Accelerate button should start disabled saveSettings(); @@ -2043,6 +2074,7 @@ function updateDayDisplay(state = '') { // Ensure button states are correct for dusk startBtn.disabled = false; updateStartButtonText(BUTTON_LABELS.WAKE_UP); + resetAccelerateButton(); accelerateBtn.disabled = true; resetBtn.disabled = true; } else { From 5fe75d12c6d34db64ee84b45d988598f6f18ec4a Mon Sep 17 00:00:00 2001 From: Chisel Date: Tue, 3 Feb 2026 14:58:04 +0000 Subject: [PATCH 03/21] chore: dim past day clocktower presets --- script.js | 10 ++++++---- styles.css | 9 +++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/script.js b/script.js index 3df50ee..1deb230 100644 --- a/script.js +++ b/script.js @@ -1334,6 +1334,9 @@ function updateClocktowerPresets() { if (preset.day === currentDay) { button.classList.add('current-day'); } + if (preset.day < currentDay) { + button.classList.add('past-day'); + } button.innerHTML = ` ${preset.display} Day ${preset.day} @@ -2083,10 +2086,9 @@ function updateDayDisplay(state = '') { // Update preset button highlighting document.querySelectorAll('.clocktower-btn').forEach((btn) => { - btn.classList.toggle( - 'current-day', - Number.parseInt(btn.dataset.day) === currentDay - ); + const btnDay = Number.parseInt(btn.dataset.day); + btn.classList.toggle('current-day', btnDay === currentDay); + btn.classList.toggle('past-day', btnDay < currentDay); }); // Save the current state diff --git a/styles.css b/styles.css index 2f91af7..ac78479 100644 --- a/styles.css +++ b/styles.css @@ -1185,6 +1185,15 @@ body[data-pace='blitz'] .info-value { box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } +.clocktower-presets button.past-day { + opacity: 0.55; + filter: saturate(0.6); +} + +.clocktower-presets button.past-day:hover { + opacity: 0.7; +} + .clocktower-presets button.current-day { box-shadow: 0 0 0 2px var(--colour-gold); animation: pulse 2s infinite; From 6e3e04cf1b47856226bab4c045a3a904b59c9053 Mon Sep 17 00:00:00 2001 From: Chisel Date: Wed, 4 Feb 2026 10:26:06 +0000 Subject: [PATCH 04/21] chore(tests): turn off effects in 05-timer-behavior.cy.js; make tests quiet again --- cypress/e2e/05-timer-behavior.cy.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cypress/e2e/05-timer-behavior.cy.js b/cypress/e2e/05-timer-behavior.cy.js index 0ee517f..68fd164 100644 --- a/cypress/e2e/05-timer-behavior.cy.js +++ b/cypress/e2e/05-timer-behavior.cy.js @@ -11,6 +11,10 @@ describe('Timer Behavior', () => { // Settings dialog should be open on fresh start cy.get('#settingsDialog').should('be.visible'); + // Turn OFF effects (trigger change so playSoundEffects is updated before save) + cy.get('.tab-button[data-tab="effects"]').click(); + cy.get('#playSoundEffects').uncheck().trigger('change'); + // Save and Close settings cy.get('#closeSettings').click(); cy.get('#settingsDialog').should('not.be.visible'); @@ -49,6 +53,11 @@ describe('Timer Behavior', () => { cy.visit('/'); cy.get('#settingsDialog').should('be.visible'); + + // Turn OFF effects (trigger change so playSoundEffects is updated before save) + cy.get('.tab-button[data-tab="effects"]').click(); + cy.get('#playSoundEffects').uncheck().trigger('change'); + cy.get('#closeSettings').click(); cy.get('#settingsDialog').should('not.be.visible'); From 3665b72e3c1827e6b86efa66e43e5e21b0cff559 Mon Sep 17 00:00:00 2001 From: Chisel Date: Wed, 4 Feb 2026 10:07:41 +0000 Subject: [PATCH 05/21] chore(tests): add cypress spec to test preset skipping --- cypress/e2e/08-preset-skips.cy.js | 172 ++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 cypress/e2e/08-preset-skips.cy.js diff --git a/cypress/e2e/08-preset-skips.cy.js b/cypress/e2e/08-preset-skips.cy.js new file mode 100644 index 0000000..655b2a4 --- /dev/null +++ b/cypress/e2e/08-preset-skips.cy.js @@ -0,0 +1,172 @@ +describe('Preset skips and accelerate behaviour', () => { + beforeEach(() => { + cy.clearLocalStorage(); + }); + + it('keeps all days while hiding only appropriate skipped presets', () => { + // Load page + cy.visit('/'); + + // Settings dialog open on first load + cy.get('#settingsDialog').should('be.visible'); + + // Game settings: 12 players + cy.get('#playerCount').clear().type('12'); + + // Turn OFF effects (trigger change so playSoundEffects is updated before save) + cy.get('.tab-button[data-tab="effects"]').click(); + cy.get('#playSoundEffects').uncheck().trigger('change'); + + // Turn OFF music (trigger change so playMusic is updated before save) + cy.get('.tab-button[data-tab="music"]').click(); + cy.get('#playMusic').uncheck().trigger('change'); + + // Start new game + cy.get('#startNewGame').click(); + cy.get('#settingsDialog').should('not.be.visible'); + + // Capture the initial set of visible day labels (Day 1..Day N for this player count) + let initialDayLabels = []; + cy.get('#clocktowerPresets .clocktower-btn:visible .day').then(($spans) => { + const labels = Array.from($spans, (el) => el.textContent.trim()); + initialDayLabels = labels.filter((t) => t.startsWith('Day ')); + expect(initialDayLabels.length).to.be.greaterThan(0); + }); + + // Helper: assert visible day labels stay the same set as at the start + const expectSameDaySet = () => { + cy.get('#clocktowerPresets .clocktower-btn:visible .day').then( + ($spans) => { + const labels = Array.from($spans, (el) => el.textContent.trim()); + const dayLabels = labels.filter((t) => t.startsWith('Day ')); + expect(dayLabels).to.deep.equal(initialDayLabels); + } + ); + }; + + // Initially we should have a stable set of Day presets + expectSameDaySet(); + + // Select the Day 4 preset while on Day 1 + cy.contains('#clocktowerPresets .clocktower-btn .day', 'Day 4') + .closest('button') + .click(); + + // First three presets visible and marked as skipped (💀) + cy.get('#clocktowerPresets .clocktower-btn') + .filter(':visible') + .eq(0) + .should('have.class', 'skipped-day'); + cy.get('#clocktowerPresets .clocktower-btn') + .filter(':visible') + .eq(1) + .should('have.class', 'skipped-day'); + cy.get('#clocktowerPresets .clocktower-btn') + .filter(':visible') + .eq(2) + .should('have.class', 'skipped-day'); + + // Still have the same visible day presets + expectSameDaySet(); + + // Day 4 preset click already started the timer; accelerate is enabled + // Use Accelerate Time (click then confirm) + cy.get('#accelerateBtn', { timeout: 20000 }).should('not.be.disabled'); + cy.get('#accelerateBtn').click(); + cy.get('#accelerateBtn').click(); + + // Wait for accelerated day to complete (Wake Up active again) + cy.contains('#startBtn .button-text', '⏰ Wake Up!', { + timeout: 20000, + }).should('be.visible'); + + // After day end: no visible skipped presets, same Day set + cy.get('#clocktowerPresets .clocktower-btn.skipped-day:visible').should( + 'have.length', + 0 + ); + expectSameDaySet(); + + // In dusk with Wake Up active, click Day 4 for the next day + cy.contains('#clocktowerPresets .clocktower-btn .day', 'Day 4') + .closest('button') + .click(); + + // Now on Day 2: pattern Day 1, 💀, 💀, Day 2, Day 3..Day 9 + cy.get('#clocktowerPresets .clocktower-btn') + .filter(':visible') + .then(($btns) => { + const labels = Array.from($btns, (btn) => + btn.querySelector('.day').textContent.trim() + ); + const classes = Array.from($btns, (btn) => btn.className); + + expect(labels[0]).to.equal('Day 1'); + expect(classes[1]).to.include('skipped-day'); + expect(classes[2]).to.include('skipped-day'); + expect(labels[3]).to.equal('Day 2'); + // The remaining visible day labels should still include all later days + const remainingDays = labels + .slice(3) + .filter((t) => t.startsWith('Day ')) + .map((t) => Number(t.replace('Day ', ''))); + expect(remainingDays).to.include.members([3, 4, 5, 6, 7, 8, 9]); + }); + + // Day 4 preset click already started the timer; accelerate is enabled + cy.get('#accelerateBtn', { timeout: 20000 }).should('not.be.disabled'); + cy.get('#accelerateBtn').click(); + cy.get('#accelerateBtn').click(); + + cy.contains('#startBtn .button-text', '⏰ Wake Up!', { + timeout: 20000, + }).should('be.visible'); + + // After second day end: no skulls, same Day set + cy.get('#clocktowerPresets .clocktower-btn.skipped-day:visible').should( + 'have.length', + 0 + ); + expectSameDaySet(); + + // In dusk again, click Day 4 for the next day + cy.contains('#clocktowerPresets .clocktower-btn .day', 'Day 4') + .closest('button') + .click(); + + // Now on Day 3: Day 1, Day 2, 💀, Day 3..Day 9 + cy.get('#clocktowerPresets .clocktower-btn') + .filter(':visible') + .then(($btns) => { + const labels = Array.from($btns, (btn) => + btn.querySelector('.day').textContent.trim() + ); + const classes = Array.from($btns, (btn) => btn.className); + + expect(labels[0]).to.equal('Day 1'); + expect(labels[1]).to.equal('Day 2'); + expect(classes[2]).to.include('skipped-day'); + expect(labels[3]).to.equal('Day 3'); + const remainingDays = labels + .slice(3) + .filter((t) => t.startsWith('Day ')); + expect(remainingDays.length).to.be.greaterThan(0); + }); + + // Day 4 preset click already started the timer; accelerate is enabled + cy.get('#accelerateBtn', { timeout: 20000 }).should('not.be.disabled'); + cy.get('#accelerateBtn').click(); + cy.get('#accelerateBtn').click(); + + cy.contains('#startBtn .button-text', '⏰ Wake Up!', { + timeout: 20000, + }).should('be.visible'); + + // After third day end: no skulls, same Day set + cy.get('#clocktowerPresets .clocktower-btn.skipped-day:visible').should( + 'have.length', + 0 + ); + expectSameDaySet(); + }); +}); From dc91c29f3024580f901b0a0c99790ab646c69436 Mon Sep 17 00:00:00 2001 From: Chisel Date: Tue, 3 Feb 2026 16:55:33 +0000 Subject: [PATCH 06/21] chore: shorten wake-up countdown to 6 seconds --- script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script.js b/script.js index 1deb230..2037ab4 100644 --- a/script.js +++ b/script.js @@ -1910,7 +1910,7 @@ function playWakeUpSound() { updateStartButtonText(BUTTON_LABELS.WAKE_UP); accelerateBtn.disabled = true; - let countdownSeconds = 10; + let countdownSeconds = 6; timeLeft = countdownSeconds; updateDisplay(); From a3c0fb06b5c3cb1da5773ac9e6ff59c396121667 Mon Sep 17 00:00:00 2001 From: Chisel Date: Tue, 3 Feb 2026 16:28:38 +0000 Subject: [PATCH 07/21] fix: prevent font flicker on timer display reload --- index.html | 5 +++++ script.js | 16 ++++++++++++++++ styles.css | 7 +++++++ 3 files changed, 28 insertions(+) diff --git a/index.html b/index.html index c7dc6c2..db6f974 100644 --- a/index.html +++ b/index.html @@ -31,6 +31,11 @@ + Date: Tue, 3 Feb 2026 16:37:31 +0000 Subject: [PATCH 08/21] fix: reset nominations countdown when starting a new day --- script.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script.js b/script.js index 03c8d64..93151e4 100644 --- a/script.js +++ b/script.js @@ -1372,8 +1372,9 @@ function updateClocktowerPresets() { selectedMinutes = preset.minutes; selectedSeconds = preset.seconds; - // Reset any existing timer + // Reset any existing timer and cancel nominations countdown when starting a new day clearInterval(timerId); + clearNominationsCountdown(); // Check if we're in dusk state and increment day if needed const dayInfo = document.querySelector('.day-display'); From 91fc09c962b627b5819f1c63677f565f858fd5af Mon Sep 17 00:00:00 2001 From: Chisel Date: Tue, 3 Feb 2026 16:23:31 +0000 Subject: [PATCH 09/21] feat: make it clear if we skipped a preset day --- script.js | 112 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 97 insertions(+), 15 deletions(-) diff --git a/script.js b/script.js index 93151e4..c989c47 100644 --- a/script.js +++ b/script.js @@ -356,6 +356,8 @@ let playerCount = 10; // Default to 10 players let travellerCount = 0; // Default to 0 travellers let isFirstLoad = false; let currentDay = null; +/** Day number → preset index used for that day. Only presets we skipped get 💀; used presets keep "Day N"; rest renumbered from current. */ +let usedPresetByDay = {}; let currentPace = 'normal'; // Default pace let playMusic = false; // Default to false for new users let playMusicAtNight = false; // Default to false for new users @@ -1083,6 +1085,19 @@ function applyParsedSettings(settings) { playerCount = settings.playerCount || 10; travellerCount = settings.travellerCount || 0; currentDay = settings.currentDay || 1; + if ( + settings.usedPresetByDay && + typeof settings.usedPresetByDay === 'object' + ) { + usedPresetByDay = settings.usedPresetByDay; + } else if (settings.dayOnePresetIndex != null) { + // Migrate: assume sequential use from dayOnePresetIndex + usedPresetByDay = {}; + for (let d = 1; d <= currentDay; d++) + usedPresetByDay[d] = settings.dayOnePresetIndex + d - 1; + } else { + usedPresetByDay = {}; + } currentPace = settings.currentPace || 'normal'; playMusic = settings.playMusic === undefined ? false : settings.playMusic; playMusicAtNight = @@ -1316,6 +1331,7 @@ function saveSettings() { playerCount, travellerCount, currentDay, + usedPresetByDay, currentPace, playMusic, playMusicAtNight, @@ -1338,35 +1354,97 @@ function saveSettings() { localStorage.setItem('quickTimerSettings', JSON.stringify(settings)); } +// Returns { label, effectiveDay } for a preset. effectiveDay is null for skipped (💀). +function getPresetDayLabel(presetDay, numberOfDays) { + const used = Object.values(usedPresetByDay).filter( + (p) => p >= 1 && p <= numberOfDays + ); + if (used.length === 0) { + return { label: `Day ${presetDay}`, effectiveDay: presetDay }; + } + const usedSorted = [...used].sort((a, b) => a - b); + const minUsed = usedSorted[0]; + const maxUsed = usedSorted.at(-1); + // Used for a specific day → show that day + for (const [d, p] of Object.entries(usedPresetByDay)) { + if (Number(p) === presetDay) { + return { label: `Day ${d}`, effectiveDay: Number(d) }; + } + } + if (presetDay < minUsed) return { label: '💀', effectiveDay: null }; + // Between two used presets → skipped + for (let i = 0; i < usedSorted.length - 1; i++) { + if (presetDay > usedSorted[i] && presetDay < usedSorted[i + 1]) + return { label: '💀', effectiveDay: null }; + } + // From current day's preset onward (or from first future if in dusk) + const anchor = + usedPresetByDay[currentDay] !== undefined + ? usedPresetByDay[currentDay] + : maxUsed + 1; + const baseDay = + usedPresetByDay[currentDay] !== undefined ? currentDay : currentDay + 1; + if (presetDay >= anchor) { + const eff = baseDay + (presetDay - anchor); + return { label: `Day ${eff}`, effectiveDay: eff }; + } + return { label: '💀', effectiveDay: null }; +} + // Settings functionality function updateClocktowerPresets() { const clocktowerPresetsDiv = document.getElementById('clocktowerPresets'); clocktowerPresetsDiv.innerHTML = ''; // Clear existing presets const presets = generateDayPresets(playerCount); + const numberOfDays = presets.length; presets.forEach((preset) => { + const { label: dayLabel, effectiveDay: eff } = getPresetDayLabel( + preset.day, + numberOfDays + ); const button = document.createElement('button'); button.className = 'preset-btn clocktower-btn'; - if (preset.day === currentDay) { + if (eff !== null && eff === currentDay) { button.classList.add('current-day'); } - if (preset.day < currentDay) { + if (eff !== null && eff < currentDay) { button.classList.add('past-day'); } button.innerHTML = ` ${preset.display} - Day ${preset.day} + ${dayLabel} `; button.dataset.minutes = preset.minutes; button.dataset.seconds = preset.seconds; button.dataset.day = preset.day; button.addEventListener('click', (e) => { - // Update active state + const dayInfo = document.querySelector('.day-display'); + const isDusk = dayInfo?.classList.contains('dusk'); + const { effectiveDay: eff } = getPresetDayLabel(preset.day, numberOfDays); + const isAhead = eff !== null && eff > currentDay; + + if (isAhead && !isDusk) { + // Not in dusk: use this preset for current day + usedPresetByDay[currentDay] = preset.day; + saveSettings(); + updateClocktowerPresets(); + } else if (isDusk && currentDay !== null) { + // In dusk: use this preset for the next day + usedPresetByDay[currentDay + 1] = preset.day; + currentDay++; + saveSettings(); + updateClocktowerPresets(); + } + + // Update active state (use selector in case presets were just rebuilt) document .querySelectorAll('.clocktower-btn') .forEach((btn) => btn.classList.remove('active')); - button.classList.add('active'); + document + .querySelector(`.clocktower-btn[data-day="${preset.day}"]`) + ?.classList.add('active'); // Update selected time selectedMinutes = preset.minutes; @@ -1376,13 +1454,6 @@ function updateClocktowerPresets() { clearInterval(timerId); clearNominationsCountdown(); - // Check if we're in dusk state and increment day if needed - const dayInfo = document.querySelector('.day-display'); - if (currentDay !== null && dayInfo.classList.contains('dusk')) { - currentDay++; - saveSettings(); - } - // Set and display the new time timeLeft = selectedMinutes * 60 + selectedSeconds; updateDisplay(); @@ -1944,8 +2015,12 @@ function playWakeUpSound() { updateDayDisplay(); // Find and start the current day's timer directly instead of clicking the preset + const currentPresetDay = + usedPresetByDay[currentDay] !== undefined + ? usedPresetByDay[currentDay] + : currentDay; const dayPreset = document.querySelector( - `.clocktower-btn[data-day="${currentDay}"]` + `.clocktower-btn[data-day="${currentPresetDay}"]` ); if (dayPreset) { // Update active state @@ -2036,6 +2111,7 @@ function startNewGame() { // Set to Day 1 currentDay = 1; + usedPresetByDay = {}; updateDayDisplay(); // Set button to Wake Up state @@ -2102,10 +2178,16 @@ function updateDayDisplay(state = '') { } // Update preset button highlighting + const numberOfDays = document.querySelectorAll('.clocktower-btn').length; + const currentPresetDay = + usedPresetByDay[currentDay] !== undefined + ? usedPresetByDay[currentDay] + : currentDay; document.querySelectorAll('.clocktower-btn').forEach((btn) => { const btnDay = Number.parseInt(btn.dataset.day); - btn.classList.toggle('current-day', btnDay === currentDay); - btn.classList.toggle('past-day', btnDay < currentDay); + const { effectiveDay: eff } = getPresetDayLabel(btnDay, numberOfDays); + btn.classList.toggle('current-day', btnDay === currentPresetDay); + btn.classList.toggle('past-day', eff !== null && eff < currentDay); }); // Save the current state From a00bcdbb80a4621bec5c13b7dbb63b3b5792b1c3 Mon Sep 17 00:00:00 2001 From: Chisel Date: Tue, 3 Feb 2026 16:31:12 +0000 Subject: [PATCH 10/21] fix: add 'past day' class to skipped presets --- script.js | 3 +++ styles.css | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/script.js b/script.js index c989c47..1ddb9ba 100644 --- a/script.js +++ b/script.js @@ -1405,6 +1405,9 @@ function updateClocktowerPresets() { ); const button = document.createElement('button'); button.className = 'preset-btn clocktower-btn'; + if (eff === null) { + button.classList.add('skipped-day'); + } if (eff !== null && eff === currentDay) { button.classList.add('current-day'); } diff --git a/styles.css b/styles.css index 3961742..f1f736b 100644 --- a/styles.css +++ b/styles.css @@ -1192,6 +1192,17 @@ body[data-pace='blitz'] .info-value { box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } +.clocktower-presets button.skipped-day { + padding-left: 0.4rem; + padding-right: 0.4rem; + opacity: 0.5; + filter: saturate(0.5); +} + +.clocktower-presets button.skipped-day:hover { + opacity: 0.65; +} + .clocktower-presets button.past-day { opacity: 0.55; filter: saturate(0.6); From c97f0ac0985d84ca78fcf49907a40a0cdada59a9 Mon Sep 17 00:00:00 2001 From: Chisel Date: Tue, 3 Feb 2026 16:35:02 +0000 Subject: [PATCH 11/21] chore: use different style for skipped days --- styles.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/styles.css b/styles.css index f1f736b..dc6bdb3 100644 --- a/styles.css +++ b/styles.css @@ -1195,12 +1195,15 @@ body[data-pace='blitz'] .info-value { .clocktower-presets button.skipped-day { padding-left: 0.4rem; padding-right: 0.4rem; - opacity: 0.5; + background-color: rgba(26, 26, 26, 0.85); + border: 1px solid var(--colour-border); + opacity: 0.55; filter: saturate(0.5); } .clocktower-presets button.skipped-day:hover { opacity: 0.65; + background-color: rgba(32, 32, 32, 0.9); } .clocktower-presets button.past-day { From 69abc36686f7f3202cb3359a50b503e3cf9c5ee4 Mon Sep 17 00:00:00 2001 From: Chisel Date: Tue, 3 Feb 2026 16:53:28 +0000 Subject: [PATCH 12/21] fix: honour skip-ahead numbering when starting a new day with Wake Up --- script.js | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/script.js b/script.js index 1ddb9ba..70ba875 100644 --- a/script.js +++ b/script.js @@ -2017,11 +2017,15 @@ function playWakeUpSound() { // Remove dawn state and show regular day display updateDayDisplay(); - // Find and start the current day's timer directly instead of clicking the preset - const currentPresetDay = - usedPresetByDay[currentDay] !== undefined - ? usedPresetByDay[currentDay] - : currentDay; + // Find and start the current day's timer (honour skip-ahead numbering) + let currentPresetDay = usedPresetByDay[currentDay]; + if (currentPresetDay === undefined) { + const used = Object.values(usedPresetByDay); + currentPresetDay = used.length > 0 ? Math.max(...used) + 1 : currentDay; + usedPresetByDay[currentDay] = currentPresetDay; + saveSettings(); + updateClocktowerPresets(); + } const dayPreset = document.querySelector( `.clocktower-btn[data-day="${currentPresetDay}"]` ); @@ -2180,12 +2184,13 @@ function updateDayDisplay(state = '') { dayInfo.innerHTML = `${currentDay}
${paceEmoji} ${paceText}
`; } - // Update preset button highlighting + // Update preset button highlighting (honour skip-ahead: use next preset if current day not yet chosen) const numberOfDays = document.querySelectorAll('.clocktower-btn').length; - const currentPresetDay = - usedPresetByDay[currentDay] !== undefined - ? usedPresetByDay[currentDay] - : currentDay; + let currentPresetDay = usedPresetByDay[currentDay]; + if (currentPresetDay === undefined) { + const used = Object.values(usedPresetByDay); + currentPresetDay = used.length > 0 ? Math.max(...used) + 1 : currentDay; + } document.querySelectorAll('.clocktower-btn').forEach((btn) => { const btnDay = Number.parseInt(btn.dataset.day); const { effectiveDay: eff } = getPresetDayLabel(btnDay, numberOfDays); From 9d691c56e3e66a7ebd4b71d88b5995dbb18b1337 Mon Sep 17 00:00:00 2001 From: Chisel Date: Tue, 3 Feb 2026 16:59:22 +0000 Subject: [PATCH 13/21] fix: use the sounds we chose in the settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary of what was wrong and what was changed: Cause: On load the app does two things in order: In the DOMContentLoaded handler it creates the sound Audio objects using the default filenames (Cathedral Bells and Chisel Bell). Later it calls loadSettings(), which reads your saved choices from localStorage and updates the variables and the UI (so the dropdowns showed “Single Church Bell” and “Quick Bell”), but it did not create new Audio objects from those values. So the variables and the form were correct, but endSound and wakeUpSound were still the original Audio instances pointing at the default files. Fix: In loadSettings(), when there are saved settings, the code now recreates the three sound Audio objects after applying the parsed settings, so they use the loaded endOfDaySound, wakeUpSoundFile, and nominationsOpenSoundFile. That way the sounds that play match what’s saved and what you see in Settings. After a refresh, the end-of-day and wake-up sounds should be the ones you saved (e.g. Single Church Bell and Quick Bell). No need to change settings again. --- script.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/script.js b/script.js index 70ba875..111b09f 100644 --- a/script.js +++ b/script.js @@ -1256,6 +1256,13 @@ function loadSettings() { const savedSettings = localStorage.getItem('quickTimerSettings'); if (savedSettings) { applyParsedSettings(JSON.parse(savedSettings)); + // Recreate Audio objects so they use the loaded sound files (they were + // initially created with defaults in DOMContentLoaded). + endSound = new Audio(`sounds/end-of-day/${endOfDaySound}`); + wakeUpSound = new Audio(`sounds/wake-up/${wakeUpSoundFile}`); + nominationsOpenSound = new Audio( + `sounds/nominations-open/${nominationsOpenSoundFile}` + ); } else { initSoundsFirstLoad(); } From a19cbdb0de5786e49b788a59898c18f178729e74 Mon Sep 17 00:00:00 2001 From: Chisel Date: Tue, 3 Feb 2026 17:03:27 +0000 Subject: [PATCH 14/21] chore: fix sound loading (YT init) --- script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script.js b/script.js index 111b09f..4f74165 100644 --- a/script.js +++ b/script.js @@ -1047,7 +1047,7 @@ document.addEventListener('DOMContentLoaded', async () => { // Create YouTube container immediately if music is enabled if (playMusic) { - createYoutubePlayer(); + initYoutubePlayer(); } // Load keyboard shortcuts into UI From 960c77f29e50719e834006a56d8682aeb97c338b Mon Sep 17 00:00:00 2001 From: Chisel Date: Tue, 3 Feb 2026 17:08:37 +0000 Subject: [PATCH 15/21] chore: add end days for each skipped preset --- script.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/script.js b/script.js index 4f74165..69a9825 100644 --- a/script.js +++ b/script.js @@ -1363,9 +1363,7 @@ function saveSettings() { // Returns { label, effectiveDay } for a preset. effectiveDay is null for skipped (💀). function getPresetDayLabel(presetDay, numberOfDays) { - const used = Object.values(usedPresetByDay).filter( - (p) => p >= 1 && p <= numberOfDays - ); + const used = Object.values(usedPresetByDay).filter((p) => p >= 1); if (used.length === 0) { return { label: `Day ${presetDay}`, effectiveDay: presetDay }; } @@ -1405,6 +1403,23 @@ function updateClocktowerPresets() { const presets = generateDayPresets(playerCount); const numberOfDays = presets.length; + + // Count skipped presets and append a suitable-timed extra preset for each (so we don't run out) + let skippedCount = 0; + for (const preset of presets) { + if (getPresetDayLabel(preset.day, numberOfDays).effectiveDay === null) + skippedCount++; + } + const lastBasePreset = presets[numberOfDays - 1]; + for (let i = 1; i <= skippedCount; i++) { + presets.push({ + minutes: lastBasePreset.minutes, + seconds: lastBasePreset.seconds, + display: lastBasePreset.display, + day: numberOfDays + i, + }); + } + presets.forEach((preset) => { const { label: dayLabel, effectiveDay: eff } = getPresetDayLabel( preset.day, From 8e939df479499af731590ec27051736b64b96509 Mon Sep 17 00:00:00 2001 From: Chisel Date: Tue, 3 Feb 2026 17:18:09 +0000 Subject: [PATCH 16/21] feat: hide skipped presets when they're not the current day --- script.js | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/script.js b/script.js index 69a9825..bd89c58 100644 --- a/script.js +++ b/script.js @@ -358,6 +358,8 @@ let isFirstLoad = false; let currentDay = null; /** Day number → preset index used for that day. Only presets we skipped get 💀; used presets keep "Day N"; rest renumbered from current. */ let usedPresetByDay = {}; +/** Preset day indices permanently hidden when their day ended (countdown reached zero). Never shown again until new game. */ +let hiddenPresetDays = []; let currentPace = 'normal'; // Default pace let playMusic = false; // Default to false for new users let playMusicAtNight = false; // Default to false for new users @@ -1098,6 +1100,9 @@ function applyParsedSettings(settings) { } else { usedPresetByDay = {}; } + hiddenPresetDays = Array.isArray(settings.hiddenPresetDays) + ? settings.hiddenPresetDays + : []; currentPace = settings.currentPace || 'normal'; playMusic = settings.playMusic === undefined ? false : settings.playMusic; playMusicAtNight = @@ -1339,6 +1344,7 @@ function saveSettings() { travellerCount, currentDay, usedPresetByDay, + hiddenPresetDays, currentPace, playMusic, playMusicAtNight, @@ -1420,7 +1426,32 @@ function updateClocktowerPresets() { }); } - presets.forEach((preset) => { + // When the day has ended (dusk), add currently skipped presets to hidden set (they stay hidden for the rest of the game) + const dayInfo = document.querySelector('.day-display'); + const isDusk = dayInfo?.classList.contains('dusk'); + if (isDusk) { + let added = false; + for (const p of presets) { + if ( + getPresetDayLabel(p.day, numberOfDays).effectiveDay === null && + !hiddenPresetDays.includes(p.day) + ) { + hiddenPresetDays.push(p.day); + added = true; + } + } + if (added) saveSettings(); + } + + // Always exclude permanently hidden presets; in dusk also exclude any remaining skipped (effective-only) + let presetsToShow = presets.filter((p) => !hiddenPresetDays.includes(p.day)); + if (isDusk) { + presetsToShow = presetsToShow.filter( + (p) => getPresetDayLabel(p.day, numberOfDays).effectiveDay !== null + ); + } + + presetsToShow.forEach((preset) => { const { label: dayLabel, effectiveDay: eff } = getPresetDayLabel( preset.day, numberOfDays @@ -1460,6 +1491,8 @@ function updateClocktowerPresets() { usedPresetByDay[currentDay + 1] = preset.day; currentDay++; saveSettings(); + // Clear dusk before rebuilding presets so skips stay visible for the new day until its countdown ends + updateDayDisplay(); updateClocktowerPresets(); } @@ -1821,6 +1854,7 @@ function accelerateTime() { } if (currentDay !== null) { updateDayDisplay('dusk'); + updateClocktowerPresets(); } updateDisplay(); // Make sure to update display one final time @@ -2120,6 +2154,7 @@ function startCountdown() { } if (currentDay !== null) { updateDayDisplay('dusk'); + updateClocktowerPresets(); } updateDisplay(); @@ -2141,6 +2176,7 @@ function startNewGame() { // Set to Day 1 currentDay = 1; usedPresetByDay = {}; + hiddenPresetDays = []; updateDayDisplay(); // Set button to Wake Up state From ac152e67900ce7be07a0c513338bab150a446665 Mon Sep 17 00:00:00 2001 From: Chisel Date: Tue, 3 Feb 2026 22:56:21 +0000 Subject: [PATCH 17/21] fix: refine behaviour for hiding skipped presets --- script.js | 50 +++++++++++++++++++++++--------------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/script.js b/script.js index bd89c58..35a4986 100644 --- a/script.js +++ b/script.js @@ -360,6 +360,8 @@ let currentDay = null; let usedPresetByDay = {}; /** Preset day indices permanently hidden when their day ended (countdown reached zero). Never shown again until new game. */ let hiddenPresetDays = []; +/** True only when the timer has run out this day (dusk); used so we show compact preset list only then, not when e.g. clicking a preset. */ +let isDuskPresetView = false; let currentPace = 'normal'; // Default pace let playMusic = false; // Default to false for new users let playMusicAtNight = false; // Default to false for new users @@ -1410,7 +1412,7 @@ function updateClocktowerPresets() { const presets = generateDayPresets(playerCount); const numberOfDays = presets.length; - // Count skipped presets and append a suitable-timed extra preset for each (so we don't run out) + // Count skipped presets and append that many extras at the end (so we don't run out). let skippedCount = 0; for (const preset of presets) { if (getPresetDayLabel(preset.day, numberOfDays).effectiveDay === null) @@ -1426,29 +1428,22 @@ function updateClocktowerPresets() { }); } - // When the day has ended (dusk), add currently skipped presets to hidden set (they stay hidden for the rest of the game) - const dayInfo = document.querySelector('.day-display'); - const isDusk = dayInfo?.classList.contains('dusk'); - if (isDusk) { - let added = false; - for (const p of presets) { - if ( - getPresetDayLabel(p.day, numberOfDays).effectiveDay === null && - !hiddenPresetDays.includes(p.day) - ) { - hiddenPresetDays.push(p.day); - added = true; - } - } - if (added) saveSettings(); - } - - // Always exclude permanently hidden presets; in dusk also exclude any remaining skipped (effective-only) - let presetsToShow = presets.filter((p) => !hiddenPresetDays.includes(p.day)); - if (isDusk) { + // Preset visibility: ONLY about hiding skipped (💀) presets. Never hide "Day N" presets. + // Dusk: hide all skips (compact next-day picker). Day 2+: hide only 💀 before Day N-1's preset. + let presetsToShow = presets; + if (isDuskPresetView) { presetsToShow = presetsToShow.filter( (p) => getPresetDayLabel(p.day, numberOfDays).effectiveDay !== null ); + } else if (currentDay >= 2) { + const skipCutoff = usedPresetByDay[currentDay - 1]; + if (skipCutoff !== undefined) { + presetsToShow = presets.filter((p) => { + const { effectiveDay } = getPresetDayLabel(p.day, numberOfDays); + if (effectiveDay !== null) return true; // always show every Day preset + return p.day >= skipCutoff; // hide only 💀 before Day N-1's preset + }); + } } presetsToShow.forEach((preset) => { @@ -1476,22 +1471,20 @@ function updateClocktowerPresets() { button.dataset.day = preset.day; button.addEventListener('click', (e) => { - const dayInfo = document.querySelector('.day-display'); - const isDusk = dayInfo?.classList.contains('dusk'); const { effectiveDay: eff } = getPresetDayLabel(preset.day, numberOfDays); const isAhead = eff !== null && eff > currentDay; - if (isAhead && !isDusk) { - // Not in dusk: use this preset for current day + if (isAhead && !isDuskPresetView) { + // Not in dusk: use this preset for current day (keeps full preset row including skipped) usedPresetByDay[currentDay] = preset.day; saveSettings(); updateClocktowerPresets(); - } else if (isDusk && currentDay !== null) { + } else if (isDuskPresetView && currentDay !== null) { // In dusk: use this preset for the next day usedPresetByDay[currentDay + 1] = preset.day; currentDay++; saveSettings(); - // Clear dusk before rebuilding presets so skips stay visible for the new day until its countdown ends + // Clear dusk so new day shows full preset row until its countdown ends updateDayDisplay(); updateClocktowerPresets(); } @@ -2212,6 +2205,9 @@ function updateDayDisplay(state = '') { // Remove existing state classes dayInfo.classList.remove('dawn', 'dusk'); + // Single source of truth: compact preset list only when timer has run out (dusk), not when e.g. clicking a preset + isDuskPresetView = state === 'dusk'; + // Ensure currentDay is at least 1 if (currentDay === null || currentDay === undefined) { currentDay = 1; From a45ac068cb93ec600cb5937510221963a43e88ca Mon Sep 17 00:00:00 2001 From: Chisel Date: Wed, 4 Feb 2026 09:51:04 +0000 Subject: [PATCH 18/21] fix: more fixes for the skip preset feature --- script.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/script.js b/script.js index 35a4986..2d83de0 100644 --- a/script.js +++ b/script.js @@ -1412,14 +1412,22 @@ function updateClocktowerPresets() { const presets = generateDayPresets(playerCount); const numberOfDays = presets.length; - // Count skipped presets and append that many extras at the end (so we don't run out). + // Count skipped presets and ensure we have enough extras so we never run out of preset slots. + // We need at least one preset per day; if user skips ahead (e.g. Day 8 for Day 1, then Day 8 for Day 2), + // the max preset index used can exceed the base count, so add extras to cover that. let skippedCount = 0; for (const preset of presets) { if (getPresetDayLabel(preset.day, numberOfDays).effectiveDay === null) skippedCount++; } + const maxUsed = + currentDay !== null && Object.keys(usedPresetByDay).length > 0 + ? Math.max(...Object.values(usedPresetByDay)) + : 0; + const curDay = currentDay ?? 1; + const extrasNeeded = Math.max(skippedCount, Math.max(0, maxUsed - curDay)); const lastBasePreset = presets[numberOfDays - 1]; - for (let i = 1; i <= skippedCount; i++) { + for (let i = 1; i <= extrasNeeded; i++) { presets.push({ minutes: lastBasePreset.minutes, seconds: lastBasePreset.seconds, From d3126f996293ff2c8e3f4f727cca7b51be5fccac Mon Sep 17 00:00:00 2001 From: Chisel Date: Tue, 3 Feb 2026 17:24:59 +0000 Subject: [PATCH 19/21] fix: address new sonarqube issues --- index.html | 18 +++++++++--------- script.js | 39 ++++++++++++++++++++++----------------- styles.css | 14 ++++++++++++++ 3 files changed, 45 insertions(+), 26 deletions(-) diff --git a/index.html b/index.html index db6f974..a697bd3 100644 --- a/index.html +++ b/index.html @@ -133,10 +133,10 @@
-
+ -
Normal
-
+
00:00
- +
-
+
+ Timer controls
-
+
@@ -318,10 +318,10 @@
- + diff --git a/script.js b/script.js index 2d83de0..e708147 100644 --- a/script.js +++ b/script.js @@ -1079,6 +1079,23 @@ function roundToNearestQuarter(n) { return Math.round(n * 4) / 4; } +function resolveUsedPresetByDay(settings, currentDay) { + if ( + settings.usedPresetByDay && + typeof settings.usedPresetByDay === 'object' + ) { + return settings.usedPresetByDay; + } + if (settings.dayOnePresetIndex == null) { + return {}; + } + // Migrate: assume sequential use from dayOnePresetIndex + const result = {}; + for (let d = 1; d <= currentDay; d++) + result[d] = settings.dayOnePresetIndex + d - 1; + return result; +} + // Apply parsed settings object to globals and persist; runs migration and version check function applyParsedSettings(settings) { const normaliseSoundFile = (value, defaultVal) => { @@ -1089,19 +1106,7 @@ function applyParsedSettings(settings) { playerCount = settings.playerCount || 10; travellerCount = settings.travellerCount || 0; currentDay = settings.currentDay || 1; - if ( - settings.usedPresetByDay && - typeof settings.usedPresetByDay === 'object' - ) { - usedPresetByDay = settings.usedPresetByDay; - } else if (settings.dayOnePresetIndex != null) { - // Migrate: assume sequential use from dayOnePresetIndex - usedPresetByDay = {}; - for (let d = 1; d <= currentDay; d++) - usedPresetByDay[d] = settings.dayOnePresetIndex + d - 1; - } else { - usedPresetByDay = {}; - } + usedPresetByDay = resolveUsedPresetByDay(settings, currentDay); hiddenPresetDays = Array.isArray(settings.hiddenPresetDays) ? settings.hiddenPresetDays : []; @@ -1392,11 +1397,11 @@ function getPresetDayLabel(presetDay, numberOfDays) { } // From current day's preset onward (or from first future if in dusk) const anchor = - usedPresetByDay[currentDay] !== undefined - ? usedPresetByDay[currentDay] - : maxUsed + 1; + usedPresetByDay[currentDay] === undefined + ? maxUsed + 1 + : usedPresetByDay[currentDay]; const baseDay = - usedPresetByDay[currentDay] !== undefined ? currentDay : currentDay + 1; + usedPresetByDay[currentDay] === undefined ? currentDay + 1 : currentDay; if (presetDay >= anchor) { const eff = baseDay + (presetDay - anchor); return { label: `Day ${eff}`, effectiveDay: eff }; diff --git a/styles.css b/styles.css index dc6bdb3..0322162 100644 --- a/styles.css +++ b/styles.css @@ -391,6 +391,20 @@ html.fonts-ready .timer-display .time { flex-direction: column; gap: 0.5rem; margin-top: 0; + border: none; + padding: 0; + min-inline-size: 0; +} + +.controls legend { + padding: 0; + float: inline-start; + overflow: hidden; + position: absolute; + width: 1px; + height: 1px; + clip: rect(0, 0, 0, 0); + clip-path: inset(50%); } .time-inputs { From 68d3bbb7b22c00768c06222057254e7d3801bfa2 Mon Sep 17 00:00:00 2001 From: Chisel Date: Wed, 4 Feb 2026 10:48:47 +0000 Subject: [PATCH 20/21] feat: add accelerate time keyboard shortcut --- cypress/e2e/07-keyboard-shortcuts.cy.js | 12 +++++++++--- index.html | 12 ++++++++++++ script.js | 23 +++++++++++++++++++++++ styles.css | 6 +++--- 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/cypress/e2e/07-keyboard-shortcuts.cy.js b/cypress/e2e/07-keyboard-shortcuts.cy.js index 06ed2df..a8271d8 100644 --- a/cypress/e2e/07-keyboard-shortcuts.cy.js +++ b/cypress/e2e/07-keyboard-shortcuts.cy.js @@ -21,6 +21,7 @@ describe('Keyboard Shortcuts', () => { cy.get('#shortcutSettings').should('exist'); cy.get('#shortcutWakeUp').should('exist'); cy.get('#shortcutReset').should('exist'); + cy.get('#shortcutAccelerate').should('exist'); cy.get('#shortcutFullscreen').should('exist'); cy.get('#shortcutInfo').should('exist'); @@ -28,6 +29,7 @@ describe('Keyboard Shortcuts', () => { cy.get('#shortcutSettings').should('have.value', 'q'); cy.get('#shortcutWakeUp').should('have.value', 'Space'); cy.get('#shortcutReset').should('have.value', 'r'); + cy.get('#shortcutAccelerate').should('have.value', 'a'); cy.get('#shortcutFullscreen').should('have.value', 'f'); cy.get('#shortcutInfo').should('have.value', 'i'); @@ -53,6 +55,7 @@ describe('Keyboard Shortcuts', () => { // Verify other inputs are disabled during recording cy.get('#shortcutSettings').should('be.disabled'); cy.get('#shortcutReset').should('be.disabled'); + cy.get('#shortcutAccelerate').should('be.disabled'); cy.get('#shortcutFullscreen').should('be.disabled'); cy.get('#shortcutInfo').should('be.disabled'); @@ -66,6 +69,7 @@ describe('Keyboard Shortcuts', () => { // Verify other inputs are re-enabled cy.get('#shortcutSettings').should('not.be.disabled'); cy.get('#shortcutReset').should('not.be.disabled'); + cy.get('#shortcutAccelerate').should('not.be.disabled'); cy.get('#shortcutFullscreen').should('not.be.disabled'); cy.get('#shortcutInfo').should('not.be.disabled'); }); @@ -161,6 +165,7 @@ describe('Keyboard Shortcuts', () => { cy.get('#shortcutSettings').should('have.value', 'q'); cy.get('#shortcutWakeUp').should('have.value', 'Space'); cy.get('#shortcutReset').should('have.value', 'r'); + cy.get('#shortcutAccelerate').should('have.value', 'a'); cy.get('#shortcutFullscreen').should('have.value', 'f'); cy.get('#shortcutInfo').should('have.value', 'i'); }); @@ -182,6 +187,7 @@ describe('Keyboard Shortcuts', () => { cy.get('#shortcutSettings').should('have.value', ''); cy.get('#shortcutWakeUp').should('have.value', ''); cy.get('#shortcutReset').should('have.value', ''); + cy.get('#shortcutAccelerate').should('have.value', ''); cy.get('#shortcutFullscreen').should('have.value', ''); cy.get('#shortcutInfo').should('have.value', ''); }); @@ -245,10 +251,10 @@ describe('Keyboard Shortcuts', () => { cy.get('.shortcuts-container').should('have.css', 'grid-template-columns'); // Verify all shortcut items are present - cy.get('.shortcut-item').should('have.length', 5); + cy.get('.shortcut-item').should('have.length', 6); - // Verify the 5th item spans both columns - cy.get('.shortcut-item:nth-child(5)').should( + // Verify the 6th item spans both columns + cy.get('.shortcut-item:nth-child(6)').should( 'have.css', 'grid-column', '1 / -1' diff --git a/index.html b/index.html index a697bd3..0178a36 100644 --- a/index.html +++ b/index.html @@ -653,6 +653,18 @@

Keyboard Shortcuts

/>
+
+ +
diff --git a/script.js b/script.js index bf2d5fe..3a85a6f 100644 --- a/script.js +++ b/script.js @@ -234,8 +234,9 @@ const keyboardShortcutsUtils = { // Update the UI immediately keyboardShortcutsUtils.loadShortcuts(); - // Update wake up shortcut hint + // Update shortcut hints on buttons updateWakeUpShortcutHint(); + updateAccelerateShortcutHint(); // Save the clean defaults to localStorage saveSettings(); @@ -262,6 +263,8 @@ const keyboardShortcutsUtils = { // Update the UI immediately keyboardShortcutsUtils.loadShortcuts(); + updateWakeUpShortcutHint(); + updateAccelerateShortcutHint(); // Save the clean defaults to localStorage saveSettings(); @@ -286,8 +289,9 @@ const keyboardShortcutsUtils = { // Update the UI immediately keyboardShortcutsUtils.loadShortcuts(); - // Update wake up shortcut hint + // Update shortcut hints on buttons updateWakeUpShortcutHint(); + updateAccelerateShortcutHint(); // Save the cleared shortcuts to localStorage saveSettings(); @@ -501,10 +505,13 @@ function startShortcutRecording(input, action) { // Re-enable other shortcut inputs reEnableShortcutInputs(); - // Update wake up shortcut hint if wakeUp was changed + // Update shortcut hints if changed if (action === 'wakeUp') { updateWakeUpShortcutHint(); } + if (action === 'accelerate') { + updateAccelerateShortcutHint(); + } saveSettings(); document.removeEventListener('keydown', handleKeyDown); @@ -546,6 +553,31 @@ function updateWakeUpShortcutHint() { } } +// Helper function to update accelerate shortcut hint +function updateAccelerateShortcutHint() { + const hintElement = document.getElementById('accelerateShortcutHint'); + if (!hintElement) return; + + const shortcutKey = keyboardShortcuts.accelerate; + + if (!shortcutKey) { + hintElement.textContent = ''; + return; + } + + hintElement.textContent = shortcutKey.toUpperCase(); +} + +// Helper to set only the accelerate button label (preserves shortcut hint) +function setAccelerateButtonLabel(text) { + const textEl = accelerateBtn?.querySelector('.button-text'); + if (textEl) textEl.textContent = text; +} + +function getAccelerateButtonLabel() { + return accelerateBtn?.querySelector('.button-text')?.textContent ?? ''; +} + // Helper function to update start button text (preserving the shortcut hint) function updateStartButtonText(text) { const buttonTextElement = startBtn.querySelector('.button-text'); @@ -780,8 +812,8 @@ document.addEventListener('DOMContentLoaded', async () => { // Accelerate button: click -> "Confirm…", second click within 5s triggers acceleration accelerateBtn.addEventListener('click', () => { if (accelerateBtn.disabled) return; - if (accelerateBtn.textContent === BUTTON_LABELS.ACCELERATE) { - accelerateBtn.textContent = BUTTON_LABELS.ACCELERATE_CONFIRM; + if (getAccelerateButtonLabel() === BUTTON_LABELS.ACCELERATE) { + setAccelerateButtonLabel(BUTTON_LABELS.ACCELERATE_CONFIRM); accelerateBtn.setAttribute( 'aria-label', 'Click again to accelerate time' @@ -797,7 +829,7 @@ document.addEventListener('DOMContentLoaded', async () => { clearTimeout(accelerateConfirmTimeout); accelerateConfirmTimeout = null; } - accelerateBtn.textContent = BUTTON_LABELS.ACCELERATE_TIME_FLIES; + setAccelerateButtonLabel(BUTTON_LABELS.ACCELERATE_TIME_FLIES); accelerateBtn.disabled = true; accelerateTime(); } @@ -1065,8 +1097,9 @@ document.addEventListener('DOMContentLoaded', async () => { // Load keyboard shortcuts into UI keyboardShortcutsUtils.loadShortcuts(); - // Update wake up shortcut hint + // Update shortcut hints on buttons updateWakeUpShortcutHint(); + updateAccelerateShortcutHint(); // Setup keyboard navigation after shortcuts are loaded setupKeyboardNavigation(); @@ -1803,7 +1836,7 @@ function updateDisplay() { startBtn.disabled = false; // Keep accelerate disabled during accelerated countdown ("time flies") accelerateBtn.disabled = - accelerateBtn.textContent === BUTTON_LABELS.ACCELERATE_TIME_FLIES; + getAccelerateButtonLabel() === BUTTON_LABELS.ACCELERATE_TIME_FLIES; resetBtn.disabled = false; // Enable reset while running } else if (timeLeft > 0 && !hasReset) { updateStartButtonText(BUTTON_LABELS.RESUME); @@ -1831,7 +1864,7 @@ function resetAccelerateButton() { clearTimeout(accelerateConfirmTimeout); accelerateConfirmTimeout = null; } - accelerateBtn.textContent = BUTTON_LABELS.ACCELERATE; + setAccelerateButtonLabel(BUTTON_LABELS.ACCELERATE); accelerateBtn.setAttribute( 'aria-label', 'Accelerate time (click then confirm)' diff --git a/styles.css b/styles.css index 3d0132c..486c9cd 100644 --- a/styles.css +++ b/styles.css @@ -572,6 +572,13 @@ html.fonts-ready .timer-display .time { --progress-width: 0%; } +#accelerateBtn .button-content { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.5rem; +} + #accelerateBtn:focus-visible { outline: none; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2), 0 0 0 3px rgba(76, 175, 80, 0.4);