feat(design): players on touch and the C5 control set, restart on the card (E28 S3f2)

Plex player: a finger's tap on the picture only shows or hides the controls,
the hide timer starts with playback, a double-tap on a side third skips 10 s,
the seek bar is a 2.75rem touch-none hit area with its head shown on touch.
The row is play/pause, Back by position, Next only with a next item, mute,
time, tracks, a fixed ⋯ (settings, media info, stats, shortcuts, download)
and fullscreen; Stop, Restart and the ±10 s buttons are gone.

A started card shows the video card's Continue + Restart pair (one shared
MediaResumeButtons, beside the picture's button), and its menu lists the
restart; the player opens that item at 0 (fromStart). YouTube modal: the
description opens from an ⓘ, the queue arrows sit inside the screen.
This commit is contained in:
2026-09-27 02:12:54 +02:00
parent 17916b411a
commit 89e65ca331
33 changed files with 1100 additions and 282 deletions
+2 -47
View File
@@ -8,7 +8,6 @@ import {
Clock,
Play,
Radio,
RotateCcw,
// Aliased: `Video` is this app's own domain type (lib/api), and it wins the name.
Video as VideoIcon,
} from "lucide-react";
@@ -23,7 +22,6 @@ import { isPlainLeftClick } from "../lib/nav";
import { endsTextSelection } from "../lib/textSelection";
import ChannelNameLink from "./ChannelNameLink";
import { IconButton } from "./ui/IconButton";
import { Button } from "./ui/Button";
import { SURFACE } from "./ui/surfaces";
import { MoreButton } from "./ui/MoreButton";
import { HOVER_REVEAL, HOVER_YIELD } from "./ui/reveal";
@@ -36,6 +34,7 @@ import {
mediaTitleClass,
} from "./ui/mediaCardStyles";
import { MediaCard, MediaProgress } from "./ui/MediaCard";
import { MediaResumeButtons } from "./MediaResumeButtons";
/** The "…" that opens the video's action list, and whether its menu is the open one. */
type More = { props: Record<string, unknown>; open: boolean };
@@ -294,51 +293,7 @@ function Thumb({
{onOpen && (
<div className={MEDIA_HOVER_LAYER}>
{inProgress ? (
<>
{small ? (
<IconButton
variant="primary"
size="md"
round
onClick={open(null)}
label={t("card.continue")}
title={t("card.continueTitle")}
>
<Play className="fill-current" />
</IconButton>
) : (
<Button
size="sm"
round
onClick={open(null)}
title={t("card.continueTitle")}
icon={<Play className="fill-current" />}
>
{t("card.continue")}
</Button>
)}
{small ? (
<IconButton
variant="media"
size="md"
onClick={open(0)}
label={t("card.restart")}
title={t("card.restartTitle")}
>
<RotateCcw />
</IconButton>
) : (
<Button
variant="media"
size="sm"
onClick={open(0)}
title={t("card.restartTitle")}
icon={<RotateCcw />}
>
{t("card.restart")}
</Button>
)}
</>
<MediaResumeButtons small={small} onContinue={open(null)} onRestart={open(0)} />
) : (
<IconButton
variant="media"