fix(ui): stable shelf-card menu triggers, a named Button by type, true layout comments

- Home's rail handed each card a fresh `() => menu.triggerProps(c)` per
  render, defeating memo(VideoCard) on every tile whenever the menu's own
  state changed. `useItemTriggers` (ui/useContextMenu) keeps one trigger per
  item, index-aligned; tested for identity across renders.
- The Button accessible-name union had one hole: `<Button icon={…} />` with
  no children and no size typechecked into a nameless button. A textful
  button now needs children or a name; pinned with @ts-expect-error.
- comfortPin's header said the pin belongs "on every card and row", which
  the code never meant (an episode, a season, a person are not pin kinds).
  It now says what is pinnable — and the show page's Related strip, whose
  shows ARE pinnable, gets the pin menu it was missing (and a rem width).
- gridColumns / feedView / VirtualGrid claimed layouts 1024–2560 are
  "byte-identical" under the column cap; the small cards are capped at 2560
  with the filter rail collapsed. The comments say so.
This commit is contained in:
2026-09-19 07:40:48 +02:00
parent 7ae5c57b6b
commit 29875b7367
11 changed files with 79 additions and 20 deletions
+18 -1
View File
@@ -1,4 +1,10 @@
import { useCallback, useRef, useState, type PointerEvent as ReactPointerEvent } from "react";
import {
useCallback,
useMemo,
useRef,
useState,
type PointerEvent as ReactPointerEvent,
} from "react";
import { matchesShortcut, useKeyScope } from "../../lib/keymap";
import { useAnchoredPopover } from "./anchoredPopoverHooks";
@@ -177,3 +183,14 @@ export function useContextMenu<T>(): ContextMenuController<T> {
return { target, open, close: () => setOpen(false), triggerProps, popover: pop };
}
/** One trigger per item, stable while the items and the menu are — for a list whose card component
* takes `menuProps: () => props` but whose menu acts on a WRAPPER of what the card holds (Home's
* unified shelf card around a `Video`). A fresh `() => menu.triggerProps(c)` per render defeats a
* `memo` card on every tile each time the menu's own state changes. Index-aligned with `items`. */
export function useItemTriggers<T>(
items: readonly T[],
triggerProps: (payload: T) => Record<string, unknown>,
): (() => Record<string, unknown>)[] {
return useMemo(() => items.map((it) => () => triggerProps(it)), [items, triggerProps]);
}