diff --git a/docs/architecture/player-controls-contract.md b/docs/architecture/player-controls-contract.md index 7686eee6c..055121a8c 100644 --- a/docs/architecture/player-controls-contract.md +++ b/docs/architecture/player-controls-contract.md @@ -732,7 +732,10 @@ neither opens nor arms the hint: the zone re-arms on the next real scrim and below the panel while open, preserving the pointer target until the opening animation covers it. A delayed fullscreen paint therefore cannot turn a stationary edge hover into a synthetic mouse-leave that closes the -panel. The zone stops above the controls bar (`bottom: max(25%, 140px)`) so +panel. The open card is inset from the edge, so the zone's left strip stays +exposed beside it: hovering there still counts as inside (a hover-opened +panel leaves the pointer resting in that strip), but a completed primary +press there closes the panel as the scrim would. The zone stops above the controls bar (`bottom: max(25%, 140px)`) so the leftmost transport button never loses a click or tap to it. The `C` key opens it too and focuses the search field — or, for a host without one, the panel itself (`tabindex="-1"`), so the next Tab reaches its first control diff --git a/libs/ui/playback/src/lib/fullscreen-channel-panel/fullscreen-channel-panel.component.spec.ts b/libs/ui/playback/src/lib/fullscreen-channel-panel/fullscreen-channel-panel.component.spec.ts index dbeb58e32..53f427d4d 100644 --- a/libs/ui/playback/src/lib/fullscreen-channel-panel/fullscreen-channel-panel.component.spec.ts +++ b/libs/ui/playback/src/lib/fullscreen-channel-panel/fullscreen-channel-panel.component.spec.ts @@ -547,6 +547,17 @@ describe('FullscreenChannelPanelComponent', () => { expect(query('host-list')?.textContent?.trim()).toBe(''); }); + it('closes on a click in the edge gutter the inset card leaves exposed', () => { + setFullscreen(stage); + openByHover(); + hotZone()?.dispatchEvent(pointerEvent('pointerdown')); + hotZone()?.dispatchEvent(pointerEvent('pointerup')); + fixture.detectChanges(); + + expect(isOpen()).toBe(false); + expect(query('host-list')).not.toBeNull(); + }); + it('closes on the scrim but keeps the list mounted for the next opening', () => { setFullscreen(stage); openByHover(); diff --git a/libs/ui/playback/src/lib/fullscreen-channel-panel/fullscreen-channel-panel.component.ts b/libs/ui/playback/src/lib/fullscreen-channel-panel/fullscreen-channel-panel.component.ts index d69d2332a..07ebbf87b 100644 --- a/libs/ui/playback/src/lib/fullscreen-channel-panel/fullscreen-channel-panel.component.ts +++ b/libs/ui/playback/src/lib/fullscreen-channel-panel/fullscreen-channel-panel.component.ts @@ -230,6 +230,8 @@ export class FullscreenChannelPanelComponent implements OnDestroy { * pointerup, not pointerdown: the hot zone must still be the click * target when the press completes, so the click that follows dies on it * instead of reaching the video's click-to-pause. + * While the panel is open the zone is still exposed in the card's left + * inset, above the scrim, so a click there dismisses like the scrim does. */ onHotZonePointerDown(event: PointerEvent): void { this.hotZonePressPointerId = @@ -242,6 +244,10 @@ export class FullscreenChannelPanelComponent implements OnDestroy { if (pressed === null || pressed !== event.pointerId) { return; } + if (this.state.open()) { + this.state.hide(); + return; + } this.state.show('pointer'); } diff --git a/libs/ui/playback/src/lib/fullscreen-episode-panel/fullscreen-episode-panel.component.scss b/libs/ui/playback/src/lib/fullscreen-episode-panel/fullscreen-episode-panel.component.scss index d6fc0c771..ba06cf187 100644 --- a/libs/ui/playback/src/lib/fullscreen-episode-panel/fullscreen-episode-panel.component.scss +++ b/libs/ui/playback/src/lib/fullscreen-episode-panel/fullscreen-episode-panel.component.scss @@ -167,6 +167,12 @@ .episode-panel__item--playing { background: rgba(92, 214, 255, 0.14); cursor: default; + + // Keep the cue while the row is hovered or focused. + &:hover, + &:focus-visible { + background: rgba(92, 214, 255, 0.2); + } } // Watched rows step back so the unwatched ones read as "what's left".