@inui/design-system 0.0.0-stage → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/README.md +360 -2
  2. package/dist/components/ui/accordion.d.ts +44 -0
  3. package/dist/components/ui/accordion.d.ts.map +1 -0
  4. package/dist/components/ui/accordion.js +86 -0
  5. package/dist/components/ui/accordion.js.map +1 -0
  6. package/dist/components/ui/ai-avatar.d.ts +46 -0
  7. package/dist/components/ui/ai-avatar.d.ts.map +1 -0
  8. package/dist/components/ui/ai-avatar.js +43 -0
  9. package/dist/components/ui/ai-avatar.js.map +1 -0
  10. package/dist/components/ui/ai-label.d.ts +118 -0
  11. package/dist/components/ui/ai-label.d.ts.map +1 -0
  12. package/dist/components/ui/ai-label.js +184 -0
  13. package/dist/components/ui/ai-label.js.map +1 -0
  14. package/dist/components/ui/ai-sparkle.d.ts +91 -0
  15. package/dist/components/ui/ai-sparkle.d.ts.map +1 -0
  16. package/dist/components/ui/ai-sparkle.js +200 -0
  17. package/dist/components/ui/ai-sparkle.js.map +1 -0
  18. package/dist/components/ui/alert-dialog.d.ts +79 -0
  19. package/dist/components/ui/alert-dialog.d.ts.map +1 -0
  20. package/dist/components/ui/alert-dialog.js +147 -0
  21. package/dist/components/ui/alert-dialog.js.map +1 -0
  22. package/dist/components/ui/alert.d.ts +71 -0
  23. package/dist/components/ui/alert.d.ts.map +1 -0
  24. package/dist/components/ui/alert.js +108 -0
  25. package/dist/components/ui/alert.js.map +1 -0
  26. package/dist/components/ui/aspect-ratio.d.ts +30 -0
  27. package/dist/components/ui/aspect-ratio.d.ts.map +1 -0
  28. package/dist/components/ui/aspect-ratio.js +40 -0
  29. package/dist/components/ui/aspect-ratio.js.map +1 -0
  30. package/dist/components/ui/attachment.d.ts +74 -0
  31. package/dist/components/ui/attachment.d.ts.map +1 -0
  32. package/dist/components/ui/attachment.js +142 -0
  33. package/dist/components/ui/attachment.js.map +1 -0
  34. package/dist/components/ui/avatar.d.ts +82 -0
  35. package/dist/components/ui/avatar.d.ts.map +1 -0
  36. package/dist/components/ui/avatar.js +123 -0
  37. package/dist/components/ui/avatar.js.map +1 -0
  38. package/dist/components/ui/badge.d.ts +130 -0
  39. package/dist/components/ui/badge.d.ts.map +1 -0
  40. package/dist/components/ui/badge.js +226 -0
  41. package/dist/components/ui/badge.js.map +1 -0
  42. package/dist/components/ui/breadcrumb.d.ts +43 -0
  43. package/dist/components/ui/breadcrumb.d.ts.map +1 -0
  44. package/dist/components/ui/breadcrumb.js +98 -0
  45. package/dist/components/ui/breadcrumb.js.map +1 -0
  46. package/dist/components/ui/browser-tabs.d.ts +218 -0
  47. package/dist/components/ui/browser-tabs.d.ts.map +1 -0
  48. package/dist/components/ui/browser-tabs.js +528 -0
  49. package/dist/components/ui/browser-tabs.js.map +1 -0
  50. package/dist/components/ui/bubble.d.ts +80 -0
  51. package/dist/components/ui/bubble.d.ts.map +1 -0
  52. package/dist/components/ui/bubble.js +235 -0
  53. package/dist/components/ui/bubble.js.map +1 -0
  54. package/dist/components/ui/button-group.d.ts +60 -0
  55. package/dist/components/ui/button-group.d.ts.map +1 -0
  56. package/dist/components/ui/button-group.js +89 -0
  57. package/dist/components/ui/button-group.js.map +1 -0
  58. package/dist/components/ui/button.d.ts +157 -0
  59. package/dist/components/ui/button.d.ts.map +1 -0
  60. package/dist/components/ui/button.js +144 -0
  61. package/dist/components/ui/button.js.map +1 -0
  62. package/dist/components/ui/calendar.d.ts +57 -0
  63. package/dist/components/ui/calendar.d.ts.map +1 -0
  64. package/dist/components/ui/calendar.js +163 -0
  65. package/dist/components/ui/calendar.js.map +1 -0
  66. package/dist/components/ui/card.d.ts +96 -0
  67. package/dist/components/ui/card.d.ts.map +1 -0
  68. package/dist/components/ui/card.js +131 -0
  69. package/dist/components/ui/card.js.map +1 -0
  70. package/dist/components/ui/carousel.d.ts +86 -0
  71. package/dist/components/ui/carousel.d.ts.map +1 -0
  72. package/dist/components/ui/carousel.js +207 -0
  73. package/dist/components/ui/carousel.js.map +1 -0
  74. package/dist/components/ui/checkbox.d.ts +54 -0
  75. package/dist/components/ui/checkbox.d.ts.map +1 -0
  76. package/dist/components/ui/checkbox.js +78 -0
  77. package/dist/components/ui/checkbox.js.map +1 -0
  78. package/dist/components/ui/collapsible.d.ts +45 -0
  79. package/dist/components/ui/collapsible.d.ts.map +1 -0
  80. package/dist/components/ui/collapsible.js +65 -0
  81. package/dist/components/ui/collapsible.js.map +1 -0
  82. package/dist/components/ui/combobox.d.ts +111 -0
  83. package/dist/components/ui/combobox.d.ts.map +1 -0
  84. package/dist/components/ui/combobox.js +264 -0
  85. package/dist/components/ui/combobox.js.map +1 -0
  86. package/dist/components/ui/command.d.ts +71 -0
  87. package/dist/components/ui/command.d.ts.map +1 -0
  88. package/dist/components/ui/command.js +133 -0
  89. package/dist/components/ui/command.js.map +1 -0
  90. package/dist/components/ui/context-menu.d.ts +70 -0
  91. package/dist/components/ui/context-menu.d.ts.map +1 -0
  92. package/dist/components/ui/context-menu.js +163 -0
  93. package/dist/components/ui/context-menu.js.map +1 -0
  94. package/dist/components/ui/count-badge.d.ts +40 -0
  95. package/dist/components/ui/count-badge.d.ts.map +1 -0
  96. package/dist/components/ui/count-badge.js +43 -0
  97. package/dist/components/ui/count-badge.js.map +1 -0
  98. package/dist/components/ui/data-table.d.ts +1026 -0
  99. package/dist/components/ui/data-table.d.ts.map +1 -0
  100. package/dist/components/ui/data-table.js +3423 -0
  101. package/dist/components/ui/data-table.js.map +1 -0
  102. package/dist/components/ui/dialog.d.ts +73 -0
  103. package/dist/components/ui/dialog.d.ts.map +1 -0
  104. package/dist/components/ui/dialog.js +145 -0
  105. package/dist/components/ui/dialog.js.map +1 -0
  106. package/dist/components/ui/direction.d.ts +35 -0
  107. package/dist/components/ui/direction.d.ts.map +1 -0
  108. package/dist/components/ui/direction.js +35 -0
  109. package/dist/components/ui/direction.js.map +1 -0
  110. package/dist/components/ui/drawer.d.ts +53 -0
  111. package/dist/components/ui/drawer.d.ts.map +1 -0
  112. package/dist/components/ui/drawer.js +115 -0
  113. package/dist/components/ui/drawer.js.map +1 -0
  114. package/dist/components/ui/dropdown-menu.d.ts +75 -0
  115. package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
  116. package/dist/components/ui/dropdown-menu.js +173 -0
  117. package/dist/components/ui/dropdown-menu.js.map +1 -0
  118. package/dist/components/ui/dropzone.d.ts +141 -0
  119. package/dist/components/ui/dropzone.d.ts.map +1 -0
  120. package/dist/components/ui/dropzone.js +257 -0
  121. package/dist/components/ui/dropzone.js.map +1 -0
  122. package/dist/components/ui/empty.d.ts +94 -0
  123. package/dist/components/ui/empty.d.ts.map +1 -0
  124. package/dist/components/ui/empty.js +149 -0
  125. package/dist/components/ui/empty.js.map +1 -0
  126. package/dist/components/ui/field.d.ts +122 -0
  127. package/dist/components/ui/field.d.ts.map +1 -0
  128. package/dist/components/ui/field.js +254 -0
  129. package/dist/components/ui/field.js.map +1 -0
  130. package/dist/components/ui/hover-card.d.ts +51 -0
  131. package/dist/components/ui/hover-card.d.ts.map +1 -0
  132. package/dist/components/ui/hover-card.js +101 -0
  133. package/dist/components/ui/hover-card.js.map +1 -0
  134. package/dist/components/ui/input-group.d.ts +62 -0
  135. package/dist/components/ui/input-group.d.ts.map +1 -0
  136. package/dist/components/ui/input-group.js +115 -0
  137. package/dist/components/ui/input-group.js.map +1 -0
  138. package/dist/components/ui/input-otp.d.ts +52 -0
  139. package/dist/components/ui/input-otp.d.ts.map +1 -0
  140. package/dist/components/ui/input-otp.js +89 -0
  141. package/dist/components/ui/input-otp.js.map +1 -0
  142. package/dist/components/ui/input.d.ts +47 -0
  143. package/dist/components/ui/input.d.ts.map +1 -0
  144. package/dist/components/ui/input.js +47 -0
  145. package/dist/components/ui/input.js.map +1 -0
  146. package/dist/components/ui/item.d.ts +74 -0
  147. package/dist/components/ui/item.d.ts.map +1 -0
  148. package/dist/components/ui/item.js +176 -0
  149. package/dist/components/ui/item.js.map +1 -0
  150. package/dist/components/ui/kbd.d.ts +41 -0
  151. package/dist/components/ui/kbd.d.ts.map +1 -0
  152. package/dist/components/ui/kbd.js +58 -0
  153. package/dist/components/ui/kbd.js.map +1 -0
  154. package/dist/components/ui/key-value-pair.d.ts +56 -0
  155. package/dist/components/ui/key-value-pair.d.ts.map +1 -0
  156. package/dist/components/ui/key-value-pair.js +110 -0
  157. package/dist/components/ui/key-value-pair.js.map +1 -0
  158. package/dist/components/ui/label.d.ts +83 -0
  159. package/dist/components/ui/label.d.ts.map +1 -0
  160. package/dist/components/ui/label.js +85 -0
  161. package/dist/components/ui/label.js.map +1 -0
  162. package/dist/components/ui/marker.d.ts +54 -0
  163. package/dist/components/ui/marker.d.ts.map +1 -0
  164. package/dist/components/ui/marker.js +79 -0
  165. package/dist/components/ui/marker.js.map +1 -0
  166. package/dist/components/ui/menubar.d.ts +72 -0
  167. package/dist/components/ui/menubar.d.ts.map +1 -0
  168. package/dist/components/ui/menubar.js +174 -0
  169. package/dist/components/ui/menubar.js.map +1 -0
  170. package/dist/components/ui/message-scroller.d.ts +53 -0
  171. package/dist/components/ui/message-scroller.d.ts.map +1 -0
  172. package/dist/components/ui/message-scroller.js +95 -0
  173. package/dist/components/ui/message-scroller.js.map +1 -0
  174. package/dist/components/ui/message.d.ts +63 -0
  175. package/dist/components/ui/message.d.ts.map +1 -0
  176. package/dist/components/ui/message.js +92 -0
  177. package/dist/components/ui/message.js.map +1 -0
  178. package/dist/components/ui/meta-list.d.ts +55 -0
  179. package/dist/components/ui/meta-list.d.ts.map +1 -0
  180. package/dist/components/ui/meta-list.js +75 -0
  181. package/dist/components/ui/meta-list.js.map +1 -0
  182. package/dist/components/ui/native-select.d.ts +51 -0
  183. package/dist/components/ui/native-select.d.ts.map +1 -0
  184. package/dist/components/ui/native-select.js +75 -0
  185. package/dist/components/ui/native-select.js.map +1 -0
  186. package/dist/components/ui/navigation-menu.d.ts +56 -0
  187. package/dist/components/ui/navigation-menu.d.ts.map +1 -0
  188. package/dist/components/ui/navigation-menu.js +116 -0
  189. package/dist/components/ui/navigation-menu.js.map +1 -0
  190. package/dist/components/ui/page-header.d.ts +223 -0
  191. package/dist/components/ui/page-header.d.ts.map +1 -0
  192. package/dist/components/ui/page-header.js +233 -0
  193. package/dist/components/ui/page-header.js.map +1 -0
  194. package/dist/components/ui/pagination.d.ts +68 -0
  195. package/dist/components/ui/pagination.d.ts.map +1 -0
  196. package/dist/components/ui/pagination.js +145 -0
  197. package/dist/components/ui/pagination.js.map +1 -0
  198. package/dist/components/ui/popover.d.ts +61 -0
  199. package/dist/components/ui/popover.d.ts.map +1 -0
  200. package/dist/components/ui/popover.js +144 -0
  201. package/dist/components/ui/popover.js.map +1 -0
  202. package/dist/components/ui/progress.d.ts +35 -0
  203. package/dist/components/ui/progress.d.ts.map +1 -0
  204. package/dist/components/ui/progress.js +46 -0
  205. package/dist/components/ui/progress.js.map +1 -0
  206. package/dist/components/ui/questionnaire.d.ts +92 -0
  207. package/dist/components/ui/questionnaire.d.ts.map +1 -0
  208. package/dist/components/ui/questionnaire.js +277 -0
  209. package/dist/components/ui/questionnaire.js.map +1 -0
  210. package/dist/components/ui/radio-group.d.ts +50 -0
  211. package/dist/components/ui/radio-group.d.ts.map +1 -0
  212. package/dist/components/ui/radio-group.js +65 -0
  213. package/dist/components/ui/radio-group.js.map +1 -0
  214. package/dist/components/ui/resizable.d.ts +44 -0
  215. package/dist/components/ui/resizable.d.ts.map +1 -0
  216. package/dist/components/ui/resizable.js +73 -0
  217. package/dist/components/ui/resizable.js.map +1 -0
  218. package/dist/components/ui/scroll-area.d.ts +55 -0
  219. package/dist/components/ui/scroll-area.d.ts.map +1 -0
  220. package/dist/components/ui/scroll-area.js +72 -0
  221. package/dist/components/ui/scroll-area.js.map +1 -0
  222. package/dist/components/ui/select.d.ts +274 -0
  223. package/dist/components/ui/select.d.ts.map +1 -0
  224. package/dist/components/ui/select.js +609 -0
  225. package/dist/components/ui/select.js.map +1 -0
  226. package/dist/components/ui/separator.d.ts +31 -0
  227. package/dist/components/ui/separator.d.ts.map +1 -0
  228. package/dist/components/ui/separator.js +38 -0
  229. package/dist/components/ui/separator.js.map +1 -0
  230. package/dist/components/ui/sheet.d.ts +59 -0
  231. package/dist/components/ui/sheet.d.ts.map +1 -0
  232. package/dist/components/ui/sheet.js +128 -0
  233. package/dist/components/ui/sheet.js.map +1 -0
  234. package/dist/components/ui/sidebar.d.ts +166 -0
  235. package/dist/components/ui/sidebar.d.ts.map +1 -0
  236. package/dist/components/ui/sidebar.js +512 -0
  237. package/dist/components/ui/sidebar.js.map +1 -0
  238. package/dist/components/ui/skeleton.d.ts +35 -0
  239. package/dist/components/ui/skeleton.d.ts.map +1 -0
  240. package/dist/components/ui/skeleton.js +46 -0
  241. package/dist/components/ui/skeleton.js.map +1 -0
  242. package/dist/components/ui/slider.d.ts +39 -0
  243. package/dist/components/ui/slider.d.ts.map +1 -0
  244. package/dist/components/ui/slider.js +75 -0
  245. package/dist/components/ui/slider.js.map +1 -0
  246. package/dist/components/ui/sonner.d.ts +56 -0
  247. package/dist/components/ui/sonner.d.ts.map +1 -0
  248. package/dist/components/ui/sonner.js +96 -0
  249. package/dist/components/ui/sonner.js.map +1 -0
  250. package/dist/components/ui/spinner.d.ts +37 -0
  251. package/dist/components/ui/spinner.d.ts.map +1 -0
  252. package/dist/components/ui/spinner.js +51 -0
  253. package/dist/components/ui/spinner.js.map +1 -0
  254. package/dist/components/ui/stepper.d.ts +84 -0
  255. package/dist/components/ui/stepper.d.ts.map +1 -0
  256. package/dist/components/ui/stepper.js +145 -0
  257. package/dist/components/ui/stepper.js.map +1 -0
  258. package/dist/components/ui/switch.d.ts +36 -0
  259. package/dist/components/ui/switch.d.ts.map +1 -0
  260. package/dist/components/ui/switch.js +44 -0
  261. package/dist/components/ui/switch.js.map +1 -0
  262. package/dist/components/ui/table.d.ts +80 -0
  263. package/dist/components/ui/table.d.ts.map +1 -0
  264. package/dist/components/ui/table.js +130 -0
  265. package/dist/components/ui/table.js.map +1 -0
  266. package/dist/components/ui/tabs.d.ts +87 -0
  267. package/dist/components/ui/tabs.d.ts.map +1 -0
  268. package/dist/components/ui/tabs.js +128 -0
  269. package/dist/components/ui/tabs.js.map +1 -0
  270. package/dist/components/ui/textarea.d.ts +35 -0
  271. package/dist/components/ui/textarea.d.ts.map +1 -0
  272. package/dist/components/ui/textarea.js +38 -0
  273. package/dist/components/ui/textarea.js.map +1 -0
  274. package/dist/components/ui/time-picker.d.ts +58 -0
  275. package/dist/components/ui/time-picker.d.ts.map +1 -0
  276. package/dist/components/ui/time-picker.js +188 -0
  277. package/dist/components/ui/time-picker.js.map +1 -0
  278. package/dist/components/ui/toggle-group.d.ts +52 -0
  279. package/dist/components/ui/toggle-group.d.ts.map +1 -0
  280. package/dist/components/ui/toggle-group.js +94 -0
  281. package/dist/components/ui/toggle-group.js.map +1 -0
  282. package/dist/components/ui/toggle.d.ts +50 -0
  283. package/dist/components/ui/toggle.d.ts.map +1 -0
  284. package/dist/components/ui/toggle.js +70 -0
  285. package/dist/components/ui/toggle.js.map +1 -0
  286. package/dist/components/ui/tooltip.d.ts +51 -0
  287. package/dist/components/ui/tooltip.d.ts.map +1 -0
  288. package/dist/components/ui/tooltip.js +78 -0
  289. package/dist/components/ui/tooltip.js.map +1 -0
  290. package/dist/hooks/use-dark-mode.d.ts +2 -0
  291. package/dist/hooks/use-dark-mode.d.ts.map +1 -0
  292. package/dist/hooks/use-dark-mode.js +36 -0
  293. package/dist/hooks/use-dark-mode.js.map +1 -0
  294. package/dist/hooks/use-mobile.d.ts +2 -0
  295. package/dist/hooks/use-mobile.d.ts.map +1 -0
  296. package/dist/hooks/use-mobile.js +20 -0
  297. package/dist/hooks/use-mobile.js.map +1 -0
  298. package/dist/index.d.ts +88 -0
  299. package/dist/index.d.ts.map +1 -0
  300. package/dist/index.js +77 -0
  301. package/dist/lib/scope.d.ts +64 -0
  302. package/dist/lib/scope.d.ts.map +1 -0
  303. package/dist/lib/scope.js +74 -0
  304. package/dist/lib/scope.js.map +1 -0
  305. package/dist/lib/utils.d.ts +3 -0
  306. package/dist/lib/utils.d.ts.map +1 -0
  307. package/dist/lib/utils.js +11 -0
  308. package/dist/lib/utils.js.map +1 -0
  309. package/dist/styles.css +2 -0
  310. package/dist/theme-tokens.css +537 -0
  311. package/dist/theme.css +417 -0
  312. package/package.json +103 -3
@@ -0,0 +1,3423 @@
1
+ "use client";
2
+ import { cn } from "../../lib/utils.js";
3
+ import { AISparkle } from "./ai-sparkle.js";
4
+ import { Button } from "./button.js";
5
+ import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger } from "./popover.js";
6
+ import { AILabel, AILabelContent, AILabelTrigger } from "./ai-label.js";
7
+ import { Input } from "./input.js";
8
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.js";
9
+ import { Label } from "./label.js";
10
+ import { Checkbox } from "./checkbox.js";
11
+ import { InputGroup, InputGroupAddon, InputGroupInput } from "./input-group.js";
12
+ import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "./table.js";
13
+ import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "./pagination.js";
14
+ import { Skeleton } from "./skeleton.js";
15
+ import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "./empty.js";
16
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./dropdown-menu.js";
17
+ import { Select, SelectContent, SelectItem, SelectMultiple, SelectMultipleContent, SelectMultipleFooter, SelectMultipleGroup, SelectMultipleItem, SelectMultipleLabel, SelectMultipleTrigger, SelectTrigger, SelectValue } from "./select.js";
18
+ import * as React from "react";
19
+ import { ArrowDownIcon, ArrowLeftIcon, ArrowLeftToLineIcon, ArrowRightIcon, ArrowRightToLineIcon, ArrowUpIcon, CheckCheckIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsUpDownIcon, CircleAlertIcon, Columns3Icon, EllipsisIcon, EyeOffIcon, ListFilterIcon, PinIcon, PinOffIcon, RotateCcwIcon, SearchIcon, SearchXIcon, UnfoldHorizontalIcon, XIcon } from "lucide-react";
20
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
21
+ import { flexRender, getCoreRowModel, getExpandedRowModel, getFacetedMinMaxValues, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
22
+ //#region src/components/ui/data-table.tsx
23
+ /**
24
+ * DataTable — Table plus sorting, filtering, search, row selection,
25
+ * row actions, expandable rows, AI affordances, column management, and
26
+ * pagination.
27
+ *
28
+ * A composite over the existing `Table` primitive (unmodified) — state is
29
+ * owned by `@tanstack/react-table`, a headless logic-only library with no
30
+ * rendered markup of its own (same "headless engine, MMDS renders every
31
+ * pixel" shape as RichTextEditor's use of draft-js), not hand-rolled here.
32
+ * Every visible piece (checkboxes, buttons, the search field, filter
33
+ * popovers, the column menu, pagination, loading/empty states) is an
34
+ * existing MMDS primitive — Checkbox, Button, InputGroup, Input, Select,
35
+ * Popover, DropdownMenu, Skeleton, Empty, AISparkle.
36
+ */
37
+ var SELECTION_CHECKBOX_CLASS = "size-6 rounded-[6px] border-0 p-1 bg-clip-content [outline-style:solid]! outline-1 -outline-offset-4 outline-input data-[state=checked]:[outline-style:none]! data-[state=indeterminate]:[outline-style:none]!";
38
+ var SELECTION_COLUMN_ID = "__select__";
39
+ var EXPAND_COLUMN_ID = "__expand__";
40
+ var AI_COLUMN_ID = "__ai__";
41
+ var INJECTED_COLUMN_IDS = /* @__PURE__ */ new Set([
42
+ SELECTION_COLUMN_ID,
43
+ EXPAND_COLUMN_ID,
44
+ AI_COLUMN_ID
45
+ ]);
46
+ function AIMarkerTrigger({ size, ariaLabel }) {
47
+ return /* @__PURE__ */ jsx(AILabelTrigger, {
48
+ size: size === "default" ? "default" : "sm",
49
+ "aria-label": ariaLabel
50
+ });
51
+ }
52
+ function getColumnId(column) {
53
+ if (column.id) return column.id;
54
+ if ("accessorKey" in column && typeof column.accessorKey === "string") return column.accessorKey;
55
+ }
56
+ /** The dev-configured default pinning — every column's own `meta.pin`, collected into TanStack's `{ left, right }` shape. Seeds `columnPinning`'s initial state and is what "Reset to defaults" restores it to. */
57
+ function derivePinningFromMeta(columns) {
58
+ const left = [];
59
+ const right = [];
60
+ columns.forEach((col) => {
61
+ const id = getColumnId(col);
62
+ if (!id) return;
63
+ if (col.meta?.pin === "left") left.push(id);
64
+ if (col.meta?.pin === "right") right.push(id);
65
+ });
66
+ return {
67
+ left,
68
+ right
69
+ };
70
+ }
71
+ function idListsEqual(a, b) {
72
+ const left = a ?? [];
73
+ const right = b ?? [];
74
+ return left.length === right.length && left.every((value, index) => value === right[index]);
75
+ }
76
+ var measureCanvasContext;
77
+ function getMeasureContext() {
78
+ if (measureCanvasContext !== void 0) return measureCanvasContext;
79
+ measureCanvasContext = typeof document === "undefined" ? null : document.createElement("canvas").getContext("2d") ?? null;
80
+ return measureCanvasContext;
81
+ }
82
+ var MIN_SIZE_FALLBACK_CHAR_WIDTH = 8;
83
+ function measureTextWidth(text, font) {
84
+ const ctx = getMeasureContext();
85
+ if (!ctx) return text.length * MIN_SIZE_FALLBACK_CHAR_WIDTH;
86
+ ctx.font = font;
87
+ return ctx.measureText(text).width;
88
+ }
89
+ var HEADER_ICON_CONTROL_ALLOWANCE = 26;
90
+ /**
91
+ * The narrowest a column should ever resize to — just enough for its
92
+ * header's first character, an ellipsis, the sort icon (if sortable),
93
+ * and the filter icon/headerActions cluster (if this column has them —
94
+ * both occupy real layout space in the header row regardless of their
95
+ * own hover-reveal opacity), so a squeezed column still reads as "this
96
+ * column, truncated" rather than disappearing into an unreadable sliver
97
+ * or squeezing its own label to nothing to make room for its sibling
98
+ * controls. Measured per column via an offscreen canvas (the "W" in
99
+ * "Website" needs more room than the "I" in "Invoice") rather than one
100
+ * flat constant for every column — that's what makes this "dynamic."
101
+ * Only works out the real first character when `header` is a plain
102
+ * string; a render-function header falls back to
103
+ * `MIN_SIZE_FALLBACK_CHAR_WIDTH` since there's no text to introspect
104
+ * without actually mounting it.
105
+ */
106
+ function computeColumnMinSize(col, tableSize) {
107
+ const font = tableSize === "xs" ? "500 12px system-ui, sans-serif" : "500 14px system-ui, sans-serif";
108
+ const label = (col.meta?.label ?? (typeof col.header === "string" ? col.header : "")).trim();
109
+ const charWidth = label ? measureTextWidth(label.charAt(0), font) : MIN_SIZE_FALLBACK_CHAR_WIDTH;
110
+ const ellipsisWidth = measureTextWidth("…", font);
111
+ const canSort = col.enableSorting !== false;
112
+ const sortIconAllowance = canSort ? 18 : 0;
113
+ const canFilter = col.enableColumnFilter !== false;
114
+ const hasHeaderActions = Boolean(col.meta?.headerActions);
115
+ const siblingControlsAllowance = (canFilter ? HEADER_ICON_CONTROL_ALLOWANCE : 0) + (hasHeaderActions ? HEADER_ICON_CONTROL_ALLOWANCE : 0);
116
+ const cellPadding = 16;
117
+ const buttonPadding = canSort ? 8 : 0;
118
+ return Math.ceil(charWidth + ellipsisWidth + sortIconAllowance + siblingControlsAllowance + cellPadding + buttonPadding);
119
+ }
120
+ /** A column absent from either map is implicitly visible (`true`) — comparing raw keys would treat "never touched" and "explicitly shown again" as different when they're not. */
121
+ function visibilityEqual(a, b) {
122
+ return [.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])].every((key) => (a[key] ?? true) === (b[key] ?? true));
123
+ }
124
+ /** A column absent from either map has no explicit width set — same "missing means untouched" comparison as `visibilityEqual`, just with no shared implicit value to fall back to (there's no one universal default width), so a column present in only one side always counts as a difference. */
125
+ function sizingEqual(a, b) {
126
+ return [.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])].every((key) => a[key] === b[key]);
127
+ }
128
+ var selectFilterFn = (row, columnId, filterValue) => {
129
+ if (!Array.isArray(filterValue) || filterValue.length === 0) return true;
130
+ return filterValue.includes(String(row.getValue(columnId)));
131
+ };
132
+ var singleSelectFilterFn = (row, columnId, filterValue) => {
133
+ if (!filterValue) return true;
134
+ return String(row.getValue(columnId)) === filterValue;
135
+ };
136
+ var DEFAULT_RANGE_BUCKET_SIZE = 3;
137
+ var ALL_VALUE = "__all__";
138
+ /** The last run of digits in a string — "INV003" -> 3. Powers `filterVariant: "range"`. */
139
+ function extractTrailingNumber(value) {
140
+ const match = String(value ?? "").match(/(\d+)(?!.*\d)/);
141
+ return match ? Number(match[0]) : null;
142
+ }
143
+ var rangeBucketCache = /* @__PURE__ */ new WeakMap();
144
+ /** Buckets a column's sorted unique values (by trailing number) into fixed-size groups, each rendered as a "min-max" (or just "min" for a single-value bucket) label. */
145
+ function computeRangeBuckets(column, bucketSize) {
146
+ const uniqueValues = column.getFacetedUniqueValues();
147
+ const cached = rangeBucketCache.get(uniqueValues)?.get(bucketSize);
148
+ if (cached) return cached;
149
+ const numbers = Array.from(uniqueValues.keys()).map(extractTrailingNumber).filter((value) => value !== null).sort((a, b) => a - b);
150
+ const buckets = [];
151
+ for (let index = 0; index < numbers.length; index += bucketSize) {
152
+ const slice = numbers.slice(index, index + bucketSize);
153
+ const min = slice[0];
154
+ const max = slice[slice.length - 1];
155
+ buckets.push(min === max ? `${min}` : `${min}-${max}`);
156
+ }
157
+ const byBucketSize = rangeBucketCache.get(uniqueValues) ?? /* @__PURE__ */ new Map();
158
+ byBucketSize.set(bucketSize, buckets);
159
+ rangeBucketCache.set(uniqueValues, byBucketSize);
160
+ return buckets;
161
+ }
162
+ /** A "min-max" (or single "n") bucket label back into its numeric bounds. */
163
+ function parseRangeBucket(label) {
164
+ const [min, max] = label.split("-").map(Number);
165
+ return max === void 0 ? [min, min] : [min, max];
166
+ }
167
+ var rangeFilterFn = (row, columnId, filterValue) => {
168
+ if (!Array.isArray(filterValue) || filterValue.length === 0) return true;
169
+ const value = extractTrailingNumber(row.getValue(columnId));
170
+ if (value === null) return false;
171
+ return filterValue.some((bucket) => {
172
+ const [min, max] = parseRangeBucket(bucket);
173
+ return value >= min && value <= max;
174
+ });
175
+ };
176
+ /** Matches `query` only where a word starts — at the beginning of the text or right after anything that isn't a letter or digit. The one pattern behind both the `searchMatch="startsWith"` filter and its highlight, so they can never disagree. Capture group 1 is the (possibly empty) boundary character, so the highlight can skip it; a consuming prefix instead of a lookbehind keeps this working on Safari before 16.4. */
177
+ function wordStartPattern(query, flags) {
178
+ return new RegExp(`(^|[^\\p{L}\\p{N}])${query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, flags);
179
+ }
180
+ var startsWithWordFilterFn = (row, columnId, filterValue) => {
181
+ const query = String(filterValue ?? "").trim();
182
+ if (!query) return true;
183
+ return wordStartPattern(query, "iu").test(String(row.getValue(columnId) ?? ""));
184
+ };
185
+ /**
186
+ * Wraps the substring of `text` matching `query` in a `<mark>`. Exported so a
187
+ * column with a custom `cell` can highlight matches itself — read the live
188
+ * query off `info.table.getState().globalFilter` inside that `cell`. Pass
189
+ * `startsWith` when the table's `searchMatch` is `"startsWith"`, so only
190
+ * word-start matches are marked.
191
+ */
192
+ function HighlightMatch({ text, query, startsWith = false }) {
193
+ const trimmed = query.trim();
194
+ if (!trimmed) return /* @__PURE__ */ jsx(Fragment, { children: text });
195
+ const mark = (part, index) => /* @__PURE__ */ jsx("mark", {
196
+ className: "rounded-[2px] bg-primary/15 text-inherit",
197
+ children: part
198
+ }, index);
199
+ if (startsWith) {
200
+ const parts = [];
201
+ let last = 0;
202
+ for (const match of text.matchAll(wordStartPattern(trimmed, "giu"))) {
203
+ const start = match.index + match[1].length;
204
+ if (start > last) parts.push(text.slice(last, start));
205
+ parts.push(mark(match[0].slice(match[1].length), parts.length));
206
+ last = match.index + match[0].length;
207
+ }
208
+ if (last < text.length) parts.push(text.slice(last));
209
+ return /* @__PURE__ */ jsx(Fragment, { children: parts });
210
+ }
211
+ const pattern = new RegExp(`(${trimmed.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi");
212
+ const parts = text.split(pattern);
213
+ return /* @__PURE__ */ jsx(Fragment, { children: parts.map((part, index) => part.toLowerCase() === trimmed.toLowerCase() ? mark(part, index) : part) });
214
+ }
215
+ /**
216
+ * Renders `actions` as inline icon buttons (≤2 actions) or an overflow
217
+ * menu (>2) — `mode` overrides the auto pick. Stops its own click from
218
+ * bubbling, so it composes safely inside a row that also has `onRowClick`.
219
+ */
220
+ function RowActions({ actions, mode = "auto", revealClassName, onOpenChange }) {
221
+ const [overflowOpen, setOverflowOpenState] = React.useState(false);
222
+ const setOverflowOpen = (open) => {
223
+ setOverflowOpenState(open);
224
+ onOpenChange?.(open);
225
+ };
226
+ if ((mode === "auto" ? actions.length <= 2 ? "inline" : "overflow" : mode) === "inline") return /* @__PURE__ */ jsx("div", {
227
+ "data-fit-exclude": true,
228
+ draggable: false,
229
+ className: cn("flex items-center justify-end gap-1", revealClassName),
230
+ onClick: (event) => event.stopPropagation(),
231
+ children: actions.map((action) => /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
232
+ asChild: true,
233
+ children: /* @__PURE__ */ jsx(Button, {
234
+ type: "button",
235
+ variant: "ghost",
236
+ size: "xs",
237
+ iconOnly: true,
238
+ "aria-label": action.label,
239
+ className: cn(action.variant === "destructive" && "text-destructive hover:text-destructive"),
240
+ onClick: action.onClick,
241
+ onMouseEnter: action.onMouseEnter,
242
+ onMouseLeave: action.onMouseLeave,
243
+ onFocus: action.onFocus,
244
+ onBlur: action.onBlur,
245
+ children: action.icon ? /* @__PURE__ */ jsx(action.icon, {}) : null
246
+ })
247
+ }), /* @__PURE__ */ jsx(TooltipContent, { children: action.label })] }, action.label))
248
+ });
249
+ return /* @__PURE__ */ jsx("div", {
250
+ "data-fit-exclude": true,
251
+ draggable: false,
252
+ className: cn("flex justify-end", overflowOpen ? "opacity-100" : revealClassName),
253
+ onClick: (event) => event.stopPropagation(),
254
+ children: /* @__PURE__ */ jsxs(DropdownMenu, {
255
+ open: overflowOpen,
256
+ onOpenChange: setOverflowOpen,
257
+ children: [/* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
258
+ asChild: true,
259
+ children: /* @__PURE__ */ jsx(DropdownMenuTrigger, {
260
+ asChild: true,
261
+ children: /* @__PURE__ */ jsx(Button, {
262
+ type: "button",
263
+ variant: "ghost",
264
+ size: "xs",
265
+ iconOnly: true,
266
+ "aria-label": "Row actions",
267
+ children: /* @__PURE__ */ jsx(EllipsisIcon, {})
268
+ })
269
+ })
270
+ }), /* @__PURE__ */ jsx(TooltipContent, { children: "Row actions" })] }), /* @__PURE__ */ jsx(DropdownMenuContent, {
271
+ align: "end",
272
+ children: actions.map((action) => /* @__PURE__ */ jsxs(DropdownMenuItem, {
273
+ variant: action.variant,
274
+ onClick: action.onClick,
275
+ onMouseEnter: action.onMouseEnter,
276
+ onMouseLeave: action.onMouseLeave,
277
+ onFocus: action.onFocus,
278
+ onBlur: action.onBlur,
279
+ children: [action.icon ? /* @__PURE__ */ jsx(action.icon, { "data-icon": "inline-start" }) : null, action.label]
280
+ }, action.label))
281
+ })]
282
+ })
283
+ });
284
+ }
285
+ var FIT_TO_CONTENT_PADDING = 2;
286
+ var DEFAULT_MAX_COLUMN_SIZE = 1200;
287
+ var PAGE_SELECT_THRESHOLD = 50;
288
+ /**
289
+ * The page numbers (and `"ellipsis"` gaps) a numbered `Pagination` control
290
+ * actually renders for `current` (1-indexed) out of `total` pages — always
291
+ * page 1 and `total`, always `current`'s own immediate neighbors, an
292
+ * ellipsis standing in for whatever's skipped between those two clusters.
293
+ */
294
+ function getPaginationWindow(current, total) {
295
+ const pages = /* @__PURE__ */ new Set([1, total]);
296
+ for (let page = current - 1; page <= current + 1; page++) if (page >= 1 && page <= total) pages.add(page);
297
+ const sorted = [...pages].sort((a, b) => a - b);
298
+ const result = [];
299
+ let previous = 0;
300
+ for (const page of sorted) {
301
+ if (page - previous > 1) result.push("ellipsis");
302
+ result.push(page);
303
+ previous = page;
304
+ }
305
+ return result;
306
+ }
307
+ /**
308
+ * Clones every element in `elements` into one shared, unconstrained, hidden
309
+ * container appended to `document.body` exactly once, to measure each
310
+ * clone's natural (unwrapped, unclipped) content width independent of
311
+ * whatever width the live layout currently gives it — one batch of
312
+ * append/measure/remove instead of one full reflow-triggering cycle per
313
+ * cell, which is what a header plus a full page of body rows would
314
+ * otherwise cost on every "Fit to content" click. Measuring the live
315
+ * elements' own `scrollWidth` directly doesn't work here: once a column
316
+ * has already been fit once, its cells are exactly as wide as their
317
+ * content, so `scrollWidth` on those *live* cells just reports back their
318
+ * own current box width (there's nothing left to overflow) rather than the
319
+ * true minimum — clicking "Fit to content" again would then add
320
+ * `FIT_TO_CONTENT_PADDING` on top of that every time, growing the column a
321
+ * couple pixels on every click forever. The clones sidestep that by never
322
+ * being constrained by the table's own column width to begin with. Strips
323
+ * anything marked `data-fit-exclude` first — hover-only header actions
324
+ * shouldn't force permanent extra width onto a column just because they're
325
+ * in the DOM.
326
+ */
327
+ function measureNaturalWidths(elements) {
328
+ const container = document.createElement("div");
329
+ Object.assign(container.style, {
330
+ position: "absolute",
331
+ visibility: "hidden",
332
+ left: "-9999px",
333
+ top: "-9999px"
334
+ });
335
+ const clones = elements.map((element) => {
336
+ const clone = element.cloneNode(true);
337
+ clone.querySelectorAll("[data-fit-exclude]").forEach((node) => node.remove());
338
+ Object.assign(clone.style, {
339
+ display: "inline-block",
340
+ width: "auto",
341
+ maxWidth: "none",
342
+ whiteSpace: "nowrap"
343
+ });
344
+ container.appendChild(clone);
345
+ return clone;
346
+ });
347
+ document.body.appendChild(container);
348
+ const widths = clones.map((clone) => clone.scrollWidth);
349
+ container.remove();
350
+ return widths;
351
+ }
352
+ /**
353
+ * Measures every currently-rendered cell in `column` — its header plus
354
+ * this page's body rows, via `measureNaturalWidths` — and sets the
355
+ * column's size to the widest one found, via `table.setColumnSizing`.
356
+ * Only rows rendered on the *current* page are measured — a deliberate,
357
+ * cheaper scope than the whole dataset. Requires `enableColumnResizing`
358
+ * on `DataTable` (a column that can't resize can't be fit either — see
359
+ * `columnMenuActions`). The measuring pass runs one frame later
360
+ * (`requestAnimationFrame`) so it never shares a frame with whatever
361
+ * triggered it (a click, a double-click) — it's a batch of forced
362
+ * layout reads either way, just not fighting that frame's own paint too.
363
+ */
364
+ function fitColumnToContent(column, table) {
365
+ const elements = table.options.meta?.getColumnCellElements?.(column.id) ?? [];
366
+ if (elements.length === 0) return;
367
+ requestAnimationFrame(() => {
368
+ const widest = Math.max(...measureNaturalWidths(elements));
369
+ table.setColumnSizing((old) => ({
370
+ ...old,
371
+ [column.id]: widest + FIT_TO_CONTENT_PADDING
372
+ }));
373
+ });
374
+ }
375
+ /**
376
+ * Pin left/Pin right `RowAction`s for `column` — each becomes "Unpin"
377
+ * instead once the column's already pinned to that side. Empty when the
378
+ * column can't pin (`getCanPin()`) or `meta.pinLocked` is set. Shared by
379
+ * `columnMenuActions` (the header-cell menu) and the Columns-menu popover
380
+ * row, which renders these same two actions as plain inline icon buttons
381
+ * instead of inside a dropdown.
382
+ */
383
+ function pinActions(column) {
384
+ if (!column.getCanPin() || column.columnDef.meta?.pinLocked) return [];
385
+ const pinned = column.getIsPinned();
386
+ return [pinned === "left" ? {
387
+ label: "Unpin",
388
+ icon: PinOffIcon,
389
+ onClick: () => column.pin(false)
390
+ } : {
391
+ label: "Pin left",
392
+ icon: ArrowLeftToLineIcon,
393
+ onClick: () => column.pin("left")
394
+ }, pinned === "right" ? {
395
+ label: "Unpin",
396
+ icon: PinOffIcon,
397
+ onClick: () => column.pin(false)
398
+ } : {
399
+ label: "Pin right",
400
+ icon: ArrowRightToLineIcon,
401
+ onClick: () => column.pin("right")
402
+ }];
403
+ }
404
+ /**
405
+ * A ready-made column-menu bundle for `headerActions` — Pin left, Pin
406
+ * right (via `pinActions`), Hide column, and Fit to content — each
407
+ * omitted when the column can't do it (`getCanPin()`/`getCanHide()`/
408
+ * `getCanResize()`, the last only true once `enableColumnResizing` is
409
+ * set). Typically 3-4 actions, so `RowActions`' own auto mode already
410
+ * renders it as a single overflow "more" menu — no need to force
411
+ * `mode="overflow"`.
412
+ */
413
+ function columnMenuActions(column, table) {
414
+ const actions = [...pinActions(column)];
415
+ if (column.getCanHide()) actions.push({
416
+ label: "Hide column",
417
+ icon: EyeOffIcon,
418
+ onClick: () => column.toggleVisibility(false)
419
+ });
420
+ if (column.getCanResize()) actions.push({
421
+ label: "Fit to content",
422
+ icon: UnfoldHorizontalIcon,
423
+ onClick: () => fitColumnToContent(column, table)
424
+ });
425
+ return actions;
426
+ }
427
+ var numericAwareCollator = new Intl.Collator(void 0, { numeric: true });
428
+ /** `"bucket"` is the canonical name; `"range"` is still accepted (with a dev warning — see the deprecation checks near the component's other dev warnings) since it originally had no more specific name to disambiguate against. */
429
+ function isBucketFilterVariant(column) {
430
+ const variant = column.columnDef.meta?.filterVariant;
431
+ return variant === "bucket" || variant === "range";
432
+ }
433
+ /** `bucketSize` is the canonical name; `rangeBucketSize` is still accepted (same deprecation as `"range"` above). */
434
+ function resolveBucketSize(column) {
435
+ const meta = column.columnDef.meta;
436
+ return meta?.bucketSize ?? meta?.rangeBucketSize ?? DEFAULT_RANGE_BUCKET_SIZE;
437
+ }
438
+ /** The resolved option list for an enumerable filter (`select`/`singleSelect`/`bucket`) — `meta.filterOptions` if given, else raw unique values or computed range buckets, sorted per `meta.filterOptionsSort` and decorated with a facet `count` when `showFilterCounts`. Shared by the header filter icon, the inline pinned filter, and the "more filters" pane — all three render the same resolved list the same way, see `renderFilterOptionContent`. */
439
+ function resolveFilterOptions(column, showFilterCounts) {
440
+ const meta = column.columnDef.meta;
441
+ const explicit = meta?.filterOptions;
442
+ let options;
443
+ if (explicit) options = typeof explicit === "function" ? explicit(column) : explicit;
444
+ else if (isBucketFilterVariant(column)) options = computeRangeBuckets(column, resolveBucketSize(column)).map((value) => ({ value }));
445
+ else options = Array.from(column.getFacetedUniqueValues().keys()).map((value) => ({ value: String(value) }));
446
+ const sortMode = meta?.filterOptionsSort ?? "auto";
447
+ if (typeof sortMode === "function") options = [...options].sort(sortMode);
448
+ else if (sortMode === "auto") options = [...options].sort((a, b) => numericAwareCollator.compare(a.textValue ?? (typeof a.label === "string" ? a.label : a.value), b.textValue ?? (typeof b.label === "string" ? b.label : b.value)));
449
+ if (showFilterCounts) {
450
+ const counts = column.getFacetedUniqueValues();
451
+ options = options.map((option) => ({
452
+ ...option,
453
+ count: counts.get(option.value)
454
+ }));
455
+ }
456
+ return options;
457
+ }
458
+ /** Whether any option in the list opts into grouping — grouping only activates once at least one option sets `group`; a list with none renders flat. */
459
+ function hasGroupedFilterOptions(options) {
460
+ return options.some((option) => option.group);
461
+ }
462
+ /** Stable-orders `options` into `{ group, options }` buckets, in whichever order each group first appears — used by every surface that renders grouped options the same way. */
463
+ function groupFilterOptions(options) {
464
+ const order = [];
465
+ const byGroup = /* @__PURE__ */ new Map();
466
+ for (const option of options) {
467
+ if (!byGroup.has(option.group)) {
468
+ order.push(option.group);
469
+ byGroup.set(option.group, []);
470
+ }
471
+ byGroup.get(option.group).push(option);
472
+ }
473
+ return order.map((group) => ({
474
+ group,
475
+ options: byGroup.get(group)
476
+ }));
477
+ }
478
+ /** The label content shared by every filter surface for one resolved option — icon, label (falling back to the raw value), a muted count when present, and a muted `description` line underneath when given. `size-4` is explicit, not left to each surface's own ambient `[&_svg:not([class*='size-'])]:size-4` ("skip if the class already sets a size") sizing rule — the checklist's plain `Label`+`Checkbox` row has no such rule the way `SelectItem`/`SelectMultipleItem` do, so an unsized icon there falls back to the browser's native (much larger) SVG default instead. */
479
+ function FilterOptionContent({ option }) {
480
+ const Icon = option.icon;
481
+ return /* @__PURE__ */ jsxs("span", {
482
+ className: "flex min-w-0 flex-1 items-center gap-1.5",
483
+ children: [Icon && /* @__PURE__ */ jsx(Icon, { className: "size-4 shrink-0" }), /* @__PURE__ */ jsxs("span", {
484
+ className: "flex min-w-0 flex-col",
485
+ children: [/* @__PURE__ */ jsxs("span", {
486
+ className: "flex items-center gap-1 truncate",
487
+ children: [option.label ?? option.value, option.count != null && /* @__PURE__ */ jsxs("span", {
488
+ className: "text-muted-foreground",
489
+ children: [
490
+ "(",
491
+ option.count,
492
+ ")"
493
+ ]
494
+ })]
495
+ }), option.description && /* @__PURE__ */ jsx("span", {
496
+ className: "truncate text-xs text-muted-foreground",
497
+ children: option.description
498
+ })]
499
+ })]
500
+ });
501
+ }
502
+ /** Stand-in for the option list while `meta.filterOptionsLoading` is set — same shape in every surface (checklist, `SelectMultipleContent`, plain `Select`'s content), since DataTable never fetches anything itself and has no real option count to size the skeleton to. */
503
+ function FilterOptionsSkeleton() {
504
+ return /* @__PURE__ */ jsxs("div", {
505
+ className: "flex flex-col gap-1.5 p-1",
506
+ children: [
507
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }),
508
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-4/5" }),
509
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-3/5" })
510
+ ]
511
+ });
512
+ }
513
+ var FILTER_SEARCH_THRESHOLD = 8;
514
+ var FILTER_SELECT_ALL_THRESHOLD = 2;
515
+ function optionSearchText(option) {
516
+ return option.textValue ?? (typeof option.label === "string" ? option.label : option.value);
517
+ }
518
+ /** The always-open checklist `ColumnFilterField`'s `select`/`range` branch renders — its own component so the search/select-all state doesn't leak into the parent's. */
519
+ function FilterOptionChecklist({ options, selected, onValueChange, stickySelectAll }) {
520
+ const [query, setQuery] = React.useState("");
521
+ const showSearch = options.length > FILTER_SEARCH_THRESHOLD;
522
+ const grouped = hasGroupedFilterOptions(options);
523
+ const visibleOptions = showSearch ? options.filter((option) => optionSearchText(option).toLowerCase().includes(query.toLowerCase())) : options;
524
+ const allVisibleSelected = visibleOptions.length > 0 && visibleOptions.every((option) => selected.includes(option.value));
525
+ const someVisibleSelected = visibleOptions.some((option) => selected.includes(option.value));
526
+ function toggleAll() {
527
+ if (allVisibleSelected) {
528
+ const visibleValues = new Set(visibleOptions.map((option) => option.value));
529
+ const next = selected.filter((value) => !visibleValues.has(value));
530
+ onValueChange(next.length ? next : void 0);
531
+ } else onValueChange(Array.from(/* @__PURE__ */ new Set([...selected, ...visibleOptions.map((option) => option.value)])));
532
+ }
533
+ function renderOption(option) {
534
+ return /* @__PURE__ */ jsxs(Label, {
535
+ className: "flex items-center gap-2 font-normal",
536
+ children: [/* @__PURE__ */ jsx(Checkbox, {
537
+ checked: selected.includes(option.value),
538
+ disabled: option.disabled,
539
+ onCheckedChange: (checked) => {
540
+ const next = checked ? [...selected, option.value] : selected.filter((value) => value !== option.value);
541
+ onValueChange(next.length ? next : void 0);
542
+ }
543
+ }), /* @__PURE__ */ jsx(FilterOptionContent, { option })]
544
+ }, option.value);
545
+ }
546
+ const selectAllRow = options.length > FILTER_SELECT_ALL_THRESHOLD && visibleOptions.length > 0 && /* @__PURE__ */ jsxs(Label, {
547
+ className: "flex items-center gap-2 pb-1.5 font-normal",
548
+ children: [/* @__PURE__ */ jsx(Checkbox, {
549
+ checked: allVisibleSelected ? true : someVisibleSelected ? "indeterminate" : false,
550
+ onCheckedChange: toggleAll
551
+ }), "Select all"]
552
+ });
553
+ return /* @__PURE__ */ jsxs("div", {
554
+ className: "flex flex-col gap-1.5",
555
+ children: [
556
+ showSearch && /* @__PURE__ */ jsx(Input, {
557
+ placeholder: "Search...",
558
+ value: query,
559
+ onChange: (event) => setQuery(event.target.value)
560
+ }),
561
+ stickySelectAll && selectAllRow,
562
+ /* @__PURE__ */ jsxs("div", {
563
+ className: "flex max-h-64 flex-col gap-1.5 overflow-y-auto",
564
+ children: [
565
+ !stickySelectAll && selectAllRow,
566
+ visibleOptions.length === 0 && /* @__PURE__ */ jsx("p", {
567
+ className: "text-sm text-muted-foreground",
568
+ children: "No values"
569
+ }),
570
+ grouped ? groupFilterOptions(visibleOptions).map(({ group, options }) => /* @__PURE__ */ jsxs("div", { children: [group && /* @__PURE__ */ jsx(Label, {
571
+ className: "mb-1 block text-xs text-muted-foreground",
572
+ children: group
573
+ }), /* @__PURE__ */ jsx("div", {
574
+ className: "flex flex-col gap-1.5",
575
+ children: options.map(renderOption)
576
+ })] }, group ?? "__ungrouped__")) : visibleOptions.map(renderOption)
577
+ ]
578
+ })
579
+ ]
580
+ });
581
+ }
582
+ /**
583
+ * `value`/`onValueChange` are handed in rather than read straight off
584
+ * `column` so this same field can drive either the real, immediately-applied
585
+ * filter state or a popover's own pending draft — see `ColumnFilterTrigger`'s
586
+ * `deferred` prop for the latter. Both still read `column` itself for
587
+ * `filterVariant`/unique values; only the value being edited is decoupled.
588
+ */
589
+ /** A text filter input whose own displayed value updates every keystroke, while the real filter it drives only catches up 200ms after typing stops (flushed immediately on blur or Enter) — same "instant to type into, debounced to act on" split as the toolbar's global search. */
590
+ function DebouncedFilterInput({ value, onValueChange, placeholder = "Filter..." }) {
591
+ const [inputValue, setInputValue] = React.useState(value);
592
+ const debouncedOnValueChange = useDebouncedCallback(onValueChange, 200);
593
+ React.useEffect(() => {
594
+ setInputValue(value);
595
+ }, [value]);
596
+ return /* @__PURE__ */ jsx(Input, {
597
+ placeholder,
598
+ value: inputValue,
599
+ onChange: (event) => {
600
+ setInputValue(event.target.value);
601
+ debouncedOnValueChange(event.target.value || void 0);
602
+ },
603
+ onBlur: () => onValueChange(inputValue || void 0),
604
+ onKeyDown: (event) => {
605
+ if (event.key === "Enter") onValueChange(inputValue || void 0);
606
+ }
607
+ });
608
+ }
609
+ function ColumnFilterField({ column, value, onValueChange, showFilterCounts, stickyFilterSelectAll }) {
610
+ const meta = column.columnDef.meta;
611
+ if (meta?.filterRender) return meta.filterRender({
612
+ column,
613
+ value,
614
+ onValueChange
615
+ });
616
+ if (meta?.filterOptionsLoading) return /* @__PURE__ */ jsx(FilterOptionsSkeleton, {});
617
+ if (meta?.filterVariant === "select" || isBucketFilterVariant(column)) {
618
+ const options = resolveFilterOptions(column, showFilterCounts);
619
+ return /* @__PURE__ */ jsx(FilterOptionChecklist, {
620
+ options,
621
+ selected: value ?? [],
622
+ onValueChange,
623
+ stickySelectAll: stickyFilterSelectAll
624
+ });
625
+ }
626
+ if (meta?.filterVariant === "singleSelect") {
627
+ const options = resolveFilterOptions(column, showFilterCounts);
628
+ return /* @__PURE__ */ jsxs(Select, {
629
+ value: value ?? ALL_VALUE,
630
+ onValueChange: (next) => onValueChange(next === ALL_VALUE ? void 0 : next),
631
+ children: [/* @__PURE__ */ jsx(SelectTrigger, {
632
+ className: "w-full",
633
+ children: /* @__PURE__ */ jsx(SelectValue, {})
634
+ }), /* @__PURE__ */ jsxs(SelectContent, { children: [/* @__PURE__ */ jsx(SelectItem, {
635
+ value: ALL_VALUE,
636
+ children: "All"
637
+ }), options.map((option) => /* @__PURE__ */ jsx(SelectItem, {
638
+ value: option.value,
639
+ disabled: option.disabled,
640
+ children: /* @__PURE__ */ jsx(FilterOptionContent, { option })
641
+ }, option.value))] })]
642
+ });
643
+ }
644
+ return /* @__PURE__ */ jsx(DebouncedFilterInput, {
645
+ value: value ?? "",
646
+ onValueChange
647
+ });
648
+ }
649
+ /** The search input + select-all/clear row + grouped-or-flat `SelectMultipleItem`s shared by every `SelectMultiple`-based filter surface (inline toolbar chip and pane, for `select`/`range`/`singleSelect` alike) — matches `select.tsx`'s own documented search recipe: hidden via `className`, never unmounted, so a selected-but-filtered-out item keeps its label registered for the trigger's own comma-list. `showSelectAll` is only meaningful for true multi-select (`select`/`range`) — a `singleSelect` list has nothing for "select all" to mean. */
650
+ function SelectMultipleOptionsContent({ options, value, onValueChangeAll, showSelectAll = true, stickySelectAll }) {
651
+ const [query, setQuery] = React.useState("");
652
+ const showSearch = options.length > FILTER_SEARCH_THRESHOLD;
653
+ const grouped = hasGroupedFilterOptions(options);
654
+ const matches = (option) => !showSearch || !query || optionSearchText(option).toLowerCase().includes(query.toLowerCase());
655
+ const visibleOptions = options.filter(matches);
656
+ const allVisibleSelected = visibleOptions.length > 0 && visibleOptions.every((option) => value.includes(option.value));
657
+ const someVisibleSelected = visibleOptions.some((option) => value.includes(option.value));
658
+ function toggleAll() {
659
+ if (allVisibleSelected) {
660
+ const visibleValues = new Set(visibleOptions.map((option) => option.value));
661
+ const next = value.filter((v) => !visibleValues.has(v));
662
+ onValueChangeAll(next.length ? next : void 0);
663
+ } else onValueChangeAll(Array.from(/* @__PURE__ */ new Set([...value, ...visibleOptions.map((option) => option.value)])));
664
+ }
665
+ function renderItem(option) {
666
+ return /* @__PURE__ */ jsx(SelectMultipleItem, {
667
+ value: option.value,
668
+ disabled: option.disabled,
669
+ textValue: option.textValue ?? (typeof option.label === "string" ? option.label : void 0),
670
+ className: cn(!matches(option) && "hidden"),
671
+ children: /* @__PURE__ */ jsx(FilterOptionContent, { option })
672
+ }, option.value);
673
+ }
674
+ const selectAllRow = showSelectAll && options.length > FILTER_SELECT_ALL_THRESHOLD && options.length > 0 && /* @__PURE__ */ jsxs(Label, {
675
+ className: "flex items-center gap-2 p-1.5 font-normal",
676
+ children: [/* @__PURE__ */ jsx(Checkbox, {
677
+ checked: allVisibleSelected ? true : someVisibleSelected ? "indeterminate" : false,
678
+ onCheckedChange: toggleAll
679
+ }), "Select all"]
680
+ });
681
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
682
+ showSearch && /* @__PURE__ */ jsx("div", {
683
+ className: "p-1 pb-0",
684
+ children: /* @__PURE__ */ jsx(Input, {
685
+ placeholder: "Search...",
686
+ value: query,
687
+ onChange: (event) => setQuery(event.target.value)
688
+ })
689
+ }),
690
+ stickySelectAll && selectAllRow,
691
+ /* @__PURE__ */ jsxs("div", {
692
+ className: "max-h-64 overflow-y-auto",
693
+ children: [!stickySelectAll && selectAllRow, grouped ? groupFilterOptions(options).map(({ group, options: groupOptions }) => /* @__PURE__ */ jsxs(SelectMultipleGroup, { children: [group && /* @__PURE__ */ jsx(SelectMultipleLabel, { children: group }), groupOptions.map(renderItem)] }, group ?? "__ungrouped__")) : options.map(renderItem)]
694
+ })
695
+ ] });
696
+ }
697
+ /** Same filtering as `ColumnFilterField` (including `meta.filterRender`, checked first), but the select/range/singleSelect variants stay closed by default (a `SelectMultiple`/`Select`) instead of listing every value upfront — the filters pane holds every filterable column at once, so an always-open checklist per column would be too much to scan. `value`/`onValueChange` are the pane's own shared draft under `filterApplyMode="deferred"` (see the pane's footer), or the real filter directly otherwise — same dual-mode shape `ColumnFilterField` already uses. */
698
+ /**
699
+ * Same filter shapes as `ColumnFilterField`, but self-contained rather
700
+ * than taking `value`/`onValueChange` from a caller — the pane has no
701
+ * shared batch draft or footer of its own (just "Clear filters", which
702
+ * clears the real, already-applied filters). A multiselect field
703
+ * (`select`/`range`) is still always deferred, same as everywhere else,
704
+ * but that's *this* field's own popover — its own draft, its own
705
+ * Cancel/Apply footer inside it — exactly like the inline toolbar's
706
+ * identical field, not a pane-wide batch.
707
+ */
708
+ /** The pane's `filterRender` field under a deferred apply mode — a local draft plus an inline Cancel/Apply row, since `filterRender` in the pane isn't wrapped in a popover of its own the way every other variant here is (it just renders directly, always visible), so there's no popover footer to hang Cancel/Apply off of the way `SelectMultipleFooter`/`FilterPopoverFooter` do elsewhere. */
709
+ function DeferredPaneFilterRender({ column, filterRender }) {
710
+ const committedValue = column.getFilterValue();
711
+ const [pendingValue, setPendingValue] = React.useState(committedValue);
712
+ React.useEffect(() => {
713
+ setPendingValue(committedValue);
714
+ }, [committedValue]);
715
+ return /* @__PURE__ */ jsxs("div", {
716
+ className: "flex flex-col gap-2",
717
+ children: [filterRender({
718
+ column,
719
+ value: pendingValue,
720
+ onValueChange: setPendingValue
721
+ }), /* @__PURE__ */ jsxs("div", {
722
+ className: "flex items-center justify-end gap-1",
723
+ children: [/* @__PURE__ */ jsx(Button, {
724
+ type: "button",
725
+ variant: "ghost",
726
+ size: "xs",
727
+ onClick: () => setPendingValue(committedValue),
728
+ children: "Cancel"
729
+ }), /* @__PURE__ */ jsx(Button, {
730
+ type: "button",
731
+ size: "xs",
732
+ disabled: filterDraftUnchanged(committedValue, pendingValue),
733
+ onClick: () => column.setFilterValue(pendingValue),
734
+ children: "Apply"
735
+ })]
736
+ })]
737
+ });
738
+ }
739
+ function PaneColumnFilterField({ column, showFilterCounts, tableDeferred, stickyFilterSelectAll }) {
740
+ const meta = column.columnDef.meta;
741
+ const [open, setOpenState] = React.useState(false);
742
+ const [pendingValue, setPendingValue] = React.useState(void 0);
743
+ const setOpen = (value) => {
744
+ if (value) setPendingValue(column.getFilterValue());
745
+ setOpenState(value);
746
+ };
747
+ if (meta?.filterRender) {
748
+ if (isColumnDeferred(column, !!tableDeferred)) return /* @__PURE__ */ jsx(DeferredPaneFilterRender, {
749
+ column,
750
+ filterRender: meta.filterRender
751
+ });
752
+ return meta.filterRender({
753
+ column,
754
+ value: column.getFilterValue(),
755
+ onValueChange: (value) => column.setFilterValue(value)
756
+ });
757
+ }
758
+ if (meta?.filterOptionsLoading) return /* @__PURE__ */ jsx(FilterOptionsSkeleton, {});
759
+ if (meta?.filterVariant === "select" || isBucketFilterVariant(column)) {
760
+ const options = resolveFilterOptions(column, showFilterCounts);
761
+ const selected = open ? pendingValue ?? [] : column.getFilterValue() ?? [];
762
+ return /* @__PURE__ */ jsxs(SelectMultiple, {
763
+ open,
764
+ onOpenChange: setOpen,
765
+ value: selected,
766
+ onValueChange: (next) => setPendingValue(next.length ? next : void 0),
767
+ children: [/* @__PURE__ */ jsx(SelectMultipleTrigger, {
768
+ placeholder: "All",
769
+ className: "w-full"
770
+ }), /* @__PURE__ */ jsxs(SelectMultipleContent, { children: [/* @__PURE__ */ jsx(SelectMultipleOptionsContent, {
771
+ options,
772
+ value: selected,
773
+ onValueChangeAll: setPendingValue,
774
+ stickySelectAll: stickyFilterSelectAll
775
+ }), /* @__PURE__ */ jsxs(SelectMultipleFooter, { children: [/* @__PURE__ */ jsx(Button, {
776
+ type: "button",
777
+ variant: "ghost",
778
+ size: "xs",
779
+ onClick: () => setOpen(false),
780
+ children: "Cancel"
781
+ }), /* @__PURE__ */ jsx(Button, {
782
+ type: "button",
783
+ size: "xs",
784
+ disabled: filterDraftUnchanged(column.getFilterValue(), pendingValue),
785
+ onClick: () => {
786
+ column.setFilterValue(pendingValue);
787
+ setOpen(false);
788
+ },
789
+ children: "Apply"
790
+ })] })] })]
791
+ });
792
+ }
793
+ if (meta?.filterVariant === "singleSelect") {
794
+ const options = resolveFilterOptions(column, showFilterCounts);
795
+ const selected = column.getFilterValue() ?? "";
796
+ return /* @__PURE__ */ jsxs(SelectMultiple, {
797
+ closeOnSelect: true,
798
+ value: selected ? [selected] : [],
799
+ onValueChange: (next) => column.setFilterValue(next.at(-1)),
800
+ children: [/* @__PURE__ */ jsx(SelectMultipleTrigger, {
801
+ placeholder: "All",
802
+ className: "w-full"
803
+ }), /* @__PURE__ */ jsx(SelectMultipleContent, { children: /* @__PURE__ */ jsx(SelectMultipleOptionsContent, {
804
+ options,
805
+ value: selected ? [selected] : [],
806
+ onValueChangeAll: () => {},
807
+ showSelectAll: false,
808
+ stickySelectAll: stickyFilterSelectAll
809
+ }) })]
810
+ });
811
+ }
812
+ return /* @__PURE__ */ jsx(DebouncedFilterInput, {
813
+ value: column.getFilterValue() ?? "",
814
+ onValueChange: (value) => column.setFilterValue(value)
815
+ });
816
+ }
817
+ var warnedMissingLabelIds = /* @__PURE__ */ new Set();
818
+ function columnLabel(column) {
819
+ if (column.columnDef.meta?.label) return column.columnDef.meta.label;
820
+ if (typeof column.columnDef.header === "string") return column.columnDef.header;
821
+ if (process.env.NODE_ENV !== "production" && !warnedMissingLabelIds.has(column.id)) {
822
+ warnedMissingLabelIds.add(column.id);
823
+ console.warn(`DataTable: column "${column.id}" has a non-string \`header\` (an icon, a custom component, ...) and no \`meta.label\` — every place that needs this column's *name* rather than its rendered header (the Columns menu, the "All filters" pane, tooltips, applied-filter/chip copy) falls back to its raw \`id\` instead. Set \`meta.label\` to fix this.`);
824
+ }
825
+ return column.id;
826
+ }
827
+ /**
828
+ * Bulk-actions bar shown while any row is selected — hovers over the
829
+ * bottom edge of the table's own viewport, never the screen: `sticky`
830
+ * inside `tableContainerRef`, which is unconditionally a genuine scroll
831
+ * container (its own `overflow-auto`) by the time this renders. `sticky`
832
+ * (not `absolute`) is what actually matters here:
833
+ * `absolute` positions against the *content* box, so it scrolls away
834
+ * with the rows above it instead of staying put — `sticky` is what
835
+ * tracks the scroll *viewport* itself. Since `tableContainerRef` is the
836
+ * nearest scrolling ancestor either way, this never hits the Card
837
+ * `overflow-hidden` problem `sticky` usually has (that only happens when
838
+ * `sticky` has to bubble past a non-scrolling container to find one).
839
+ * Either way it's a child of `tableContainerRef`, not a portal, so it's
840
+ * bounded by the table's own box, not the viewport.
841
+ */
842
+ var BulkActionsBar = React.forwardRef(function BulkActionsBar({ count, hiddenByFilterCount = 0, totalCount, isAllSelected, maxSelectedRows, atSelectionCap = false, onSelectAll, onClear, clearLabel = "Clear selection", controlSize, controlIconSize, children }, ref) {
843
+ return /* @__PURE__ */ jsx("div", {
844
+ ref,
845
+ className: "pointer-events-none sticky bottom-2 z-10 flex justify-center",
846
+ children: /* @__PURE__ */ jsxs("div", {
847
+ className: "pointer-events-auto flex items-center gap-1 rounded-full border border-border bg-popover p-1.5 shadow-popover",
848
+ children: [
849
+ /* @__PURE__ */ jsx("div", {
850
+ className: "flex items-center gap-1.5 px-2",
851
+ children: /* @__PURE__ */ jsx("span", {
852
+ className: "text-sm text-muted-foreground",
853
+ children: isAllSelected ? `All ${totalCount} selected` : /* @__PURE__ */ jsxs(Fragment, { children: [
854
+ count,
855
+ " ",
856
+ maxSelectedRows != null ? `of ${maxSelectedRows} max selected` : "selected",
857
+ hiddenByFilterCount > 0 && ` (${hiddenByFilterCount} hidden by filters)`
858
+ ] })
859
+ })
860
+ }),
861
+ /* @__PURE__ */ jsx(Button, {
862
+ type: "button",
863
+ variant: "ghost",
864
+ size: controlSize,
865
+ disabled: !isAllSelected && atSelectionCap,
866
+ title: !isAllSelected && atSelectionCap ? `Up to ${maxSelectedRows} rows can be selected` : void 0,
867
+ onClick: isAllSelected ? onClear : onSelectAll,
868
+ children: isAllSelected ? "Clear selection" : /* @__PURE__ */ jsxs(Fragment, { children: [
869
+ /* @__PURE__ */ jsx(CheckCheckIcon, {}),
870
+ "Select all ",
871
+ totalCount
872
+ ] })
873
+ }),
874
+ children && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", { className: "h-5 w-px shrink-0 bg-border" }), /* @__PURE__ */ jsx("div", {
875
+ className: "flex items-center gap-1",
876
+ children
877
+ })] }),
878
+ /* @__PURE__ */ jsx("div", { className: "h-5 w-px shrink-0 bg-border" }),
879
+ /* @__PURE__ */ jsx(Button, {
880
+ type: "button",
881
+ variant: "ghost",
882
+ size: controlIconSize,
883
+ iconOnly: true,
884
+ "aria-label": clearLabel,
885
+ onClick: onClear,
886
+ children: /* @__PURE__ */ jsx(XIcon, {})
887
+ })
888
+ ]
889
+ })
890
+ });
891
+ });
892
+ /** Sticky offset for a pinned column — `undefined` (no inline style) when the column isn't pinned. */
893
+ function getPinnedStyle(column) {
894
+ const pinned = column.getIsPinned();
895
+ if (!pinned) return void 0;
896
+ return pinned === "left" ? {
897
+ position: "sticky",
898
+ left: column.getStart("left")
899
+ } : {
900
+ position: "sticky",
901
+ right: column.getAfter("right")
902
+ };
903
+ }
904
+ /**
905
+ * Solid `bg-card` (the table's own real white surface, not `bg-background`
906
+ * — that token is the page canvas gray, which read as a mismatched "grey
907
+ * bg" against the rest of the table) so scrolled content doesn't show
908
+ * through underneath, plus a thicker boundary divider on the outermost
909
+ * pinned column of each side — on its right edge for the left-pinned
910
+ * group, its left edge for the right-pinned group, i.e. towards whichever
911
+ * side actually borders the scrollable/center columns. `bg-card` is opaque
912
+ * and painted directly on this cell, which would otherwise hide this row's
913
+ * own hover/selected tint (both painted on the `<tr>`, a layer down) for
914
+ * just this one column — `group-hover/row`/`group-data-[state=selected]/row`
915
+ * (reacting to `TableRow`'s own `group/row`, see table.tsx) repaints the
916
+ * same tint directly on this cell instead of relying on it showing through.
917
+ */
918
+ function getPinnedClassName(column, table) {
919
+ const pinned = column.getIsPinned();
920
+ if (!pinned) return "";
921
+ const visibleLeft = table.getLeftLeafColumns().filter((c) => c.getIsVisible());
922
+ const visibleRight = table.getRightLeafColumns().filter((c) => c.getIsVisible());
923
+ const isLastLeft = pinned === "left" && visibleLeft.at(-1)?.id === column.id;
924
+ const isFirstRight = pinned === "right" && visibleRight[0]?.id === column.id;
925
+ return cn("z-10 bg-card", "group-hover/row:bg-[color-mix(in_oklab,var(--color-muted)_50%,var(--color-card))]", "group-data-[state=selected]/row:bg-[color-mix(in_oklab,var(--color-muted)_100%,var(--color-card))]", isLastLeft && "border-r-2 border-border", isFirstRight && "border-l-2 border-border");
926
+ }
927
+ /**
928
+ * The "scroll fade" cue — an inset shadow indicating there's scrollable
929
+ * content sliding in underneath a pinned column — lives on the *scrolling*
930
+ * side of the pinned boundary (the first/last center column next to it),
931
+ * not on the pinned column itself: the fade represents content that's
932
+ * about to disappear under something fixed, which is a property of the
933
+ * content moving, not of the fixed thing it's moving under. Only rendered
934
+ * once `canScroll` is true (the table actually overflows horizontally) —
935
+ * a shadow implying "more content behind this edge" would be misleading
936
+ * otherwise.
937
+ */
938
+ function getScrollFadeClassName(column, table, canScroll) {
939
+ if (!canScroll || column.getIsPinned()) return "";
940
+ const visibleLeft = table.getLeftLeafColumns().filter((c) => c.getIsVisible());
941
+ const visibleRight = table.getRightLeafColumns().filter((c) => c.getIsVisible());
942
+ const visibleCenter = table.getCenterLeafColumns().filter((c) => c.getIsVisible());
943
+ const isFirstCenter = visibleLeft.length > 0 && visibleCenter[0]?.id === column.id;
944
+ const isLastCenter = visibleRight.length > 0 && visibleCenter.at(-1)?.id === column.id;
945
+ return cn(isFirstCenter && "shadow-[inset_8px_0_8px_-8px_rgba(0,0,0,0.15)]", isLastCenter && "shadow-[inset_-8px_0_8px_-8px_rgba(0,0,0,0.15)]");
946
+ }
947
+ /**
948
+ * Cancel/Apply footer for a plain-`Popover`-based filter field (see
949
+ * `ColumnFilterTrigger` and `InlineColumnFilter`'s own fallback branch) —
950
+ * picks edit a local draft only, never the real filter; Apply commits that
951
+ * draft, Cancel just closes and discards it, leaving the real filter
952
+ * exactly as it was before the popover opened.
953
+ */
954
+ function FilterPopoverFooter({ onCancel, onApply, applyDisabled }) {
955
+ return /* @__PURE__ */ jsxs("div", {
956
+ className: "-mx-2.5 mt-2 flex w-[calc(100%+--spacing(5))] items-center justify-end gap-1 border-t border-border px-2.5 pt-2",
957
+ children: [/* @__PURE__ */ jsx(Button, {
958
+ type: "button",
959
+ variant: "ghost",
960
+ size: "xs",
961
+ onClick: onCancel,
962
+ children: "Cancel"
963
+ }), /* @__PURE__ */ jsx(Button, {
964
+ type: "button",
965
+ size: "xs",
966
+ disabled: applyDisabled,
967
+ onClick: onApply,
968
+ children: "Apply"
969
+ })]
970
+ });
971
+ }
972
+ /**
973
+ * A multiselect filter (`filterVariant: "select"` or `"range"` — either
974
+ * way, a checklist of several values at once) always needs a deliberate
975
+ * Apply, in every surface, regardless of `filterApplyMode` — picking one
976
+ * value out of a list of many isn't a complete thought the way a single
977
+ * text edit or a `singleSelect` pick already is; the whole point of
978
+ * checking several boxes is to review the set before it takes effect.
979
+ * `filterApplyMode="deferred"` still separately opts every *other* filter
980
+ * shape into the same Cancel/Apply step.
981
+ */
982
+ /** True for every filter shape with more than one field/pick to review before it's a complete thought — `select`/`range`/`bucket` checklists. */
983
+ function isMultiSelectFilterVariant(column) {
984
+ const variant = column.columnDef.meta?.filterVariant;
985
+ return variant === "select" || variant === "range" || variant === "bucket";
986
+ }
987
+ /** `singleSelect` commits on a single click/pick — there's never a moment an open draft exists to defer in the first place, so it can never be put into deferred mode, the same way a multiselect variant can never be taken *out* of it (see `isMultiSelectFilterVariant`). */
988
+ function isNeverDeferredFilterVariant(column) {
989
+ return column.columnDef.meta?.filterVariant === "singleSelect";
990
+ }
991
+ /** Whether this column's filter should defer to an explicit Apply — `meta.filterApplyMode` overrides the table-wide `filterApplyMode` when set, but neither can turn a multiselect (`select`/`range`/`bucket`) *off* deferred, or a `singleSelect` *into* it. */
992
+ function isColumnDeferred(column, tableDeferred) {
993
+ const columnMode = column.columnDef.meta?.filterApplyMode;
994
+ if (column.columnDef.meta?.filterRender) return columnMode === "deferred" || !columnMode && tableDeferred;
995
+ if (isNeverDeferredFilterVariant(column)) return false;
996
+ return (columnMode ? columnMode === "deferred" : tableDeferred) || isMultiSelectFilterVariant(column);
997
+ }
998
+ /**
999
+ * Whether a filter draft actually differs from what's currently applied —
1000
+ * gates every deferred filter surface's Apply button so it's disabled
1001
+ * (not just harmless) when clicking it would be a no-op. Filter values
1002
+ * here are always plain JSON-serializable data (strings, string arrays,
1003
+ * small plain objects from a custom `filterRender`), so a `JSON.stringify`
1004
+ * comparison is a legitimate deep-equality check, not a shortcut.
1005
+ */
1006
+ function filterDraftUnchanged(committed, pending) {
1007
+ return JSON.stringify(committed ?? null) === JSON.stringify(pending ?? null);
1008
+ }
1009
+ /** Delays invoking `callback` until `delayMs` have passed with no new call — the returned function is stable across renders (safe in a dep array), always calling whatever `callback` currently is by the time the delay elapses, not whatever it was when the delay started. */
1010
+ function useDebouncedCallback(callback, delayMs) {
1011
+ const callbackRef = React.useRef(callback);
1012
+ React.useEffect(() => {
1013
+ callbackRef.current = callback;
1014
+ });
1015
+ const timeoutRef = React.useRef(void 0);
1016
+ React.useEffect(() => () => clearTimeout(timeoutRef.current), []);
1017
+ return React.useCallback((...args) => {
1018
+ clearTimeout(timeoutRef.current);
1019
+ timeoutRef.current = setTimeout(() => callbackRef.current(...args), delayMs);
1020
+ }, [delayMs]);
1021
+ }
1022
+ /** Resolves `column`'s own `meta.filterMergeWith` ids against `table`, dropping any that don't resolve to a real column — the list `ColumnFilterTrigger` actually renders a field for, `column` itself always first. */
1023
+ function mergedFilterColumns(column, table) {
1024
+ return [column, ...(column.columnDef.meta?.filterMergeWith ?? []).map((id) => table.getColumn(id)).filter((c) => !!c)];
1025
+ }
1026
+ function ColumnFilterTrigger({ column, table, revealClassName, onOpenChange, tableDeferred, showFilterCounts, stickyFilterSelectAll }) {
1027
+ const [open, setOpenState] = React.useState(false);
1028
+ const [pendingValues, setPendingValues] = React.useState({});
1029
+ const mergedColumns = mergedFilterColumns(column, table);
1030
+ const deferred = mergedColumns.some((c) => isColumnDeferred(c, !!tableDeferred));
1031
+ const setOpen = (value) => {
1032
+ if (value) setPendingValues(Object.fromEntries(mergedColumns.map((c) => [c.id, c.getFilterValue()])));
1033
+ setOpenState(value);
1034
+ onOpenChange?.(value);
1035
+ };
1036
+ const label = columnLabel(column);
1037
+ const filtered = mergedColumns.some((c) => c.getIsFiltered());
1038
+ return /* @__PURE__ */ jsx("div", {
1039
+ "data-fit-exclude": true,
1040
+ draggable: false,
1041
+ className: cn("shrink-0", filtered || open ? "opacity-100" : revealClassName),
1042
+ children: /* @__PURE__ */ jsxs(Popover, {
1043
+ open,
1044
+ onOpenChange: setOpen,
1045
+ children: [/* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
1046
+ asChild: true,
1047
+ children: /* @__PURE__ */ jsx(PopoverTrigger, {
1048
+ asChild: true,
1049
+ children: /* @__PURE__ */ jsx(Button, {
1050
+ type: "button",
1051
+ variant: "ghost",
1052
+ size: "xs",
1053
+ iconOnly: true,
1054
+ "aria-label": `Filter ${label}`,
1055
+ className: cn("shrink-0", filtered && "text-primary"),
1056
+ children: /* @__PURE__ */ jsx(ListFilterIcon, {})
1057
+ })
1058
+ })
1059
+ }), /* @__PURE__ */ jsx(TooltipContent, { children: `Filter ${label}` })] }), /* @__PURE__ */ jsxs(PopoverContent, {
1060
+ align: "start",
1061
+ className: "max-w-(--radix-popover-content-available-width) min-w-56",
1062
+ children: [/* @__PURE__ */ jsx("div", {
1063
+ className: "flex flex-col gap-3",
1064
+ children: mergedColumns.map((c, index) => /* @__PURE__ */ jsxs("div", {
1065
+ className: cn(index > 0 && "border-t border-border pt-3"),
1066
+ children: [mergedColumns.length > 1 && /* @__PURE__ */ jsx(Label, {
1067
+ className: "mb-1.5 block text-xs text-muted-foreground",
1068
+ children: columnLabel(c)
1069
+ }), /* @__PURE__ */ jsx(ColumnFilterField, {
1070
+ column: c,
1071
+ value: deferred ? pendingValues[c.id] : c.getFilterValue(),
1072
+ onValueChange: (value) => deferred ? setPendingValues((prev) => ({
1073
+ ...prev,
1074
+ [c.id]: value
1075
+ })) : c.setFilterValue(value),
1076
+ showFilterCounts,
1077
+ stickyFilterSelectAll
1078
+ })]
1079
+ }, c.id))
1080
+ }), deferred && /* @__PURE__ */ jsx(FilterPopoverFooter, {
1081
+ onCancel: () => setOpen(false),
1082
+ onApply: () => {
1083
+ mergedColumns.forEach((c) => c.setFilterValue(pendingValues[c.id]));
1084
+ setOpen(false);
1085
+ },
1086
+ applyDisabled: mergedColumns.every((c) => filterDraftUnchanged(c.getFilterValue(), pendingValues[c.id]))
1087
+ })]
1088
+ })]
1089
+ })
1090
+ });
1091
+ }
1092
+ /** A pinned/always-visible filter, shown as a labeled button in the toolbar next to search. */
1093
+ function InlineColumnFilter({ column, size, tableDeferred, showFilterCounts, stickyFilterSelectAll }) {
1094
+ const label = columnLabel(column);
1095
+ const filterVariant = column.columnDef.meta?.filterVariant;
1096
+ const deferred = isColumnDeferred(column, !!tableDeferred);
1097
+ const [open, setOpenState] = React.useState(false);
1098
+ const [pendingValue, setPendingValue] = React.useState(void 0);
1099
+ const setOpen = (value) => {
1100
+ if (value) setPendingValue(column.getFilterValue());
1101
+ setOpenState(value);
1102
+ };
1103
+ if (column.columnDef.meta?.filterRender && !deferred) return column.columnDef.meta.filterRender({
1104
+ column,
1105
+ value: column.getFilterValue(),
1106
+ onValueChange: (value) => column.setFilterValue(value)
1107
+ });
1108
+ if (column.columnDef.meta?.filterOptionsLoading) return /* @__PURE__ */ jsx(FilterOptionsSkeleton, {});
1109
+ if (filterVariant === "select" || isBucketFilterVariant(column)) {
1110
+ const options = resolveFilterOptions(column, showFilterCounts);
1111
+ const committed = column.getFilterValue() ?? [];
1112
+ const effectiveDeferred = deferred && open;
1113
+ const selected = effectiveDeferred ? pendingValue ?? [] : committed;
1114
+ return /* @__PURE__ */ jsxs(SelectMultiple, {
1115
+ size,
1116
+ value: selected,
1117
+ onValueChange: (next) => {
1118
+ const value = next.length ? next : void 0;
1119
+ if (effectiveDeferred) setPendingValue(value);
1120
+ else column.setFilterValue(value);
1121
+ },
1122
+ ...deferred ? {
1123
+ open,
1124
+ onOpenChange: setOpen
1125
+ } : {},
1126
+ children: [/* @__PURE__ */ jsx(SelectMultipleTrigger, {
1127
+ placeholder: label,
1128
+ inlineLabel: label,
1129
+ maxLabelChars: 12
1130
+ }), /* @__PURE__ */ jsxs(SelectMultipleContent, { children: [/* @__PURE__ */ jsx(SelectMultipleOptionsContent, {
1131
+ options,
1132
+ value: selected,
1133
+ onValueChangeAll: (next) => {
1134
+ if (effectiveDeferred) setPendingValue(next);
1135
+ else column.setFilterValue(next);
1136
+ },
1137
+ stickySelectAll: stickyFilterSelectAll
1138
+ }), deferred && /* @__PURE__ */ jsxs(SelectMultipleFooter, { children: [/* @__PURE__ */ jsx(Button, {
1139
+ type: "button",
1140
+ variant: "ghost",
1141
+ size: "xs",
1142
+ onClick: () => setOpen(false),
1143
+ children: "Cancel"
1144
+ }), /* @__PURE__ */ jsx(Button, {
1145
+ type: "button",
1146
+ size: "xs",
1147
+ disabled: filterDraftUnchanged(column.getFilterValue(), pendingValue),
1148
+ onClick: () => {
1149
+ column.setFilterValue(pendingValue);
1150
+ setOpen(false);
1151
+ },
1152
+ children: "Apply"
1153
+ })] })] })]
1154
+ });
1155
+ }
1156
+ if (filterVariant === "singleSelect") {
1157
+ const options = resolveFilterOptions(column, showFilterCounts);
1158
+ const selected = column.getFilterValue() ?? "";
1159
+ return /* @__PURE__ */ jsxs(SelectMultiple, {
1160
+ size,
1161
+ closeOnSelect: true,
1162
+ value: selected ? [selected] : [],
1163
+ onValueChange: (next) => column.setFilterValue(next.at(-1)),
1164
+ children: [/* @__PURE__ */ jsx(SelectMultipleTrigger, {
1165
+ placeholder: label,
1166
+ inlineLabel: label
1167
+ }), /* @__PURE__ */ jsx(SelectMultipleContent, { children: /* @__PURE__ */ jsx(SelectMultipleOptionsContent, {
1168
+ options,
1169
+ value: selected ? [selected] : [],
1170
+ onValueChangeAll: () => {},
1171
+ showSelectAll: false,
1172
+ stickySelectAll: stickyFilterSelectAll
1173
+ }) })]
1174
+ });
1175
+ }
1176
+ return /* @__PURE__ */ jsxs(Popover, {
1177
+ open: deferred ? open : void 0,
1178
+ onOpenChange: deferred ? setOpen : void 0,
1179
+ children: [/* @__PURE__ */ jsx(PopoverTrigger, {
1180
+ asChild: true,
1181
+ children: /* @__PURE__ */ jsxs(Button, {
1182
+ type: "button",
1183
+ variant: "outline",
1184
+ size,
1185
+ className: cn(column.getIsFiltered() && "border-primary text-primary"),
1186
+ children: [label, /* @__PURE__ */ jsx(ChevronDownIcon, {
1187
+ "data-icon": "inline-end",
1188
+ className: "opacity-50"
1189
+ })]
1190
+ })
1191
+ }), /* @__PURE__ */ jsxs(PopoverContent, {
1192
+ align: "start",
1193
+ className: "max-w-(--radix-popover-content-available-width) min-w-56",
1194
+ children: [/* @__PURE__ */ jsx(ColumnFilterField, {
1195
+ column,
1196
+ value: deferred ? pendingValue : column.getFilterValue(),
1197
+ onValueChange: deferred ? setPendingValue : (value) => column.setFilterValue(value),
1198
+ showFilterCounts,
1199
+ stickyFilterSelectAll
1200
+ }), deferred && /* @__PURE__ */ jsx(FilterPopoverFooter, {
1201
+ onCancel: () => setOpen(false),
1202
+ onApply: () => {
1203
+ column.setFilterValue(pendingValue);
1204
+ setOpen(false);
1205
+ },
1206
+ applyDisabled: filterDraftUnchanged(column.getFilterValue(), pendingValue)
1207
+ })]
1208
+ })]
1209
+ });
1210
+ }
1211
+ function DataTable({ columns: userColumns, data, loading = false, error, errorDescription, emptyTitle = "No results", emptyDescription = "Try adjusting your search.", searchPlaceholder = "Search...", searchableColumnIds, searchMatch = "contains", filterApplyMode = "immediate", defaultSorting, defaultColumnFilters, defaultGlobalFilter, defaultPageIndex, defaultColumnVisibility, defaultColumnOrder, defaultColumnPinning, defaultColumnSizing, defaultExpanded, onExpandedChange, tableRef, onColumnLayoutChange, defaultPinnedFilterIds, allowFilterPinning = true, showFilterCounts = false, stickyFilterSelectAll = false, clearFiltersOnHide = false, enableRowSelection = false, selectionScope = "filtered", maxSelectedRows, defaultRowSelection, selectionBarPlacement, onRowSelectionChange, onStateChange, renderBulkActions, onRowClick, renderExpandedRow, getRowId, lockedColumnIds = [], enableColumnResizing = false, enableMultiSort = false, revealColumnControlsOnHover = true, striped = false, cellDividers = false, aiAction, actionButtons, aiRowPredicate, aiColumnIds, aiCellPredicate, pageSize = 10, pageSizeOptions = [
1212
+ 10,
1213
+ 20,
1214
+ 30
1215
+ ], showPageSizeSelector = true, paginationStyle = "compact", onLoadMore, loadMoreOffset = 200, hasMore = false, showColumnToggle = true, loadingMore = false, maxHeight, stickyHeader = true, caption, captionHidden = true, size = "default", className, "aria-label": ariaLabel }) {
1216
+ React.useEffect(() => {
1217
+ if (process.env.NODE_ENV === "production") return;
1218
+ if (actionButtons && actionButtons.length > 3) console.warn(`DataTable: \`actionButtons\` has ${actionButtons.length} entries, but only the first 3 render in the toolbar — the rest are silently dropped. Move the overflow into a dropdown of your own, or trim the list.`);
1219
+ if (typeof error !== "boolean" && error != null) console.warn("DataTable: passing a string/node to `error` is deprecated — put that copy in `errorDescription` instead and pass `error={true}`. `error` will stop accepting anything but a boolean in a future version.");
1220
+ if (!ariaLabel && !caption) console.warn("DataTable: no `aria-label` or `caption` provided — the table has no accessible name, so assistive tech has nothing to announce it by. Pass one describing what the table shows.");
1221
+ if (userColumns.some((col) => col.meta?.filterVariant === "range")) console.warn("DataTable: `filterVariant: \"range\"` is deprecated — use `\"bucket\"` instead (same behavior, clearer name). `\"range\"` will stop being accepted in a future version.");
1222
+ if (userColumns.some((col) => col.meta?.rangeBucketSize != null)) console.warn("DataTable: `meta.rangeBucketSize` is deprecated — use `meta.bucketSize` instead (same option, renamed to match `filterVariant: \"bucket\"`).");
1223
+ }, [
1224
+ actionButtons,
1225
+ error,
1226
+ ariaLabel,
1227
+ caption
1228
+ ]);
1229
+ const [sorting, setSorting] = React.useState(defaultSorting ?? []);
1230
+ const [columnVisibility, setColumnVisibility] = React.useState(defaultColumnVisibility ?? {});
1231
+ const [columnOrder, setColumnOrder] = React.useState(defaultColumnOrder ?? []);
1232
+ const [columnPinning, setColumnPinning] = React.useState(() => defaultColumnPinning ?? derivePinningFromMeta(userColumns));
1233
+ const [columnSizing, setColumnSizing] = React.useState(defaultColumnSizing ?? {});
1234
+ const tableContainerRef = React.useRef(null);
1235
+ const bulkActionsBarRef = React.useRef(null);
1236
+ const [bulkActionsBarHeight, setBulkActionsBarHeight] = React.useState(0);
1237
+ const mainColumnRef = React.useRef(null);
1238
+ const footerRef = React.useRef(null);
1239
+ const [columnSearchQuery, setColumnSearchQuery] = React.useState("");
1240
+ const [filtersPaneOpen, setFiltersPaneOpen] = React.useState(false);
1241
+ const [columnsMenuOpen, setColumnsMenuOpen] = React.useState(false);
1242
+ const columnsMenuSnapshotRef = React.useRef(null);
1243
+ const columnsMenuAppliedRef = React.useRef(false);
1244
+ const [paginationState, setPaginationState] = React.useState({
1245
+ pageSize,
1246
+ pageIndex: defaultPageIndex ?? 0
1247
+ });
1248
+ const [pageRestorePending, setPageRestorePending] = React.useState(Boolean(defaultPageIndex) && data.length === 0);
1249
+ React.useEffect(() => {
1250
+ if (pageRestorePending && data.length > 0) setPageRestorePending(false);
1251
+ }, [pageRestorePending, data.length]);
1252
+ const [rowSelection, setRowSelection] = React.useState(defaultRowSelection ?? {});
1253
+ const atSelectionCap = maxSelectedRows != null && Object.keys(rowSelection).length >= maxSelectedRows;
1254
+ const effectiveEnableRowSelection = React.useCallback((row) => {
1255
+ if (!(typeof enableRowSelection === "function" ? enableRowSelection(row) : enableRowSelection)) return false;
1256
+ return !atSelectionCap || row.getIsSelected();
1257
+ }, [enableRowSelection, atSelectionCap]);
1258
+ const [columnFilters, setColumnFilters] = React.useState(defaultColumnFilters ?? []);
1259
+ const filtersPaneRef = React.useRef(null);
1260
+ const filtersPaneId = React.useId();
1261
+ const filtersPaneHeadingId = React.useId();
1262
+ const filtersPaneTriggerRef = React.useRef(null);
1263
+ const filtersPaneCloseButtonRef = React.useRef(null);
1264
+ const filtersPaneWasOpenRef = React.useRef(false);
1265
+ React.useEffect(() => {
1266
+ if (filtersPaneOpen && !filtersPaneWasOpenRef.current) filtersPaneCloseButtonRef.current?.focus();
1267
+ else if (!filtersPaneOpen && filtersPaneWasOpenRef.current) filtersPaneTriggerRef.current?.focus();
1268
+ filtersPaneWasOpenRef.current = filtersPaneOpen;
1269
+ }, [filtersPaneOpen]);
1270
+ const [filtersPaneEdgeAdjust, setFiltersPaneEdgeAdjust] = React.useState(null);
1271
+ const [expanded, setExpanded] = React.useState(defaultExpanded ?? {});
1272
+ React.useEffect(() => {
1273
+ onExpandedChange?.(expanded);
1274
+ }, [expanded]);
1275
+ const [globalFilter, setGlobalFilter] = React.useState(defaultGlobalFilter ?? "");
1276
+ const [searchInput, setSearchInput] = React.useState(defaultGlobalFilter ?? "");
1277
+ const debouncedSetGlobalFilter = useDebouncedCallback(setGlobalFilter, 200);
1278
+ const [pinnedFilterIds, setPinnedFilterIds] = React.useState(() => {
1279
+ if (defaultPinnedFilterIds) return defaultPinnedFilterIds;
1280
+ const mergedAwayIds = new Set(userColumns.flatMap((col) => col.meta?.filterMergeWith ?? []));
1281
+ return userColumns.filter((col) => col.enableColumnFilter !== false && !col.meta?.filterOnly && !mergedAwayIds.has(getColumnId(col) ?? "")).slice(0, 2).map(getColumnId).filter((id) => !!id);
1282
+ });
1283
+ function toggleFilterPin(columnId) {
1284
+ setPinnedFilterIds((prev) => prev.includes(columnId) ? prev.filter((id) => id !== columnId) : [...prev, columnId]);
1285
+ }
1286
+ const searchableIds = React.useMemo(() => {
1287
+ if (searchableColumnIds === "all") return userColumns.filter((col) => "accessorKey" in col || "accessorFn" in col).map(getColumnId).filter((id) => !!id);
1288
+ return searchableColumnIds ?? userColumns.slice(0, 2).map(getColumnId).filter((id) => !!id);
1289
+ }, [searchableColumnIds, userColumns]);
1290
+ const columns = React.useMemo(() => {
1291
+ const processedUserColumns = userColumns.map((col) => {
1292
+ const id = getColumnId(col);
1293
+ const isSearchable = id !== void 0 && searchableIds.includes(id);
1294
+ const isLocked = id !== void 0 && lockedColumnIds.includes(id);
1295
+ const isFilterOnly = !!col.meta?.filterOnly;
1296
+ const next = {
1297
+ ...col,
1298
+ enableGlobalFilter: col.enableGlobalFilter ?? isSearchable,
1299
+ enableColumnFilter: col.enableColumnFilter ?? true,
1300
+ enableHiding: isLocked || isFilterOnly ? false : col.enableHiding,
1301
+ minSize: col.minSize ?? computeColumnMinSize(col, size),
1302
+ maxSize: col.maxSize ?? (col.size && col.size > DEFAULT_MAX_COLUMN_SIZE ? void 0 : DEFAULT_MAX_COLUMN_SIZE),
1303
+ filterFn: col.filterFn ?? (col.meta?.filterVariant === "select" ? selectFilterFn : col.meta?.filterVariant === "range" || col.meta?.filterVariant === "bucket" ? rangeFilterFn : col.meta?.filterVariant === "singleSelect" ? singleSelectFilterFn : "includesString")
1304
+ };
1305
+ if (!col.cell && isSearchable) next.cell = (info) => /* @__PURE__ */ jsx(HighlightMatch, {
1306
+ text: String(info.getValue() ?? ""),
1307
+ query: String(info.table.getState().globalFilter ?? ""),
1308
+ startsWith: searchMatch === "startsWith"
1309
+ });
1310
+ return next;
1311
+ });
1312
+ const leadingColumns = [];
1313
+ if (aiRowPredicate) leadingColumns.push({
1314
+ id: AI_COLUMN_ID,
1315
+ header: () => /* @__PURE__ */ jsx("span", {
1316
+ className: "sr-only",
1317
+ children: "AI"
1318
+ }),
1319
+ cell: ({ row }) => aiRowPredicate(row.original) ? /* @__PURE__ */ jsxs(AILabel, { children: [/* @__PURE__ */ jsx(AIMarkerTrigger, {
1320
+ size,
1321
+ ariaLabel: "AI-suggested row"
1322
+ }), /* @__PURE__ */ jsxs(AILabelContent, { children: [/* @__PURE__ */ jsx(PopoverHeader, { children: /* @__PURE__ */ jsx(PopoverTitle, { children: "Why you're seeing this" }) }), /* @__PURE__ */ jsx(PopoverDescription, { children: "This row's values are generated or suggested by AI." })] })] }) : null,
1323
+ enableSorting: false,
1324
+ enableHiding: false,
1325
+ enableGlobalFilter: false,
1326
+ enableColumnFilter: false,
1327
+ enablePinning: false,
1328
+ enableResizing: false,
1329
+ size: 56
1330
+ });
1331
+ if (renderExpandedRow) leadingColumns.push({
1332
+ id: EXPAND_COLUMN_ID,
1333
+ header: () => /* @__PURE__ */ jsx("span", {
1334
+ className: "sr-only",
1335
+ children: "Expand"
1336
+ }),
1337
+ cell: ({ row }) => row.getCanExpand() ? /* @__PURE__ */ jsx(Button, {
1338
+ type: "button",
1339
+ variant: "ghost",
1340
+ size: "xs",
1341
+ iconOnly: true,
1342
+ "aria-label": row.getIsExpanded() ? "Collapse row" : "Expand row",
1343
+ "aria-expanded": row.getIsExpanded(),
1344
+ "aria-controls": `data-table-expanded-${row.id}`,
1345
+ onClick: (event) => {
1346
+ event.stopPropagation();
1347
+ row.toggleExpanded();
1348
+ },
1349
+ children: /* @__PURE__ */ jsx(ChevronRightIcon, { className: cn("transition-transform", row.getIsExpanded() && "rotate-90") })
1350
+ }) : null,
1351
+ enableSorting: false,
1352
+ enableHiding: false,
1353
+ enableGlobalFilter: false,
1354
+ enableColumnFilter: false,
1355
+ enablePinning: false,
1356
+ enableResizing: false,
1357
+ size: 32
1358
+ });
1359
+ if (enableRowSelection) leadingColumns.push({
1360
+ id: SELECTION_COLUMN_ID,
1361
+ header: ({ table }) => {
1362
+ const state = table.getState().rowSelection;
1363
+ const pageRows = table.getRowModel().rows.filter((row) => typeof enableRowSelection === "function" ? enableRowSelection(row) : !!enableRowSelection);
1364
+ const selectedCount = pageRows.filter((row) => state[row.id]).length;
1365
+ const allSelected = pageRows.length > 0 && selectedCount === pageRows.length;
1366
+ const capReached = maxSelectedRows != null && Object.keys(state).length >= maxSelectedRows;
1367
+ return /* @__PURE__ */ jsx(Checkbox, {
1368
+ className: SELECTION_CHECKBOX_CLASS,
1369
+ "aria-label": onLoadMore ? "Select all rows" : "Select all rows on this page",
1370
+ checked: allSelected ? true : selectedCount > 0 ? "indeterminate" : false,
1371
+ disabled: capReached && !allSelected && selectedCount === 0,
1372
+ title: capReached && !allSelected && selectedCount === 0 ? `Up to ${maxSelectedRows} rows can be selected` : void 0,
1373
+ onCheckedChange: (value) => {
1374
+ const next = { ...state };
1375
+ if (!value || capReached && !allSelected) pageRows.forEach((row) => delete next[row.id]);
1376
+ else {
1377
+ let remaining = maxSelectedRows != null ? Math.max(0, maxSelectedRows - Object.keys(next).length) : Infinity;
1378
+ for (const row of pageRows) {
1379
+ if (next[row.id]) continue;
1380
+ if (remaining <= 0) break;
1381
+ next[row.id] = true;
1382
+ remaining -= 1;
1383
+ }
1384
+ }
1385
+ table.setRowSelection(next);
1386
+ }
1387
+ });
1388
+ },
1389
+ cell: ({ row }) => /* @__PURE__ */ jsx(Checkbox, {
1390
+ className: SELECTION_CHECKBOX_CLASS,
1391
+ "aria-label": "Select row",
1392
+ checked: row.getIsSelected(),
1393
+ disabled: !row.getCanSelect(),
1394
+ title: !row.getCanSelect() && atSelectionCap && !row.getIsSelected() ? `Up to ${maxSelectedRows} rows can be selected` : void 0,
1395
+ onCheckedChange: (value) => row.toggleSelected(!!value),
1396
+ onClick: (event) => event.stopPropagation()
1397
+ }),
1398
+ enableSorting: false,
1399
+ enableHiding: false,
1400
+ enableGlobalFilter: false,
1401
+ enableColumnFilter: false,
1402
+ enablePinning: false,
1403
+ enableResizing: false,
1404
+ size: 40
1405
+ });
1406
+ return [...leadingColumns, ...processedUserColumns];
1407
+ }, [
1408
+ userColumns,
1409
+ enableRowSelection,
1410
+ atSelectionCap,
1411
+ maxSelectedRows,
1412
+ searchableIds,
1413
+ searchMatch,
1414
+ renderExpandedRow,
1415
+ aiRowPredicate,
1416
+ lockedColumnIds,
1417
+ size,
1418
+ onLoadMore
1419
+ ]);
1420
+ const effectiveColumnVisibility = React.useMemo(() => {
1421
+ const forcedHidden = userColumns.reduce((acc, col) => {
1422
+ const id = getColumnId(col);
1423
+ if (id && col.meta?.filterOnly) acc[id] = false;
1424
+ return acc;
1425
+ }, {});
1426
+ return {
1427
+ ...columnVisibility,
1428
+ ...forcedHidden
1429
+ };
1430
+ }, [columnVisibility, userColumns]);
1431
+ const effectiveColumnPinning = React.useMemo(() => {
1432
+ const leadingIds = [
1433
+ aiRowPredicate && AI_COLUMN_ID,
1434
+ renderExpandedRow && EXPAND_COLUMN_ID,
1435
+ enableRowSelection && SELECTION_COLUMN_ID
1436
+ ].filter((id) => !!id);
1437
+ const left = columnPinning.left ?? [];
1438
+ if (leadingIds.length === 0 || left.length === 0) return columnPinning;
1439
+ return {
1440
+ ...columnPinning,
1441
+ left: [...leadingIds, ...left.filter((id) => !leadingIds.includes(id))]
1442
+ };
1443
+ }, [
1444
+ columnPinning,
1445
+ aiRowPredicate,
1446
+ renderExpandedRow,
1447
+ enableRowSelection
1448
+ ]);
1449
+ const needsFacetedFiltering = userColumns.some((col) => col.meta?.filterVariant === "select" || col.meta?.filterVariant === "singleSelect" || col.meta?.filterVariant === "range" || col.meta?.filterVariant === "bucket" || col.meta?.filterRender);
1450
+ const needsFacetedMinMax = userColumns.some((col) => col.meta?.filterRender);
1451
+ const table = useReactTable({
1452
+ data,
1453
+ columns,
1454
+ getRowId,
1455
+ state: {
1456
+ sorting,
1457
+ columnVisibility: effectiveColumnVisibility,
1458
+ columnOrder,
1459
+ columnPinning: effectiveColumnPinning,
1460
+ columnSizing,
1461
+ rowSelection,
1462
+ columnFilters,
1463
+ expanded,
1464
+ globalFilter,
1465
+ pagination: paginationState
1466
+ },
1467
+ onSortingChange: setSorting,
1468
+ onColumnVisibilityChange: (updater) => {
1469
+ setColumnVisibility((old) => {
1470
+ const next = typeof updater === "function" ? updater(old) : updater;
1471
+ if (clearFiltersOnHide) {
1472
+ const newlyHidden = Object.keys(next).filter((id) => next[id] === false && old[id] !== false);
1473
+ if (newlyHidden.length > 0) setColumnFilters((filters) => filters.filter((filter) => !newlyHidden.includes(filter.id)));
1474
+ }
1475
+ return next;
1476
+ });
1477
+ },
1478
+ onColumnOrderChange: setColumnOrder,
1479
+ onColumnPinningChange: setColumnPinning,
1480
+ onColumnSizingChange: setColumnSizing,
1481
+ onRowSelectionChange: setRowSelection,
1482
+ onColumnFiltersChange: setColumnFilters,
1483
+ onExpandedChange: setExpanded,
1484
+ onGlobalFilterChange: setGlobalFilter,
1485
+ enableRowSelection: effectiveEnableRowSelection,
1486
+ getRowCanExpand: renderExpandedRow ? () => true : void 0,
1487
+ getCoreRowModel: getCoreRowModel(),
1488
+ getSortedRowModel: getSortedRowModel(),
1489
+ globalFilterFn: searchMatch === "startsWith" ? startsWithWordFilterFn : "auto",
1490
+ getFilteredRowModel: getFilteredRowModel(),
1491
+ getPaginationRowModel: onLoadMore ? void 0 : getPaginationRowModel(),
1492
+ getFacetedRowModel: needsFacetedFiltering || needsFacetedMinMax ? getFacetedRowModel() : void 0,
1493
+ getFacetedUniqueValues: needsFacetedFiltering ? getFacetedUniqueValues() : void 0,
1494
+ getFacetedMinMaxValues: needsFacetedMinMax ? getFacetedMinMaxValues() : void 0,
1495
+ getExpandedRowModel: getExpandedRowModel(),
1496
+ initialState: { pagination: {
1497
+ pageSize,
1498
+ pageIndex: 0
1499
+ } },
1500
+ onPaginationChange: setPaginationState,
1501
+ autoResetPageIndex: pageRestorePending ? false : void 0,
1502
+ enableColumnResizing,
1503
+ columnResizeMode: "onChange",
1504
+ enableMultiSort,
1505
+ meta: { getColumnCellElements: (columnId) => {
1506
+ const root = tableContainerRef.current;
1507
+ if (!root) return [];
1508
+ return Array.from(root.querySelectorAll(`[data-column-id="${columnId}"]`));
1509
+ } }
1510
+ });
1511
+ React.useEffect(() => {
1512
+ if (!tableRef) return;
1513
+ tableRef(table);
1514
+ return () => tableRef(null);
1515
+ }, [tableRef, table]);
1516
+ const clampPageCount = onLoadMore ? 0 : table.getPageCount();
1517
+ const clampPageIndex = paginationState.pageIndex;
1518
+ React.useEffect(() => {
1519
+ if (clampPageCount > 0 && clampPageIndex >= clampPageCount) setPaginationState((prev) => ({
1520
+ ...prev,
1521
+ pageIndex: clampPageCount - 1
1522
+ }));
1523
+ }, [clampPageCount, clampPageIndex]);
1524
+ const previousPageSizeRef = React.useRef(pageSize);
1525
+ React.useEffect(() => {
1526
+ if (previousPageSizeRef.current !== pageSize) {
1527
+ previousPageSizeRef.current = pageSize;
1528
+ table.setPageSize(pageSize);
1529
+ }
1530
+ }, [pageSize, table]);
1531
+ const isResizingColumn = table.getState().columnSizingInfo.isResizingColumn;
1532
+ const previousIsResizingColumnRef = React.useRef(isResizingColumn);
1533
+ React.useEffect(() => {
1534
+ if (previousIsResizingColumnRef.current && !isResizingColumn) onColumnLayoutChange?.({
1535
+ visibility: columnVisibility,
1536
+ order: columnOrder,
1537
+ pinning: columnPinning,
1538
+ sizing: columnSizing
1539
+ });
1540
+ previousIsResizingColumnRef.current = isResizingColumn;
1541
+ }, [
1542
+ isResizingColumn,
1543
+ columnVisibility,
1544
+ columnOrder,
1545
+ columnPinning,
1546
+ columnSizing,
1547
+ onColumnLayoutChange
1548
+ ]);
1549
+ const pagination = table.getState().pagination;
1550
+ React.useEffect(() => {
1551
+ onStateChange?.({
1552
+ sorting,
1553
+ columnFilters,
1554
+ globalFilter,
1555
+ pagination,
1556
+ expanded,
1557
+ pinnedFilterIds
1558
+ });
1559
+ }, [
1560
+ sorting,
1561
+ columnFilters,
1562
+ globalFilter,
1563
+ pagination,
1564
+ expanded,
1565
+ pinnedFilterIds
1566
+ ]);
1567
+ const lockedIds = React.useMemo(() => /* @__PURE__ */ new Set([...INJECTED_COLUMN_IDS, ...lockedColumnIds]), [lockedColumnIds]);
1568
+ const [draggedColumnId, setDraggedColumnId] = React.useState(null);
1569
+ const [dragOverColumnId, setDragOverColumnId] = React.useState(null);
1570
+ const orderedRealColumns = React.useMemo(() => {
1571
+ const leaf = table.getAllLeafColumns().filter((column) => !INJECTED_COLUMN_IDS.has(column.id));
1572
+ if (!columnOrder.length) return leaf;
1573
+ const rank = new Map(columnOrder.map((id, index) => [id, index]));
1574
+ return [...leaf].sort((a, b) => (rank.get(a.id) ?? 0) - (rank.get(b.id) ?? 0));
1575
+ }, [table, columnOrder]);
1576
+ const manageableColumns = React.useMemo(() => orderedRealColumns.filter((column) => !column.columnDef.meta?.filterOnly), [orderedRealColumns]);
1577
+ function columnSection(columnId) {
1578
+ if (columnPinning.left?.includes(columnId)) return "left";
1579
+ if (columnPinning.right?.includes(columnId)) return "right";
1580
+ return "center";
1581
+ }
1582
+ const manageableColumnsBySection = React.useMemo(() => {
1583
+ const byId = new Map(manageableColumns.map((column) => [column.id, column]));
1584
+ const toColumns = (ids) => (ids ?? []).map((id) => byId.get(id)).filter((column) => column !== void 0);
1585
+ const left = toColumns(columnPinning.left);
1586
+ const right = toColumns(columnPinning.right);
1587
+ const pinnedIds = /* @__PURE__ */ new Set([...columnPinning.left ?? [], ...columnPinning.right ?? []]);
1588
+ return {
1589
+ left,
1590
+ center: manageableColumns.filter((column) => !pinnedIds.has(column.id)),
1591
+ right
1592
+ };
1593
+ }, [manageableColumns, columnPinning]);
1594
+ function computeReorderedIds(draggedId, targetId) {
1595
+ if (!draggedId || !targetId || draggedId === targetId) return null;
1596
+ if (lockedIds.has(draggedId) || lockedIds.has(targetId)) return null;
1597
+ const section = columnSection(draggedId);
1598
+ if (section !== columnSection(targetId)) return null;
1599
+ const order = manageableColumnsBySection[section].map((column) => column.id);
1600
+ const fromIndex = order.indexOf(draggedId);
1601
+ const toIndex = order.indexOf(targetId);
1602
+ if (fromIndex === -1 || toIndex === -1) return null;
1603
+ const next = [...order];
1604
+ next.splice(fromIndex, 1);
1605
+ next.splice(toIndex, 0, draggedId);
1606
+ return {
1607
+ section,
1608
+ order: next
1609
+ };
1610
+ }
1611
+ function dropInsertSide(draggedId, targetId) {
1612
+ if (!draggedId || !targetId || draggedId === targetId) return null;
1613
+ if (lockedIds.has(draggedId) || lockedIds.has(targetId)) return null;
1614
+ const section = columnSection(draggedId);
1615
+ if (section !== columnSection(targetId)) return null;
1616
+ const order = manageableColumnsBySection[section].map((column) => column.id);
1617
+ const fromIndex = order.indexOf(draggedId);
1618
+ const toIndex = order.indexOf(targetId);
1619
+ if (fromIndex === -1 || toIndex === -1) return null;
1620
+ return fromIndex < toIndex ? "after" : "before";
1621
+ }
1622
+ const previewReorder = React.useMemo(() => {
1623
+ if (!draggedColumnId || !dragOverColumnId) return null;
1624
+ return computeReorderedIds(draggedColumnId, dragOverColumnId);
1625
+ }, [
1626
+ draggedColumnId,
1627
+ dragOverColumnId,
1628
+ manageableColumnsBySection,
1629
+ lockedIds
1630
+ ]);
1631
+ function displayedSection(section) {
1632
+ const base = manageableColumnsBySection[section];
1633
+ if (!previewReorder || previewReorder.section !== section) return base;
1634
+ const byId = new Map(base.map((column) => [column.id, column]));
1635
+ return previewReorder.order.map((id) => byId.get(id)).filter((column) => column !== void 0);
1636
+ }
1637
+ const displayedLeft = displayedSection("left");
1638
+ const displayedCenter = displayedSection("center");
1639
+ const displayedRight = displayedSection("right");
1640
+ const displayedColumns = [
1641
+ ...displayedLeft,
1642
+ ...displayedCenter,
1643
+ ...displayedRight
1644
+ ];
1645
+ const derivedDefaultColumnPinning = React.useMemo(() => derivePinningFromMeta(userColumns), [userColumns]);
1646
+ const effectiveDefaultColumnPinning = defaultColumnPinning ?? derivedDefaultColumnPinning;
1647
+ const effectiveDefaultColumnVisibility = defaultColumnVisibility ?? {};
1648
+ const effectiveDefaultColumnOrder = defaultColumnOrder ?? [];
1649
+ const effectiveDefaultColumnSizing = defaultColumnSizing ?? {};
1650
+ const hasColumnCustomization = !idListsEqual(columnOrder, effectiveDefaultColumnOrder) || !visibilityEqual(columnVisibility, effectiveDefaultColumnVisibility) || !idListsEqual(columnPinning.left, effectiveDefaultColumnPinning.left) || !idListsEqual(columnPinning.right, effectiveDefaultColumnPinning.right) || !sizingEqual(columnSizing, effectiveDefaultColumnSizing);
1651
+ const columnsMenuSnapshot = columnsMenuSnapshotRef.current;
1652
+ const hasPendingColumnsMenuChanges = columnsMenuSnapshot ? !visibilityEqual(columnVisibility, columnsMenuSnapshot.visibility) || !idListsEqual(columnOrder, columnsMenuSnapshot.order) || !idListsEqual(columnPinning.left, columnsMenuSnapshot.pinning.left) || !idListsEqual(columnPinning.right, columnsMenuSnapshot.pinning.right) : false;
1653
+ function commitColumnOrder(centerOrder) {
1654
+ const leadingIds = table.getAllLeafColumns().filter((column) => INJECTED_COLUMN_IDS.has(column.id)).map((column) => column.id);
1655
+ const pinnedIds = [...columnPinning.left ?? [], ...columnPinning.right ?? []];
1656
+ setColumnOrder([
1657
+ ...leadingIds,
1658
+ ...centerOrder,
1659
+ ...pinnedIds
1660
+ ]);
1661
+ }
1662
+ function commitPinOrder(section, order) {
1663
+ setColumnPinning((old) => ({
1664
+ ...old,
1665
+ [section]: order
1666
+ }));
1667
+ }
1668
+ function commitSectionOrder(section, order) {
1669
+ if (section === "center") commitColumnOrder(order);
1670
+ else commitPinOrder(section, order);
1671
+ }
1672
+ function findMoveTargetIndex(columnId, direction) {
1673
+ const section = columnSection(columnId);
1674
+ const order = manageableColumnsBySection[section].map((column) => column.id);
1675
+ const index = order.indexOf(columnId);
1676
+ if (index === -1) return null;
1677
+ let targetIndex = direction === "up" ? index - 1 : index + 1;
1678
+ while (targetIndex >= 0 && targetIndex < order.length && lockedIds.has(order[targetIndex])) targetIndex += direction === "up" ? -1 : 1;
1679
+ return targetIndex >= 0 && targetIndex < order.length ? targetIndex : null;
1680
+ }
1681
+ function canMoveColumn(columnId, direction) {
1682
+ return findMoveTargetIndex(columnId, direction) !== null;
1683
+ }
1684
+ function moveColumn(columnId, direction) {
1685
+ const section = columnSection(columnId);
1686
+ const order = manageableColumnsBySection[section].map((column) => column.id);
1687
+ const index = order.indexOf(columnId);
1688
+ const targetIndex = findMoveTargetIndex(columnId, direction);
1689
+ if (index === -1 || targetIndex === null) return;
1690
+ const next = [...order];
1691
+ next.splice(index, 1);
1692
+ next.splice(targetIndex, 0, columnId);
1693
+ commitSectionOrder(section, next);
1694
+ const newPosition = (section === "left" ? [] : section === "center" ? manageableColumnsBySection.left : [...manageableColumnsBySection.left, ...manageableColumnsBySection.center]).length + targetIndex + 1;
1695
+ const column = manageableColumnsBySection[section].find((c) => c.id === columnId);
1696
+ if (column) setLiveAnnouncement(`${columnLabel(column)} moved ${direction === "up" ? "left" : "right"}, now column ${newPosition} of ${manageableColumns.length}`);
1697
+ }
1698
+ function resetColumns() {
1699
+ setColumnOrder(effectiveDefaultColumnOrder);
1700
+ setColumnVisibility(effectiveDefaultColumnVisibility);
1701
+ setColumnPinning(effectiveDefaultColumnPinning);
1702
+ setColumnSizing(effectiveDefaultColumnSizing);
1703
+ onColumnLayoutChange?.({
1704
+ visibility: effectiveDefaultColumnVisibility,
1705
+ order: effectiveDefaultColumnOrder,
1706
+ pinning: effectiveDefaultColumnPinning,
1707
+ sizing: effectiveDefaultColumnSizing
1708
+ });
1709
+ }
1710
+ function handleColumnsMenuOpenChange(open) {
1711
+ if (open) {
1712
+ columnsMenuSnapshotRef.current = {
1713
+ visibility: columnVisibility,
1714
+ order: columnOrder,
1715
+ pinning: columnPinning
1716
+ };
1717
+ columnsMenuAppliedRef.current = false;
1718
+ } else {
1719
+ if (!columnsMenuAppliedRef.current && columnsMenuSnapshotRef.current) {
1720
+ setColumnVisibility(columnsMenuSnapshotRef.current.visibility);
1721
+ setColumnOrder(columnsMenuSnapshotRef.current.order);
1722
+ setColumnPinning(columnsMenuSnapshotRef.current.pinning);
1723
+ }
1724
+ setColumnSearchQuery("");
1725
+ }
1726
+ setColumnsMenuOpen(open);
1727
+ }
1728
+ React.useLayoutEffect(() => {
1729
+ if (!filtersPaneOpen) {
1730
+ setFiltersPaneEdgeAdjust(null);
1731
+ return;
1732
+ }
1733
+ const pane = filtersPaneRef.current;
1734
+ const surface = pane?.closest("[data-slot=\"card\"]");
1735
+ if (!pane || !surface) {
1736
+ setFiltersPaneEdgeAdjust(null);
1737
+ return;
1738
+ }
1739
+ const paneRect = pane.getBoundingClientRect();
1740
+ const surfaceRect = surface.getBoundingClientRect();
1741
+ const topDelta = Math.max(0, paneRect.top - surfaceRect.top);
1742
+ const bottomDelta = Math.max(0, surfaceRect.bottom - paneRect.bottom);
1743
+ if (topDelta === 0 && bottomDelta === 0) {
1744
+ setFiltersPaneEdgeAdjust(null);
1745
+ return;
1746
+ }
1747
+ setFiltersPaneEdgeAdjust({
1748
+ marginTop: -topDelta,
1749
+ height: paneRect.height + topDelta + bottomDelta
1750
+ });
1751
+ }, [filtersPaneOpen]);
1752
+ const columnRowRefs = React.useRef(/* @__PURE__ */ new Map());
1753
+ const columnRowRefCallbacks = React.useRef(/* @__PURE__ */ new Map());
1754
+ const columnRowRectsRef = React.useRef(/* @__PURE__ */ new Map());
1755
+ const pendingFrameRef = React.useRef(null);
1756
+ const dragOverDebounceRef = React.useRef(null);
1757
+ const displayedOrderKey = displayedColumns.map((column) => column.id).join(",");
1758
+ function getColumnRowRef(id) {
1759
+ let callback = columnRowRefCallbacks.current.get(id);
1760
+ if (!callback) {
1761
+ callback = (element) => {
1762
+ if (element) columnRowRefs.current.set(id, element);
1763
+ else columnRowRefs.current.delete(id);
1764
+ };
1765
+ columnRowRefCallbacks.current.set(id, callback);
1766
+ }
1767
+ return callback;
1768
+ }
1769
+ React.useLayoutEffect(() => {
1770
+ const previousRects = columnRowRectsRef.current;
1771
+ if (pendingFrameRef.current !== null) {
1772
+ cancelAnimationFrame(pendingFrameRef.current);
1773
+ pendingFrameRef.current = null;
1774
+ }
1775
+ columnRowRefs.current.forEach((element) => {
1776
+ element.style.transition = "none";
1777
+ element.style.transform = "";
1778
+ });
1779
+ const nextRects = /* @__PURE__ */ new Map();
1780
+ columnRowRefs.current.forEach((element, id) => {
1781
+ nextRects.set(id, element.getBoundingClientRect());
1782
+ });
1783
+ const toAnimate = [];
1784
+ columnRowRefs.current.forEach((element, id) => {
1785
+ const previous = previousRects.get(id);
1786
+ const next = nextRects.get(id);
1787
+ if (!previous || !next) return;
1788
+ const deltaY = previous.top - next.top;
1789
+ if (Math.abs(deltaY) < .5) return;
1790
+ element.style.transform = `translateY(${deltaY}px)`;
1791
+ toAnimate.push(element);
1792
+ });
1793
+ if (toAnimate.length > 0) {
1794
+ toAnimate.forEach((element) => void element.getBoundingClientRect());
1795
+ pendingFrameRef.current = requestAnimationFrame(() => {
1796
+ toAnimate.forEach((element) => {
1797
+ element.style.transition = "transform 180ms ease";
1798
+ element.style.transform = "";
1799
+ });
1800
+ pendingFrameRef.current = null;
1801
+ });
1802
+ }
1803
+ columnRowRectsRef.current = nextRects;
1804
+ }, [displayedOrderKey]);
1805
+ const { filterableColumns, showAllFiltersInline, inlineFilterColumns, paneFilterCount } = React.useMemo(() => {
1806
+ const mergedAwayFilterIds = new Set(userColumns.flatMap((col) => {
1807
+ const hostId = getColumnId(col);
1808
+ return !hostId || effectiveColumnVisibility[hostId] !== false ? col.meta?.filterMergeWith ?? [] : [];
1809
+ }));
1810
+ const withFilterOrderPositions = orderedRealColumns.filter((column) => column.getCanFilter() && !mergedAwayFilterIds.has(column.id)).map((column, index) => ({
1811
+ column,
1812
+ order: column.columnDef.meta?.filterOrder ?? index
1813
+ }));
1814
+ withFilterOrderPositions.sort((a, b) => a.order - b.order);
1815
+ const filterableColumns = withFilterOrderPositions.map((entry) => entry.column);
1816
+ const hasFilterOnlyColumn = filterableColumns.some((column) => !!column.columnDef.meta?.filterOnly);
1817
+ const showAllFiltersInline = !hasFilterOnlyColumn && filterableColumns.length <= 3;
1818
+ const inlineFilterColumns = showAllFiltersInline ? filterableColumns : filterableColumns.filter((column) => !column.columnDef.meta?.filterOnly && pinnedFilterIds.includes(column.id));
1819
+ return {
1820
+ filterableColumns,
1821
+ hasFilterOnlyColumn,
1822
+ showAllFiltersInline,
1823
+ inlineFilterColumns,
1824
+ paneFilterCount: showAllFiltersInline ? 0 : filterableColumns.length - inlineFilterColumns.length
1825
+ };
1826
+ }, [
1827
+ orderedRealColumns,
1828
+ userColumns,
1829
+ pinnedFilterIds,
1830
+ effectiveColumnVisibility
1831
+ ]);
1832
+ const allSelectedRows = table.getSelectedRowModel().rows;
1833
+ const filteredSelectedRows = table.getFilteredSelectedRowModel().rows;
1834
+ const selectedRows = selectionScope === "all" ? allSelectedRows : filteredSelectedRows;
1835
+ const hiddenByFilterCount = selectionScope === "all" ? 0 : allSelectedRows.length - filteredSelectedRows.length;
1836
+ const hasSelection = selectedRows.length > 0;
1837
+ const isRowSelectableIgnoringCap = (row) => typeof enableRowSelection === "function" ? enableRowSelection(row) : !!enableRowSelection;
1838
+ const selectableFilteredRows = table.getFilteredRowModel().rows.filter(isRowSelectableIgnoringCap);
1839
+ const isAllFilteredSelected = selectableFilteredRows.length > 0 && selectableFilteredRows.every((row) => !!rowSelection[row.id]);
1840
+ const selectAllFiltered = () => {
1841
+ const next = { ...rowSelection };
1842
+ let remaining = maxSelectedRows != null ? Math.max(0, maxSelectedRows - Object.keys(next).length) : Infinity;
1843
+ for (const row of selectableFilteredRows) {
1844
+ if (next[row.id]) continue;
1845
+ if (remaining <= 0) break;
1846
+ next[row.id] = true;
1847
+ remaining -= 1;
1848
+ }
1849
+ setRowSelection(next);
1850
+ };
1851
+ const effectiveSelectionBarPlacement = onLoadMore ? "floating" : selectionBarPlacement ?? "toolbar";
1852
+ const bulkActionsBarMounted = hasSelection && effectiveSelectionBarPlacement === "floating";
1853
+ React.useLayoutEffect(() => {
1854
+ const el = bulkActionsBarRef.current;
1855
+ if (!el) {
1856
+ setBulkActionsBarHeight(0);
1857
+ return;
1858
+ }
1859
+ const observer = new ResizeObserver(([entry]) => setBulkActionsBarHeight(entry.contentRect.height));
1860
+ observer.observe(el);
1861
+ return () => observer.disconnect();
1862
+ }, [bulkActionsBarMounted]);
1863
+ const [canScrollHorizontally, setCanScrollHorizontally] = React.useState(false);
1864
+ React.useLayoutEffect(() => {
1865
+ const container = tableContainerRef.current;
1866
+ const tableEl = container?.querySelector("table");
1867
+ if (!container || !tableEl) return;
1868
+ function measure() {
1869
+ if (!container) return;
1870
+ setCanScrollHorizontally(container.scrollWidth > container.clientWidth);
1871
+ }
1872
+ measure();
1873
+ const observer = new ResizeObserver(measure);
1874
+ observer.observe(tableEl);
1875
+ observer.observe(container);
1876
+ return () => observer.disconnect();
1877
+ }, []);
1878
+ const rowClickHintId = React.useId();
1879
+ const selectedIdsKey = selectedRows.map((row) => row.id).join("\0");
1880
+ React.useEffect(() => {
1881
+ onRowSelectionChange?.(selectedRows.map((row) => row.original), {
1882
+ allSelectedRows: allSelectedRows.map((row) => row.original),
1883
+ hiddenByFilterCount
1884
+ });
1885
+ }, [
1886
+ rowSelection,
1887
+ columnFilters,
1888
+ globalFilter,
1889
+ selectionScope,
1890
+ selectedIdsKey
1891
+ ]);
1892
+ const rows = table.getRowModel().rows;
1893
+ const visibleLeafColumns = table.getVisibleLeafColumns();
1894
+ const resizableColumnIds = visibleLeafColumns.filter((column) => column.getCanResize()).map((column) => column.id);
1895
+ const [activeResizeColumnId, setActiveResizeColumnId] = React.useState(null);
1896
+ const resizeHandleRefs = React.useRef(/* @__PURE__ */ new Map());
1897
+ const showFooter = !loading && !error && !onLoadMore;
1898
+ const headerRowHeightPx = size === "xs" ? 32 : size === "sm" ? 36 : 40;
1899
+ const headerGroupsForRender = table.getHeaderGroups();
1900
+ const passthroughLeafIds = new Set(headerGroupsForRender[0]?.headers.filter((header) => header.subHeaders.length > 0 && !Array.isArray(header.column.columnDef.columns)).map((header) => header.column.id) ?? []);
1901
+ const groupBoundaryColumnIds = React.useMemo(() => {
1902
+ const starts = /* @__PURE__ */ new Set();
1903
+ const ends = /* @__PURE__ */ new Set();
1904
+ for (const header of headerGroupsForRender[0]?.headers ?? []) {
1905
+ if (header.subHeaders.length === 0) continue;
1906
+ if (passthroughLeafIds.has(header.column.id)) continue;
1907
+ const leaves = header.getLeafHeaders().filter((leaf) => leaf.subHeaders.length === 0);
1908
+ if (leaves.length === 0) continue;
1909
+ starts.add(leaves[0].column.id);
1910
+ ends.add(leaves[leaves.length - 1].column.id);
1911
+ }
1912
+ return {
1913
+ starts,
1914
+ ends
1915
+ };
1916
+ }, [headerGroupsForRender]);
1917
+ const mergedCellRowSpans = React.useMemo(() => {
1918
+ const spans = /* @__PURE__ */ new Map();
1919
+ for (const column of visibleLeafColumns) {
1920
+ if (!column.columnDef.meta?.mergeRepeatedValues) continue;
1921
+ const columnSpans = /* @__PURE__ */ new Map();
1922
+ let runStart = 0;
1923
+ for (let i = 1; i <= rows.length; i++) {
1924
+ if (i < rows.length && rows[i].getValue(column.id) === rows[runStart].getValue(column.id)) continue;
1925
+ columnSpans.set(rows[runStart].id, i - runStart);
1926
+ for (let covered = runStart + 1; covered < i; covered++) columnSpans.set(rows[covered].id, 0);
1927
+ runStart = i;
1928
+ }
1929
+ spans.set(column.id, columnSpans);
1930
+ }
1931
+ return spans;
1932
+ }, [visibleLeafColumns, rows]);
1933
+ const [rowsAreaHeight, setRowsAreaHeight] = React.useState(null);
1934
+ React.useLayoutEffect(() => {
1935
+ const mainColumn = mainColumnRef.current;
1936
+ const container = tableContainerRef.current;
1937
+ if (!mainColumn || !container) return;
1938
+ function measure() {
1939
+ if (!mainColumn || !container) return;
1940
+ const mainRect = mainColumn.getBoundingClientRect();
1941
+ const containerRect = container.getBoundingClientRect();
1942
+ const footerEl = footerRef.current;
1943
+ const footerHeight = footerEl ? footerEl.getBoundingClientRect().height : 0;
1944
+ const available = mainRect.bottom - containerRect.top - footerHeight;
1945
+ setRowsAreaHeight(Math.max(available, 0));
1946
+ }
1947
+ measure();
1948
+ const observer = new ResizeObserver(measure);
1949
+ observer.observe(mainColumn);
1950
+ if (footerRef.current) observer.observe(footerRef.current);
1951
+ return () => observer.disconnect();
1952
+ }, [showFooter]);
1953
+ const pageIndex = table.getState().pagination.pageIndex;
1954
+ const filteredRowCount = table.getFilteredRowModel().rows.length;
1955
+ const currentPageSize = table.getState().pagination.pageSize;
1956
+ const rangeStart = filteredRowCount === 0 ? 0 : pageIndex * currentPageSize + 1;
1957
+ const rangeEnd = Math.min((pageIndex + 1) * currentPageSize, filteredRowCount);
1958
+ const [liveAnnouncement, setLiveAnnouncement] = React.useState("");
1959
+ React.useEffect(() => {
1960
+ const timeout = setTimeout(() => {
1961
+ setLiveAnnouncement(!hasSelection ? "" : isAllFilteredSelected ? `All ${filteredRowCount} rows selected` : `${selectedRows.length} of ${filteredRowCount} rows selected`);
1962
+ }, 400);
1963
+ return () => clearTimeout(timeout);
1964
+ }, [
1965
+ hasSelection,
1966
+ isAllFilteredSelected,
1967
+ selectedRows.length,
1968
+ filteredRowCount
1969
+ ]);
1970
+ React.useEffect(() => {
1971
+ const timeout = setTimeout(() => {
1972
+ setLiveAnnouncement(filteredRowCount === 0 ? "No results" : `Showing ${rangeStart}–${rangeEnd} of ${filteredRowCount} results`);
1973
+ }, 500);
1974
+ return () => clearTimeout(timeout);
1975
+ }, [
1976
+ columnFilters,
1977
+ globalFilter,
1978
+ filteredRowCount,
1979
+ rangeStart,
1980
+ rangeEnd
1981
+ ]);
1982
+ const sortingMountedRef = React.useRef(false);
1983
+ React.useEffect(() => {
1984
+ if (!sortingMountedRef.current) {
1985
+ sortingMountedRef.current = true;
1986
+ return;
1987
+ }
1988
+ if (sorting.length === 0) {
1989
+ setLiveAnnouncement("Sort cleared");
1990
+ return;
1991
+ }
1992
+ const labels = sorting.map((sort) => `${columnLabel(table.getColumn(sort.id))}, ${sort.desc ? "descending" : "ascending"}`);
1993
+ setLiveAnnouncement(`Sorted by ${labels.join(", then ")}`);
1994
+ }, [sorting]);
1995
+ const previousColumnPinningRef = React.useRef(columnPinning);
1996
+ React.useEffect(() => {
1997
+ const previous = previousColumnPinningRef.current;
1998
+ previousColumnPinningRef.current = columnPinning;
1999
+ const newlyPinned = (side) => (columnPinning[side] ?? []).find((id) => !(previous[side] ?? []).includes(id));
2000
+ const newlyUnpinned = [...previous.left ?? [], ...previous.right ?? []].find((id) => !(columnPinning.left ?? []).includes(id) && !(columnPinning.right ?? []).includes(id));
2001
+ const pinnedLeftId = newlyPinned("left");
2002
+ const pinnedRightId = newlyPinned("right");
2003
+ const columnId = pinnedLeftId ?? pinnedRightId ?? newlyUnpinned;
2004
+ if (!columnId) return;
2005
+ const column = table.getColumn(columnId);
2006
+ if (!column) return;
2007
+ setLiveAnnouncement(pinnedLeftId ? `${columnLabel(column)} pinned left` : pinnedRightId ? `${columnLabel(column)} pinned right` : `${columnLabel(column)} unpinned`);
2008
+ }, [columnPinning]);
2009
+ const loadMoreSentinelRef = React.useRef(null);
2010
+ React.useEffect(() => {
2011
+ if (!onLoadMore || !hasMore) return;
2012
+ const sentinel = loadMoreSentinelRef.current;
2013
+ if (!sentinel) return;
2014
+ const observer = new IntersectionObserver((entries) => {
2015
+ if (entries[0]?.isIntersecting) onLoadMore();
2016
+ }, { rootMargin: `${loadMoreOffset}px` });
2017
+ observer.observe(sentinel);
2018
+ return () => observer.disconnect();
2019
+ }, [
2020
+ onLoadMore,
2021
+ hasMore,
2022
+ loadMoreOffset
2023
+ ]);
2024
+ const controlSize = size === "default" ? "sm" : "xs";
2025
+ const controlIconSize = size === "default" ? "sm" : "xs";
2026
+ const hoverRevealClass = revealColumnControlsOnHover ? "opacity-0 transition-opacity group-hover/header-actions:opacity-100 group-focus-within/header-actions:opacity-100 [@media(hover:none)]:opacity-100" : "";
2027
+ const hoverRevealRowClass = revealColumnControlsOnHover ? "opacity-0 transition-opacity group-hover/column-row:opacity-100 group-focus-within/column-row:opacity-100 [@media(hover:none)]:opacity-100" : "";
2028
+ const hoverRevealFilterRowClass = revealColumnControlsOnHover ? "opacity-0 transition-opacity group-hover/filter-row:opacity-100 group-focus-within/filter-row:opacity-100 [@media(hover:none)]:opacity-100" : "";
2029
+ const [openHeaderColumnId, setOpenHeaderColumnId] = React.useState(null);
2030
+ function renderColumnMenuRow(column) {
2031
+ const locked = lockedIds.has(column.id);
2032
+ const canHide = column.getCanHide();
2033
+ const label = columnLabel(column);
2034
+ const isDragging = draggedColumnId === column.id;
2035
+ const isDropTarget = dragOverColumnId === column.id && draggedColumnId !== null && !isDragging;
2036
+ const dropSide = isDropTarget && draggedColumnId ? dropInsertSide(draggedColumnId, column.id) : null;
2037
+ const pins = pinActions(column);
2038
+ return /* @__PURE__ */ jsxs("div", {
2039
+ ref: getColumnRowRef(column.id),
2040
+ draggable: !locked,
2041
+ onDragStart: () => {
2042
+ if (locked) return;
2043
+ setDraggedColumnId(column.id);
2044
+ },
2045
+ onDragEnd: () => {
2046
+ if (dragOverDebounceRef.current) {
2047
+ clearTimeout(dragOverDebounceRef.current);
2048
+ dragOverDebounceRef.current = null;
2049
+ }
2050
+ setDraggedColumnId(null);
2051
+ setDragOverColumnId(null);
2052
+ },
2053
+ onDragEnter: (event) => {
2054
+ if (locked || !draggedColumnId) return;
2055
+ event.preventDefault();
2056
+ },
2057
+ onDragOver: (event) => {
2058
+ if (locked || !draggedColumnId) return;
2059
+ event.preventDefault();
2060
+ if (dragOverColumnId === column.id) return;
2061
+ if (dragOverDebounceRef.current) clearTimeout(dragOverDebounceRef.current);
2062
+ dragOverDebounceRef.current = setTimeout(() => {
2063
+ setDragOverColumnId(column.id);
2064
+ dragOverDebounceRef.current = null;
2065
+ }, 80);
2066
+ },
2067
+ onDrop: (event) => {
2068
+ event.preventDefault();
2069
+ if (dragOverDebounceRef.current) {
2070
+ clearTimeout(dragOverDebounceRef.current);
2071
+ dragOverDebounceRef.current = null;
2072
+ }
2073
+ if (draggedColumnId) {
2074
+ const finalReorder = computeReorderedIds(draggedColumnId, column.id);
2075
+ if (finalReorder) commitSectionOrder(finalReorder.section, finalReorder.order);
2076
+ }
2077
+ setDraggedColumnId(null);
2078
+ setDragOverColumnId(null);
2079
+ },
2080
+ className: cn("group/column-row relative flex items-center gap-1 rounded-md px-1.5 py-1 hover:bg-accent/50", !locked && "cursor-grab active:cursor-grabbing", isDragging && "opacity-40", isDropTarget && "bg-accent"),
2081
+ children: [
2082
+ dropSide && /* @__PURE__ */ jsx("div", {
2083
+ "aria-hidden": "true",
2084
+ className: cn("absolute inset-x-1.5 z-10 h-0.5 bg-primary", dropSide === "before" ? "top-0" : "bottom-0")
2085
+ }),
2086
+ /* @__PURE__ */ jsxs(Label, {
2087
+ className: "flex flex-1 items-center gap-2 truncate font-normal",
2088
+ children: [/* @__PURE__ */ jsx(Checkbox, {
2089
+ checked: canHide ? column.getIsVisible() : true,
2090
+ disabled: !canHide,
2091
+ onCheckedChange: (value) => column.toggleVisibility(!!value)
2092
+ }), /* @__PURE__ */ jsx("span", {
2093
+ className: "truncate",
2094
+ children: label
2095
+ })]
2096
+ }),
2097
+ pins.length > 0 && /* @__PURE__ */ jsx("div", {
2098
+ className: cn("flex shrink-0 items-center", hoverRevealRowClass),
2099
+ children: pins.map((action) => /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
2100
+ asChild: true,
2101
+ children: /* @__PURE__ */ jsx(Button, {
2102
+ type: "button",
2103
+ variant: "ghost",
2104
+ size: "xs",
2105
+ iconOnly: true,
2106
+ "aria-label": action.label,
2107
+ onClick: action.onClick,
2108
+ children: action.icon ? /* @__PURE__ */ jsx(action.icon, { className: "size-3.5" }) : null
2109
+ })
2110
+ }), /* @__PURE__ */ jsx(TooltipContent, { children: action.label })] }, action.label))
2111
+ }),
2112
+ /* @__PURE__ */ jsxs("div", {
2113
+ className: cn("flex shrink-0 items-center", hoverRevealRowClass),
2114
+ children: [/* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
2115
+ asChild: true,
2116
+ children: /* @__PURE__ */ jsx(Button, {
2117
+ type: "button",
2118
+ variant: "ghost",
2119
+ size: "xs",
2120
+ iconOnly: true,
2121
+ disabled: locked || !canMoveColumn(column.id, "up"),
2122
+ "aria-label": `Move ${label} left`,
2123
+ onClick: () => moveColumn(column.id, "up"),
2124
+ children: /* @__PURE__ */ jsx(ArrowLeftIcon, { className: "size-3.5" })
2125
+ })
2126
+ }), /* @__PURE__ */ jsx(TooltipContent, { children: "Move left" })] }), /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
2127
+ asChild: true,
2128
+ children: /* @__PURE__ */ jsx(Button, {
2129
+ type: "button",
2130
+ variant: "ghost",
2131
+ size: "xs",
2132
+ iconOnly: true,
2133
+ disabled: locked || !canMoveColumn(column.id, "down"),
2134
+ "aria-label": `Move ${label} right`,
2135
+ onClick: () => moveColumn(column.id, "down"),
2136
+ children: /* @__PURE__ */ jsx(ArrowRightIcon, { className: "size-3.5" })
2137
+ })
2138
+ }), /* @__PURE__ */ jsx(TooltipContent, { children: "Move right" })] })]
2139
+ })
2140
+ ]
2141
+ }, column.id);
2142
+ }
2143
+ function commitPageJump(value) {
2144
+ if (value.trim() === "") return;
2145
+ const page = Number(value);
2146
+ if (!Number.isFinite(page)) return;
2147
+ const clamped = Math.min(table.getPageCount(), Math.max(1, Math.round(page)));
2148
+ table.setPageIndex(clamped - 1);
2149
+ }
2150
+ const renderPageSelect = (triggerClassName) => table.getPageCount() > PAGE_SELECT_THRESHOLD ? /* @__PURE__ */ jsx(Input, {
2151
+ type: "number",
2152
+ min: 1,
2153
+ max: table.getPageCount(),
2154
+ defaultValue: pageIndex + 1,
2155
+ "aria-label": "Current page",
2156
+ className: cn("w-14 text-center", triggerClassName),
2157
+ onKeyDown: (event) => {
2158
+ if (event.key !== "Enter") return;
2159
+ commitPageJump(event.currentTarget.value);
2160
+ },
2161
+ onBlur: (event) => commitPageJump(event.currentTarget.value)
2162
+ }, pageIndex) : /* @__PURE__ */ jsxs(Select, {
2163
+ value: String(pageIndex + 1),
2164
+ onValueChange: (value) => table.setPageIndex(Number(value) - 1),
2165
+ children: [/* @__PURE__ */ jsx(SelectTrigger, {
2166
+ size: controlSize,
2167
+ "aria-label": "Current page",
2168
+ className: triggerClassName,
2169
+ children: /* @__PURE__ */ jsx(SelectValue, {})
2170
+ }), /* @__PURE__ */ jsx(SelectContent, { children: Array.from({ length: table.getPageCount() }, (_, index) => /* @__PURE__ */ jsx(SelectItem, {
2171
+ value: String(index + 1),
2172
+ children: index + 1
2173
+ }, index)) })]
2174
+ });
2175
+ const renderNumberedPagination = () => {
2176
+ const pageCount = table.getPageCount();
2177
+ const currentPage = pageIndex + 1;
2178
+ return /* @__PURE__ */ jsx(Pagination, {
2179
+ className: "mx-0 w-auto justify-start",
2180
+ children: /* @__PURE__ */ jsxs(PaginationContent, { children: [
2181
+ /* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(PaginationPrevious, {
2182
+ size: controlIconSize,
2183
+ disabled: !table.getCanPreviousPage(),
2184
+ onClick: () => table.previousPage()
2185
+ }) }),
2186
+ getPaginationWindow(currentPage, pageCount).map((page, index) => page === "ellipsis" ? /* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(PaginationEllipsis, {}) }, `ellipsis-${index}`) : /* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(PaginationLink, {
2187
+ size: controlIconSize,
2188
+ isActive: page === currentPage,
2189
+ onClick: () => table.setPageIndex(page - 1),
2190
+ children: page
2191
+ }) }, page)),
2192
+ /* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(PaginationNext, {
2193
+ size: controlIconSize,
2194
+ disabled: !table.getCanNextPage(),
2195
+ onClick: () => table.nextPage()
2196
+ }) })
2197
+ ] })
2198
+ });
2199
+ };
2200
+ return /* @__PURE__ */ jsxs("div", {
2201
+ "data-slot": "data-table",
2202
+ className: cn("flex h-full", className),
2203
+ children: [/* @__PURE__ */ jsxs("div", {
2204
+ ref: mainColumnRef,
2205
+ className: "flex min-h-0 min-w-0 flex-1 flex-col",
2206
+ children: [
2207
+ /* @__PURE__ */ jsxs("div", {
2208
+ className: cn("@container/data-table-toolbar mb-[calc(var(--card-spacing,1rem)*0.75)] flex flex-wrap items-center justify-between gap-2 px-(--card-spacing,1rem) @max-sm/data-table-toolbar:flex-col @max-sm/data-table-toolbar:items-stretch", filtersPaneOpen ? "-ml-(--card-spacing,0px) w-[calc(100%+var(--card-spacing,0px))]" : "-mx-(--card-spacing,0px) w-[calc(100%+var(--card-spacing,0px)*2)]"),
2209
+ children: [/* @__PURE__ */ jsx("div", {
2210
+ className: cn("flex min-w-0 flex-1 flex-wrap items-center gap-2", size === "default" && "min-h-8", size === "sm" && "min-h-7", size === "xs" && "min-h-6"),
2211
+ children: hasSelection && effectiveSelectionBarPlacement === "toolbar" ? /* @__PURE__ */ jsxs("div", {
2212
+ className: "flex min-w-0 flex-1 items-center gap-1.5",
2213
+ children: [
2214
+ isAllFilteredSelected ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
2215
+ className: "px-1 text-sm whitespace-nowrap text-muted-foreground",
2216
+ children: [
2217
+ "All ",
2218
+ table.getFilteredRowModel().rows.length,
2219
+ " selected"
2220
+ ]
2221
+ }), /* @__PURE__ */ jsx(Button, {
2222
+ type: "button",
2223
+ variant: "ghost",
2224
+ size: controlSize,
2225
+ onClick: () => setRowSelection({}),
2226
+ children: "Clear selection"
2227
+ })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
2228
+ className: "px-1 text-sm whitespace-nowrap text-muted-foreground",
2229
+ children: [maxSelectedRows != null ? `${selectedRows.length} of ${maxSelectedRows} max selected` : `${selectedRows.length} selected`, hiddenByFilterCount > 0 && ` (${hiddenByFilterCount} hidden by filters)`]
2230
+ }), /* @__PURE__ */ jsxs(Button, {
2231
+ type: "button",
2232
+ variant: "ghost",
2233
+ size: controlSize,
2234
+ disabled: atSelectionCap,
2235
+ title: atSelectionCap ? `Up to ${maxSelectedRows} rows can be selected` : void 0,
2236
+ onClick: selectAllFiltered,
2237
+ children: [
2238
+ /* @__PURE__ */ jsx(CheckCheckIcon, {}),
2239
+ "Select all ",
2240
+ selectableFilteredRows.length
2241
+ ]
2242
+ })] }),
2243
+ renderBulkActions && /* @__PURE__ */ jsx("div", {
2244
+ className: "flex items-center gap-1",
2245
+ children: renderBulkActions(selectedRows.map((row) => row.original), () => setRowSelection({}))
2246
+ }),
2247
+ /* @__PURE__ */ jsx(Button, {
2248
+ type: "button",
2249
+ variant: "ghost",
2250
+ size: controlIconSize,
2251
+ iconOnly: true,
2252
+ "aria-label": "Clear selection",
2253
+ onClick: () => setRowSelection({}),
2254
+ children: /* @__PURE__ */ jsx(XIcon, {})
2255
+ })
2256
+ ]
2257
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(InputGroup, {
2258
+ className: cn("max-w-64", controlSize === "sm" && "h-7", controlSize === "xs" && "h-6"),
2259
+ children: [/* @__PURE__ */ jsx(InputGroupAddon, { children: /* @__PURE__ */ jsx(SearchIcon, {}) }), /* @__PURE__ */ jsx(InputGroupInput, {
2260
+ "aria-label": searchPlaceholder,
2261
+ placeholder: searchPlaceholder,
2262
+ value: searchInput,
2263
+ onChange: (event) => {
2264
+ setSearchInput(event.target.value);
2265
+ debouncedSetGlobalFilter(event.target.value);
2266
+ },
2267
+ onBlur: () => setGlobalFilter(searchInput),
2268
+ onKeyDown: (event) => {
2269
+ if (event.key === "Enter") setGlobalFilter(searchInput);
2270
+ }
2271
+ })]
2272
+ }), (inlineFilterColumns.length > 0 || !showAllFiltersInline && paneFilterCount > 0) && /* @__PURE__ */ jsxs("div", {
2273
+ className: "flex flex-wrap items-center gap-1",
2274
+ children: [inlineFilterColumns.map((column) => /* @__PURE__ */ jsx("div", {
2275
+ className: cn(filtersPaneOpen && "hidden"),
2276
+ children: /* @__PURE__ */ jsx(InlineColumnFilter, {
2277
+ column,
2278
+ size: controlSize,
2279
+ tableDeferred: filterApplyMode === "deferred",
2280
+ showFilterCounts,
2281
+ stickyFilterSelectAll
2282
+ })
2283
+ }, column.id)), /* @__PURE__ */ jsxs("div", {
2284
+ className: "flex items-center gap-4",
2285
+ children: [!showAllFiltersInline && /* @__PURE__ */ jsx(Button, {
2286
+ ref: filtersPaneTriggerRef,
2287
+ type: "button",
2288
+ variant: "outline",
2289
+ size: controlSize,
2290
+ "aria-expanded": filtersPaneOpen,
2291
+ "aria-controls": filtersPaneId,
2292
+ onClick: () => setFiltersPaneOpen((open) => !open),
2293
+ children: "All filters"
2294
+ }), !showAllFiltersInline && !filtersPaneOpen && columnFilters.length > 0 && /* @__PURE__ */ jsxs("div", {
2295
+ className: "flex items-center gap-2",
2296
+ children: [/* @__PURE__ */ jsxs("span", {
2297
+ className: "text-sm text-muted-foreground",
2298
+ children: [
2299
+ columnFilters.length,
2300
+ " filter",
2301
+ columnFilters.length === 1 ? "" : "s",
2302
+ " applied"
2303
+ ]
2304
+ }), /* @__PURE__ */ jsx(Button, {
2305
+ type: "button",
2306
+ variant: "ghost",
2307
+ size: controlSize,
2308
+ onClick: () => setColumnFilters([]),
2309
+ children: "Clear filters"
2310
+ })]
2311
+ })]
2312
+ })]
2313
+ })] })
2314
+ }), /* @__PURE__ */ jsxs("div", {
2315
+ className: "flex items-center gap-2",
2316
+ children: [
2317
+ aiAction && /* @__PURE__ */ jsxs(Button, {
2318
+ type: "button",
2319
+ variant: "ai",
2320
+ size,
2321
+ onClick: aiAction.onClick,
2322
+ children: [/* @__PURE__ */ jsx(AISparkle, {
2323
+ size: "sm",
2324
+ "data-icon": "inline-start"
2325
+ }), aiAction.label]
2326
+ }),
2327
+ actionButtons?.slice(0, 3).map((action, index) => action.variant === "icon" ? /* @__PURE__ */ jsx(Button, {
2328
+ type: "button",
2329
+ variant: "outline",
2330
+ size: controlIconSize,
2331
+ iconOnly: true,
2332
+ disabled: action.disabled,
2333
+ "aria-label": action.label,
2334
+ onClick: action.onClick,
2335
+ children: action.icon
2336
+ }, index) : /* @__PURE__ */ jsxs(Button, {
2337
+ type: "button",
2338
+ variant: action.variant === "secondary" ? "outline" : "default",
2339
+ size: controlSize,
2340
+ disabled: action.disabled,
2341
+ onClick: action.onClick,
2342
+ children: [action.icon && /* @__PURE__ */ jsx("span", {
2343
+ "data-icon": "inline-start",
2344
+ children: action.icon
2345
+ }), action.label]
2346
+ }, index)),
2347
+ showColumnToggle && manageableColumns.length > 0 && /* @__PURE__ */ jsxs(Popover, {
2348
+ open: columnsMenuOpen,
2349
+ onOpenChange: handleColumnsMenuOpenChange,
2350
+ children: [/* @__PURE__ */ jsx(PopoverTrigger, {
2351
+ asChild: true,
2352
+ children: /* @__PURE__ */ jsx(Button, {
2353
+ type: "button",
2354
+ variant: "outline",
2355
+ size: controlIconSize,
2356
+ iconOnly: true,
2357
+ "aria-label": "Columns",
2358
+ children: /* @__PURE__ */ jsx(Columns3Icon, {})
2359
+ })
2360
+ }), /* @__PURE__ */ jsxs(PopoverContent, {
2361
+ align: "end",
2362
+ className: "w-72 gap-0 p-1",
2363
+ children: [
2364
+ manageableColumns.length > 10 && /* @__PURE__ */ jsx("div", {
2365
+ className: "px-1.5 pb-1",
2366
+ children: /* @__PURE__ */ jsx(Input, {
2367
+ "aria-label": "Search columns",
2368
+ placeholder: "Search columns...",
2369
+ value: columnSearchQuery,
2370
+ onChange: (event) => setColumnSearchQuery(event.target.value),
2371
+ className: "h-7",
2372
+ onKeyDown: (event) => event.stopPropagation()
2373
+ })
2374
+ }),
2375
+ /* @__PURE__ */ jsx("div", {
2376
+ className: "max-h-72 overflow-y-auto",
2377
+ children: (() => {
2378
+ const matchesSearch = (column) => columnLabel(column).toLowerCase().includes(columnSearchQuery.toLowerCase());
2379
+ const filteredLeft = displayedLeft.filter(matchesSearch);
2380
+ const filteredCenter = displayedCenter.filter(matchesSearch);
2381
+ const filteredRight = displayedRight.filter(matchesSearch);
2382
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2383
+ filteredLeft.map(renderColumnMenuRow),
2384
+ filteredLeft.length > 0 && (filteredCenter.length > 0 || filteredRight.length > 0) && /* @__PURE__ */ jsx("div", { className: "my-1 border-t border-border" }),
2385
+ filteredCenter.map(renderColumnMenuRow),
2386
+ filteredRight.length > 0 && (filteredLeft.length > 0 || filteredCenter.length > 0) && /* @__PURE__ */ jsx("div", { className: "my-1 border-t border-border" }),
2387
+ filteredRight.map(renderColumnMenuRow)
2388
+ ] });
2389
+ })()
2390
+ }),
2391
+ /* @__PURE__ */ jsxs("div", {
2392
+ className: "-mx-1 flex w-[calc(100%+--spacing(2))] items-center justify-between gap-2 border-t border-border px-1.5 pt-2 pb-1.5",
2393
+ children: [hasColumnCustomization ? /* @__PURE__ */ jsxs(Button, {
2394
+ type: "button",
2395
+ variant: "ghost",
2396
+ size: "xs",
2397
+ onClick: resetColumns,
2398
+ children: [/* @__PURE__ */ jsx(RotateCcwIcon, { "data-icon": "inline-start" }), "Reset to defaults"]
2399
+ }) : /* @__PURE__ */ jsx("span", {}), /* @__PURE__ */ jsxs("div", {
2400
+ className: "flex items-center gap-1",
2401
+ children: [/* @__PURE__ */ jsx(Button, {
2402
+ type: "button",
2403
+ variant: "ghost",
2404
+ size: "xs",
2405
+ onClick: () => handleColumnsMenuOpenChange(false),
2406
+ children: "Cancel"
2407
+ }), /* @__PURE__ */ jsx(Button, {
2408
+ type: "button",
2409
+ size: "xs",
2410
+ disabled: !hasPendingColumnsMenuChanges,
2411
+ onClick: () => {
2412
+ columnsMenuAppliedRef.current = true;
2413
+ handleColumnsMenuOpenChange(false);
2414
+ onColumnLayoutChange?.({
2415
+ visibility: columnVisibility,
2416
+ order: columnOrder,
2417
+ pinning: columnPinning,
2418
+ sizing: columnSizing
2419
+ });
2420
+ },
2421
+ children: "Apply"
2422
+ })]
2423
+ })]
2424
+ })
2425
+ ]
2426
+ })]
2427
+ })
2428
+ ]
2429
+ })]
2430
+ }),
2431
+ /* @__PURE__ */ jsxs("div", {
2432
+ ref: tableContainerRef,
2433
+ tabIndex: canScrollHorizontally ? 0 : void 0,
2434
+ "aria-label": canScrollHorizontally ? `${ariaLabel ?? "Table"} scroll area` : void 0,
2435
+ className: cn(canScrollHorizontally && "focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none", "relative min-h-0", "-mx-(--card-spacing,0px) w-[calc(100%+var(--card-spacing,0px)*2)] [scrollbar-width:thin] [scrollbar-color:var(--color-border)_transparent] overflow-auto", "[&_[data-slot=table-container]]:mx-0 [&_[data-slot=table-container]]:w-full [&_[data-slot=table-container]]:overflow-visible", size === "xs" ? "[&_tbody_:focus-visible]:scroll-mt-8" : size === "sm" ? "[&_tbody_:focus-visible]:scroll-mt-9" : "[&_tbody_:focus-visible]:scroll-mt-10", filtersPaneOpen && "mr-0 w-[calc(100%+var(--card-spacing,0px))]"),
2436
+ style: onLoadMore ? { maxHeight: maxHeight ?? "32rem" } : {
2437
+ ...rowsAreaHeight === null ? void 0 : { height: rowsAreaHeight },
2438
+ ...maxHeight ? { maxHeight } : void 0
2439
+ },
2440
+ children: [
2441
+ onRowClick && /* @__PURE__ */ jsx("span", {
2442
+ id: rowClickHintId,
2443
+ className: "sr-only",
2444
+ children: "Press Enter to view details"
2445
+ }),
2446
+ (loading || loadingMore) && /* @__PURE__ */ jsx("span", {
2447
+ role: "status",
2448
+ className: "sr-only",
2449
+ children: "Loading…"
2450
+ }),
2451
+ /* @__PURE__ */ jsx("span", {
2452
+ role: "status",
2453
+ className: "sr-only",
2454
+ children: liveAnnouncement
2455
+ }),
2456
+ /* @__PURE__ */ jsxs(Table, {
2457
+ size,
2458
+ "aria-label": ariaLabel,
2459
+ "aria-rowcount": filteredRowCount + headerGroupsForRender.length,
2460
+ "aria-colcount": visibleLeafColumns.length,
2461
+ className: cn("table-fixed border-separate border-spacing-0", striped && "[&_tbody_tr:nth-child(even)]:bg-muted/20", striped && "[&_tbody_tr:hover]:bg-muted/50!", cellDividers && "[&_td]:border-r [&_td:last-child]:border-r-0 [&_th]:border-r [&_th:last-child]:border-r-0"),
2462
+ children: [
2463
+ caption != null && /* @__PURE__ */ jsx(TableCaption, {
2464
+ className: cn(captionHidden && "sr-only"),
2465
+ children: caption
2466
+ }),
2467
+ /* @__PURE__ */ jsx("colgroup", { children: table.getHeaderGroups().at(-1)?.headers.filter((header) => !header.column.columnDef.meta?.filterOnly).map((header) => /* @__PURE__ */ jsx("col", { style: { width: header.column.getSize() } }, header.column.id)) }),
2468
+ /* @__PURE__ */ jsx(TableHeader, { children: headerGroupsForRender.map((headerGroup, headerGroupIndex) => /* @__PURE__ */ jsx(TableRow, { children: headerGroup.headers.filter((header) => !header.column.columnDef.meta?.filterOnly && !(headerGroupIndex > 0 && passthroughLeafIds.has(header.column.id))).map((header) => {
2469
+ const isPassthroughLeaf = passthroughLeafIds.has(header.column.id);
2470
+ const isRealGroupHeader = header.subHeaders.length > 0 && !isPassthroughLeaf;
2471
+ const isLeafHeader = !isRealGroupHeader && (!header.isPlaceholder || isPassthroughLeaf);
2472
+ const canSort = isLeafHeader && header.column.getCanSort();
2473
+ const sortDirection = header.column.getIsSorted();
2474
+ const canResize = isLeafHeader && header.column.getCanResize();
2475
+ const align = header.column.columnDef.meta?.align ?? "left";
2476
+ const headerControlsReveal = openHeaderColumnId === header.column.id ? "opacity-100" : hoverRevealClass;
2477
+ const setHeaderOpen = (open) => setOpenHeaderColumnId(open ? header.column.id : null);
2478
+ const headerActions = !isLeafHeader ? [] : header.column.columnDef.meta?.headerActions?.(header.column, table) ?? [];
2479
+ return /* @__PURE__ */ jsxs(TableHead, {
2480
+ "data-column-id": header.column.id,
2481
+ colSpan: header.colSpan,
2482
+ rowSpan: isPassthroughLeaf && headerGroupIndex === 0 ? headerGroupsForRender.length : void 0,
2483
+ "aria-sort": !canSort ? void 0 : sortDirection === "asc" ? "ascending" : sortDirection === "desc" ? "descending" : "none",
2484
+ style: {
2485
+ ...stickyHeader && {
2486
+ position: "sticky",
2487
+ top: headerGroupIndex * headerRowHeightPx
2488
+ },
2489
+ ...getPinnedStyle(header.column)
2490
+ },
2491
+ className: cn("group/header-actions relative", isRealGroupHeader && "border-x border-b border-border text-center", groupBoundaryColumnIds.starts.has(header.column.id) && "border-l border-border", groupBoundaryColumnIds.ends.has(header.column.id) && "border-r border-border", header.column.columnDef.meta?.mergeRepeatedValues && "border-x border-border", getPinnedClassName(header.column, table), getScrollFadeClassName(header.column, table, canScrollHorizontally), stickyHeader && "z-20 bg-background", stickyHeader && header.column.getIsPinned() && "z-30", isLeafHeader && aiColumnIds?.includes(header.column.id) && "border-t-2 border-t-ai-wash bg-ai-glow"),
2492
+ children: [/* @__PURE__ */ jsxs("div", {
2493
+ className: cn("flex items-center gap-0.5", align === "right" && "flex-row-reverse"),
2494
+ children: [
2495
+ /* @__PURE__ */ jsx("div", {
2496
+ className: cn("min-w-0 flex-1", align === "right" && "flex justify-end"),
2497
+ children: isRealGroupHeader ? /* @__PURE__ */ jsx("div", {
2498
+ className: "min-w-0 truncate text-center text-xs font-medium",
2499
+ children: flexRender(header.column.columnDef.header, header.getContext())
2500
+ }) : header.isPlaceholder && !isPassthroughLeaf ? null : canSort ? /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
2501
+ asChild: true,
2502
+ children: /* @__PURE__ */ jsxs(Button, {
2503
+ type: "button",
2504
+ variant: "ghost",
2505
+ size: "xs",
2506
+ className: cn("h-7 max-w-full min-w-0 shrink! gap-1 px-2 font-medium", align === "right" ? "-mr-2" : "-ml-2"),
2507
+ onClick: header.column.getToggleSortingHandler(),
2508
+ children: [
2509
+ /* @__PURE__ */ jsx("span", {
2510
+ className: "min-w-0 truncate",
2511
+ children: flexRender(header.column.columnDef.header, header.getContext())
2512
+ }),
2513
+ enableMultiSort && sorting.length > 1 && sortDirection && (() => {
2514
+ const sortRank = sorting.findIndex((sort) => sort.id === header.column.id) + 1;
2515
+ return /* @__PURE__ */ jsx("span", {
2516
+ "aria-hidden": "true",
2517
+ className: "flex size-3.5 shrink-0 items-center justify-center rounded-full bg-muted text-[10px] font-medium text-muted-foreground",
2518
+ children: sortRank
2519
+ });
2520
+ })(),
2521
+ sortDirection === "asc" ? /* @__PURE__ */ jsx(ArrowUpIcon, { className: "size-3.5 shrink-0" }) : sortDirection === "desc" ? /* @__PURE__ */ jsx(ArrowDownIcon, { className: "size-3.5 shrink-0" }) : /* @__PURE__ */ jsx(ChevronsUpDownIcon, { className: "size-3.5 shrink-0 opacity-50" })
2522
+ ]
2523
+ })
2524
+ }), /* @__PURE__ */ jsxs(TooltipContent, { children: [sortDirection === "asc" ? "Sort descending" : sortDirection === "desc" ? "Clear sort" : "Sort ascending", enableMultiSort && " — Shift+click to sort by multiple columns"] })] }) : /* @__PURE__ */ jsx("div", {
2525
+ className: "min-w-0 truncate text-xs",
2526
+ children: flexRender(header.column.columnDef.header, header.getContext())
2527
+ })
2528
+ }),
2529
+ isLeafHeader && mergedFilterColumns(header.column, table).some((c) => c.getCanFilter()) && /* @__PURE__ */ jsx(ColumnFilterTrigger, {
2530
+ column: header.column,
2531
+ table,
2532
+ revealClassName: headerControlsReveal,
2533
+ onOpenChange: setHeaderOpen,
2534
+ tableDeferred: filterApplyMode === "deferred",
2535
+ showFilterCounts,
2536
+ stickyFilterSelectAll
2537
+ }),
2538
+ headerActions.length > 0 && /* @__PURE__ */ jsx(RowActions, {
2539
+ actions: headerActions,
2540
+ revealClassName: headerControlsReveal,
2541
+ onOpenChange: setHeaderOpen
2542
+ }),
2543
+ isLeafHeader && aiColumnIds?.includes(header.column.id) && /* @__PURE__ */ jsxs(AILabel, { children: [/* @__PURE__ */ jsx(AIMarkerTrigger, {
2544
+ size,
2545
+ ariaLabel: `AI-generated column: ${columnLabel(header.column)}`
2546
+ }), /* @__PURE__ */ jsxs(AILabelContent, { children: [/* @__PURE__ */ jsx(PopoverHeader, { children: /* @__PURE__ */ jsx(PopoverTitle, { children: "Why you're seeing this" }) }), /* @__PURE__ */ jsx(PopoverDescription, { children: "This column's values are generated or suggested by AI." })] })] })
2547
+ ]
2548
+ }), canResize && /* @__PURE__ */ jsx("div", {
2549
+ ref: (element) => {
2550
+ if (element) resizeHandleRefs.current.set(header.column.id, element);
2551
+ else resizeHandleRefs.current.delete(header.column.id);
2552
+ },
2553
+ onMouseDown: header.getResizeHandler(),
2554
+ onTouchStart: header.getResizeHandler(),
2555
+ onDoubleClick: () => fitColumnToContent(header.column, table),
2556
+ tabIndex: (activeResizeColumnId ?? resizableColumnIds[0]) === header.column.id ? 0 : -1,
2557
+ onFocus: () => setActiveResizeColumnId(header.column.id),
2558
+ onKeyDown: (event) => {
2559
+ if (event.key === "ArrowUp" || event.key === "ArrowDown") {
2560
+ event.preventDefault();
2561
+ const index = resizableColumnIds.indexOf(header.column.id);
2562
+ const nextIndex = event.key === "ArrowDown" ? index + 1 : index - 1;
2563
+ const nextId = resizableColumnIds[nextIndex];
2564
+ if (!nextId) return;
2565
+ resizeHandleRefs.current.get(nextId)?.focus();
2566
+ setActiveResizeColumnId(nextId);
2567
+ return;
2568
+ }
2569
+ const minSize = header.column.columnDef.minSize ?? 20;
2570
+ const maxSize = header.column.columnDef.maxSize ?? Number.MAX_SAFE_INTEGER;
2571
+ const clamp = (size) => Math.min(maxSize, Math.max(minSize, size));
2572
+ let nextSize;
2573
+ if (event.key === "Home") nextSize = minSize;
2574
+ else if (event.key === "End") nextSize = maxSize < 1e6 ? maxSize : void 0;
2575
+ else if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
2576
+ const step = event.shiftKey ? 50 : 10;
2577
+ const delta = event.key === "ArrowRight" ? step : -step;
2578
+ nextSize = clamp(header.getSize() + delta);
2579
+ }
2580
+ if (nextSize === void 0) return;
2581
+ event.preventDefault();
2582
+ setColumnSizing((prev) => ({
2583
+ ...prev,
2584
+ [header.column.id]: nextSize
2585
+ }));
2586
+ },
2587
+ role: "separator",
2588
+ "aria-orientation": "vertical",
2589
+ "aria-label": `Resize ${columnLabel(header.column)}`,
2590
+ "aria-valuenow": header.getSize(),
2591
+ "aria-valuemin": header.column.columnDef.minSize ?? 20,
2592
+ "aria-valuemax": typeof header.column.columnDef.maxSize === "number" && header.column.columnDef.maxSize < 1e6 ? header.column.columnDef.maxSize : void 0,
2593
+ className: cn("absolute top-0 right-0 h-full w-1 cursor-col-resize touch-none select-none hover:bg-primary/50 focus-visible:bg-primary focus-visible:outline-none", header.column.getIsResizing() && "bg-primary")
2594
+ })]
2595
+ }, header.id);
2596
+ }) }, headerGroup.id)) }),
2597
+ /* @__PURE__ */ jsxs(TableBody, { children: [loading ? Array.from({ length: table.getState().pagination.pageSize }).map((_, rowIndex) => /* @__PURE__ */ jsx(TableRow, { children: visibleLeafColumns.map((_, columnIndex) => /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }) }, columnIndex)) }, rowIndex)) : error ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
2598
+ colSpan: visibleLeafColumns.length,
2599
+ className: "h-56 text-center",
2600
+ children: /* @__PURE__ */ jsx(Empty, {
2601
+ role: "alert",
2602
+ children: /* @__PURE__ */ jsxs(EmptyHeader, { children: [
2603
+ /* @__PURE__ */ jsx(EmptyMedia, {
2604
+ variant: "icon",
2605
+ children: /* @__PURE__ */ jsx(CircleAlertIcon, {})
2606
+ }),
2607
+ /* @__PURE__ */ jsx(EmptyTitle, { children: "Failed to load data" }),
2608
+ /* @__PURE__ */ jsx(EmptyDescription, { children: errorDescription ?? (typeof error === "boolean" ? "Something went wrong. Try again." : error) })
2609
+ ] })
2610
+ })
2611
+ }) }) : rows.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
2612
+ colSpan: visibleLeafColumns.length,
2613
+ className: "h-56 text-center",
2614
+ children: /* @__PURE__ */ jsx(Empty, { children: /* @__PURE__ */ jsxs(EmptyHeader, { children: [
2615
+ /* @__PURE__ */ jsx(EmptyMedia, {
2616
+ variant: "icon",
2617
+ children: /* @__PURE__ */ jsx(SearchXIcon, {})
2618
+ }),
2619
+ /* @__PURE__ */ jsx(EmptyTitle, { children: emptyTitle }),
2620
+ /* @__PURE__ */ jsx(EmptyDescription, { children: emptyDescription })
2621
+ ] }) })
2622
+ }) }) : rows.map((row, rowIndex) => /* @__PURE__ */ jsxs(React.Fragment, { children: [/* @__PURE__ */ jsx(TableRow, {
2623
+ "aria-rowindex": (onLoadMore ? 0 : pageIndex * currentPageSize) + rowIndex + headerGroupsForRender.length + 1,
2624
+ "data-state": row.getIsSelected() ? "selected" : void 0,
2625
+ onClick: onRowClick ? () => onRowClick(row.original) : void 0,
2626
+ onKeyDown: onRowClick ? (event) => {
2627
+ if (event.key !== "Enter" && event.key !== " ") return;
2628
+ event.preventDefault();
2629
+ onRowClick(row.original);
2630
+ } : void 0,
2631
+ tabIndex: onRowClick ? 0 : void 0,
2632
+ "aria-describedby": onRowClick ? rowClickHintId : void 0,
2633
+ className: cn(onRowClick && "cursor-pointer outline-none focus-visible:bg-muted/50 focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset", aiRowPredicate?.(row.original) && "bg-ai-glow", renderExpandedRow && row.getIsExpanded() && "border-b-0"),
2634
+ children: row.getVisibleCells().filter((cell) => !cell.column.columnDef.meta?.filterOnly && mergedCellRowSpans.get(cell.column.id)?.get(row.id) !== 0).map((cell, cellIndex) => {
2635
+ const alignRight = cell.column.columnDef.meta?.align === "right";
2636
+ const mergeRowSpan = mergedCellRowSpans.get(cell.column.id)?.get(row.id);
2637
+ const cellContent = aiCellPredicate?.(row.original, cell.column.id) ? /* @__PURE__ */ jsxs("span", {
2638
+ className: "inline-flex items-center gap-1",
2639
+ children: [flexRender(cell.column.columnDef.cell, cell.getContext()), /* @__PURE__ */ jsxs(AILabel, { children: [/* @__PURE__ */ jsx(AIMarkerTrigger, {
2640
+ size,
2641
+ ariaLabel: "AI-suggested value"
2642
+ }), /* @__PURE__ */ jsxs(AILabelContent, { children: [/* @__PURE__ */ jsx(PopoverHeader, { children: /* @__PURE__ */ jsx(PopoverTitle, { children: "Why you're seeing this" }) }), /* @__PURE__ */ jsx(PopoverDescription, { children: "This value is generated or suggested by AI." })] })] })]
2643
+ }) : flexRender(cell.column.columnDef.cell, cell.getContext());
2644
+ const needsPinnedAiGlow = cell.column.getIsPinned() === "left" && aiRowPredicate?.(row.original);
2645
+ return /* @__PURE__ */ jsx(TableCell, {
2646
+ "data-column-id": cell.column.id,
2647
+ rowSpan: mergeRowSpan && mergeRowSpan > 1 ? mergeRowSpan : void 0,
2648
+ style: {
2649
+ ...getPinnedStyle(cell.column),
2650
+ ...needsPinnedAiGlow && {
2651
+ backgroundSize: `${table.getTotalSize()}px 100%`,
2652
+ backgroundPositionX: `-${cell.column.getStart("left")}px`
2653
+ }
2654
+ },
2655
+ className: cn("overflow-hidden text-ellipsis", mergeRowSpan != null && "align-top", mergeRowSpan != null && "border-x border-border", groupBoundaryColumnIds.starts.has(cell.column.id) && "border-l border-border", groupBoundaryColumnIds.ends.has(cell.column.id) && "border-r border-border", getPinnedClassName(cell.column, table), getScrollFadeClassName(cell.column, table, canScrollHorizontally), aiColumnIds?.includes(cell.column.id) && "bg-ai-glow", cellIndex === 0 && aiRowPredicate?.(row.original) && "shadow-[inset_2px_0_0_0_var(--color-ai-wash)]", needsPinnedAiGlow && "bg-ai-glow"),
2656
+ children: alignRight ? /* @__PURE__ */ jsx("div", {
2657
+ className: "flex items-center justify-end",
2658
+ children: cellContent
2659
+ }) : cellContent
2660
+ }, cell.id);
2661
+ })
2662
+ }), renderExpandedRow && row.getIsExpanded() && /* @__PURE__ */ jsx(TableRow, {
2663
+ "data-slot": "data-table-expanded-row",
2664
+ id: `data-table-expanded-${row.id}`,
2665
+ children: /* @__PURE__ */ jsx(TableCell, {
2666
+ colSpan: visibleLeafColumns.length,
2667
+ className: "bg-muted/20 p-0",
2668
+ children: /* @__PURE__ */ jsx("div", {
2669
+ className: "ml-8 border-t border-border p-4",
2670
+ children: renderExpandedRow(row.original)
2671
+ })
2672
+ })
2673
+ })] }, row.id)), onLoadMore && hasMore && rows.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(TableRow, {
2674
+ ref: loadMoreSentinelRef,
2675
+ "aria-hidden": "true",
2676
+ "data-slot": "data-table-load-more-sentinel",
2677
+ className: "border-none",
2678
+ children: /* @__PURE__ */ jsx(TableCell, {
2679
+ colSpan: visibleLeafColumns.length,
2680
+ className: "h-px p-0"
2681
+ })
2682
+ }), loadingMore && Array.from({ length: 3 }).map((_, rowIndex) => /* @__PURE__ */ jsx(TableRow, { children: visibleLeafColumns.map((_, columnIndex) => /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }) }, columnIndex)) }, `load-more-skeleton-${rowIndex}`))] })] })
2683
+ ]
2684
+ }),
2685
+ bulkActionsBarMounted && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
2686
+ "aria-hidden": "true",
2687
+ style: { height: bulkActionsBarHeight + 8 }
2688
+ }), /* @__PURE__ */ jsx(BulkActionsBar, {
2689
+ ref: bulkActionsBarRef,
2690
+ count: selectedRows.length,
2691
+ hiddenByFilterCount,
2692
+ totalCount: selectableFilteredRows.length,
2693
+ isAllSelected: isAllFilteredSelected,
2694
+ maxSelectedRows,
2695
+ atSelectionCap,
2696
+ onSelectAll: selectAllFiltered,
2697
+ onClear: () => setRowSelection({}),
2698
+ controlSize,
2699
+ controlIconSize,
2700
+ children: renderBulkActions?.(selectedRows.map((row) => row.original), () => setRowSelection({}))
2701
+ })] })
2702
+ ]
2703
+ }),
2704
+ showFooter && /* @__PURE__ */ jsxs("div", {
2705
+ ref: footerRef,
2706
+ "data-slot": "card-footer",
2707
+ className: cn("flex flex-wrap items-center gap-4 border-t border-border bg-muted/50 px-(--card-spacing,1rem) py-1.5", filtersPaneOpen ? "-ml-(--card-spacing,0px) w-[calc(100%+var(--card-spacing,0px))] rounded-bl-xl" : "-mx-(--card-spacing,0px) w-[calc(100%+var(--card-spacing,0px)*2)] rounded-b-xl"),
2708
+ children: [
2709
+ /* @__PURE__ */ jsx("p", {
2710
+ role: "status",
2711
+ className: "text-sm whitespace-nowrap text-muted-foreground",
2712
+ children: filteredRowCount === 0 ? "0 items" : `${rangeStart}–${rangeEnd} of ${filteredRowCount} items`
2713
+ }),
2714
+ showPageSizeSelector && /* @__PURE__ */ jsxs("div", {
2715
+ className: "flex items-center gap-1.5 text-sm text-muted-foreground",
2716
+ children: [/* @__PURE__ */ jsx("span", {
2717
+ className: "whitespace-nowrap",
2718
+ children: "Items per page:"
2719
+ }), /* @__PURE__ */ jsxs(Select, {
2720
+ value: String(currentPageSize),
2721
+ onValueChange: (value) => table.setPageSize(Number(value)),
2722
+ children: [/* @__PURE__ */ jsx(SelectTrigger, {
2723
+ size: controlSize,
2724
+ "aria-label": "Items per page",
2725
+ className: "h-auto border-none bg-transparent px-1 font-medium text-foreground",
2726
+ children: /* @__PURE__ */ jsx(SelectValue, {})
2727
+ }), /* @__PURE__ */ jsx(SelectContent, { children: pageSizeOptions.map((option) => /* @__PURE__ */ jsx(SelectItem, {
2728
+ value: String(option),
2729
+ children: option
2730
+ }, option)) })]
2731
+ })]
2732
+ }),
2733
+ table.getPageCount() > 1 && (paginationStyle === "numbered" ? /* @__PURE__ */ jsx("div", {
2734
+ className: "ml-auto border-l border-border pl-4",
2735
+ children: renderNumberedPagination()
2736
+ }) : /* @__PURE__ */ jsxs("div", {
2737
+ className: "ml-auto flex items-center gap-3 border-l border-border pl-4",
2738
+ children: [/* @__PURE__ */ jsxs("div", {
2739
+ className: "flex items-center gap-1.5 text-sm text-muted-foreground",
2740
+ children: [renderPageSelect("h-auto border-none bg-transparent px-1 font-medium text-foreground"), /* @__PURE__ */ jsxs("span", {
2741
+ className: "whitespace-nowrap",
2742
+ children: [
2743
+ "of ",
2744
+ table.getPageCount(),
2745
+ " pages"
2746
+ ]
2747
+ })]
2748
+ }), /* @__PURE__ */ jsxs("div", {
2749
+ className: "flex items-center gap-0.5",
2750
+ children: [/* @__PURE__ */ jsx(Button, {
2751
+ type: "button",
2752
+ variant: "ghost",
2753
+ size: controlIconSize,
2754
+ iconOnly: true,
2755
+ "aria-label": "Previous page",
2756
+ disabled: !table.getCanPreviousPage(),
2757
+ onClick: () => table.previousPage(),
2758
+ children: /* @__PURE__ */ jsx(ChevronLeftIcon, {})
2759
+ }), /* @__PURE__ */ jsx(Button, {
2760
+ type: "button",
2761
+ variant: "ghost",
2762
+ size: controlIconSize,
2763
+ iconOnly: true,
2764
+ "aria-label": "Next page",
2765
+ disabled: !table.getCanNextPage(),
2766
+ onClick: () => table.nextPage(),
2767
+ children: /* @__PURE__ */ jsx(ChevronRightIcon, {})
2768
+ })]
2769
+ })]
2770
+ }))
2771
+ ]
2772
+ })
2773
+ ]
2774
+ }), filtersPaneOpen && /* @__PURE__ */ jsxs("div", {
2775
+ ref: filtersPaneRef,
2776
+ id: filtersPaneId,
2777
+ role: "region",
2778
+ "aria-labelledby": filtersPaneHeadingId,
2779
+ onKeyDown: (event) => {
2780
+ if (event.key !== "Escape") return;
2781
+ if (event.target.closest("[data-slot=popover-content], [role=listbox]")) return;
2782
+ setFiltersPaneOpen(false);
2783
+ },
2784
+ style: filtersPaneEdgeAdjust ? {
2785
+ marginTop: filtersPaneEdgeAdjust.marginTop,
2786
+ height: filtersPaneEdgeAdjust.height
2787
+ } : void 0,
2788
+ className: "-mr-(--card-spacing,0px) flex w-72 shrink-0 flex-col gap-4 border-l border-border bg-card p-(--card-spacing,1rem)",
2789
+ children: [
2790
+ /* @__PURE__ */ jsxs("div", {
2791
+ className: "flex items-center justify-between gap-2",
2792
+ children: [/* @__PURE__ */ jsx("h2", {
2793
+ id: filtersPaneHeadingId,
2794
+ className: "text-base font-medium text-foreground",
2795
+ children: "All filters"
2796
+ }), /* @__PURE__ */ jsx(Button, {
2797
+ ref: filtersPaneCloseButtonRef,
2798
+ type: "button",
2799
+ variant: "ghost",
2800
+ size: "xs",
2801
+ iconOnly: true,
2802
+ "aria-label": "Close filters",
2803
+ onClick: () => setFiltersPaneOpen(false),
2804
+ children: /* @__PURE__ */ jsx(XIcon, {})
2805
+ })]
2806
+ }),
2807
+ /* @__PURE__ */ jsx("div", {
2808
+ className: "-m-1 flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-1",
2809
+ children: filterableColumns.map((column) => {
2810
+ const pinned = pinnedFilterIds.includes(column.id);
2811
+ const label = columnLabel(column);
2812
+ return /* @__PURE__ */ jsxs("div", {
2813
+ className: "group/filter-row flex flex-col gap-1.5",
2814
+ children: [/* @__PURE__ */ jsxs("div", {
2815
+ className: "flex items-center justify-between gap-2",
2816
+ children: [/* @__PURE__ */ jsxs(Label, { children: [label, !column.getIsVisible() && !column.columnDef.meta?.filterOnly && /* @__PURE__ */ jsx("span", {
2817
+ className: "ml-1 font-normal text-muted-foreground",
2818
+ children: "(hidden)"
2819
+ })] }), allowFilterPinning && !column.columnDef.meta?.filterOnly && /* @__PURE__ */ jsx(Button, {
2820
+ type: "button",
2821
+ variant: "ghost",
2822
+ size: "xs",
2823
+ iconOnly: true,
2824
+ "aria-label": pinned ? `Unpin ${label}` : `Pin ${label}`,
2825
+ className: hoverRevealFilterRowClass,
2826
+ onClick: () => toggleFilterPin(column.id),
2827
+ children: pinned ? /* @__PURE__ */ jsx(PinOffIcon, { className: "size-3.5" }) : /* @__PURE__ */ jsx(PinIcon, { className: "size-3.5" })
2828
+ })]
2829
+ }), /* @__PURE__ */ jsx(PaneColumnFilterField, {
2830
+ column,
2831
+ showFilterCounts,
2832
+ tableDeferred: filterApplyMode === "deferred",
2833
+ stickyFilterSelectAll
2834
+ })]
2835
+ }, column.id);
2836
+ })
2837
+ }),
2838
+ /* @__PURE__ */ jsx("div", {
2839
+ className: "-mx-(--card-spacing,1rem) -mb-(--card-spacing,1rem) flex w-[calc(100%+var(--card-spacing,1rem)*2)] items-center justify-center border-t border-border px-(--card-spacing,1rem) py-1.5",
2840
+ children: /* @__PURE__ */ jsx(Button, {
2841
+ type: "button",
2842
+ variant: "ghost",
2843
+ size: controlSize,
2844
+ className: "w-full",
2845
+ disabled: columnFilters.length === 0,
2846
+ onClick: () => setColumnFilters([]),
2847
+ children: "Clear filters"
2848
+ })
2849
+ })
2850
+ ]
2851
+ })]
2852
+ });
2853
+ }
2854
+ /**
2855
+ * WHEN TO USE
2856
+ * - Tabular data that needs sorting, search, column filters, selectable
2857
+ * rows with bulk actions, row actions, expandable detail rows, row-click
2858
+ * navigation, AI affordances, column management, or pagination handled
2859
+ * for you — an admin list, a resource picker, anything past "just render
2860
+ * some rows."
2861
+ *
2862
+ * NOT WHEN
2863
+ * - A fixed, small table with no interaction beyond display — use Table
2864
+ * directly, it's lighter and this adds nothing over it.
2865
+ * - You need virtualization or server-driven async fetching — neither is
2866
+ * built in this pass. Virtualization has no existing MMDS primitive to
2867
+ * lean on and would need a further headless dependency (e.g.
2868
+ * `@tanstack/react-virtual`) — out of scope here. (Column resize is
2869
+ * already built — `enableColumnResizing` — this isn't that.)
2870
+ *
2871
+ * KEY RULES
2872
+ * - `columns`/`data` follow `@tanstack/react-table`'s `ColumnDef<TData>[]`
2873
+ * shape directly — this component doesn't wrap or reinvent that API, it
2874
+ * renders it. Read TanStack Table's own docs for `accessorKey`, custom
2875
+ * `cell` renderers, `enableSorting: false` per column, etc.
2876
+ * - Sorting, filtering, search, selection, expansion, column order/
2877
+ * visibility, and pagination state all live inside DataTable (via
2878
+ * `useReactTable`) — an exception to MMDS's usual fully-uncontrolled
2879
+ * convention, because the whole point of this component is to own that
2880
+ * state so you don't have to. `onRowSelectionChange` is one escape
2881
+ * hatch, for wiring selection to something outside (a bulk delete call, a
2882
+ * parent form); `onStateChange` is another, firing sorting/columnFilters/
2883
+ * globalFilter live for the same reason (a URL param, analytics, a
2884
+ * server-driven refetch) — read-only, no live two-way binding, but
2885
+ * paired with `defaultSorting`/`defaultColumnFilters`/`defaultGlobalFilter`
2886
+ * (seeding each on first render) it's enough to round-trip state out to
2887
+ * a URL and back in on next mount. `defaultPinnedFilterIds` seeds the
2888
+ * toolbar's inline-filter set the same way — all four still fully
2889
+ * uncontrolled after first render, same as everything else here.
2890
+ * - Columns render at a fixed width (`table-fixed` + a `<colgroup>` driven by
2891
+ * each column's `ColumnDef.size`, default 150) instead of auto-fitting to
2892
+ * content — set `size` per column for anything narrower/wider than that.
2893
+ * This trades content-driven auto-sizing for widths that stay put across
2894
+ * sorting, filtering, and pagination.
2895
+ * - The toolbar carries the same edge-to-edge treatment as `Table` itself
2896
+ * (see Table's own KEY RULES) — its outer box matches the table's wider
2897
+ * footprint instead of looking narrower next to it, and its own padding
2898
+ * keeps search/filter/Columns content at the same inset as the table's
2899
+ * cell content. Automatic, no prop for it.
2900
+ * - Toolbar controls (search, inline/"All filters" filters, Columns) and
2901
+ * footer controls (items-per-page, page-jump, Previous/Next) render one
2902
+ * size notch below `size` itself — "default" gets "sm" controls, "sm"
2903
+ * gets "xs", "xs" stays "xs" (already the floor). They're always-visible,
2904
+ * repetitive chrome, not row content competing for the same emphasis —
2905
+ * automatic, not a prop, and it scales with `size` rather than being
2906
+ * fixed at one density. The `aiAction` button is the deliberate exception
2907
+ * — it's meant to stand out, so it stays at `size` itself.
2908
+ * - `loading` swaps every cell for a `Skeleton` shaped to the real columns —
2909
+ * don't also filter `data` to empty while loading, pass the real (stale
2910
+ * or previous) data and let `loading` cover it, or column widths jump.
2911
+ * - `error` (a boolean) swaps the whole body for an Empty-based failure
2912
+ * state — same composition the error-state pattern uses elsewhere
2913
+ * (Patterns/UI States), just pre-wired here — and `errorDescription`
2914
+ * supplies its copy (default "Something went wrong. Try again."). Two
2915
+ * props on purpose: whether it failed and what to say are different
2916
+ * things; passing the copy as `error` itself still works but is
2917
+ * deprecated. A plain empty `data` array
2918
+ * (not loading, no error) renders the "no results" Empty state instead.
2919
+ * - `enableRowSelection` injects a checkbox column automatically — don't
2920
+ * add your own selection column to `columns`.
2921
+ * - Row actions are just a normal column whose `cell` renders the exported
2922
+ * `RowActions` helper (inline icon buttons for ≤2 actions, an overflow
2923
+ * menu past that) — there's no dedicated `actions` prop, this is the same
2924
+ * "put a Button in a cell" technique any custom `cell` already uses.
2925
+ * `RowActions` stops its own clicks from bubbling, so it's safe inside a
2926
+ * row that also sets `onRowClick`.
2927
+ * - `renderExpandedRow` adds a leading expand/collapse toggle to every row;
2928
+ * expanding inserts a full-width row below it with whatever you return —
2929
+ * a detail card, or a nested table of its own. The toggle stops its click
2930
+ * from bubbling, so it composes with `onRowClick` too.
2931
+ * - `onRowClick` makes the whole row clickable and keyboard-activatable
2932
+ * (Enter/Space), for row→detail navigation. It composes with
2933
+ * `enableRowSelection` — the selection checkbox stops its own click from
2934
+ * bubbling up, so checking a row never also triggers `onRowClick`. Any
2935
+ * other interactive element you put inside a cell (a button, a link)
2936
+ * needs its own `stopPropagation` the same way if it shouldn't also
2937
+ * trigger the row click.
2938
+ * - `striped` and `cellDividers` are independent visual modifiers — zebra
2939
+ * row tinting and vertical column rules, respectively — for tables where
2940
+ * plain row/column boundaries are hard to scan.
2941
+ * - AI presence has a distinct answer at each level, but DataTable's own
2942
+ * outer surface never takes a border/shadow of its own — there's no `ai`
2943
+ * boolean prop. For whole-table AI presence, the table just sits inside
2944
+ * an AI Card — the exact same recipe AI/Card's "BoundaryTreatments" story uses (a
2945
+ * `bg-gradient-to-r from-ai-wash to-ai-grad-from p-px` wrapper div, not
2946
+ * a one-off DataTable-specific border) — plus a soft inward glow rising
2947
+ * from the Card's own bottom edge (`bg-ai-glow-bottom`, fading out at 15%
2948
+ * of the Card's own height) as a plain Card className — a background-image,
2949
+ * so it layers over Card's own `bg-card` (`cn` registers the glow classes
2950
+ * as background-image utilities so tailwind-merge keeps both).
2951
+ * `AILabel` sits in the
2952
+ * Card's top corner (a `CardAction`) — see the "WithAIWholeTable" story.
2953
+ * The whole-table treatment is a boundary (gradient outline), an inward
2954
+ * bottom glow and a corner marker — not the row/column glow used below.
2955
+ * Below that: `aiAction` (table level)
2956
+ * renders a `variant="ai"` toolbar button; `aiRowPredicate` (row level)
2957
+ * puts a leading `AILabel` as the very first thing in the row — ahead
2958
+ * of the expand toggle and selection checkbox, its own leading slot —
2959
+ * plus a colored left edge (an inset box-shadow on the row's first
2960
+ * cell, not `border-l` on the `<tr>` — the table is `border-separate`
2961
+ * for pinned columns, and in that model a `<tr>` never paints borders)
2962
+ * and a soft inward glow (`bg-ai-glow`, fading out at 20% of the row's own
2963
+ * width, a background-image, not a solid fill) bleeding in from that
2964
+ * same left edge — plus a second copy of that same glow painted directly
2965
+ * on every left-pinned cell (however many — the injected AI column and a
2966
+ * genuinely pinned column can both land in that same sticky group, see
2967
+ * `effectiveColumnPinning` above), needed because pinning makes each
2968
+ * `position: sticky` with its own opaque background, so the `<tr>`'s copy
2969
+ * (anchored to the row's original, unscrolled left edge) would otherwise
2970
+ * end up hidden behind them once the table scrolls horizontally. Each
2971
+ * copy is sized via an explicit `backgroundSize` (the row's real total
2972
+ * width, from `table.getTotalSize()`, not that cell's own narrower one)
2973
+ * and shifted by `backgroundPositionX: -column.getStart("left")` so it
2974
+ * shows the exact slice of that same gradient that falls at its actual
2975
+ * position in the row — the whole pinned group reads as one continuous
2976
+ * glow, pixel-identical to the unpinned case, rather than a smaller
2977
+ * version of it; `aiColumnIds` (column level)
2978
+ * adds the same left-edge glow down the whole column, header cell
2979
+ * included (every cell in a column shares the same width, so 20% of it
2980
+ * resolves to the same px value down the whole column, tiling into one
2981
+ * continuous glow with no visible seam), a colored top rule on that
2982
+ * column's header cell
2983
+ * (mirrored by the row's own left edge above), and a leading
2984
+ * `AILabel` (generic copy) automatically in that header;
2985
+ * `aiCellPredicate` (cell level) adds just a trailing `AILabel` inline
2986
+ * after the value — no glow, no border, since a single AI-touched value
2987
+ * doesn't need its own boundary treatment, only the explainability
2988
+ * entry point. Every `AILabelTrigger` here follows DataTable's own
2989
+ * density prop — "default" at default density, "sm" at both sm and xs
2990
+ * — never AILabel's `iconOnly`, which is opt-in only for when a
2991
+ * designer explicitly asks for it. Give an AI-marked column enough
2992
+ * width for its title plus the visible "AI" marker (the header keeps
2993
+ * both; the title truncates first). Row/column backgrounds use
2994
+ * `background-image` rather than `box-shadow` specifically for the row
2995
+ * case — `box-shadow` doesn't paint reliably on `<tr>` in every
2996
+ * browser, `background-image` does. Compose `AILabel` directly into a
2997
+ * column's own `header`/`cell` instead of `aiColumnIds` when a column
2998
+ * needs its own specific explanation text.
2999
+ * - `meta: { align: "left" | "right" }` (default `"left"`) aligns both a
3000
+ * column's header and its cell content — the guideline is quantitative
3001
+ * content (amounts, counts, percentages, anything meant to be compared
3002
+ * or summed down the column) gets `"right"`, text stays `"left"`. Cell
3003
+ * alignment uses `flex justify-end`, not just `text-align`, so it also
3004
+ * right-aligns a custom `cell` that renders its own Badge/icon cluster,
3005
+ * not only plain text. `"right"` mirrors the header too — the sort
3006
+ * button's label moves to that side and the filter icon/`headerActions`
3007
+ * cluster moves to the *other* side (`flex-row-reverse` on the same row,
3008
+ * not a second layout to maintain), so a numeric column's controls sit
3009
+ * off to the side its digits don't occupy.
3010
+ * - Search matches only `searchableColumnIds` (default: the first two
3011
+ * entries in `columns`) — set it explicitly to widen/narrow the scope.
3012
+ * Matches highlight automatically for plain columns (no custom `cell`);
3013
+ * for a column with a custom `cell`, highlight it yourself with the
3014
+ * exported `HighlightMatch`, reading the query off
3015
+ * `info.table.getState().globalFilter`.
3016
+ * - Every column gets a filter icon in its header cell. In the toolbar,
3017
+ * filters show inline right after search — all of them if there are 3 or
3018
+ * fewer filterable columns, otherwise just the pinned ones (the first two
3019
+ * by default, and however many the user pins from the pane after that —
3020
+ * uncapped, the row wraps onto more lines rather than overflowing) plus
3021
+ * an "All filters" button, shown whenever there are more than 3
3022
+ * filterable columns at all — regardless of how many are currently
3023
+ * pinned inline, even every one of them, since the pane is still the
3024
+ * only place to *unpin* one. That button opens a docked pane, not a
3025
+ * floating overlay — the table area (`flex-1`) shrinks to make room for
3026
+ * it on the surface DataTable sits on, instead of the pane covering the
3027
+ * table, and reaches that surface's true top/right/bottom edge (measured
3028
+ * in JS against the nearest Card, since a header/footer's height isn't a
3029
+ * fixed token) while never growing taller than that surface — its own
3030
+ * filter-list area is a `min-h-0` flex child so a long list scrolls
3031
+ * internally instead of pushing the pane past the surface's edge. While
3032
+ * the pane is open, the inline pinned filter buttons and the "All
3033
+ * filters" trigger both hide — every filterable column and its current
3034
+ * value already lives in the pane itself, each with a pin/unpin toggle
3035
+ * (hover/keyboard-focus-revealed by default, like the header cell's own
3036
+ * controls — see `revealColumnControlsOnHover`), so there's one place to
3037
+ * look, not two copies of the same control. `allowFilterPinning` (default
3038
+ * `true`) turns that toggle off entirely for a designer-fixed toolbar —
3039
+ * `defaultPinnedFilterIds` (or its first-two default) becomes the
3040
+ * permanent inline set, with no user-facing way to change which columns
3041
+ * those are; every other filterable column still lives in the pane, just
3042
+ * with nothing to move it out. Both the inline pinned chips and the pane
3043
+ * list order their filterable columns by the table's own column order by
3044
+ * default; set `meta: { filterOrder: N }` on a `ColumnDef` to decouple a
3045
+ * filter's display position from where its column currently sits — handy
3046
+ * when drag-and-drop/the Columns menu can reorder columns but a filter
3047
+ * should stay put regardless. Once
3048
+ * any filter is set, the toolbar shows "N filters applied" (every
3049
+ * applied filter, inline pinned ones included, not just pane-only ones)
3050
+ * next to a "Clear filters" button that clears all of them the same way
3051
+ * — the pane's own footer "Clear filters" does the same, just from
3052
+ * inside the pane. Default filter is text-contains; set
3053
+ * `meta: { filterVariant: "select" }` on a `ColumnDef` for a
3054
+ * multi-select filter instead — a checklist in the header icon's own
3055
+ * popover; a `SelectMultiple` for the inline pinned button (its
3056
+ * trigger's placeholder is the column label, its value only ever the
3057
+ * first selected value's label, `maxLabelChars={12}` ellipsis-truncating
3058
+ * that one past 12 characters even with just one selected, plus "+N"
3059
+ * once more than one is — so neither a single long value nor a long list
3060
+ * can run the toolbar out of room) and for the pane (which holds every
3061
+ * filterable column at once). A multiselect filter (`"select"` or
3062
+ * `"range"`) always requires a deliberate Apply — in every surface,
3063
+ * regardless of `filterApplyMode` — since checking several boxes is a
3064
+ * set to review before committing, not a single complete thought; picks
3065
+ * still show as checked/selected immediately, on the trigger and in the
3066
+ * list both — only the real filtering of the table's rows waits for
3067
+ * Apply, not the visible selection itself. Closed (nothing open to
3068
+ * preview into), the trigger always reflects whatever's actually
3069
+ * committed. Its Cancel/Apply footer always lives inside that one
3070
+ * field's own popover — the header icon's, the inline pinned button's,
3071
+ * or (in the pane) that field's own, not a pane-wide batch, even though
3072
+ * the pane holds every filterable column open at once; each field there
3073
+ * resolves independently, exactly like the toolbar's. `filterVariant:
3074
+ * "singleSelect"` picks exactly one value from that same kind of list —
3075
+ * `SelectMultiple` capped at one value (`closeOnSelect`), in every
3076
+ * surface including the pane, not a native-feeling dropdown — and,
3077
+ * since it closes the instant a pick is made, is never itself deferred
3078
+ * regardless of `filterApplyMode` (no open popover left by the time a
3079
+ * footer could matter). `meta: { filterRender: (props) => node }` is
3080
+ * the escape hatch past those four built-in shapes — a date-range
3081
+ * picker, a slider, a radio group, colored selection badges, anything a
3082
+ * designer's own filter needs — given the same `{ column, value,
3083
+ * onValueChange }` every built-in variant already receives. Deferred
3084
+ * under `filterApplyMode="deferred"` in every surface, not just the
3085
+ * header-icon popover: the inline toolbar button wraps it in a Popover +
3086
+ * the same shared Cancel/Apply footer text filters get there; the pane —
3087
+ * which has no popover for any variant — adds an inline Cancel/Apply row
3088
+ * directly under the rendered content instead. Immediate, every surface
3089
+ * renders it with no extra chrome, same as always.
3090
+ * - `meta: { filterOnly: true }` defines a column purely to filter a data
3091
+ * point that isn't shown anywhere in the table — its `header`/`cell`
3092
+ * never render, and unlike a plain hidden column it has no checkbox
3093
+ * anywhere to show it (excluded from the Columns menu entirely, not just
3094
+ * defaulted off), while still showing up as a normal filterable column in
3095
+ * the "All filters" pane — never inline in the toolbar, and never
3096
+ * pinnable there either (no header cell for either to point back at).
3097
+ * Its presence alone forces the pane to exist even when 3 or fewer
3098
+ * *other* columns are filterable, since the pane is its only access
3099
+ * point.
3100
+ * - `meta: { filterMergeWith: [...ids] }` folds other columns' filter
3101
+ * fields into *this* column's own header-icon popover instead of each
3102
+ * getting a separate toolbar/pane entry — for a cell whose custom `cell`
3103
+ * already displays more than one filterable value at once (e.g. a name
3104
+ * plus an MRN below it), so filtering either reads as one decision from
3105
+ * one icon. Pairs naturally with `filterOnly` on the merged column(s),
3106
+ * since the value being merged in is usually already folded into this
3107
+ * column's own `cell` rather than shown in a column of its own — but
3108
+ * that's a convention, not a requirement. Each merged field renders
3109
+ * below this column's own (always first, unlabeled), under a divider,
3110
+ * labeled with its own `header`. The icon fills, and the
3111
+ * `filterApplyMode="deferred"` footer commits/discards, based on any of
3112
+ * them having an active filter — not just this column's own.
3113
+ * - Column reordering lives only in the Columns menu, not the header
3114
+ * cells themselves — drag a row there (a thin primary-colored line
3115
+ * marks exactly where it'll land) or use its Move left/Move right
3116
+ * buttons — real, individually focusable/clickable buttons, so no
3117
+ * custom keyboard handling is needed for them to be keyboard-operable,
3118
+ * unlike the mouse-only drag. They say left/right, not up/down, even
3119
+ * though the list itself is vertical — that's what the button actually
3120
+ * does to the real table, which is what the label should describe, not
3121
+ * the popover's own incidental layout. Every one of these buttons —
3122
+ * Move left/right, and (once a column can pin) Pin left/Pin right or
3123
+ * "Unpin" using the same icons `columnMenuActions` renders in the
3124
+ * header — is hover/keyboard-focus-revealed by default (see
3125
+ * `revealColumnControlsOnHover`) and carries its own Tooltip labelling
3126
+ * it. Once any column is pinned, the list splits into up to three
3127
+ * sections — pinned left, unpinned/center, pinned right, each separated
3128
+ * by its own divider — mirroring the same grouping the real header/body
3129
+ * cells render in; reordering (drag or the Move buttons) stays confined
3130
+ * to one section at a time, matching what a plain visual drag could
3131
+ * mean anyway. Moving a column *into* or *out of* a pinned section is
3132
+ * what the Pin left/Pin right/Unpin buttons are for instead. Past 10
3133
+ * manageable columns the list grows a search box and scrolls instead of
3134
+ * pushing the menu taller. Visibility, order, and pinning changes apply
3135
+ * live as you make them (so the menu's own rows and the real table
3136
+ * never disagree), but the menu's footer still offers Cancel (reverts
3137
+ * all three to whatever was committed when the menu opened, including
3138
+ * if you dismiss it by clicking outside or Escape) and Apply (disabled
3139
+ * until at least one of the three has actually changed since the menu
3140
+ * opened) — so the whole session reads as one reviewable batch of
3141
+ * changes, not silent live edits. "Reset to defaults" sits in that same
3142
+ * footer, only rendered once order, hidden columns, or pinning have
3143
+ * actually changed from default — a different comparison than Apply's
3144
+ * (against the dev-configured default, not against this session's own
3145
+ * starting point).
3146
+ * - `lockedColumnIds` marks a column as fully static: the user can't
3147
+ * reorder it (shows a lock glyph instead of a drag handle, and isn't a
3148
+ * valid drop target) or hide it (the show/hide checkbox doesn't even
3149
+ * render — `enableHiding: false` is forced regardless of what the
3150
+ * column's own `ColumnDef` says). Every injected column (selection,
3151
+ * expand, AI) is always locked. Column-visibility control (the checkbox
3152
+ * on each row) also only lists columns that don't set
3153
+ * `enableHiding: false` themselves — non-hideable columns still show up
3154
+ * for reordering.
3155
+ * - Column pinning: set `meta: { pin: "left" | "right" }` on a `ColumnDef`
3156
+ * to seed a column's *initial* pinned side. From there it's live,
3157
+ * user-togglable `@tanstack/react-table` state — wire the exported
3158
+ * `columnMenuActions` into `meta.headerActions` for a ready-made Pin
3159
+ * left/Pin right toggle (see below), or call `column.pin(...)` from any
3160
+ * custom action of your own. Any number of columns can pin to the same
3161
+ * side at once — they render as a contiguous group at that edge, in the
3162
+ * order they were pinned (reorderable afterward from the Columns menu,
3163
+ * confined to that group — see above), with a divider towards whichever
3164
+ * edge actually borders the scrollable/center columns (the left group's
3165
+ * right edge, the right group's left edge) on top of the solid
3166
+ * background every pinned cell already gets so scrolled content doesn't
3167
+ * show through it. Set `meta: { pinLocked: true }` to keep a column's
3168
+ * pin fully dev-controlled with no user-facing toggle (`columnMenuActions`
3169
+ * omits both Pin actions for it — a custom `headerActions` entry could
3170
+ * still call `column.pin()` on it directly, since this only suppresses
3171
+ * the built-in affordance, not the underlying state). A pinned column
3172
+ * renders `position: sticky` at the correct cumulative offset, so it
3173
+ * stays in view while the rest of the table scrolls horizontally —
3174
+ * hover/selected/striped row tinting won't show through a pinned cell
3175
+ * either, a known trade-off of frozen columns. Pinning a real column left
3176
+ * also pulls the injected AI/expand/selection columns (whichever are
3177
+ * present) into that same left-pinned group ahead of it, live — so they
3178
+ * stay genuinely first and sticky too, matching their own "leading slot"
3179
+ * documentation, instead of being pushed behind a real pinned column the
3180
+ * moment one exists. Only kicks in once something's actually pinned left;
3181
+ * otherwise these render exactly as before (unpinned, first in the
3182
+ * unpinned/center group by definition order — no sticky, no divider).
3183
+ * Nothing to configure — this is automatic and not itself user-togglable.
3184
+ * - "Sticky first column" isn't its own prop — it's this same pinning
3185
+ * recipe, combined with `lockedColumnIds` so the user can't unpin or
3186
+ * reorder it away from that edge: `defaultColumnPinning={{ left: ["id"] }}`
3187
+ * plus `lockedColumnIds={["id"]}` (see `ColumnManagement`).
3188
+ * - `meta.headerActions` adds custom actions to a column's header cell,
3189
+ * next to its sort button and filter icon — hidden until that header
3190
+ * cell is hovered or keyboard-focused (see `revealColumnControlsOnHover`
3191
+ * below), and rendered with the same exported `RowActions` helper row
3192
+ * actions use (inline for ≤2 actions, an overflow menu past that),
3193
+ * uniformly for every column including pinned ones. It's a function of
3194
+ * `(column, table)`, not a static array, so it can reflect live
3195
+ * per-column state. The exported `columnMenuActions` is the ready-made
3196
+ * bundle for this slot — Pin left/Pin right (each becomes "Unpin" once
3197
+ * pinned to that side, and both are omitted for a `pinLocked` column),
3198
+ * Hide column, and Fit to content, each omitted when the column can't
3199
+ * do it. Being 3-4 actions, it renders as a single overflow "more" menu
3200
+ * in practice — the same shape as a plain custom `headerActions`
3201
+ * function returning your own `RowAction[]` would, if you need
3202
+ * something `columnMenuActions` doesn't cover.
3203
+ * - `revealColumnControlsOnHover` (default `true`) is the one knob for
3204
+ * whether the filter icon, `headerActions`/`columnMenuActions`, and the
3205
+ * Columns menu's own pin/move controls stay hidden until hovered or
3206
+ * keyboard-focused, or render always-visible — set `false` for a
3207
+ * consumer whose final UX calls for every control visible up front.
3208
+ * - `enableColumnResizing` adds a drag handle to every resizable column's
3209
+ * right border (`ColumnDef.enableResizing: false` exempts one; injected
3210
+ * columns already are). It's also what unlocks `columnMenuActions`' "Fit
3211
+ * to content" — that action measures every currently-rendered cell in
3212
+ * the column (this page's rows, not the whole dataset) via `scrollWidth`
3213
+ * and sets the column to the widest one found.
3214
+ * - Bulk actions while any row is selected land in one of two places,
3215
+ * never both:
3216
+ * - `onLoadMore` (infinite scroll, no footer): the toolbar stays
3217
+ * mounted and interactive (search, filters) — searching/filtering
3218
+ * the rows loaded so far while some of them are selected is exactly
3219
+ * the "load more, then narrow down what's loaded" flow this mode is
3220
+ * for. Instead, a floating bar hovers over the table's own bottom
3221
+ * edge (never the raw viewport). Three sections divided by a line:
3222
+ * count + "Select all" (every filtered row across everything loaded
3223
+ * so far, not just what's rendered), `renderBulkActions`'s own
3224
+ * product-configured actions in the middle, and a close button that
3225
+ * clears the selection. Scrolling to the last row while any row is
3226
+ * selected reveals extra bottom padding sized to the bar's own
3227
+ * height — otherwise the last row(s) would stay hidden behind it
3228
+ * permanently.
3229
+ * - Paginated (a real footer): no floating bar, and the toolbar's own
3230
+ * search/filter controls are swapped out in place for the selection
3231
+ * itself — count, "Select all" (every filtered row across every
3232
+ * page, not just the current one — same as infinite scroll's own),
3233
+ * `renderBulkActions`'s actions, then a close button. Search/filters
3234
+ * act on rows that may not even be the ones selected once paging is
3235
+ * involved, so showing both the selection and a still-live search
3236
+ * box read as two competing toolbars — this is the table's original
3237
+ * selection behavior, kept for exactly that reason.
3238
+ * - `getRowId` (e.g. `(row) => row.id`) gives each row a stable identity
3239
+ * across a sort/filter/pagination change, passed straight through to
3240
+ * `@tanstack/react-table` — without it, a row's identity defaults to its
3241
+ * index, which is meaningless the moment the row set changes underneath
3242
+ * it (filtering 100 rows down to 30 shouldn't treat "row 3" before and
3243
+ * after as the same thing unless it actually is).
3244
+ * - Footer is a full-width bar with the current range/total first, then
3245
+ * "Items per page" (`pageSizeOptions`, default `[10, 20, 30]`; set
3246
+ * `showPageSizeSelector={false}` to drop it — a design call for tables
3247
+ * where the consumer shouldn't be choosing page size, not a user
3248
+ * preference), and — past one page — a jump-to-page select plus
3249
+ * Previous/Next. It carries the same
3250
+ * edge-to-edge treatment as the toolbar and `Table` itself, but on all
3251
+ * three sides it borders — left, right, *and* bottom — so on a Card it
3252
+ * reads as that Card's own footer (same border-t/bg-muted/rounded-b-xl
3253
+ * look as a real CardFooter, and it borrows CardFooter's own
3254
+ * `data-slot` so a surrounding Card zeroes its bottom padding for it
3255
+ * too).
3256
+ * - `onLoadMore` switches to infinite scroll instead of pagination: every
3257
+ * row in `data` renders (no client-side page slicing), there's no footer
3258
+ * at all, and the table scrolls inside its own `maxHeight`-bounded
3259
+ * container (default `32rem`) instead of the page — the header (and any
3260
+ * pinned columns) stay locked in place via `position: sticky` while the
3261
+ * body scrolls. `onLoadMore` fires once an invisible sentinel below the
3262
+ * last row scrolls near-into view. Fetch the next page and append it to
3263
+ * `data` yourself; `hasMore` stops rendering the sentinel once there's
3264
+ * nothing left, and `loadingMore` renders 3 skeleton rows below the last
3265
+ * real one — same shape as `loading`, not a spinner — while a fetch is
3266
+ * in flight. Sorting/filtering still run client-side over whatever's
3267
+ * currently loaded — an inherent limitation with partial data, not
3268
+ * something this mode solves.
3269
+ *
3270
+ * USAGE
3271
+ * const columns: ColumnDef<Invoice>[] = [
3272
+ * { accessorKey: "id", header: "Invoice" },
3273
+ * { accessorKey: "amount", header: "Amount" },
3274
+ * { accessorKey: "status", header: "Status", meta: { filterVariant: "select" } },
3275
+ * ]
3276
+ *
3277
+ * <DataTable columns={columns} data={invoices} aria-label="Invoices" />
3278
+ *
3279
+ * // Selectable rows with a bulk-delete action — the count (swapped
3280
+ * // into the toolbar here, since this table paginates), "Select all",
3281
+ * // and the close/clear button are all built in; this only fills the
3282
+ * // actions themselves
3283
+ * <DataTable
3284
+ * columns={columns}
3285
+ * data={invoices}
3286
+ * enableRowSelection
3287
+ * renderBulkActions={(selected, clear) => (
3288
+ * <Button size="sm" variant="ghost" onClick={() => { onDelete(selected); clear() }}>
3289
+ * <TrashIcon />
3290
+ * Delete
3291
+ * </Button>
3292
+ * )}
3293
+ * />
3294
+ *
3295
+ * // Row actions (inline for ≤2, overflow menu for >2)
3296
+ * const withActions: ColumnDef<Invoice>[] = [
3297
+ * ...columns,
3298
+ * {
3299
+ * id: "actions",
3300
+ * header: "",
3301
+ * size: 72,
3302
+ * enableSorting: false,
3303
+ * enableHiding: false,
3304
+ * enableColumnFilter: false,
3305
+ * cell: ({ row }) => (
3306
+ * <RowActions
3307
+ * actions={[
3308
+ * { label: "Edit", icon: PencilIcon, onClick: () => onEdit(row.original) },
3309
+ * { label: "Delete", icon: TrashIcon, variant: "destructive", onClick: () => onDelete(row.original) },
3310
+ * ]}
3311
+ * />
3312
+ * ),
3313
+ * },
3314
+ * ]
3315
+ *
3316
+ * // Header-cell actions: a fully custom one, for anything columnMenuActions
3317
+ * // doesn't cover
3318
+ * const withHeaderActions: ColumnDef<Invoice>[] = columns.map((col) =>
3319
+ * col.id === "status"
3320
+ * ? {
3321
+ * ...col,
3322
+ * meta: {
3323
+ * ...col.meta,
3324
+ * headerActions: () => [
3325
+ * { label: "Search", icon: SearchIcon, onClick: () => openColumnSearch("status") },
3326
+ * ],
3327
+ * },
3328
+ * }
3329
+ * : col
3330
+ * )
3331
+ *
3332
+ * // Column resizing (drag handles) plus the ready-made Pin left/Pin
3333
+ * // right/Hide/Fit-to-content menu on every column
3334
+ * const withColumnMenu: ColumnDef<Invoice>[] = columns.map((col) => ({
3335
+ * ...col,
3336
+ * meta: { ...col.meta, headerActions: columnMenuActions },
3337
+ * }))
3338
+ * <DataTable columns={withColumnMenu} data={invoices} enableColumnResizing />
3339
+ *
3340
+ * // Right-align a quantitative column — header and cells both flip
3341
+ * const withAlignedAmount: ColumnDef<Invoice>[] = columns.map((col) =>
3342
+ * col.id === "amount"
3343
+ * ? { ...col, meta: { ...col.meta, align: "right" } }
3344
+ * : col
3345
+ * )
3346
+ *
3347
+ * // Expandable detail row, striping, and column dividers
3348
+ * <DataTable
3349
+ * columns={columns}
3350
+ * data={invoices}
3351
+ * renderExpandedRow={(invoice) => <InvoiceDetailCard invoice={invoice} />}
3352
+ * striped
3353
+ * cellDividers
3354
+ * />
3355
+ *
3356
+ * // AI presence at every level (except whole-table — wrap in a Card for that, see below)
3357
+ * <DataTable
3358
+ * columns={columns}
3359
+ * data={invoices}
3360
+ * aiAction={{ label: "Summarize", onClick: () => runAISummary(invoices) }}
3361
+ * aiRowPredicate={(invoice) => invoice.flaggedByAI}
3362
+ * aiColumnIds={["amount"]}
3363
+ * aiCellPredicate={(invoice, columnId) => columnId === "customer" && invoice.customerSuggestedByAI}
3364
+ * />
3365
+ *
3366
+ * // Whole-table AI presence: the table just sits inside an AI Card (the
3367
+ * // same recipe AI/Card's "BoundaryTreatments" story uses) plus a soft glow rising from
3368
+ * // the Card's own bottom edge (bg-ai-glow-bottom, a plain Card className) —
3369
+ * // AILabel in its top corner, never on DataTable's own surface
3370
+ * <div className="rounded-xl bg-gradient-to-r from-ai-wash to-ai-grad-from p-px">
3371
+ * <Card className="w-full bg-ai-glow-bottom">
3372
+ * <CardHeader>
3373
+ * <CardTitle>Invoices</CardTitle>
3374
+ * <CardAction>
3375
+ * <AILabel>
3376
+ * <AILabelTrigger />
3377
+ * <AILabelContent>...</AILabelContent>
3378
+ * </AILabel>
3379
+ * </CardAction>
3380
+ * </CardHeader>
3381
+ * <CardContent>
3382
+ * <DataTable columns={columns} data={invoices} />
3383
+ * </CardContent>
3384
+ * </Card>
3385
+ * </div>
3386
+ *
3387
+ * // Loading and error states
3388
+ * <DataTable columns={columns} data={[]} loading />
3389
+ * <DataTable columns={columns} data={[]} error errorDescription="Couldn't reach the server." />
3390
+ *
3391
+ * // Clickable rows (e.g. navigate to a detail view), a locked first column,
3392
+ * // and a wider items-per-page range
3393
+ * <DataTable
3394
+ * columns={columns}
3395
+ * data={invoices}
3396
+ * onRowClick={(invoice) => router.push(`/invoices/${invoice.id}`)}
3397
+ * lockedColumnIds={["id"]}
3398
+ * pageSizeOptions={[10, 25, 50]}
3399
+ * />
3400
+ *
3401
+ * // Drop the "Items per page" control from the static footer — page size
3402
+ * // stays fixed at whatever `pageSize` is, not something the user picks
3403
+ * <DataTable columns={columns} data={invoices} showPageSizeSelector={false} />
3404
+ *
3405
+ * // Infinite scroll instead of pagination — no footer
3406
+ * const [invoices, setInvoices] = useState(firstPage)
3407
+ * const [loadingMore, setLoadingMore] = useState(false)
3408
+ * <DataTable
3409
+ * columns={columns}
3410
+ * data={invoices}
3411
+ * hasMore={invoices.length < total}
3412
+ * loadingMore={loadingMore}
3413
+ * onLoadMore={async () => {
3414
+ * setLoadingMore(true)
3415
+ * setInvoices((current) => [...current, ...await fetchNextPage(current.length)])
3416
+ * setLoadingMore(false)
3417
+ * }}
3418
+ * />
3419
+ */
3420
+ //#endregion
3421
+ export { DataTable, HighlightMatch, RowActions, columnMenuActions };
3422
+
3423
+ //# sourceMappingURL=data-table.js.map