import { ChevronRightIcon } from "@heroicons/react/24/solid"; import { Link } from "@remix-run/react"; import { ClipboardCheckIcon, ClipboardIcon } from "lucide-react"; import React, { type ReactNode, createContext, forwardRef, useContext, useState } from "react"; import { useCopy } from "~/hooks/useCopy"; import { cn } from "~/utils/cn"; import { Popover, PopoverContent, PopoverVerticalEllipseTrigger } from "./Popover"; import { InfoIconTooltip, SimpleTooltip } from "./Tooltip"; const variants = { bright: { header: "bg-background-bright", headerCell: "px-3 py-2.5 pb-3 text-sm", cell: "group-hover/table-row:bg-background-hover group-has-[[tabindex='0']:focus]/table-row:bg-background-hover", cellSize: "px-3 py-3", cellText: "text-xs group-hover/table-row:text-text-bright", stickyCell: "bg-background-bright group-hover/table-row:bg-background-hover", menuButton: "bg-background-bright group-hover/table-row:bg-background-hover group-hover/table-row:ring-border-bright/70 group-has-[[tabindex='0']:focus]/table-row:bg-background-hover", menuButtonDivider: "group-hover/table-row:border-border-bright/70", rowSelected: "bg-background-hover group-hover/table-row:bg-background-hover", }, "bright/no-hover": { header: "bg-transparent", headerCell: "px-3 py-2.5 pb-3 text-sm", cell: "group-hover/table-row:bg-transparent", cellSize: "px-3 py-3", cellText: "text-xs", stickyCell: "bg-background-bright", menuButton: "bg-background-bright", menuButtonDivider: "", rowSelected: "bg-background-hover", }, dimmed: { header: "bg-background-dimmed", headerCell: "px-3 py-2.5 pb-3 text-sm", cell: "group-hover/table-row:bg-background-bright group-has-[[tabindex='0']:focus]/table-row:bg-background-bright", cellSize: "px-3 py-3", cellText: "text-xs group-hover/table-row:text-text-bright", stickyCell: "group-hover/table-row:bg-background-bright", menuButton: "bg-background-dimmed group-hover/table-row:bg-background-bright group-hover/table-row:ring-grid-bright group-has-[[tabindex='0']:focus]/table-row:bg-background-bright", menuButtonDivider: "group-hover/table-row:border-grid-bright", rowSelected: "bg-background-hover group-hover/table-row:bg-background-hover", }, "compact/mono": { header: "bg-background-dimmed", headerCell: "px-2 py-1.5 text-sm", cell: "group-hover/table-row:bg-background-bright group-has-[[tabindex='0']:focus]/table-row:bg-background-bright", cellSize: "px-2 py-1.5", cellText: "text-xs font-mono group-hover/table-row:text-text-bright", stickyCell: "group-hover/table-row:bg-background-bright", menuButton: "bg-background-dimmed group-hover/table-row:bg-background-bright group-hover/table-row:ring-grid-bright group-has-[[tabindex='0']:focus]/table-row:bg-background-bright", menuButtonDivider: "group-hover/table-row:border-grid-bright", rowSelected: "bg-background-hover group-hover/table-row:bg-background-hover", }, } as const; export type TableVariant = keyof typeof variants; type TableProps = { containerClassName?: string; className?: string; children: ReactNode; fullWidth?: boolean; showTopBorder?: boolean; stickyHeader?: boolean; }; // Add TableContext const TableContext = createContext<{ variant: TableVariant }>({ variant: "dimmed" }); export const Table = forwardRef( ( { className, containerClassName, children, fullWidth, variant = "dimmed", showTopBorder = true, stickyHeader = false, }, ref ) => { return (
{children}
); } ); type TableHeaderProps = { className?: string; children: ReactNode; }; export const TableHeader = forwardRef( ({ className, children }, ref) => { const { variant } = useContext(TableContext); return ( {children} ); } ); type TableBodyProps = { className?: string; children?: ReactNode; style?: React.CSSProperties; }; export const TableBody = forwardRef( ({ className, children, style }, ref) => { return ( {children} ); } ); type TableRowProps = JSX.IntrinsicElements["tr"] & { className?: string; children: ReactNode; disabled?: boolean; isSelected?: boolean; }; export const TableRow = forwardRef( ({ className, disabled, isSelected, children, ...props }, ref) => { const { variant } = useContext(TableContext); return ( {children} ); } ); type TableCellBasicProps = { className?: string; alignment?: "left" | "center" | "right"; children?: ReactNode; colSpan?: number; }; type TableHeaderCellProps = TableCellBasicProps & { hiddenLabel?: boolean; tooltip?: ReactNode; disableTooltipHoverableContent?: boolean; }; export const TableHeaderCell = forwardRef( ( { className, alignment = "left", children, colSpan, hiddenLabel = false, tooltip, disableTooltipHoverableContent = false, }, ref ) => { const { variant } = useContext(TableContext); let alignmentClassName = "text-left"; switch (alignment) { case "center": alignmentClassName = "text-center"; break; case "right": alignmentClassName = "text-right"; break; } const [isHovered, setIsHovered] = useState(false); return ( setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > {hiddenLabel ? ( {children} ) : tooltip ? (
{children}
) : ( children )} ); } ); type TableCellProps = TableCellBasicProps & { to?: string; onClick?: (event: React.MouseEvent) => void; hasAction?: boolean; isSticky?: boolean; actionClassName?: string; rowHoverStyle?: string; isSelected?: boolean; isTabbableCell?: boolean; children?: ReactNode; style?: React.CSSProperties; }; export const TableCell = forwardRef( ( { className, actionClassName, alignment = "left", children, colSpan, to, onClick, hasAction = false, isSticky = false, isSelected, isTabbableCell = false, style, }, ref ) => { let alignmentClassName = "text-left"; switch (alignment) { case "center": alignmentClassName = "text-center"; break; case "right": alignmentClassName = "text-right"; break; } const { variant } = useContext(TableContext); const flexClasses = cn( "flex w-full whitespace-nowrap items-center text-text-dimmed", variants[variant].cellSize, variants[variant].cellText, alignment === "left" ? "justify-start text-left" : alignment === "center" ? "justify-center text-center" : "justify-end text-right" ); return ( {to ? ( {children} ) : onClick ? ( ) : ( <>{children} )} ); } ); type CopyableTableCellProps = TableCellProps & { value: string; }; export const CopyableTableCell = forwardRef( ({ value, children, className, ...props }, ref) => { const [isHovered, setIsHovered] = useState(false); const { copy, copied } = useCopy(value); return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > {children} {isHovered && ( { e.stopPropagation(); e.preventDefault(); copy(); }} className="absolute -right-2 top-1/2 z-10 flex -translate-y-1/2 cursor-pointer" > {copied ? ( ) : ( )} } content={copied ? "Copied!" : "Copy"} disableHoverableContent /> )}
); } ); export const TableCellChevron = forwardRef< HTMLTableCellElement, { className?: string; to?: string; children?: ReactNode; isSticky?: boolean; onClick?: (event: React.MouseEvent) => void; } >(({ className, to, children, isSticky, onClick }, ref) => { return ( {children} ); }); export const TableCellMenu = forwardRef< HTMLTableCellElement, TableCellProps & { className?: string; isSticky?: boolean; onClick?: (event: React.MouseEvent) => void; visibleButtons?: ReactNode; hiddenButtons?: ReactNode; popoverContent?: ReactNode | ((close: () => void) => ReactNode); children?: ReactNode; isSelected?: boolean; } >( ( { className, isSticky, onClick, visibleButtons, hiddenButtons, popoverContent, children, isSelected, }, ref ) => { const [isOpen, setIsOpen] = useState(false); const { variant } = useContext(TableContext); const resolvedContent = typeof popoverContent === "function" ? popoverContent(() => setIsOpen(false)) : popoverContent; return (
{/* Hidden buttons that show on hover */} {hiddenButtons && (
{hiddenButtons}
)} {/* Always visible buttons */} {visibleButtons} {/* Always visible popover with ellipsis trigger */} {resolvedContent && ( setIsOpen(open)}> {typeof popoverContent === "function" ? ( resolvedContent ) : (
{resolvedContent}
)}
)}
); } ); type TableBlankRowProps = { className?: string; colSpan: number; children?: ReactNode; }; export const TableBlankRow = forwardRef( ({ children, colSpan, className }, ref) => { return ( {children} ); } );