diff --git a/scripts/qa-ui-disposal.mjs b/scripts/qa-ui-disposal.mjs index d393648df..95ad3818b 100644 --- a/scripts/qa-ui-disposal.mjs +++ b/scripts/qa-ui-disposal.mjs @@ -139,7 +139,6 @@ try { const focusBefore = document.activeElement; const stoppedBeforeRestoration = ui._panelLayout.destroyed && - ui._panelPosition.destroyed && ui._feedback.destroyed && ui._recording.destroyed && cockpit.destroyed && diff --git a/src/ui/panelChrome.js b/src/ui/panelChrome.js index 5a671e453..72249e0f4 100644 --- a/src/ui/panelChrome.js +++ b/src/ui/panelChrome.js @@ -60,8 +60,6 @@ export class PanelChrome { this._cockpitContextCollapsedForDataPanel = false; this._panelPosition = new PanelPositionControls({ syncPanelCollapseButton: (panel) => this._syncPanelCollapseButton(panel), - layoutRightPanels: () => this._layoutRightPanels(), - syncCctvPanelViewport: () => this._syncCctvPanelViewport(), showToast: (message) => this._showToast(message), }); this._panelLayout = new PanelLayoutController({ @@ -615,7 +613,6 @@ export class PanelChrome { if (this._disposed) return; this._disposed = true; this._lifetime.destroy(); - this._panelPosition.destroy(); this._panelLayout.destroy(); for (const control of this._panelDisclosureControls || []) control.destroy(); diff --git a/src/ui/panelLayoutController.js b/src/ui/panelLayoutController.js index fc170646e..dccce2988 100644 --- a/src/ui/panelLayoutController.js +++ b/src/ui/panelLayoutController.js @@ -194,7 +194,6 @@ export class PanelLayoutController { this._ppToggles.style.removeProperty('bottom'); this._ppToggles.style.removeProperty('left'); this._ppToggles.style.removeProperty('z-index'); - this._ppToggles.classList.remove('panel-draggable', 'panel-dragging'); this._ppToggles.querySelector('.pp-header-row')?.removeAttribute('title'); stack.prepend(this._ppToggles); const globalContextPanel = document.getElementById('global-context-panel'); @@ -207,7 +206,6 @@ export class PanelLayoutController { if (!panel) continue; for (const property of ['top', 'right', 'bottom', 'left', 'z-index']) panel.style.removeProperty(property); - panel.classList.remove('panel-draggable', 'panel-dragging'); stack.insertBefore(panel, globalContextPanel); this._syncPanelCollapseButton(panel); } diff --git a/src/ui/panelPositionControls.js b/src/ui/panelPositionControls.js index 1f67eb655..20926198c 100644 --- a/src/ui/panelPositionControls.js +++ b/src/ui/panelPositionControls.js @@ -1,49 +1,16 @@ -/** Own panel position preferences, viewport clamping and drag listeners. */ +/** Own panel collapsed-state preferences and the one-time layout-reset notice. */ /** Versioned localStorage namespace prefix to invalidate stale panel layouts. */ const PANEL_LAYOUT_STORAGE_VERSION = 'v6'; /** - * Position keys are versioned separately from collapsed-state keys so layout - * default changes (e.g. right-rail origin) can reset positions without also - * resetting every panel's open/closed preference. + * Position keys are versioned separately from collapsed-state keys. The rails + * now lay panels out themselves and store no position; the version still names + * the layout-reset notice and the documented key. */ const PANEL_POSITION_STORAGE_VERSION = 'v8'; -/** Z ladder: panels promote within [100, 139]; voice pill 150, toast 200, clean-view-exit 300. */ -const PANEL_Z_BASE = 100; -const PANEL_Z_MAX = 139; export class PanelPositionControls { - constructor({ - syncPanelCollapseButton, - layoutRightPanels, - syncCctvPanelViewport, - showToast, - }) { + constructor({ syncPanelCollapseButton, showToast }) { this._syncPanelCollapseButton = syncPanelCollapseButton; - this._layoutRightPanels = layoutRightPanels; - this._syncCctvPanelViewport = syncCctvPanelViewport; this._showToast = showToast; - this._ppToggles = document.getElementById('pp-toggles'); - this._panelZCounter = PANEL_Z_BASE + 10; - this._draggableResizeObserver = null; - this._cancelDrag = null; - this.removers = []; - this.destroyed = false; - } - listen(target, type, callback) { - if (this.destroyed || !target) return; - const listener = (event) => { - if (!this.destroyed) callback(event); - }; - target.addEventListener(type, listener); - this.removers.push(() => target.removeEventListener(type, listener)); - } - _reclampDraggablePanels() { - if (this.destroyed) return; - const el = this._ppToggles; - if (!el || !el.style.top || el.style.top === 'auto') return; - const top = parseInt(el.style.top, 10); - if (!Number.isFinite(top)) return; - el.style.top = `${this._clampToViewport(0, top, el).top}px`; - this._pinPanelToRight(el); } _maybeNotifyLayoutReset() { @@ -64,35 +31,6 @@ export class PanelPositionControls { } } - _initPanelDrag() { - if (this.destroyed) return; - const dragSpecs = [ - { - id: 'pp-toggles', - panel: this._ppToggles, - handle: this._ppToggles?.querySelector('.panel-drag-handle.compact'), - }, - ].filter(Boolean); - - for (const spec of dragSpecs) { - if (!spec.panel || !spec.handle) continue; - this._restorePanelPosition(spec.id, spec.panel); - this._makePanelDraggable(spec.id, spec.panel, spec.handle); - } - // A restored panel grows a frame or two later, so the restore-time clamp - // is stale: re-clamp on every size change. - if (this._ppToggles && typeof ResizeObserver !== 'undefined') { - this._draggableResizeObserver = new ResizeObserver(() => - this._reclampDraggablePanels(), - ); - this._draggableResizeObserver.observe(this._ppToggles); - } - } - - _panelStorageKey(panelId) { - return `godsEyeView.${PANEL_POSITION_STORAGE_VERSION}.panelPos.${panelId}`; - } - _panelCollapseStorageKey(panelId) { return `godsEyeView.${PANEL_LAYOUT_STORAGE_VERSION}.panelCollapsed.${panelId}`; } @@ -142,159 +80,4 @@ export class PanelPositionControls { // storage unavailable } } - - _pinPanelToRight(panelEl) { - if (!panelEl) return; - const rect = panelEl.getBoundingClientRect(); - const rightOffset = Math.max(6, Math.round(window.innerWidth - rect.right)); - panelEl.style.right = `${rightOffset}px`; - panelEl.style.left = 'auto'; - } - - _restorePanelPosition(panelId, panelEl) { - try { - const raw = localStorage.getItem(this._panelStorageKey(panelId)); - if (!raw) return; - const pos = JSON.parse(raw); - if (!pos || typeof pos.left !== 'number' || typeof pos.top !== 'number') - return; - // A position saved at another window size can land off-screen. - const { left, top } = this._clampToViewport( - Math.round(pos.left), - Math.round(pos.top), - panelEl, - ); - panelEl.style.left = `${left}px`; - panelEl.style.top = `${top}px`; - panelEl.style.right = 'auto'; - panelEl.style.bottom = 'auto'; - if (panelId === 'pp-toggles') { - this._pinPanelToRight(panelEl); - } - } catch { - // ignore malformed saved panel position - } - } - - _clampToViewport(left, top, panelEl) { - const rect = panelEl.getBoundingClientRect(); - const maxLeft = Math.max(6, window.innerWidth - rect.width - 6); - const maxTop = Math.max(6, window.innerHeight - rect.height - 6); - return { - left: Math.max(6, Math.min(maxLeft, left)), - top: Math.max(6, Math.min(maxTop, top)), - }; - } - - _savePanelPosition(panelId, panelEl) { - const rect = panelEl.getBoundingClientRect(); - try { - localStorage.setItem( - this._panelStorageKey(panelId), - JSON.stringify({ - left: Math.round(rect.left), - top: Math.round(rect.top), - }), - ); - } catch { - // storage unavailable - } - } - - _promotePanelZ(panelEl) { - this._panelZCounter += 1; - if (this._panelZCounter > PANEL_Z_MAX) { - const promoted = [...document.querySelectorAll('.panel-draggable')] - .filter((el) => el.style.zIndex) - .sort((a, b) => Number(a.style.zIndex) - Number(b.style.zIndex)); - let z = PANEL_Z_BASE + 1; - for (const el of promoted) { - el.style.zIndex = String(z); - z += 1; - } - this._panelZCounter = z; - } - panelEl.style.zIndex = String(this._panelZCounter); - } - - _makePanelDraggable(panelId, panelEl, handleEl) { - // Z-order promotion: bring clicked panel to front of the stacking context - this.listen(panelEl, 'pointerdown', () => { - this._promotePanelZ(panelEl); - }); - - this.listen(handleEl, 'pointerdown', (event) => { - if (event.button !== 0) return; - if (event.target.closest('.panel-collapse-btn')) return; - if ( - event.target.closest( - 'input, select, option, button:not(.panel-collapse-btn)', - ) - ) - return; - - this._cancelDrag?.(); - event.preventDefault(); - const rect = panelEl.getBoundingClientRect(); - const startX = event.clientX; - const startY = event.clientY; - const offsetX = startX - rect.left; - const offsetY = startY - rect.top; - - panelEl.style.left = `${rect.left}px`; - panelEl.style.top = `${rect.top}px`; - panelEl.style.right = 'auto'; - panelEl.style.bottom = 'auto'; - panelEl.classList.add('panel-dragging'); - this._promotePanelZ(panelEl); - - const onMove = (moveEvent) => { - const nextLeftRaw = moveEvent.clientX - offsetX; - const nextTopRaw = moveEvent.clientY - offsetY; - const maxLeft = Math.max(6, window.innerWidth - rect.width - 6); - const maxTop = Math.max(6, window.innerHeight - rect.height - 6); - const nextLeft = Math.max(6, Math.min(maxLeft, nextLeftRaw)); - const nextTop = Math.max(6, Math.min(maxTop, nextTopRaw)); - panelEl.style.left = `${nextLeft}px`; - panelEl.style.top = `${nextTop}px`; - if (panelId === 'pp-toggles') { - this._layoutRightPanels(); - } - if (panelId === 'cctv-panel') { - this._syncCctvPanelViewport(); - } - }; - - const cancel = () => { - panelEl.classList.remove('panel-dragging'); - window.removeEventListener('pointermove', onMove); - window.removeEventListener('pointerup', onUp); - window.removeEventListener('pointercancel', onUp); - this._cancelDrag = null; - }; - const onUp = () => { - cancel(); - if (panelId === 'pp-toggles') { - this._pinPanelToRight(panelEl); - } - this._savePanelPosition(panelId, panelEl); - if (panelId === 'cctv-panel') { - this._syncCctvPanelViewport(); - } - }; - - this._cancelDrag = cancel; - window.addEventListener('pointermove', onMove); - window.addEventListener('pointerup', onUp); - window.addEventListener('pointercancel', onUp); - }); - } - destroy() { - if (this.destroyed) return; - this.destroyed = true; - this._cancelDrag?.(); - for (const remove of this.removers.splice(0)) remove(); - this._draggableResizeObserver?.disconnect(); - this._draggableResizeObserver = null; - } } diff --git a/src/ui/shellFacade.js b/src/ui/shellFacade.js index 43051ac38..2c1861b31 100644 --- a/src/ui/shellFacade.js +++ b/src/ui/shellFacade.js @@ -576,16 +576,6 @@ export class ShellFacade { return this._layerBindings._connectDirectionsCamera(...args); } - /** - * On window resize, keep the draggable panel on-screen — a panel positioned near an edge can fall - * outside a now-smaller viewport (audit U2). pp-toggles is right-pinned, so re-pin (horizontal) and - * clamp its top. No-op until the panel has been positioned (explicit inline top). - * @returns {void} - */ - _reclampDraggablePanels() { - return this._panelPosition._reclampDraggablePanels(); - } - /** * Creates one CesiumJS PostProcessStage per visual style and registers * it with the scene. Each stage starts with intensity 0 (invisible) @@ -911,15 +901,6 @@ export class ShellFacade { return this._panelChrome._maybeNotifyLayoutReset(...arguments); } - /** - * Sets up drag-to-reposition for legacy floating controls. The right rail - * and left accordion remain fixed so their HUD alignment is deterministic. - * @returns {void} - */ - _initPanelDrag() { - return this._panelPosition._initPanelDrag(); - } - _persistAwarenessSelection(...args) { return this._layerBindings._persistAwarenessSelection(...args); } @@ -980,15 +961,6 @@ export class ShellFacade { return this._radioControls?._renderRadioState?.(...args); } - /** - * Returns the versioned localStorage key for a panel's saved position. - * @param {string} panelId - DOM id of the panel. - * @returns {string} localStorage key. - */ - _panelStorageKey(panelId) { - return this._panelPosition._panelStorageKey(panelId); - } - /** * Returns the versioned localStorage key for a panel's collapsed state. * @param {string} panelId - DOM id of the panel. @@ -1083,64 +1055,6 @@ export class ShellFacade { return this._panelChrome._syncPanelCollapseButton(...arguments); } - /** - * Converts a panel from left-positioned to right-anchored so it expands - * leftward on resize. Used for the right-rail parameter panel. - * @param {HTMLElement} panelEl - The panel to re-anchor. - * @returns {void} - */ - _pinPanelToRight(panelEl) { - return this._panelPosition._pinPanelToRight(panelEl); - } - - /** - * Restores a panel's top/left position from localStorage. - * Right-rail panels are additionally pinned to the right edge. - * @param {string} panelId - DOM id of the panel. - * @param {HTMLElement} panelEl - The panel DOM element. - * @returns {void} - */ - _restorePanelPosition(panelId, panelEl) { - return this._panelPosition._restorePanelPosition(panelId, panelEl); - } - - /** - * Clamp a desired left/top so the panel stays fully on-screen (6px inset), matching the drag - * clamp (ui.js ~1822). Width/height are position-independent, so reading the rect first is safe. - * @param {number} left - desired left (px) - * @param {number} top - desired top (px) - * @param {HTMLElement} panelEl - the panel element - * @returns {{left:number, top:number}} - */ - _clampToViewport(left, top, panelEl) { - return this._panelPosition._clampToViewport(left, top, panelEl); - } - - /** - * Persists a panel's current bounding-rect position to localStorage. - * @param {string} panelId - DOM id of the panel. - * @param {HTMLElement} panelEl - The panel DOM element. - * @returns {void} - */ - _savePanelPosition(panelId, panelEl) { - return this._panelPosition._savePanelPosition(panelId, panelEl); - } - - /** - * Promotes a panel to the top of the panel z band [PANEL_Z_BASE, PANEL_Z_MAX]. - * Renormalizes all promoted panels when the band is exhausted so panels can - * never climb above the voice pill (150), toasts (200), or clean-view exit (300). - * @param {HTMLElement} panelEl - Panel to bring to front. - * @returns {void} - */ - _promotePanelZ(panelEl) { - return this._panelPosition._promotePanelZ(panelEl); - } - - _makePanelDraggable(panelId, panelEl, handleEl) { - return this._panelPosition._makePanelDraggable(panelId, panelEl, handleEl); - } - _buildSharePanelState() { return this._panelChrome._buildSharePanelState(...arguments); } diff --git a/src/ui/shellLifecycle.test.mjs b/src/ui/shellLifecycle.test.mjs index 7d48ffb06..b381d3165 100644 --- a/src/ui/shellLifecycle.test.mjs +++ b/src/ui/shellLifecycle.test.mjs @@ -3,7 +3,6 @@ import assert from 'node:assert/strict'; import { UiLifetime } from './uiLifetime.js'; import { PanelChrome } from './panelChrome.js'; import { PanelLayoutController } from './panelLayoutController.js'; -import { PanelPositionControls } from './panelPositionControls.js'; import { ShellFeedback } from './shellFeedback.js'; import { RecordingControls } from './recordingControls.js'; @@ -174,46 +173,6 @@ test('panel layout coalesces each rail and cannot rearm after destruction', () = } }); -test('panel disposal cancels a live drag without saving or accepting later pointer movement', () => { - const f = fixture(); - try { - const owner = new PanelPositionControls({ - syncPanelCollapseButton() {}, - layoutRightPanels() {}, - syncCctvPanelViewport() {}, - showToast() {}, - }); - const panel = f.element(), - handle = f.element(); - owner._makePanelDraggable('sample', panel, handle); - handle.dispatchEvent( - Object.assign(new Event('pointerdown', { cancelable: true }), { - button: 0, - clientX: 30, - clientY: 40, - }), - ); - assert.equal(panel.classList.contains('panel-dragging'), true); - owner.destroy(); - const before = { ...panel.style }; - window.dispatchEvent( - Object.assign(new Event('pointermove'), { clientX: 500, clientY: 300 }), - ); - window.dispatchEvent(new Event('pointerup')); - assert.deepEqual(panel.style, before); - assert.equal(panel.classList.contains('panel-dragging'), false); - owner._makePanelDraggable('sample', panel, handle); - owner._initPanelDrag(); - owner._ppToggles.style.top = '10000px'; - owner._reclampDraggablePanels(); - assert.equal(owner._ppToggles.style.top, '10000px'); - assert.equal(owner.removers.length, 0); - assert.deepEqual(f.writes, []); - } finally { - f.restore(); - } -}); - test('feedback disposal clears toast and polling work and rejects retained notices', () => { const f = fixture(); try { @@ -314,7 +273,7 @@ test('weather rail orders between CCTV and Context, resets positions, observes a removeProperty: (key) => positions.delete(key), positions, }; - panel.classList.add('panel-draggable', 'panel-dragging', 'collapsed'); + panel.classList.add('collapsed'); const button = f.element(); button.closest = () => panel; const attributes = new Map(); @@ -349,8 +308,6 @@ test('weather rail orders between CCTV and Context, resets positions, observes a assert.ok(synced.includes('weather-panel')); assert.ok(observed.has(weather)); assert.equal(weather.style.positions.size, 0); - assert.equal(weather.classList.contains('panel-draggable'), false); - assert.equal(weather.classList.contains('panel-dragging'), false); assert.equal(weather.button.textContent, '◀'); assert.equal(weather.button.getAttribute('aria-expanded'), 'false'); assert.equal(weather.button.title, 'Expand WEATHER'); diff --git a/src/ui/styles/controls.css b/src/ui/styles/controls.css index 78932fc3b..11c5089e4 100644 --- a/src/ui/styles/controls.css +++ b/src/ui/styles/controls.css @@ -72,37 +72,6 @@ text-shadow: 0 0 8px rgba(0, 212, 255, 0.3); } -.panel-drag-handle { - cursor: grab; - user-select: none; -} - -.panel-drag-handle:active { - cursor: grabbing; -} - -.panel-drag-handle.compact { - margin-left: auto; - margin-bottom: 2px; - padding: 2px 8px; - border-radius: 999px; - border: 1px solid rgba(255, 255, 255, 0.12); - background: rgba(255, 255, 255, 0.04); - font-family: var(--font-mono); - font-size: 8px; - letter-spacing: 1.4px; - color: var(--text-dim); -} - -.panel-drag-handle.compact:hover { - color: var(--text-primary); - border-color: var(--glass-border-hover); -} - -.panel-dragging { - opacity: 0.96; -} - .panel-title { font-family: var(--font-mono); font-size: 9px;