feat(design): a playlist's one action list (C1) — the row's "…", Delete + undo, the header's "…" (E28 S3e2)
- Playlist rows: the X column became the row's "…" (play, pin, copy title/link, move, order,
remove). Remove runs at once, with no confirm; its toast's Undo puts the row back at its index.
The Delete key removes a keyboard-walked row, and focus moves on to the next.
- POST /api/playlists/{id}/items takes an optional `position`, so an undo is one request that
renumbers the list around the row.
- A new `listDelete` key scope. Only a list with `onDelete` registers it, so the help sheet does not
offer Delete where nothing handles it.
- A playlist has one list, `usePlaylistActions.items`: rename, pin, YouTube open/push/revert,
delete. The rail row's "…" and the page header's "…" both open it. Push and revert moved into
the hook; the header keeps Play all.
- Phone-width playlist table: "#" and Channel are hidden below sm (`Column.phoneHidden`). Their
fixed widths had left the title no width.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { type Ref } from "react";
|
||||
import { MoreHorizontal } from "lucide-react";
|
||||
import { IconButton } from "./IconButton";
|
||||
import { type IconButtonSize, type IconButtonVariant } from "./buttonStyles";
|
||||
@@ -13,6 +14,7 @@ export function MoreButton({
|
||||
variant = "ghost",
|
||||
size = "sm",
|
||||
className,
|
||||
ref,
|
||||
}: {
|
||||
/** The menu's name ("Video actions"): the button's accessible name and tooltip. */
|
||||
label: string;
|
||||
@@ -21,9 +23,12 @@ export function MoreButton({
|
||||
variant?: IconButtonVariant;
|
||||
size?: IconButtonSize;
|
||||
className?: string;
|
||||
/** For a caller that hands the focus back here — an in-place rename the menu opened. */
|
||||
ref?: Ref<HTMLButtonElement>;
|
||||
}) {
|
||||
return (
|
||||
<IconButton
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
label={label}
|
||||
|
||||
@@ -10,14 +10,17 @@ import { useListNav } from "../../lib/listNav";
|
||||
export default function NavGrid({
|
||||
as: Tag = "div",
|
||||
className,
|
||||
onDelete,
|
||||
children,
|
||||
}: {
|
||||
as?: "div" | "tbody";
|
||||
className?: string;
|
||||
/** The Delete key takes the focused item out (lib/listNav): its index among the items. */
|
||||
onDelete?: (index: number) => void;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement & HTMLTableSectionElement>(null);
|
||||
useListNav(ref, { enabled: true });
|
||||
useListNav(ref, { enabled: true, onDelete });
|
||||
return (
|
||||
<Tag ref={ref} className={className}>
|
||||
{children}
|
||||
|
||||
@@ -219,7 +219,7 @@ export function ColumnHead<T>({
|
||||
<th
|
||||
key={col.key}
|
||||
style={col.width ? { width: col.width } : undefined}
|
||||
className={`relative font-medium px-2 py-2 whitespace-nowrap bg-bg ${alignClass(col.align)}`}
|
||||
className={`relative font-medium px-2 py-2 whitespace-nowrap bg-bg ${alignClass(col.align)}${col.phoneHidden ? " hidden sm:table-cell" : ""}`}
|
||||
aria-sort={
|
||||
col.sortable
|
||||
? isSorted
|
||||
|
||||
Reference in New Issue
Block a user