Add masonry layout calc to project grid

This commit is contained in:
chris 2025-12-10 14:04:54 -05:00
parent d5b17016f7
commit 54c6fef694
2 changed files with 67 additions and 9 deletions

View File

@ -48,6 +48,7 @@ let fireflyTimer = null;
let fireflyActive = false;
let titleClicks = [];
let easterEggCooling = false;
let pendingImport = null;
// --- Service Worker ---
if ('serviceWorker' in navigator) {
@ -262,7 +263,7 @@ async function handleImport(event) {
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;
pendingImport = data.projects;
if (typeof data.isDarkMode === 'boolean') {
isDarkMode = data.isDarkMode;
localStorage.setItem('crochetDarkMode', isDarkMode);
@ -271,11 +272,7 @@ async function handleImport(event) {
animationsEnabled = data.animationsEnabled;
localStorage.setItem('crochetAnimations', animationsEnabled);
}
localStorage.setItem('crochetCounters', JSON.stringify(projects));
applyTheme();
render();
closeSaveModal();
await showAlert({ title: 'Import complete', text: `${projects.length} project${projects.length === 1 ? '' : 's'} loaded.` });
showImportSelection();
} catch (err) {
showAlert({ title: 'Import failed', text: err.message });
}
@ -286,6 +283,47 @@ if (importInput) {
importInput.addEventListener('change', handleImport);
}
function showImportSelection() {
if (!pendingImport || !Array.isArray(pendingImport) || !saveOverlay) return;
const importSelection = document.getElementById('importSelection');
const importList = document.getElementById('importList');
if (!importSelection || !importList) return;
importList.innerHTML = '';
pendingImport.forEach((p, idx) => {
const item = document.createElement('label');
item.className = 'save-item';
item.innerHTML = `
<input type="checkbox" checked data-idx="${idx}">
<span>${p.name || 'Project ' + (idx + 1)}</span>
`;
importList.appendChild(item);
});
importSelection.classList.remove('hidden');
saveOverlay.classList.add('active');
}
function cancelImportSelection() {
pendingImport = null;
const importSelection = document.getElementById('importSelection');
const importList = document.getElementById('importList');
if (importSelection) importSelection.classList.add('hidden');
if (importList) importList.innerHTML = '';
}
function applyImportSelection() {
const importList = document.getElementById('importList');
if (!importList || !pendingImport) { cancelImportSelection(); return; }
const inputs = importList.querySelectorAll('input[type="checkbox"]');
const selected = Array.from(inputs).filter(i => i.checked).map(i => Number(i.dataset.idx));
if (selected.length === 0) { cancelImportSelection(); return; }
const selectedProjects = pendingImport.filter((_, idx) => selected.includes(idx));
projects = projects.concat(selectedProjects);
localStorage.setItem('crochetCounters', JSON.stringify(projects));
render();
cancelImportSelection();
closeSaveModal();
showAlert({ title: 'Import complete', text: `${selectedProjects.length} project(s) added.` });
}
function openSaveModal() {
if (!saveOverlay || !saveList) return;
saveList.innerHTML = '';
@ -407,6 +445,24 @@ function handleAmbientDrift() {
}
handleAmbientDrift();
function applyMasonry() {
const grid = document.querySelector('.projects-grid');
if (!grid) return;
const rowHeight = 12; // matches grid-auto-rows
const gap = 20; // matches grid gap
grid.querySelectorAll('.project-container').forEach(card => {
const height = card.getBoundingClientRect().height;
const span = Math.ceil((height + gap) / (rowHeight + gap));
card.style.gridRowEnd = `span ${span}`;
});
}
let resizeTimer = null;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(applyMasonry, 150);
});
const logoIcon = document.querySelector('.brand-icon');
if (logoIcon) {
logoIcon.addEventListener('click', () => {
@ -801,6 +857,7 @@ function render() {
lastCountPulse = null;
lastFinishedId = null;
app.appendChild(grid);
applyMasonry();
}
render();

View File

@ -247,8 +247,9 @@ h1 {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 20px;
align-items: start; /* Prevent shorter cards from stretching to tallest row mate */
grid-auto-flow: row dense; /* Fill gaps when possible */
align-items: start;
grid-auto-rows: 12px; /* Base row height for masonry calc */
grid-auto-flow: dense;
}
@media (max-width: 768px) {
@ -262,7 +263,7 @@ h1 {
background: var(--card-bg);
border-radius: 18px;
padding: 5px 15px 15px 15px;
margin-bottom: 2rem;
margin: 0;
box-shadow: var(--shadow);
transition: background-color 0.3s;
border: 1px solid var(--border);