Public: anyone can read this; the crew can change it.
On a phone, the forge pill in a top corner sits 48 px down, clear of the camera, and opening its panel moves nothing #7717
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/7714-on-a-phone-the-forge-pill-in-a-top-corne → main 3 commits fork 1 thread · 1 unresolved
3 files
changed +58
−4
src/apps/toolbar.ts +5 −1
| @@ -362,10 +362,14 @@ export const toolbarJs = `(function () { | ||
| 362 | 362 | var saved = window.localStorage.getItem(KEY); |
| 363 | 363 | if (CORNERS.indexOf(saved) >= 0) corner = saved; |
| 364 | 364 | } catch (e) {} |
| 365 | var NARROW_MAX = 600, CAMERA_TOP = "48px"; | |
| 365 | 366 | function place(c) { |
| 366 | 367 | corner = c; |
| 367 | 368 | var top = c.charAt(0) === "t", left = c.charAt(1) === "l"; |
| 368 | host.style.top = top ? "12px" : "auto"; | |
| 369 | var narrow = top && window.innerWidth <= NARROW_MAX; | |
| 370 | host.style.top = top ? (narrow ? CAMERA_TOP : "12px") : "auto"; | |
| 371 | // The cssText cap assumes a pill 12px down; 36px lower, the cap drops by as much. | |
| 372 | panel.style.maxHeight = narrow ? "min(70vh,calc(100vh - 124px))" : ""; | |
| 369 | 373 | host.style.bottom = top ? "auto" : "12px"; |
| 370 | 374 | host.style.left = left ? "12px" : "auto"; |
| 371 | 375 | host.style.right = left ? "auto" : "12px"; |
test/toolbar-camera.test.ts added +50
| @@ -0,0 +1,50 @@ | ||
| 1 | /** | |
| 2 | * On a phone a top-corner pill sits clear of the camera (#7714), and opening | |
| 3 | * the panel moves nothing. | |
| 4 | */ | |
| 5 | ||
| 6 | import { describe, expect, test } from "bun:test"; | |
| 7 | import { boot } from "./toolbar-move.test.ts"; | |
| 8 | ||
| 9 | const at = (corner: string, width: number) => boot({ "forge-pill-corner": corner }, width, 844); | |
| 10 | ||
| 11 | describe("the pill and the camera", () => { | |
| 12 | for (const c of ["tl", "tr"]) { | |
| 13 | test(`${c} sits 48px down at 390 wide and 360 wide, 12px at 1440`, () => { | |
| 14 | expect(at(c, 390).host.style.top).toBe("48px"); | |
| 15 | expect(at(c, 360).host.style.top).toBe("48px"); | |
| 16 | expect(at(c, 600).host.style.top).toBe("48px"); | |
| 17 | expect(at(c, 601).host.style.top).toBe("12px"); | |
| 18 | expect(at(c, 1440).host.style.top).toBe("12px"); | |
| 19 | }); | |
| 20 | } | |
| 21 | ||
| 22 | for (const c of ["bl", "br"]) { | |
| 23 | test(`${c} keeps bottom:12px at any width`, () => { | |
| 24 | for (const w of [360, 390, 1440]) { | |
| 25 | const { host, panel } = at(c, w); | |
| 26 | expect(host.style.bottom).toBe("12px"); | |
| 27 | expect(host.style.top).toBe("auto"); | |
| 28 | expect(panel.style.maxHeight ?? "").toBe(""); | |
| 29 | } | |
| 30 | }); | |
| 31 | } | |
| 32 | ||
| 33 | test("a narrow top-corner panel is capped for the lower pill; others keep the pinned cap", () => { | |
| 34 | expect(at("tr", 390).panel.style.maxHeight).toBe("min(70vh,calc(100vh - 124px))"); | |
| 35 | expect(at("tr", 1440).panel.style.maxHeight).toBe(""); | |
| 36 | }); | |
| 37 | ||
| 38 | test("opening and closing the panel leaves the host where it was", () => { | |
| 39 | for (const [c, w] of [["tr", 390], ["tl", 390], ["tr", 1440], ["br", 390]] as const) { | |
| 40 | const { host, pill, panel, fire } = at(c, w); | |
| 41 | const before = { ...host.style }; | |
| 42 | fire(pill, "click"); | |
| 43 | expect(panel.style.display).toBe("flex"); | |
| 44 | expect(host.style).toEqual(before); | |
| 45 | fire(pill, "click"); | |
| 46 | expect(panel.style.display).toBe("none"); | |
| 47 | expect(host.style).toEqual(before); | |
| 48 | } | |
| 49 | }); | |
| 50 | }); |
test/toolbar-move.test.ts +3 −3
| @@ -33,7 +33,7 @@ const make = (tag: string): El => ({ | ||
| 33 | 33 | }); |
| 34 | 34 | const all = (el: El): El[] => [el, ...el.children.flatMap(all)]; |
| 35 | 35 | |
| 36 | function boot(store: Record<string, string> | null, width = 400, height = 800) { | |
| 36 | export function boot(store: Record<string, string> | null, width = 400, height = 800) { | |
| 37 | 37 | let root!: El; |
| 38 | 38 | const host = { el: null as El | null }; |
| 39 | 39 | const doc = { |
| @@ -80,7 +80,7 @@ describe("moving the pill", () => { | ||
| 80 | 80 | fire(pill, "pointermove", { clientX: 100, clientY: 60 }); |
| 81 | 81 | expect(host.style.left).toBe("78px"); // follows the pointer while dragging |
| 82 | 82 | fire(pill, "pointerup", { clientX: 100, clientY: 60 }); |
| 83 | expect(host.style.top).toBe("12px"); | |
| 83 | expect(host.style.top).toBe("48px"); | |
| 84 | 84 | expect(host.style.left).toBe("12px"); |
| 85 | 85 | expect(host.style.right).toBe("auto"); |
| 86 | 86 | expect(box.style.flexDirection).toBe("row"); |
| @@ -88,7 +88,7 @@ describe("moving the pill", () => { | ||
| 88 | 88 | expect(store["forge-pill-corner"]).toBe("tl"); |
| 89 | 89 | // The next page load puts it there again. |
| 90 | 90 | expect(boot(store).host.style.left).toBe("12px"); |
| 91 | expect(boot(store).host.style.top).toBe("12px"); | |
| 91 | expect(boot(store).host.style.top).toBe("48px"); | |
| 92 | 92 | }); |
| 93 | 93 | |
| 94 | 94 | test("a press that barely moves is a tap, not a drag", () => { |
new above
offline
Non-blocking: this import re-runs every test in toolbar-move.test.ts. Extract boot into a helper module both files import.