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
+12 -9
View File
@@ -1,4 +1,4 @@
import { useLayoutEffect, useRef, useState } from "react";
import { useLayoutEffect, useRef, useState, type ReactNode } from "react";
import { Button } from "./Button";
// Static class names, so Tailwind sees them.
@@ -8,19 +8,25 @@ const CLAMP = { 3: "line-clamp-3", 5: "line-clamp-5", 8: "line-clamp-8" } as con
* when the text is actually cut — measured, so a short text carries no dead button. Re-measured on a
* resize: the window and the text-size slider both move where the cut falls. Another text starts
* collapsed: a view reused for the next item (a show page opening another show) must not open its
* text already expanded. */
* text already expanded. `children` draws the text in a richer form (a description's links and
* timestamps); `text` still names it, so another text is still another text. */
export function ClampedText({
text,
lines,
moreLabel,
lessLabel,
className,
textClassName = "measure whitespace-pre-line text-sm leading-relaxed text-muted",
children,
}: {
text: string;
lines: keyof typeof CLAMP;
moreLabel: string;
lessLabel: string;
className?: string;
/** The text box's own dress (the default: a summary's muted paragraph). */
textClassName?: string;
children?: ReactNode;
}) {
const [expanded, setExpanded] = useState(false);
const [clipped, setClipped] = useState(false);
@@ -29,7 +35,7 @@ export function ClampedText({
setShown(text);
setExpanded(false);
}
const ref = useRef<HTMLParagraphElement>(null);
const ref = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
const el = ref.current;
if (!el || expanded) return;
@@ -41,12 +47,9 @@ export function ClampedText({
}, [text, expanded]);
return (
<div className={className}>
<p
ref={ref}
className={`measure whitespace-pre-line text-sm leading-relaxed text-muted ${expanded ? "" : CLAMP[lines]}`}
>
{text}
</p>
<div ref={ref} className={`${textClassName} ${expanded ? "" : CLAMP[lines]}`}>
{children ?? text}
</div>
{(clipped || expanded) && (
<Button
variant="link"
@@ -24,7 +24,6 @@ const PLAIN: Record<string, [number, string]> = {
"LanguageSwitcher.tsx": [1, "an option row"],
"NavSidebar.tsx": [4, "the account trigger and its menu rows"],
"PaletteLauncher.tsx": [2, "launcher controls"],
"PlayerModal.tsx": [2, "previous / next arrows"],
"PlaylistsRail.tsx": [1, "the load-error retry"],
"PlexCollectionEditor.tsx": [1, "a checkbox row in a picker"],
"PlexInfo.tsx": [1, "the rating filter (a number)"],