Fix Layout tab canvas not rendering on mobile
Build and push server image / test (push) Successful in 20s
Build and push server image / build-and-push (push) Successful in 1m58s

The placement canvas sized itself with CSS aspect-ratio plus
percentage widths/heights on the widget boxes. aspect-ratio isn't
supported on every mobile browser this app gets viewed from, and
without it a percentage height on an absolutely-positioned child
collapses to 0 against an indeterminate-height ancestor -- the canvas
rendered with no visible size at all, just unstyled labels and
buttons floating in normal document flow. Confirmed working on
desktop, broken on the reporter's phone.

Canvas and widget-box geometry is now computed and applied in pixels
from JS (measuring the wrap's own width, deriving height from the
grid's row/col ratio), recalculated on window resize. Also gives
.widget-box's color-mix() background a plain-color fallback for the
same class of older-browser gap.
This commit is contained in:
2026-07-24 13:08:09 -04:00
parent 5d4bb53b8a
commit 77fe78d874
2 changed files with 62 additions and 20 deletions
+53 -13
View File
@@ -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();
+9 -7
View File
@@ -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;