// --- Data Init & Colors --- let projects = JSON.parse(localStorage.getItem('crochetCounters')) || []; // New Earthy/Woodland Palette extracted from image vibes const colors = [ '#a17d63', // Soft oak '#7a8c6a', // Moss sage '#c7a272', // Warm amber '#b88b8a', // Rose clay '#7b9189', // Sage teal '#aa9a7a', // Wheat linen '#5f6d57', // Forest dusk '#b07d6f', // Terra blush '#6d5947', // Walnut '#c4b08a', // Oat '#7c7565', // Stone '#8ca58c' // Meadow ]; const oldColors = [ '#b56b54', // Rust/Mushroom '#7a8b4f', // Olive Green '#cba052', // Mustard/Daisy center '#5f8a8b', // Muted Teal '#8c6246', // Warm Wood tone '#a87b8c', // Dusty Rose '#4a5d43', // Deep Forest '#9c7e63' // Taupe ]; // --- State Variables --- let modalState = { type: null, projectId: null, partId: null }; const app = document.getElementById('app'); const modal = document.getElementById('modalOverlay'); const modalInput = document.getElementById('modalInput'); const modalTitle = document.getElementById('modalTitle'); const hapticTick = () => { if ('vibrate' in navigator) navigator.vibrate(12); }; const installBtn = document.getElementById('installBtn'); const importInput = document.getElementById('importFile'); const motionBtn = document.getElementById('motionBtn'); const colorOverlay = document.getElementById('colorOverlay'); const colorGrid = document.getElementById('colorGrid'); const customColorInput = document.getElementById('customColorInput'); const saveOverlay = document.getElementById('saveOverlay'); const saveList = document.getElementById('saveList'); let pendingSaveSelection = []; let lastCountPulse = null; let lastFinishedId = null; let fireflyTimer = null; let fireflyActive = false; let titleClicks = []; let easterEggCooling = false; // --- Service Worker --- if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js').catch(() => { // fail silently; optional log }); }); } // --- Install Prompt --- let deferredInstallPrompt = null; const isStandalone = () => window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true; function hideInstall() { if (installBtn) installBtn.classList.add('hidden'); } function showInstall() { if (installBtn) installBtn.classList.remove('hidden'); } window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredInstallPrompt = e; if (!isStandalone()) showInstall(); }); window.addEventListener('appinstalled', () => { deferredInstallPrompt = null; hideInstall(); }); if (installBtn) { installBtn.addEventListener('click', async () => { if (!deferredInstallPrompt) return; deferredInstallPrompt.prompt(); const choice = await deferredInstallPrompt.userChoice; if (choice.outcome === 'accepted') hideInstall(); deferredInstallPrompt = null; }); } if (isStandalone()) hideInstall(); // --- Sweet-ish Alerts --- function removeSwal() { const existing = document.querySelector('.swal-overlay'); if (existing) existing.remove(); } function showConfirm({ title = 'Are you sure?', text = '', confirmText = 'Yes', cancelText = 'Cancel', danger = false } = {}) { return new Promise(resolve => { removeSwal(); const overlay = document.createElement('div'); overlay.className = 'swal-overlay'; overlay.innerHTML = `
${title}
${text}
`; const cancelBtn = overlay.querySelector('.swal-cancel'); const confirmBtn = overlay.querySelector('.swal-confirm, .swal-danger'); cancelBtn.onclick = () => { removeSwal(); resolve(false); }; confirmBtn.onclick = () => { removeSwal(); resolve(true); }; overlay.addEventListener('click', (e) => { if (e.target === overlay) { removeSwal(); resolve(false); } }); document.addEventListener('keydown', function onKey(e) { if (e.key === 'Escape') { removeSwal(); resolve(false); document.removeEventListener('keydown', onKey); } }); document.body.appendChild(overlay); }); } function showAlert({ title = 'Notice', text = '' } = {}) { return new Promise(resolve => { removeSwal(); const overlay = document.createElement('div'); overlay.className = 'swal-overlay'; overlay.innerHTML = `
${title}
${text}
`; const okBtn = overlay.querySelector('.swal-confirm'); okBtn.onclick = () => { removeSwal(); resolve(); }; overlay.addEventListener('click', (e) => { if (e.target === overlay) { removeSwal(); resolve(); } }); document.addEventListener('keydown', function onKey(e) { if (e.key === 'Escape') { removeSwal(); resolve(); document.removeEventListener('keydown', onKey); } }); document.body.appendChild(overlay); }); } // --- Theme Logic --- let isDarkMode = JSON.parse(localStorage.getItem('crochetDarkMode')); if (isDarkMode === null) { if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { isDarkMode = true; } else { isDarkMode = false; } } let animationsEnabled = JSON.parse(localStorage.getItem('crochetAnimations')); if (animationsEnabled === null) animationsEnabled = true; function applyTheme() { if (isDarkMode) { document.body.classList.add('dark-mode'); document.getElementById('themeBtn').innerHTML = ''; } else { document.body.classList.remove('dark-mode'); document.getElementById('themeBtn').innerHTML = ''; } handleAmbientDrift(); updateMotionBtn(); } function toggleTheme() { isDarkMode = !isDarkMode; localStorage.setItem('crochetDarkMode', isDarkMode); applyTheme(); if (animationsEnabled) { document.body.classList.add('theme-animating'); setTimeout(() => document.body.classList.remove('theme-animating'), 750); } } applyTheme(); function updateMotionBtn() { if (!motionBtn) return; motionBtn.innerHTML = animationsEnabled ? '' : ''; motionBtn.title = animationsEnabled ? 'Toggle Animations' : 'Animations disabled'; } function toggleAnimations() { animationsEnabled = !animationsEnabled; localStorage.setItem('crochetAnimations', animationsEnabled); updateMotionBtn(); if (!animationsEnabled) { stopAmbientDrift(); document.body.classList.remove('theme-animating'); } else { handleAmbientDrift(); } } function openColorPicker(pId, partId) { if (!colorOverlay || !colorGrid) return; const project = projects.find(p => p.id === pId); const part = project.parts.find(pt => pt.id === partId); colorGrid.innerHTML = colors.map(c => ` `).join(''); if (customColorInput) { customColorInput.value = part.color || project.color || colors[0]; customColorInput.oninput = (e) => { setPartColor(pId, partId, e.target.value); }; } colorOverlay.classList.add('active'); colorOverlay.dataset.projectId = pId; colorOverlay.dataset.partId = partId; } function closeColorPicker() { if (!colorOverlay) return; colorOverlay.classList.remove('active'); colorGrid.innerHTML = ''; colorOverlay.dataset.projectId = ''; colorOverlay.dataset.partId = ''; } function exportData(selectedProjects = projects) { const payload = { projects: selectedProjects, isDarkMode, animationsEnabled }; const names = selectedProjects.map(p => p.name || 'Project').join('_').replace(/\s+/g, '-').slice(0, 50) || 'projects'; const filename = `toadstool_${names}.json`; const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function triggerImport() { if (!importInput) return; importInput.value = ''; importInput.click(); } async function handleImport(event) { const file = event.target.files[0]; if (!file) return; try { const text = await file.text(); const data = JSON.parse(text); if (!data.projects || !Array.isArray(data.projects)) throw new Error('Invalid file'); projects = data.projects; if (typeof data.isDarkMode === 'boolean') { isDarkMode = data.isDarkMode; localStorage.setItem('crochetDarkMode', isDarkMode); } if (typeof data.animationsEnabled === 'boolean') { animationsEnabled = data.animationsEnabled; localStorage.setItem('crochetAnimations', animationsEnabled); } localStorage.setItem('crochetCounters', JSON.stringify(projects)); applyTheme(); render(); } catch (err) { showAlert({ title: 'Import failed', text: err.message }); } event.target.value = ''; } if (importInput) { importInput.addEventListener('change', handleImport); } function openSaveModal() { if (!saveOverlay || !saveList) return; saveList.innerHTML = ''; pendingSaveSelection = projects.map(p => p.id); projects.forEach(p => { const item = document.createElement('label'); item.className = 'save-item'; item.innerHTML = ` ${p.name} `; saveList.appendChild(item); }); saveOverlay.classList.add('active'); } function closeSaveModal() { if (!saveOverlay) return; saveOverlay.classList.remove('active'); saveList.innerHTML = ''; } function exportSelected() { if (!saveOverlay) return; const inputs = saveList.querySelectorAll('input[type="checkbox"]'); const selectedIds = Array.from(inputs).filter(i => i.checked).map(i => Number(i.dataset.id)); if (selectedIds.length === 0) { closeSaveModal(); return; } const selectedProjects = projects.filter(p => selectedIds.includes(p.id)); exportData(selectedProjects); closeSaveModal(); } // --- Firefly Animation --- function spawnFirefly({ markActive = false, source = 'ambient', side = 'any' } = {}) { const wrap = document.createElement('div'); wrap.className = 'firefly-wrap'; const el = document.createElement('div'); el.className = 'firefly'; const top = Math.random() * 55 + 5; // 5vh–60vh const scale = 0.9 + Math.random() * 0.4; const duration = 12 + Math.random() * 8; // 12–20s const chosenSide = side === 'any' ? ['left','right','top','bottom'][Math.floor(Math.random()*4)] : side; let startX = '-10vw', endX = '110vw', startY = `${top}vh`, endY = `${top + (Math.random()*12 - 6)}vh`; let midX = '25vw', midY = `${top - 6}vh`, mid2X = '65vw', mid2Y = `${top + 6}vh`; if (chosenSide === 'right') { startX = '110vw'; endX = '-10vw'; midX = '-25vw'; mid2X = '-65vw'; } else if (chosenSide === 'top') { const x = Math.random()*80 + 10; startX = `${x}vw`; endX = `${x + (Math.random()*20 - 10)}vw`; startY = '-12vh'; endY = '110vh'; midX = `${x + 8}vw`; mid2X = `${x - 8}vw`; midY = '25vh'; mid2Y = '65vh'; } else if (chosenSide === 'bottom') { const x = Math.random()*80 + 10; startX = `${x}vw`; endX = `${x + (Math.random()*20 - 10)}vw`; startY = '110vh'; endY = '-12vh'; midX = `${x - 8}vw`; mid2X = `${x + 8}vw`; midY = '75vh'; mid2Y = '35vh'; } wrap.style.setProperty('--fly-scale', scale); wrap.style.setProperty('--fly-duration', `${duration}s`); wrap.style.setProperty('--fly-start-x', startX); wrap.style.setProperty('--fly-start-y', startY); wrap.style.setProperty('--fly-mid-x', midX); wrap.style.setProperty('--fly-mid-y', midY); wrap.style.setProperty('--fly-mid2-x', mid2X); wrap.style.setProperty('--fly-mid2-y', mid2Y); wrap.style.setProperty('--fly-end-x', endX); wrap.style.setProperty('--fly-end-y', endY); if (markActive) fireflyActive = true; wrap.addEventListener('animationend', (e) => { if (e.animationName !== 'fireflyGlide') return; wrap.remove(); if (markActive) fireflyActive = false; }); wrap.appendChild(el); document.body.appendChild(wrap); } function spawnSeed({ markActive = false, source = 'ambient' } = {}) { const wrap = document.createElement('div'); wrap.className = 'seed-wrap'; const el = document.createElement('div'); el.className = 'seed'; const top = Math.random() * 55 + 5; // 5vh–60vh const scale = 0.85 + Math.random() * 0.4; const duration = 14 + Math.random() * 8; // 14–22s const tilt = (Math.random() * 16 + 8) * (Math.random() < 0.5 ? -1 : 1); // +/-8–24deg const sway = 4 + Math.random() * 6; // px const flipDur = 5 + Math.random() * 4; // 5–9s const dir = ['left','right','top'][Math.floor(Math.random()*3)]; const fromLeft = dir === 'left'; let start = fromLeft ? '-12vw' : '112vw'; let mid = fromLeft ? '30vw' : '-30vw'; let end = fromLeft ? '112vw' : '-12vw'; if (dir === 'top') { const x = Math.random()*80 + 10; start = `${x}vw`; mid = `${x + (Math.random()*10 - 5)}vw`; end = `${x + (Math.random()*20 - 10)}vw`; wrap.style.top = '-12vh'; } else { wrap.style.top = `${top}vh`; } wrap.style.setProperty('--seed-scale', scale); wrap.style.setProperty('--seed-duration', `${duration}s`); wrap.style.setProperty('--seed-tilt', `${tilt}deg`); wrap.style.setProperty('--seed-sway', `${sway}px`); wrap.style.setProperty('--seed-flip-duration', `${flipDur}s`); wrap.style.setProperty('--seed-start', start); wrap.style.setProperty('--seed-mid', mid); wrap.style.setProperty('--seed-end', end); if (markActive) fireflyActive = true; wrap.addEventListener('animationend', (e) => { if (e.animationName !== 'seedGlide') return; wrap.remove(); if (markActive) fireflyActive = false; }); wrap.appendChild(el); document.body.appendChild(wrap); } function stopAmbientDrift() { if (fireflyTimer) { clearTimeout(fireflyTimer); fireflyTimer = null; } document.querySelectorAll('.firefly-wrap').forEach(el => el.remove()); document.querySelectorAll('.seed-wrap').forEach(el => el.remove()); fireflyActive = false; } function scheduleAmbientDrift() { const delay = 10000 + Math.random() * 10000; // 10–20s fireflyTimer = setTimeout(() => { if (!animationsEnabled) { stopAmbientDrift(); return; } const selector = isDarkMode ? '.firefly-wrap' : '.seed-wrap'; let existing = document.querySelectorAll(selector).length; if (existing === 0) { isDarkMode ? spawnFirefly() : spawnSeed(); existing++; } else if (existing < 5) { isDarkMode ? spawnFirefly() : spawnSeed(); } scheduleAmbientDrift(); }, delay); } function handleAmbientDrift() { stopAmbientDrift(); if (!animationsEnabled) return; if (isDarkMode) { spawnFirefly(); } else { spawnSeed(); } scheduleAmbientDrift(); } handleAmbientDrift(); const logoIcon = document.querySelector('.brand-icon'); if (logoIcon) { logoIcon.addEventListener('click', () => { if (!animationsEnabled || fireflyActive) return; if (isDarkMode) { spawnFirefly({ markActive: true, source: 'logo', side: 'any' }); } else { spawnSeed({ markActive: true, source: 'logo' }); } }); } if (colorOverlay) { colorOverlay.addEventListener('click', (e) => { if (e.target === colorOverlay) closeColorPicker(); }); } document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && colorOverlay && colorOverlay.classList.contains('active')) { closeColorPicker(); } }); const importBtn = document.getElementById('importBtn'); if (importBtn) { importBtn.addEventListener('click', triggerImport); } const titleEl = document.getElementById('appTitle'); if (titleEl) { titleEl.addEventListener('click', () => { const now = Date.now(); titleClicks = titleClicks.filter(ts => now - ts < 7000); titleClicks.push(now); if (titleClicks.length >= 5 && !easterEggCooling) { easterEggCooling = true; triggerBurst(); setTimeout(() => { easterEggCooling = false; titleClicks = []; }, 8000); } }); } function triggerBurst() { if (!animationsEnabled) return; const burstCount = isDarkMode ? 24 : 18; const spawner = isDarkMode ? (opts) => spawnFirefly({ ...opts, side: 'any' }) : spawnSeed; for (let i = 0; i < burstCount; i++) { const jitter = Math.random() * 200; setTimeout(() => spawner({ source: 'burst' }), i * 140 + jitter); } } // --- Focus Mode Logic --- let wakeLock = null; let isFocusMode = false; const focusBtn = document.getElementById('focusBtn'); // --- Migration Check --- if (projects.length > 0) { let changed = false; projects.forEach((p, index) => { if (!p.parts) { p.parts = []; changed = true; } if (!p.color) { p.color = colors[index % colors.length]; changed = true; } const oldIdx = oldColors.indexOf(p.color); if (oldIdx !== -1) { p.color = colors[oldIdx % colors.length]; changed = true; } if (p.note === undefined) { p.note = ''; changed = true; } p.parts.forEach(pt => { if (pt.max === undefined) { pt.max = null; changed = true; } if (pt.note === undefined) { pt.note = ''; changed = true; } if (!pt.color) { pt.color = p.color; changed = true; } const oldPartIdx = oldColors.indexOf(pt.color); if (oldPartIdx !== -1) { pt.color = colors[oldPartIdx % colors.length]; changed = true; } }); }); if (changed) { localStorage.setItem('crochetCounters', JSON.stringify(projects)); } } // --- Core Functions --- function save() { localStorage.setItem('crochetCounters', JSON.stringify(projects)); render(); } async function toggleFocusMode() { if (!isFocusMode) { try { if (document.documentElement.requestFullscreen) await document.documentElement.requestFullscreen(); if ('wakeLock' in navigator) { wakeLock = await navigator.wakeLock.request('screen'); } isFocusMode = true; focusBtn.classList.add('is-active'); } catch (err) { showAlert({ title: 'Focus Mode failed', text: err.message }); } } else { if (document.fullscreenElement) document.exitFullscreen(); if (wakeLock !== null) { wakeLock.release(); wakeLock = null; } isFocusMode = false; focusBtn.classList.remove('is-active'); } } document.addEventListener('visibilitychange', async () => { if (wakeLock !== null && document.visibilityState === 'visible') { wakeLock = await navigator.wakeLock.request('screen'); } }); // --- Interaction Logic --- async function deleteProject(pId) { const ok = await showConfirm({ title: 'Delete project?', text: 'This will remove the entire project.', confirmText: 'Delete', danger: true }); if (ok) { projects = projects.filter(p => p.id !== pId); save(); } } function toggleProjectCollapse(pId) { const project = projects.find(p => p.id === pId); project.collapsed = !project.collapsed; save(); } function renameProject(pId) { modalState = { type: 'renameProject', pId, partId: null }; const project = projects.find(p => p.id === pId); modalTitle.innerText = "Rename Project"; modalInput.value = project.name; modalInput.type = "text"; modalInput.placeholder = "Project name"; modal.classList.add('active'); setTimeout(() => modalInput.focus(), 100); } async function deletePart(pId, partId) { const project = projects.find(p => p.id === pId); const part = project.parts.find(pt => pt.id === partId); if (part.locked) return; const ok = await showConfirm({ title: 'Delete part?', text: 'This part will be removed.', confirmText: 'Delete', danger: true }); if (ok) { project.parts = project.parts.filter(pt => pt.id !== partId); save(); } } function togglePartMinimize(pId, partId) { const project = projects.find(p => p.id === pId); const part = project.parts.find(pt => pt.id === partId); part.minimized = !part.minimized; save(); } function togglePartLock(pId, partId) { const project = projects.find(p => p.id === pId); const part = project.parts.find(pt => pt.id === partId); part.locked = !part.locked; save(); } function setPartColor(pId, partId, color) { const project = projects.find(p => p.id === pId); const part = project.parts.find(pt => pt.id === partId); part.color = color; save(); } function togglePartFinish(pId, partId) { const project = projects.find(p => p.id === pId); const part = project.parts.find(pt => pt.id === partId); part.finished = !part.finished; if(part.finished) { part.locked = false; lastFinishedId = part.id; } else { lastFinishedId = null; } save(); } function updateCount(pId, partId, change) { const project = projects.find(p => p.id === pId); const part = project.parts.find(pt => pt.id === partId); if (part.locked || part.finished) return; part.count += change; if (part.max !== null && part.count > part.max) part.count = part.max; if (part.count < 0) part.count = 0; hapticTick(); lastCountPulse = { partId, dir: change > 0 ? 'up' : 'down' }; save(); } async function resetCount(pId, partId) { const project = projects.find(p => p.id === pId); const part = project.parts.find(pt => pt.id === partId); if (part.locked || part.finished) return; const ok = await showConfirm({ title: 'Reset count?', text: 'Set this count back to zero.', confirmText: 'Reset', danger: true }); if(ok) { part.count = 0; save(); } } // --- Modal Logic --- function openModal(type, pId = null, partId = null) { modalState = { type, pId, partId }; modalInput.value = ""; if (type === 'addProject') { modalTitle.innerText = "New Project Name"; modalInput.type = "text"; modalInput.placeholder = "e.g., Amigurumi Bear"; } else if (type === 'addPart') { modalTitle.innerText = "New Part Name"; modalInput.type = "text"; modalInput.placeholder = "e.g., Head"; } else if (type === 'setMax') { const part = projects.find(p => p.id === pId).parts.find(pt => pt.id === partId); modalTitle.innerText = "Set Max Stitches"; modalInput.value = part.max ?? ''; modalInput.type = "number"; modalInput.placeholder = "Leave blank to clear"; } else if (type === 'renamePart') { const part = projects.find(p => p.id === pId).parts.find(pt => pt.id === partId); if(part.locked || part.finished) return; modalTitle.innerText = "Rename Part"; modalInput.value = part.name; modalInput.type = "text"; } else if (type === 'manualCount') { const part = projects.find(p => p.id === pId).parts.find(pt => pt.id === partId); if(part.locked || part.finished) return; modalTitle.innerText = "Set Row Count"; modalInput.value = part.count; modalInput.type = "number"; } else if (type === 'setMax') { const part = projects.find(p => p.id === pId).parts.find(pt => pt.id === partId); if (part.locked) return; } modal.classList.add('active'); setTimeout(() => modalInput.focus(), 100); } function closeModal() { modal.classList.remove('active'); modalInput.blur(); } function saveModal() { const val = modalInput.value.trim(); if (!val && modalState.type !== 'manualCount' && modalState.type !== 'setMax') return closeModal(); if (modalState.type === 'addProject') { const nextColor = colors[projects.length % colors.length]; projects.push({ id: Date.now(), name: val, color: nextColor, collapsed: false, parts: [] }); projects[projects.length-1].parts.push({ id: Date.now() + 1, name: 'Part 1', count: 0, locked: false, finished: false, minimized: false, max: null, color: nextColor }); } else if (modalState.type === 'addPart') { const project = projects.find(p => p.id === modalState.pId); project.parts.push({ id: Date.now(), name: val, count: 0, locked: false, finished: false, minimized: false, max: null, color: project.color }); project.collapsed = false; } else if (modalState.type === 'renamePart') { const part = projects.find(p => p.id === modalState.pId).parts.find(pt => pt.id === modalState.partId); part.name = val; } else if (modalState.type === 'renameProject') { const project = projects.find(p => p.id === modalState.pId); project.name = val; } else if (modalState.type === 'manualCount') { const num = parseInt(val); if (!isNaN(num) && num >= 0) { const part = projects.find(p => p.id === modalState.pId).parts.find(pt => pt.id === modalState.partId); part.count = num; if (part.max !== null && part.count > part.max) part.count = part.max; } } else if (modalState.type === 'setMax') { const part = projects.find(p => p.id === modalState.pId).parts.find(pt => pt.id === modalState.partId); if (val === '') { part.max = null; } else { const num = parseInt(val); if (!isNaN(num) && num > 0) { part.max = num; if (part.count > part.max) part.count = part.max; } } } save(); closeModal(); } modalInput.addEventListener("keyup", (e) => { if (e.key === "Enter") saveModal(); }); function toggleNote(id) { const el = document.getElementById(id); if (!el) return; el.classList.toggle('show'); } function updateProjectNote(e, pId) { const project = projects.find(p => p.id === pId); project.note = e.target.value; localStorage.setItem('crochetCounters', JSON.stringify(projects)); } function updatePartNote(e, pId, partId) { const project = projects.find(p => p.id === pId); const part = project.parts.find(pt => pt.id === partId); part.note = e.target.value; localStorage.setItem('crochetCounters', JSON.stringify(projects)); } // --- Render Logic --- function render() { app.innerHTML = ''; if (projects.length === 0) { app.innerHTML = '
Toadstools & twine await...
Tap + to begin a new project.
'; return; } const grid = document.createElement('div'); grid.className = 'projects-grid'; projects.forEach(project => { const sortedParts = [...project.parts].sort((a, b) => a.finished - b.finished); const projectCollapsedClass = project.collapsed ? 'project-collapsed' : ''; let partsHtml = ''; sortedParts.forEach(part => { const accent = part.color || project.color; const isLocked = part.locked ? 'is-locked' : ''; const isFinished = part.finished ? 'is-finished' : ''; const isMinimized = part.minimized ? 'is-minimized' : ''; const lockIcon = part.locked ? '' : ''; const lockBtnClass = part.locked ? 'btn-lock locked-active' : 'btn-lock'; const controlsDimmed = (part.locked || part.finished) ? 'dimmed' : ''; const hideControls = (part.finished || part.minimized) ? 'hidden-controls' : ''; const showSetMax = part.minimized ? 'hidden' : ''; const partNoteId = `part-note-${project.id}-${part.id}`; const countId = `count-${part.id}`; const pulseClass = lastCountPulse && lastCountPulse.partId === part.id ? (lastCountPulse.dir === 'up' ? 'count-bump-up' : 'count-bump-down') : ''; const finishPulseClass = part.finished && lastFinishedId === part.id ? 'finish-shimmer' : ''; const partCardId = `part-${part.id}`; const partCardFullClass = `${isLocked} ${isFinished} ${isMinimized} ${finishPulseClass}`; const lockDisabled = part.locked ? 'disabled' : ''; const actionsHtml = part.minimized ? `
` : `
`; const countSubtext = part.minimized ? '' : `
${part.max !== null ? `${part.count} / ${part.max}` : 'No max set'}
`; partsHtml += `
${part.name} ${part.count}
${actionsHtml}
${part.count}
${countSubtext}
`; }); const projectContainer = document.createElement('div'); projectContainer.className = `project-container ${projectCollapsedClass}`; projectContainer.style = `--project-color: ${project.color}`; const projectNoteId = `project-note-${project.id}`; projectContainer.innerHTML = `
${project.name}
${partsHtml}
`; grid.appendChild(projectContainer); }); lastCountPulse = null; lastFinishedId = null; app.appendChild(grid); } render();