// Shortest-column masonry distribution (ported from ImageRepo). // // Predictive relative height = height / width. Using the aspect ratio means // columns are balanced before thumbnails load, so there is no reflow when // images arrive. Missing or zero dimensions fall back to a square (1.0). import { ref, onMounted, onUnmounted } from 'vue' function relativeHeight(item) { const w = Number(item.width) const h = Number(item.height) if (!w || !h) return 1 return h / w } export function distributeIntoColumns(items, columnCount) { const n = Math.max(1, columnCount | 0) const columns = Array.from({ length: n }, () => []) const heights = new Array(n).fill(0) for (const item of items) { let shortest = 0 for (let c = 1; c < n; c++) { if (heights[c] < heights[shortest]) shortest = c } columns[shortest].push(item) heights[shortest] += relativeHeight(item) } return columns } // Reactive column count from container width. Breakpoints mirror the // gallery grid intent: ~390px target column width, min 1 column. Bumped // from 260px on 2026-05-23 per dogfood UX feedback (thumbs were too small). export function columnCountForWidth(width) { if (!width || width < 0) return 1 return Math.max(1, Math.floor(width / 390)) } export function usePolyMasonry(containerRef) { const columnCount = ref(1) let ro = null function measure() { const el = containerRef.value if (el) columnCount.value = columnCountForWidth(el.clientWidth) } onMounted(() => { measure() if (typeof ResizeObserver !== 'undefined' && containerRef.value) { ro = new ResizeObserver(measure) ro.observe(containerRef.value) } }) onUnmounted(() => ro && ro.disconnect()) return { columnCount, distribute: distributeIntoColumns } }