Untitled
unknown
plain_text
10 months ago
32 kB
12
Indexable
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Production Line Timer - Sprues & Males</title>
<style>
/* CSS Variables for theming */
:root {
--sprues-color: #2E8B57;
--males-color: #DC143C;
--background: #1a1a1a;
--surface: rgba(40, 40, 40, 0.95);
--text-primary: #ffffff;
--text-secondary: #cccccc;
--success: #27ae60;
--warning: #f39c12;
--danger: #e74c3c;
--shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
--shadow-light: 0 4px 16px rgba(0, 0, 0, 0.2);
}
```
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: var(--background);
color: var(--text-primary);
overflow: hidden;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
cursor: none;
}
body.show-cursor {
cursor: default;
}
.container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
padding: 2rem;
}
.main-display {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}
.main-display.shift-transition {
transform: scale(1.02);
}
.status-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 8px;
z-index: 100;
transition: background-color 0.5s ease;
}
.status-bar.sprues {
background: var(--sprues-color);
}
.status-bar.males {
background: var(--males-color);
}
.current-shift {
font-size: clamp(8rem, 20vw, 24rem);
font-weight: 900;
margin-bottom: 2rem;
transition: all 0.8s ease;
text-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
letter-spacing: -0.02em;
line-height: 0.9;
}
.current-shift.sprues {
color: var(--sprues-color);
}
.current-shift.males {
color: var(--males-color);
}
.time-display {
font-size: clamp(6rem, 15vw, 18rem);
font-weight: 800;
font-variant-numeric: tabular-nums;
margin-bottom: 3rem;
text-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
letter-spacing: -0.02em;
}
.progress-container {
position: relative;
width: clamp(300px, 40vw, 600px);
height: clamp(300px, 40vw, 600px);
margin: 2rem auto;
}
.progress-ring {
width: 100%;
height: 100%;
transform: rotate(-90deg);
}
.progress-ring-bg {
fill: none;
stroke: rgba(255, 255, 255, 0.1);
stroke-width: 12;
}
.progress-ring-progress {
fill: none;
stroke: var(--sprues-color);
stroke-width: 12;
stroke-linecap: round;
transition: stroke 0.5s ease, stroke-dasharray 0.3s ease;
filter: drop-shadow(0 0 10px currentColor);
}
.next-indicator {
position: fixed;
bottom: 4rem;
left: 50%;
transform: translateX(-50%);
font-size: clamp(1.5rem, 4vw, 3rem);
color: var(--text-secondary);
font-weight: 600;
opacity: 0.8;
}
.settings-trigger {
position: fixed;
top: 2rem;
right: 2rem;
width: 60px;
height: 60px;
background: transparent;
border: none;
cursor: pointer;
opacity: 0;
transition: opacity 0.3s ease;
z-index: 200;
}
.settings-trigger:hover,
.settings-trigger:focus {
opacity: 0.7;
}
.settings-trigger::before {
content: '⚙️';
font-size: 2rem;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(10px);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.show {
display: flex;
}
.modal-content {
background: var(--surface);
border-radius: 16px;
padding: 3rem;
width: 90%;
max-width: 600px;
box-shadow: var(--shadow);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 3rem;
}
.modal-title {
font-size: 2rem;
font-weight: 700;
color: var(--text-primary);
}
.close-btn {
background: none;
border: none;
font-size: 2rem;
cursor: pointer;
padding: 0.5rem;
border-radius: 50%;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
transition: all 0.2s ease;
}
.close-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--text-primary);
}
.controls-section {
margin-bottom: 3rem;
padding-bottom: 2rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.controls-title {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 2rem;
color: var(--text-primary);
}
.controls {
display: flex;
gap: 1rem;
flex-wrap: wrap;
justify-content: center;
margin-top: 2rem;
}
.btn {
padding: 1rem 2rem;
border: none;
border-radius: 12px;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
min-width: 120px;
position: relative;
overflow: hidden;
backdrop-filter: blur(10px);
}
.btn:focus {
outline: 3px solid var(--primary-color);
outline-offset: 2px;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background: var(--primary-color);
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #357abd;
transform: translateY(-2px);
box-shadow: var(--shadow);
}
.btn-secondary {
background: var(--surface);
color: var(--text-primary);
border: 2px solid var(--primary-color);
}
.btn-secondary:hover:not(:disabled) {
background: var(--primary-color);
color: white;
transform: translateY(-2px);
}
.form-group {
margin-bottom: 2rem;
}
.form-label {
display: block;
margin-bottom: 1rem;
font-weight: 600;
font-size: 1.2rem;
color: var(--text-primary);
}
.time-inputs {
display: flex;
gap: 1rem;
align-items: center;
font-size: 1.1rem;
}
.form-input {
padding: 1rem;
border: 2px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
font-size: 1.2rem;
width: 100px;
text-align: center;
background: rgba(255, 255, 255, 0.1);
color: var(--text-primary);
}
.form-input:focus {
outline: none;
border-color: var(--sprues-color);
background: rgba(255, 255, 255, 0.15);
}
.toggle {
display: flex;
align-items: center;
gap: 1rem;
}
.toggle-switch {
position: relative;
width: 60px;
height: 30px;
background: rgba(255, 255, 255, 0.2);
border-radius: 15px;
cursor: pointer;
transition: background 0.3s;
}
.toggle-switch.active {
background: var(--sprues-color);
}
.toggle-slider {
position: absolute;
top: 3px;
left: 3px;
width: 24px;
height: 24px;
background: white;
border-radius: 50%;
transition: transform 0.3s;
}
.toggle-switch.active .toggle-slider {
transform: translateX(30px);
}
.flash-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.3);
display: none;
z-index: 500;
animation: flash 1s ease-out;
}
@keyframes flash {
0% { opacity: 0; }
25% { opacity: 1; }
75% { opacity: 1; }
100% { opacity: 0; }
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Hide cursor after inactivity */
.cursor-timer {
position: fixed;
top: -100px;
opacity: 0;
pointer-events: none;
}
/* Mobile optimizations */
@media (max-width: 768px) {
.container {
padding: 1rem;
}
.current-shift {
font-size: clamp(4rem, 18vw, 12rem);
}
.time-display {
font-size: clamp(3rem, 12vw, 8rem);
}
.next-indicator {
font-size: clamp(1rem, 3vw, 2rem);
bottom: 2rem;
}
.modal-content {
padding: 2rem;
margin: 1rem;
}
.controls {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.progress-container {
width: 250px;
height: 250px;
}
}
/* Large screen optimizations */
@media (min-width: 1920px) {
.current-shift {
font-size: clamp(12rem, 15vw, 20rem);
}
.time-display {
font-size: clamp(8rem, 10vw, 14rem);
}
}
</style>
```
</head>
<body>
<div class="container">
<!-- Status bar at top -->
<div class="status-bar sprues" id="statusBar"></div>
```
<!-- Flash overlay for visual feedback -->
<div class="flash-overlay" id="flashOverlay"></div>
<!-- Hidden settings trigger -->
<button class="settings-trigger" id="settingsBtn" aria-label="Open settings"></button>
<!-- Main timer interface -->
<div class="timer-card" id="timerCard">
<div class="status-indicator">NOW:</div>
<h1 class="current-shift sprues" id="currentShift" aria-live="polite">Sprues</h1>
<div class="progress-container">
<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" id="progressRing" cx="100" cy="100" r="90"
stroke-dasharray="565.48" stroke-dashoffset="565.48"></circle>
</svg>
<div class="time-display" id="timeDisplay" aria-live="polite">5:00</div>
</div>
<div class="next-indicator" id="nextIndicator">Next: Males (in 5:00)</div>
<!-- Screen reader descriptions -->
<div id="startPauseDesc" class="sr-only">Start or pause the timer. Keyboard shortcut: Space</div>
<div id="resetDesc" class="sr-only">Reset the timer to the beginning. Keyboard shortcut: R</div>
<div id="skipDesc" class="sr-only">Skip to the next shift. Keyboard shortcut: S</div>
</div>
<!-- ARIA live region for announcements -->
<div id="announcements" class="sr-only" aria-live="assertive" aria-atomic="true"></div>
</div>
<!-- Settings Modal -->
<div class="modal" id="settingsModal" role="dialog" aria-labelledby="modalTitle" aria-modal="true">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="modalTitle">Production Line Settings</h2>
<button class="close-btn" id="closeModalBtn" aria-label="Close settings">×</button>
</div>
<!-- Timer Controls Section -->
<div class="controls-section">
<h3 class="controls-title">Timer Controls</h3>
<div class="controls">
<button class="btn btn-primary" id="startPauseBtn" aria-describedby="startPauseDesc">Start</button>
<button class="btn btn-secondary" id="resetBtn" aria-describedby="resetDesc">Reset</button>
<button class="btn btn-secondary" id="skipBtn" aria-describedby="skipDesc">Skip</button>
</div>
</div>
<!-- Duration Settings -->
<form id="settingsForm">
<div class="form-group">
<label class="form-label">Sprues Duration</label>
<div class="time-inputs">
<input type="number" class="form-input" id="spruesMinutes" min="0" max="59" value="5" aria-label="Sprues minutes">
<span>min</span>
<input type="number" class="form-input" id="spruesSeconds" min="0" max="59" value="0" aria-label="Sprues seconds">
<span>sec</span>
</div>
</div>
<div class="form-group">
<label class="form-label">Males Duration</label>
<div class="time-inputs">
<input type="number" class="form-input" id="malesMinutes" min="0" max="59" value="2" aria-label="Males minutes">
<span>min</span>
<input type="number" class="form-input" id="malesSeconds" min="0" max="59" value="0" aria-label="Males seconds">
<span>sec</span>
</div>
</div>
<div class="form-group">
<div class="toggle">
<label class="form-label">Visual Flash Alert</label>
<div class="toggle-switch active" id="alertToggle" role="switch" aria-checked="true" aria-label="Toggle visual alerts" tabindex="0">
<div class="toggle-slider"></div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 2rem;">Save Settings</button>
</form>
</div>
</div>
<script>
/**
* Production Line Timer - Traffic System for Plastic Conveyer
* Full-screen display alternating between Sprues and Males shifts
*/
class ProductionTimer {
constructor() {
// Timer state
this.isRunning = false;
this.currentShift = 'sprues'; // 'sprues' or 'males'
this.startTime = null;
this.pausedTime = 0;
this.animationFrame = null;
this.cursorTimeout = null;
// Default durations (in seconds)
this.durations = {
sprues: 300, // 5 minutes
males: 120 // 2 minutes
};
// Settings
this.settings = {
visualAlert: true
};
// DOM elements
this.elements = {
statusBar: document.getElementById('statusBar'),
currentShift: document.getElementById('currentShift'),
timeDisplay: document.getElementById('timeDisplay'),
nextIndicator: document.getElementById('nextIndicator'),
startPauseBtn: document.getElementById('startPauseBtn'),
resetBtn: document.getElementById('resetBtn'),
skipBtn: document.getElementById('skipBtn'),
progressRing: document.getElementById('progressRing'),
timerCard: document.getElementById('timerCard'),
flashOverlay: document.getElementById('flashOverlay'),
announcements: document.getElementById('announcements'),
settingsModal: document.getElementById('settingsModal'),
settingsBtn: document.getElementById('settingsBtn'),
closeModalBtn: document.getElementById('closeModalBtn'),
settingsForm: document.getElementById('settingsForm'),
spruesMinutes: document.getElementById('spruesMinutes'),
spruesSeconds: document.getElementById('spruesSeconds'),
malesMinutes: document.getElementById('malesMinutes'),
malesSeconds: document.getElementById('malesSeconds'),
alertToggle: document.getElementById('alertToggle')
};
// Initialize
this.loadSettings();
this.updateDisplay();
this.bindEvents();
this.updateSettingsForm();
this.initCursorBehavior();
}
/**
* Initialize cursor hiding behavior
*/
initCursorBehavior() {
this.hideCursor();
// Show cursor on mouse movement
document.addEventListener('mousemove', () => {
this.showCursor();
clearTimeout(this.cursorTimeout);
this.cursorTimeout = setTimeout(() => {
if (!this.elements.settingsModal.classList.contains('show')) {
this.hideCursor();
}
}, 3000);
});
}
showCursor() {
document.body.classList.add('show-cursor');
}
hideCursor() {
document.body.classList.remove('show-cursor');
}
/**
* Load settings from localStorage
*/
loadSettings() {
try {
const savedSettings = localStorage.getItem('productionTimerSettings');
if (savedSettings) {
const parsed = JSON.parse(savedSettings);
Object.assign(this.durations, parsed.durations || {});
Object.assign(this.settings, parsed.settings || {});
}
} catch (e) {
console.warn('Failed to load settings from localStorage:', e);
}
}
/**
* Save settings to localStorage
*/
saveSettings() {
try {
const settingsData = {
durations: this.durations,
settings: this.settings
};
localStorage.setItem('productionTimerSettings', JSON.stringify(settingsData));
} catch (e) {
console.warn('Failed to save settings to localStorage:', e);
}
}
/**
* Bind event listeners
*/
bindEvents() {
// Main controls
this.elements.startPauseBtn.addEventListener('click', () => this.toggleTimer());
this.elements.resetBtn.addEventListener('click', () => this.resetTimer());
this.elements.skipBtn.addEventListener('click', () => this.skipShift());
// Settings modal
this.elements.settingsBtn.addEventListener('click', () => this.openSettings());
this.elements.closeModalBtn.addEventListener('click', () => this.closeSettings());
this.elements.settingsForm.addEventListener('submit', (e) => this.saveSettingsForm(e));
// Alert toggle
this.elements.alertToggle.addEventListener('click', () => this.toggleAlert());
this.elements.alertToggle.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.toggleAlert();
}
});
// Modal overlay click to close
this.elements.settingsModal.addEventListener('click', (e) => {
if (e.target === this.elements.settingsModal) {
this.closeSettings();
}
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => this.handleKeyboard(e));
// Handle tab visibility changes for accurate timing
document.addEventListener('visibilitychange', () => {
if (!document.hidden && this.isRunning) {
this.updateTimer();
}
});
// Double click to open settings (for kiosk mode)
this.elements.currentShift.addEventListener('dblclick', () => {
this.openSettings();
});
}
/**
* Handle keyboard shortcuts
*/
handleKeyboard(e) {
// Don't trigger shortcuts when modal is open or typing in inputs
if (this.elements.settingsModal.classList.contains('show') ||
e.target.tagName === 'INPUT') {
return;
}
switch (e.key.toLowerCase()) {
case ' ':
e.preventDefault();
this.toggleTimer();
break;
case 'r':
e.preventDefault();
this.resetTimer();
break;
case 's':
e.preventDefault();
this.skipShift();
break;
case 'escape':
if (this.elements.settingsModal.classList.contains('show')) {
this.closeSettings();
} else {
this.stopTimer();
}
break;
case 'enter':
e.preventDefault();
this.toggleTimer();
break;
}
}
/**
* Toggle timer start/pause
*/
toggleTimer() {
if (this.getDuration(this.currentShift) <= 0) {
this.announce('Cannot start timer: duration must be greater than zero');
return;
}
if (this.isRunning) {
this.pauseTimer();
} else {
this.startTimer();
}
}
/**
* Start the timer
*/
startTimer() {
this.isRunning = true;
this.startTime = performance.now() - this.pausedTime;
this.elements.startPauseBtn.textContent = 'Pause';
this.updateTimer();
this.announce(`${this.currentShift.toUpperCase()} timer started`);
}
/**
* Pause the timer
*/
pauseTimer() {
this.isRunning = false;
if (this.animationFrame) {
cancelAnimationFrame(this.animationFrame);
}
this.elements.startPauseBtn.textContent = 'Start';
this.announce('Timer paused');
}
/**
* Stop the timer completely
*/
stopTimer() {
this.isRunning = false;
this.pausedTime = 0;
this.startTime = null;
if (this.animationFrame) {
cancelAnimationFrame(this.animationFrame);
}
this.elements.startPauseBtn.textContent = 'Start';
this.updateDisplay();
this.announce('Timer stopped');
}
/**
* Reset the timer
*/
resetTimer() {
this.stopTimer();
this.announce('Timer reset');
}
/**
* Skip to next shift
*/
skipShift() {
const wasRunning = this.isRunning;
this.switchShift();
if (wasRunning) {
this.startTimer();
} else {
this.resetTimer();
}
this.announce(`Switched to ${this.currentShift.toUpperCase()}`);
}
/**
* Switch to the next shift
*/
switchShift() {
this.currentShift = this.currentShift === 'sprues' ? 'males' : 'sprues';
this.pausedTime = 0;
this.startTime = performance.now();
// Trigger transition animation
this.elements.timerCard.classList.add('shift-transition');
setTimeout(() => {
this.elements.timerCard.classList.remove('shift-transition');
}, 500);
// Show visual alert if enabled
if (this.settings.visualAlert) {
this.showFlash();
}
this.updateDisplay();
this.announce(`Now showing: ${this.currentShift.toUpperCase()}`);
}
/**
* Show flash overlay for visual feedback
*/
showFlash() {
this.elements.flashOverlay.style.display = 'block';
this.elements.flashOverlay.style.animation = 'none';
// Force reflow
this.elements.flashOverlay.offsetHeight;
this.elements.flashOverlay.style.animation = 'flash 1s ease-out';
setTimeout(() => {
this.elements.flashOverlay.style.display = 'none';
}, 1000);
}
/**
* Update timer - called every frame when running
*/
updateTimer() {
if (!this.isRunning) return;
const elapsed = performance.now() - this.startTime;
this.pausedTime = elapsed;
const duration = this.getDuration(this.currentShift) * 1000; // Convert to ms
const remaining = Math.max(0, duration - elapsed);
if (remaining <= 0) {
// Time's up - switch shifts
this.switchShift();
if (this.isRunning) {
this.startTimer();
}
} else {
this.updateDisplay();
this.animationFrame = requestAnimationFrame(() => this.updateTimer());
}
}
/**
* Update all display elements
*/
updateDisplay() {
const duration = this.getDuration(this.currentShift) * 1000;
const elapsed = this.pausedTime;
const remaining = Math.max(0, duration - elapsed);
// Update current shift display
this.elements.currentShift.textContent = this.capitalize(this.currentShift);
this.elements.currentShift.className = `current-shift ${this.currentShift}`;
// Update time display
const timeString = this.formatTime(Math.ceil(remaining / 1000));
this.elements.timeDisplay.textContent = timeString;
// Update progress ring
this.updateProgressRing(elapsed, duration);
// Update next shift indicator
this.updateNextIndicator();
// Update progress ring color based on current shift
const progressColor = this.currentShift === 'sprues' ?
'var(--sprues-color)' : 'var(--males-color)';
this.elements.progressRing.style.stroke = progressColor;
}
/**
* Update circular progress indicator
*/
updateProgressRing(elapsed, duration) {
const circumference = 2 * Math.PI * 90; // radius = 90
const progress = Math.min(elapsed / duration, 1);
const offset = circumference * (1 - progress);
this.elements.progressRing.style.strokeDasharray = circumference;
this.elements.progressRing.style.strokeDashoffset = offset;
}
/**
* Update next shift indicator
*/
updateNextIndicator() {
const nextShift = this.currentShift === 'sprues' ? 'males' : 'sprues';
const nextDuration = this.getDuration(nextShift);
const nextTimeString = this.formatTime(nextDuration);
this.elements.nextIndicator.textContent =
`Next: ${this.capitalize(nextShift)} (${nextTimeString})`;
}
/**
* Open settings modal
*/
openSettings() {
this.elements.settingsModal.classList.add('show');
this.elements.settingsModal.focus();
this.updateSettingsForm();
this.showCursor();
}
/**
* Close settings modal
*/
closeSettings() {
this.elements.settingsModal.classList.remove('show');
this.elements.settingsBtn.focus();
this.hideCursor();
}
/**
* Update settings form with current values
*/
updateSettingsForm() {
const spruesTotal = this.durations.sprues;
const malesTotal = this.durations.males;
this.elements.spruesMinutes.value = Math.floor(spruesTotal / 60);
this.elements.spruesSeconds.value = spruesTotal % 60;
this.elements.malesMinutes.value = Math.floor(malesTotal / 60);
this.elements.malesSeconds.value = malesTotal % 60;
// Update toggle
this.elements.alertToggle.classList.toggle('active', this.settings.visualAlert);
this.elements.alertToggle.setAttribute('aria-checked', this.settings.visualAlert);
// Update start/pause button text
this.elements.startPauseBtn.textContent = this.isRunning ? 'Pause' : 'Start';
}
/**
* Save settings form
*/
saveSettingsForm(e) {
e.preventDefault();
// Validate and save durations
const spruesMinutes = parseInt(this.elements.spruesMinutes.value) || 0;
const spruesSeconds = parseInt(this.elements.spruesSeconds.value) || 0;
const malesMinutes = parseInt(this.elements.malesMinutes.value) || 0;
const malesSeconds = parseInt(this.elements.malesSeconds.value) || 0;
const spruesTotal = spruesMinutes * 60 + spruesSeconds;
const malesTotal = malesMinutes * 60 + malesSeconds;
// Validation
if (spruesTotal <= 0 || malesTotal <= 0) {
this.announce('Error: Both shift durations must be greater than zero');
return;
}
if (spruesTotal > 3600 || malesTotal > 3600) {
this.announce('Error: Maximum duration is 60 minutes');
return;
}
// Save new durations
this.durations.sprues = spruesTotal;
this.durations.males = malesTotal;
this.saveSettings();
this.closeSettings();
this.resetTimer();
this.announce('Settings saved successfully');
}
/**
* Toggle visual alert setting
*/
toggleAlert() {
this.settings.visualAlert = !this.settings.visualAlert;
this.elements.alertToggle.classList.toggle('active', this.settings.visualAlert);
this.elements.alertToggle.setAttribute('aria-checked', this.settings.visualAlert);
this.saveSettings();
}
/**
* Get duration for a shift in seconds
*/
getDuration(shift) {
return this.durations[shift] || 0;
}
/**
* Format time in MM:SS format
*/
formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
/**
* Announce message to screen readers
*/
announce(message) {
this.elements.announcements.textContent = message;
// Clear after a short delay to allow for new announcements
setTimeout(() => {
this.elements.announcements.textContent = '';
}, 1000);
}
}
// Initialize the timer when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
new ProductionTimer();
});
</script>
```
</body>
</html>Editor is loading...
Leave a Comment