table.tsx184 lines · main
| 1 | import { ArrowDown, ArrowUp, ChevronsUpDown } from 'lucide-react' |
| 2 | import type { ComponentProps } from 'react' |
| 3 | import * as React from 'react' |
| 4 | |
| 5 | import { cn } from '../../../lib/utils/cn' |
| 6 | import { ShadowScrollArea } from '../../ShadowScrollArea' |
| 7 | |
| 8 | interface TableProps extends React.HTMLAttributes<HTMLTableElement> { |
| 9 | containerProps?: Partial<ComponentProps<typeof ShadowScrollArea>> |
| 10 | } |
| 11 | |
| 12 | const Table = React.forwardRef<HTMLTableElement, TableProps>( |
| 13 | ({ className, containerProps, ...props }, ref) => { |
| 14 | return ( |
| 15 | <ShadowScrollArea {...containerProps}> |
| 16 | <table |
| 17 | ref={ref} |
| 18 | className={cn('group/table w-full caption-bottom text-sm', className)} |
| 19 | {...props} |
| 20 | /> |
| 21 | </ShadowScrollArea> |
| 22 | ) |
| 23 | } |
| 24 | ) |
| 25 | Table.displayName = 'Table' |
| 26 | |
| 27 | const TableHeader = React.forwardRef< |
| 28 | HTMLTableSectionElement, |
| 29 | React.HTMLAttributes<HTMLTableSectionElement> |
| 30 | >(({ className, ...props }, ref) => ( |
| 31 | <thead ref={ref} className={cn('[&_tr]:border-b [&>tr]:bg-200', className)} {...props} /> |
| 32 | )) |
| 33 | TableHeader.displayName = 'TableHeader' |
| 34 | |
| 35 | const TableBody = React.forwardRef< |
| 36 | HTMLTableSectionElement, |
| 37 | React.HTMLAttributes<HTMLTableSectionElement> |
| 38 | >(({ className, ...props }, ref) => ( |
| 39 | <tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} /> |
| 40 | )) |
| 41 | TableBody.displayName = 'TableBody' |
| 42 | |
| 43 | const TableFooter = React.forwardRef< |
| 44 | HTMLTableSectionElement, |
| 45 | React.HTMLAttributes<HTMLTableSectionElement> |
| 46 | >(({ className, ...props }, ref) => ( |
| 47 | <tfoot ref={ref} className={cn('border-t font-medium', className)} {...props} /> |
| 48 | )) |
| 49 | TableFooter.displayName = 'TableFooter' |
| 50 | |
| 51 | const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>( |
| 52 | ({ className, ...props }, ref) => ( |
| 53 | <tr |
| 54 | ref={ref} |
| 55 | className={cn('border-b group data-[state=selected]:bg-muted', className)} |
| 56 | {...props} |
| 57 | /> |
| 58 | ) |
| 59 | ) |
| 60 | TableRow.displayName = 'TableRow' |
| 61 | |
| 62 | const TableHead = React.forwardRef< |
| 63 | HTMLTableCellElement, |
| 64 | React.ThHTMLAttributes<HTMLTableCellElement> |
| 65 | >(({ className, ...props }, ref) => ( |
| 66 | <th |
| 67 | ref={ref} |
| 68 | className={cn( |
| 69 | 'h-10 px-4 text-left align-middle heading-meta whitespace-nowrap text-foreground-lighter [&:has([role=checkbox])]:pr-0', |
| 70 | // Transition text color when NoSearchResults or NoFilterResults empty state is shown |
| 71 | 'transition-colors', |
| 72 | className |
| 73 | )} |
| 74 | {...props} |
| 75 | /> |
| 76 | )) |
| 77 | TableHead.displayName = 'TableHead' |
| 78 | |
| 79 | interface TableHeadSortProps<TColumn extends string = string> { |
| 80 | column: TColumn |
| 81 | currentSort: string |
| 82 | onSortChange: (column: TColumn) => void |
| 83 | children: React.ReactNode |
| 84 | className?: string |
| 85 | } |
| 86 | |
| 87 | function TableHeadSort<TColumn extends string = string>({ |
| 88 | column, |
| 89 | currentSort, |
| 90 | onSortChange, |
| 91 | children, |
| 92 | className, |
| 93 | }: TableHeadSortProps<TColumn>) { |
| 94 | const [currentCol, currentOrder] = currentSort.split(':') |
| 95 | const isActive = currentCol === column |
| 96 | const isAsc = isActive && currentOrder === 'asc' |
| 97 | const isDesc = isActive && currentOrder === 'desc' |
| 98 | |
| 99 | const getSortIcon = () => { |
| 100 | const baseIconClass = 'w-3 h-3 absolute inset-0' |
| 101 | |
| 102 | return ( |
| 103 | <> |
| 104 | <ArrowUp |
| 105 | className={cn( |
| 106 | baseIconClass, |
| 107 | 'transition-transform', |
| 108 | isAsc ? 'translate-y-0' : 'translate-y-full' |
| 109 | )} |
| 110 | /> |
| 111 | <ArrowDown |
| 112 | className={cn( |
| 113 | baseIconClass, |
| 114 | 'transition-transform', |
| 115 | isDesc ? 'translate-y-0' : '-translate-y-full' |
| 116 | )} |
| 117 | /> |
| 118 | <ChevronsUpDown |
| 119 | className={cn( |
| 120 | baseIconClass, |
| 121 | 'transition-opacity opacity-80 md:opacity-40', |
| 122 | !isActive ? 'group-hover/table-head-sort:opacity-80' : 'opacity-0!' |
| 123 | )} |
| 124 | /> |
| 125 | </> |
| 126 | ) |
| 127 | } |
| 128 | |
| 129 | return ( |
| 130 | <button |
| 131 | type="button" |
| 132 | className={cn( |
| 133 | 'group/table-head-sort heading-meta whitespace-nowrap flex items-center gap-1 cursor-pointer select-none bg-transparent! border-none p-0 w-full text-left', |
| 134 | className |
| 135 | )} |
| 136 | onClick={() => onSortChange(column)} |
| 137 | > |
| 138 | {children} |
| 139 | <div className="w-3 h-3 relative overflow-hidden">{getSortIcon()}</div> |
| 140 | </button> |
| 141 | ) |
| 142 | } |
| 143 | TableHeadSort.displayName = 'TableHeadSort' |
| 144 | |
| 145 | const TableCell = React.forwardRef< |
| 146 | HTMLTableCellElement, |
| 147 | React.TdHTMLAttributes<HTMLTableCellElement> |
| 148 | >(({ className, ...props }, ref) => ( |
| 149 | <td |
| 150 | ref={ref} |
| 151 | className={cn('transition-colors p-4 align-middle [&:has([role=checkbox])]:pr-0', className)} |
| 152 | {...props} |
| 153 | /> |
| 154 | )) |
| 155 | TableCell.displayName = 'TableCell' |
| 156 | |
| 157 | const TableCaption = React.forwardRef< |
| 158 | HTMLTableCaptionElement, |
| 159 | React.HTMLAttributes<HTMLTableCaptionElement> |
| 160 | >(({ className, ...props }, ref) => ( |
| 161 | <caption |
| 162 | ref={ref} |
| 163 | className={cn( |
| 164 | 'border-t', // TableCaption is aligned by parent Table at caption-bottom |
| 165 | 'p-4 text-sm text-foreground-muted', // Match styling of TableCell |
| 166 | className |
| 167 | )} |
| 168 | // Should only contain inline elements |
| 169 | {...props} |
| 170 | /> |
| 171 | )) |
| 172 | TableCaption.displayName = 'TableCaption' |
| 173 | |
| 174 | export { |
| 175 | Table, |
| 176 | TableBody, |
| 177 | TableCaption, |
| 178 | TableCell, |
| 179 | TableFooter, |
| 180 | TableHead, |
| 181 | TableHeader, |
| 182 | TableHeadSort, |
| 183 | TableRow, |
| 184 | } |