mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Review fixes P1.1-P2.7: a panel hidden at mount gets its tab when revealed; any live opening of a rail panel makes it the owner, restores do not; a short lane scrolls the strip with a thin scrollbar and brings a newly opened tab into view once; the join and the panel cover stay inside the lane and the fillet shrinks to the room left; a share link carries an owner-collapsed peer as collapsed so the recipient opens the sender's panel; border-box on phones; the strip's column folds into the panel width at 721-760px; the focus ring draws inside the tab. The compact Radio launcher moves from Context's header into a tool slot under the tabs on desktop (back into the header for the phone sheet) and always toggles the compact player, also while Context is open; the player's Details button leads to the Radio section. The jump-to-section role, its two labels and the #804 closings of the player on Context expansion and rail layout passes are gone with it. The rail panels' own collapse arrows are hidden on desktop: the tab opens and closes a panel, Escape inside closes it and returns focus to the tab. The tabs are denser and the rail climbs toward the HUD corner above its left-rail baseline when the strip's column is taller than the lane, so the strip no longer scrolls on windows down to about 680px tall; the measuring pass keeps the strip's scroll offset. scripts/qa-radio.mjs expectations follow the launcher's new role. Co-Authored-By: Claude Fable 5.1 <[email protected]>
158 lines
5.2 KiB
JavaScript
158 lines
5.2 KiB
JavaScript
import { t, msg } from '../i18n/index.js';
|
|
import { bindRadioControls } from './radioBindings.js';
|
|
import { renderRadioState } from './radioPresentation.js';
|
|
|
|
/** Own Radio DOM, tuner interaction and subscription; receive playback and application actions. */
|
|
export class RadioControls {
|
|
constructor({ elements, radio, actions, canvas }) {
|
|
Object.assign(this, elements);
|
|
this.radio = radio;
|
|
this.actions = actions;
|
|
this.canvas = canvas;
|
|
this.destroyed = false;
|
|
this.listeners = new AbortController();
|
|
this._radioUnsubscribe = null;
|
|
this._radioState = null;
|
|
this._radioCategorySignature = '';
|
|
this._radioTunerStations = [];
|
|
this._radioTunerPool = [];
|
|
this._radioTunerDragging = false;
|
|
this._radioTunerDragStartSlot = 0;
|
|
this._radioTunerDragSnapshot = null;
|
|
this._radioTunerLastSlot = 0;
|
|
this._radioTunerDragDirection = 0;
|
|
this._radioTunerCoordinate = 0;
|
|
this._radioTunerPointerId = null;
|
|
this._radioTunerKeyboardKey = null;
|
|
this._radioTunerBandSignature = '';
|
|
this._radioTunerSelectedId = null;
|
|
this._radioTunerBandPinnedForNavigation = false;
|
|
this._refreshRadioTunerBand = null;
|
|
bindRadioControls.call(this);
|
|
}
|
|
|
|
listen(target, type, handler, options = {}) {
|
|
target?.addEventListener(type, handler, {
|
|
...options,
|
|
signal: this.listeners.signal,
|
|
});
|
|
}
|
|
|
|
connect() {
|
|
this._radioUnsubscribe?.();
|
|
this._radioUnsubscribe = null;
|
|
if (this.destroyed) return;
|
|
this._radioUnsubscribe = this.radio.subscribe?.((state) =>
|
|
this._renderRadioState(state),
|
|
);
|
|
}
|
|
|
|
_renderRadioState(state) {
|
|
renderRadioState.call(this, state);
|
|
}
|
|
|
|
/**
|
|
* Reveal the newly enabled directory and transport inside Context without
|
|
* moving focus, the page, or the globe. Only the expanded Enable path calls
|
|
* this helper.
|
|
* @param {HTMLElement} trigger Initiating Radio Enable button.
|
|
* @returns {Promise<boolean>} Whether the internal scroller moved.
|
|
*/
|
|
async _revealRadioControlsAfterExplicitEnable(trigger) {
|
|
const contextPanel = document.getElementById('global-context-panel');
|
|
const scroller =
|
|
(document.documentElement?.dataset.uiTheme === 'cyber' &&
|
|
contextPanel?.querySelector('.cyber-panel-body')) ||
|
|
contextPanel?.querySelector('.global-context-panel-inner');
|
|
const directory = this._radioPanel?.querySelector('.radio-directory-row');
|
|
const transport = this._radioPanel?.querySelector('.radio-transport');
|
|
if (
|
|
!contextPanel ||
|
|
contextPanel.classList.contains('collapsed') ||
|
|
!scroller ||
|
|
!directory ||
|
|
!transport ||
|
|
!this._radioState?.enabled
|
|
)
|
|
return false;
|
|
|
|
await new Promise((resolve) =>
|
|
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
|
);
|
|
if (this.destroyed || !this._radioState?.enabled || !trigger?.isConnected)
|
|
return false;
|
|
|
|
const viewport = scroller.getBoundingClientRect();
|
|
const directoryRect = directory.getBoundingClientRect();
|
|
const transportRect = transport.getBoundingClientRect();
|
|
const margin = 10;
|
|
const minimum =
|
|
scroller.scrollTop + transportRect.bottom - (viewport.bottom - margin);
|
|
const maximum =
|
|
scroller.scrollTop + directoryRect.top - (viewport.top + margin);
|
|
const desired =
|
|
minimum <= maximum
|
|
? Math.min(Math.max(scroller.scrollTop, minimum), maximum)
|
|
: minimum;
|
|
const next = Math.min(
|
|
Math.max(0, scroller.scrollHeight - scroller.clientHeight),
|
|
Math.max(0, desired),
|
|
);
|
|
if (Math.abs(next - scroller.scrollTop) < 1) return false;
|
|
const reducedMotion = window.matchMedia?.(
|
|
'(prefers-reduced-motion: reduce)',
|
|
)?.matches;
|
|
scroller.scrollTo({
|
|
top: next,
|
|
behavior: reducedMotion ? 'auto' : 'smooth',
|
|
});
|
|
return true;
|
|
}
|
|
|
|
/** Keep the launcher's ARIA state matched to the compact player. */
|
|
_syncContextRadioLauncherState() {
|
|
if (this.destroyed || !this._contextRadioToggleBtn) return;
|
|
const compactOpen = Boolean(
|
|
this._contextRadioDock?.classList.contains('disclosure-open'),
|
|
);
|
|
this._contextRadioToggleBtn.setAttribute(
|
|
'aria-expanded',
|
|
String(compactOpen),
|
|
);
|
|
const launcherLabel = t(
|
|
compactOpen
|
|
? msg('Close compact Radio controls')
|
|
: msg('Open compact Radio controls'),
|
|
);
|
|
this._contextRadioToggleBtn.setAttribute('aria-label', launcherLabel);
|
|
this._contextRadioToggleBtn.title = launcherLabel;
|
|
}
|
|
|
|
destroy() {
|
|
if (this.destroyed) return;
|
|
this.destroyed = true;
|
|
this.listeners.abort();
|
|
this._radioUnsubscribe?.();
|
|
this._radioUnsubscribe = null;
|
|
const pointer = this._radioTunerPointerId;
|
|
this._radioTunerPointerId = null;
|
|
this._radioTunerDragging = false;
|
|
this._radioTunerKeyboardKey = null;
|
|
this._radioTunerDragSnapshot = null;
|
|
this._radioTunerPool = [];
|
|
this._radioTunerStations = [];
|
|
this._radioTunerBandPinnedForNavigation = false;
|
|
try {
|
|
if (pointer !== null)
|
|
this._radioTunerSlider?.releasePointerCapture(pointer);
|
|
} catch {
|
|
/* capture already released */
|
|
}
|
|
this._radioTuner?.classList.remove('is-dragging', 'is-static');
|
|
document
|
|
.getElementById('title-bar')
|
|
?.classList.remove('radio-broadcasting');
|
|
this.radio.endTuning();
|
|
}
|
|
}
|