@health-samurai/react-components 0.0.0-alpha.4 → 0.0.0-alpha.6

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 (304) hide show
  1. package/dist/bundle.css +1375 -484
  2. package/dist/src/components/code-editor/http/grammar/http.d.ts +3 -0
  3. package/dist/src/components/code-editor/http/grammar/http.d.ts.map +1 -0
  4. package/dist/src/components/code-editor/http/grammar/http.grammar +74 -0
  5. package/dist/src/components/code-editor/http/grammar/http.js +38 -0
  6. package/dist/src/components/code-editor/http/grammar/http.js.map +1 -0
  7. package/dist/src/components/code-editor/http/grammar/http.terms.d.ts +2 -0
  8. package/dist/src/components/code-editor/http/grammar/http.terms.d.ts.map +1 -0
  9. package/dist/src/components/code-editor/http/grammar/http.terms.js +4 -0
  10. package/dist/src/components/code-editor/http/grammar/http.terms.js.map +1 -0
  11. package/dist/src/components/code-editor/http/grammar/http.test.d.ts +2 -0
  12. package/dist/src/components/code-editor/http/grammar/http.test.d.ts.map +1 -0
  13. package/dist/src/components/code-editor/http/grammar/http.test.js +80 -0
  14. package/dist/src/components/code-editor/http/grammar/http.test.js.map +1 -0
  15. package/dist/src/components/code-editor/http/index.d.ts +4 -0
  16. package/dist/src/components/code-editor/http/index.d.ts.map +1 -0
  17. package/dist/src/components/code-editor/http/index.js +66 -0
  18. package/dist/src/components/code-editor/http/index.js.map +1 -0
  19. package/dist/src/components/code-editor/index.d.ts +14 -2
  20. package/dist/src/components/code-editor/index.d.ts.map +1 -1
  21. package/dist/src/components/code-editor/index.js +309 -20
  22. package/dist/src/components/code-editor/index.js.map +1 -1
  23. package/dist/src/components/code-editor.stories.d.ts +1 -0
  24. package/dist/src/components/code-editor.stories.d.ts.map +1 -1
  25. package/dist/src/components/code-editor.stories.js +255 -2
  26. package/dist/src/components/code-editor.stories.js.map +1 -1
  27. package/dist/src/components/copy-icon.d.ts +5 -1
  28. package/dist/src/components/copy-icon.d.ts.map +1 -1
  29. package/dist/src/components/copy-icon.js +41 -3
  30. package/dist/src/components/copy-icon.js.map +1 -1
  31. package/dist/src/components/data-table.d.ts +8 -0
  32. package/dist/src/components/data-table.d.ts.map +1 -0
  33. package/dist/src/components/data-table.js +65 -0
  34. package/dist/src/components/data-table.js.map +1 -0
  35. package/dist/src/components/data-table.stories.d.ts +7 -0
  36. package/dist/src/components/data-table.stories.d.ts.map +1 -0
  37. package/dist/src/components/data-table.stories.js +44 -0
  38. package/dist/src/components/data-table.stories.js.map +1 -0
  39. package/dist/src/components/fhir-structure-view.d.ts +34 -0
  40. package/dist/src/components/fhir-structure-view.d.ts.map +1 -0
  41. package/dist/src/components/fhir-structure-view.js +229 -0
  42. package/dist/src/components/fhir-structure-view.js.map +1 -0
  43. package/dist/src/components/fhir-structure-view.stories.d.ts +7 -0
  44. package/dist/src/components/fhir-structure-view.stories.d.ts.map +1 -0
  45. package/dist/src/components/fhir-structure-view.stories.js +447 -0
  46. package/dist/src/components/fhir-structure-view.stories.js.map +1 -0
  47. package/dist/src/components/patient-table.d.ts +73 -0
  48. package/dist/src/components/patient-table.d.ts.map +1 -0
  49. package/dist/src/components/patient-table.js +921 -0
  50. package/dist/src/components/patient-table.js.map +1 -0
  51. package/dist/src/components/patient-table.stories.d.ts +111 -0
  52. package/dist/src/components/patient-table.stories.d.ts.map +1 -0
  53. package/dist/src/components/patient-table.stories.js +172 -0
  54. package/dist/src/components/patient-table.stories.js.map +1 -0
  55. package/dist/src/components/request-line-editor.d.ts +13 -35
  56. package/dist/src/components/request-line-editor.d.ts.map +1 -1
  57. package/dist/src/components/request-line-editor.js +72 -49
  58. package/dist/src/components/request-line-editor.js.map +1 -1
  59. package/dist/src/components/request-line-editor.stories.d.ts.map +1 -1
  60. package/dist/src/components/request-line-editor.stories.js +17 -53
  61. package/dist/src/components/request-line-editor.stories.js.map +1 -1
  62. package/dist/src/components/segment-control.d.ts +16 -0
  63. package/dist/src/components/segment-control.d.ts.map +1 -0
  64. package/dist/src/components/segment-control.js +48 -0
  65. package/dist/src/components/segment-control.js.map +1 -0
  66. package/dist/src/components/segment-control.stories.d.ts +15 -0
  67. package/dist/src/components/segment-control.stories.d.ts.map +1 -0
  68. package/dist/src/components/segment-control.stories.js +33 -0
  69. package/dist/src/components/segment-control.stories.js.map +1 -0
  70. package/dist/src/components/split-button.d.ts +5 -0
  71. package/dist/src/components/split-button.d.ts.map +1 -0
  72. package/dist/src/components/split-button.js +12 -0
  73. package/dist/src/components/split-button.js.map +1 -0
  74. package/dist/src/components/split-button.stories.d.ts +7 -0
  75. package/dist/src/components/split-button.stories.d.ts.map +1 -0
  76. package/dist/src/components/split-button.stories.js +57 -0
  77. package/dist/src/components/split-button.stories.js.map +1 -0
  78. package/dist/src/components/tree-view.d.ts +22 -0
  79. package/dist/src/components/tree-view.d.ts.map +1 -0
  80. package/dist/src/components/tree-view.js +101 -0
  81. package/dist/src/components/tree-view.js.map +1 -0
  82. package/dist/src/components/tree-view.stories.d.ts +13 -0
  83. package/dist/src/components/tree-view.stories.d.ts.map +1 -0
  84. package/dist/src/components/tree-view.stories.js +274 -0
  85. package/dist/src/components/tree-view.stories.js.map +1 -0
  86. package/dist/src/icons.d.ts +9 -0
  87. package/dist/src/icons.d.ts.map +1 -0
  88. package/dist/src/icons.js +279 -0
  89. package/dist/src/icons.js.map +1 -0
  90. package/dist/src/index.css +42 -3
  91. package/dist/src/index.d.ts +9 -1
  92. package/dist/src/index.d.ts.map +1 -1
  93. package/dist/src/index.js +9 -1
  94. package/dist/src/index.js.map +1 -1
  95. package/dist/src/shadcn/components/ui/accordion.d.ts.map +1 -1
  96. package/dist/src/shadcn/components/ui/accordion.js +23 -5
  97. package/dist/src/shadcn/components/ui/accordion.js.map +1 -1
  98. package/dist/src/shadcn/components/ui/alert-dialog.d.ts +3 -1
  99. package/dist/src/shadcn/components/ui/alert-dialog.d.ts.map +1 -1
  100. package/dist/src/shadcn/components/ui/alert-dialog.js +5 -2
  101. package/dist/src/shadcn/components/ui/alert-dialog.js.map +1 -1
  102. package/dist/src/shadcn/components/ui/alert.d.ts.map +1 -1
  103. package/dist/src/shadcn/components/ui/alert.js +12 -5
  104. package/dist/src/shadcn/components/ui/alert.js.map +1 -1
  105. package/dist/src/shadcn/components/ui/avatar.d.ts.map +1 -1
  106. package/dist/src/shadcn/components/ui/avatar.js +4 -3
  107. package/dist/src/shadcn/components/ui/avatar.js.map +1 -1
  108. package/dist/src/shadcn/components/ui/badge.d.ts.map +1 -1
  109. package/dist/src/shadcn/components/ui/badge.js +16 -5
  110. package/dist/src/shadcn/components/ui/badge.js.map +1 -1
  111. package/dist/src/shadcn/components/ui/breadcrumb.d.ts.map +1 -1
  112. package/dist/src/shadcn/components/ui/breadcrumb.js +6 -6
  113. package/dist/src/shadcn/components/ui/breadcrumb.js.map +1 -1
  114. package/dist/src/shadcn/components/ui/button.d.ts.map +1 -1
  115. package/dist/src/shadcn/components/ui/button.js +19 -11
  116. package/dist/src/shadcn/components/ui/button.js.map +1 -1
  117. package/dist/src/shadcn/components/ui/card.d.ts.map +1 -1
  118. package/dist/src/shadcn/components/ui/card.js +14 -6
  119. package/dist/src/shadcn/components/ui/card.js.map +1 -1
  120. package/dist/src/shadcn/components/ui/checkbox.d.ts.map +1 -1
  121. package/dist/src/shadcn/components/ui/checkbox.js +20 -5
  122. package/dist/src/shadcn/components/ui/checkbox.js.map +1 -1
  123. package/dist/src/shadcn/components/ui/checkbox.stories.d.ts.map +1 -1
  124. package/dist/src/shadcn/components/ui/checkbox.stories.js +44 -35
  125. package/dist/src/shadcn/components/ui/checkbox.stories.js.map +1 -1
  126. package/dist/src/shadcn/components/ui/combobox.d.ts +18 -0
  127. package/dist/src/shadcn/components/ui/combobox.d.ts.map +1 -0
  128. package/dist/src/shadcn/components/ui/combobox.js +121 -0
  129. package/dist/src/shadcn/components/ui/combobox.js.map +1 -0
  130. package/dist/src/shadcn/components/ui/combobox.stories.d.ts +11 -0
  131. package/dist/src/shadcn/components/ui/combobox.stories.d.ts.map +1 -0
  132. package/dist/src/shadcn/components/ui/combobox.stories.js +16 -0
  133. package/dist/src/shadcn/components/ui/combobox.stories.js.map +1 -0
  134. package/dist/src/shadcn/components/ui/command.d.ts.map +1 -1
  135. package/dist/src/shadcn/components/ui/command.js +73 -12
  136. package/dist/src/shadcn/components/ui/command.js.map +1 -1
  137. package/dist/src/shadcn/components/ui/command.stories.js +0 -1
  138. package/dist/src/shadcn/components/ui/command.stories.js.map +1 -1
  139. package/dist/src/shadcn/components/ui/dialog.d.ts.map +1 -1
  140. package/dist/src/shadcn/components/ui/dialog.js +35 -7
  141. package/dist/src/shadcn/components/ui/dialog.js.map +1 -1
  142. package/dist/src/shadcn/components/ui/drawer.d.ts.map +1 -1
  143. package/dist/src/shadcn/components/ui/drawer.js +26 -5
  144. package/dist/src/shadcn/components/ui/drawer.js.map +1 -1
  145. package/dist/src/shadcn/components/ui/dropdown-menu.d.ts.map +1 -1
  146. package/dist/src/shadcn/components/ui/dropdown-menu.js +12 -1
  147. package/dist/src/shadcn/components/ui/dropdown-menu.js.map +1 -1
  148. package/dist/src/shadcn/components/ui/form.d.ts.map +1 -1
  149. package/dist/src/shadcn/components/ui/form.js +12 -4
  150. package/dist/src/shadcn/components/ui/form.js.map +1 -1
  151. package/dist/src/shadcn/components/ui/input.d.ts +3 -1
  152. package/dist/src/shadcn/components/ui/input.d.ts.map +1 -1
  153. package/dist/src/shadcn/components/ui/input.js +126 -17
  154. package/dist/src/shadcn/components/ui/input.js.map +1 -1
  155. package/dist/src/shadcn/components/ui/label.d.ts.map +1 -1
  156. package/dist/src/shadcn/components/ui/label.js +8 -1
  157. package/dist/src/shadcn/components/ui/label.js.map +1 -1
  158. package/dist/src/shadcn/components/ui/menubar.d.ts.map +1 -1
  159. package/dist/src/shadcn/components/ui/menubar.js +35 -13
  160. package/dist/src/shadcn/components/ui/menubar.js.map +1 -1
  161. package/dist/src/shadcn/components/ui/pagination.d.ts.map +1 -1
  162. package/dist/src/shadcn/components/ui/pagination.js +6 -6
  163. package/dist/src/shadcn/components/ui/pagination.js.map +1 -1
  164. package/dist/src/shadcn/components/ui/popover.d.ts.map +1 -1
  165. package/dist/src/shadcn/components/ui/popover.js +12 -1
  166. package/dist/src/shadcn/components/ui/popover.js.map +1 -1
  167. package/dist/src/shadcn/components/ui/progress.d.ts.map +1 -1
  168. package/dist/src/shadcn/components/ui/progress.js +6 -2
  169. package/dist/src/shadcn/components/ui/progress.js.map +1 -1
  170. package/dist/src/shadcn/components/ui/radio-group.d.ts.map +1 -1
  171. package/dist/src/shadcn/components/ui/radio-group.js +11 -6
  172. package/dist/src/shadcn/components/ui/radio-group.js.map +1 -1
  173. package/dist/src/shadcn/components/ui/radio-group.stories.d.ts.map +1 -1
  174. package/dist/src/shadcn/components/ui/radio-group.stories.js +57 -34
  175. package/dist/src/shadcn/components/ui/radio-group.stories.js.map +1 -1
  176. package/dist/src/shadcn/components/ui/scroll-area.d.ts.map +1 -1
  177. package/dist/src/shadcn/components/ui/scroll-area.js +9 -3
  178. package/dist/src/shadcn/components/ui/scroll-area.js.map +1 -1
  179. package/dist/src/shadcn/components/ui/select.d.ts.map +1 -1
  180. package/dist/src/shadcn/components/ui/select.js +49 -14
  181. package/dist/src/shadcn/components/ui/select.js.map +1 -1
  182. package/dist/src/shadcn/components/ui/select.stories.d.ts.map +1 -1
  183. package/dist/src/shadcn/components/ui/select.stories.js +1 -4
  184. package/dist/src/shadcn/components/ui/select.stories.js.map +1 -1
  185. package/dist/src/shadcn/components/ui/separator.d.ts.map +1 -1
  186. package/dist/src/shadcn/components/ui/separator.js +7 -1
  187. package/dist/src/shadcn/components/ui/separator.js.map +1 -1
  188. package/dist/src/shadcn/components/ui/sidebar.d.ts.map +1 -1
  189. package/dist/src/shadcn/components/ui/sidebar.js +20 -6
  190. package/dist/src/shadcn/components/ui/sidebar.js.map +1 -1
  191. package/dist/src/shadcn/components/ui/skeleton.d.ts.map +1 -1
  192. package/dist/src/shadcn/components/ui/skeleton.js +3 -1
  193. package/dist/src/shadcn/components/ui/skeleton.js.map +1 -1
  194. package/dist/src/shadcn/components/ui/slider.d.ts.map +1 -1
  195. package/dist/src/shadcn/components/ui/slider.js +34 -4
  196. package/dist/src/shadcn/components/ui/slider.js.map +1 -1
  197. package/dist/src/shadcn/components/ui/sonner.d.ts +16 -1
  198. package/dist/src/shadcn/components/ui/sonner.d.ts.map +1 -1
  199. package/dist/src/shadcn/components/ui/sonner.js +23 -3
  200. package/dist/src/shadcn/components/ui/sonner.js.map +1 -1
  201. package/dist/src/shadcn/components/ui/sonner.stories.d.ts.map +1 -1
  202. package/dist/src/shadcn/components/ui/sonner.stories.js +19 -11
  203. package/dist/src/shadcn/components/ui/sonner.stories.js.map +1 -1
  204. package/dist/src/shadcn/components/ui/switch.d.ts.map +1 -1
  205. package/dist/src/shadcn/components/ui/switch.js +18 -2
  206. package/dist/src/shadcn/components/ui/switch.js.map +1 -1
  207. package/dist/src/shadcn/components/ui/table.d.ts.map +1 -1
  208. package/dist/src/shadcn/components/ui/table.js +12 -8
  209. package/dist/src/shadcn/components/ui/table.js.map +1 -1
  210. package/dist/src/shadcn/components/ui/tabs.d.ts +21 -3
  211. package/dist/src/shadcn/components/ui/tabs.d.ts.map +1 -1
  212. package/dist/src/shadcn/components/ui/tabs.js +315 -9
  213. package/dist/src/shadcn/components/ui/tabs.js.map +1 -1
  214. package/dist/src/shadcn/components/ui/tabs.stories.d.ts.map +1 -1
  215. package/dist/src/shadcn/components/ui/tabs.stories.js +50 -1
  216. package/dist/src/shadcn/components/ui/tabs.stories.js.map +1 -1
  217. package/dist/src/shadcn/components/ui/textarea.d.ts.map +1 -1
  218. package/dist/src/shadcn/components/ui/textarea.js +15 -1
  219. package/dist/src/shadcn/components/ui/textarea.js.map +1 -1
  220. package/dist/src/shadcn/components/ui/toggle-group.d.ts.map +1 -1
  221. package/dist/src/shadcn/components/ui/toggle-group.js +6 -2
  222. package/dist/src/shadcn/components/ui/toggle-group.js.map +1 -1
  223. package/dist/src/shadcn/components/ui/toggle.d.ts.map +1 -1
  224. package/dist/src/shadcn/components/ui/toggle.js +18 -6
  225. package/dist/src/shadcn/components/ui/toggle.js.map +1 -1
  226. package/dist/src/shadcn/components/ui/tooltip.d.ts.map +1 -1
  227. package/dist/src/shadcn/components/ui/tooltip.js +11 -1
  228. package/dist/src/shadcn/components/ui/tooltip.js.map +1 -1
  229. package/dist/src/shadcn/components/ui/tree.d.ts +28 -0
  230. package/dist/src/shadcn/components/ui/tree.d.ts.map +1 -0
  231. package/dist/src/shadcn/components/ui/tree.js +122 -0
  232. package/dist/src/shadcn/components/ui/tree.js.map +1 -0
  233. package/dist/src/typography.css +12 -0
  234. package/package.json +13 -2
  235. package/src/components/code-editor/http/grammar/http.grammar +74 -0
  236. package/src/components/code-editor/http/grammar/http.terms.ts +9 -0
  237. package/src/components/code-editor/http/grammar/http.test.ts +110 -0
  238. package/src/components/code-editor/http/grammar/http.ts +21 -0
  239. package/src/components/code-editor/http/index.ts +87 -0
  240. package/src/components/code-editor/index.tsx +307 -21
  241. package/src/components/code-editor.stories.tsx +295 -1
  242. package/src/components/copy-icon.tsx +57 -3
  243. package/src/components/data-table.stories.tsx +38 -0
  244. package/src/components/data-table.tsx +117 -0
  245. package/src/components/fhir-structure-view.stories.tsx +439 -0
  246. package/src/components/fhir-structure-view.tsx +231 -0
  247. package/src/components/patient-table.stories.tsx +111 -0
  248. package/src/components/patient-table.tsx +1301 -0
  249. package/src/components/request-line-editor.stories.tsx +17 -27
  250. package/src/components/request-line-editor.tsx +98 -61
  251. package/src/components/segment-control.stories.tsx +29 -0
  252. package/src/components/segment-control.tsx +80 -0
  253. package/src/components/split-button.stories.tsx +49 -0
  254. package/src/components/split-button.tsx +17 -0
  255. package/src/components/tree-view.stories.tsx +259 -0
  256. package/src/components/tree-view.tsx +172 -0
  257. package/src/icons.tsx +287 -0
  258. package/src/index.css +42 -3
  259. package/src/index.tsx +9 -2
  260. package/src/shadcn/components/ui/accordion.tsx +66 -8
  261. package/src/shadcn/components/ui/alert-dialog.tsx +6 -2
  262. package/src/shadcn/components/ui/alert.tsx +53 -15
  263. package/src/shadcn/components/ui/avatar.tsx +17 -6
  264. package/src/shadcn/components/ui/badge.tsx +67 -18
  265. package/src/shadcn/components/ui/breadcrumb.tsx +35 -7
  266. package/src/shadcn/components/ui/button.tsx +118 -57
  267. package/src/shadcn/components/ui/card.tsx +44 -13
  268. package/src/shadcn/components/ui/checkbox.stories.tsx +20 -24
  269. package/src/shadcn/components/ui/checkbox.tsx +45 -4
  270. package/src/shadcn/components/ui/combobox.stories.tsx +19 -0
  271. package/src/shadcn/components/ui/combobox.tsx +142 -0
  272. package/src/shadcn/components/ui/command.stories.tsx +1 -1
  273. package/src/shadcn/components/ui/command.tsx +192 -36
  274. package/src/shadcn/components/ui/dialog.tsx +101 -13
  275. package/src/shadcn/components/ui/drawer.tsx +93 -18
  276. package/src/shadcn/components/ui/dropdown-menu.tsx +37 -9
  277. package/src/shadcn/components/ui/form.tsx +16 -4
  278. package/src/shadcn/components/ui/input.tsx +400 -29
  279. package/src/shadcn/components/ui/label.tsx +21 -4
  280. package/src/shadcn/components/ui/menubar.tsx +188 -43
  281. package/src/shadcn/components/ui/pagination.tsx +12 -6
  282. package/src/shadcn/components/ui/popover.tsx +35 -4
  283. package/src/shadcn/components/ui/progress.tsx +21 -5
  284. package/src/shadcn/components/ui/radio-group.stories.tsx +22 -14
  285. package/src/shadcn/components/ui/radio-group.tsx +42 -5
  286. package/src/shadcn/components/ui/scroll-area.tsx +33 -5
  287. package/src/shadcn/components/ui/select.stories.tsx +0 -2
  288. package/src/shadcn/components/ui/select.tsx +184 -33
  289. package/src/shadcn/components/ui/separator.tsx +15 -5
  290. package/src/shadcn/components/ui/sidebar.tsx +68 -26
  291. package/src/shadcn/components/ui/skeleton.tsx +4 -1
  292. package/src/shadcn/components/ui/slider.tsx +82 -11
  293. package/src/shadcn/components/ui/sonner.stories.tsx +19 -15
  294. package/src/shadcn/components/ui/sonner.tsx +53 -3
  295. package/src/shadcn/components/ui/switch.tsx +53 -7
  296. package/src/shadcn/components/ui/table.tsx +38 -11
  297. package/src/shadcn/components/ui/tabs.stories.tsx +32 -0
  298. package/src/shadcn/components/ui/tabs.tsx +456 -17
  299. package/src/shadcn/components/ui/textarea.tsx +42 -4
  300. package/src/shadcn/components/ui/toggle-group.tsx +27 -5
  301. package/src/shadcn/components/ui/toggle.tsx +59 -18
  302. package/src/shadcn/components/ui/tooltip.tsx +33 -8
  303. package/src/shadcn/components/ui/tree.tsx +233 -0
  304. package/src/typography.css +12 -0
@@ -0,0 +1,1301 @@
1
+ import type {
2
+ Column,
3
+ ColumnDef,
4
+ ColumnPinningState,
5
+ Header,
6
+ } from "@tanstack/react-table";
7
+ import {
8
+ createColumnHelper,
9
+ flexRender,
10
+ getCoreRowModel,
11
+ useReactTable,
12
+ } from "@tanstack/react-table";
13
+ import {
14
+ ArrowLeftToLine,
15
+ ArrowRightToLine,
16
+ ChevronDown,
17
+ ChevronUp,
18
+ GripVertical,
19
+ MoreHorizontal,
20
+ PinOff,
21
+ Search,
22
+ } from "lucide-react";
23
+ import type { CSSProperties } from "react";
24
+ import { useEffect, useMemo, useState } from "react";
25
+ import { Button } from "#shadcn/components/ui/button";
26
+ import {
27
+ DropdownMenu,
28
+ DropdownMenuContent,
29
+ DropdownMenuItem,
30
+ DropdownMenuTrigger,
31
+ } from "#shadcn/components/ui/dropdown-menu";
32
+ import { Input } from "#shadcn/components/ui/input";
33
+ import { cn } from "#shadcn/lib/utils";
34
+
35
+ // Unstyled table components to preserve custom styling
36
+ const Table = ({ className, ...props }: React.ComponentProps<"table">) => (
37
+ <table className={className} {...props} />
38
+ );
39
+ const TableHeader = ({
40
+ className,
41
+ ...props
42
+ }: React.ComponentProps<"thead">) => <thead className={className} {...props} />;
43
+ const TableBody = ({ className, ...props }: React.ComponentProps<"tbody">) => (
44
+ <tbody className={className} {...props} />
45
+ );
46
+ const TableRow = ({ className, ...props }: React.ComponentProps<"tr">) => (
47
+ <tr className={className} {...props} />
48
+ );
49
+ const TableHead = ({ className, ...props }: React.ComponentProps<"th">) => (
50
+ <th className={className} {...props} />
51
+ );
52
+ const TableCell = ({ className, ...props }: React.ComponentProps<"td">) => (
53
+ <td className={className} {...props} />
54
+ );
55
+
56
+ const getPinningStyles = <T,>(column: Column<T>): CSSProperties => {
57
+ const isPinned = column.getIsPinned();
58
+ return {
59
+ left: isPinned === "left" ? `${column.getStart("left")}px` : undefined,
60
+ right: isPinned === "right" ? `${column.getAfter("right")}px` : undefined,
61
+ position: isPinned ? "sticky" : "relative",
62
+ width: column.getSize(),
63
+ zIndex: isPinned ? 1 : 0,
64
+ };
65
+ };
66
+
67
+ // Column configuration types
68
+ export type ColumnType = "text" | "code" | "link" | "button";
69
+
70
+ export interface ActionConfig {
71
+ label: string;
72
+ onClick: (rowId: string) => void;
73
+ variant?: "primary" | "secondary" | "link" | "ghost";
74
+ danger?: boolean;
75
+ }
76
+
77
+ export interface ColumnConfig {
78
+ key: string;
79
+ label: string;
80
+ width?: string;
81
+ fixed?: boolean;
82
+ rightAlign?: boolean;
83
+ type?: ColumnType;
84
+ sortable?: boolean;
85
+ filterable?: boolean;
86
+ // For action columns
87
+ actions?: ActionConfig[];
88
+ }
89
+
90
+ // Styles
91
+ const styles = {
92
+ // Table container
93
+ tableContainer: cn(
94
+ "w-full",
95
+ "overflow-x-auto",
96
+ "border",
97
+ "border-border-secondary",
98
+ "rounded-md",
99
+ ),
100
+ table: cn(
101
+ "w-full",
102
+ "table-fixed",
103
+ "border-collapse",
104
+ "text-sm",
105
+ "[&_td]:border-border",
106
+ "[&_th]:border-border",
107
+ "border-separate",
108
+ "border-spacing-0",
109
+ "[&_tfoot_td]:border-t",
110
+ "[&_th]:border-b",
111
+ "[&_tr]:border-none",
112
+ ),
113
+
114
+ // Global cell padding
115
+ cellPadding: cn("px-4"),
116
+
117
+ pinnedColumn: cn(
118
+ "data-pinned:bg-background/90",
119
+ "data-pinned:backdrop-blur-xs",
120
+ "[&[data-pinned=left][data-last-col=left]]:border-r",
121
+ "[&[data-pinned=left][data-last-col=left]]:border-r-border-primary",
122
+ "[&[data-pinned=right][data-last-col=right]]:border-l",
123
+ "[&[data-pinned=right][data-last-col=right]]:border-l-border-primary",
124
+ ),
125
+ pinnedHeader: cn(
126
+ "data-pinned:bg-muted/90",
127
+ "data-pinned:backdrop-blur-xs",
128
+ "[&[data-pinned=left][data-last-col=left]]:border-r",
129
+ "[&[data-pinned=left][data-last-col=left]]:border-r-border-primary",
130
+ "[&[data-pinned=right][data-last-col=right]]:border-l",
131
+ "[&[data-pinned=right][data-last-col=right]]:border-l-border-primary",
132
+ ),
133
+
134
+ // Header styles
135
+ thead: cn("bg-bg-secondary"),
136
+ th: cn("h-8", "text-left", "font-medium", "text-text-primary", "select-none"),
137
+ thSortable: cn("cursor-pointer", "hover:bg-bg-tertiary"),
138
+ thSorted: cn("bg-bg-link/3", "border-b", "border-border-link"),
139
+ thRightAlign: cn("text-right"),
140
+
141
+ // Cell styles for sorted columns
142
+ cellSorted: cn("bg-bg-link/3"),
143
+
144
+ // Header content
145
+ headerContent: cn(
146
+ "h-8",
147
+ "flex",
148
+ "items-center",
149
+ "justify-between",
150
+ "typo-body",
151
+ "text-text-secondary",
152
+ "relative",
153
+ "w-full",
154
+ ),
155
+ headerText: cn("truncate", "pr-3"),
156
+ headerIcons: cn("flex", "flex-col", "items-center", "justify-center"),
157
+ headerIcon: cn("w-3", "h-3"),
158
+ headerIconInactive: cn("w-3", "h-3", "opacity-30"),
159
+
160
+ // Cell content
161
+ cellContent: cn("h-8", "flex", "items-center", "text-text-primary"),
162
+ cellText: cn("truncate"),
163
+ cellCode: cn("h-8", "flex", "items-center", "text-text-primary", "typo-code"),
164
+ cellRightAlign: cn("justify-end"),
165
+ cellTextRightAlign: cn("text-right"),
166
+
167
+ // Filter row
168
+ filterRow: cn("bg-white", "border-b", "border-border-secondary"),
169
+ filterCell: cn("px-1", "h-8"),
170
+ filterCellPinned: cn("px-1", "h-8", "bg-white/90", "backdrop-blur-xs"),
171
+ filterActions: cn("text-text-tertiary", "text-sm"),
172
+ filterInput: cn("border-0", "h-8"),
173
+ filterIcon: cn("w-4", "h-4", "text-text-tertiary"),
174
+
175
+ dataRow: cn("hover:bg-bg-link/10"),
176
+ dataRowZebra: cn("bg-bg-secondary", "hover:bg-bg-link/10"),
177
+ dataCell: cn("h-8"),
178
+
179
+ // Action button
180
+ actionButton: cn(
181
+ "text-text-link",
182
+ "hover:text-text-link_hover",
183
+ "transition-colors",
184
+ "cursor-pointer",
185
+ ),
186
+
187
+ // Action link
188
+ actionLink: cn(
189
+ "text-text-link",
190
+ "hover:text-text-link_hover",
191
+ "transition-colors",
192
+ "cursor-pointer",
193
+ ),
194
+
195
+ // Column resize handle
196
+ resizeHandle: cn(
197
+ "absolute",
198
+ "right-0",
199
+ "top-0",
200
+ "h-full",
201
+ "w-1",
202
+ "bg-border-primary",
203
+ "cursor-col-resize",
204
+ "user-select-none",
205
+ "touch-action-none",
206
+ "opacity-0",
207
+ "hover:opacity-100",
208
+ "active:opacity-100",
209
+ "transition-opacity",
210
+ "duration-150",
211
+ "before:absolute",
212
+ "before:right-[-4px]",
213
+ "before:top-0",
214
+ "before:w-2",
215
+ "before:h-full",
216
+ "before:content-['']",
217
+ ),
218
+ resizeHandleActive: cn("opacity-100", "bg-border-link"),
219
+
220
+ // Resizable header indicator
221
+ resizableHeader: cn(
222
+ "group-hover:[&:not(:last-child)]:after:opacity-30",
223
+ "after:absolute",
224
+ "after:right-0",
225
+ "after:top-1/2",
226
+ "after:-translate-y-1/2",
227
+ "after:h-4",
228
+ "after:w-px",
229
+ "after:bg-border-primary",
230
+ "after:opacity-0",
231
+ "after:transition-opacity",
232
+ "after:duration-150",
233
+ "hover:after:opacity-50",
234
+ "last:after:w-0",
235
+ ),
236
+
237
+ // Draggable header styles
238
+ draggableHeader: cn("transition-all", "duration-150", "group/header"),
239
+ dragZone: cn(
240
+ "absolute",
241
+ "left-0",
242
+ "top-0",
243
+ "h-full",
244
+ "cursor-grab",
245
+ "active:cursor-grabbing",
246
+ "flex",
247
+ "items-center",
248
+ "justify-start",
249
+ "pl-0",
250
+ "opacity-0",
251
+ "hover:opacity-100",
252
+ "group-hover/header:opacity-60",
253
+ "hover:!opacity-100",
254
+ "transition-opacity",
255
+ "duration-150",
256
+ "bg-transparent",
257
+ "border-none",
258
+ "text-text-tertiary",
259
+ "hover:text-text-secondary",
260
+ "right-5",
261
+ ),
262
+ draggingHeader: cn(
263
+ "bg-bg-primary_inverse/10",
264
+ "scale-105",
265
+ "shadow-lg",
266
+ "z-50",
267
+ ),
268
+ draggingColumn: cn("bg-bg-primary_inverse/10", "shadow-inner"),
269
+ dropZone: cn(
270
+ "relative",
271
+ "before:absolute",
272
+ "before:left-0",
273
+ "before:top-0",
274
+ "before:w-1",
275
+ "before:h-full",
276
+ "before:bg-border-link",
277
+ "before:opacity-0",
278
+ "before:transition-opacity",
279
+ "before:duration-150",
280
+ ),
281
+ dropZoneActive: cn("before:opacity-100"),
282
+ } as const;
283
+
284
+ // Types
285
+ export interface PatientRow {
286
+ id: string;
287
+ firstName: string;
288
+ lastName: string;
289
+ phoneNumber: string;
290
+ email: string;
291
+ birthDate: string;
292
+ gender: "MALE" | "FEMALE" | "OTHER";
293
+ street: string;
294
+ city: string;
295
+ state: string;
296
+ zip: string;
297
+ country?: string;
298
+ encounters: number;
299
+ }
300
+
301
+ export interface ColumnSearchConfig {
302
+ [key: string]: {
303
+ enabled: boolean;
304
+ type: "text" | "date";
305
+ placeholder: string;
306
+ };
307
+ }
308
+
309
+ export interface PaginationValues {
310
+ pageIndex: number;
311
+ pageSize: number;
312
+ }
313
+
314
+ export interface TableHeaderContentProps {
315
+ content: React.ReactNode;
316
+ isSortable?: boolean;
317
+ sortDirection?: "asc" | "desc" | null;
318
+ isPinnable?: boolean;
319
+ pinnedDirection?: "left" | "right" | false;
320
+ onPin?: (direction: "left" | "right" | false) => void;
321
+ }
322
+
323
+ export interface TableCellContentProps {
324
+ content: React.ReactNode;
325
+ type?: ColumnType;
326
+ rightAlign?: boolean;
327
+ actions?: ActionConfig[];
328
+ rowId?: string;
329
+ }
330
+
331
+ // Mock components
332
+ function TableHeaderContent({
333
+ content,
334
+ isSortable = false,
335
+ sortDirection = null,
336
+ isPinnable = false,
337
+ pinnedDirection = false,
338
+ onPin,
339
+ }: TableHeaderContentProps) {
340
+ return (
341
+ <div className={styles.headerContent}>
342
+ <span className={styles.headerText}>{content}</span>
343
+ <div className="flex items-center">
344
+ {isSortable && (
345
+ <div className={styles.headerIcons}>
346
+ {sortDirection === "asc" ? (
347
+ <ChevronUp className={styles.headerIcon} />
348
+ ) : sortDirection === "desc" ? (
349
+ <ChevronDown className={styles.headerIcon} />
350
+ ) : (
351
+ <div className="flex flex-col">
352
+ <ChevronUp className={styles.headerIconInactive} />
353
+ <ChevronDown className={styles.headerIconInactive} />
354
+ </div>
355
+ )}
356
+ </div>
357
+ )}
358
+ {isPinnable &&
359
+ onPin &&
360
+ (pinnedDirection ? (
361
+ <Button
362
+ size="small"
363
+ variant="ghost"
364
+ className="h-6 w-6 p-0 ml-1 opacity-60 hover:opacity-100"
365
+ onClick={() => onPin(false)}
366
+ title="Unpin column"
367
+ >
368
+ <PinOff className="h-3 w-3" />
369
+ </Button>
370
+ ) : (
371
+ <DropdownMenu>
372
+ <DropdownMenuTrigger asChild>
373
+ <Button
374
+ size="small"
375
+ variant="ghost"
376
+ className="h-6 w-6 p-0 ml-1 opacity-60 hover:opacity-100"
377
+ title="Pin column"
378
+ >
379
+ <MoreHorizontal className="h-3 w-3" />
380
+ </Button>
381
+ </DropdownMenuTrigger>
382
+ <DropdownMenuContent align="end">
383
+ <DropdownMenuItem onClick={() => onPin("left")}>
384
+ <ArrowLeftToLine className="mr-2 h-4 w-4" />
385
+ Stick to left
386
+ </DropdownMenuItem>
387
+ <DropdownMenuItem onClick={() => onPin("right")}>
388
+ <ArrowRightToLine className="mr-2 h-4 w-4" />
389
+ Stick to right
390
+ </DropdownMenuItem>
391
+ </DropdownMenuContent>
392
+ </DropdownMenu>
393
+ ))}
394
+ </div>
395
+ </div>
396
+ );
397
+ }
398
+
399
+ function TableCellContent({
400
+ content,
401
+ type = "text",
402
+ rightAlign = false,
403
+ actions = [],
404
+ rowId,
405
+ }: TableCellContentProps) {
406
+ // For action types, render actions instead of content
407
+ if (type === "link" || type === "button") {
408
+ return (
409
+ <div
410
+ className={cn(styles.cellContent, rightAlign && styles.cellRightAlign)}
411
+ >
412
+ <div className="flex gap-2">
413
+ {actions.map((action, index) => {
414
+ const handleClick = (e: React.MouseEvent) => {
415
+ e.stopPropagation();
416
+ action.onClick(rowId || "");
417
+ };
418
+
419
+ const actionKey = `${rowId}-${action.label}-${index}`;
420
+
421
+ if (type === "button") {
422
+ return (
423
+ <Button
424
+ key={actionKey}
425
+ variant={action.variant || "primary"}
426
+ size="small"
427
+ {...(action.danger !== undefined && {
428
+ danger: action.danger,
429
+ })}
430
+ onClick={handleClick}
431
+ >
432
+ {action.label}
433
+ </Button>
434
+ );
435
+ }
436
+
437
+ // Default to link type
438
+ return (
439
+ <button
440
+ key={actionKey}
441
+ type="button"
442
+ className={styles.actionLink}
443
+ onClick={handleClick}
444
+ >
445
+ {action.label}
446
+ </button>
447
+ );
448
+ })}
449
+ </div>
450
+ </div>
451
+ );
452
+ }
453
+
454
+ // For regular content
455
+ const cellClass = cn(
456
+ type === "code" ? styles.cellCode : styles.cellContent,
457
+ rightAlign && styles.cellRightAlign,
458
+ );
459
+
460
+ const textClass = cn(
461
+ styles.cellText,
462
+ rightAlign && styles.cellTextRightAlign,
463
+ );
464
+
465
+ return (
466
+ <div className={cellClass}>
467
+ <span className={textClass}>{content}</span>
468
+ </div>
469
+ );
470
+ }
471
+
472
+ // Filter row component
473
+ function FilterRow<TData, TValue>({
474
+ headers,
475
+ draggedColumn,
476
+ dropTarget,
477
+ }: {
478
+ headers: Header<TData, TValue>[]; // TanStack Table headers
479
+ draggedColumn?: string | null;
480
+ dropTarget?: string | null;
481
+ }) {
482
+ return (
483
+ <TableRow className={styles.filterRow}>
484
+ {headers.map((header) => {
485
+ const tableColumn = header.column;
486
+ const columnKey = header.id;
487
+
488
+ const isPinned = tableColumn.getIsPinned();
489
+ const isBeingDragged = draggedColumn === columnKey;
490
+ const isDropTarget = dropTarget === columnKey;
491
+ const isLastLeftPinned =
492
+ isPinned === "left" && tableColumn.getIsLastColumn("left");
493
+ const isFirstRightPinned =
494
+ isPinned === "right" && tableColumn.getIsFirstColumn("right");
495
+
496
+ return (
497
+ <TableCell
498
+ key={`filter-${columnKey}`}
499
+ className={cn(
500
+ isPinned ? styles.filterCellPinned : styles.filterCell,
501
+ styles.pinnedColumn,
502
+ isBeingDragged && styles.draggingColumn,
503
+ isDropTarget && styles.dropZone,
504
+ isDropTarget && styles.dropZoneActive,
505
+ )}
506
+ style={{
507
+ ...getPinningStyles(tableColumn),
508
+ }}
509
+ data-pinned={isPinned || undefined}
510
+ data-last-col={
511
+ isLastLeftPinned
512
+ ? "left"
513
+ : isFirstRightPinned
514
+ ? "right"
515
+ : undefined
516
+ }
517
+ >
518
+ {columnKey === "actions" ? (
519
+ <div className={styles.filterActions}></div>
520
+ ) : (
521
+ <Input
522
+ placeholder="Search"
523
+ leftSlot={<Search className={styles.filterIcon} />}
524
+ onChange={(e) =>
525
+ console.log(`Filter ${columnKey}:`, e.target.value)
526
+ }
527
+ className={styles.filterInput}
528
+ />
529
+ )}
530
+ </TableCell>
531
+ );
532
+ })}
533
+ </TableRow>
534
+ );
535
+ }
536
+
537
+ // Mock DataTable component
538
+ interface DataTableProps<T> {
539
+ columns: ColumnDef<T>[];
540
+ data: T[];
541
+ showZebraStripes: boolean;
542
+ showFilters?: boolean;
543
+ showSorting?: boolean;
544
+ enableColumnResizing?: boolean;
545
+ enableColumnReordering?: boolean;
546
+ enableColumnPinning?: boolean;
547
+ onSort?: (columnKey: string) => void;
548
+ columnWidths?: Record<string, string>;
549
+ sortConfig?: { key: string; direction: "asc" | "desc" } | null;
550
+ columnOrder?: string[];
551
+ onColumnOrderChange?: (newOrder: string[]) => void;
552
+ columnPinning?: ColumnPinningState;
553
+ onColumnPinningChange?: (pinning: ColumnPinningState) => void;
554
+ }
555
+
556
+ function DataTable<T>({
557
+ columns,
558
+ data,
559
+ showZebraStripes,
560
+ showFilters = true,
561
+ showSorting = true,
562
+ enableColumnResizing = false,
563
+ enableColumnReordering = false,
564
+ enableColumnPinning = false,
565
+ onSort,
566
+ columnWidths = {},
567
+ sortConfig,
568
+ columnOrder = [],
569
+ onColumnOrderChange,
570
+ columnPinning = {},
571
+ onColumnPinningChange,
572
+ columnConfigs,
573
+ }: DataTableProps<T> & { columnConfigs?: ColumnConfig[] }) {
574
+ const [draggedColumn, setDraggedColumn] = useState<string | null>(null);
575
+ const [dropTarget, setDropTarget] = useState<string | null>(null);
576
+
577
+ const initialColumnSizing = useMemo(() => {
578
+ if (!enableColumnResizing || !columnConfigs) return {};
579
+
580
+ const sizing: Record<string, number> = {};
581
+ columnConfigs.forEach((config) => {
582
+ if (config.width) {
583
+ const width = parseInt(config.width.replace("px", ""), 10);
584
+ if (!Number.isNaN(width)) {
585
+ sizing[config.key] = width;
586
+ }
587
+ }
588
+ });
589
+ return sizing;
590
+ }, [enableColumnResizing, columnConfigs]);
591
+
592
+ const table = useReactTable({
593
+ data,
594
+ columns,
595
+ getCoreRowModel: getCoreRowModel(),
596
+ enableColumnResizing,
597
+ enableColumnPinning,
598
+ columnResizeMode: "onChange",
599
+ state: {
600
+ ...(columnOrder.length > 0 && { columnOrder }),
601
+ ...(enableColumnPinning && { columnPinning }),
602
+ },
603
+ ...(onColumnOrderChange && {
604
+ onColumnOrderChange: (updater) => {
605
+ const newOrder =
606
+ typeof updater === "function" ? updater(columnOrder) : updater;
607
+ onColumnOrderChange(newOrder);
608
+ },
609
+ }),
610
+ ...(onColumnPinningChange &&
611
+ enableColumnPinning && {
612
+ onColumnPinningChange: (updater) => {
613
+ const newPinning =
614
+ typeof updater === "function" ? updater(columnPinning) : updater;
615
+ onColumnPinningChange(newPinning);
616
+ },
617
+ }),
618
+ initialState: {
619
+ columnSizing: initialColumnSizing,
620
+ },
621
+ });
622
+
623
+ return (
624
+ <div className={styles.tableContainer}>
625
+ <Table className={styles.table}>
626
+ <TableHeader className={styles.thead}>
627
+ <TableRow className={enableColumnResizing ? "group" : ""}>
628
+ {table.getHeaderGroups()[0]?.headers.map((header) => {
629
+ const columnKey = header.id;
630
+ const isSortable = columnKey !== "actions";
631
+
632
+ const isPinned = header.column.getIsPinned();
633
+ const isSorted = sortConfig?.key === columnKey;
634
+ const isDraggable = enableColumnReordering && !isPinned;
635
+ const isDragging = draggedColumn === columnKey;
636
+ const isDropTarget = dropTarget === columnKey;
637
+ const isLastLeftPinned =
638
+ isPinned === "left" && header.column.getIsLastColumn("left");
639
+ const isFirstRightPinned =
640
+ isPinned === "right" && header.column.getIsFirstColumn("right");
641
+
642
+ const columnConfig = columnConfigs?.find(
643
+ (config) => config.key === columnKey,
644
+ );
645
+ const isRightAlign = columnConfig?.rightAlign || false;
646
+
647
+ return (
648
+ <TableHead
649
+ key={`header-${columnKey}`}
650
+ className={cn(
651
+ styles.cellPadding,
652
+ styles.th,
653
+ isSortable && showSorting && styles.thSortable,
654
+ styles.pinnedHeader,
655
+ isSorted && styles.thSorted,
656
+ isRightAlign && styles.thRightAlign,
657
+ "relative",
658
+ enableColumnResizing &&
659
+ header.column.getCanResize() &&
660
+ styles.resizableHeader,
661
+ isDraggable && styles.draggableHeader,
662
+ isDragging && styles.draggingHeader,
663
+ isDragging && styles.draggingColumn,
664
+ isDropTarget && styles.dropZone,
665
+ isDropTarget && styles.dropZoneActive,
666
+ )}
667
+ style={{
668
+ ...getPinningStyles(header.column),
669
+ ...(enableColumnResizing
670
+ ? {}
671
+ : { width: columnWidths[columnKey] || "200px" }),
672
+ }}
673
+ data-pinned={isPinned || undefined}
674
+ data-last-col={
675
+ isLastLeftPinned
676
+ ? "left"
677
+ : isFirstRightPinned
678
+ ? "right"
679
+ : undefined
680
+ }
681
+ onClick={(e) => {
682
+ const target = e.currentTarget;
683
+ if (
684
+ target.closest("[data-resize-handle]") ||
685
+ target.closest("[data-drag-zone]")
686
+ ) {
687
+ return;
688
+ }
689
+ if (isSortable && showSorting && onSort) {
690
+ onSort(String(columnKey));
691
+ }
692
+ }}
693
+ onDragOver={(e) => {
694
+ if (
695
+ !isDraggable ||
696
+ !draggedColumn ||
697
+ draggedColumn === columnKey
698
+ )
699
+ return;
700
+ e.preventDefault();
701
+ setDropTarget(columnKey);
702
+ }}
703
+ onDragLeave={(e) => {
704
+ const rect = e.currentTarget.getBoundingClientRect();
705
+ const x = e.clientX;
706
+ const y = e.clientY;
707
+ if (
708
+ x < rect.left ||
709
+ x > rect.right ||
710
+ y < rect.top ||
711
+ y > rect.bottom
712
+ ) {
713
+ setDropTarget(null);
714
+ }
715
+ }}
716
+ onDrop={(e) => {
717
+ e.preventDefault();
718
+ if (!isDraggable || !draggedColumn || !onColumnOrderChange)
719
+ return;
720
+
721
+ const currentOrder = table
722
+ .getAllLeafColumns()
723
+ .map((col) => col.id);
724
+ const draggedIndex = currentOrder.indexOf(draggedColumn);
725
+ const targetIndex = currentOrder.indexOf(columnKey);
726
+
727
+ if (draggedIndex !== -1 && targetIndex !== -1) {
728
+ const newOrder = [...currentOrder];
729
+ newOrder.splice(draggedIndex, 1);
730
+ newOrder.splice(targetIndex, 0, draggedColumn);
731
+ onColumnOrderChange(newOrder);
732
+ }
733
+
734
+ setDraggedColumn(null);
735
+ setDropTarget(null);
736
+ }}
737
+ >
738
+ {isDraggable && (
739
+ <button
740
+ type="button"
741
+ data-drag-zone
742
+ draggable
743
+ className={styles.dragZone}
744
+ onDragStart={(e) => {
745
+ setDraggedColumn(columnKey);
746
+ e.dataTransfer.effectAllowed = "move";
747
+ }}
748
+ onDragEnd={() => {
749
+ setDraggedColumn(null);
750
+ setDropTarget(null);
751
+ }}
752
+ >
753
+ <GripVertical className="w-4 h-4" />
754
+ </button>
755
+ )}
756
+
757
+ {flexRender(
758
+ header.column.columnDef.header,
759
+ header.getContext(),
760
+ )}
761
+ {enableColumnResizing && header.column.getCanResize() && (
762
+ <div
763
+ data-resize-handle
764
+ {...{
765
+ onMouseDown: header.getResizeHandler(),
766
+ onTouchStart: header.getResizeHandler(),
767
+ onClick: (e) => e.stopPropagation(),
768
+ className: cn(
769
+ styles.resizeHandle,
770
+ header.column.getIsResizing() &&
771
+ styles.resizeHandleActive,
772
+ ),
773
+ }}
774
+ />
775
+ )}
776
+ </TableHead>
777
+ );
778
+ })}
779
+ </TableRow>
780
+ </TableHeader>
781
+ <TableBody>
782
+ {/* Filter row */}
783
+ {showFilters && (
784
+ <FilterRow
785
+ headers={table.getHeaderGroups()[0]?.headers || []}
786
+ draggedColumn={draggedColumn}
787
+ dropTarget={dropTarget}
788
+ />
789
+ )}
790
+
791
+ {/* Data rows */}
792
+ {table.getRowModel().rows.map((row, rowIndex) => (
793
+ <TableRow
794
+ key={`row-${row.id}`}
795
+ className={cn(
796
+ styles.dataRow,
797
+ showZebraStripes &&
798
+ rowIndex % 2 === (showFilters ? 0 : 1) &&
799
+ styles.dataRowZebra,
800
+ )}
801
+ >
802
+ {row.getVisibleCells().map((cell) => {
803
+ const columnKey = cell.column.id;
804
+ const isPinned = cell.column.getIsPinned();
805
+ const isSorted = sortConfig?.key === columnKey;
806
+ const isBeingDragged = draggedColumn === columnKey;
807
+ const isDropTarget = dropTarget === columnKey;
808
+ const isLastLeftPinned =
809
+ isPinned === "left" && cell.column.getIsLastColumn("left");
810
+ const isFirstRightPinned =
811
+ isPinned === "right" && cell.column.getIsFirstColumn("right");
812
+
813
+ return (
814
+ <TableCell
815
+ key={cell.id}
816
+ className={cn(
817
+ styles.cellPadding,
818
+ styles.dataCell,
819
+ styles.pinnedColumn,
820
+ isSorted && styles.cellSorted,
821
+ isBeingDragged && styles.draggingColumn,
822
+ isDropTarget && styles.dropZone,
823
+ isDropTarget && styles.dropZoneActive,
824
+ )}
825
+ style={{
826
+ ...getPinningStyles(cell.column),
827
+ ...(enableColumnResizing
828
+ ? {}
829
+ : { width: columnWidths[columnKey] || "200px" }),
830
+ }}
831
+ data-pinned={isPinned || undefined}
832
+ data-last-col={
833
+ isLastLeftPinned
834
+ ? "left"
835
+ : isFirstRightPinned
836
+ ? "right"
837
+ : undefined
838
+ }
839
+ >
840
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
841
+ </TableCell>
842
+ );
843
+ })}
844
+ </TableRow>
845
+ ))}
846
+ </TableBody>
847
+ </Table>
848
+ </div>
849
+ );
850
+ }
851
+
852
+ const columnHelper = createColumnHelper<PatientRow>();
853
+
854
+ const columns = [
855
+ columnHelper.accessor("firstName", {
856
+ header: () => <TableHeaderContent content="First name" />,
857
+ cell: (props) => <TableCellContent content={props.getValue()} />,
858
+ }),
859
+ columnHelper.accessor("lastName", {
860
+ header: () => <TableHeaderContent content="Last name" />,
861
+ cell: (props) => <TableCellContent content={props.getValue()} />,
862
+ }),
863
+ columnHelper.accessor("id", {
864
+ header: () => <TableHeaderContent content="ID" />,
865
+ cell: (props) => (
866
+ <TableCellContent content={props.getValue()} type="code" />
867
+ ),
868
+ }),
869
+ columnHelper.accessor("birthDate", {
870
+ header: () => <TableHeaderContent content="Birth" />,
871
+ cell: (props) => (
872
+ <TableCellContent content={props.getValue()} type="code" />
873
+ ),
874
+ }),
875
+ columnHelper.accessor("phoneNumber", {
876
+ header: () => <TableHeaderContent content="Phone number" />,
877
+ cell: (props) => <TableCellContent content={props.getValue()} />,
878
+ }),
879
+ columnHelper.accessor("email", {
880
+ header: () => <TableHeaderContent content="Email" />,
881
+ cell: (props) => <TableCellContent content={props.getValue()} />,
882
+ }),
883
+ columnHelper.accessor("gender", {
884
+ header: () => <TableHeaderContent content="Gender" />,
885
+ cell: (props) => <TableCellContent content={props.getValue()} />,
886
+ }),
887
+ columnHelper.accessor("street", {
888
+ header: () => <TableHeaderContent content="Street" />,
889
+ cell: (props) => <TableCellContent content={props.getValue()} />,
890
+ }),
891
+ columnHelper.accessor("city", {
892
+ header: () => <TableHeaderContent content="City" />,
893
+ cell: (props) => <TableCellContent content={props.getValue()} />,
894
+ }),
895
+ columnHelper.accessor("state", {
896
+ header: () => <TableHeaderContent content="State" />,
897
+ cell: (props) => <TableCellContent content={props.getValue()} />,
898
+ }),
899
+ columnHelper.accessor("zip", {
900
+ header: () => <TableHeaderContent content="ZIP" />,
901
+ cell: (props) => <TableCellContent content={props.getValue()} />,
902
+ }),
903
+ columnHelper.accessor("country", {
904
+ header: () => <TableHeaderContent content="Country" />,
905
+ cell: (props) => <TableCellContent content={props.getValue()} />,
906
+ }),
907
+ columnHelper.accessor("encounters", {
908
+ header: () => <TableHeaderContent content="Encounters" />,
909
+ cell: (props) => <TableCellContent content={props.getValue()} />,
910
+ }),
911
+ columnHelper.display({
912
+ id: "actions",
913
+ header: () => <TableHeaderContent content={"Actions"} />,
914
+ cell: ({ row }) => (
915
+ <TableCellContent
916
+ content={
917
+ <button
918
+ type="button"
919
+ className={styles.actionButton}
920
+ onClick={(e) => {
921
+ e.stopPropagation();
922
+ console.log("Match clicked for:", row.original.id);
923
+ }}
924
+ >
925
+ Match
926
+ </button>
927
+ }
928
+ />
929
+ ),
930
+ meta: {
931
+ fixed: "right",
932
+ },
933
+ }),
934
+ ];
935
+
936
+ export type PatientTableProps = {
937
+ // data: PatientRow[],
938
+ page: number;
939
+ count: number;
940
+ showFilters?: boolean;
941
+ showSorting?: boolean;
942
+ enableColumnResizing?: boolean;
943
+ enableColumnReordering?: boolean;
944
+ enableColumnPinning?: boolean;
945
+ showPinningMenu?: boolean;
946
+ columnWidths?: Record<string, string>;
947
+ columnConfigs?: ColumnConfig[];
948
+ };
949
+
950
+ export function PatientTable(props: PatientTableProps) {
951
+ const {
952
+ showFilters = true,
953
+ showSorting = true,
954
+ enableColumnResizing = false,
955
+ enableColumnReordering = false,
956
+ enableColumnPinning = false,
957
+ showPinningMenu = true,
958
+ columnWidths = {},
959
+ columnConfigs,
960
+ } = props;
961
+
962
+ const [sortConfig, setSortConfig] = useState<{
963
+ key: string;
964
+ direction: "asc" | "desc";
965
+ } | null>(null);
966
+
967
+ const [columnOrder, setColumnOrder] = useState<string[]>([]);
968
+
969
+ const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({});
970
+
971
+ useEffect(() => {
972
+ if (!enableColumnPinning || !columnConfigs) return;
973
+
974
+ const fixedColumns = columnConfigs.filter((config) => config.fixed);
975
+ if (fixedColumns.length > 0) {
976
+ const initialPinning: ColumnPinningState = {
977
+ right: fixedColumns.map((config) => config.key),
978
+ };
979
+ setColumnPinning(initialPinning);
980
+ }
981
+ }, [enableColumnPinning, columnConfigs]);
982
+
983
+ const handleSort = (columnKey: string) => {
984
+ setSortConfig((current) => {
985
+ if (current?.key === columnKey) {
986
+ return current.direction === "asc"
987
+ ? { key: columnKey, direction: "desc" }
988
+ : null;
989
+ }
990
+ return { key: columnKey, direction: "asc" };
991
+ });
992
+ };
993
+
994
+ const data: PatientRow[] = [
995
+ {
996
+ id: "PAT-000001",
997
+ firstName: "John",
998
+ lastName: "Smith",
999
+ phoneNumber: "(555) 123-4567",
1000
+ email: "john.smith@gmail.com",
1001
+ birthDate: "1985-03-15",
1002
+ gender: "MALE",
1003
+ street: "123 Main St",
1004
+ city: "New York",
1005
+ state: "NY",
1006
+ zip: "10001",
1007
+ encounters: 5,
1008
+ },
1009
+ {
1010
+ id: "PAT-000002",
1011
+ firstName: "Jane",
1012
+ lastName: "Johnson",
1013
+ phoneNumber: "(555) 234-5678",
1014
+ email: "jane.johnson@yahoo.com",
1015
+ birthDate: "1990-07-22",
1016
+ gender: "FEMALE",
1017
+ street: "456 Oak Ave",
1018
+ city: "Los Angeles",
1019
+ state: "CA",
1020
+ zip: "90210",
1021
+ encounters: 12,
1022
+ },
1023
+ {
1024
+ id: "PAT-000003",
1025
+ firstName: "Michael",
1026
+ lastName: "Williams",
1027
+ phoneNumber: "(555) 345-6789",
1028
+ email: "michael.williams@hotmail.com",
1029
+ birthDate: "1978-11-08",
1030
+ gender: "MALE",
1031
+ street: "789 Pine St",
1032
+ city: "Chicago",
1033
+ state: "IL",
1034
+ zip: "60601",
1035
+ encounters: 8,
1036
+ },
1037
+ {
1038
+ id: "PAT-000004",
1039
+ firstName: "Sarah",
1040
+ lastName: "Brown",
1041
+ phoneNumber: "(555) 456-7890",
1042
+ email: "sarah.brown@outlook.com",
1043
+ birthDate: "1992-01-30",
1044
+ gender: "FEMALE",
1045
+ street: "321 Elm Ave",
1046
+ city: "Houston",
1047
+ state: "TX",
1048
+ zip: "77001",
1049
+ encounters: 3,
1050
+ },
1051
+ {
1052
+ id: "PAT-000005",
1053
+ firstName: "David",
1054
+ lastName: "Garcia",
1055
+ phoneNumber: "(555) 567-8901",
1056
+ email: "david.garcia@gmail.com",
1057
+ birthDate: "1980-09-12",
1058
+ gender: "MALE",
1059
+ street: "654 Cedar St",
1060
+ city: "Phoenix",
1061
+ state: "AZ",
1062
+ zip: "85001",
1063
+ encounters: 15,
1064
+ },
1065
+ {
1066
+ id: "PAT-000006",
1067
+ firstName: "Emily",
1068
+ lastName: "Martinez",
1069
+ phoneNumber: "(555) 678-9012",
1070
+ email: "emily.martinez@icloud.com",
1071
+ birthDate: "1995-05-18",
1072
+ gender: "FEMALE",
1073
+ street: "987 Washington St",
1074
+ city: "Philadelphia",
1075
+ state: "PA",
1076
+ zip: "19101",
1077
+ encounters: 7,
1078
+ },
1079
+ {
1080
+ id: "PAT-000007",
1081
+ firstName: "Robert",
1082
+ lastName: "Davis",
1083
+ phoneNumber: "(555) 789-0123",
1084
+ email: "robert.davis@aol.com",
1085
+ birthDate: "1973-12-03",
1086
+ gender: "MALE",
1087
+ street: "147 Park Ave",
1088
+ city: "San Antonio",
1089
+ state: "TX",
1090
+ zip: "78201",
1091
+ encounters: 22,
1092
+ },
1093
+ {
1094
+ id: "PAT-000008",
1095
+ firstName: "Jessica",
1096
+ lastName: "Rodriguez",
1097
+ phoneNumber: "(555) 890-1234",
1098
+ email: "jessica.rodriguez@protonmail.com",
1099
+ birthDate: "1988-04-25",
1100
+ gender: "FEMALE",
1101
+ street: "258 Lincoln St",
1102
+ city: "San Diego",
1103
+ state: "CA",
1104
+ zip: "92101",
1105
+ encounters: 9,
1106
+ },
1107
+ {
1108
+ id: "PAT-000009",
1109
+ firstName: "William",
1110
+ lastName: "Wilson",
1111
+ phoneNumber: "(555) 901-2345",
1112
+ email: "william.wilson@gmail.com",
1113
+ birthDate: "1965-08-14",
1114
+ gender: "MALE",
1115
+ street: "369 Jefferson Ave",
1116
+ city: "Dallas",
1117
+ state: "TX",
1118
+ zip: "75201",
1119
+ encounters: 18,
1120
+ },
1121
+ {
1122
+ id: "PAT-000010",
1123
+ firstName: "Ashley",
1124
+ lastName: "Anderson",
1125
+ phoneNumber: "(555) 012-3456",
1126
+ email: "ashley.anderson@yahoo.com",
1127
+ birthDate: "1993-10-07",
1128
+ gender: "FEMALE",
1129
+ street: "741 Madison St",
1130
+ city: "San Jose",
1131
+ state: "CA",
1132
+ zip: "95101",
1133
+ encounters: 4,
1134
+ },
1135
+ {
1136
+ id: "PAT-000011",
1137
+ firstName: "Alex",
1138
+ lastName: "Taylor",
1139
+ phoneNumber: "(555) 111-2222",
1140
+ email: "alex.taylor@outlook.com",
1141
+ birthDate: "1987-06-19",
1142
+ gender: "OTHER",
1143
+ street: "852 Adams Ave",
1144
+ city: "Austin",
1145
+ state: "TX",
1146
+ zip: "73301",
1147
+ encounters: 11,
1148
+ },
1149
+ ];
1150
+
1151
+ const sortedData = useMemo(() => {
1152
+ if (!sortConfig) return data;
1153
+
1154
+ return [...data].sort((a, b) => {
1155
+ const aValue = a[sortConfig.key as keyof PatientRow] ?? "";
1156
+ const bValue = b[sortConfig.key as keyof PatientRow] ?? "";
1157
+
1158
+ if (aValue < bValue) return sortConfig.direction === "asc" ? -1 : 1;
1159
+ if (aValue > bValue) return sortConfig.direction === "asc" ? 1 : -1;
1160
+ return 0;
1161
+ });
1162
+ }, [sortConfig]);
1163
+
1164
+ const generatedColumnWidths = useMemo(() => {
1165
+ if (!columnConfigs) {
1166
+ return columnWidths;
1167
+ }
1168
+
1169
+ const configWidths: Record<string, string> = {};
1170
+ columnConfigs.forEach((config) => {
1171
+ if (config.width) {
1172
+ configWidths[config.key] = config.width;
1173
+ }
1174
+ });
1175
+
1176
+ return { ...columnWidths, ...configWidths };
1177
+ }, [columnConfigs, columnWidths]);
1178
+
1179
+ const generatedColumns = useMemo(() => {
1180
+ if (!columnConfigs) {
1181
+ return columns;
1182
+ }
1183
+
1184
+ return columnConfigs.map((config): ColumnDef<PatientRow> => {
1185
+ const columnSize = config.width
1186
+ ? parseInt(config.width.replace("px", ""), 10)
1187
+ : undefined;
1188
+
1189
+ if (config.type === "link" || config.type === "button") {
1190
+ return {
1191
+ id: config.key,
1192
+ accessorFn: () => null,
1193
+ header: () => <TableHeaderContent content={config.label} />,
1194
+ cell: ({ row }: { row: { original: PatientRow } }) => (
1195
+ <TableCellContent
1196
+ content=""
1197
+ {...(config.type && { type: config.type })}
1198
+ {...(config.rightAlign !== undefined && {
1199
+ rightAlign: config.rightAlign,
1200
+ })}
1201
+ actions={config.actions || []}
1202
+ rowId={row.original.id}
1203
+ />
1204
+ ),
1205
+ ...(config.fixed && { meta: { fixed: "right" } }),
1206
+ ...(columnSize && { size: columnSize }),
1207
+ };
1208
+ }
1209
+
1210
+ return {
1211
+ accessorKey: config.key,
1212
+ header: () => <TableHeaderContent content={config.label} />,
1213
+ cell: ({ cell }: { cell: { getValue: () => any } }) => (
1214
+ <TableCellContent
1215
+ content={cell.getValue()}
1216
+ type={config.type || "text"}
1217
+ {...(config.rightAlign !== undefined && {
1218
+ rightAlign: config.rightAlign,
1219
+ })}
1220
+ />
1221
+ ),
1222
+ ...(config.fixed && { meta: { fixed: "right" } }),
1223
+ ...(columnSize && { size: columnSize }),
1224
+ };
1225
+ });
1226
+ }, [columnConfigs]);
1227
+
1228
+ const columnsWithSorting = generatedColumns.map((column) => ({
1229
+ ...column,
1230
+ header: () => {
1231
+ const columnKey = (column as any).id || (column as any).accessorKey;
1232
+ const isSortable = columnKey !== "actions";
1233
+ const isPinnedLeft = columnPinning.left?.includes(columnKey);
1234
+ const isPinnedRight = columnPinning.right?.includes(columnKey);
1235
+ const pinnedDirection = isPinnedLeft
1236
+ ? "left"
1237
+ : isPinnedRight
1238
+ ? "right"
1239
+ : false;
1240
+
1241
+ const handlePin = (direction: "left" | "right" | false) => {
1242
+ const newPinning: ColumnPinningState = { ...columnPinning };
1243
+
1244
+ if (newPinning.left) {
1245
+ newPinning.left = newPinning.left.filter((id) => id !== columnKey);
1246
+ }
1247
+ if (newPinning.right) {
1248
+ newPinning.right = newPinning.right.filter((id) => id !== columnKey);
1249
+ }
1250
+
1251
+ if (direction === "left") {
1252
+ newPinning.left = [...(newPinning.left || []), columnKey];
1253
+ } else if (direction === "right") {
1254
+ newPinning.right = [...(newPinning.right || []), columnKey];
1255
+ }
1256
+
1257
+ setColumnPinning(newPinning);
1258
+ };
1259
+
1260
+ return (
1261
+ <TableHeaderContent
1262
+ content={
1263
+ typeof column.header === "function"
1264
+ ? column.header({} as any)
1265
+ : column.header
1266
+ }
1267
+ isSortable={isSortable && showSorting}
1268
+ sortDirection={
1269
+ sortConfig?.key === columnKey && sortConfig
1270
+ ? sortConfig.direction
1271
+ : null
1272
+ }
1273
+ isPinnable={enableColumnPinning && showPinningMenu}
1274
+ pinnedDirection={pinnedDirection}
1275
+ onPin={handlePin}
1276
+ />
1277
+ );
1278
+ },
1279
+ })) as ColumnDef<PatientRow>[];
1280
+
1281
+ return (
1282
+ <DataTable
1283
+ columns={columnsWithSorting}
1284
+ data={sortedData}
1285
+ showZebraStripes={true}
1286
+ showFilters={showFilters}
1287
+ showSorting={showSorting}
1288
+ enableColumnResizing={enableColumnResizing}
1289
+ enableColumnReordering={enableColumnReordering}
1290
+ enableColumnPinning={enableColumnPinning}
1291
+ onSort={handleSort}
1292
+ columnWidths={generatedColumnWidths}
1293
+ sortConfig={sortConfig}
1294
+ columnOrder={columnOrder}
1295
+ onColumnOrderChange={setColumnOrder}
1296
+ columnPinning={columnPinning}
1297
+ onColumnPinningChange={setColumnPinning}
1298
+ {...(columnConfigs && { columnConfigs })}
1299
+ />
1300
+ );
1301
+ }