mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
docs(street-level): shorten docs and comments before merge
Trim the doc text this PR adds (README, CHANGELOG, CURRENT-STATE, KNOWN-ISSUES, SECURITY, DATA_SOURCES, .env.example, the CI job comment), drop code comments that restate the code, cut design-note JSDoc to a line or two, and drop review tags from test names. Comments only; no code changes. Also: README key count (seven keys, two metered), CURRENT-STATE's gate paragraph (CI runs a production build with a dummy token), the stacked doc above meshBoxInRange and the orphaned inMeshRange line in coverage.js, and a duplicate ALPR row in DATA_SOURCES that a merge had brought back. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
c65feda4b1
commit
4ea6631425
85 files changed
+701
-1520
No files matched your search
@@ -954,8 +954,7 @@ export class StyleManager extends ShellFacade {
|
||||
this.setPanelCollapsed('street-level-panel', collapsed, options),
|
||||
dockPanel: () => this._panelChrome.dockPanel('street-level-panel'),
|
||||
showToast: (message) => this._showToast(message),
|
||||
// Who asked for Street Level to switch on: only a user, voice or tool
|
||||
// request opens the panel, never a saved-state restore.
|
||||
// Lets the panel open for a user, voice or tool switch-on, not a restore.
|
||||
subscribeEnableRequests: (listener) =>
|
||||
this._dataManager?.subscribeVisibilityRequests?.((change) => {
|
||||
if (change?.layerId === 'street-level' && change.enabled)
|
||||
@@ -1509,12 +1508,7 @@ export class StyleManager extends ShellFacade {
|
||||
this._initCockpitDisplayPortal();
|
||||
}
|
||||
|
||||
/**
|
||||
* A portable panel finished a resize or snapped back to its rail. A panel
|
||||
* with a viewport of its own refits here.
|
||||
* @param {string} panelId
|
||||
* @returns {void}
|
||||
*/
|
||||
/** A portable panel was resized or docked: refit any viewport inside it. */
|
||||
_onPanelResized(panelId) {
|
||||
if (panelId === 'cctv-panel') this._syncCctvPanelViewport();
|
||||
if (panelId === 'street-level-panel')
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ export function syncChipGroup(container, chips = [], { before = null } = {}) {
|
||||
`data-toggle-chip chip-${state}${chip.active ? ' active' : ''}`,
|
||||
);
|
||||
set(button, 'textContent', chip.label);
|
||||
// Optional per-chip colour (a Street Level source); CSS decides its use.
|
||||
// Optional per-chip colour; CSS decides how it is used.
|
||||
const color = chip.color || '';
|
||||
if ((button.dataset.chipColor || '') !== color) {
|
||||
if (color) {
|
||||
|
||||
+4
-10
@@ -388,11 +388,7 @@ export class PanelChrome {
|
||||
this.shareLinkManager?.onPanelStateChange?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dock a floating portable panel back into its rail at its default size.
|
||||
* @param {string} panelId
|
||||
* @returns {boolean} Whether the panel was floating.
|
||||
*/
|
||||
/** @returns {boolean} Whether the panel was floating and is now docked. */
|
||||
dockPanel(panelId) {
|
||||
return this._panelPosition?.dockPanel?.(panelId) === true;
|
||||
}
|
||||
@@ -419,8 +415,7 @@ export class PanelChrome {
|
||||
panelEl.classList.contains('cyber-accordion-collapsed');
|
||||
// A user opening a Cyber rail panel owns the whole accordion, including
|
||||
// peers that were only presentation-collapsed during a saved-state restore.
|
||||
// A floating window is outside the accordion both ways: opening one
|
||||
// leaves the rail's panels as they are.
|
||||
// A floating window is outside the accordion both ways.
|
||||
if (
|
||||
explicit &&
|
||||
!restore &&
|
||||
@@ -433,7 +428,6 @@ export class PanelChrome {
|
||||
if (
|
||||
peer !== panelEl &&
|
||||
peer.matches('[data-panel-id]') &&
|
||||
// A floating window is not part of the rail's accordion.
|
||||
!peer.classList.contains('panel-floating') &&
|
||||
!peer.hidden
|
||||
) {
|
||||
@@ -562,8 +556,8 @@ export class PanelChrome {
|
||||
}
|
||||
}
|
||||
panelEl.classList.toggle('collapsed', nextCollapsed);
|
||||
// Only a user's own collapse docks a floating window; cockpit entry,
|
||||
// restores and accordion peers must not discard where they put it.
|
||||
// Only a user's own collapse docks a floating window; restores, cockpit
|
||||
// entry and accordion peers keep its place.
|
||||
if (explicit && !restore)
|
||||
this._panelPosition?.onPanelCollapsed?.(panelId, nextCollapsed);
|
||||
if (
|
||||
|
||||
@@ -377,10 +377,9 @@ test('a header press that travels under the drag threshold is a click, not a lif
|
||||
});
|
||||
|
||||
/**
|
||||
* Both portable panels wired through _initPanelDrag. Each box follows its
|
||||
* inline left/top/width/height like a fixed window (`box` holds the natural
|
||||
* size), its header is the top 36 px, and `obstacles` stand in for the
|
||||
* command dock and voice pill. ResizeObserver callbacks run on `observe()`.
|
||||
* Both portable panels wired through _initPanelDrag. Headers are the top
|
||||
* 36 px; `obstacles` stand in for the command dock and voice pill.
|
||||
* ResizeObserver callbacks run on `observe()`.
|
||||
*/
|
||||
function portableShell(f, { stored = {}, box = {}, obstacles = {} } = {}) {
|
||||
const observers = [];
|
||||
|
||||
@@ -205,8 +205,7 @@ export class PanelLayoutController {
|
||||
this._streetLevelPanel,
|
||||
]) {
|
||||
if (!panel) continue;
|
||||
// A portable panel restored as a floating window keeps the place and
|
||||
// size PanelPositionControls gave it; only docked panels are reset.
|
||||
// A floating window keeps its restored place and size.
|
||||
if (!panel.classList.contains('panel-floating')) {
|
||||
for (const property of ['top', 'right', 'bottom', 'left', 'z-index'])
|
||||
panel.style.removeProperty(property);
|
||||
|
||||
@@ -13,37 +13,29 @@ const PANEL_POSITION_STORAGE_VERSION = 'v8';
|
||||
const PANEL_Z_BASE = 100;
|
||||
const PANEL_Z_MAX = 139;
|
||||
/**
|
||||
* Lowest z a promoted (dragged or floating) panel may take: above every panel
|
||||
* docked in a rail, the highest of which is #pp-toggles at 110 (controls.css).
|
||||
* Renumbering restarts here, so a floating window never slips under DISPLAY.
|
||||
* Keep in step with `#right-context-rail > .panel-floating` in controls.css.
|
||||
* Lowest z for a promoted panel: above every docked rail panel (#pp-toggles is
|
||||
* 110). Keep in step with `#right-context-rail > .panel-floating` in controls.css.
|
||||
*/
|
||||
const PANEL_Z_FLOATING_FLOOR = PANEL_Z_BASE + 11;
|
||||
/** Pointer travel before a header press becomes a drag that lifts a portable panel out. */
|
||||
/** Pointer travel before a press becomes a drag or resize. */
|
||||
const DRAG_THRESHOLD_PX = 4;
|
||||
/**
|
||||
* Two header presses this close in time and space snap a floating panel
|
||||
* back. Detected from pointerdown because the drag handler's
|
||||
* preventDefault() suppresses the mouse events a native dblclick needs.
|
||||
* Double press that snaps a floating panel back. Detected from pointerdown
|
||||
* because the drag's preventDefault() suppresses a native dblclick.
|
||||
*/
|
||||
const DOUBLE_PRESS_MS = 400;
|
||||
const DOUBLE_PRESS_SLOP_PX = 6;
|
||||
/** Viewport margin every positioned panel keeps clear. */
|
||||
const VIEWPORT_MARGIN_PX = 6;
|
||||
/**
|
||||
* Fixed chrome that paints over every panel: the rail is its own stacking
|
||||
* context (z 110), below the command dock (145) and the voice pill (150). A
|
||||
* floating window keeps its header clear of these so it can always be
|
||||
* grabbed again.
|
||||
* Fixed chrome that paints over the rail's stacking context; a floating
|
||||
* window keeps its header clear of it so it can always be grabbed again.
|
||||
*/
|
||||
const PANEL_OBSTACLE_IDS = ['command-dock', 'gev-voice-control'];
|
||||
/** One-time hint shown the first time a portable panel leaves its rail. */
|
||||
const PANEL_FLOAT_HINT_STORAGE_KEY = `godsEyeView.${PANEL_POSITION_STORAGE_VERSION}.panelFloatHintShown`;
|
||||
const PANEL_FLOAT_HINT = 'Double-click the header to snap the panel back';
|
||||
/** Header children whose own interaction wins over a drag or a snap-back. */
|
||||
const INTERACTIVE_SELECTOR =
|
||||
'input, select, option, textarea, button, a, [role="button"]';
|
||||
/** Inline geometry a snap-back clears so the rail owns placement again. */
|
||||
const FLOATING_STYLE_PROPERTIES = [
|
||||
'left',
|
||||
'top',
|
||||
@@ -51,7 +43,6 @@ const FLOATING_STYLE_PROPERTIES = [
|
||||
'bottom',
|
||||
'width',
|
||||
'height',
|
||||
// A docked panel takes the rail's stacking, not a floating window's.
|
||||
'z-index',
|
||||
];
|
||||
|
||||
@@ -59,7 +50,6 @@ function clamp(value, min, max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
/** A window extent that fits the viewport margins but never drops under its minimum. */
|
||||
function fitWindowExtent(value, minimum, viewportExtent) {
|
||||
return clamp(
|
||||
Math.round(value),
|
||||
@@ -68,11 +58,6 @@ function fitWindowExtent(value, minimum, viewportExtent) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only a mouse or pen lifts and resizes a portable panel. On a phone the
|
||||
* rail is a scroll container, so a finger on a header is a swipe.
|
||||
*/
|
||||
/** Pin a panel's top-left corner at (left, top) as a fixed window would sit. */
|
||||
function placeAt(panelEl, left, top) {
|
||||
panelEl.style.left = `${left}px`;
|
||||
panelEl.style.top = `${top}px`;
|
||||
@@ -80,27 +65,19 @@ function placeAt(panelEl, left, top) {
|
||||
panelEl.style.bottom = 'auto';
|
||||
}
|
||||
|
||||
/** On a phone the rail scrolls, so a finger on a header is a swipe, not a drag. */
|
||||
function isPrecisePointer(event) {
|
||||
const pointerType = event.pointerType || 'mouse';
|
||||
return pointerType === 'mouse' || pointerType === 'pen';
|
||||
}
|
||||
|
||||
/** Inline geometry a cancelled gesture puts back. */
|
||||
function geometryOf(panelEl) {
|
||||
const { left, top, width, height } = panelEl.style;
|
||||
return { left, top, width, height };
|
||||
}
|
||||
|
||||
export class PanelPositionControls {
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {Function} options.syncPanelCollapseButton
|
||||
* @param {Function} options.layoutRightPanels
|
||||
* @param {Function} options.syncCctvPanelViewport
|
||||
* @param {Function} options.showToast
|
||||
* @param {(panelId: string) => void} [options.onPanelResized] Fires after a
|
||||
* portable panel's resize gesture ends or the panel snaps back to its rail.
|
||||
*/
|
||||
/** `onPanelResized(panelId)` runs after a portable panel moves, resizes or docks. */
|
||||
constructor({
|
||||
syncPanelCollapseButton,
|
||||
layoutRightPanels,
|
||||
@@ -117,7 +94,6 @@ export class PanelPositionControls {
|
||||
this._panelZCounter = PANEL_Z_BASE + 10;
|
||||
this._draggableResizeObserver = null;
|
||||
this._cancelDrag = null;
|
||||
/** Portable panels by id: `{ panel, handle, min: { width, height }, dockOnCollapse }`. */
|
||||
this._portablePanels = new Map();
|
||||
this._resizeHandles = [];
|
||||
this._dragInitialized = false;
|
||||
@@ -154,10 +130,7 @@ export class PanelPositionControls {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bring a floating window back on-screen. Its size is the user's, except
|
||||
* that a window wider or taller than the viewport shrinks to fit it.
|
||||
*/
|
||||
/** Bring a floating window on-screen; shrink it only if it exceeds the viewport. */
|
||||
_reclampPortablePanel(entry) {
|
||||
const { panel, min } = entry;
|
||||
const left = parseInt(panel.style.left, 10);
|
||||
@@ -178,15 +151,8 @@ export class PanelPositionControls {
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a floating window may sit: inside the viewport margin, with its
|
||||
* header out of the command dock and the voice pill, measured now because
|
||||
* the dock grows and shrinks. Both sit at the foot of the screen, so the
|
||||
* header goes above whichever it would land under.
|
||||
* @param {object} entry A `_portablePanels` value.
|
||||
* @param {number} left
|
||||
* @param {number} top
|
||||
* @param {{width: number, height: number, header: number}} [geometry]
|
||||
* Window size and header depth; measured when omitted.
|
||||
* Clamp a floating window inside the viewport margin with its header above
|
||||
* the command dock and voice pill, measured each time since the dock resizes.
|
||||
*/
|
||||
_clampPortableWindow(
|
||||
entry,
|
||||
@@ -224,7 +190,6 @@ export class PanelPositionControls {
|
||||
return { left: x, top: y };
|
||||
}
|
||||
|
||||
/** A window's size and how far down its header reaches. */
|
||||
_windowGeometry({ panel, handle }, rect = panel.getBoundingClientRect()) {
|
||||
const handleRect = handle?.getBoundingClientRect();
|
||||
const header = handleRect ? handleRect.bottom - rect.top : 0;
|
||||
@@ -269,9 +234,8 @@ export class PanelPositionControls {
|
||||
this._dragInitialized = true;
|
||||
const cctvPanel = document.getElementById('cctv-panel');
|
||||
const streetLevelPanel = document.getElementById('street-level-panel');
|
||||
// A `portable` spec lifts out of its rail on a header drag, resizes from
|
||||
// every edge and remembers its window; the others only reposition in
|
||||
// place. Another panel opts in by adding a spec here.
|
||||
// A `portable` panel lifts out of its rail on a header drag, resizes from
|
||||
// every edge and remembers its window; the others only reposition.
|
||||
const dragSpecs = [
|
||||
{
|
||||
id: 'pp-toggles',
|
||||
@@ -316,8 +280,8 @@ export class PanelPositionControls {
|
||||
}
|
||||
// Keep a positioned panel on-screen when its HEIGHT changes after restore — it expands to its
|
||||
// full row set a frame or two later, so the restore-time clamp used a stale (shorter) height and
|
||||
// the panel could still hang off the bottom (audit U2). Re-clamp on every size change. A floating
|
||||
// window grows the same way when its content does (an image opens, a collapsed strip expands).
|
||||
// the panel could still hang off the bottom (audit U2). Re-clamp on every size change. Floating
|
||||
// windows grow the same way when their content does.
|
||||
const observed = [
|
||||
this._ppToggles,
|
||||
...[...this._portablePanels.values()].map(({ panel }) => panel),
|
||||
@@ -403,14 +367,12 @@ export class PanelPositionControls {
|
||||
const pos = JSON.parse(raw);
|
||||
if (!pos || typeof pos.left !== 'number' || typeof pos.top !== 'number')
|
||||
return;
|
||||
// A floating record only means something to a panel that can float; a
|
||||
// portable panel in turn only ever stores floating records.
|
||||
// Only portable panels store, and accept, floating records.
|
||||
const portable = this._portablePanels.get(panelId);
|
||||
if (Boolean(pos.floating) !== Boolean(portable)) return;
|
||||
if (portable) {
|
||||
panelEl.classList.add('panel-floating', 'panel-draggable');
|
||||
// A restored window must stack above the docked rail panels too, not
|
||||
// only one that was lifted or clicked this session.
|
||||
// A restored window must stack above the docked rail panels too.
|
||||
this._promotePanelZ(panelEl);
|
||||
if (Number.isFinite(pos.width)) {
|
||||
panelEl.style.width = `${fitWindowExtent(
|
||||
@@ -504,7 +466,7 @@ export class PanelPositionControls {
|
||||
if (localStorage.getItem(PANEL_FLOAT_HINT_STORAGE_KEY)) return;
|
||||
localStorage.setItem(PANEL_FLOAT_HINT_STORAGE_KEY, '1');
|
||||
} catch {
|
||||
// storage unavailable: still worth saying once this session
|
||||
// storage unavailable: still show it once this session
|
||||
if (this._floatHintShown) return;
|
||||
this._floatHintShown = true;
|
||||
}
|
||||
@@ -512,9 +474,8 @@ export class PanelPositionControls {
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch a portable panel from rail flow to a fixed window at the place it
|
||||
* currently occupies. Width is frozen so a `width: 100%` rail panel keeps
|
||||
* its size once fixed-positioned; height stays natural until a resize.
|
||||
* Turn a rail panel into a fixed window in place. Width is frozen so a
|
||||
* `width: 100%` panel keeps its size; height stays natural until a resize.
|
||||
*/
|
||||
_liftPanelOut(panelId, panelEl, rect = panelEl.getBoundingClientRect()) {
|
||||
if (panelEl.classList.contains('panel-floating')) return false;
|
||||
@@ -522,7 +483,6 @@ export class PanelPositionControls {
|
||||
if (!panelEl.classList.contains('collapsed')) {
|
||||
panelEl.style.width = `${Math.round(rect.width)}px`;
|
||||
}
|
||||
// Rail bookkeeping no longer applies to a window the rail does not allocate.
|
||||
panelEl.style.removeProperty('--right-panel-allocated-height');
|
||||
panelEl.removeAttribute('aria-hidden');
|
||||
panelEl.classList.add('panel-floating', 'panel-draggable');
|
||||
@@ -619,13 +579,9 @@ export class PanelPositionControls {
|
||||
}
|
||||
|
||||
/**
|
||||
* Follow one pointer gesture on the window; every panel gesture runs on
|
||||
* this. With a `threshold`, a press only becomes a gesture once the pointer
|
||||
* has travelled that far: `onStart` runs then, and `onMove` (given the
|
||||
* travel so far) only after it. `onEnd` (pointerup) and `onCancel`
|
||||
* (pointercancel, `onEnd` unless given) learn whether it started. A new
|
||||
* press, a snap-back or destroy() stops it through `_cancelDrag`, which
|
||||
* calls neither.
|
||||
* Track one pointer gesture. With a `threshold`, `onStart` and `onMove` wait
|
||||
* until the pointer has travelled that far; `onEnd`/`onCancel` get whether
|
||||
* it started. Stopping through `_cancelDrag` calls neither.
|
||||
*/
|
||||
_trackPointerGesture(
|
||||
event,
|
||||
@@ -665,18 +621,7 @@ export class PanelPositionControls {
|
||||
window.addEventListener('pointercancel', cancel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Portable drag: a press on the header only becomes a drag after a few
|
||||
* pixels of travel, at which point the panel lifts out of its rail. The
|
||||
* collapse button still toggles on a plain click, and the click that ends
|
||||
* a real drag is swallowed so it cannot toggle by accident. Double-clicking
|
||||
* the header snaps the panel back.
|
||||
*/
|
||||
/**
|
||||
* Put a floating portable panel back in its rail at its default size.
|
||||
* @param {string} panelId
|
||||
* @returns {boolean} Whether the panel was floating.
|
||||
*/
|
||||
/** @returns {boolean} Whether the panel was floating and is now docked. */
|
||||
dockPanel(panelId) {
|
||||
const portable = this._portablePanels.get(panelId);
|
||||
if (!portable?.panel.classList.contains('panel-floating')) return false;
|
||||
@@ -684,18 +629,17 @@ export class PanelPositionControls {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* A panel was collapsed or expanded. A portable panel that opted into
|
||||
* `dockOnCollapse` returns to its rail when collapsed while floating.
|
||||
* @param {string} panelId
|
||||
* @param {boolean} collapsed
|
||||
*/
|
||||
/** A `dockOnCollapse` panel returns to its rail when collapsed while floating. */
|
||||
onPanelCollapsed(panelId, collapsed) {
|
||||
if (!collapsed || !this._portablePanels.get(panelId)?.dockOnCollapse)
|
||||
return;
|
||||
this.dockPanel(panelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* The collapse button still toggles on a plain click; the click that ends a
|
||||
* real drag is swallowed so it cannot toggle by accident.
|
||||
*/
|
||||
_makePortablePanelDraggable(panelId, panelEl, handleEl) {
|
||||
let swallowNextClick = false;
|
||||
this.listen(
|
||||
@@ -790,11 +734,7 @@ export class PanelPositionControls {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The browser took a gesture back (pointercancel): leave no trace and save
|
||||
* nothing. A panel the gesture lifted returns to its rail; a window that
|
||||
* was already floating returns to where it was.
|
||||
*/
|
||||
/** On pointercancel, restore the panel as the gesture found it; save nothing. */
|
||||
_revertPanelGesture(panelId, panelEl, lifted, before) {
|
||||
if (lifted) {
|
||||
this._resetPanelPosition(panelId);
|
||||
@@ -804,11 +744,7 @@ export class PanelPositionControls {
|
||||
this._onPanelResized?.(panelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a resize handle for every edge and corner. Seven are invisible
|
||||
* strips; the bottom-right corner shows a grip. Resizing a docked panel
|
||||
* lifts it out first, so the handles work in both states.
|
||||
*/
|
||||
/** Resizing a docked panel lifts it out first, so handles work in both states. */
|
||||
_makePanelResizable(panelId, panelEl, min = {}) {
|
||||
const limits = { width: 300, height: 160, ...min };
|
||||
for (const dir of RESIZE_DIRECTIONS) {
|
||||
@@ -832,8 +768,8 @@ export class PanelPositionControls {
|
||||
if (event.button !== 0 || !isPrecisePointer(event)) return;
|
||||
event.preventDefault();
|
||||
this._cancelDrag?.();
|
||||
// Set once the pointer has travelled: a plain click on an edge (or on
|
||||
// the globe just beside a docked panel) must not lift it out.
|
||||
// Set once the pointer has travelled: a plain click on an edge must not
|
||||
// lift the panel out.
|
||||
let startBox = null;
|
||||
let lifted = false;
|
||||
let before = null;
|
||||
@@ -874,7 +810,7 @@ export class PanelPositionControls {
|
||||
onStop: () => panelEl.classList.remove('panel-resizing'),
|
||||
onEnd: (resized) => {
|
||||
if (!resized) return;
|
||||
// Re-clamping skipped the live gesture; a north edge may have carried
|
||||
// Re-clamping skipped the live gesture; a north edge may have moved
|
||||
// the header under the command dock.
|
||||
const entry = this._portablePanels.get(panelId);
|
||||
if (entry) this._reclampPortablePanel(entry);
|
||||
|
||||
@@ -357,10 +357,8 @@ test('right rail aligns to the current left rail and excludes hidden obstacles',
|
||||
assert.equal(f.stack.dataset.safeTop, '200.0');
|
||||
});
|
||||
|
||||
// The left pass commits its top and requests this pass in the same frame, but
|
||||
// `top` then animates for 150 ms and nothing re-runs the right pass when it
|
||||
// lands. CI docked the right rail at 234 px (26vh) while the left settled at
|
||||
// 266.4 px (29.6vh). Mid-flight the right rail must read the destination.
|
||||
// The left rail's `top` animates after the right pass runs, and nothing
|
||||
// re-runs it when the transition lands: the right rail must read the end value.
|
||||
function animatingLeftRail({ keyframes = true } = {}) {
|
||||
const f = fixture('right');
|
||||
const left = f.options.leftStack;
|
||||
|
||||
+3
-21
@@ -1,11 +1,6 @@
|
||||
/**
|
||||
* Pure geometry for resizing a floating panel from any edge or corner. The
|
||||
* DOM wiring lives in PanelPositionControls; this module only decides boxes.
|
||||
*/
|
||||
// Pure resize geometry for floating panels; PanelPositionControls wires the DOM.
|
||||
|
||||
/** Viewport margin a resized panel keeps clear on every side. */
|
||||
const EDGE_MARGIN_PX = 6;
|
||||
/** Resize directions: compass edges first, then corners. */
|
||||
export const RESIZE_DIRECTIONS = ['n', 's', 'e', 'w', 'ne', 'nw', 'sw', 'se'];
|
||||
|
||||
function clamp(value, min, max) {
|
||||
@@ -13,21 +8,8 @@ function clamp(value, min, max) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Work out the box a resize gesture produces. Edges named in `dir` move with
|
||||
* the pointer; the opposite edge stays put, so growing from the left keeps
|
||||
* the right side pinned. Minimum size and the viewport margin win over the
|
||||
* pointer.
|
||||
* @param {{left: number, top: number, width: number, height: number}} box Box at gesture start.
|
||||
* @param {string} dir One of RESIZE_DIRECTIONS.
|
||||
* @param {number} dx Pointer travel since gesture start.
|
||||
* @param {number} dy Pointer travel since gesture start.
|
||||
* @param {object} [limits]
|
||||
* @param {number} limits.minWidth
|
||||
* @param {number} limits.minHeight
|
||||
* @param {number} [limits.viewportWidth=Infinity]
|
||||
* @param {number} [limits.viewportHeight=Infinity]
|
||||
* @param {number} [limits.margin=6]
|
||||
* @returns {{left: number, top: number, width: number, height: number}}
|
||||
* Edges named in `dir` follow the pointer (dx, dy since gesture start) and the
|
||||
* opposite edge stays put. Minimum size and the viewport margin win.
|
||||
*/
|
||||
export function resizeBox(
|
||||
box,
|
||||
|
||||
@@ -12,16 +12,9 @@ import { displayPanelScroller } from './displayPanelScroll.js';
|
||||
const pendingCollapseRetries = new WeakSet();
|
||||
|
||||
/**
|
||||
* Read where the left rail's top edge is going to settle, not where it is.
|
||||
* The left pass commits `--left-stack-safe-top` and requests this pass in the
|
||||
* same frame, but `top` then animates; nothing re-runs this pass when the
|
||||
* animation ends, because ResizeObserver ignores moves. Mid-flight, take the
|
||||
* running transition's end value (which also covers Cyber's fixed rail top),
|
||||
* falling back to the left pass's committed target.
|
||||
* @param {HTMLElement} leftStack Rail supplying the shared top baseline.
|
||||
* @param {number} viewportHeight Current viewport height in px.
|
||||
* @param {Function} getComputedStyle DOM style reader.
|
||||
* @returns {number} Settled top in px, or NaN when there is no left rail.
|
||||
* Where the left rail's top will settle, not where it is mid-transition:
|
||||
* nothing re-runs this pass when the `top` animation ends (ResizeObserver
|
||||
* ignores moves). Returns NaN when there is no left rail.
|
||||
*/
|
||||
function settledLeftRailTop(leftStack, viewportHeight, getComputedStyle) {
|
||||
const top = leftStack?.getBoundingClientRect().top;
|
||||
@@ -76,8 +69,7 @@ export function layoutRightPanelRail({
|
||||
// A collapse may schedule one follow-up, which only measures and allocates.
|
||||
const isCollapseRetry = pendingCollapseRetries.delete(stack);
|
||||
|
||||
// A panel lifted out of the rail (PanelPositionControls' portable mode)
|
||||
// is a fixed window the rail neither measures nor allocates.
|
||||
// A floating (lifted-out) panel is not laid out by the rail.
|
||||
const panels = [...stack.children].filter(
|
||||
(panel) =>
|
||||
panel.matches('[data-panel-id]:not(.panel-floating)') && !panel.hidden,
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
const RENDER_MODE_KEY = 'gev:street-level:render-mode';
|
||||
const FOCUSABLE =
|
||||
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
||||
/** Arrow keys inside a radiogroup: the step to the next radio. */
|
||||
const RADIO_STEPS = Object.freeze({
|
||||
ArrowRight: 1,
|
||||
ArrowDown: 1,
|
||||
@@ -26,11 +25,8 @@ const setAttr = (node, key, value) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Own the Street Level panel: the provider chips, the imagery filters, the
|
||||
* legend and the embedded street-level viewer. The panel itself is
|
||||
* ordinary GEV chrome (collapse button, rail layout, persistence) driven by
|
||||
* the application shell; this class only fills the body and asks the shell
|
||||
* to open the panel when something worth seeing arrives.
|
||||
* Fills the Street Level panel body: provider chips, filters, legend and
|
||||
* viewer. The panel chrome itself belongs to the application shell.
|
||||
*/
|
||||
export class StreetLevelControls {
|
||||
constructor({ root, layer, actions }) {
|
||||
@@ -47,21 +43,14 @@ export class StreetLevelControls {
|
||||
// Until the user first touches the app, an off → on is the saved layer
|
||||
// state being restored, not a user switching the layer on.
|
||||
this._restoreWindow = true;
|
||||
/**
|
||||
* Whether the last request to switch the layer on came from the user
|
||||
* (or voice, or a tool) rather than a restore; null when the shell does
|
||||
* not report requests, and the restore window decides instead.
|
||||
*/
|
||||
// Whether the last switch-on was explicit (user, voice, tool) rather than
|
||||
// a restore; null when the shell does not report request origins.
|
||||
this._explicitEnable = null;
|
||||
this._unsubscribeEnableRequests = null;
|
||||
this._resizeQueued = false;
|
||||
this._wrapHome = null;
|
||||
this._expandReturnFocus = null;
|
||||
/**
|
||||
* The <body> children this class made inert while the expanded viewer is
|
||||
* a modal on screen; only these are released again.
|
||||
* @type {Set<Element>}
|
||||
*/
|
||||
// Only the <body> children this class made inert are released again.
|
||||
this._inerted = new Set();
|
||||
this._modalObserver = null;
|
||||
this._elements = this._collect();
|
||||
@@ -111,7 +100,6 @@ export class StreetLevelControls {
|
||||
this.layer.attachViewerHost?.(el.viewer);
|
||||
|
||||
this.listen(el.status, 'click', () => this._toggleEnabled());
|
||||
// One delegated listener; chips are re-synced in place on every render.
|
||||
this.listen(el.providerChips, 'click', (event) => {
|
||||
const button = event.target?.closest?.('.data-toggle-chip');
|
||||
if (!button || button.disabled) return;
|
||||
@@ -185,11 +173,7 @@ export class StreetLevelControls {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One radiogroup (ARIA radio pattern): a single tab stop, arrow keys move
|
||||
* the selection, Home and End jump to the ends. Selecting is the radio's
|
||||
* own click, so the keyboard takes the same path as the pointer.
|
||||
*/
|
||||
/** ARIA radio keys. Selecting clicks the radio, so keys and pointer share a path. */
|
||||
_bindRadioGroup(buttons) {
|
||||
buttons.forEach((button, index) => {
|
||||
this.listen(button, 'keydown', (event) => {
|
||||
@@ -208,9 +192,8 @@ export class StreetLevelControls {
|
||||
}
|
||||
|
||||
/**
|
||||
* Refit the viewer once, on the next frame, however many asked. A hidden
|
||||
* viewer measures 0×0 and would ask for tiles at z=NaN; the observer fires
|
||||
* again once it has a size.
|
||||
* Refit the viewer once per frame. Skips a hidden (0×0) viewer, which would
|
||||
* ask for tiles at z=NaN; the observer fires again once it has a size.
|
||||
*/
|
||||
_requestResize() {
|
||||
if (this._resizeQueued) return;
|
||||
@@ -247,11 +230,7 @@ export class StreetLevelControls {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter and provider changes go through the data manager as a user params
|
||||
* request, so saved state and share links record them; the layer applies
|
||||
* them through its own `setParams`.
|
||||
*/
|
||||
/** Through the data manager, so saved state and share links record it. */
|
||||
_setParams(params) {
|
||||
if (this.actions.setParams)
|
||||
this.actions.setParams(params, { origin: 'user' });
|
||||
@@ -259,9 +238,8 @@ export class StreetLevelControls {
|
||||
}
|
||||
|
||||
/**
|
||||
* A provider chip is the layer's switch: lighting one turns the layer on
|
||||
* with that provider; darkening the last lit one turns the layer off (the
|
||||
* provider stays switched on, so the layer comes back with it).
|
||||
* Lighting a chip turns the layer on; darkening the last lit one turns the
|
||||
* layer off but keeps the provider on, so the layer comes back with it.
|
||||
*/
|
||||
async _toggleProvider(providerId) {
|
||||
const chip = this._view?.providers.find((entry) => entry.id === providerId);
|
||||
@@ -298,12 +276,10 @@ export class StreetLevelControls {
|
||||
if (this.layer.getUIState) this.render(this.layer.getUIState());
|
||||
}
|
||||
|
||||
/** The panel window was resized or docked again: refit the viewer. */
|
||||
onPanelResized() {
|
||||
this._requestResize();
|
||||
}
|
||||
|
||||
/** Ask the shell to open (or close) the rail panel. */
|
||||
setCollapsed(collapsed, options = {}) {
|
||||
this.actions.setPanelCollapsed?.(collapsed, options);
|
||||
if (!collapsed) this._requestResize();
|
||||
@@ -318,9 +294,8 @@ export class StreetLevelControls {
|
||||
}
|
||||
|
||||
/**
|
||||
* Grow the street-level viewer to most of the screen, or shrink it back.
|
||||
* A user's shrink (`dock`) also returns a floating panel to its rail at its
|
||||
* default size, so the viewer does not land in a window over the globe.
|
||||
* A user's shrink (`dock`) also returns a floating panel to its rail, so the
|
||||
* viewer does not land in a window over the globe.
|
||||
*/
|
||||
setViewerExpanded(expanded, { dock = false } = {}) {
|
||||
const wrap = this._elements.viewerWrap;
|
||||
@@ -384,10 +359,7 @@ export class StreetLevelControls {
|
||||
target?.focus?.({ preventScroll: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* The expanded viewer holds Esc and Tab only while it is on screen: Clean
|
||||
* View, recording and the cockpit hide it with CSS.
|
||||
*/
|
||||
/** Clean View, recording and the cockpit hide the expanded viewer with CSS. */
|
||||
_isDialogShown() {
|
||||
if (!this.isViewerExpanded()) return false;
|
||||
const wrap = this._elements.viewerWrap;
|
||||
@@ -402,11 +374,9 @@ export class StreetLevelControls {
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the application modal-inert while the expanded viewer is on screen:
|
||||
* every other <body> child is `inert` (one already inert is left alone), so
|
||||
* no control around the viewer takes a click or focus. A viewer that CSS
|
||||
* hides (Clean View, recording, cockpit) releases them, since a hidden
|
||||
* modal must not leave the app dead. Returns whether the dialog is shown.
|
||||
* Make every other <body> child inert while the expanded viewer is shown.
|
||||
* A CSS-hidden viewer releases them so a hidden modal never leaves the app
|
||||
* dead. Returns whether the dialog is shown.
|
||||
*/
|
||||
_syncModal() {
|
||||
if (this.destroyed || !this._isDialogShown()) {
|
||||
@@ -422,17 +392,12 @@ export class StreetLevelControls {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Give the application back every node `_syncModal` made inert. */
|
||||
_releaseModal() {
|
||||
for (const node of this._inerted) node.inert = false;
|
||||
this._inerted.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* While expanded, re-check the modal whenever <body> changes class (the
|
||||
* modes that hide the viewer) or gains a child (which must be inert too).
|
||||
* Shrinking stops watching and releases the application.
|
||||
*/
|
||||
/** Re-check the modal when <body> changes class or gains a child. */
|
||||
_watchModal(on) {
|
||||
this._modalObserver?.disconnect();
|
||||
this._modalObserver = null;
|
||||
@@ -534,7 +499,6 @@ export class StreetLevelControls {
|
||||
setProp(el.errorText, 'textContent', view.error || '');
|
||||
}
|
||||
|
||||
/** Mark the checked radio of a group and make it the group's tab stop. */
|
||||
_renderRadios(buttons, isChecked) {
|
||||
for (const button of buttons) {
|
||||
const checked = isChecked(button);
|
||||
@@ -629,16 +593,14 @@ export class StreetLevelControls {
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the panel at the moments a user would look for it. These opens are
|
||||
* never persisted: the stored collapse state stays the user's own choice.
|
||||
* Open the panel on a switch-on or when an image opens. Never persisted:
|
||||
* the stored collapse state stays the user's own choice.
|
||||
*/
|
||||
_reactToTransitions(view, state) {
|
||||
const enabled = view.enabled;
|
||||
if (enabled && this._wasEnabled === false) {
|
||||
// A restore that turns the saved layer back on must not reopen a panel
|
||||
// the user (or a share link) chose to keep collapsed. With the shell's
|
||||
// request origins the panel opens only for a user, voice or tool
|
||||
// switch-on; without them, the restore window stands in.
|
||||
// A restore must not reopen a panel the user or a share link kept
|
||||
// collapsed. Without request origins, the restore window decides.
|
||||
const preference = this.root.dataset?.collapsedPreference;
|
||||
const restoring =
|
||||
this._explicitEnable === null
|
||||
@@ -650,10 +612,8 @@ export class StreetLevelControls {
|
||||
this._explicitEnable = this._explicitEnable === null ? null : false;
|
||||
}
|
||||
this._wasEnabled = enabled;
|
||||
// Size changes reach the viewer through the ResizeObserver; resizing on
|
||||
// every render ran each frame while a panorama was dragged. Opening is the
|
||||
// one moment the element may not have reported a size yet, and opening
|
||||
// the panel asks for that one (coalesced) resize.
|
||||
// Other size changes reach the viewer through the ResizeObserver; opening
|
||||
// asks for one resize because the element may not have a size yet.
|
||||
const open = state.street.open === true;
|
||||
if (open && !this._wasStreetOpen)
|
||||
this.setCollapsed(false, { persist: false });
|
||||
|
||||
@@ -13,10 +13,7 @@ import { fakeStreetLevelProvider } from '../testSupport/streetLevelFakes.mjs';
|
||||
|
||||
/* ── A small DOM: just what the panel controls touch ───────────────────── */
|
||||
|
||||
/**
|
||||
* Every write the page would see as a mutation: an attribute set or removed,
|
||||
* or a reflected property assigned (even to the value it already has).
|
||||
*/
|
||||
/** Every write a page would see as a mutation, even one to the same value. */
|
||||
const mutations = { count: 0 };
|
||||
const REFLECTED = ['hidden', 'disabled', 'textContent', 'title', 'value'];
|
||||
/** The reflected properties a MutationObserver sees, as the record it gets. */
|
||||
@@ -27,10 +24,8 @@ const REFLECTED_RECORDS = {
|
||||
textContent: { type: 'childList' },
|
||||
};
|
||||
|
||||
/** The fake MutationObservers currently observing (see fakeMutationObserver). */
|
||||
const liveObservers = new Set();
|
||||
|
||||
/** Queue a mutation record for every observer whose options cover it. */
|
||||
function recordMutation(target, { type, attributeName = null }) {
|
||||
for (const observer of liveObservers)
|
||||
observer.consider(target, type, attributeName);
|
||||
@@ -116,10 +111,7 @@ class FakeNode {
|
||||
const siblings = this.parent.children;
|
||||
return siblings[siblings.indexOf(this) + 1] || null;
|
||||
}
|
||||
/**
|
||||
* As layout reports it: null when this node or an ancestor is hidden (no
|
||||
* box), or when it is not in the document; else its parent.
|
||||
*/
|
||||
/** Null when this node or an ancestor is hidden or disconnected. */
|
||||
get offsetParent() {
|
||||
if (!this.isConnected) return null;
|
||||
for (let node = this; node; node = node.parent)
|
||||
@@ -388,11 +380,7 @@ const memoryStorage = () => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* The production path a panel click takes: controls → dataManager
|
||||
* .setLayerParams → layer.setParams → LayerStateCoordinator's durable state,
|
||||
* which is what share links and saved state encode.
|
||||
*/
|
||||
/** The production path: controls → dataManager → layer → durable layer state. */
|
||||
async function productionPanel(dom) {
|
||||
const provider = standInProvider();
|
||||
const layer = createStreetLevelLayer({ providers: [provider.def] });
|
||||
@@ -592,7 +580,7 @@ test('lighting a dark chip switches the provider on as a user params request, th
|
||||
assert.deepEqual(calls.setEnabled, [true]);
|
||||
}));
|
||||
|
||||
test('the legend rebuilds when a swatch changes, even at the same count (review #9)', () =>
|
||||
test('the legend rebuilds when a swatch changes, even at the same count', () =>
|
||||
withDom(async (dom) => {
|
||||
const { layer } = stubPanel(dom, uiState());
|
||||
const legend = dom.root.querySelector('#sl-legend');
|
||||
@@ -610,7 +598,7 @@ test('the legend rebuilds when a swatch changes, even at the same count (review
|
||||
assert.equal(swatch(), '#a66bff');
|
||||
}));
|
||||
|
||||
test('the viewer is resized once when it opens, not on every render (review #8, gekh P3)', () =>
|
||||
test('the viewer is resized once when it opens, not on every render', () =>
|
||||
withDom(async (dom) => {
|
||||
const { layer } = stubPanel(dom, uiState());
|
||||
await settle();
|
||||
@@ -714,7 +702,7 @@ test('an image closed while expanded does not return focus into the hidden viewe
|
||||
controls.destroy();
|
||||
}));
|
||||
|
||||
/* ── gekh review on #768 ───────────────────────────────────────────────── */
|
||||
/* ── Restores, the modal viewer and keyboard rules ─────────────────────── */
|
||||
|
||||
test('a restored layer does not reopen a panel the user collapsed, and automatic opens are never stored (P2-1)', () =>
|
||||
withDom(async (dom) => {
|
||||
@@ -811,10 +799,8 @@ test('a hidden expanded viewer (Clean View, recording, cockpit) holds neither Es
|
||||
}));
|
||||
|
||||
/**
|
||||
* A MutationObserver stand-in. Like the real one it queues only the records
|
||||
* its `observe` options cover (childList, attributes / attributeFilter,
|
||||
* subtree) and delivers them in a microtask, only when there are some;
|
||||
* `fire()` delivers what is queued at once.
|
||||
* A MutationObserver stand-in: queues only the records its options cover and
|
||||
* delivers them in a microtask; `fire()` delivers at once.
|
||||
*/
|
||||
function fakeMutationObserver() {
|
||||
const observers = [];
|
||||
@@ -888,7 +874,7 @@ const inertOutside = (dom) =>
|
||||
.filter((node) => node.id !== 'sl-viewer-wrap')
|
||||
.map((node) => [node.id, node.inert === true]);
|
||||
|
||||
test('the expanded viewer makes the rest of the app inert and gives it back on shrink (IC8 P2)', () =>
|
||||
test('the expanded viewer makes the rest of the app inert and gives it back on shrink', () =>
|
||||
withObserver(() =>
|
||||
withDom(async (dom) => {
|
||||
const { controls } = stubPanel(dom, uiState({ open: true }));
|
||||
@@ -919,7 +905,7 @@ test('the expanded viewer makes the rest of the app inert and gives it back on s
|
||||
}),
|
||||
));
|
||||
|
||||
test('a CSS-hidden expanded viewer releases the app; showing it again, and new children, go inert (IC8 P2)', () =>
|
||||
test('a CSS-hidden expanded viewer releases the app; showing it again, and new children, go inert', () =>
|
||||
withObserver((observer) =>
|
||||
withDom(async (dom) => {
|
||||
const { controls } = stubPanel(dom, uiState({ open: true }));
|
||||
@@ -954,7 +940,7 @@ test('a CSS-hidden expanded viewer releases the app; showing it again, and new c
|
||||
}),
|
||||
));
|
||||
|
||||
test('an image closed while expanded releases the app (IC8 P2)', () =>
|
||||
test('an image closed while expanded releases the app', () =>
|
||||
withObserver(() =>
|
||||
withDom(async (dom) => {
|
||||
const { layer, controls } = stubPanel(dom, uiState({ open: true }));
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import { keySetupRequirement } from '../keySetupCore.mjs';
|
||||
|
||||
/**
|
||||
* Turn the Street Level layer's UI state into the strings and flags the panel
|
||||
* renders. Pure: no DOM, no layer calls, so every wording decision is testable.
|
||||
*/
|
||||
// Street Level UI state to panel strings and flags. Pure, so it is testable.
|
||||
|
||||
const DAY_MS = 86_400_000;
|
||||
|
||||
/**
|
||||
* Stops of the SINCE slider, oldest window on the left: position 0 shows
|
||||
* every capture, the right end only the last month. Stored as relative days
|
||||
* so a share link keeps its meaning over time.
|
||||
*/
|
||||
/** SINCE slider stops, in relative days so a share link keeps its meaning. */
|
||||
export const SINCE_STOPS = Object.freeze([
|
||||
Object.freeze({ days: 0, label: 'ANY DATE' }),
|
||||
Object.freeze({ days: 3652, label: 'LAST 10 YEARS' }),
|
||||
@@ -24,7 +17,7 @@ export const SINCE_STOPS = Object.freeze([
|
||||
Object.freeze({ days: 30, label: 'LAST MONTH' }),
|
||||
]);
|
||||
|
||||
/** Slider position for a day count: the exact stop, else the nearest one. */
|
||||
/** The exact stop for a day count, else the nearest one. */
|
||||
export function sinceStopIndex(days) {
|
||||
const value = Number(days) || 0;
|
||||
if (value <= 0) return 0;
|
||||
@@ -38,7 +31,6 @@ export function sinceStopIndex(days) {
|
||||
return best;
|
||||
}
|
||||
|
||||
/** Readout beside the slider: the window, plus the cut-off date it means today. */
|
||||
function presentSince(days, now) {
|
||||
const value = Number(days) || 0;
|
||||
const index = sinceStopIndex(value);
|
||||
@@ -61,7 +53,6 @@ function formatDate(ms) {
|
||||
}
|
||||
}
|
||||
|
||||
/** The header pill doubles as the layer's on/off switch. */
|
||||
function presentStatus(state) {
|
||||
const pressed = state.enabled === true;
|
||||
const title = pressed ? 'Turn Street Level off' : 'Turn Street Level on';
|
||||
@@ -76,11 +67,7 @@ function presentStatus(state) {
|
||||
: { text: 'OFF', tone: '', pressed, title };
|
||||
}
|
||||
|
||||
/**
|
||||
* One chip per registered provider; a keyless provider reads as an error
|
||||
* chip. A chip is lit only while the layer is on and that provider is
|
||||
* switched on, so with a single provider the chip is the layer's switch.
|
||||
*/
|
||||
/** A chip is lit only while the layer and its provider are both on. */
|
||||
function presentProviders(state) {
|
||||
const enabled = state.enabled === true;
|
||||
return (state.providers || []).map((provider) => {
|
||||
@@ -148,18 +135,13 @@ function presentMeta(state) {
|
||||
return state.coverage.hint || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} state Snapshot from the layer's `getUIState()`.
|
||||
* @param {{now?: number}} [options] Clock for the SINCE readout (tests pin it).
|
||||
* @returns {object} Everything the panel needs, already worded.
|
||||
*/
|
||||
/** @param {{now?: number}} [options] Clock for the SINCE readout. */
|
||||
export function presentStreetLevelPanel(state, { now = Date.now() } = {}) {
|
||||
const enabled = state.enabled === true;
|
||||
const keyRequired = state.keyRequired === true;
|
||||
const filter = state.filter || { pano: 'all', sinceDays: 0 };
|
||||
// KEY REQUIRED already says it, and the chip's tooltip says how to fix it;
|
||||
// the error under it would only repeat a raw code (`no_key`). A rejected
|
||||
// key keeps its line: that message names the fix.
|
||||
// A missing key is already shown by the status and chip tooltip; only a
|
||||
// rejected key keeps its error line, since that message names the fix.
|
||||
const keyMissing = keyRequired && state.keyRejected !== true;
|
||||
return {
|
||||
enabled,
|
||||
@@ -175,7 +157,6 @@ export function presentStreetLevelPanel(state, { now = Date.now() } = {}) {
|
||||
legend: state.legend || [],
|
||||
viewer: presentViewer(state),
|
||||
meta: presentMeta(state),
|
||||
/** The panel opens itself when an image opens (a native panel stays put otherwise). */
|
||||
wantsOpen: state.street.open === true,
|
||||
};
|
||||
}
|
||||
@@ -65,7 +65,7 @@ test('a key-gated layer disables every control and flags KEY REQUIRED', () => {
|
||||
assert.equal(view.status.tone, 'warn');
|
||||
});
|
||||
|
||||
test('KEY REQUIRED shows no raw error code under the pill (review P3)', () => {
|
||||
test('KEY REQUIRED shows no raw error code under the pill', () => {
|
||||
const view = presentStreetLevelPanel(
|
||||
snapshot({
|
||||
enabled: true,
|
||||
@@ -80,7 +80,7 @@ test('KEY REQUIRED shows no raw error code under the pill (review P3)', () => {
|
||||
assert.match(view.providers[0].title, /^Mapillary: Needs /);
|
||||
});
|
||||
|
||||
test('a key Mapillary rejected reads KEY REJECTED and names the fix (review IC8 P2)', () => {
|
||||
test('a key Mapillary rejected reads KEY REJECTED and names the fix', () => {
|
||||
const error =
|
||||
'Mapillary rejected MAPILLARY_CLIENT_TOKEN — replace it in Provider Settings';
|
||||
const view = presentStreetLevelPanel(
|
||||
|
||||
@@ -104,10 +104,8 @@
|
||||
}
|
||||
|
||||
/* ── Portable rail panels ────────────────────
|
||||
A header drag lifts a portable panel (PanelPositionControls) out of its
|
||||
rail into a fixed window that remembers its place and size. The rail's
|
||||
own placement rules in layers.css are guarded with :not(.panel-floating),
|
||||
so these only have to beat a panel's base placement (one id). */
|
||||
Rail placement in layers.css is guarded with :not(.panel-floating), so
|
||||
these rules only have to beat a panel's base placement (one id). */
|
||||
.panel-portable .panel-header {
|
||||
cursor: grab;
|
||||
}
|
||||
@@ -117,16 +115,14 @@
|
||||
#right-context-rail > .panel-floating {
|
||||
position: fixed;
|
||||
inset: auto;
|
||||
/* Above every docked rail panel (#pp-toggles is 110). PanelPositionControls
|
||||
promotes floating windows from this floor (PANEL_Z_FLOATING_FLOOR). */
|
||||
/* Above every docked rail panel; keep in step with PANEL_Z_FLOATING_FLOOR. */
|
||||
z-index: 111;
|
||||
max-height: calc(100vh - 12px);
|
||||
margin: 0;
|
||||
flex: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
/* A collapsed window keeps its stored height for later but shows only its
|
||||
header strip, so the resize handles have nothing to attach to. */
|
||||
/* A collapsed window shows only its header strip; its inline height stays. */
|
||||
#right-context-rail > .panel-floating.collapsed {
|
||||
max-height: max-content;
|
||||
}
|
||||
@@ -1110,7 +1106,7 @@ body.ui-clean-view #cctv-panel,
|
||||
body.ui-clean-view #radio-panel,
|
||||
body.ui-clean-view #global-context-panel,
|
||||
body.ui-clean-view #scene-panel,
|
||||
/* The expanded Street Level viewer lives on <body>, outside every panel. */
|
||||
/* The expanded viewer lives on <body>, outside every panel. */
|
||||
body.ui-clean-view .sl-viewer-wrap-expanded {
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
|
||||
@@ -16,7 +16,7 @@ body.recording-mode #radio-panel,
|
||||
body.recording-mode #global-context-panel,
|
||||
body.recording-mode #gev-voice-control,
|
||||
body.recording-mode #scene-panel,
|
||||
/* The expanded Street Level viewer lives on <body>, outside every panel. */
|
||||
/* The expanded viewer lives on <body>, outside every panel. */
|
||||
body.recording-mode .sl-viewer-wrap-expanded {
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* ── Street Level panel ─────────────────────────────────────────
|
||||
Lives in the right context rail next to CCTV; the rail owns position and
|
||||
width (see layers.css). Everything here is panel-internal. The expanded
|
||||
viewer is re-parented to <body>, so it carries the tokens too. */
|
||||
The rail owns position and width (layers.css). The expanded viewer is
|
||||
re-parented to <body>, so it carries the tokens too. */
|
||||
#street-level-panel,
|
||||
.sl-viewer-wrap {
|
||||
--sl-green: #05cb63;
|
||||
@@ -111,8 +110,6 @@ button.sl-status:focus-visible {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* The filters a missing key gates. The provider chips stay outside it, so
|
||||
their tooltip (how to add the key) is still reachable. */
|
||||
.sl-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -128,8 +125,7 @@ button.sl-status:focus-visible {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Everything but the viewer. Docked, the whole panel scrolls; in a resized
|
||||
window the viewer keeps its room and only this block scrolls. */
|
||||
/* In a resized window only this block scrolls; the viewer keeps its room. */
|
||||
.sl-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -151,8 +147,7 @@ button.sl-status:focus-visible {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* One colour per source: a swatch dot, and the whole chip in that colour
|
||||
while the source is on. `--chip-color` comes from the provider. */
|
||||
/* `--chip-color` comes from the provider. */
|
||||
.sl-chips .data-toggle-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -246,7 +241,6 @@ button.sl-status:focus-visible {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* SINCE: a stepped slider, oldest window left, newest right. */
|
||||
.sl-since {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
@@ -318,8 +312,7 @@ button.sl-status:focus-visible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* The image stage: the viewer and its loading veil, nothing on top of it,
|
||||
so the provider's own controls (sequence bar, zoom, bearing) stay clear. */
|
||||
/* Nothing sits over the image, so the provider's own controls stay clear. */
|
||||
.sl-viewer-stage {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -362,7 +355,6 @@ button.sl-status:focus-visible {
|
||||
}
|
||||
}
|
||||
|
||||
/* Our tools live in a bar above the image rather than over it. */
|
||||
.sl-viewer-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -485,16 +477,15 @@ button.sl-status:focus-visible {
|
||||
}
|
||||
|
||||
/* ── Floating window ─────────────────────────────────────────────────────── */
|
||||
/* A window that was only dragged keeps its natural height; bound it to the
|
||||
viewport so nothing runs off-screen (the rail rule's max-height: 100% does
|
||||
not bind an auto-height parent). Id-qualified to outrank that rule. */
|
||||
/* A dragged window keeps its natural height, so bound it to the viewport:
|
||||
the rail's max-height: 100% does not bind an auto-height parent.
|
||||
Id-qualified to outrank that rule. */
|
||||
#right-context-rail > #street-level-panel.panel-floating > .street-level-panel-inner {
|
||||
max-height: calc(100vh - 12px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* A resized window (PanelPositionControls writes an inline height) hands the
|
||||
spare height to the viewer, so the image never needs scrolling to. */
|
||||
/* A resized window (inline height) gives its spare height to the viewer. */
|
||||
.panel-floating[style*='height'] > .street-level-panel-inner {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
@@ -532,9 +523,8 @@ button.sl-status:focus-visible {
|
||||
}
|
||||
|
||||
/* ── Expanded viewer ─────────────────────────────────────────────────────── */
|
||||
/* The panel inner's backdrop-filter makes it the containing block for fixed
|
||||
descendants, so the expanded viewer is re-parented to <body>. z-index sits
|
||||
above panels (≤139) and below the voice pill (150) and toasts (200). */
|
||||
/* Re-parented to <body>: the panel's backdrop-filter would contain a fixed
|
||||
child. z-index sits above panels (≤139), below the voice pill (150). */
|
||||
.sl-viewer-wrap-expanded {
|
||||
position: fixed;
|
||||
left: 4vw;
|
||||
@@ -604,10 +594,9 @@ button.sl-status:focus-visible {
|
||||
}
|
||||
|
||||
/* ── Phones ──────────────────────────────────────────────────────────────── */
|
||||
/* At this width the right rail is the band between mid-screen and the credit
|
||||
line (responsive.css: top 50vh + 8px, bottom 2vh + 7.5rem). Size the photo
|
||||
from that band, less the panel's header, tool bar and caption (~150px), so
|
||||
the whole image shows without scrolling; EXPAND still gives full screen. */
|
||||
/* The phone rail spans 50vh + 8px to 2vh + 7.5rem from the bottom
|
||||
(responsive.css). Size the photo to that band, less ~150px of panel chrome,
|
||||
so it shows without scrolling. */
|
||||
@media (max-width: 720px) {
|
||||
.sl-viewer {
|
||||
aspect-ratio: auto;
|
||||
|
||||
@@ -71,8 +71,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Street Level (Mapillary) Panel — moved into #right-context-rail at
|
||||
runtime next to CCTV (see PanelLayoutController). -->
|
||||
<!-- Street Level Panel (moved into #right-context-rail at runtime) -->
|
||||
<div id="street-level-panel" class="panel-collapsible collapsed" data-panel-id="street-level-panel">
|
||||
<div class="panel-glow"></div>
|
||||
<div class="street-level-panel-inner" data-rail-scroller>
|
||||
@@ -116,8 +115,8 @@
|
||||
<div id="sl-provider-chips" class="sl-chips"></div>
|
||||
</div>
|
||||
<div id="sl-error" class="sl-error" role="alert" hidden><span aria-hidden="true">⚠</span> <span id="sl-error-text"></span></div>
|
||||
<!-- KEY REQUIRED gates the filters only: the provider chips above
|
||||
stay focusable so their tooltip can say how to add the key. -->
|
||||
<!-- A missing key gates the filters only: the chips above stay
|
||||
focusable so their tooltip can say how to add the key. -->
|
||||
<fieldset id="sl-controls" class="sl-controls">
|
||||
<div class="sl-filters" role="group" aria-label="Imagery filters">
|
||||
<span class="cctv-summary-label">IMAGERY</span>
|
||||
|
||||
Reference in new issue
Block a user