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:
@@ -82,6 +82,9 @@ import {
|
||||
anyBlockingLayerAbove,
|
||||
escapeHandled,
|
||||
} from "../lib/layerStack";
|
||||
import { Button } from "./ui/Button";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
|
||||
// The rich info overlay (poster/cast/ratings) reuses the same component as the card's info page.
|
||||
const PlexInfo = lazy(() => import("./PlexInfo"));
|
||||
@@ -1231,13 +1234,15 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3 text-white">
|
||||
<button
|
||||
<IconButton
|
||||
variant="overlay"
|
||||
size="player"
|
||||
onMedia
|
||||
onClick={handleBack}
|
||||
className="p-1.5 rounded-lg hover:bg-white/15"
|
||||
title={t("plex.player.back")}
|
||||
label={t("plex.player.back")}
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<ArrowLeft />
|
||||
</IconButton>
|
||||
<div className="min-w-0">
|
||||
<div className="font-semibold truncate">
|
||||
{detail?.kind === "episode" && detail.show_title ? detail.show_title : detail?.title}
|
||||
@@ -1270,9 +1275,10 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
|
||||
{/* Skip intro / credits (with the auto-skip countdown fill, if armed) */}
|
||||
{activeMarker && (
|
||||
<button
|
||||
<Button
|
||||
variant="media"
|
||||
onClick={() => doSkip(activeMarker)}
|
||||
className={`absolute right-6 bottom-28 overflow-hidden rounded-lg bg-white/90 px-4 py-2 text-sm font-medium text-black transition hover:bg-white ${
|
||||
className={`absolute right-6 bottom-28 overflow-hidden ${
|
||||
uiVisible ? "opacity-100" : "opacity-80"
|
||||
}`}
|
||||
>
|
||||
@@ -1295,7 +1301,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
? t("plex.player.skipIntro")
|
||||
: t("plex.player.skipCredits")}
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Bottom controls */}
|
||||
@@ -1476,14 +1482,17 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
{/* Quick TRACKS menu (audio / subtitle) — the frequent action, on its own button. */}
|
||||
{detail && (detail.audio_streams.length > 1 || textSubs.length > 0) && (
|
||||
<div className="relative">
|
||||
<button
|
||||
<IconButton
|
||||
variant="overlay"
|
||||
size="player"
|
||||
onMedia
|
||||
ref={tracksTriggerRef}
|
||||
onClick={() => setTracksOpen((o) => !o)}
|
||||
aria-label={t("plex.player.tracks")}
|
||||
className={`p-1 hover:text-accent ${tracksOpen ? "text-accent" : ""}`}
|
||||
aria-expanded={tracksOpen}
|
||||
label={t("plex.player.tracks")}
|
||||
>
|
||||
<Languages className="w-5 h-5" />
|
||||
</button>
|
||||
<Languages />
|
||||
</IconButton>
|
||||
{tracksOpen && (
|
||||
<div
|
||||
ref={setTracksNode}
|
||||
@@ -1496,42 +1505,42 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
<>
|
||||
<PanelHead>{t("plex.player.audio")}</PanelHead>
|
||||
{detail.audio_streams.map((a) => (
|
||||
<button
|
||||
<Pressable
|
||||
key={a.ord}
|
||||
onClick={() => changeAudio(a.ord)}
|
||||
aria-pressed={(audioOrd ?? 0) === a.ord}
|
||||
className={`block w-full truncate rounded px-2 py-1 text-left hover:bg-white/10 ${
|
||||
className={`block w-full truncate rounded px-2 py-1 hover:bg-white/10 ${
|
||||
(audioOrd ?? 0) === a.ord ? "text-accent" : "text-white"
|
||||
}`}
|
||||
>
|
||||
{a.label}
|
||||
</button>
|
||||
</Pressable>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{textSubs.length > 0 && (
|
||||
<>
|
||||
<PanelHead>{t("plex.player.subtitles")}</PanelHead>
|
||||
<button
|
||||
<Pressable
|
||||
onClick={() => changeSubtitle(null)}
|
||||
aria-pressed={subOrd == null}
|
||||
className={`block w-full rounded px-2 py-1 text-left hover:bg-white/10 ${
|
||||
className={`block w-full rounded px-2 py-1 hover:bg-white/10 ${
|
||||
subOrd == null ? "text-accent" : "text-white"
|
||||
}`}
|
||||
>
|
||||
{t("plex.player.subOff")}
|
||||
</button>
|
||||
</Pressable>
|
||||
{textSubs.map((s) => (
|
||||
<button
|
||||
<Pressable
|
||||
key={s.ord}
|
||||
onClick={() => changeSubtitle(s.ord)}
|
||||
aria-pressed={subOrd === s.ord}
|
||||
className={`block w-full truncate rounded px-2 py-1 text-left hover:bg-white/10 ${
|
||||
className={`block w-full truncate rounded px-2 py-1 hover:bg-white/10 ${
|
||||
subOrd === s.ord ? "text-accent" : "text-white"
|
||||
}`}
|
||||
>
|
||||
{s.label}
|
||||
</button>
|
||||
</Pressable>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
@@ -1543,14 +1552,17 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
{/* Gear = tuning, split into tabs (Sync / Playback / Subtitle). */}
|
||||
{detail && (
|
||||
<div className="relative">
|
||||
<button
|
||||
<IconButton
|
||||
variant="overlay"
|
||||
size="player"
|
||||
onMedia
|
||||
ref={menuTriggerRef}
|
||||
onClick={() => setMenuOpen((o) => !o)}
|
||||
aria-label={t("plex.player.settings")}
|
||||
className={`p-1 hover:text-accent ${menuOpen ? "text-accent" : ""}`}
|
||||
aria-expanded={menuOpen}
|
||||
label={t("plex.player.settings")}
|
||||
>
|
||||
<Settings className="w-5 h-5" />
|
||||
</button>
|
||||
<Settings />
|
||||
</IconButton>
|
||||
{menuOpen && (
|
||||
<PlexPlayerSettings
|
||||
menuRef={menuRef}
|
||||
|
||||
Reference in New Issue
Block a user