1 line
27 KiB
JavaScript
1 line
27 KiB
JavaScript
let projects=JSON.parse(localStorage.getItem("crochetCounters"))||[];const colors=["#a17d63","#7a8c6a","#c7a272","#b88b8a","#7b9189","#aa9a7a","#5f6d57","#b07d6f","#6d5947","#c4b08a","#7c7565","#8ca58c"];const oldColors=["#b56b54","#7a8b4f","#cba052","#5f8a8b","#8c6246","#a87b8c","#4a5d43","#9c7e63"];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;if("serviceWorker"in navigator){window.addEventListener("load",(()=>{navigator.serviceWorker.register("/sw.js").catch((()=>{}))}))}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();function removeSwal(){const existing=document.querySelector(".swal-overlay");if(existing)existing.remove()}function showConfirm({title:title="Are you sure?",text:text="",confirmText:confirmText="Yes",cancelText:cancelText="Cancel",danger:danger=false}={}){return new Promise((resolve=>{removeSwal();const overlay=document.createElement("div");overlay.className="swal-overlay";overlay.innerHTML=`\n <div class="swal-dialog">\n <div class="swal-title">${title}</div>\n <div class="swal-text">${text}</div>\n <div class="swal-actions">\n <button class="swal-btn swal-cancel">${cancelText}</button>\n <button class="swal-btn ${danger?"swal-danger":"swal-confirm"}">${confirmText}</button>\n </div>\n </div>\n `;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:title="Notice",text:text=""}={}){return new Promise((resolve=>{removeSwal();const overlay=document.createElement("div");overlay.className="swal-overlay";overlay.innerHTML=`\n <div class="swal-dialog">\n <div class="swal-title">${title}</div>\n <div class="swal-text">${text}</div>\n <div class="swal-actions">\n <button class="swal-btn swal-confirm">OK</button>\n </div>\n </div>\n `;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)}))}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='<i class="fa-solid fa-moon"></i>'}else{document.body.classList.remove("dark-mode");document.getElementById("themeBtn").innerHTML='<i class="fa-solid fa-sun"></i>'}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?'<i class="fa-solid fa-wand-magic-sparkles"></i>':'<i class="fa-solid fa-ban"></i>';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=>`\n <button class="color-swatch" style="background:${c}" onclick="setPartColor(${pId}, ${partId}, '${c}'); closeColorPicker();"></button>\n `)).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:isDarkMode,animationsEnabled: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=`\n <input type="checkbox" checked data-id="${p.id}">\n <span>${p.name}</span>\n `;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()}function spawnFirefly({markActive:markActive=false,source:source="ambient",side: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;const scale=.9+Math.random()*.4;const duration=12+Math.random()*8;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:markActive=false,source: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;const scale=.85+Math.random()*.4;const duration=14+Math.random()*8;const tilt=(Math.random()*16+8)*(Math.random()<.5?-1:1);const sway=4+Math.random()*6;const flipDur=5+Math.random()*4;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=1e4+Math.random()*1e4;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<7e3));titleClicks.push(now);if(titleClicks.length>=5&&!easterEggCooling){easterEggCooling=true;triggerBurst();setTimeout((()=>{easterEggCooling=false;titleClicks=[]}),8e3)}}))}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)}}let wakeLock=null;let isFocusMode=false;const focusBtn=document.getElementById("focusBtn");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))}}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")}}));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: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: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()}}function openModal(type,pId=null,partId=null){modalState={type:type,pId:pId,partId: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))}function render(){app.innerHTML="";if(projects.length===0){app.innerHTML='<div class="empty-state">Toadstools & twine await...<br>Tap + to begin a new project.</div>';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?'<i class="fa-solid fa-lock"></i>':'<i class="fa-solid fa-lock-open"></i>';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?`<div class="part-actions"><button class="icon-btn btn-toggle-part" onclick="togglePartMinimize(${project.id}, ${part.id})" title="Expand"><i class="fa-solid fa-chevron-down"></i></button></div>`:`<div class="part-actions">\n <button class="btn-color" style="--project-color: ${accent}" onclick="openColorPicker(${project.id}, ${part.id})" title="Set color"></button>\n <button class="icon-btn btn-reset-part" onclick="resetCount(${project.id}, ${part.id})" ${isFinished||part.locked?"disabled":""}><i class="fa-solid fa-rotate-left"></i></button>\n <button class="icon-btn btn-delete-part" onclick="deletePart(${project.id}, ${part.id})" ${lockDisabled}><i class="fa-solid fa-trash"></i></button>\n <button class="icon-btn btn-toggle-part" onclick="togglePartMinimize(${project.id}, ${part.id})" title="Minimize"><i class="fa-solid fa-chevron-down"></i></button>\n </div>`;const countSubtext=part.minimized?"":`\n <div class="count-subtext">\n ${part.max!==null?`<strong>${part.count}</strong> / ${part.max}`:"No max set"}\n <button class="icon-btn ${showSetMax}" onclick="openModal('setMax', ${project.id}, ${part.id})" title="Set max" ${lockDisabled}><i class="fa-solid fa-gear"></i></button>\n </div>\n `;partsHtml+=`\n <div class="part-card ${partCardFullClass}" id="${partCardId}" style="--project-color: ${accent}">\n <div class="part-header">\n <div class="part-name-group">\n <label class="check-container">\n <input type="checkbox" ${part.finished?"checked":""} onchange="togglePartFinish(${project.id}, ${part.id})">\n <span class="checkmark"></span>\n </label>\n <span class="part-name" onclick="openModal('renamePart', ${project.id}, ${part.id})">${part.name}</span>\n <span class="part-mini-count">${part.count}</span>\n </div>\n ${actionsHtml}\n </div>\n <div class="count-display ${pulseClass}" id="${countId}" ondblclick="openModal('manualCount', ${project.id}, ${part.id})">${part.count}</div>\n ${countSubtext}\n <div class="controls ${hideControls}">\n <button class="action-btn btn-minus ${controlsDimmed}" onclick="updateCount(${project.id}, ${part.id}, -1)">-</button>\n <button class="action-btn ${lockBtnClass}" onclick="togglePartLock(${project.id}, ${part.id})">${lockIcon}</button>\n <button class="action-btn btn-plus ${controlsDimmed}" onclick="updateCount(${project.id}, ${part.id}, 1)">+</button>\n </div>\n <div class="note-area" id="${partNoteId}">\n <textarea placeholder="Notes for this part..." oninput="updatePartNote(event, ${project.id}, ${part.id})">${part.note||""}</textarea>\n </div>\n <button class="note-toggle" onclick="toggleNote('${partNoteId}')">Notes</button>\n </div>`}));const projectContainer=document.createElement("div");projectContainer.className=`project-container ${projectCollapsedClass}`;projectContainer.style=`--project-color: ${project.color}`;const projectNoteId=`project-note-${project.id}`;projectContainer.innerHTML=`\n <div class="project-header">\n <div class="project-title-group">\n <button class="btn-toggle-project" onclick="toggleProjectCollapse(${project.id})">▼</button>\n <span class="project-title">${project.name}</span>\n <button class="btn-rename-project" onclick="renameProject(${project.id})" title="Rename project"><span class="icon-pencil">✎</span></button>\n </div>\n <div class="project-actions">\n <button class="btn-add-part" onclick="openModal('addPart', ${project.id})">+ Part</button>\n <button class="btn-delete-project" onclick="deleteProject(${project.id})">×</button>\n </div>\n </div>\n <div class="note-area" id="${projectNoteId}">\n <textarea placeholder="Notes for this project..." oninput="updateProjectNote(event, ${project.id})">${project.note||""}</textarea>\n </div>\n <button class="note-toggle" onclick="toggleNote('${projectNoteId}')">Notes</button>\n <div class="part-list">${partsHtml}</div>\n `;grid.appendChild(projectContainer)}));lastCountPulse=null;lastFinishedId=null;app.appendChild(grid)}render(); |