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:
@@ -27,11 +27,9 @@ import {
|
||||
ExternalLink,
|
||||
FolderInput,
|
||||
History,
|
||||
GripVertical,
|
||||
ListPlus,
|
||||
Pencil,
|
||||
Play,
|
||||
RefreshCw,
|
||||
RotateCcw,
|
||||
Trash2,
|
||||
Upload,
|
||||
@@ -68,8 +66,8 @@ import { ColumnHead } from "./ui/tableHeader";
|
||||
import { alignClass } from "./ui/tableAlign";
|
||||
import { filterRows, isActive, type Column, type FilterMap } from "../lib/tableColumns";
|
||||
import { GRIP_KEY, playlistColumns } from "./playlistColumns";
|
||||
import { FocusSafeButton } from "./ui/FocusSafeButton";
|
||||
import { useSortableA11y, useSortableItem } from "./ui/sortableA11y";
|
||||
import { DragHandle } from "./ui/DragHandle";
|
||||
|
||||
// The playlist detail is a HYBRID table (E4 S4): real sortable/filterable column headers over a
|
||||
// body that is still a drag-reorderable @dnd-kit list.
|
||||
@@ -146,21 +144,12 @@ function Row({
|
||||
// The one cell the column defs can't render: the handle needs this row's sortable
|
||||
// attributes/listeners. Disabled (not hidden) under a view, so the affordance stays
|
||||
// put and can explain itself instead of silently doing nothing.
|
||||
<button
|
||||
<DragHandle
|
||||
{...attributes}
|
||||
{...(dragDisabled ? {} : listeners)}
|
||||
// `disabled` would drop it from the tab order AND suppress the tooltip, so the
|
||||
// reason reached nobody. aria-disabled keeps it focusable and the name carries the why.
|
||||
aria-disabled={dragDisabled || undefined}
|
||||
onClick={(e) => dragDisabled && e.preventDefault()}
|
||||
title={dragDisabled ? dragHint : t("playlists.dragHandle")}
|
||||
aria-label={
|
||||
dragDisabled ? dragHint : t("playlists.dragHandleOf", { title: video.title })
|
||||
}
|
||||
className={`text-muted ${dragDisabled ? "opacity-30 cursor-not-allowed" : "hover:text-fg cursor-grab active:cursor-grabbing"}`}
|
||||
>
|
||||
<GripVertical className="w-4 h-4" />
|
||||
</button>
|
||||
unavailable={dragDisabled}
|
||||
label={dragDisabled ? dragHint : t("playlists.dragHandleOf", { title: video.title })}
|
||||
/>
|
||||
) : (
|
||||
col.render(video)
|
||||
)}
|
||||
@@ -896,17 +885,15 @@ export default function Playlists() {
|
||||
<>
|
||||
<h2 className="text-lg font-semibold truncate">{plName(detail)}</h2>
|
||||
{editable && (
|
||||
<button
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setRenameValue(detail.name);
|
||||
setRenaming(true);
|
||||
}}
|
||||
title={t("playlists.rename")}
|
||||
aria-label={t("playlists.rename")}
|
||||
className="shrink-0 text-muted hover:text-fg"
|
||||
label={t("playlists.rename")}
|
||||
>
|
||||
<Pencil className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<Pencil />
|
||||
</IconButton>
|
||||
)}
|
||||
{detail.yt_playlist_id && (
|
||||
<a
|
||||
@@ -932,37 +919,25 @@ export default function Playlists() {
|
||||
{t("playlists.unsynced")}
|
||||
</span>
|
||||
)}
|
||||
<FocusSafeButton
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => shown.length && openPlayer(shown[0]!.id)}
|
||||
disabled={!shown.length}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-semibold px-3 py-1.5 rounded-lg btn-primary disabled:opacity-40 transition"
|
||||
icon={<Play className="fill-current" />}
|
||||
>
|
||||
<Play className="w-3.5 h-3.5 fill-current" /> {t("playlists.playAll")}
|
||||
</FocusSafeButton>
|
||||
{t("playlists.playAll")}
|
||||
</Button>
|
||||
{editable && canWrite && (
|
||||
<FocusSafeButton
|
||||
<IconButton
|
||||
variant={detail.dirty ? "outline-accent" : "outline"}
|
||||
onClick={pushToYoutube}
|
||||
disabled={pushing}
|
||||
title={
|
||||
loading={pushing}
|
||||
label={
|
||||
linked ? t("playlists.pushToYoutube") : t("playlists.exportToYoutube")
|
||||
}
|
||||
aria-label={
|
||||
linked ? t("playlists.pushToYoutube") : t("playlists.exportToYoutube")
|
||||
}
|
||||
className={`inline-flex items-center justify-center p-2 rounded-lg border transition disabled:opacity-40 ${
|
||||
detail.dirty
|
||||
? "border-accent text-accent hover:bg-accent/10"
|
||||
: "border-border text-muted hover:text-fg hover:border-accent"
|
||||
}`}
|
||||
>
|
||||
{pushing ? (
|
||||
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
|
||||
) : linked ? (
|
||||
<Youtube className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<Upload className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</FocusSafeButton>
|
||||
{linked ? <Youtube /> : <Upload />}
|
||||
</IconButton>
|
||||
)}
|
||||
{linked && detail.dirty && (
|
||||
<IconButton
|
||||
@@ -1009,12 +984,13 @@ export default function Playlists() {
|
||||
{t("playlists.groupByChannel")}
|
||||
</label>
|
||||
{viewActive && (
|
||||
<button
|
||||
<Button
|
||||
variant="link-muted"
|
||||
onClick={clearView}
|
||||
className="text-xs text-muted hover:text-accent underline underline-offset-2 transition"
|
||||
className="text-xs underline underline-offset-2"
|
||||
>
|
||||
{t("playlists.clearView")}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
{canSaveOrder && (
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user