diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 9329e4122..aceebe477 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -2935,8 +2935,10 @@ its criteria cannot be silently ignored. Fire Perimeters uses capped, timed server reads with stale-on-error caching and a per-client limit. Unchanged snapshots retain geometry; link checks cancel on disable or selection change, and the row legend shows reported containment. Street Level lives in the right context rail next to CCTV as an ordinary -collapsible panel (`#street-level-panel`, share token `0`, option owner -`street-level`) and starts collapsed. It is modelled on the iD editor's photo +collapsible panel (`#street-level-panel`, layer share token `0`, option owner +`street-level`, panel `ui` token `t`) and starts collapsed. It opens itself +only when the user (or voice, or a tool) switches the layer on or a photo +opens, never on a saved-state restore, and never stores those automatic opens. It is modelled on the iD editor's photo overlay: one panel, a PROVIDERS chip per registered imagery provider, shared 360°/flat and captured-since filters (stored as relative days so a link keeps its meaning), one viewer host, and one on-globe credit per active provider. diff --git a/src/sharelink.celestial.test.mjs b/src/sharelink.celestial.test.mjs index 567bb09dd..d9e3395d2 100644 --- a/src/sharelink.celestial.test.mjs +++ b/src/sharelink.celestial.test.mjs @@ -184,6 +184,24 @@ test('a collapsed Recent Imagery panel survives the share-link round trip beside ] }); }); +test('a collapsed Street Level panel survives the share-link round trip (review P3)', () => { + const manager = makeManager(); + manager.setPanelStateProvider(() => ({ specs: [ + { id: 'cctv-panel', collapsed: false }, + { id: 'street-level-panel', collapsed: true }, + ] })); + clearTimeout(manager._debounceTimer); + manager._updateHash(); + const params = new URLSearchParams(window.location.hash.slice(1)); + assert.equal(params.get('ui'), 'v.c.0_t.c.1'); + const restored = makeManager(`#v=2&lat=10&lon=20&ui=${params.get('ui')}`) + .parseInitialHash(); + assert.deepEqual(restored.panelState, { specs: [ + { id: 'cctv-panel', collapsed: false, pinned: null }, + { id: 'street-level-panel', collapsed: true, pinned: null }, + ] }); +}); + test('camera-only, partial, and malformed panel shares remain valid incoming state', () => { const cameraOnly = makeManager('#lat=10&lon=20').parseInitialHash(); assert.ok(cameraOnly); diff --git a/src/sharelink.js b/src/sharelink.js index 36862dcbf..e2a9e41b2 100644 --- a/src/sharelink.js +++ b/src/sharelink.js @@ -34,6 +34,7 @@ const SHARE_PANEL_STATE_REGISTRY = Object.freeze([ { id: 'data-panel', token: 'd', pinnable: false }, { id: 'cctv-panel', token: 'v', pinnable: false }, { id: 'recent-imagery-panel', token: 'i', pinnable: false }, + { id: 'street-level-panel', token: 't', pinnable: false }, { id: 'radio-panel', token: 'r', pinnable: false }, { id: 'scene-panel', token: 's', pinnable: false }, { id: 'global-context-panel', token: 'g', pinnable: false }, diff --git a/src/streetLevelMarkup.test.mjs b/src/streetLevelMarkup.test.mjs index 9e327bec7..3ace37ca2 100644 --- a/src/streetLevelMarkup.test.mjs +++ b/src/streetLevelMarkup.test.mjs @@ -83,6 +83,15 @@ test('the keyless state gates the controls rather than leaving dead buttons', () ); assert.match(html, /
<\/div>/); + // The gate holds the filters only: the provider chips stay reachable, so + // their tooltip can say how to add the key (gekh P3). + const gate = html.slice( + html.indexOf('
', html.indexOf('
{ - const controlsBlock = html.slice(html.indexOf('
')); assert.ok( controlsBlock.indexOf('id="sl-viewer-wrap"') < controlsBlock.indexOf('class="sl-settings"'), @@ -178,3 +187,70 @@ test('the viewer comes first and the panel is a portable, resizable window', () /panelId === 'street-level-panel'\)\s*this\._streetLevelControls\?\.onPanelResized\(\)/, ); }); + +/* ── gekh review on #768 ───────────────────────────────────────────────── */ + +test('Clean View, recording and the cockpit hide the expanded viewer on (P2-5)', () => { + // EXPAND moves the viewer out of every panel, past the panel-hiding rules. + assert.match( + read('src/ui/styles/controls.css'), + /body\.ui-clean-view \.sl-viewer-wrap-expanded \{\s*opacity: 0 !important;\s*visibility: hidden !important;/, + ); + assert.match( + read('src/ui/styles/recording.css'), + /body\.recording-mode \.sl-viewer-wrap-expanded \{\s*opacity: 0 !important;\s*visibility: hidden !important;/, + ); + assert.match( + read('src/ui/styles/cockpit.css'), + /body\.cockpit-mode\s*:is\([^)]*\.sl-viewer-wrap-expanded\s*\)\s*\{\s*display: none !important;/, + ); + // Hidden, it must not keep holding Esc and Tab. + assert.match(controls, /_isDialogShown\(\)/); +}); + +test('the expanded viewer keeps the panel tokens (P3)', () => { + assert.match( + panelCss, + /#street-level-panel,\s*\.sl-viewer-wrap \{\s*--sl-green:[^}]*--sl-text:[^}]*--sl-text-small:/, + ); +}); + +test('radiogroups are one tab stop and their focus ring is not clipped (P3)', () => { + const seg = /\.sl-seg \{([^}]*)\}/.exec(panelCss)[1]; + assert.doesNotMatch(seg, /overflow/); + assert.match( + panelCss, + /\.sl-seg-btn:focus-visible \{[^}]*outline: 1px solid/, + ); + for (const [group, checked] of [ + ['data-sl-render', 'letterbox'], + ['data-sl-pano', 'all'], + ]) { + const radios = [ + ...html.matchAll( + new RegExp(`
-
+
    -
    +