fix(dashboard): unsqueeze board on Android tablets

- Drop the tablet-tier `.board` grid rule that crammed 6 columns into
  ≤1024px viewports with no min-width, scrunching column content to
  unreadable widths. Tablets now use the default `minmax(300px, 1fr)`
  and scroll horizontally like desktop.
- Drop `maximum-scale=1.0, user-scalable=no` from the viewport meta.
  Combined with `initial-scale=1.0` those flags trigger Android Chrome
  layout bugs in multi-window mode; the Capacitor-feel justification
  isn't worth the breakage in a browser-rendered dashboard.
- Broaden the existing iOS scroll-snap stabilization in Board.tsx from
  `(max-width: 768px)` to any touch-primary device, and re-run it the
  first time tasks populate so Android tablets get the same first-cards-
  loaded reflow that mobile already had.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-21 23:05:15 -07:00
parent 496d275595
commit fbf7e2cb4d
9 changed files with 27 additions and 172 deletions

View File

@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { MobileNavBar } from "../MobileNavBar";
@@ -761,83 +761,4 @@ describe("MobileNavBar", () => {
});
});
});
describe("ResizeObserver height measurement", () => {
const originalResizeObserver = globalThis.ResizeObserver;
let resizeObserverCallback: ResizeObserverCallback | null = null;
let offsetHeightValue = 52;
class ResizeObserverMock {
constructor(callback: ResizeObserverCallback) {
resizeObserverCallback = callback;
}
observe() {}
unobserve() {}
disconnect() {}
}
afterEach(() => {
globalThis.ResizeObserver = originalResizeObserver;
resizeObserverCallback = null;
offsetHeightValue = 52;
document.documentElement.style.removeProperty("--mobile-nav-height");
vi.restoreAllMocks();
});
it("publishes measured height on mount and clears inline token on unmount", () => {
globalThis.ResizeObserver = ResizeObserverMock as typeof ResizeObserver;
vi.spyOn(window, "getComputedStyle").mockReturnValue({ paddingBottom: "0px" } as CSSStyleDeclaration);
const { container, unmount } = render(<MobileNavBar {...createDefaultProps()} />);
const navEl = container.querySelector(".mobile-nav-bar") as HTMLElement;
Object.defineProperty(navEl, "offsetHeight", {
configurable: true,
get: () => offsetHeightValue,
});
if (resizeObserverCallback) {
resizeObserverCallback([], {} as ResizeObserver);
}
expect(document.documentElement.style.getPropertyValue("--mobile-nav-height")).toBe("52px");
unmount();
expect(document.documentElement.style.getPropertyValue("--mobile-nav-height")).toBe("");
});
it("updates measured token when ResizeObserver callback fires", () => {
globalThis.ResizeObserver = ResizeObserverMock as typeof ResizeObserver;
vi.spyOn(window, "getComputedStyle").mockReturnValue({ paddingBottom: "0px" } as CSSStyleDeclaration);
const { container } = render(<MobileNavBar {...createDefaultProps()} />);
const navEl = container.querySelector(".mobile-nav-bar") as HTMLElement;
Object.defineProperty(navEl, "offsetHeight", {
configurable: true,
get: () => offsetHeightValue,
});
if (resizeObserverCallback) {
resizeObserverCallback([], {} as ResizeObserver);
}
expect(document.documentElement.style.getPropertyValue("--mobile-nav-height")).toBe("52px");
offsetHeightValue = 58;
if (resizeObserverCallback) {
resizeObserverCallback([{ target: navEl } as ResizeObserverEntry], {} as ResizeObserver);
}
expect(document.documentElement.style.getPropertyValue("--mobile-nav-height")).toBe("58px");
});
it("still performs initial measurement when ResizeObserver is unavailable", () => {
// @ts-expect-error test intentionally clears ResizeObserver
delete globalThis.ResizeObserver;
vi.spyOn(window, "getComputedStyle").mockReturnValue({ paddingBottom: "0px" } as CSSStyleDeclaration);
render(<MobileNavBar {...createDefaultProps()} />);
expect(document.documentElement.style.getPropertyValue("--mobile-nav-height")).toBe("44px");
});
});
});