feat(design): every button on its family, the chip primitive, the lint rule (E28 S3b2)
The second half of the button sweep: the remaining hand-rolled buttons in
feature components, the design lab and the components/ui composites now
stand on Button, IconButton, Pressable, Chip or DragHandle.
- Chip + DismissibleChip (filter / token / tag / suggest / ghost, 24px
floor, 44 on touch) replace five chip geometries; FilterChipGroup
renders Chips; overflow lanes measure a mirror painted by chipClass.
- The Plex control row is IconButton's player rung (36, 20px glyph,
round, overlay; 44 on touch); the YT modal bar is on the sm rung with
pressed cycle toggles and names that survive a folded label.
- New variants: IconButton ghost-accent, danger-soft; Button warn on a
new state-warn utility. The .btn-{tone} recipes and btnCls are gone.
- Fixed on the way: membership rows (collection editor, Plex playlist
add) announce checked/mixed; a one-letter tag chip was 21px wide;
descriptionLinks moved out of lib/ (it renders components).
- local/button-primitive: a raw <button>/<FocusSafeButton> outside the
control-primitive files is a lint error (contract-tested, seen red).
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Repeat, Repeat1, Shuffle, SkipBack, SkipForward } from "lucide-react";
|
||||
import type { AutoMode, LoopMode } from "../lib/playerModes";
|
||||
import { FocusSafeButton } from "./ui/FocusSafeButton";
|
||||
import { Button } from "./ui/Button";
|
||||
|
||||
// Presentational controls for the YouTube modal player (PlayerModal), split out of the god component —
|
||||
// the sibling of plexPlayerControls.tsx. No state or effects here; all behaviour comes in via props.
|
||||
@@ -35,76 +35,69 @@ export function PlayerTransportControls({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const docked = variant === "docked";
|
||||
const navBtn = docked
|
||||
? "inline-flex items-center gap-1 text-white/80 enabled:not-aria-disabled:hover:text-white disabled:opacity-30 transition"
|
||||
: "inline-flex items-center gap-1.5 text-muted enabled:not-aria-disabled:hover:text-fg disabled:opacity-40 transition";
|
||||
// The docked bar sits on the video (always dark): the overlay variant; the modal's is a page surface.
|
||||
const chrome = docked ? "overlay" : "ghost";
|
||||
const navLabel = docked ? "hidden sm:inline" : "";
|
||||
const counter = docked ? "tabular-nums text-white/70" : "text-muted tabular-nums";
|
||||
const divider = docked ? "h-4 w-px bg-white/20" : "w-px h-4 bg-border";
|
||||
const toggle = (on: boolean) =>
|
||||
docked
|
||||
? `inline-flex items-center gap-1 rounded-lg px-2 py-1 transition ${
|
||||
on ? "bg-white/20 text-white" : "text-white/70 hover:bg-white/10 hover:text-white"
|
||||
}`
|
||||
: `inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1 transition ${
|
||||
on ? "bg-accent/15 text-accent" : "text-muted hover:bg-surface hover:text-fg"
|
||||
}`;
|
||||
const toggleLabel = docked ? "hidden md:inline" : "";
|
||||
return (
|
||||
<>
|
||||
<FocusSafeButton
|
||||
<Button
|
||||
variant={chrome}
|
||||
size="sm"
|
||||
icon={<SkipBack />}
|
||||
onClick={onPrev}
|
||||
disabled={index === 0}
|
||||
label={t("player.previous")}
|
||||
title={`${t("player.previous")} · Shift+←`}
|
||||
className={navBtn}
|
||||
>
|
||||
<SkipBack className="h-4 w-4" />
|
||||
<span className={navLabel}>{t("player.previous")}</span>
|
||||
</FocusSafeButton>
|
||||
</Button>
|
||||
<span className={counter}>
|
||||
{index + 1} / {queueLength}
|
||||
</span>
|
||||
<FocusSafeButton
|
||||
<Button
|
||||
variant={chrome}
|
||||
size="sm"
|
||||
onClick={onNext}
|
||||
disabled={index === queueLength - 1}
|
||||
label={t("player.next")}
|
||||
title={`${t("player.next")} · Shift+→`}
|
||||
className={navBtn}
|
||||
>
|
||||
<span className={navLabel}>{t("player.next")}</span>
|
||||
<SkipForward className="h-4 w-4" />
|
||||
</FocusSafeButton>
|
||||
<SkipForward />
|
||||
</Button>
|
||||
<span className={divider} />
|
||||
{/* Persistent playback settings — cycle on click, saved to your account. */}
|
||||
<button
|
||||
<Button
|
||||
variant={chrome}
|
||||
size="sm"
|
||||
pressed={autoMode !== "off"}
|
||||
icon={
|
||||
autoMode === "prev" ? <SkipBack /> : autoMode === "random" ? <Shuffle /> : <SkipForward />
|
||||
}
|
||||
onClick={onCycleAuto}
|
||||
label={`${t("player.autoAdvance.label")}: ${t(`player.autoAdvance.${autoMode}`)}`}
|
||||
title={t("player.autoAdvance.hint")}
|
||||
className={toggle(autoMode !== "off")}
|
||||
>
|
||||
{autoMode === "prev" ? (
|
||||
<SkipBack className="h-3.5 w-3.5" />
|
||||
) : autoMode === "random" ? (
|
||||
<Shuffle className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<SkipForward className="h-3.5 w-3.5" />
|
||||
)}
|
||||
<span className={toggleLabel}>
|
||||
{t("player.autoAdvance.label")}: {t(`player.autoAdvance.${autoMode}`)}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant={chrome}
|
||||
size="sm"
|
||||
pressed={loopMode !== "off"}
|
||||
icon={loopMode === "one" ? <Repeat1 /> : <Repeat />}
|
||||
onClick={onCycleLoop}
|
||||
label={`${t("player.loop.label")}: ${t(`player.loop.${loopMode}`)}`}
|
||||
title={t("player.loop.hint")}
|
||||
className={toggle(loopMode !== "off")}
|
||||
>
|
||||
{loopMode === "one" ? (
|
||||
<Repeat1 className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Repeat className="h-3.5 w-3.5" />
|
||||
)}
|
||||
<span className={toggleLabel}>
|
||||
{t("player.loop.label")}: {t(`player.loop.${loopMode}`)}
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user