feat(design): a video's one action list (C1) — the card's "…", the menu holds the rest (E28 S3e1)
- useContextMenu gains buttonProps (a row's visible "…" opens the same menu under the button; a second press closes it) and anchor (the element the menu came from, for a panel a row opens). - ui/MoreButton; ActionMenu rows can run inGesture (a clipboard write inside the click, Safari). - useVideoActions: one builder for a video's menu (play / restart, channel, watched, save, add to playlist, download, details, copy title / link, hide, reset, pin), composed by the feed and by Home's rails; the playlists and details panels open anchored beside the card (VideoFollowUp). - The card keeps watched / save / add to playlist and the "…": on the picture on hover (the duration steps aside), in the rows' action column; on touch only a 44px "…" beside the title. Download, hide, reset and the hover info card leave the card. - The title is selectable: a click ending a selection neither plays nor follows the link. - DESIGN.md: how C1 is built + the log row.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import type { Video, VideoStatus } from "../lib/api";
|
||||
import { FEED_VIEW_SPEC, type FeedView } from "../lib/feedView";
|
||||
import VideoCard from "./VideoCard";
|
||||
import { FeedVideoMenu } from "./ComfortPinMenu";
|
||||
import { VideoMenu } from "./VideoMenu";
|
||||
import { useContextMenu } from "./ui/useContextMenu";
|
||||
import VirtualGrid from "./ui/VirtualGrid";
|
||||
|
||||
@@ -9,15 +9,15 @@ import VirtualGrid from "./ui/VirtualGrid";
|
||||
// the very first paint and the scrollbar's initial guess. Taken from measuring each mode in the
|
||||
// browser rather than guessed — a bad estimate makes the scrollbar jump as you scroll in.
|
||||
// RE-MEASURE these when a mode's layout changes: they have gone stale twice already, most recently
|
||||
// when the bare row became a single line and halved (82 -> 46). The card figures are the roughest
|
||||
// of the set by nature — a card's height follows its column width (measured ~307 at 3 columns,
|
||||
// ~301 at 4, taller still at 2), so no single constant is right for every window.
|
||||
// when the E28 C1 action row moved onto the card's picture (cards 340 -> 262, small 257 -> 226).
|
||||
// The card figures are the roughest of the set by nature — a card's height follows its column width
|
||||
// (262 measured at 4 columns in a 1268px window), so no single constant is right for every window.
|
||||
const ROW_EST: Record<FeedView, number> = {
|
||||
cards: 340,
|
||||
cardsSmall: 257,
|
||||
rows: 115,
|
||||
cards: 262,
|
||||
cardsSmall: 226,
|
||||
rows: 117,
|
||||
rowsCompact: 46,
|
||||
tiles: 132,
|
||||
tiles: 138,
|
||||
};
|
||||
|
||||
export interface VirtualFeedProps {
|
||||
@@ -47,11 +47,12 @@ export default function VirtualFeed({
|
||||
fetchNextPage,
|
||||
}: VirtualFeedProps) {
|
||||
const { minCol, colMaxWidth, maxCols, thumb } = FEED_VIEW_SPEC[view];
|
||||
// The card menu (its state actions and the comfort-shelf pin, E24 S3) lives HERE rather than in each
|
||||
// of the feed's callers: this is the list, and `useContextMenu`'s contract is one menu per list. Mounting it at this level is also
|
||||
// what gives the channel page the gesture for free — it renders `<Feed channelScoped>`, so it is
|
||||
// the same component all the way down.
|
||||
const pinMenu = useContextMenu<Video>();
|
||||
// The card menu (the video's whole action list, E28 C1) lives HERE rather than in each of the
|
||||
// feed's callers: this is the list, and `useContextMenu`'s contract is one menu per list. Mounting
|
||||
// it at this level is also what gives the channel page the menu for free — it renders
|
||||
// `<Feed channelScoped>`, so it is the same component all the way down.
|
||||
const menu = useContextMenu<Video>();
|
||||
const openId = menu.open ? menu.target?.id : undefined;
|
||||
return (
|
||||
<>
|
||||
<VirtualGrid
|
||||
@@ -76,18 +77,15 @@ export default function VirtualFeed({
|
||||
width={thumb ? 0 : itemWidth}
|
||||
onState={onState}
|
||||
onToggleSave={onToggleSave}
|
||||
onResetState={onResetState}
|
||||
onOpen={onOpen}
|
||||
menuProps={pinMenu.triggerProps}
|
||||
menuProps={menu.triggerProps}
|
||||
menuButtonProps={menu.buttonProps}
|
||||
// A boolean per card, not the open id: only the two cards whose answer changes re-render.
|
||||
menuOpen={openId === v.id}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<FeedVideoMenu
|
||||
menu={pinMenu}
|
||||
onState={onState}
|
||||
onToggleSave={onToggleSave}
|
||||
onResetState={onResetState}
|
||||
/>
|
||||
<VideoMenu menu={menu} handlers={{ onState, onToggleSave, onResetState, onOpen }} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user