diff --git a/package-lock.json b/package-lock.json index b062247..970faba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "void-server", - "version": "2.6.6", + "version": "2.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "void-server", - "version": "2.6.6", + "version": "2.7.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "@mozilla/readability": "^0.6.0", diff --git a/package.json b/package.json index 17d04c8..e169aaf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "void-server", - "version": "2.6.6", + "version": "2.7.0", "type": "module", "private": true, "scripts": { diff --git a/public/style.css b/public/style.css index 1a0c746..7c12ae8 100644 --- a/public/style.css +++ b/public/style.css @@ -382,7 +382,7 @@ ul.plain li:last-child { border-bottom: none; } /* reserved for a future agent-output phase — unused now: --hue-dross: #ff4f2e; --hue-yerin: #c45a4a; --hue-orthos: #6fa86a; */ } -#sv-cards { display: grid; grid-template-columns: repeat(12, 1fr); gap: 16px; align-items: start; } +#sv-cards { display: grid; grid-template-columns: repeat(12, 1fr); gap: 16px; align-items: start; grid-auto-rows: 8px; grid-auto-flow: row dense; } .sv-card { grid-column: span 6; } /* fallback; svCard sets an inline span (1–12) */ @media (max-width: 700px) { #sv-cards { grid-template-columns: 1fr; } .sv-card { grid-column: 1 / -1 !important; } } .sv-ed-span { display: inline-flex; align-items: center; gap: 3px; } diff --git a/public/views/sacred_valley.js b/public/views/sacred_valley.js index f91733a..17b80c9 100644 --- a/public/views/sacred_valley.js +++ b/public/views/sacred_valley.js @@ -27,6 +27,24 @@ let layout = { card_order: [], hidden: [], sizes: {} }; const grid = () => document.getElementById('sv-cards'); +// ---- masonry packing: cards keep their column span (width) but pack vertically by +// content height (via grid-row span over small auto-rows), so mismatched heights no +// longer leave gaps / rigid rows. ResizeObserver re-packs as async cards fill in. +const ROW_UNIT = 8, GRID_GAP = 16; +function packCard(node) { + if (!node || !node.isConnected) return; + const h = node.getBoundingClientRect().height; + if (h) node.style.gridRowEnd = 'span ' + Math.max(1, Math.ceil((h + GRID_GAP) / (ROW_UNIT + GRID_GAP))); +} +const ro = typeof ResizeObserver !== 'undefined' + ? new ResizeObserver(entries => entries.forEach(e => packCard(e.target))) : null; +let repackRaf; +function repackAll() { + cancelAnimationFrame(repackRaf); + repackRaf = requestAnimationFrame(() => grid()?.querySelectorAll('.sv-card').forEach(packCard)); +} +if (typeof window !== 'undefined') window.addEventListener('resize', repackAll); + async function saveLayout() { try { await api.put('/api/dashboard/layout', layout); } catch (e) { console.error('save layout', e); } @@ -72,6 +90,7 @@ function mountOne(def) { const { root, body } = svCard({ ...def, span }); root.appendChild(editOverlay(def)); grid().appendChild(root); + ro?.observe(root); packCard(root); try { def.mount(body); def.start && def.start(); active.push(def); } catch (e) { body.appendChild(el('span', { class: 'muted' }, 'card failed')); console.error(def.id, e); } } @@ -160,7 +179,7 @@ async function resetLayout() { export async function render(main) { mainEl = main; const myGen = ++renderGen; - active.forEach(c => c.stop && c.stop()); active = []; stopHealthBand(); stopDevicesBand(); + active.forEach(c => c.stop && c.stop()); active = []; ro?.disconnect(); stopHealthBand(); stopDevicesBand(); editing = false; mount(main, el('h1', { class: 'view-h1' }, 'Sacred Valley'),