fix(playback): dismiss the side panel from the exposed edge gutter

The inset card leaves the hot zone's left strip visible beside it, above the
scrim, so a click there re-ran show() instead of dismissing. A completed
primary press in the zone now closes an open panel; hovering there still
counts as inside, since a hover-opened panel leaves the pointer resting in
that strip. The playing episode row also keeps its cyan tint on hover/focus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-09-30 20:17:48 +02:00
1 parent 07d3f48bc2
commit 5ff9de0acb
4 files changed
+27 -1

No files matched your search

@@ -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
@@ -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();
@@ -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');
}
@@ -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".