feat(sacred-valley): masonry card layout — content-height row spans + dense flow
Cards keep their column span but now span grid rows proportional to measured content height (ResizeObserver re-packs async cards), with grid-auto-flow: dense. Fixes mismatched sizes / rigid rows / vertical gaps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "void-server",
|
||||
"version": "2.6.6",
|
||||
"version": "2.7.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user