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:
@@ -23,6 +23,8 @@ import { useMe } from "../lib/useMe";
|
||||
import { playlistName } from "../lib/playlistName";
|
||||
import { usePlaylistActions } from "./usePlaylistActions";
|
||||
import PlaylistLimitsHint from "./PlaylistLimitsHint";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
|
||||
type PlSort = {
|
||||
key: "custom" | "name" | "count" | "duration" | "recent";
|
||||
@@ -255,14 +257,14 @@ export default function PlaylistsRail({
|
||||
next && setPlSort({ ...plSort, key: next.key as PlSort["key"], dir: next.dir })
|
||||
}
|
||||
trailing={
|
||||
<button
|
||||
<IconButton
|
||||
variant="outline"
|
||||
onClick={() => setPlSort({ ...plSort, dirtyFirst: !plSort.dirtyFirst })}
|
||||
title={t("playlists.dirtyFirst")}
|
||||
aria-pressed={plSort.dirtyFirst}
|
||||
className="pill shrink-0 rounded-lg border p-1.5"
|
||||
label={t("playlists.dirtyFirst")}
|
||||
pressed={plSort.dirtyFirst}
|
||||
>
|
||||
<Pin className="h-4 w-4" />
|
||||
</button>
|
||||
<Pin />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
@@ -292,13 +294,12 @@ export default function PlaylistsRail({
|
||||
<>
|
||||
{playlists.length === 0 &&
|
||||
(listQuery.isError && !listQuery.data ? (
|
||||
<button
|
||||
type="button"
|
||||
<Pressable
|
||||
onClick={() => listQuery.refetch()}
|
||||
className="text-xs text-danger px-1 py-1 text-left hover:underline"
|
||||
className="text-xs text-danger px-1 py-1 hover:underline"
|
||||
>
|
||||
{t("common.loadError")} — {t("common.retry")}
|
||||
</button>
|
||||
</Pressable>
|
||||
) : listQuery.isLoading ? (
|
||||
<div className="text-xs text-muted px-1 py-1">{t("common.loading")}</div>
|
||||
) : (
|
||||
@@ -342,7 +343,7 @@ export default function PlaylistsRail({
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
<Pressable
|
||||
key={pl.id}
|
||||
ref={(el) => {
|
||||
if (pl.id === selectedId) selectedRef.current = el;
|
||||
@@ -354,10 +355,9 @@ export default function PlaylistsRail({
|
||||
}}
|
||||
onClick={() => setSelectedId(pl.id)}
|
||||
{...railMenu.triggerProps(pl)}
|
||||
className={`${ROW_BASE} text-left ${
|
||||
pl.id === selectedId
|
||||
? "bg-accent/15 border-accent"
|
||||
: "border-transparent hover:bg-card/60"
|
||||
layer={pl.id !== selectedId}
|
||||
className={`${ROW_BASE} ${
|
||||
pl.id === selectedId ? "bg-accent/15 border-accent" : "border-transparent"
|
||||
}`}
|
||||
>
|
||||
<div className="w-8.5 h-8.5 rounded-md bg-card overflow-hidden shrink-0">
|
||||
@@ -380,7 +380,7 @@ export default function PlaylistsRail({
|
||||
` · ${formatRuntime(pl.total_duration_seconds)}`}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</Pressable>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user