Merge custom option test fix into mobile boards
This commit is contained in:
32 files changed
+798
-238
No files matched your search
@@ -5,12 +5,14 @@ The board uses fixed logical units:
|
||||
- one `1 × 1` item is exactly `200 × 200` logical pixels;
|
||||
- the gap is always `24` logical pixels, giving a `224` pixel track pitch;
|
||||
- multi-cell items include the gaps between their cells (`2 × 1` is `424 × 200`);
|
||||
- the canvas is transformed once with one uniform scale derived from its available width.
|
||||
- the canvas is zoomed once with one uniform scale derived from its available width.
|
||||
|
||||
Items keep the same logical dimensions at every viewport. The viewport reserves
|
||||
the painted size while the inner canvas remains at its logical dimensions, so a
|
||||
widget's `clientWidth`, `clientHeight`, and `ResizeObserver` measurements stay
|
||||
stable.
|
||||
stable. The visual canvas always fits the available width, every root lane
|
||||
keeps at least one viewport of height, and edit-mode previews extend it downward
|
||||
as needed.
|
||||
|
||||
The read-only renderer should:
|
||||
|
||||
|
||||
@@ -5,6 +5,5 @@
|
||||
export const LOGICAL_GRID_CELL_SIZE = 200;
|
||||
export const LOGICAL_GRID_GAP = 24;
|
||||
export const LOGICAL_GRID_PITCH = LOGICAL_GRID_CELL_SIZE + LOGICAL_GRID_GAP;
|
||||
export const MIN_ACCESSIBLE_CANVAS_SCALE = 0.75;
|
||||
|
||||
export const COLLAPSED_SECTION_ROW_COUNT = 0.5;
|
||||
@@ -21,6 +21,13 @@ export const getLogicalItemStyle = ({ x, y, w, h }: GridCoordinates): CSSPropert
|
||||
export const getLayoutRowCount = (placements: readonly GridCoordinates[]) =>
|
||||
placements.reduce((rowCount, placement) => Math.max(rowCount, placement.y + placement.h), 0);
|
||||
|
||||
export const getGridRowCountForVisualHeight = (visualHeight: number, visualScale: number) => {
|
||||
if (!Number.isFinite(visualHeight) || visualHeight <= 0) return 1;
|
||||
if (!Number.isFinite(visualScale) || visualScale <= 0) return 1;
|
||||
|
||||
return Math.max(1, Math.ceil((visualHeight / visualScale + LOGICAL_GRID_GAP) / LOGICAL_GRID_PITCH));
|
||||
};
|
||||
|
||||
export const normalizeGridPlacement = <TPlacement extends GridPlacement>(
|
||||
placement: TPlacement,
|
||||
columnCount: number,
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
overflow-x: auto;
|
||||
overscroll-behavior-inline: contain;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.viewport[data-board-hydrated="false"] {
|
||||
@@ -18,8 +16,6 @@
|
||||
.canvas {
|
||||
position: absolute;
|
||||
inset: 0 auto auto 0;
|
||||
transform-origin: top left;
|
||||
will-change: transform;
|
||||
|
||||
/*
|
||||
* Keep dashboard UI at its normal readable density while the fixed canvas
|
||||
@@ -44,9 +40,3 @@
|
||||
--mantine-h5-font-size: calc(1rem * var(--board-canvas-ui-scale, 1));
|
||||
--mantine-h6-font-size: calc(0.875rem * var(--board-canvas-ui-scale, 1));
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.canvas {
|
||||
will-change: auto;
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,8 @@ import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { Box } from "@mantine/core";
|
||||
import { useElementSize } from "@mantine/hooks";
|
||||
|
||||
import { MIN_ACCESSIBLE_CANVAS_SCALE } from "./constants";
|
||||
import classes from "./scaled-board-canvas.module.css";
|
||||
|
||||
export { MIN_ACCESSIBLE_CANVAS_SCALE };
|
||||
|
||||
const BoardCanvasScaleContext = createContext(1);
|
||||
|
||||
export const useBoardCanvasScale = () => useContext(BoardCanvasScaleContext);
|
||||
@@ -18,7 +15,7 @@ export const calculateBoardCanvasScale = (availableWidth: number, logicalWidth:
|
||||
if (!Number.isFinite(availableWidth) || !Number.isFinite(logicalWidth)) return 1;
|
||||
if (availableWidth <= 0 || logicalWidth <= 0) return 1;
|
||||
|
||||
return Math.max(MIN_ACCESSIBLE_CANVAS_SCALE, availableWidth / logicalWidth);
|
||||
return availableWidth / logicalWidth;
|
||||
};
|
||||
|
||||
export const calculateBoardUiScale = (canvasScale: number) => {
|
||||
@@ -36,9 +33,8 @@ interface ScaledBoardCanvasProps {
|
||||
|
||||
/**
|
||||
* Scales the complete board as one surface while keeping widget layout in
|
||||
* fixed logical pixels. The scale floor deliberately allows horizontal
|
||||
* overflow so browser zoom still magnifies content instead of being cancelled
|
||||
* by responsive downscaling.
|
||||
* fixed logical pixels. The complete canvas always fits its viewport so board
|
||||
* content can never widen the document or require horizontal scrolling.
|
||||
*/
|
||||
export const ScaledBoardCanvas = ({
|
||||
logicalWidth,
|
||||
@@ -62,6 +58,7 @@ export const ScaledBoardCanvas = ({
|
||||
[logicalWidth, resolvedAvailableWidth],
|
||||
);
|
||||
const uiScale = calculateBoardUiScale(scale);
|
||||
const inverseScale = scale > 0 ? 1 / scale : 1;
|
||||
const visualWidth = logicalWidth * scale;
|
||||
const hasHorizontalOverflow = resolvedAvailableWidth > 0 && visualWidth - resolvedAvailableWidth > 0.5;
|
||||
|
||||
@@ -84,8 +81,9 @@ export const ScaledBoardCanvas = ({
|
||||
className={classes.canvas}
|
||||
w={logicalWidth}
|
||||
style={{
|
||||
"--board-canvas-inverse-scale": inverseScale,
|
||||
"--board-canvas-ui-scale": uiScale,
|
||||
transform: `scale(${scale})`,
|
||||
zoom: scale,
|
||||
}}
|
||||
>
|
||||
<BoardCanvasScaleContext.Provider value={scale}>{children}</BoardCanvasScaleContext.Provider>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { getLayoutRowCount, getLogicalItemStyle, getLogicalTrackSize, normalizeGridPlacement } from "../index";
|
||||
import {
|
||||
getGridRowCountForVisualHeight,
|
||||
getLayoutRowCount,
|
||||
getLogicalItemStyle,
|
||||
getLogicalTrackSize,
|
||||
normalizeGridPlacement,
|
||||
} from "../index";
|
||||
|
||||
describe("fixed dashboard geometry", () => {
|
||||
test("keeps a 1x1 item exactly 200px with a fixed 24px gap", () => {
|
||||
@@ -24,4 +30,12 @@ describe("fixed dashboard geometry", () => {
|
||||
expect(placement).toEqual({ id: "wide", x: 0, y: 0, w: 4, h: 2 });
|
||||
expect(getLayoutRowCount([placement, { x: 0, y: 4, w: 1, h: 3 }])).toBe(7);
|
||||
});
|
||||
|
||||
test("derives enough logical rows to fill the visual viewport at any canvas scale", () => {
|
||||
expect(getGridRowCountForVisualHeight(1200, 1)).toBe(6);
|
||||
expect(getLogicalTrackSize(getGridRowCountForVisualHeight(1200, 1))).toBeGreaterThanOrEqual(1200);
|
||||
|
||||
expect(getGridRowCountForVisualHeight(1200, 0.5)).toBe(11);
|
||||
expect(getLogicalTrackSize(getGridRowCountForVisualHeight(1200, 0.5)) * 0.5).toBeGreaterThanOrEqual(1200);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { calculateBoardCanvasScale, calculateBoardUiScale, MIN_ACCESSIBLE_CANVAS_SCALE } from "../scaled-board-canvas";
|
||||
import { calculateBoardCanvasScale, calculateBoardUiScale } from "../scaled-board-canvas";
|
||||
|
||||
describe("board canvas scale policy", () => {
|
||||
test("fits the logical canvas exactly when the viewport can preserve the minimum scale", () => {
|
||||
test("fits the logical canvas exactly", () => {
|
||||
const scale = calculateBoardCanvasScale(840, 1000);
|
||||
|
||||
expect(scale).toBe(0.84);
|
||||
@@ -14,16 +14,16 @@ describe("board canvas scale policy", () => {
|
||||
expect(calculateBoardCanvasScale(1800, 1000)).toBe(1.8);
|
||||
});
|
||||
|
||||
test("stops counter-scaling at 0.75 and lets the canvas overflow", () => {
|
||||
test("keeps fitting narrow viewports without horizontal overflow", () => {
|
||||
const availableWidth = 600;
|
||||
const logicalWidth = 1000;
|
||||
const scale = calculateBoardCanvasScale(availableWidth, logicalWidth);
|
||||
|
||||
expect(scale).toBe(MIN_ACCESSIBLE_CANVAS_SCALE);
|
||||
expect(logicalWidth * scale).toBeGreaterThan(availableWidth);
|
||||
expect(scale).toBe(0.6);
|
||||
expect(logicalWidth * scale).toBe(availableWidth);
|
||||
});
|
||||
|
||||
test("keeps content materially magnified at 200% effective zoom", () => {
|
||||
test("keeps the board fitted at 200% effective zoom", () => {
|
||||
const logicalWidth = 1696;
|
||||
const normalAvailableWidth = 1872;
|
||||
const normalScale = calculateBoardCanvasScale(normalAvailableWidth, logicalWidth);
|
||||
@@ -31,8 +31,8 @@ describe("board canvas scale policy", () => {
|
||||
const normalPaintedCellSize = 200 * normalScale;
|
||||
const zoomedPaintedCellSize = 200 * zoomedScale * 2;
|
||||
|
||||
expect(zoomedScale).toBe(MIN_ACCESSIBLE_CANVAS_SCALE);
|
||||
expect(zoomedPaintedCellSize).toBeGreaterThan(normalPaintedCellSize * 1.25);
|
||||
expect(zoomedScale).toBeCloseTo(normalScale / 2);
|
||||
expect(zoomedPaintedCellSize).toBeCloseTo(normalPaintedCellSize);
|
||||
});
|
||||
|
||||
test.each([
|
||||
|
||||
Reference in new issue
Block a user