feat(player): a roomier YouTube player, its panel shut on demand

- The full-height previous/next strips beside the card are gone: the panel's
  transport row and Shift+arrows step on their own, and the video gains the
  7rem the strips' gutters took.
- Close is one icon in the card's top-right corner (the wide panel's head,
  the stacked card's title row).
- The panel's description is cut to three lines with More / Less
  (ClampedText now takes the linked text as children).
- Wide, the panel shuts to a 2.75rem strip (close, open, previous, next) for
  a bigger video; it stays shut while the queue steps on, and a video opened
  from a card starts it open. Focus moves to the counterpart toggle.
- The wheel has one meaning per place: over a panel (or the stacked card)
  with something to scroll it only scrolls, never falling through to the
  volume at the end; over the video, or a panel that fits, it is the volume.
- A long channel name is cut on the panel's line instead of running the
  20rem panel into a sideways scroll.
This commit is contained in:
2026-10-09 00:27:29 +02:00
parent 600d5c39da
commit ece74a42ab
8 changed files with 466 additions and 305 deletions
+11 -9
View File
@@ -77,7 +77,7 @@ function loadYouTubeApi(): Promise<YTNamespace> {
* per video and always starts at 100, so the level lives in a ref and onReady re-applies it. Level 0 IS
* the muted state (that's how YouTube's own control behaves), so there's no separate `muted` flag.
* Mouse-wheel over the WHOLE modal (backdrop + card) adjusts volume, not just the small player area —
* except over a `[data-wheel-scrolls]` panel that can still scroll that way. */
* except over a `[data-wheel-scrolls]` panel that has anything to scroll, which keeps the wheel. */
export function useYtVolume({
playerRef,
dialogRef,
@@ -154,15 +154,17 @@ export function useYtVolume({
const onWheelSpin = useEffectEvent((e: WheelEvent) => {
// Ctrl/⌘+wheel is the browser's zoom gesture — never swallow it.
if (e.ctrlKey || e.metaKey) return;
// Over a panel that can still scroll that way, the wheel scrolls it: the wide player's side panel
// holds the description, which a wheel spent on the volume could never be read past.
const panel =
e.target instanceof Element ? e.target.closest<HTMLElement>("[data-wheel-scrolls]") : null;
// One gesture, one meaning (owner): over a panel with something to scroll the wheel only ever
// scrolls it — at its end too, where it once fell through to the volume mid-spin. Over the video,
// or a panel that fits, it is the volume. The nearer mark wins: the video inside a stacked card
// that scrolls is still the volume.
const zone =
e.target instanceof Element
? e.target.closest<HTMLElement>("[data-wheel-scrolls],[data-wheel-volume]")
: null;
if (
panel &&
(e.deltaY < 0
? panel.scrollTop > 0
: panel.scrollTop + panel.clientHeight < panel.scrollHeight - 1) // -1: sub-pixel heights
zone?.hasAttribute("data-wheel-scrolls") &&
zone.scrollHeight > zone.clientHeight + 1 // +1: sub-pixel heights
)
return;
e.preventDefault();