import type { MouseEvent, ReactNode } from "react"; import { cn } from "@/lib/cn"; import type { NativeContextMenuItem } from "@/lib/nativeContextMenu"; import { showNativeContextMenu } from "@/lib/nativeContextMenu"; import type { ArticleListDensity } from "@/components/ViewSelect"; import { Button } from "@/components/ui/Button"; export type ArticleListItem = { id: string; title: string; feed: string; summary: string; publishedAt: string; thumbnailUrl?: string; unread?: boolean; starred?: boolean; }; type ArticleListProps = { items: ArticleListItem[]; density?: ArticleListDensity; showThumbnails?: boolean; selectedItemIds?: string[]; onItemClick?: (event: MouseEvent, item: ArticleListItem) => void; onItemDoubleClick?: (item: ArticleListItem) => void; selectionActions?: ReactNode; onClearSelection?: () => void; getItemContextMenuItems?: (item: ArticleListItem) => NativeContextMenuItem[]; }; export function ArticleList({ items, density = "medium", showThumbnails = true, selectedItemIds = [], onItemClick, onItemDoubleClick, selectionActions, onClearSelection, getItemContextMenuItems, }: ArticleListProps) { const selectedItemIdSet = new Set(selectedItemIds); const hasMultiSelection = selectedItemIds.length > 1; return (
{hasMultiSelection ? (
{selectedItemIds.length} selected {selectionActions ? (
{selectionActions}
) : null}
{onClearSelection ? ( ) : null}
) : null}
{items.map((item) => ( ))}
); } function ArticleListRow({ item, density, showThumbnails, isSelected, onItemClick, onItemDoubleClick, contextMenuItems, }: { item: ArticleListItem; density: ArticleListDensity; showThumbnails: boolean; isSelected: boolean; onItemClick?: (event: MouseEvent, item: ArticleListItem) => void; onItemDoubleClick?: (item: ArticleListItem) => void; contextMenuItems?: NativeContextMenuItem[]; }) { const isUnread = Boolean(item.unread); return (
onItemClick(event, item) : undefined} onDoubleClick={onItemDoubleClick ? () => onItemDoubleClick(item) : undefined} onContextMenu={ contextMenuItems?.length ? (event) => { void showNativeContextMenu(event, contextMenuItems); } : undefined } >
{isUnread ? ( ) : (
{showThumbnails ? ( ) : null}

{item.title}

{item.starred ? ( Saved ) : null}

{item.summary}

{item.feed}
{item.publishedAt}
); } function ArticleThumbnail({ thumbnailUrl, density, title, }: { thumbnailUrl?: string; density: ArticleListDensity; title: string; }) { const className = cn( "overflow-hidden border border-border-subtle bg-surface-muted", thumbnailClassName[density], ); if (thumbnailUrl) { return (
); } return (
); } const rowClassName: Record = { dense: "py-2.5", medium: "py-3.5", loose: "py-4.5", }; const summaryClampClassName: Record = { dense: "line-clamp-1", medium: "line-clamp-2", loose: "line-clamp-3", }; const thumbnailClassName: Record = { dense: "size-10 rounded-md", medium: "size-11 rounded-md", loose: "size-14 rounded-lg", };