diff --git a/server/app/static/frame_layout.js b/server/app/static/frame_layout.js index 912425a..9c56bb4 100644 --- a/server/app/static/frame_layout.js +++ b/server/app/static/frame_layout.js @@ -7,6 +7,14 @@ // reloads the canvas from the server's actual state rather than trusting // an optimistic update -- simplest way to guarantee the canvas never // drifts from what a rejected request left in place. +// +// Widget/canvas geometry is computed and applied in *pixels* from JS, +// not CSS percentages/aspect-ratio -- aspect-ratio isn't supported on +// every mobile browser this app gets viewed from, and a percentage +// height on the widget boxes silently collapses to 0 against an +// indeterminate-height ancestor on those browsers (the canvas would +// render with no visible size at all, which is exactly what happened +// before this was pixel-based). let gridState = null; // last-loaded GET .../widgets response @@ -37,23 +45,51 @@ async function takeControl() { } document.getElementById('take-control').addEventListener('click', takeControl); -function cellSizePx() { - const rect = document.getElementById('widget-canvas').getBoundingClientRect(); - return { cellW: rect.width / gridState.grid.cols, cellH: rect.height / gridState.grid.rows }; +// Cached each time the canvas is (re)laid out (see layoutCanvas) so drag +// math doesn't re-measure the DOM on every pointermove. +let canvasMetrics = { width: 0, height: 0, cellW: 0, cellH: 0 }; + +function layoutCanvas() { + if (!gridState) return; + const wrap = document.getElementById('widget-canvas-wrap'); + const canvas = document.getElementById('widget-canvas'); + const cols = gridState.grid.cols, rows = gridState.grid.rows; + + // wrap's own width comes from ordinary CSS (100% of the card, capped + // at max-width) -- only its height is JS-driven, from that measured + // width, to keep the grid's aspect ratio without relying on the CSS + // aspect-ratio property. + const width = wrap.getBoundingClientRect().width; + const height = width * (rows / cols); + wrap.style.height = height + 'px'; + canvas.style.width = width + 'px'; + canvas.style.height = height + 'px'; + + const cellW = width / cols, cellH = height / rows; + canvasMetrics = { width, height, cellW, cellH }; + wrap.style.backgroundImage = + `linear-gradient(to right, var(--border) 1px, transparent 1px),` + + `linear-gradient(to bottom, var(--border) 1px, transparent 1px)`; + wrap.style.backgroundSize = `${cellW}px ${cellH}px`; + + for (const box of canvas.children) { + positionBox(box, box._rect); + } } function positionBox(box, rect) { - const cols = gridState.grid.cols, rows = gridState.grid.rows; - box.style.left = (rect.x / cols * 100) + '%'; - box.style.top = (rect.y / rows * 100) + '%'; - box.style.width = (rect.w / cols * 100) + '%'; - box.style.height = (rect.h / rows * 100) + '%'; + box._rect = rect; + const { cellW, cellH } = canvasMetrics; + box.style.left = (rect.x * cellW) + 'px'; + box.style.top = (rect.y * cellH) + 'px'; + box.style.width = (rect.w * cellW) + 'px'; + box.style.height = (rect.h * cellH) + 'px'; } function startDrag(e, widget, box, isResize) { e.preventDefault(); box.setPointerCapture(e.pointerId); - const { cellW, cellH } = cellSizePx(); + const { cellW, cellH } = canvasMetrics; const startX = e.clientX, startY = e.clientY; const orig = { x: widget.x, y: widget.y, w: widget.w, h: widget.h }; const cols = gridState.grid.cols, rows = gridState.grid.rows; @@ -141,9 +177,6 @@ async function addWidget(widgetType) { function renderCanvas() { const canvas = document.getElementById('widget-canvas'); - const wrap = document.getElementById('widget-canvas-wrap'); - wrap.style.setProperty('--grid-cols', gridState.grid.cols); - wrap.style.setProperty('--grid-rows', gridState.grid.rows); canvas.innerHTML = ''; document.getElementById('widget-canvas-empty-hint').style.display = gridState.widgets.length ? 'none' : ''; @@ -151,7 +184,7 @@ function renderCanvas() { const box = document.createElement('div'); box.className = 'widget-box'; box.dataset.widgetType = widget.widget_type; - positionBox(box, widget); + box._rect = widget; const label = document.createElement('span'); label.className = 'widget-box-label'; @@ -176,6 +209,7 @@ function renderCanvas() { canvas.appendChild(box); } + layoutCanvas(); } function renderAddButtons() { @@ -206,4 +240,10 @@ async function loadWidgets() { } } +let resizeTimer = null; +window.addEventListener('resize', () => { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(layoutCanvas, 100); +}); + loadWidgets(); diff --git a/server/app/static/theme.css b/server/app/static/theme.css index 657db13..f40b088 100644 --- a/server/app/static/theme.css +++ b/server/app/static/theme.css @@ -323,24 +323,26 @@ button.secondary:hover { background: var(--surface-alt); } .info-box.warn { background: var(--warn-bg); color: var(--warn-text); border-color: transparent; } #widget-canvas-wrap { - --grid-cols: 8; - --grid-rows: 5; + /* Height is set in px by frame_layout.js (measured wrap width * rows/cols) + -- not CSS aspect-ratio, which isn't supported on every mobile browser + this app gets viewed from, and percentage heights on the widget boxes + below would silently collapse to 0 against an indeterminate-height + ancestor if it weren't. Box positions/sizes are likewise set in px by + JS, not CSS percentages, for the same cross-browser reason. */ width: 100%; max-width: 640px; - aspect-ratio: var(--grid-cols) / var(--grid-rows); + box-sizing: border-box; border: 1px solid var(--border); border-radius: 10px; background-color: var(--surface-alt); - background-image: - linear-gradient(to right, var(--border) 1px, transparent 1px), - linear-gradient(to bottom, var(--border) 1px, transparent 1px); - background-size: calc(100% / var(--grid-cols)) calc(100% / var(--grid-rows)); + background-repeat: repeat; } #widget-canvas { position: relative; width: 100%; height: 100%; } .widget-box { position: absolute; box-sizing: border-box; border: 2px solid var(--accent); + background: var(--surface-alt); /* fallback for browsers without color-mix() support */ background: color-mix(in srgb, var(--accent) 14%, var(--surface)); border-radius: 6px; display: flex;