mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
refactor(ui): remove floating, resizable rail panels
PR #768 bundled a portable-panel windowing system with the Street Level
layer. Remove it so CCTV and Street Level are ordinary docked right-rail
panels again, as on main before the merge.
Removed:
- header drag to float, 8-way edge/corner resize, double-press snap-back,
dockOnCollapse, command-dock clearance, float hint toast, floating z floor,
persisted floating geometry and ResizeObserver re-clamping
(panelPositionControls.js restored to its pre-#768 version)
- src/ui/panelResize.js and its tests, panelDock.test.mjs
- onPanelResized/dockPanel wiring in PanelChrome, the application shell and
StreetLevelControls (EXPAND/SHRINK no longer docks)
- :not(.panel-floating) guards and .panel-floating/.panel-portable/resize CSS
- qa:panel-resize, qa-panel-resize.mjs, qa-panelDrag.mjs, their tests, the
CI "Portable panel gate" step, QA_FAIL_ON_RETRY, and the Street Level
gate's float/resize/dock steps
- floating-panel docs in README, CHANGELOG, CURRENT-STATE, KNOWN-ISSUES
Kept: the Street Level panel as a docked rail panel (share/cockpit/rail
lists, layout controller, rail and Cyber CSS, EXPAND viewer), and the
independent right-rail alignment fix (settledLeftRailTop, 9dde7b4).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
7cbe04f515
commit
fbbc4cda5c
31 files changed
+201
-2945
No files matched your search
+75
-210
@@ -8,9 +8,8 @@
|
||||
* because the bundle carries it; CI uses a dummy one. Fixture runs also stage
|
||||
* a rejected key and a keyless second server.
|
||||
*
|
||||
* `--strict` fails on any skip not in STRICT_ALLOWED_SKIPS. `--fail-on-retry`
|
||||
* (or QA_FAIL_ON_RETRY=1) fails a missed header press unless the panel moved.
|
||||
* A failed run saves evidence under qa-artifacts/street-level/.
|
||||
* `--strict` fails on any skip not in STRICT_ALLOWED_SKIPS. A failed run saves
|
||||
* evidence under qa-artifacts/street-level/.
|
||||
*/
|
||||
import assert from 'node:assert/strict';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
@@ -18,12 +17,6 @@ import { LAYER_STATE_REGISTRY } from '../src/data/layerState.js';
|
||||
import { tileBounds } from '../src/layers/streetLevel/tileMath.js';
|
||||
import { encodeCoverageTile } from '../src/layers/streetLevel/providers/mapillary/coverageFixture.mjs';
|
||||
import { COVERAGE_MOVE_DEBOUNCE_MS } from '../src/layers/streetLevel/providers/mapillary/policy.js';
|
||||
import {
|
||||
describePress,
|
||||
dockPanelByDoubleClick,
|
||||
liftPanelByHeader,
|
||||
waitForStill,
|
||||
} from './qa-panelDrag.mjs';
|
||||
import {
|
||||
hookRenderErrors,
|
||||
readRenderErrors,
|
||||
@@ -339,6 +332,38 @@ async function clearFirstRun(page) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Best effort: wait until the panel's box has held still for `stillMs`, so a
|
||||
* point read from it does not move before the press.
|
||||
*/
|
||||
async function waitForStill(
|
||||
page,
|
||||
panelId,
|
||||
{ stillMs = 300, timeout = 5000 } = {},
|
||||
) {
|
||||
try {
|
||||
await page.waitForFunction(
|
||||
(id, ms) => {
|
||||
const r = document.getElementById(id).getBoundingClientRect();
|
||||
const box = [r.left, r.top, r.width, r.height].map(Math.round).join();
|
||||
const now = performance.now();
|
||||
if (window.__qaStillBox !== box) {
|
||||
window.__qaStillBox = box;
|
||||
window.__qaStillSince = now;
|
||||
}
|
||||
return now - window.__qaStillSince >= ms;
|
||||
},
|
||||
{ polling: 100, timeout },
|
||||
panelId,
|
||||
stillMs,
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error?.name !== 'TimeoutError') throw error;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click a panel control once the panel holds still and the control is what
|
||||
* lies under its centre: the rail animates panel heights, so a point read
|
||||
@@ -528,7 +553,6 @@ async function main() {
|
||||
const url = urlIndex >= 0 ? args[urlIndex + 1] : 'http://localhost:4173';
|
||||
const fixtures = args.includes('--fixtures');
|
||||
const strict = args.includes('--strict');
|
||||
if (args.includes('--fail-on-retry')) process.env.QA_FAIL_ON_RETRY = '1';
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath:
|
||||
@@ -1425,12 +1449,6 @@ async function main() {
|
||||
await stacks.set(original);
|
||||
},
|
||||
);
|
||||
const expanded = () =>
|
||||
page.evaluate(() =>
|
||||
document
|
||||
.getElementById('sl-viewer-wrap')
|
||||
.classList.contains('sl-viewer-wrap-expanded'),
|
||||
);
|
||||
await step(
|
||||
'EXPAND opens a modal dialog (the rest of <body> inert) and Esc returns focus to the button',
|
||||
async () => {
|
||||
@@ -1512,209 +1530,56 @@ async function main() {
|
||||
assert.equal(after.sequence.selectedId, null);
|
||||
},
|
||||
);
|
||||
const floating = () =>
|
||||
page.$eval('#street-level-panel', (node) =>
|
||||
node.classList.contains('panel-floating'),
|
||||
);
|
||||
const center = (selector) =>
|
||||
page.$eval(selector, (node) => {
|
||||
const r = node.getBoundingClientRect();
|
||||
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
||||
});
|
||||
// Right after a dock the rail is still animating, so the shared helpers
|
||||
// re-resolve the header and retry only a press that provably missed it.
|
||||
const floatPanel = async () => {
|
||||
const attempt = await liftPanelByHeader(page, PANEL_ID, {
|
||||
dx: -400,
|
||||
dy: 80,
|
||||
});
|
||||
assert.equal(
|
||||
attempt.done,
|
||||
true,
|
||||
`a header drag lifts the panel out of the rail (${describePress(attempt)})`,
|
||||
);
|
||||
assert.equal(await floating(), true);
|
||||
};
|
||||
await step(
|
||||
'the panel floats on a header drag, resizes, and the viewer takes the room',
|
||||
async () => {
|
||||
await openById(firstImageId);
|
||||
await floatPanel();
|
||||
const viewerHeight = () =>
|
||||
page.$eval('#sl-viewer', (node) =>
|
||||
Math.round(node.getBoundingClientRect().height),
|
||||
);
|
||||
const before = await viewerHeight();
|
||||
const grip = await center('#street-level-panel .panel-resize-grip');
|
||||
await page.mouse.move(grip.x, grip.y);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(grip.x + 120, grip.y + 160, { steps: 10 });
|
||||
await page.mouse.up();
|
||||
await settle(
|
||||
page,
|
||||
(height) =>
|
||||
document.getElementById('sl-viewer').getBoundingClientRect()
|
||||
.height >
|
||||
height + 60,
|
||||
before,
|
||||
);
|
||||
const after = await viewerHeight();
|
||||
assert.ok(
|
||||
after > before + 60,
|
||||
`viewer grew with the window (${before} → ${after}px)`,
|
||||
);
|
||||
assert.equal(
|
||||
await page.$eval(
|
||||
'.sl-settings',
|
||||
(node) => getComputedStyle(node).overflowY,
|
||||
),
|
||||
'auto',
|
||||
'only the settings block scrolls',
|
||||
);
|
||||
},
|
||||
);
|
||||
await step(
|
||||
'SHRINK after EXPAND docks the window back in the rail at its default size, viewer inside the panel',
|
||||
async () => {
|
||||
await clickPanelControl(page, '#sl-viewer-expand');
|
||||
await settle(page, () =>
|
||||
document
|
||||
.getElementById('sl-viewer-wrap')
|
||||
.classList.contains('sl-viewer-wrap-expanded'),
|
||||
);
|
||||
assert.equal(await expanded(), true, 'expanded');
|
||||
await clickPanelControl(page, '#sl-viewer-expand');
|
||||
await settle(
|
||||
page,
|
||||
() =>
|
||||
!document
|
||||
.getElementById('street-level-panel')
|
||||
.classList.contains('panel-floating') &&
|
||||
!document
|
||||
.getElementById('sl-viewer-wrap')
|
||||
.classList.contains('sl-viewer-wrap-expanded'),
|
||||
);
|
||||
assert.equal(await floating(), false, 'docked again');
|
||||
const style = await page.$eval('#street-level-panel', (node) => ({
|
||||
width: node.style.width,
|
||||
height: node.style.height,
|
||||
parent: node.parentElement.id,
|
||||
}));
|
||||
assert.deepEqual(style, {
|
||||
width: '',
|
||||
height: '',
|
||||
parent: 'right-context-rail',
|
||||
});
|
||||
const home = await page.evaluate(() => {
|
||||
const wrap = document.getElementById('sl-viewer-wrap');
|
||||
return {
|
||||
inPanel: Boolean(wrap.closest('#street-level-panel')),
|
||||
onBody: wrap.parentElement === document.body,
|
||||
};
|
||||
});
|
||||
assert.deepEqual(
|
||||
home,
|
||||
{ inPanel: true, onBody: false },
|
||||
'the viewer returned into the panel',
|
||||
);
|
||||
},
|
||||
);
|
||||
await step('a header double-click docks a floating window', async () => {
|
||||
await floatPanel();
|
||||
const attempt = await dockPanelByDoubleClick(page, PANEL_ID);
|
||||
assert.equal(
|
||||
attempt.done,
|
||||
true,
|
||||
`docked again (${describePress(attempt)})`,
|
||||
);
|
||||
});
|
||||
await step(
|
||||
'collapsing a floating window docks it as the rail strip',
|
||||
async () => {
|
||||
// The double-click just docked a panel showing a photo: let the photo
|
||||
// and its sequence finish loading before pressing the header again.
|
||||
await uiUntil((u) => !u.street.loading && !u.sequence.loading);
|
||||
await floatPanel();
|
||||
await clickPanelControl(
|
||||
page,
|
||||
'.panel-collapse-btn[data-collapse-target="street-level-panel"]',
|
||||
);
|
||||
await settle(page, () => {
|
||||
const node = document.getElementById('street-level-panel');
|
||||
return (
|
||||
node.classList.contains('collapsed') &&
|
||||
!node.classList.contains('panel-floating')
|
||||
);
|
||||
});
|
||||
const state = await page.$eval('#street-level-panel', (node) => ({
|
||||
floating: node.classList.contains('panel-floating'),
|
||||
collapsed: node.classList.contains('collapsed'),
|
||||
height: node.style.height,
|
||||
}));
|
||||
assert.deepEqual(state, {
|
||||
floating: false,
|
||||
collapsed: true,
|
||||
height: '',
|
||||
});
|
||||
},
|
||||
);
|
||||
await step(
|
||||
'on a phone the whole photo fits in the docked panel',
|
||||
async () => {
|
||||
// Width alone drives the phone layout; toggling isMobile would reload.
|
||||
await page.setViewport({ width: 390, height: 844 });
|
||||
await settle(page, () => innerWidth === 390);
|
||||
if (
|
||||
await page.$eval('#street-level-panel', (node) =>
|
||||
node.classList.contains('collapsed'),
|
||||
)
|
||||
await step('on a phone the whole photo fits in the panel', async () => {
|
||||
// Width alone drives the phone layout; toggling isMobile would reload.
|
||||
await page.setViewport({ width: 390, height: 844 });
|
||||
await settle(page, () => innerWidth === 390);
|
||||
if (
|
||||
await page.$eval('#street-level-panel', (node) =>
|
||||
node.classList.contains('collapsed'),
|
||||
)
|
||||
await expandStrip();
|
||||
await openById(firstImageId);
|
||||
const measure = () =>
|
||||
page.evaluate(() => {
|
||||
const inner = document
|
||||
.querySelector('.street-level-panel-inner')
|
||||
.getBoundingClientRect();
|
||||
const meta = document
|
||||
.getElementById('sl-image-meta')
|
||||
.getBoundingClientRect();
|
||||
const viewer = document
|
||||
.getElementById('sl-viewer')
|
||||
.getBoundingClientRect();
|
||||
return {
|
||||
cut: Math.round(meta.bottom - inner.bottom),
|
||||
top: Math.round(viewer.top - inner.top),
|
||||
height: Math.round(viewer.height),
|
||||
overflowX: document.documentElement.scrollWidth > innerWidth,
|
||||
};
|
||||
});
|
||||
// The panel settles its phone layout over a frame or two.
|
||||
await settle(page, () => {
|
||||
)
|
||||
await expandStrip();
|
||||
await openById(firstImageId);
|
||||
const measure = () =>
|
||||
page.evaluate(() => {
|
||||
const inner = document
|
||||
.querySelector('.street-level-panel-inner')
|
||||
.getBoundingClientRect();
|
||||
const meta = document
|
||||
.getElementById('sl-image-meta')
|
||||
.getBoundingClientRect();
|
||||
return (
|
||||
meta.bottom - inner.bottom <= 1 &&
|
||||
document.getElementById('sl-viewer').getBoundingClientRect()
|
||||
.height >= 100
|
||||
);
|
||||
const viewer = document
|
||||
.getElementById('sl-viewer')
|
||||
.getBoundingClientRect();
|
||||
return {
|
||||
cut: Math.round(meta.bottom - inner.bottom),
|
||||
top: Math.round(viewer.top - inner.top),
|
||||
height: Math.round(viewer.height),
|
||||
overflowX: document.documentElement.scrollWidth > innerWidth,
|
||||
};
|
||||
});
|
||||
const fit = await measure();
|
||||
assert.ok(fit.cut <= 1, `photo and caption fit (${fit.cut}px cut)`);
|
||||
assert.ok(fit.height >= 100, `viewer stays usable (${fit.height}px)`);
|
||||
assert.equal(fit.overflowX, false, 'no sideways scroll');
|
||||
await page.setViewport(VIEWPORTS[0]);
|
||||
await settle(
|
||||
page,
|
||||
(width) => innerWidth === width,
|
||||
VIEWPORTS[0].width,
|
||||
// The panel settles its phone layout over a frame or two.
|
||||
await settle(page, () => {
|
||||
const inner = document
|
||||
.querySelector('.street-level-panel-inner')
|
||||
.getBoundingClientRect();
|
||||
const meta = document
|
||||
.getElementById('sl-image-meta')
|
||||
.getBoundingClientRect();
|
||||
return (
|
||||
meta.bottom - inner.bottom <= 1 &&
|
||||
document.getElementById('sl-viewer').getBoundingClientRect()
|
||||
.height >= 100
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
const fit = await measure();
|
||||
assert.ok(fit.cut <= 1, `photo and caption fit (${fit.cut}px cut)`);
|
||||
assert.ok(fit.height >= 100, `viewer stays usable (${fit.height}px)`);
|
||||
assert.equal(fit.overflowX, false, 'no sideways scroll');
|
||||
await page.setViewport(VIEWPORTS[0]);
|
||||
await settle(page, (width) => innerWidth === width, VIEWPORTS[0].width);
|
||||
});
|
||||
await step(
|
||||
'a reload with the layer on keeps a collapsed panel collapsed, and the share link carries its ui token',
|
||||
async () => {
|
||||
|
||||
Reference in new issue
Block a user