chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,538 @@
|
||||
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<HTMLTableElement, TableProps & { variant?: TableVariant }>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
containerClassName,
|
||||
children,
|
||||
fullWidth,
|
||||
variant = "dimmed",
|
||||
showTopBorder = true,
|
||||
stickyHeader = false,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<TableContext.Provider value={{ variant }}>
|
||||
<div
|
||||
className={cn(
|
||||
"whitespace-nowrap scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control",
|
||||
stickyHeader ? "overflow-visible" : "overflow-x-auto",
|
||||
showTopBorder && "border-t",
|
||||
containerClassName,
|
||||
fullWidth && "w-full"
|
||||
)}
|
||||
>
|
||||
<table ref={ref} className={cn("w-full", className)}>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
</TableContext.Provider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type TableHeaderProps = {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const TableHeader = forwardRef<HTMLTableSectionElement, TableHeaderProps>(
|
||||
({ className, children }, ref) => {
|
||||
const { variant } = useContext(TableContext);
|
||||
return (
|
||||
<thead
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"safari-only sticky top-0 z-10 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-px after:bg-grid-bright supports-[(-webkit-hyphens:none)]:after:content-none",
|
||||
variants[variant].header,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type TableBodyProps = {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
export const TableBody = forwardRef<HTMLTableSectionElement, TableBodyProps>(
|
||||
({ className, children, style }, ref) => {
|
||||
return (
|
||||
<tbody ref={ref} className={cn("relative overflow-y-auto", className)} style={style}>
|
||||
{children}
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type TableRowProps = JSX.IntrinsicElements["tr"] & {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
disabled?: boolean;
|
||||
isSelected?: boolean;
|
||||
};
|
||||
|
||||
export const TableRow = forwardRef<HTMLTableRowElement, TableRowProps>(
|
||||
({ className, disabled, isSelected, children, ...props }, ref) => {
|
||||
const { variant } = useContext(TableContext);
|
||||
return (
|
||||
<tr
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={cn(
|
||||
"group/table-row relative w-full outline-hidden after:absolute after:bottom-0 after:left-3 after:right-0 after:h-px after:bg-grid-dimmed",
|
||||
isSelected && variants[variant].rowSelected,
|
||||
disabled && "opacity-50",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
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<HTMLTableCellElement, TableHeaderCellProps>(
|
||||
(
|
||||
{
|
||||
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 (
|
||||
<th
|
||||
ref={ref}
|
||||
scope="col"
|
||||
className={cn(
|
||||
"align-middle font-medium text-text-bright",
|
||||
variants[variant].headerCell,
|
||||
alignmentClassName,
|
||||
className
|
||||
)}
|
||||
colSpan={colSpan}
|
||||
tabIndex={-1}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{hiddenLabel ? (
|
||||
<span className="sr-only">{children}</span>
|
||||
) : tooltip ? (
|
||||
<div
|
||||
className={cn("flex items-center gap-1", {
|
||||
"justify-center": alignment === "center",
|
||||
"justify-end": alignment === "right",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
<InfoIconTooltip
|
||||
content={tooltip}
|
||||
contentClassName="normal-case tracking-normal"
|
||||
enabled={isHovered}
|
||||
disableHoverableContent={disableTooltipHoverableContent}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type TableCellProps = TableCellBasicProps & {
|
||||
to?: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
hasAction?: boolean;
|
||||
isSticky?: boolean;
|
||||
actionClassName?: string;
|
||||
rowHoverStyle?: string;
|
||||
isSelected?: boolean;
|
||||
isTabbableCell?: boolean;
|
||||
children?: ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
(
|
||||
{
|
||||
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 (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"safari-only text-xs text-text-dimmed has-[[tabindex='0']:focus]:before:absolute has-[[tabindex='0']:focus]:before:-top-px has-[[tabindex='0']:focus]:before:left-0 has-[[tabindex='0']:focus]:before:h-px has-[[tabindex='0']:focus]:before:w-3 has-[[tabindex='0']:focus]:before:bg-grid-dimmed has-[[tabindex='0']:focus]:after:absolute has-[[tabindex='0']:focus]:after:bottom-0 has-[[tabindex='0']:focus]:after:left-0 has-[[tabindex='0']:focus]:after:right-0 has-[[tabindex='0']:focus]:after:h-px has-[[tabindex='0']:focus]:after:bg-grid-dimmed",
|
||||
variants[variant].cellText,
|
||||
variants[variant].cell,
|
||||
to || onClick || hasAction
|
||||
? "cursor-pointer"
|
||||
: cn("cursor-default align-middle", variants[variant].cellSize),
|
||||
!to && !onClick && alignmentClassName,
|
||||
isSticky && "[&:has([data-hidden-buttons])]:w-auto sticky right-0 bg-background-dimmed",
|
||||
isSticky && variants[variant].stickyCell,
|
||||
isSelected && variants[variant].rowSelected,
|
||||
!isSelected &&
|
||||
"group-hover/table-row:before:absolute group-hover/table-row:before:left-0 group-hover/table-row:before:-top-px group-hover/table-row:before:h-px group-hover/table-row:before:w-3 group-hover/table-row:before:bg-background-hover group-hover/table-row:after:absolute group-hover/table-row:after:bottom-0 group-hover/table-row:after:left-0 group-hover/table-row:after:h-px group-hover/table-row:after:w-3 group-hover/table-row:after:bg-background-hover group-focus-visible/table-row:bg-background-bright",
|
||||
className
|
||||
)}
|
||||
colSpan={colSpan}
|
||||
style={style}
|
||||
>
|
||||
{to ? (
|
||||
<Link
|
||||
to={to}
|
||||
className={cn("cursor-pointer focus:outline-hidden", flexClasses, actionClassName)}
|
||||
tabIndex={isTabbableCell ? 0 : -1}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
) : onClick ? (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cn("cursor-pointer focus:outline-hidden", flexClasses, actionClassName)}
|
||||
tabIndex={isTabbableCell ? 0 : -1}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
) : (
|
||||
<>{children}</>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type CopyableTableCellProps = TableCellProps & {
|
||||
value: string;
|
||||
};
|
||||
|
||||
export const CopyableTableCell = forwardRef<HTMLTableCellElement, CopyableTableCellProps>(
|
||||
({ value, children, className, ...props }, ref) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const { copy, copied } = useCopy(value);
|
||||
|
||||
return (
|
||||
<TableCell ref={ref} className={className} {...props}>
|
||||
<div
|
||||
className="relative flex items-center"
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{children}
|
||||
{isHovered && (
|
||||
<span
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
copy();
|
||||
}}
|
||||
className="absolute -right-2 top-1/2 z-10 flex -translate-y-1/2 cursor-pointer"
|
||||
>
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<span
|
||||
className={cn(
|
||||
"flex size-6 items-center justify-center rounded border border-border-bright bg-background-hover",
|
||||
copied
|
||||
? "text-green-500"
|
||||
: "text-text-dimmed hover:border-border-bright hover:bg-background-raised hover:text-text-bright"
|
||||
)}
|
||||
>
|
||||
{copied ? (
|
||||
<ClipboardCheckIcon className="size-3.5" />
|
||||
) : (
|
||||
<ClipboardIcon className="size-3.5" />
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
content={copied ? "Copied!" : "Copy"}
|
||||
disableHoverableContent
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const TableCellChevron = forwardRef<
|
||||
HTMLTableCellElement,
|
||||
{
|
||||
className?: string;
|
||||
to?: string;
|
||||
children?: ReactNode;
|
||||
isSticky?: boolean;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
}
|
||||
>(({ className, to, children, isSticky, onClick }, ref) => {
|
||||
return (
|
||||
<TableCell
|
||||
className={className}
|
||||
isSticky={isSticky}
|
||||
to={to}
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
alignment="right"
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="size-4 text-text-dimmed transition group-hover:text-text-bright" />
|
||||
</TableCell>
|
||||
);
|
||||
});
|
||||
|
||||
export const TableCellMenu = forwardRef<
|
||||
HTMLTableCellElement,
|
||||
TableCellProps & {
|
||||
className?: string;
|
||||
isSticky?: boolean;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement, 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 (
|
||||
<TableCell
|
||||
className={className}
|
||||
isSticky={isSticky}
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
alignment="right"
|
||||
hasAction={true}
|
||||
isSelected={isSelected}
|
||||
>
|
||||
<div className="relative h-full p-1">
|
||||
<div
|
||||
className={cn(
|
||||
"absolute right-0 top-1/2 mr-1 flex -translate-y-1/2 items-center justify-end gap-0.5 rounded-[0.25rem] p-0.5 group-hover/table-row:ring-1",
|
||||
variants[variant].menuButton
|
||||
)}
|
||||
>
|
||||
{/* Hidden buttons that show on hover */}
|
||||
{hiddenButtons && (
|
||||
<div
|
||||
data-hidden-buttons
|
||||
className={cn(
|
||||
"hidden group-hover/table-row:block",
|
||||
popoverContent && "pr-0.5 group-hover/table-row:border-r",
|
||||
variants[variant].menuButtonDivider
|
||||
)}
|
||||
>
|
||||
<div className={cn("flex items-center gap-x-0.5 divide-x divide-grid-bright")}>
|
||||
{hiddenButtons}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Always visible buttons */}
|
||||
{visibleButtons}
|
||||
{/* Always visible popover with ellipsis trigger */}
|
||||
{resolvedContent && (
|
||||
<Popover open={isOpen} onOpenChange={(open) => setIsOpen(open)}>
|
||||
<PopoverVerticalEllipseTrigger
|
||||
isOpen={isOpen}
|
||||
className="duration-0 group-hover/table-row:text-text-bright"
|
||||
/>
|
||||
<PopoverContent
|
||||
className="min-w-40 max-w-80 overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control"
|
||||
align="end"
|
||||
>
|
||||
{typeof popoverContent === "function" ? (
|
||||
resolvedContent
|
||||
) : (
|
||||
<div className="flex flex-col gap-1 p-1">{resolvedContent}</div>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type TableBlankRowProps = {
|
||||
className?: string;
|
||||
colSpan: number;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const TableBlankRow = forwardRef<HTMLTableRowElement, TableBlankRowProps>(
|
||||
({ children, colSpan, className }, ref) => {
|
||||
return (
|
||||
<tr ref={ref}>
|
||||
<td colSpan={colSpan} className={cn("py-6 text-center text-sm", className)}>
|
||||
{children}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user