Skip to main content
Glama
timer.html21.9 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Timer</title> <style> :root { /* Colors */ --color-primary: #4285f4; --color-primary-hover: #3367d6; --color-secondary: #34a853; --color-secondary-hover: #2d9c47; --color-danger: #ea4335; --color-background: #ffffff; --color-surface: #ffffff; --color-surface-variant: #f0f2f5; --color-surface-hover: #e0e4e8; --color-outline: #e8eaed; --color-text-primary: #333; --color-text-secondary: #666; /* Typography */ --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; --font-size-xs: 0.875rem; --font-size-sm: 1rem; --font-size-md: 1.125rem; --font-size-lg: 2.25rem; --font-weight-normal: 300; --font-weight-medium: 500; /* Spacing */ --space-xs: 0.5rem; --space-sm: 0.75rem; --space-md: 1rem; --space-lg: 1.25rem; --space-xl: 2rem; /* Border radius */ --radius-sm: 0.5rem; --radius-md: 0.75rem; --radius-lg: 1.375rem; /* Component sizes */ --timer-circle-size: 12.5rem; --timer-circle-stroke: 0.5rem; --timer-circle-radius: 5.625rem; --control-btn-size: 2.5rem; --main-btn-width: 8.75rem; --main-btn-height: 2.8125rem; --time-input-width: 3.75rem; --time-input-height: 2.5rem; /* Transitions */ --transition-fast: 0.1s ease; --transition-normal: 0.2s ease; } * { margin: 0; padding: 0; } body { font-family: var(--font-family); background: var(--color-background); min-height: 100vh; } .timer-container { display: flex; align-items: center; gap: var(--space-xl); padding: var(--space-xl); } .timer-left { display: flex; flex-direction: column; align-items: center; } .timer-right { display: flex; flex-direction: column; gap: var(--space-lg); min-width: 12.5rem; } .timer-header { position: absolute; top: var(--space-xs); left: var(--space-xs); justify-content: flex-start; align-items: center; margin-bottom: var(--space-md); } .controls-top { display: flex; gap: var(--space-sm); } .control-btn { width: var(--control-btn-size); height: var(--control-btn-size); border: none; background: var(--color-surface-variant); border-radius: var(--radius-md); cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: var(--font-size-md); transition: all var(--transition-normal); } .control-btn:hover { background: var(--color-surface-hover); } .control-btn.muted { background: var(--color-danger); color: var(--color-surface); } .mute-icon { color: var(--color-text-secondary); transition: color var(--transition-normal); } .control-btn:hover .mute-icon { color: var(--color-text-primary); } .control-btn.muted .mute-icon { color: var(--color-surface); } .timer-circle { overflow: visible; position: relative; width: var(--timer-circle-size); height: var(--timer-circle-size); margin: 0; } .progress-ring { transform: rotate(-90deg); width: 100%; height: 100%; overflow: visible; } .progress-ring-bg { fill: none; stroke: var(--color-outline); stroke-width: var(--timer-circle-stroke); } .progress-ring-progress { fill: none; stroke: var(--color-primary); stroke-width: var(--timer-circle-stroke); stroke-linecap: round; stroke-dasharray: 0 879.646; transition: stroke-dasharray var(--transition-fast); } .timer-display { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: var(--font-size-lg); font-weight: var(--font-weight-normal); color: var(--color-text-primary); letter-spacing: 0.125rem; background: transparent; border: none; text-align: center; width: 9.375rem; outline: none; cursor: pointer; } .timer-display:not([readonly]) { border: 0.125rem dashed var(--color-primary); border-radius: var(--radius-sm); cursor: text; } .timer-controls { display: flex; flex-direction: column; gap: var(--space-md); } .main-control-btn { width: var(--main-btn-width); height: var(--main-btn-height); border: none; border-radius: var(--radius-lg); cursor: pointer; font-size: var(--font-size-sm); font-weight: var(--font-weight-medium); transition: all var(--transition-normal); display: flex; align-items: center; justify-content: center; gap: var(--space-xs); } .play-pause-btn { background: var(--color-primary); color: var(--color-surface); } .play-pause-btn:hover { background: var(--color-primary-hover); } .reset-btn { background: var(--color-surface-variant); color: var(--color-text-primary); } .reset-btn:hover { background: var(--color-surface-hover); } .time-config { display: flex; gap: var(--space-sm); justify-content: flex-start; align-items: center; flex-wrap: wrap; } .time-input { width: var(--time-input-width); height: var(--time-input-height); border: 0.125rem solid var(--color-outline); border-radius: var(--radius-sm); text-align: center; font-size: var(--font-size-sm); font-weight: var(--font-weight-medium); } .time-input:focus { outline: none; border-color: var(--color-primary); } .time-label { font-size: var(--font-size-xs); color: var(--color-text-secondary); } .set-btn { padding: var(--space-xs) var(--space-md); border: none; background: var(--color-secondary); color: var(--color-surface); border-radius: var(--radius-sm); cursor: pointer; font-size: var(--font-size-xs); font-weight: var(--font-weight-medium); } .set-btn:hover { background: var(--color-secondary-hover); } .feedback-text { margin-top: var(--space-lg); color: var(--color-text-secondary); font-size: var(--font-size-xs); cursor: pointer; text-decoration: none; } .feedback-text:hover { color: var(--color-primary); } </style> </head> <body> <script> const ro = new ResizeObserver( ( entries ) => { for ( const entry of entries ) { window.parent.postMessage( { type: "ui-size-change", payload: { height: entry.contentRect.height } }, "*" ); } } ); ro.observe( document.documentElement ); </script> <div class="timer-container"> <div class="timer-left"> <div class="timer-circle"> <svg class="progress-ring" viewBox="0 0 200 200"> <circle class="progress-ring-bg" cx="100" cy="100" r="90"></circle> <circle class="progress-ring-progress" cx="100" cy="100" r="90"></circle> </svg> <input type="text" class="timer-display" value="" readonly> </div> </div> <div class="timer-right"> <div class="timer-header"> <div class="controls-top"> <button class="control-btn mute-btn" title="Toggle sound"> <svg class="mute-icon unmuted" width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M11 5L6 9H2V15H6L11 19V5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <path d="M19.07 4.93C20.9437 6.80369 21.9991 9.34785 21.9991 12C21.9991 14.6522 20.9437 17.1963 19.07 19.07M15.54 8.46C16.4774 9.39764 17.0039 10.6692 17.0039 12C17.0039 13.3308 16.4774 14.6024 15.54 15.54" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> </svg> <svg class="mute-icon muted" width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="display: none;"> <path d="M11 5L6 9H2V15H6L11 19V5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <line x1="23" y1="9" x2="17" y2="15" stroke="currentColor" stroke-width="2" stroke-linecap="round" /> <line x1="17" y1="9" x2="23" y2="15" stroke="currentColor" stroke-width="2" stroke-linecap="round" /> </svg> </button> </div> </div> <div class="timer-controls"> <button class="main-control-btn play-pause-btn"> <span class="btn-icon">▶</span> <span class="btn-text">Start</span> </button> <button class="main-control-btn reset-btn"> <span class="btn-icon">↻</span> <span class="btn-text">Reset</span> </button> </div> </div> </div> <script> const TEMPLATE_CONFIG = { "duration": 20, "mode": "timer" }; const TIMER_CONFIG = { defaultInitialTime: TEMPLATE_CONFIG.duration || 20, soundEnabled: true, progressRingRadius: 90, mode: TEMPLATE_CONFIG.mode || "timer" }; class Timer { constructor() { this.mode = TIMER_CONFIG.mode; this.initialTime = this.mode === 'stopwatch' ? 0 : TIMER_CONFIG.defaultInitialTime; this.totalTime = this.initialTime; this.currentTime = this.initialTime; this.currentMilliseconds = 0; this.lastUpdateTime = null; this.isRunning = false; this.isMuted = false; this.interval = null; this.initializeElements(); this.bindEvents(); this.updateDisplay(); this.updateMuteButtonVisibility(); } initializeElements() { this.display = document.querySelector( '.timer-display' ); this.playPauseBtn = document.querySelector( '.play-pause-btn' ); this.resetBtn = document.querySelector( '.reset-btn' ); this.muteBtn = document.querySelector( '.mute-btn' ); this.minutesInput = document.getElementById( 'minutes' ); this.secondsInput = document.getElementById( 'seconds' ); this.progressRing = document.querySelector( '.progress-ring-progress' ); this.circumference = 2 * Math.PI * TIMER_CONFIG.progressRingRadius; this.progressRing.style.strokeDasharray = `0 ${this.circumference}`; } bindEvents() { this.playPauseBtn.addEventListener( 'click', () => this.toggleTimer() ); this.resetBtn.addEventListener( 'click', () => this.resetTimer() ); this.muteBtn.addEventListener( 'click', () => this.toggleMute() ); this.display.addEventListener( 'click', () => this.makeEditable() ); this.display.addEventListener( 'blur', () => this.finishEditing() ); this.display.addEventListener( 'keydown', ( e ) => this.handleTimerInput( e ) ); this.display.addEventListener( 'input', ( e ) => this.handleTimerInputChange( e ) ); document.addEventListener( 'keydown', ( e ) => { if ( e.code === 'Space' ) { e.preventDefault(); this.toggleTimer(); } else if ( e.code === 'KeyR' ) { this.resetTimer(); } else if ( e.code === 'KeyM' ) { this.toggleMute(); } } ); } setTime( minutes, seconds ) { if ( this.mode === 'stopwatch' ) { return; } this.initialTime = minutes * 60 + seconds; this.totalTime = this.initialTime; this.currentTime = this.initialTime; this.currentMilliseconds = 0; this.updateDisplay(); this.updateProgress(); } setCustomTime() { const minutes = parseInt( this.minutesInput.value ) || 0; const seconds = parseInt( this.secondsInput.value ) || 0; this.setTime( minutes, seconds ); } toggleTimer() { if ( this.isRunning ) { this.pauseTimer(); } else { this.startTimer(); } } startTimer() { this.isRunning = true; this.lastUpdateTime = Date.now(); this.updatePlayPauseButton(); this.interval = setInterval( () => { const now = Date.now(); const deltaTime = now - this.lastUpdateTime; this.lastUpdateTime = now; this.currentMilliseconds += deltaTime; if ( this.currentMilliseconds >= 1000 ) { const secondsElapsed = Math.floor( this.currentMilliseconds / 1000 ); this.currentMilliseconds = this.currentMilliseconds % 1000; if ( this.mode === 'stopwatch' ) { this.currentTime += secondsElapsed; } else { this.currentTime -= secondsElapsed; if ( this.currentTime <= 0 ) { this.currentTime = 0; this.currentMilliseconds = 0; this.updateDisplay(); this.updateProgress(); this.completeTimer(); return; } } this.updateDisplay(); } this.updateProgress(); }, 50 ); } pauseTimer() { this.isRunning = false; clearInterval( this.interval ); this.updatePlayPauseButton(); } resetTimer() { this.pauseTimer(); this.currentTime = this.initialTime; this.currentMilliseconds = 0; this.updateDisplay(); this.updateProgress(); } async completeTimer() { if ( this.mode !== 'timer' ) return; this.pauseTimer(); for ( let i = 0; i < 4; i++ ) { await this.playSound(); this.progressRing.style.stroke = '#ea4335'; await new Promise( resolve => setTimeout( resolve, 1000 ) ); this.progressRing.style.stroke = '#4285f4'; } } updateDisplay() { const minutes = Math.floor( Math.abs( this.currentTime ) / 60 ); const seconds = Math.abs( this.currentTime ) % 60; this.display.value = `${minutes.toString().padStart( 2, '0' )}:${seconds.toString().padStart( 2, '0' )}`; console.log( this.display.value ); } updateProgress() { let progress = 0; if ( this.mode === 'stopwatch' ) { const fractionalTime = this.currentTime + ( this.currentMilliseconds / 1000 ); const secondsInMinute = fractionalTime % 60; progress = secondsInMinute / 60; } else { const fractionalTimeRemaining = Math.max( 0, this.currentTime - ( this.currentMilliseconds / 1000 ) ); progress = this.initialTime > 0 ? ( this.initialTime - fractionalTimeRemaining ) / this.initialTime : 0; progress = Math.max( 0, Math.min( 1, progress ) ); } const offset = this.circumference * progress; this.progressRing.style.strokeDasharray = `${offset} ${this.circumference}`; } updatePlayPauseButton() { const icon = this.playPauseBtn.querySelector( '.btn-icon' ); const text = this.playPauseBtn.querySelector( '.btn-text' ); if ( this.isRunning ) { icon.textContent = '⏸'; text.textContent = 'Pause'; } else { icon.textContent = '▶'; text.textContent = 'Start'; } } toggleMute() { this.isMuted = !this.isMuted; const unmutedIcon = this.muteBtn.querySelector( '.mute-icon.unmuted' ); const mutedIcon = this.muteBtn.querySelector( '.mute-icon.muted' ); if ( this.isMuted ) { unmutedIcon.style.display = 'none'; mutedIcon.style.display = 'block'; } else { unmutedIcon.style.display = 'block'; mutedIcon.style.display = 'none'; } this.muteBtn.classList.toggle( 'muted', this.isMuted ); } updateMuteButtonVisibility() { if ( this.mode === 'stopwatch' ) { this.muteBtn.style.display = 'none'; } else { this.muteBtn.style.display = 'flex'; } } makeEditable() { if ( this.mode === 'stopwatch' ) { return; } this.wasRunningBeforeEdit = this.isRunning; if ( this.isRunning ) { this.pauseTimer(); } this.display.removeAttribute( 'readonly' ); this.display.focus(); this.display.select(); this.editingDigits = this.display.value.replace( ':', '' ).padStart( 4, '0' ); } handleTimerInput( e ) { if ( this.display.hasAttribute( 'readonly' ) ) return; if ( e.key === 'Enter' ) { this.display.blur(); return; } if ( !/^\d$/.test( e.key ) && !['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Escape'].includes( e.key ) ) { e.preventDefault(); return; } if ( /^\d$/.test( e.key ) ) { e.preventDefault(); this.addDigit( e.key ); } else if ( e.key === 'Backspace' ) { e.preventDefault(); this.removeDigit(); } else if ( e.key === 'Escape' ) { this.updateDisplay(); this.display.blur(); } } handleTimerInputChange( e ) { e.preventDefault(); } addDigit( digit ) { this.editingDigits = this.editingDigits.slice( 1 ) + digit; this.updateEditingDisplay(); } removeDigit() { this.editingDigits = '0' + this.editingDigits.slice( 0, 3 ); this.updateEditingDisplay(); } updateEditingDisplay() { const minutes = this.editingDigits.slice( 0, 2 ); const seconds = this.editingDigits.slice( 2, 4 ); this.display.value = `${minutes}:${seconds}`; } finishEditing() { this.display.setAttribute( 'readonly', 'true' ); // Parse the entered time const timeStr = this.display.value; const timeRegex = /^(\d{1,2}):(\d{2})$/; const match = timeStr.match( timeRegex ); if ( match ) { const minutes = parseInt( match[1] ); const seconds = parseInt( match[2] ); if ( minutes >= 0 && minutes <= 99 && seconds >= 0 && seconds <= 59 ) { this.setTime( minutes, seconds ); // Resume timer if it was running before editing if ( this.wasRunningBeforeEdit && this.currentTime > 0 ) { this.startTimer(); } return; } } // Invalid format, revert to current time this.updateDisplay(); // Resume timer if it was running before editing if ( this.wasRunningBeforeEdit && this.currentTime > 0 ) { this.startTimer(); } } async playSound() { if ( this.isMuted || this.mode === 'stopwatch' ) return; const AudioCtx = window.AudioContext || window.webkitAudioContext; const audioContext = new AudioCtx(); if ( audioContext.state === "suspended" ) { try { await audioContext.resume(); } catch { } } const now = audioContext.currentTime; const duration = 0.5; // total beep length // --- Nodes --- const envGain = audioContext.createGain(); // envelope gain (pre-compressor) const compressor = audioContext.createDynamicsCompressor(); const postGain = audioContext.createGain(); // small makeup gain (post-compressor) // Gentler compression (allows a touch more loudness) compressor.threshold.setValueAtTime( -20, now ); // was -24 compressor.knee.setValueAtTime( 30, now ); compressor.ratio.setValueAtTime( 8, now ); // was 12 compressor.attack.setValueAtTime( 0.003, now ); compressor.release.setValueAtTime( 0.25, now ); // Routing: oscillators -> envGain -> compressor -> postGain -> destination envGain.connect( compressor ); compressor.connect( postGain ); postGain.connect( audioContext.destination ); // Small overall boost (~+1.2 dB) postGain.gain.setValueAtTime( 1.15, now ); // Envelope: slightly higher peak, same shape envGain.gain.setValueAtTime( 0.0001, now ); envGain.gain.linearRampToValueAtTime( 1.1, now + 0.015 ); // was 1.0 envGain.gain.setValueAtTime( 1.1, now + 0.015 ); envGain.gain.linearRampToValueAtTime( 0.001, now + duration ); // Layered oscillators for fullness const detunesInCents = [0, +6, -6]; const oscillators = detunesInCents.map( ( cents ) => { const osc = audioContext.createOscillator(); osc.type = "sine"; osc.frequency.setValueAtTime( 800, now ); osc.frequency.setValueAtTime( 600, now + 0.1 ); osc.frequency.setValueAtTime( 800, now + 0.2 ); osc.detune.setValueAtTime( cents, now ); osc.connect( envGain ); return osc; } ); oscillators.forEach( ( o ) => { o.start( now ); o.stop( now + duration ); } ); } } // Initialize timer when page loads document.addEventListener( 'DOMContentLoaded', () => { new Timer(); } ); </script> </body> </html>

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ref-tools/widget-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server