table.tsx184 lines · main
1import { ArrowDown, ArrowUp, ChevronsUpDown } from 'lucide-react'
2import type { ComponentProps } from 'react'
3import * as React from 'react'
4
5import { cn } from '../../../lib/utils/cn'
6import { ShadowScrollArea } from '../../ShadowScrollArea'
7
8interface TableProps extends React.HTMLAttributes<HTMLTableElement> {
9 containerProps?: Partial<ComponentProps<typeof ShadowScrollArea>>
10}
11
12const 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)
25Table.displayName = 'Table'
26
27const 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))
33TableHeader.displayName = 'TableHeader'
34
35const 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))
41TableBody.displayName = 'TableBody'
42
43const 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))
49TableFooter.displayName = 'TableFooter'
50
51const 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)
60TableRow.displayName = 'TableRow'
61
62const 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))
77TableHead.displayName = 'TableHead'
78
79interface 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
87function 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}
143TableHeadSort.displayName = 'TableHeadSort'
144
145const 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))
155TableCell.displayName = 'TableCell'
156
157const 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))
172TableCaption.displayName = 'TableCaption'
173
174export {
175 Table,
176 TableBody,
177 TableCaption,
178 TableCell,
179 TableFooter,
180 TableHead,
181 TableHeader,
182 TableHeadSort,
183 TableRow,
184}