From a6938f390b0c768aad52ecc9deee8585c63dab96 Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 10 Dec 2025 12:19:23 -0500 Subject: [PATCH] Add woodland save/load modal and picker --- assets/app.js | 109 +++++++++++++++++++++++++++++++++++++++++------ assets/style.css | 75 +++++++++++++++++++++++++++++++- index.html | 33 ++++++++++++-- 3 files changed, 199 insertions(+), 18 deletions(-) diff --git a/assets/app.js b/assets/app.js index c5e61bf..81fafa9 100644 --- a/assets/app.js +++ b/assets/app.js @@ -2,14 +2,24 @@ let projects = JSON.parse(localStorage.getItem('crochetCounters')) || []; // New Earthy/Woodland Palette extracted from image vibes const colors = [ - '#8a6b52', // Oak - '#6f8b5f', // Moss - '#c28a5c', // Burnished amber - '#b27c7a', // Rose clay - '#708a84', // Sagey teal - '#9d8b6f', // Wheat - '#5b6d4f', // Deep forest - '#a36a5f' // Terra + '#a17d63', // Soft oak + '#7a8c6a', // Moss sage + '#c7a272', // Warm amber + '#b88b8a', // Rose clay + '#7b9189', // Sage teal + '#aa9a7a', // Wheat linen + '#5f6d57', // Forest dusk + '#b07d6f' // Terra blush +]; +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 --- @@ -22,6 +32,11 @@ 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 saveOverlay = document.getElementById('saveOverlay'); +const saveList = document.getElementById('saveList'); +let pendingSaveSelection = []; let lastCountPulse = null; let lastFinishedId = null; let fireflyTimer = null; @@ -184,17 +199,39 @@ function toggleAnimations() { } } -function exportData() { +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(''); + 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, + 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 = 'toadstool-counter-backup.json'; + a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); @@ -236,6 +273,38 @@ 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' } = {}) { const wrap = document.createElement('div'); @@ -336,6 +405,16 @@ if (logoIcon) { } }); } +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); @@ -379,11 +458,15 @@ if (projects.length > 0) { 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)); } @@ -628,7 +711,6 @@ function render() { const showSetMax = part.minimized ? 'hidden' : ''; const partNoteId = `part-note-${project.id}-${part.id}`; const countId = `count-${part.id}`; - const colorInputId = `color-${project.id}-${part.id}`; const pulseClass = lastCountPulse && lastCountPulse.partId === part.id ? (lastCountPulse.dir === 'up' ? 'count-bump-up' : 'count-bump-down') : ''; @@ -639,8 +721,7 @@ function render() { const actionsHtml = part.minimized ? `
` : `
- - + diff --git a/assets/style.css b/assets/style.css index 6f92ea9..0ee5d97 100644 --- a/assets/style.css +++ b/assets/style.css @@ -135,6 +135,80 @@ h1 { .header-btn.is-active { background: var(--header-text); color: var(--header-bg); } .hidden { display: none !important; } .hidden-input { display: none; } +.color-overlay { + position: fixed; + inset: 0; + background: rgba(44, 35, 25, 0.55); + display: none; + align-items: center; + justify-content: center; + z-index: 210; + padding: 20px; + backdrop-filter: blur(2px); +} +.color-overlay.active { display: flex; } +.color-modal { + background: var(--card-bg); + color: var(--text); + border-radius: 16px; + padding: 18px 18px 14px; + width: min(420px, 90vw); + box-shadow: 0 10px 30px rgba(0,0,0,0.25); + border: 1px solid var(--border); +} +.color-title { margin: 0 0 12px; font-family: 'Cormorant Garamond', Georgia, serif; } +.color-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(60px, 1fr)); + gap: 10px; + margin-bottom: 12px; +} +.color-swatch { + height: 44px; + border-radius: 12px; + border: 2px solid var(--border); + cursor: pointer; + box-shadow: inset 0 0 0 2px rgba(255,255,255,0.55); + transition: transform 0.15s ease, box-shadow 0.15s ease; +} +.color-swatch:hover { transform: translateY(-1px) scale(1.02); box-shadow: inset 0 0 0 2px rgba(255,255,255,0.8); } +.color-swatch:active { transform: scale(0.98); } + +.save-overlay { + position: fixed; + inset: 0; + background: rgba(44, 35, 25, 0.55); + display: none; + align-items: center; + justify-content: center; + z-index: 210; + padding: 20px; + backdrop-filter: blur(2px); +} +.save-overlay.active { display: flex; } +.save-modal { + background: var(--card-bg); + color: var(--text); + border-radius: 16px; + padding: 18px 18px 14px; + width: min(420px, 90vw); + box-shadow: 0 10px 30px rgba(0,0,0,0.25); + border: 1px solid var(--border); +} +.save-subtext { margin: 0 0 8px; color: var(--text-muted); } +.save-list { display: grid; gap: 8px; max-height: 220px; overflow-y: auto; margin-bottom: 10px; } +.save-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 10px; + border-radius: 10px; + background: var(--input-bg); + border: 1px solid var(--border); +} +.save-item input { width: 18px; height: 18px; } +.save-actions { display: flex; justify-content: flex-end; gap: 8px; } +.icon-woodland { width: 22px; height: 22px; } .container { max-width: 1200px; @@ -226,7 +300,6 @@ h1 { box-shadow: inset 0 0 0 2px var(--card-bg); } .btn-color:hover { box-shadow: inset 0 0 0 2px var(--project-color); } -.color-input { display: none; } /* --- PART CARD --- */ .part-card { diff --git a/index.html b/index.html index 15f04f7..d40b00e 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,7 @@ - + @@ -28,8 +28,14 @@ - - +
@@ -49,6 +55,27 @@ +
+
+

Pick a color

+
+ +
+
+ +
+
+

Save or Load

+

Choose projects to include:

+
+
+ + + +
+
+
+