@exxatdesignux/ui 0.2.18 → 0.3.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 (618) hide show
  1. package/CHANGELOG.md +69 -1
  2. package/bin/sync-extras.mjs +116 -29
  3. package/consumer-extras/README.md +43 -4
  4. package/consumer-extras/cursor-rules/exxat-accessibility.mdc +39 -0
  5. package/consumer-extras/cursor-rules/exxat-board-cards.mdc +26 -0
  6. package/consumer-extras/cursor-rules/exxat-breadcrumbs-no-back.mdc +21 -0
  7. package/consumer-extras/cursor-rules/exxat-card-vs-list-rows.mdc +21 -0
  8. package/consumer-extras/cursor-rules/exxat-centralized-list-dataset.mdc +44 -0
  9. package/consumer-extras/cursor-rules/exxat-collaboration-access.mdc +32 -0
  10. package/consumer-extras/cursor-rules/exxat-command-menu.mdc +22 -0
  11. package/consumer-extras/cursor-rules/exxat-dashboard-view-charts.mdc +53 -0
  12. package/consumer-extras/cursor-rules/exxat-data-tables.mdc +41 -0
  13. package/consumer-extras/cursor-rules/exxat-dedicated-search-surfaces.mdc +25 -0
  14. package/consumer-extras/cursor-rules/exxat-drawer-vs-dialog.mdc +22 -0
  15. package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +56 -0
  16. package/consumer-extras/cursor-rules/exxat-fontawesome-icons.mdc +31 -0
  17. package/consumer-extras/cursor-rules/exxat-kbd-shortcuts.mdc +100 -0
  18. package/consumer-extras/cursor-rules/exxat-kpi-flat-band.mdc +28 -0
  19. package/consumer-extras/cursor-rules/exxat-kpi-max-four.mdc +21 -0
  20. package/consumer-extras/cursor-rules/exxat-kpi-trends.mdc +31 -0
  21. package/consumer-extras/cursor-rules/exxat-list-page-connected-views.mdc +24 -0
  22. package/consumer-extras/cursor-rules/exxat-list-page-view-shells.mdc +31 -0
  23. package/consumer-extras/cursor-rules/exxat-mono-ids.mdc +30 -0
  24. package/consumer-extras/cursor-rules/exxat-no-slds-leakage.mdc +78 -0
  25. package/consumer-extras/cursor-rules/exxat-no-toast.mdc +25 -0
  26. package/consumer-extras/cursor-rules/exxat-page-vs-drawer.mdc +23 -0
  27. package/consumer-extras/cursor-rules/exxat-person-identity-display.mdc +47 -0
  28. package/consumer-extras/cursor-rules/exxat-primary-nav-secondary-panel.mdc +52 -0
  29. package/consumer-extras/cursor-rules/exxat-question-bank-hub-header.mdc +28 -0
  30. package/consumer-extras/cursor-rules/exxat-reuse-before-custom.mdc +34 -0
  31. package/consumer-extras/cursor-rules/exxat-table-properties-drawer.mdc +77 -0
  32. package/consumer-extras/cursor-rules/exxat-token-discipline.mdc +103 -0
  33. package/consumer-extras/cursor-skills/exxat-accessibility/SKILL.md +1 -1
  34. package/consumer-extras/cursor-skills/exxat-board-cards/SKILL.md +2 -2
  35. package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +1 -1
  36. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +9 -9
  37. package/consumer-extras/cursor-skills/exxat-ds-skill/references/data-table-pattern.md +1 -1
  38. package/consumer-extras/handbook/HANDBOOK.md +185 -0
  39. package/consumer-extras/handbook/glossary.md +57 -0
  40. package/consumer-extras/handbook/reference-implementations.md +126 -0
  41. package/consumer-extras/handbook/voice-and-tone.md +262 -0
  42. package/consumer-extras/patterns/command-menu-pattern.md +1 -1
  43. package/consumer-extras/patterns/data-views-pattern.md +14 -14
  44. package/dist/components/data-table/filter-date-calendar.d.ts +10 -0
  45. package/dist/components/data-table/filter-date-calendar.js +280 -0
  46. package/dist/components/data-table/filter-date-calendar.js.map +1 -0
  47. package/dist/components/data-table/filter-text-value-input.d.ts +15 -0
  48. package/dist/components/data-table/filter-text-value-input.js +561 -0
  49. package/dist/components/data-table/filter-text-value-input.js.map +1 -0
  50. package/dist/components/data-table/index.d.ts +45 -0
  51. package/dist/components/data-table/index.js +3085 -0
  52. package/dist/components/data-table/index.js.map +1 -0
  53. package/dist/components/data-table/pagination.d.ts +28 -0
  54. package/dist/components/data-table/pagination.js +3264 -0
  55. package/dist/components/data-table/pagination.js.map +1 -0
  56. package/dist/components/data-table/types.d.ts +84 -0
  57. package/dist/components/data-table/types.js +3 -0
  58. package/dist/components/data-table/types.js.map +1 -0
  59. package/dist/components/data-table/use-table-state.d.ts +116 -0
  60. package/dist/components/data-table/use-table-state.js +670 -0
  61. package/dist/components/data-table/use-table-state.js.map +1 -0
  62. package/dist/components/data-views/board-card-primitives.d.ts +22 -0
  63. package/dist/components/data-views/board-card-primitives.js +84 -0
  64. package/dist/components/data-views/board-card-primitives.js.map +1 -0
  65. package/dist/components/data-views/data-row-list.d.ts +33 -0
  66. package/dist/components/data-views/data-row-list.js +106 -0
  67. package/dist/components/data-views/data-row-list.js.map +1 -0
  68. package/dist/components/data-views/finder-panel-view.d.ts +54 -0
  69. package/dist/components/data-views/finder-panel-view.js +388 -0
  70. package/dist/components/data-views/finder-panel-view.js.map +1 -0
  71. package/dist/components/data-views/folder-grid-view.d.ts +22 -0
  72. package/dist/components/data-views/folder-grid-view.js +58 -0
  73. package/dist/components/data-views/folder-grid-view.js.map +1 -0
  74. package/dist/components/data-views/hub-table.d.ts +167 -0
  75. package/dist/components/data-views/hub-table.js +5561 -0
  76. package/dist/components/data-views/hub-table.js.map +1 -0
  77. package/dist/components/data-views/index.d.ts +27 -0
  78. package/dist/components/data-views/index.js +6575 -0
  79. package/dist/components/data-views/index.js.map +1 -0
  80. package/dist/components/data-views/list-page-board-card.d.ts +72 -0
  81. package/dist/components/data-views/list-page-board-card.js +264 -0
  82. package/dist/components/data-views/list-page-board-card.js.map +1 -0
  83. package/dist/components/data-views/list-page-board-template.d.ts +24 -0
  84. package/dist/components/data-views/list-page-board-template.js +137 -0
  85. package/dist/components/data-views/list-page-board-template.js.map +1 -0
  86. package/dist/components/data-views/list-page-connected-view-body.d.ts +19 -0
  87. package/dist/components/data-views/list-page-connected-view-body.js +116 -0
  88. package/dist/components/data-views/list-page-connected-view-body.js.map +1 -0
  89. package/dist/components/data-views/list-page-split-details-placeholder.d.ts +14 -0
  90. package/dist/components/data-views/list-page-split-details-placeholder.js +38 -0
  91. package/dist/components/data-views/list-page-split-details-placeholder.js.map +1 -0
  92. package/dist/components/data-views/list-page-split-hub-chrome.d.ts +17 -0
  93. package/dist/components/data-views/list-page-split-hub-chrome.js +54 -0
  94. package/dist/components/data-views/list-page-split-hub-chrome.js.map +1 -0
  95. package/dist/components/data-views/list-page-split-hub-tokens.d.ts +12 -0
  96. package/dist/components/data-views/list-page-split-hub-tokens.js +8 -0
  97. package/dist/components/data-views/list-page-split-hub-tokens.js.map +1 -0
  98. package/dist/components/data-views/list-page-tree-column-header.d.ts +15 -0
  99. package/dist/components/data-views/list-page-tree-column-header.js +22 -0
  100. package/dist/components/data-views/list-page-tree-column-header.js.map +1 -0
  101. package/dist/components/data-views/list-page-tree-panel-shell.d.ts +25 -0
  102. package/dist/components/data-views/list-page-tree-panel-shell.js +146 -0
  103. package/dist/components/data-views/list-page-tree-panel-shell.js.map +1 -0
  104. package/dist/components/data-views/os-folder-glyph.d.ts +35 -0
  105. package/dist/components/data-views/os-folder-glyph.js +104 -0
  106. package/dist/components/data-views/os-folder-glyph.js.map +1 -0
  107. package/dist/components/data-views/outline-tree-menu.d.ts +36 -0
  108. package/dist/components/data-views/outline-tree-menu.js +131 -0
  109. package/dist/components/data-views/outline-tree-menu.js.map +1 -0
  110. package/dist/components/table-properties/column-row.d.ts +22 -0
  111. package/dist/components/table-properties/column-row.js +153 -0
  112. package/dist/components/table-properties/column-row.js.map +1 -0
  113. package/dist/components/table-properties/draggable-list.d.ts +24 -0
  114. package/dist/components/table-properties/draggable-list.js +53 -0
  115. package/dist/components/table-properties/draggable-list.js.map +1 -0
  116. package/dist/components/table-properties/drawer-button.d.ts +110 -0
  117. package/dist/components/table-properties/drawer-button.js +2748 -0
  118. package/dist/components/table-properties/drawer-button.js.map +1 -0
  119. package/dist/components/table-properties/drawer.d.ts +100 -0
  120. package/dist/components/table-properties/drawer.js +2595 -0
  121. package/dist/components/table-properties/drawer.js.map +1 -0
  122. package/dist/components/table-properties/filter-card.d.ts +24 -0
  123. package/dist/components/table-properties/filter-card.js +854 -0
  124. package/dist/components/table-properties/filter-card.js.map +1 -0
  125. package/dist/components/table-properties/index.d.ts +14 -0
  126. package/dist/components/table-properties/index.js +2768 -0
  127. package/dist/components/table-properties/index.js.map +1 -0
  128. package/dist/components/table-properties/sort-card.d.ts +20 -0
  129. package/dist/components/table-properties/sort-card.js +102 -0
  130. package/dist/components/table-properties/sort-card.js.map +1 -0
  131. package/dist/components/templates/dedicated-search-landing-template.d.ts +21 -0
  132. package/dist/components/templates/dedicated-search-landing-template.js +254 -0
  133. package/dist/components/templates/dedicated-search-landing-template.js.map +1 -0
  134. package/dist/components/templates/dedicated-search-results-template.d.ts +15 -0
  135. package/dist/components/templates/dedicated-search-results-template.js +16 -0
  136. package/dist/components/templates/dedicated-search-results-template.js.map +1 -0
  137. package/dist/components/templates/index.d.ts +9 -0
  138. package/dist/components/templates/index.js +2720 -0
  139. package/dist/components/templates/index.js.map +1 -0
  140. package/dist/components/templates/list-page.d.ts +83 -0
  141. package/dist/components/templates/list-page.js +2433 -0
  142. package/dist/components/templates/list-page.js.map +1 -0
  143. package/dist/components/templates/nested-secondary-panel-shell.d.ts +20 -0
  144. package/dist/components/templates/nested-secondary-panel-shell.js +54 -0
  145. package/dist/components/templates/nested-secondary-panel-shell.js.map +1 -0
  146. package/dist/components/ui/accordion.d.ts +10 -0
  147. package/dist/components/ui/accordion.js +74 -0
  148. package/dist/components/ui/accordion.js.map +1 -0
  149. package/dist/components/ui/alert-dialog.d.ts +37 -0
  150. package/dist/components/ui/alert-dialog.js +201 -0
  151. package/dist/components/ui/alert-dialog.js.map +1 -0
  152. package/dist/components/ui/avatar.d.ts +84 -0
  153. package/dist/components/ui/avatar.js +328 -0
  154. package/dist/components/ui/avatar.js.map +1 -0
  155. package/dist/components/ui/badge.d.ts +13 -0
  156. package/dist/components/ui/badge.js +49 -0
  157. package/dist/components/ui/badge.js.map +1 -0
  158. package/dist/components/ui/banner.d.ts +62 -0
  159. package/dist/components/ui/banner.js +364 -0
  160. package/dist/components/ui/banner.js.map +1 -0
  161. package/dist/components/ui/breadcrumb.d.ts +14 -0
  162. package/dist/components/ui/breadcrumb.js +114 -0
  163. package/dist/components/ui/breadcrumb.js.map +1 -0
  164. package/dist/components/ui/button.d.ts +16 -0
  165. package/dist/components/ui/button.js +59 -0
  166. package/dist/components/ui/button.js.map +1 -0
  167. package/dist/components/ui/calendar.d.ts +13 -0
  168. package/dist/components/ui/calendar.js +238 -0
  169. package/dist/components/ui/calendar.js.map +1 -0
  170. package/dist/components/ui/card.d.ts +14 -0
  171. package/dist/components/ui/card.js +102 -0
  172. package/dist/components/ui/card.js.map +1 -0
  173. package/dist/components/ui/chart.d.ts +58 -0
  174. package/dist/components/ui/chart.js +292 -0
  175. package/dist/components/ui/chart.js.map +1 -0
  176. package/dist/components/ui/checkbox.d.ts +23 -0
  177. package/dist/components/ui/checkbox.js +155 -0
  178. package/dist/components/ui/checkbox.js.map +1 -0
  179. package/dist/components/ui/coach-mark.d.ts +27 -0
  180. package/dist/components/ui/coach-mark.js +306 -0
  181. package/dist/components/ui/coach-mark.js.map +1 -0
  182. package/dist/components/ui/collapsible.d.ts +8 -0
  183. package/dist/components/ui/collapsible.js +35 -0
  184. package/dist/components/ui/collapsible.js.map +1 -0
  185. package/dist/components/ui/command.d.ts +36 -0
  186. package/dist/components/ui/command.js +274 -0
  187. package/dist/components/ui/command.js.map +1 -0
  188. package/dist/components/ui/context-menu.d.ts +32 -0
  189. package/dist/components/ui/context-menu.js +245 -0
  190. package/dist/components/ui/context-menu.js.map +1 -0
  191. package/dist/components/ui/date-picker-field.d.ts +38 -0
  192. package/dist/components/ui/date-picker-field.js +550 -0
  193. package/dist/components/ui/date-picker-field.js.map +1 -0
  194. package/dist/components/ui/dialog.d.ts +22 -0
  195. package/dist/components/ui/dialog.js +200 -0
  196. package/dist/components/ui/dialog.js.map +1 -0
  197. package/dist/components/ui/dot-pattern.d.ts +21 -0
  198. package/dist/components/ui/dot-pattern.js +139 -0
  199. package/dist/components/ui/dot-pattern.js.map +1 -0
  200. package/dist/components/ui/drag-handle-grip.d.ts +10 -0
  201. package/dist/components/ui/drag-handle-grip.js +15 -0
  202. package/dist/components/ui/drag-handle-grip.js.map +1 -0
  203. package/dist/components/ui/drawer.d.ts +16 -0
  204. package/dist/components/ui/drawer.js +125 -0
  205. package/dist/components/ui/drawer.js.map +1 -0
  206. package/dist/components/ui/dropdown-menu.d.ts +45 -0
  207. package/dist/components/ui/dropdown-menu.js +353 -0
  208. package/dist/components/ui/dropdown-menu.js.map +1 -0
  209. package/dist/components/ui/export-drawer.d.ts +11 -0
  210. package/dist/components/ui/export-drawer.js +1658 -0
  211. package/dist/components/ui/export-drawer.js.map +1 -0
  212. package/dist/components/ui/field.d.ts +30 -0
  213. package/dist/components/ui/field.js +249 -0
  214. package/dist/components/ui/field.js.map +1 -0
  215. package/dist/components/ui/form.d.ts +28 -0
  216. package/dist/components/ui/form.js +110 -0
  217. package/dist/components/ui/form.js.map +1 -0
  218. package/dist/components/ui/hover-card.d.ts +9 -0
  219. package/dist/components/ui/hover-card.js +43 -0
  220. package/dist/components/ui/hover-card.js.map +1 -0
  221. package/dist/components/ui/input-group.d.ts +20 -0
  222. package/dist/components/ui/input-group.js +219 -0
  223. package/dist/components/ui/input-group.js.map +1 -0
  224. package/dist/components/ui/input-mask.d.ts +39 -0
  225. package/dist/components/ui/input-mask.js +118 -0
  226. package/dist/components/ui/input-mask.js.map +1 -0
  227. package/dist/components/ui/input.d.ts +5 -0
  228. package/dist/components/ui/input.js +30 -0
  229. package/dist/components/ui/input.js.map +1 -0
  230. package/dist/components/ui/kbd.d.ts +20 -0
  231. package/dist/components/ui/kbd.js +45 -0
  232. package/dist/components/ui/kbd.js.map +1 -0
  233. package/dist/components/ui/key-metrics-context.d.ts +19 -0
  234. package/dist/components/ui/key-metrics-context.js +26 -0
  235. package/dist/components/ui/key-metrics-context.js.map +1 -0
  236. package/dist/components/ui/key-metrics.d.ts +131 -0
  237. package/dist/components/ui/key-metrics.js +1015 -0
  238. package/dist/components/ui/key-metrics.js.map +1 -0
  239. package/dist/components/ui/label.d.ts +6 -0
  240. package/dist/components/ui/label.js +28 -0
  241. package/dist/components/ui/label.js.map +1 -0
  242. package/dist/components/ui/list-page-view-frame.d.ts +22 -0
  243. package/dist/components/ui/list-page-view-frame.js +24 -0
  244. package/dist/components/ui/list-page-view-frame.js.map +1 -0
  245. package/dist/components/ui/page-header.d.ts +51 -0
  246. package/dist/components/ui/page-header.js +372 -0
  247. package/dist/components/ui/page-header.js.map +1 -0
  248. package/dist/components/ui/payment-card-fields.d.ts +10 -0
  249. package/dist/components/ui/payment-card-fields.js +80 -0
  250. package/dist/components/ui/payment-card-fields.js.map +1 -0
  251. package/dist/components/ui/popover.d.ts +10 -0
  252. package/dist/components/ui/popover.js +47 -0
  253. package/dist/components/ui/popover.js.map +1 -0
  254. package/dist/components/ui/radio-group.d.ts +29 -0
  255. package/dist/components/ui/radio-group.js +190 -0
  256. package/dist/components/ui/radio-group.js.map +1 -0
  257. package/dist/components/ui/resizable.d.ts +16 -0
  258. package/dist/components/ui/resizable.js +51 -0
  259. package/dist/components/ui/resizable.js.map +1 -0
  260. package/dist/components/ui/scroll-area.d.ts +8 -0
  261. package/dist/components/ui/scroll-area.js +66 -0
  262. package/dist/components/ui/scroll-area.js.map +1 -0
  263. package/dist/components/ui/select.d.ts +18 -0
  264. package/dist/components/ui/select.js +186 -0
  265. package/dist/components/ui/select.js.map +1 -0
  266. package/dist/components/ui/selection-tile-grid.d.ts +52 -0
  267. package/dist/components/ui/selection-tile-grid.js +347 -0
  268. package/dist/components/ui/selection-tile-grid.js.map +1 -0
  269. package/dist/components/ui/separator.d.ts +7 -0
  270. package/dist/components/ui/separator.js +33 -0
  271. package/dist/components/ui/separator.js.map +1 -0
  272. package/dist/components/ui/sheet.d.ts +18 -0
  273. package/dist/components/ui/sheet.js +181 -0
  274. package/dist/components/ui/sheet.js.map +1 -0
  275. package/dist/components/ui/sidebar.d.ts +94 -0
  276. package/dist/components/ui/sidebar.js +805 -0
  277. package/dist/components/ui/sidebar.js.map +1 -0
  278. package/dist/components/ui/skeleton.d.ts +5 -0
  279. package/dist/components/ui/skeleton.js +22 -0
  280. package/dist/components/ui/skeleton.js.map +1 -0
  281. package/dist/components/ui/slider.d.ts +7 -0
  282. package/dist/components/ui/slider.js +66 -0
  283. package/dist/components/ui/slider.js.map +1 -0
  284. package/dist/components/ui/sonner.d.ts +6 -0
  285. package/dist/components/ui/sonner.js +38 -0
  286. package/dist/components/ui/sonner.js.map +1 -0
  287. package/dist/components/ui/status-badge.d.ts +38 -0
  288. package/dist/components/ui/status-badge.js +77 -0
  289. package/dist/components/ui/status-badge.js.map +1 -0
  290. package/dist/components/ui/table.d.ts +13 -0
  291. package/dist/components/ui/table.js +115 -0
  292. package/dist/components/ui/table.js.map +1 -0
  293. package/dist/components/ui/tabs.d.ts +15 -0
  294. package/dist/components/ui/tabs.js +93 -0
  295. package/dist/components/ui/tabs.js.map +1 -0
  296. package/dist/components/ui/textarea.d.ts +6 -0
  297. package/dist/components/ui/textarea.js +25 -0
  298. package/dist/components/ui/textarea.js.map +1 -0
  299. package/dist/components/ui/tip.d.ts +12 -0
  300. package/dist/components/ui/tip.js +61 -0
  301. package/dist/components/ui/tip.js.map +1 -0
  302. package/dist/components/ui/toggle-group.d.ts +14 -0
  303. package/dist/components/ui/toggle-group.js +104 -0
  304. package/dist/components/ui/toggle-group.js.map +1 -0
  305. package/dist/components/ui/toggle-switch.d.ts +10 -0
  306. package/dist/components/ui/toggle-switch.js +33 -0
  307. package/dist/components/ui/toggle-switch.js.map +1 -0
  308. package/dist/components/ui/toggle.d.ts +13 -0
  309. package/dist/components/ui/toggle.js +51 -0
  310. package/dist/components/ui/toggle.js.map +1 -0
  311. package/dist/components/ui/tooltip.d.ts +10 -0
  312. package/dist/components/ui/tooltip.js +68 -0
  313. package/dist/components/ui/tooltip.js.map +1 -0
  314. package/dist/components/ui/view-segmented-control.d.ts +31 -0
  315. package/dist/components/ui/view-segmented-control.js +167 -0
  316. package/dist/components/ui/view-segmented-control.js.map +1 -0
  317. package/dist/data-list-view-registry-CyBoBML4.d.ts +73 -0
  318. package/dist/hooks/use-app-theme.d.ts +24 -0
  319. package/dist/hooks/use-app-theme.js +286 -0
  320. package/dist/hooks/use-app-theme.js.map +1 -0
  321. package/dist/hooks/use-coach-mark.d.ts +86 -0
  322. package/dist/hooks/use-coach-mark.js +218 -0
  323. package/dist/hooks/use-coach-mark.js.map +1 -0
  324. package/dist/hooks/use-mobile.d.ts +3 -0
  325. package/dist/hooks/use-mobile.js +29 -0
  326. package/dist/hooks/use-mobile.js.map +1 -0
  327. package/dist/hooks/use-mod-key-label.d.ts +6 -0
  328. package/dist/hooks/use-mod-key-label.js +25 -0
  329. package/dist/hooks/use-mod-key-label.js.map +1 -0
  330. package/dist/index.d.ts +120 -0
  331. package/dist/index.js +13324 -0
  332. package/dist/index.js.map +1 -0
  333. package/dist/lib/compose-refs.d.ts +6 -0
  334. package/dist/lib/compose-refs.js +17 -0
  335. package/dist/lib/compose-refs.js.map +1 -0
  336. package/dist/lib/conditional-rule-match.d.ts +30 -0
  337. package/dist/lib/conditional-rule-match.js +66 -0
  338. package/dist/lib/conditional-rule-match.js.map +1 -0
  339. package/dist/lib/data-list-display-options.d.ts +26 -0
  340. package/dist/lib/data-list-display-options.js +14 -0
  341. package/dist/lib/data-list-display-options.js.map +1 -0
  342. package/dist/lib/data-list-view-registry.d.ts +2 -0
  343. package/dist/lib/data-list-view-registry.js +102 -0
  344. package/dist/lib/data-list-view-registry.js.map +1 -0
  345. package/dist/lib/data-list-view-surface.d.ts +2 -0
  346. package/dist/lib/data-list-view-surface.js +80 -0
  347. package/dist/lib/data-list-view-surface.js.map +1 -0
  348. package/dist/lib/data-list-view.d.ts +21 -0
  349. package/dist/lib/data-list-view.js +25 -0
  350. package/dist/lib/data-list-view.js.map +1 -0
  351. package/dist/lib/date-filter.d.ts +22 -0
  352. package/dist/lib/date-filter.js +61 -0
  353. package/dist/lib/date-filter.js.map +1 -0
  354. package/dist/lib/dev-log.d.ts +8 -0
  355. package/dist/lib/dev-log.js +10 -0
  356. package/dist/lib/dev-log.js.map +1 -0
  357. package/dist/lib/dropdown-menu-surface.d.ts +14 -0
  358. package/dist/lib/dropdown-menu-surface.js +6 -0
  359. package/dist/lib/dropdown-menu-surface.js.map +1 -0
  360. package/dist/lib/editable-target.d.ts +12 -0
  361. package/dist/lib/editable-target.js +12 -0
  362. package/dist/lib/editable-target.js.map +1 -0
  363. package/dist/lib/list-page-table-properties.d.ts +35 -0
  364. package/dist/lib/list-page-table-properties.js +81 -0
  365. package/dist/lib/list-page-table-properties.js.map +1 -0
  366. package/dist/lib/raf-throttle.d.ts +23 -0
  367. package/dist/lib/raf-throttle.js +27 -0
  368. package/dist/lib/raf-throttle.js.map +1 -0
  369. package/dist/lib/row-height.d.ts +16 -0
  370. package/dist/lib/row-height.js +10 -0
  371. package/dist/lib/row-height.js.map +1 -0
  372. package/dist/lib/table-properties-types.d.ts +83 -0
  373. package/dist/lib/table-properties-types.js +19 -0
  374. package/dist/lib/table-properties-types.js.map +1 -0
  375. package/dist/lib/utils.d.ts +5 -0
  376. package/dist/lib/utils.js +11 -0
  377. package/dist/lib/utils.js.map +1 -0
  378. package/package.json +83 -18
  379. package/src/components/data-table/filter-date-calendar.tsx +38 -0
  380. package/src/components/data-table/filter-text-value-input.tsx +77 -0
  381. package/src/components/data-table/index.tsx +1678 -0
  382. package/src/components/data-table/pagination.tsx +255 -0
  383. package/src/components/data-table/types.ts +96 -0
  384. package/src/components/data-table/use-table-state.ts +767 -0
  385. package/src/components/data-views/board-card-primitives.tsx +93 -0
  386. package/src/components/data-views/data-row-list.tsx +183 -0
  387. package/src/components/data-views/finder-panel-view.tsx +405 -0
  388. package/src/components/data-views/folder-grid-view.tsx +86 -0
  389. package/src/components/data-views/hub-table.tsx +498 -0
  390. package/src/components/data-views/index.ts +28 -0
  391. package/src/components/data-views/list-page-board-card.tsx +192 -0
  392. package/src/components/data-views/list-page-board-template.tsx +122 -0
  393. package/src/components/data-views/list-page-connected-view-body.tsx +66 -0
  394. package/src/components/data-views/list-page-split-details-placeholder.tsx +39 -0
  395. package/src/components/data-views/list-page-split-hub-chrome.tsx +60 -0
  396. package/src/components/data-views/list-page-split-hub-tokens.ts +16 -0
  397. package/src/components/data-views/list-page-tree-column-header.tsx +31 -0
  398. package/src/components/data-views/list-page-tree-panel-shell.tsx +91 -0
  399. package/src/components/data-views/os-folder-glyph.tsx +141 -0
  400. package/src/components/data-views/outline-tree-menu.tsx +157 -0
  401. package/src/components/table-properties/column-row.tsx +90 -0
  402. package/src/components/table-properties/draggable-list.ts +54 -0
  403. package/src/components/table-properties/drawer-button.tsx +300 -0
  404. package/src/components/table-properties/drawer.tsx +1148 -0
  405. package/src/components/table-properties/filter-card.tsx +251 -0
  406. package/src/components/table-properties/index.ts +36 -0
  407. package/src/components/table-properties/sort-card.tsx +63 -0
  408. package/src/components/templates/dedicated-search-landing-template.tsx +124 -0
  409. package/src/components/templates/dedicated-search-results-template.tsx +19 -0
  410. package/src/components/templates/index.ts +33 -0
  411. package/src/components/templates/list-page.tsx +602 -0
  412. package/src/components/templates/nested-secondary-panel-shell.tsx +70 -0
  413. package/src/components/ui/accordion.tsx +92 -0
  414. package/src/components/ui/alert-dialog.tsx +221 -0
  415. package/src/components/ui/avatar.tsx +13 -2
  416. package/src/components/ui/banner.tsx +2 -2
  417. package/src/components/ui/calendar.tsx +1 -1
  418. package/src/components/ui/coach-mark.tsx +1 -1
  419. package/src/components/ui/context-menu.tsx +291 -0
  420. package/src/components/ui/date-picker-field.tsx +2 -2
  421. package/src/components/ui/dot-pattern.tsx +183 -0
  422. package/src/components/ui/export-drawer.tsx +375 -0
  423. package/src/components/ui/hover-card.tsx +66 -0
  424. package/src/components/ui/key-metrics-context.tsx +78 -0
  425. package/src/components/ui/key-metrics.tsx +1133 -0
  426. package/src/components/ui/list-page-view-frame.tsx +64 -0
  427. package/src/components/ui/page-header.tsx +244 -0
  428. package/src/components/ui/payment-card-fields.tsx +2 -2
  429. package/src/components/ui/resizable.tsx +68 -0
  430. package/src/components/ui/scroll-area.tsx +72 -0
  431. package/src/components/ui/selection-tile-grid.tsx +9 -2
  432. package/src/components/ui/sidebar.tsx +84 -12
  433. package/src/components/ui/slider.tsx +83 -0
  434. package/src/globals.css +494 -151
  435. package/src/globals.d.ts +20 -0
  436. package/src/index.ts +68 -1
  437. package/src/lib/conditional-rule-match.ts +119 -0
  438. package/src/lib/data-list-display-options.ts +35 -0
  439. package/src/lib/data-list-view-registry.ts +104 -0
  440. package/src/lib/data-list-view-surface.ts +83 -0
  441. package/src/lib/data-list-view.ts +47 -0
  442. package/src/lib/dev-log.ts +10 -0
  443. package/src/lib/editable-target.ts +20 -0
  444. package/src/lib/list-page-table-properties.ts +48 -0
  445. package/src/lib/raf-throttle.ts +45 -0
  446. package/src/lib/row-height.ts +19 -0
  447. package/src/lib/table-properties-types.ts +98 -0
  448. package/template/.cursor/rules/exxat-command-menu.mdc +1 -1
  449. package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +3 -3
  450. package/template/.cursor/rules/exxat-data-tables.mdc +1 -1
  451. package/template/.cursor/rules/exxat-ds-agents.mdc +2 -2
  452. package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +2 -2
  453. package/template/.cursor/rules/exxat-table-properties-drawer.mdc +1 -1
  454. package/template/AGENTS.md +84 -20
  455. package/template/app/(app)/examples/page.tsx +0 -1
  456. package/template/app/(app)/layout.tsx +17 -4
  457. package/template/app/(app)/question-bank/layout.tsx +1 -1
  458. package/template/app/(app)/question-bank/new/page.tsx +11 -24
  459. package/template/app/globals.css +13 -1972
  460. package/template/components/ask-leo-sidebar.tsx +5 -1
  461. package/template/components/brand-color-picker.tsx +2 -2
  462. package/template/components/charts-overview.tsx +1 -1
  463. package/template/components/compliance-table.tsx +240 -384
  464. package/template/components/dashboard-report-charts.tsx +1 -1
  465. package/template/components/dashboard-tabs.tsx +1 -1
  466. package/template/components/data-table/filter-date-calendar.tsx +1 -38
  467. package/template/components/data-table/filter-text-value-input.tsx +1 -77
  468. package/template/components/data-table/index.tsx +1 -1634
  469. package/template/components/data-table/pagination.tsx +1 -255
  470. package/template/components/data-table/types.ts +1 -94
  471. package/template/components/data-table/use-table-state.test.ts +420 -0
  472. package/template/components/data-table/use-table-state.ts +1 -758
  473. package/template/components/data-view-dashboard-charts-compliance.tsx +2 -2
  474. package/template/components/data-view-dashboard-charts-team.tsx +2 -2
  475. package/template/components/data-view-dashboard-charts.tsx +2 -2
  476. package/template/components/data-views/board-card-primitives.tsx +1 -93
  477. package/template/components/data-views/data-row-list.tsx +1 -183
  478. package/template/components/data-views/finder-panel-view.tsx +1 -405
  479. package/template/components/data-views/folder-grid-view.tsx +1 -86
  480. package/template/components/data-views/hub-table.tsx +1 -0
  481. package/template/components/data-views/index.ts +42 -1
  482. package/template/components/data-views/list-page-board-card.tsx +1 -192
  483. package/template/components/data-views/list-page-board-template.tsx +1 -122
  484. package/template/components/data-views/list-page-connected-view-body.tsx +1 -0
  485. package/template/components/data-views/list-page-split-details-placeholder.tsx +1 -39
  486. package/template/components/data-views/list-page-split-hub-chrome.tsx +1 -60
  487. package/template/components/data-views/list-page-split-hub-tokens.ts +1 -16
  488. package/template/components/data-views/list-page-tree-column-header.tsx +1 -31
  489. package/template/components/data-views/list-page-tree-panel-shell.tsx +1 -91
  490. package/template/components/data-views/list-page-view-frame.tsx +5 -53
  491. package/template/components/data-views/os-folder-glyph.tsx +1 -129
  492. package/template/components/data-views/outline-tree-menu.tsx +1 -157
  493. package/template/components/export-drawer.test.tsx +71 -0
  494. package/template/components/export-drawer.tsx +1 -375
  495. package/template/components/exxat-product-logo.tsx +5 -5
  496. package/template/components/hub-tree-panel-view.tsx +2 -2
  497. package/template/components/invite-collaborators-drawer.tsx +3 -3
  498. package/template/components/key-metrics-ask-leo-bridge.tsx +40 -0
  499. package/template/components/key-metrics.tsx +1 -1063
  500. package/template/components/leo-insight-indicator.tsx +2 -2
  501. package/template/components/new-placement-back-btn.tsx +1 -1
  502. package/template/components/new-placement-form.tsx +63 -189
  503. package/template/components/new-question-composer.tsx +432 -402
  504. package/template/components/onboarding/index.ts +9 -0
  505. package/template/components/onboarding/onboarding-01.tsx +1 -1
  506. package/template/components/onboarding/onboarding-02.tsx +1 -1
  507. package/template/components/onboarding/onboarding-03.tsx +1 -1
  508. package/template/components/onboarding/onboarding-04.tsx +1 -1
  509. package/template/components/page-header.tsx +8 -226
  510. package/template/components/placement-board-card.tsx +71 -83
  511. package/template/components/placements-board-view.tsx +3 -10
  512. package/template/components/placements-client.tsx +10 -42
  513. package/template/components/placements-list-view.tsx +22 -69
  514. package/template/components/placements-table-columns.tsx +8 -438
  515. package/template/components/placements-table.tsx +588 -1296
  516. package/template/components/product-switcher.tsx +1 -1
  517. package/template/components/product-wordmark.tsx +2 -1
  518. package/template/components/question-bank-client.tsx +4 -1
  519. package/template/components/question-bank-hub-client.tsx +1 -1
  520. package/template/components/question-bank-new-folder-sheet.tsx +2 -2
  521. package/template/components/question-bank-secondary-nav.tsx +3 -3
  522. package/template/components/question-bank-table.tsx +294 -526
  523. package/template/components/rotations-empty-state.tsx +1 -1
  524. package/template/components/rotations-panel-activator.tsx +1 -1
  525. package/template/components/settings-appearance-card.tsx +1 -1
  526. package/template/components/{app-sidebar-dynamic.tsx → sidebar/app-sidebar-dynamic.tsx} +1 -1
  527. package/template/components/{app-sidebar.tsx → sidebar/app-sidebar.tsx} +4 -4
  528. package/template/components/sidebar/index.ts +16 -0
  529. package/template/components/{secondary-nav.tsx → sidebar/secondary-nav.tsx} +2 -2
  530. package/template/components/{secondary-panel.tsx → sidebar/secondary-panel.tsx} +6 -3
  531. package/template/components/{sidebar-auto-collapse.tsx → sidebar/sidebar-auto-collapse.tsx} +6 -2
  532. package/template/components/{sidebar-shell.tsx → sidebar/sidebar-shell.tsx} +1 -1
  533. package/template/components/site-header.tsx +1 -1
  534. package/template/components/{sites-all-client.tsx → sites-client.tsx} +1 -1
  535. package/template/components/sites-table.tsx +124 -257
  536. package/template/components/table-properties/column-row.tsx +1 -90
  537. package/template/components/table-properties/draggable-list.ts +1 -49
  538. package/template/components/table-properties/drawer-button.tsx +1 -249
  539. package/template/components/table-properties/drawer.tsx +1 -1105
  540. package/template/components/table-properties/filter-card.tsx +1 -251
  541. package/template/components/table-properties/sort-card.tsx +1 -59
  542. package/template/components/table-properties/types.ts +28 -71
  543. package/template/components/team-table.tsx +242 -382
  544. package/template/components/templates/dedicated-search-landing-template.tsx +1 -124
  545. package/template/components/templates/dedicated-search-results-template.tsx +1 -19
  546. package/template/components/templates/list-page.tsx +1 -584
  547. package/template/components/templates/nested-secondary-panel-shell.tsx +1 -62
  548. package/template/components/templates/new-focus-template.tsx +659 -0
  549. package/template/components/templates/secondary-panel-hub-template.tsx +1 -1
  550. package/template/components/ui/accordion.tsx +1 -0
  551. package/template/components/ui/alert-dialog.tsx +1 -0
  552. package/template/components/ui/context-menu.tsx +1 -0
  553. package/template/components/ui/dot-pattern.tsx +1 -183
  554. package/template/components/ui/hover-card.tsx +1 -0
  555. package/template/components/ui/resizable.tsx +1 -68
  556. package/template/components/ui/scroll-area.tsx +1 -0
  557. package/template/components/ui/slider.tsx +1 -0
  558. package/template/docs/blueprints/README.md +86 -0
  559. package/template/docs/blueprints/_template.md +91 -0
  560. package/template/docs/blueprints/board-card.md +123 -0
  561. package/template/docs/blueprints/data-table.md +139 -0
  562. package/template/docs/blueprints/key-metrics.md +128 -0
  563. package/template/docs/blueprints/list-page-template.md +123 -0
  564. package/template/docs/blueprints/page-header.md +130 -0
  565. package/template/docs/command-menu-pattern.md +1 -1
  566. package/template/docs/component-selection-guide.md +224 -0
  567. package/template/docs/components-audit-2026-05.md +158 -0
  568. package/template/docs/data-views-pattern.md +14 -14
  569. package/template/docs/migrations/0001-brand-deep-alias-stabilization.md +95 -0
  570. package/template/docs/migrations/0002-exxat-token-namespace.md +154 -0
  571. package/template/docs/migrations/0003-globals-css-canonical.md +110 -0
  572. package/template/docs/migrations/README.md +100 -0
  573. package/template/docs/migrations/_template.md +64 -0
  574. package/template/docs/token-taxonomy.md +416 -0
  575. package/template/eslint.config.mjs +27 -0
  576. package/template/hooks/use-secondary-panel-hub-nav.ts +1 -1
  577. package/template/lib/command-menu-config.ts +0 -1
  578. package/template/lib/compliance-supported-views.ts +10 -0
  579. package/template/lib/conditional-rule-match.ts +6 -97
  580. package/template/lib/data-list-display-options.ts +1 -35
  581. package/template/lib/data-list-view-registry.ts +1 -0
  582. package/template/lib/data-list-view-surface.ts +1 -69
  583. package/template/lib/data-list-view.ts +1 -38
  584. package/template/lib/dev-log.ts +1 -8
  585. package/template/lib/editable-target.ts +1 -10
  586. package/template/lib/hub-connected-view-renderers.ts +58 -0
  587. package/template/lib/list-hub-supported-views.ts +10 -0
  588. package/template/lib/list-page-table-properties.ts +1 -52
  589. package/template/lib/mock/navigation.tsx +0 -8
  590. package/template/lib/mock/placements.ts +0 -7
  591. package/template/lib/placement-board-card-layout.ts +41 -41
  592. package/template/lib/placements-supported-views.ts +12 -0
  593. package/template/lib/question-bank-supported-views.ts +12 -0
  594. package/template/lib/raf-throttle.ts +1 -45
  595. package/template/lib/row-height.ts +4 -10
  596. package/template/lib/sidebar-state-cookie.ts +11 -2
  597. package/template/lib/sites-supported-views.ts +10 -0
  598. package/template/lib/team-supported-views.ts +10 -0
  599. package/template/package.json +1 -0
  600. package/template/tests/setup.ts +25 -0
  601. package/src/theme.css +0 -1132
  602. package/template/app/(app)/data-list/[id]/page.tsx +0 -44
  603. package/template/app/(app)/data-list/new/page.tsx +0 -34
  604. package/template/app/(app)/data-list/page.tsx +0 -10
  605. package/template/components/compliance-list-view.tsx +0 -54
  606. package/template/components/dashboard-onboarding-gallery.tsx +0 -13
  607. package/template/components/dashboard-onboarding.tsx +0 -21
  608. package/template/components/question-bank-list-view.tsx +0 -53
  609. package/template/components/section-cards.tsx +0 -106
  610. package/template/components/sites-list-view.tsx +0 -42
  611. package/template/components/team-list-view.tsx +0 -59
  612. package/template/lib/placement-lifecycle.ts +0 -5
  613. /package/template/components/{getting-started.tsx → onboarding/getting-started.tsx} +0 -0
  614. /package/template/components/{nav-documents.tsx → sidebar/nav-documents.tsx} +0 -0
  615. /package/template/components/{nav-main.tsx → sidebar/nav-main.tsx} +0 -0
  616. /package/template/components/{nav-secondary.tsx → sidebar/nav-secondary.tsx} +0 -0
  617. /package/template/components/{nav-user.tsx → sidebar/nav-user.tsx} +0 -0
  618. /package/template/components/{sidebar-auto-open.tsx → sidebar/sidebar-auto-open.tsx} +0 -0
@@ -1,29 +1,30 @@
1
1
  "use client"
2
2
 
3
3
  /**
4
- * PlacementsTable — placements hub composition on top of the generic
5
- * `DataTable`. Owns: placement-specific column defs, board column grouping,
6
- * KPI dashboards, the "open table properties" imperative handle, and the
7
- * Properties drawer + multi-view composition (table / list / board / …).
4
+ * PlacementsTable — placements hub composed on top of the centralized `<HubTable>`. Owns:
5
+ * placement-specific column defs / cells, board/list/folder/tree/panel/dashboard renderers,
6
+ * pagination chrome wrapping the table + list views, and the dashboard layout state.
8
7
  *
9
- * NOTE: this is hub composition, NOT a parallel table primitive. Every hub
10
- * has its own `*-table.tsx` of the same shape (`team-table.tsx`,
11
- * `compliance-table.tsx`, …); all of them render `<DataTable>` from
12
- * `@/components/data-table`.
8
+ * Single dataset rule: `HubTable` runs one `useTableState(rows, columns, …)`. Every renderer
9
+ * (board, list, folder, tree, panel, dashboard) reads `state.rows`/`state.pagedRows` the same
10
+ * filtered/sorted/paged bag as the grid.
13
11
  *
14
- * View tabs drive `view` (table | list | board | …). `lifecycleTabId` selects which **demo row
15
- * segment** (columns + filtered rows) to use keep in sync with each tab's `filterId`, or pass
16
- * `"all"` for tabs that only change layout.
12
+ * View tabs drive `view` (table | list | board | …). One canonical column set + row bag —
13
+ * lifecycle segmentation has been removed; every tab sees the same placements.
17
14
  */
18
15
 
19
16
  import * as React from "react"
20
17
  import dynamic from "next/dynamic"
18
+ import { useRouter } from "next/navigation"
21
19
  import { cn } from "@/lib/utils"
22
20
  import { mailtoHref } from "@/lib/mailto"
23
- import { useRouter } from "next/navigation"
24
21
  import { Button } from "@/components/ui/button"
25
22
  import { Tip } from "@/components/ui/tip"
26
23
  import { Skeleton } from "@/components/ui/skeleton"
24
+ import { AvatarInitials } from "@/components/ui/avatar"
25
+ import { CoachMark } from "@/components/ui/coach-mark"
26
+ import { useCoachMark } from "@/hooks/use-coach-mark"
27
+ import { DASHBOARD_CUSTOMIZE_COACH_STEPS } from "@/lib/dashboard-customize-coach-mark"
27
28
  import { KEY_METRICS_KPI_COUNT_DEFAULT } from "@/lib/dashboard-layout-merge"
28
29
  import {
29
30
  ALL_DASHBOARD_CARDS,
@@ -36,38 +37,48 @@ import {
36
37
  type ChartType,
37
38
  type DashboardLayout,
38
39
  } from "@/lib/data-view-dashboard-placements-layout"
39
- import { CoachMark } from "@/components/ui/coach-mark"
40
- import { useCoachMark } from "@/hooks/use-coach-mark"
41
- import { DASHBOARD_CUSTOMIZE_COACH_STEPS } from "@/lib/dashboard-customize-coach-mark"
42
40
  import { PlacementsBoardView, type PlacementsBoardColumnMenu } from "@/components/placements-board-view"
43
- import { PlacementsListView } from "@/components/placements-list-view"
44
- import { FolderGridView, ListPageTreePanelShell } from "@/components/data-views"
41
+ import {
42
+ PlacementListRowContent,
43
+ PLACEMENT_LIST_ESTIMATE_ROW_PX,
44
+ PLACEMENT_LIST_VIRTUAL_ROWS_THRESHOLD,
45
+ } from "@/components/placements-list-view"
46
+ import {
47
+ FolderGridView,
48
+ ListPageTreePanelShell,
49
+ HubTable,
50
+ type HubTableHandle,
51
+ type HubTableRenderers,
52
+ type HubTableRendererArgs,
53
+ } from "@/components/data-views"
54
+ import { DataRowList } from "@/components/data-views/data-row-list"
45
55
  import { ListPageSplitHubChrome } from "@/components/data-views/list-page-split-hub-chrome"
46
56
  import { ListPageTreeColumnHeader } from "@/components/data-views/list-page-tree-column-header"
47
57
  import { FinderPanelView, type FinderGroup } from "@/components/data-views/finder-panel-view"
48
58
  import { ListPageSplitDetailsPlaceholder } from "@/components/data-views/list-page-split-details-placeholder"
49
- import { AvatarInitials } from "@/components/ui/avatar"
50
59
  import { getConditionalRowBackground } from "@/lib/conditional-rule-match"
51
60
  import { isBoardFieldActive } from "@/lib/placement-board-card-layout"
52
- import type { BoardCardLifecycleTabId } from "@/lib/placement-board-card-layout"
53
61
  import { TablePropertiesDrawerButton } from "@/components/table-properties"
54
- import type { FilterFieldDef } from "@/components/table-properties/types"
55
62
  import type { DataListViewType } from "@/lib/data-list-view"
56
63
  import {
57
64
  DEFAULT_DATA_LIST_DISPLAY_OPTIONS,
58
65
  type DataListDisplayOptions,
59
66
  } from "@/lib/data-list-display-options"
60
- import type { OpenTablePropertiesHandle } from "@/lib/list-page-table-properties"
61
67
  import { StatusBadge } from "@/components/placements-table-cells"
62
- import { columnsToFilterFields } from "@/components/placements-table-columns"
63
68
  import { DataTable, DataTableToolbar } from "@/components/data-table"
64
69
  import { CountSyncer, PaginationBar } from "@/components/data-table/pagination"
65
- import type { DataTableExtendedProps } from "@/components/data-table"
66
70
  import type { ColumnDef, ConditionalRule } from "@/components/data-table/types"
67
71
  import { useTableState } from "@/components/data-table/use-table-state"
68
- import { placementsForPhase, type Placement, type Status } from "@/lib/mock/placements"
69
- import type { PlacementLifecycleTabId } from "@/lib/placement-lifecycle"
72
+ import { ALL_PLACEMENTS, type Placement, type Status } from "@/lib/mock/placements"
73
+ import {
74
+ getPlacementColumns,
75
+ PLACEMENT_DRAWER_LABEL,
76
+ PLACEMENT_EMPTY_COPY,
77
+ } from "@/components/placements-table-columns"
70
78
  import { placementKpiInsightFromRows, placementKpiMetricsFromRows } from "@/lib/mock/placements-kpi"
79
+ import { PLACEMENTS_SUPPORTED_VIEWS } from "@/lib/placements-supported-views"
80
+
81
+ // ─── Dynamic dashboard charts section (heavy; loaded only on dashboard tab) ──
71
82
 
72
83
  const PlacementsDashboardChartsSection = dynamic(
73
84
  () =>
@@ -86,463 +97,27 @@ const PlacementsDashboardChartsSection = dynamic(
86
97
  },
87
98
  )
88
99
 
89
- function DataListBoardShell({
90
- state,
91
- openDrawerRef,
92
- tableData,
93
- columns,
94
- lifecycleTabId,
95
- view,
96
- onViewChange,
97
- pagination,
98
- onPaginationChange,
99
- conditionalRules,
100
- onAddConditionalRule,
101
- onRemoveConditionalRule,
102
- onUpdateConditionalRule,
103
- filterFields,
104
- lifecycleDrawerLabel,
105
- fieldDefinitionsForDrawer,
106
- resolveColumnLabel,
107
- renderFilterOptionValue,
108
- displayOptions,
109
- onDisplayOptionsChange,
110
- }: {
111
- state: ReturnType<typeof useTableState<Placement>>
112
- openDrawerRef: React.MutableRefObject<() => void>
113
- tableData: Placement[]
114
- columns: ColumnDef<Placement>[]
115
- lifecycleTabId: PlacementLifecycleTabId
116
- view: DataListViewType
117
- onViewChange?: (view: DataListViewType) => void
118
- pagination: boolean
119
- onPaginationChange: (v: boolean) => void
120
- conditionalRules: ConditionalRule[]
121
- onAddConditionalRule: (rule: Omit<ConditionalRule, "id">) => void
122
- onRemoveConditionalRule: (id: string) => void
123
- onUpdateConditionalRule: (id: string, patch: Partial<ConditionalRule>) => void
124
- filterFields: FilterFieldDef[]
125
- lifecycleDrawerLabel: string
126
- fieldDefinitionsForDrawer: { key: string; label: string; sortable?: boolean }[]
127
- resolveColumnLabel: (key: string) => string
128
- renderFilterOptionValue: (fieldKey: string, value: string) => React.ReactNode
129
- displayOptions: DataListDisplayOptions
130
- onDisplayOptionsChange: (patch: Partial<DataListDisplayOptions>) => void
131
- }) {
132
- // Store the "open properties drawer" callback on a stable ref so the parent
133
- // imperative handle can invoke it without re-rendering the whole table.
134
- // `state` is freshly returned each render by useTableState; only the React
135
- // setter is stable and needed here.
136
- React.useEffect(() => {
137
- openDrawerRef.current = () => state.setSheetOpen(true)
138
- // eslint-disable-next-line react-hooks/exhaustive-deps
139
- }, [openDrawerRef, state.setSheetOpen])
140
-
141
- const boardColumnMenu: PlacementsBoardColumnMenu = React.useMemo(
142
- () => ({
143
- filterableColumns: columns.filter(c => c.filter).map(c => ({ key: c.key, label: c.label })),
144
- sortableColumns: columns.filter(c => c.sortable && c.sortKey).map(c => ({ key: c.key, label: c.label })),
145
- groupableColumns: columns.filter(c => c.key !== "select" && c.key !== "actions").map(c => ({ key: c.key, label: c.label })),
146
- groupBy: state.groupBy,
147
- onAddFilter: state.addFilter,
148
- onSortByField: (fieldKey, direction) => {
149
- state.setSortRules(prev => {
150
- const filtered = prev.filter(r => r.fieldKey !== fieldKey)
151
- return [{ id: `sort-${Date.now()}`, fieldKey, direction }, ...filtered]
152
- })
153
- },
154
- onToggleGroupBy: (fieldKey: string) => {
155
- state.setGroupBy(prev => (prev === fieldKey ? null : fieldKey))
156
- },
157
- onOpenProperties: () => state.setSheetOpen(true),
158
- }),
159
- [columns, state],
160
- )
161
-
162
- return (
163
- <>
164
- <DataTableToolbar
165
- state={state}
166
- columns={columns}
167
- searchable
168
- renderFilterOptionValue={renderFilterOptionValue}
169
- searchAriaLabel="Search rows"
170
- toolbarSlot={(s) => (
171
- <TablePropertiesDrawerButton
172
- state={s}
173
- totalRows={tableData.length}
174
- pagination={pagination}
175
- onPaginationChange={onPaginationChange}
176
- conditionalRules={conditionalRules}
177
- onAddConditionalRule={onAddConditionalRule}
178
- onRemoveConditionalRule={onRemoveConditionalRule}
179
- onUpdateConditionalRule={onUpdateConditionalRule}
180
- filterFields={filterFields}
181
- currentView={view}
182
- onViewChange={onViewChange}
183
- lifecycleTabLabel={lifecycleDrawerLabel}
184
- fieldDefinitions={fieldDefinitionsForDrawer}
185
- resolveColumnLabel={resolveColumnLabel}
186
- displayOptions={displayOptions}
187
- onDisplayOptionsChange={onDisplayOptionsChange}
188
- renderFilterOptionValue={renderFilterOptionValue}
189
- />
190
- )}
191
- />
192
- <PlacementsBoardView
193
- placements={state.rows as Placement[]}
194
- lifecycleTabId={lifecycleTabId}
195
- boardColumnMenu={boardColumnMenu}
196
- boardDisplay={{
197
- lineCount: displayOptions.boardLineCount,
198
- showColumnLabels: displayOptions.showColumnLabels,
199
- showColumnCounts: displayOptions.showBoardColumnCounts,
200
- newCardAbove: displayOptions.boardNewCardAbove,
201
- }}
202
- hiddenColKeys={state.hiddenCols}
203
- conditionalRules={conditionalRules}
204
- boardColumns={state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")}
205
- />
206
- </>
207
- )
208
- }
209
-
210
- /** List / row view: shared table state + toolbar + full-width rows */
211
- function DataListListShell({
212
- state,
213
- openDrawerRef,
214
- tableData,
215
- columns,
216
- lifecycleTabId,
217
- view,
218
- onViewChange,
219
- pagination,
220
- onPaginationChange,
221
- conditionalRules,
222
- onAddConditionalRule,
223
- onRemoveConditionalRule,
224
- onUpdateConditionalRule,
225
- filterFields,
226
- lifecycleDrawerLabel,
227
- fieldDefinitionsForDrawer,
228
- resolveColumnLabel,
229
- renderFilterOptionValue,
230
- displayOptions,
231
- onDisplayOptionsChange,
232
- listRows,
233
- emptyTableCopy,
234
- }: {
235
- state: ReturnType<typeof useTableState<Placement>>
236
- openDrawerRef: React.MutableRefObject<() => void>
237
- tableData: Placement[]
238
- columns: ColumnDef<Placement>[]
239
- lifecycleTabId: PlacementLifecycleTabId
240
- view: DataListViewType
241
- onViewChange?: (view: DataListViewType) => void
242
- pagination: boolean
243
- onPaginationChange: (v: boolean) => void
244
- conditionalRules: ConditionalRule[]
245
- onAddConditionalRule: (rule: Omit<ConditionalRule, "id">) => void
246
- onRemoveConditionalRule: (id: string) => void
247
- onUpdateConditionalRule: (id: string, patch: Partial<ConditionalRule>) => void
248
- filterFields: FilterFieldDef[]
249
- lifecycleDrawerLabel: string
250
- fieldDefinitionsForDrawer: { key: string; label: string; sortable?: boolean }[]
251
- resolveColumnLabel: (key: string) => string
252
- renderFilterOptionValue: (fieldKey: string, value: string) => React.ReactNode
253
- displayOptions: DataListDisplayOptions
254
- onDisplayOptionsChange: (patch: Partial<DataListDisplayOptions>) => void
255
- listRows: Placement[]
256
- emptyTableCopy: string
257
- }) {
258
- // Stable "open properties drawer" callback ref — see top of this file.
259
- React.useEffect(() => {
260
- openDrawerRef.current = () => state.setSheetOpen(true)
261
- // eslint-disable-next-line react-hooks/exhaustive-deps
262
- }, [openDrawerRef, state.setSheetOpen])
263
-
264
- return (
265
- <>
266
- <DataTableToolbar
267
- state={state}
268
- columns={columns}
269
- searchable
270
- renderFilterOptionValue={renderFilterOptionValue}
271
- searchAriaLabel="Search rows"
272
- toolbarSlot={s => (
273
- <TablePropertiesDrawerButton
274
- state={s}
275
- totalRows={tableData.length}
276
- pagination={pagination}
277
- onPaginationChange={onPaginationChange}
278
- conditionalRules={conditionalRules}
279
- onAddConditionalRule={onAddConditionalRule}
280
- onRemoveConditionalRule={onRemoveConditionalRule}
281
- onUpdateConditionalRule={onUpdateConditionalRule}
282
- filterFields={filterFields}
283
- currentView={view}
284
- onViewChange={onViewChange}
285
- lifecycleTabLabel={lifecycleDrawerLabel}
286
- fieldDefinitions={fieldDefinitionsForDrawer}
287
- resolveColumnLabel={resolveColumnLabel}
288
- displayOptions={displayOptions}
289
- onDisplayOptionsChange={onDisplayOptionsChange}
290
- renderFilterOptionValue={renderFilterOptionValue}
291
- />
292
- )}
293
- />
294
- <PlacementsListView
295
- rows={listRows}
296
- lifecycleTabId={lifecycleTabId}
297
- hiddenColKeys={state.hiddenCols}
298
- boardColumns={state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")}
299
- conditionalRules={conditionalRules}
300
- emptyCopy={emptyTableCopy}
301
- />
302
- </>
303
- )
304
- }
305
-
306
- /** Dashboard view tab: same toolbar + properties as list/board; KPIs from filtered rows. */
307
- function DataListDashboardShell({
308
- state,
309
- openDrawerRef,
310
- tableData,
311
- columns,
312
- view,
313
- onViewChange,
314
- pagination,
315
- onPaginationChange,
316
- conditionalRules,
317
- onAddConditionalRule,
318
- onRemoveConditionalRule,
319
- onUpdateConditionalRule,
320
- filterFields,
321
- lifecycleDrawerLabel,
322
- fieldDefinitionsForDrawer,
323
- resolveColumnLabel,
324
- renderFilterOptionValue,
325
- displayOptions,
326
- onDisplayOptionsChange,
327
- }: {
328
- state: ReturnType<typeof useTableState<Placement>>
329
- openDrawerRef: React.MutableRefObject<() => void>
330
- tableData: Placement[]
331
- columns: ColumnDef<Placement>[]
332
- view: DataListViewType
333
- onViewChange?: (view: DataListViewType) => void
334
- pagination: boolean
335
- onPaginationChange: (v: boolean) => void
336
- conditionalRules: ConditionalRule[]
337
- onAddConditionalRule: (rule: Omit<ConditionalRule, "id">) => void
338
- onRemoveConditionalRule: (id: string) => void
339
- onUpdateConditionalRule: (id: string, patch: Partial<ConditionalRule>) => void
340
- filterFields: FilterFieldDef[]
341
- lifecycleDrawerLabel: string
342
- fieldDefinitionsForDrawer: { key: string; label: string; sortable?: boolean }[]
343
- resolveColumnLabel: (key: string) => string
344
- renderFilterOptionValue: (fieldKey: string, value: string) => React.ReactNode
345
- displayOptions: DataListDisplayOptions
346
- onDisplayOptionsChange: (patch: Partial<DataListDisplayOptions>) => void
347
- }) {
348
- // Stable "open properties drawer" callback ref — see top of this file.
349
- React.useEffect(() => {
350
- openDrawerRef.current = () => state.setSheetOpen(true)
351
- // eslint-disable-next-line react-hooks/exhaustive-deps
352
- }, [openDrawerRef, state.setSheetOpen])
353
-
354
- const dashboardKpi = React.useMemo(
355
- () => ({
356
- metrics: placementKpiMetricsFromRows(state.rows as Placement[]),
357
- insight: placementKpiInsightFromRows(state.rows as Placement[]),
358
- }),
359
- [state.rows],
360
- )
361
-
362
- /* Dashboard card layout — persisted to localStorage */
363
- const [visibleCards, setVisibleCards] = React.useState<string[]>(DEFAULT_VISIBLE_CARDS)
364
- const [cardOrder, setCardOrder] = React.useState<string[]>(ALL_DASHBOARD_CARDS.map(c => c.id))
365
- const [cardSpans, setCardSpans] = React.useState<Record<string, 1 | 2>>(() => ({ ...DEFAULT_SPANS }))
366
- const [cardChartTypes, setCardChartTypes] = React.useState<Record<string, ChartType>>(() => ({ ...DEFAULT_CHART_TYPES }))
367
- const [keyMetricsKpiCount, setKeyMetricsKpiCount] = React.useState<number>(KEY_METRICS_KPI_COUNT_DEFAULT)
368
- const [dashboardLayoutEdit, setDashboardLayoutEdit] = React.useState(false)
369
- const dashboardLayoutHydrated = React.useRef(false)
370
- const dashboardLayoutEditBaselineRef = React.useRef<DashboardLayout | null>(null)
371
-
372
- React.useEffect(() => {
373
- const saved = loadDashboardLayout()
374
- const m = mergeDashboardLayout(saved)
375
- setVisibleCards(m.visible)
376
- setCardOrder(m.order)
377
- setCardSpans(m.spans ?? { ...DEFAULT_SPANS })
378
- setCardChartTypes(m.chartTypes ?? { ...DEFAULT_CHART_TYPES })
379
- setKeyMetricsKpiCount(m.keyMetricsKpiCount ?? KEY_METRICS_KPI_COUNT_DEFAULT)
380
- dashboardLayoutHydrated.current = true
381
- }, [])
382
-
383
- React.useEffect(() => {
384
- if (!dashboardLayoutHydrated.current) return
385
- saveDashboardLayout({
386
- visible: visibleCards,
387
- order: cardOrder,
388
- spans: cardSpans,
389
- chartTypes: cardChartTypes,
390
- keyMetricsKpiCount,
391
- })
392
- }, [visibleCards, cardOrder, cardSpans, cardChartTypes, keyMetricsKpiCount])
393
-
394
- const handleVisibleChange = React.useCallback((v: string[]) => {
395
- setVisibleCards(v)
396
- }, [])
397
-
398
- const handleOrderChange = React.useCallback((o: string[]) => {
399
- setCardOrder(o)
400
- }, [])
401
-
402
- const handleSpanChange = React.useCallback((id: string, span: 1 | 2) => {
403
- setCardSpans(prev => ({ ...prev, [id]: span }))
404
- }, [])
405
-
406
- const handleChartTypeChange = React.useCallback((id: string, t: ChartType) => {
407
- setCardChartTypes(prev => ({ ...prev, [id]: t }))
408
- }, [])
409
-
410
- const handleResetDashboardLayout = React.useCallback(() => {
411
- setVisibleCards(ALL_DASHBOARD_CARDS.map(c => c.id))
412
- setCardOrder(ALL_DASHBOARD_CARDS.map(c => c.id))
413
- setCardSpans({ ...DEFAULT_SPANS })
414
- setCardChartTypes({ ...DEFAULT_CHART_TYPES })
415
- setKeyMetricsKpiCount(KEY_METRICS_KPI_COUNT_DEFAULT)
416
- }, [])
417
-
418
- const handleDashboardLayoutEditStart = React.useCallback(() => {
419
- dashboardLayoutEditBaselineRef.current = {
420
- visible: [...visibleCards],
421
- order: [...cardOrder],
422
- spans: { ...cardSpans },
423
- chartTypes: { ...cardChartTypes },
424
- keyMetricsKpiCount,
425
- }
426
- setDashboardLayoutEdit(true)
427
- }, [visibleCards, cardOrder, cardSpans, cardChartTypes, keyMetricsKpiCount])
428
-
429
- const handleDashboardLayoutEditDone = React.useCallback(() => {
430
- setDashboardLayoutEdit(false)
431
- }, [])
432
-
433
- const handleDashboardLayoutEditCancel = React.useCallback(() => {
434
- const b = dashboardLayoutEditBaselineRef.current
435
- if (b) {
436
- setVisibleCards(b.visible)
437
- setCardOrder(b.order)
438
- setCardSpans(b.spans ?? { ...DEFAULT_SPANS })
439
- setCardChartTypes(b.chartTypes ?? { ...DEFAULT_CHART_TYPES })
440
- setKeyMetricsKpiCount(b.keyMetricsKpiCount ?? KEY_METRICS_KPI_COUNT_DEFAULT)
441
- }
442
- setDashboardLayoutEdit(false)
443
- }, [])
444
-
445
- const dashboardCustomizeCoach = useCoachMark({
446
- flowId: "data-list-dashboard-customize",
447
- steps: DASHBOARD_CUSTOMIZE_COACH_STEPS,
448
- delay: 700,
449
- dependsOnDismissedFlowId: "data-list-views-tour",
450
- })
451
-
452
- return (
453
- <>
454
- <CoachMark state={dashboardCustomizeCoach} />
455
- {!dashboardLayoutEdit ? (
456
- <DataTableToolbar
457
- state={state}
458
- columns={columns}
459
- searchable={displayOptions.showToolbarSearch}
460
- renderFilterOptionValue={renderFilterOptionValue}
461
- searchAriaLabel="Search rows"
462
- toolbarSlot={s => (
463
- <TablePropertiesDrawerButton
464
- state={s}
465
- totalRows={tableData.length}
466
- pagination={pagination}
467
- onPaginationChange={onPaginationChange}
468
- conditionalRules={conditionalRules}
469
- onAddConditionalRule={onAddConditionalRule}
470
- onRemoveConditionalRule={onRemoveConditionalRule}
471
- onUpdateConditionalRule={onUpdateConditionalRule}
472
- filterFields={filterFields}
473
- currentView={view}
474
- onViewChange={onViewChange}
475
- lifecycleTabLabel={lifecycleDrawerLabel}
476
- fieldDefinitions={fieldDefinitionsForDrawer}
477
- resolveColumnLabel={resolveColumnLabel}
478
- displayOptions={displayOptions}
479
- onDisplayOptionsChange={onDisplayOptionsChange}
480
- renderFilterOptionValue={renderFilterOptionValue}
481
- extraActions={
482
- <Tip side="bottom" label="Edit dashboard layout on canvas">
483
- <Button
484
- type="button"
485
- variant="ghost"
486
- size="icon-sm"
487
- aria-label="Edit dashboard layout"
488
- onClick={handleDashboardLayoutEditStart}
489
- className="text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover"
490
- >
491
- <i className="fa-light fa-pen-ruler text-[13px]" aria-hidden="true" />
492
- </Button>
493
- </Tip>
494
- }
495
- />
496
- )}
497
- />
498
- ) : null}
499
-
500
- {/* Contextual placement charts + KPI card (customise on canvas) */}
501
- <PlacementsDashboardChartsSection
502
- placements={state.rows as Placement[]}
503
- keyMetrics={dashboardKpi}
504
- visibleCards={visibleCards}
505
- cardOrder={cardOrder}
506
- cardSpans={cardSpans}
507
- cardChartTypes={cardChartTypes}
508
- keyMetricsKpiCount={keyMetricsKpiCount}
509
- layoutEditMode={dashboardLayoutEdit}
510
- onVisibleChange={handleVisibleChange}
511
- onOrderChange={handleOrderChange}
512
- onSpanChange={handleSpanChange}
513
- onChartTypeChange={handleChartTypeChange}
514
- onKeyMetricsKpiCountChange={setKeyMetricsKpiCount}
515
- onResetLayout={handleResetDashboardLayout}
516
- onLayoutEditDone={handleDashboardLayoutEditDone}
517
- onLayoutEditCancel={handleDashboardLayoutEditCancel}
518
- />
519
- </>
520
- )
521
- }
522
-
523
100
  // ─── Placement-specific tile for FolderGridView ──────────────────────────────
524
101
 
525
102
  function PlacementFolderTile({
526
103
  row,
527
- tab,
528
104
  hiddenColKeys,
529
105
  boardColumns,
530
106
  conditionalRules,
531
107
  onClick,
532
108
  }: {
533
109
  row: Placement
534
- tab: BoardCardLifecycleTabId
535
110
  hiddenColKeys: Set<string>
536
111
  boardColumns: ColumnDef<Placement>[]
537
112
  conditionalRules?: ConditionalRule[]
538
113
  onClick: () => void
539
114
  }) {
540
115
  const ruleBg = getConditionalRowBackground(row, conditionalRules, boardColumns)
541
- const showStudent = isBoardFieldActive("student", tab, hiddenColKeys, boardColumns)
542
- const showStatus = isBoardFieldActive("status", tab, hiddenColKeys, boardColumns)
543
- const showSite = isBoardFieldActive("site", tab, hiddenColKeys, boardColumns)
544
- const showSpec = isBoardFieldActive("specialization", tab, hiddenColKeys, boardColumns)
545
- const showProgram = isBoardFieldActive("program", tab, hiddenColKeys, boardColumns)
116
+ const showStudent = isBoardFieldActive("student", hiddenColKeys, boardColumns)
117
+ const showStatus = isBoardFieldActive("status", hiddenColKeys, boardColumns)
118
+ const showSite = isBoardFieldActive("site", hiddenColKeys, boardColumns)
119
+ const showSpec = isBoardFieldActive("specialization", hiddenColKeys, boardColumns)
120
+ const showProgram = isBoardFieldActive("program", hiddenColKeys, boardColumns)
546
121
  const name = showStudent ? row.student : `Placement ${row.id}`
547
122
 
548
123
  const statusDotClass: Record<Status, string> = {
@@ -574,7 +149,7 @@ function PlacementFolderTile({
574
149
  <div className="flex w-full flex-col gap-0.5">
575
150
  {showSite && (
576
151
  <p className="truncate text-center text-[11px] text-muted-foreground leading-tight">
577
- <i className="fa-light fa-building mr-1" aria-hidden="true" />{row.site}
152
+ <i className="fa-light fa-building me-1" aria-hidden="true" />{row.site}
578
153
  </p>
579
154
  )}
580
155
  {showSpec && <p className="truncate text-center text-[11px] text-muted-foreground leading-tight">{row.specialization}</p>}
@@ -585,351 +160,68 @@ function PlacementFolderTile({
585
160
  )
586
161
  }
587
162
 
163
+ // ─── Placement-specific list row for FinderPanelView ─────────────────────────
588
164
 
589
- // ─── Folder view shell ────────────────────────────────────────────────────────
590
-
591
- /** Folder / icon-grid view shell */
592
- function DataListFolderShell({
593
- state,
594
- openDrawerRef,
595
- tableData,
596
- columns,
597
- lifecycleTabId,
598
- view,
599
- onViewChange,
600
- pagination,
601
- onPaginationChange,
165
+ function PlacementFinderListRow({
166
+ row,
167
+ isSelected,
168
+ hiddenColKeys,
169
+ boardColumns,
602
170
  conditionalRules,
603
- onAddConditionalRule,
604
- onRemoveConditionalRule,
605
- onUpdateConditionalRule,
606
- filterFields,
607
- lifecycleDrawerLabel,
608
- fieldDefinitionsForDrawer,
609
- resolveColumnLabel,
610
- renderFilterOptionValue,
611
- displayOptions,
612
- onDisplayOptionsChange,
613
- listRows,
614
- emptyTableCopy,
615
171
  }: {
616
- state: ReturnType<typeof useTableState<Placement>>
617
- openDrawerRef: React.MutableRefObject<() => void>
618
- tableData: Placement[]
619
- columns: ColumnDef<Placement>[]
620
- lifecycleTabId: PlacementLifecycleTabId
621
- view: DataListViewType
622
- onViewChange?: (view: DataListViewType) => void
623
- pagination: boolean
624
- onPaginationChange: (v: boolean) => void
625
- conditionalRules: ConditionalRule[]
626
- onAddConditionalRule: (rule: Omit<ConditionalRule, "id">) => void
627
- onRemoveConditionalRule: (id: string) => void
628
- onUpdateConditionalRule: (id: string, patch: Partial<ConditionalRule>) => void
629
- filterFields: FilterFieldDef[]
630
- lifecycleDrawerLabel: string
631
- fieldDefinitionsForDrawer: { key: string; label: string; sortable?: boolean }[]
632
- resolveColumnLabel: (key: string) => string
633
- renderFilterOptionValue: (fieldKey: string, value: string) => React.ReactNode
634
- displayOptions: DataListDisplayOptions
635
- onDisplayOptionsChange: (patch: Partial<DataListDisplayOptions>) => void
636
- listRows: Placement[]
637
- emptyTableCopy: string
172
+ row: Placement
173
+ isSelected: boolean
174
+ hiddenColKeys: Set<string>
175
+ boardColumns: ColumnDef<Placement>[]
176
+ conditionalRules?: ConditionalRule[]
638
177
  }) {
639
- const router = useRouter()
640
-
641
- // Stable "open properties drawer" callback ref — see top of this file.
642
- React.useEffect(() => {
643
- openDrawerRef.current = () => state.setSheetOpen(true)
644
- // eslint-disable-next-line react-hooks/exhaustive-deps
645
- }, [openDrawerRef, state.setSheetOpen])
646
-
647
- const boardColumns = state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")
648
-
178
+ const ruleBg = getConditionalRowBackground(row, conditionalRules, boardColumns)
179
+ const showStudent = isBoardFieldActive("student", hiddenColKeys, boardColumns)
180
+ const showSite = isBoardFieldActive("site", hiddenColKeys, boardColumns)
181
+ const name = showStudent ? row.student : `Placement ${row.id}`
649
182
  return (
650
- <>
651
- <DataTableToolbar
652
- state={state}
653
- columns={columns}
654
- searchable
655
- renderFilterOptionValue={renderFilterOptionValue}
656
- searchAriaLabel="Search rows"
657
- toolbarSlot={s => (
658
- <TablePropertiesDrawerButton
659
- state={s}
660
- totalRows={tableData.length}
661
- pagination={pagination}
662
- onPaginationChange={onPaginationChange}
663
- conditionalRules={conditionalRules}
664
- onAddConditionalRule={onAddConditionalRule}
665
- onRemoveConditionalRule={onRemoveConditionalRule}
666
- onUpdateConditionalRule={onUpdateConditionalRule}
667
- filterFields={filterFields}
668
- currentView={view}
669
- onViewChange={onViewChange}
670
- lifecycleTabLabel={lifecycleDrawerLabel}
671
- fieldDefinitions={fieldDefinitionsForDrawer}
672
- resolveColumnLabel={resolveColumnLabel}
673
- displayOptions={displayOptions}
674
- onDisplayOptionsChange={onDisplayOptionsChange}
675
- renderFilterOptionValue={renderFilterOptionValue}
676
- />
183
+ <div
184
+ className={`flex w-full min-w-0 items-center gap-3 transition-colors duration-75 ${
185
+ isSelected ? "bg-transparent text-accent-foreground" : cn("text-foreground", ruleBg)
186
+ }`}
187
+ >
188
+ <AvatarInitials
189
+ initials={row.initials}
190
+ className={cn(
191
+ "size-8 shrink-0 rounded-full text-[11px] font-semibold",
192
+ isSelected ? "ring-2 ring-accent-foreground/35" : "",
677
193
  )}
678
194
  />
679
- <FolderGridView<Placement>
680
- rows={listRows}
681
- getRowId={r => r.id}
682
- ariaLabel="Demo folder view"
683
- emptyContent={<p>{emptyTableCopy}</p>}
684
- renderTile={row => (
685
- <PlacementFolderTile
686
- row={row}
687
- tab={lifecycleTabId as BoardCardLifecycleTabId}
688
- hiddenColKeys={state.hiddenCols}
689
- boardColumns={boardColumns}
690
- conditionalRules={conditionalRules}
691
- onClick={() => router.push(`/data-list/${row.id}`)}
692
- />
195
+ <div className="min-w-0 flex-1">
196
+ <p className={cn("truncate text-[13px] font-medium leading-tight", isSelected ? "text-accent-foreground" : "text-foreground")}>
197
+ {name}
198
+ </p>
199
+ {showSite && (
200
+ <p className={cn("mt-0.5 truncate text-[11px] leading-tight", isSelected ? "text-accent-foreground/80" : "text-muted-foreground")}>
201
+ {row.site}
202
+ </p>
693
203
  )}
694
- />
695
- </>
204
+ </div>
205
+ {!isSelected && <StatusBadge status={row.status} />}
206
+ </div>
696
207
  )
697
208
  }
698
209
 
699
- // ─── Tree / outline + details shell ───────────────────────────────────────────
210
+ // ─── Placement-specific detail pane for FinderPanelView ──────────────────────
700
211
 
701
- function DataListTreeShell({
702
- state,
703
- openDrawerRef,
704
- tableData,
705
- columns,
706
- lifecycleTabId,
707
- view,
708
- onViewChange,
709
- pagination,
710
- onPaginationChange,
711
- conditionalRules,
712
- onAddConditionalRule,
713
- onRemoveConditionalRule,
714
- onUpdateConditionalRule,
715
- filterFields,
716
- lifecycleDrawerLabel,
717
- fieldDefinitionsForDrawer,
718
- resolveColumnLabel,
719
- renderFilterOptionValue,
720
- displayOptions,
721
- onDisplayOptionsChange,
722
- listRows,
723
- emptyTableCopy,
212
+ function PlacementFinderDetail({
213
+ row,
214
+ hiddenColKeys,
215
+ boardColumns,
724
216
  }: {
725
- state: ReturnType<typeof useTableState<Placement>>
726
- openDrawerRef: React.MutableRefObject<() => void>
727
- tableData: Placement[]
728
- columns: ColumnDef<Placement>[]
729
- lifecycleTabId: PlacementLifecycleTabId
730
- view: DataListViewType
731
- onViewChange?: (view: DataListViewType) => void
732
- pagination: boolean
733
- onPaginationChange: (v: boolean) => void
734
- conditionalRules: ConditionalRule[]
735
- onAddConditionalRule: (rule: Omit<ConditionalRule, "id">) => void
736
- onRemoveConditionalRule: (id: string) => void
737
- onUpdateConditionalRule: (id: string, patch: Partial<ConditionalRule>) => void
738
- filterFields: FilterFieldDef[]
739
- lifecycleDrawerLabel: string
740
- fieldDefinitionsForDrawer: { key: string; label: string; sortable?: boolean }[]
741
- resolveColumnLabel: (key: string) => string
742
- renderFilterOptionValue: (fieldKey: string, value: string) => React.ReactNode
743
- displayOptions: DataListDisplayOptions
744
- onDisplayOptionsChange: (patch: Partial<DataListDisplayOptions>) => void
745
- listRows: Placement[]
746
- emptyTableCopy: string
747
- }) {
748
- const boardColumns = state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")
749
- const [selectedId, setSelectedId] = React.useState<number | null>(() => listRows[0]?.id ?? null)
750
-
751
- // Stable "open properties drawer" callback ref — see top of this file.
752
- React.useEffect(() => {
753
- openDrawerRef.current = () => state.setSheetOpen(true)
754
- // eslint-disable-next-line react-hooks/exhaustive-deps
755
- }, [openDrawerRef, state.setSheetOpen])
756
-
757
- React.useEffect(() => {
758
- if (selectedId == null) {
759
- setSelectedId(listRows[0]?.id ?? null)
760
- return
761
- }
762
- if (!listRows.some(r => r.id === selectedId)) {
763
- setSelectedId(listRows[0]?.id ?? null)
764
- }
765
- }, [listRows, selectedId])
766
-
767
- const selected = listRows.find(r => r.id === selectedId) ?? null
768
-
769
- return (
770
- <>
771
- <DataTableToolbar
772
- state={state}
773
- columns={columns}
774
- searchable
775
- renderFilterOptionValue={renderFilterOptionValue}
776
- searchAriaLabel="Search rows"
777
- toolbarSlot={s => (
778
- <TablePropertiesDrawerButton
779
- state={s}
780
- totalRows={tableData.length}
781
- pagination={pagination}
782
- onPaginationChange={onPaginationChange}
783
- conditionalRules={conditionalRules}
784
- onAddConditionalRule={onAddConditionalRule}
785
- onRemoveConditionalRule={onRemoveConditionalRule}
786
- onUpdateConditionalRule={onUpdateConditionalRule}
787
- filterFields={filterFields}
788
- currentView={view}
789
- onViewChange={onViewChange}
790
- lifecycleTabLabel={lifecycleDrawerLabel}
791
- fieldDefinitions={fieldDefinitionsForDrawer}
792
- resolveColumnLabel={resolveColumnLabel}
793
- displayOptions={displayOptions}
794
- onDisplayOptionsChange={onDisplayOptionsChange}
795
- renderFilterOptionValue={renderFilterOptionValue}
796
- />
797
- )}
798
- />
799
- <ListPageTreePanelShell
800
- resizableGroupId={`data-list-tree-${lifecycleTabId}`}
801
- ariaLabel="Record outline and details"
802
- tree={
803
- <div className="flex min-h-0 flex-1 flex-col">
804
- <ListPageTreeColumnHeader title="Records" />
805
- {listRows.length === 0 ? (
806
- <p className="p-3 text-sm text-muted-foreground">{emptyTableCopy}</p>
807
- ) : (
808
- <ul
809
- role="tree"
810
- aria-label="Demo records"
811
- className="min-h-0 flex-1 list-none space-y-0.5 overflow-y-auto py-1"
812
- >
813
- {listRows.map(row => {
814
- const isSel = selectedId === row.id
815
- return (
816
- <li key={row.id} role="none" className="py-0.5">
817
- <button
818
- type="button"
819
- role="treeitem"
820
- aria-selected={isSel}
821
- tabIndex={isSel ? 0 : -1}
822
- onClick={() => setSelectedId(row.id)}
823
- className={cn(
824
- "flex w-full min-h-8 items-center rounded-md px-3 py-2 text-left text-sm transition-colors duration-75",
825
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
826
- isSel
827
- ? "bg-accent font-medium text-accent-foreground"
828
- : "text-foreground hover:bg-muted/50",
829
- )}
830
- >
831
- <span className="min-w-0 truncate">{row.student}</span>
832
- </button>
833
- </li>
834
- )
835
- })}
836
- </ul>
837
- )}
838
- </div>
839
- }
840
- details={
841
- selected ? (
842
- <div className="flex min-h-0 flex-1 flex-col overflow-hidden bg-card">
843
- <ListPageTreeColumnHeader title="Details" />
844
- <div className="min-h-0 flex-1 overflow-y-auto">
845
- <PlacementFinderDetail
846
- row={selected}
847
- tab={lifecycleTabId as BoardCardLifecycleTabId}
848
- hiddenColKeys={state.hiddenCols}
849
- boardColumns={boardColumns}
850
- />
851
- </div>
852
- </div>
853
- ) : (
854
- <ListPageSplitDetailsPlaceholder title="Nothing selected" />
855
- )
856
- }
857
- />
858
- </>
859
- )
860
- }
861
-
862
- // ─── Placement-specific list row for FinderPanelView ─────────────────────────
863
-
864
- function PlacementFinderListRow({
865
- row,
866
- isSelected,
867
- tab,
868
- hiddenColKeys,
869
- boardColumns,
870
- conditionalRules,
871
- }: {
872
- row: Placement
873
- isSelected: boolean
874
- tab: BoardCardLifecycleTabId
875
- hiddenColKeys: Set<string>
876
- boardColumns: ColumnDef<Placement>[]
877
- conditionalRules?: ConditionalRule[]
878
- }) {
879
- const ruleBg = getConditionalRowBackground(row, conditionalRules, boardColumns)
880
- const showStudent = isBoardFieldActive("student", tab, hiddenColKeys, boardColumns)
881
- const showSite = isBoardFieldActive("site", tab, hiddenColKeys, boardColumns)
882
- const name = showStudent ? row.student : `Placement ${row.id}`
883
-
884
- return (
885
- <div
886
- className={`flex w-full min-w-0 items-center gap-3 transition-colors duration-75 ${
887
- isSelected
888
- ? "bg-transparent text-accent-foreground"
889
- : cn("text-foreground", ruleBg)
890
- }`}
891
- >
892
- <AvatarInitials
893
- initials={row.initials}
894
- className={cn(
895
- "size-8 shrink-0 rounded-full text-[11px] font-semibold",
896
- isSelected ? "ring-2 ring-accent-foreground/35" : "",
897
- )}
898
- />
899
- <div className="min-w-0 flex-1">
900
- <p className={cn("truncate text-[13px] font-medium leading-tight", isSelected ? "text-accent-foreground" : "text-foreground")}>
901
- {name}
902
- </p>
903
- {showSite && (
904
- <p className={cn("mt-0.5 truncate text-[11px] leading-tight", isSelected ? "text-accent-foreground/80" : "text-muted-foreground")}>
905
- {row.site}
906
- </p>
907
- )}
908
- </div>
909
- {!isSelected && <StatusBadge status={row.status} />}
910
- </div>
911
- )
912
- }
913
-
914
- // ─── Placement-specific detail pane for FinderPanelView ──────────────────────
915
-
916
- function PlacementFinderDetail({
917
- row,
918
- tab,
919
- hiddenColKeys,
920
- boardColumns,
921
- }: {
922
- row: Placement
923
- tab: BoardCardLifecycleTabId
924
- hiddenColKeys: Set<string>
925
- boardColumns: ColumnDef<Placement>[]
217
+ row: Placement
218
+ hiddenColKeys: Set<string>
219
+ boardColumns: ColumnDef<Placement>[]
926
220
  }) {
927
221
  const router = useRouter()
928
- const show = (key: string) => isBoardFieldActive(key, tab, hiddenColKeys, boardColumns)
929
-
222
+ const show = (key: string) => isBoardFieldActive(key, hiddenColKeys, boardColumns)
930
223
  return (
931
224
  <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
932
- {/* Header */}
933
225
  <div className="flex shrink-0 items-start gap-4 border-b border-border px-5 py-4">
934
226
  <AvatarInitials initials={row.initials} className="size-14 shrink-0 rounded-full text-lg font-semibold" />
935
227
  <div className="min-w-0 flex-1">
@@ -938,16 +230,19 @@ function PlacementFinderDetail({
938
230
  {show("status") && <div className="mt-2"><StatusBadge status={row.status} /></div>}
939
231
  </div>
940
232
  <Tip side="bottom" label="Open full detail page">
941
- <Button type="button" variant="outline" size="sm" className="shrink-0"
233
+ <Button
234
+ type="button"
235
+ variant="outline"
236
+ size="sm"
237
+ className="shrink-0"
942
238
  onClick={() => router.push(`/data-list/${row.id}`)}
943
- aria-label={`Open full detail for ${row.student}`}>
239
+ aria-label={`Open full detail for ${row.student}`}
240
+ >
944
241
  <i className="fa-light fa-arrow-up-right-from-square text-[12px]" aria-hidden="true" />
945
242
  Open
946
243
  </Button>
947
244
  </Tip>
948
245
  </div>
949
-
950
- {/* Fields */}
951
246
  <div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
952
247
  <dl className="grid grid-cols-1 gap-4 sm:grid-cols-2">
953
248
  {show("email") && (
@@ -1008,7 +303,7 @@ function PlacementFinderDetail({
1008
303
  <dd className="text-[13px] text-foreground">{row.duration}</dd>
1009
304
  </div>
1010
305
  )}
1011
- {tab === "ongoing" && (
306
+ {row.placementPhase === "ongoing" && (
1012
307
  <div className="flex flex-col gap-0.5 sm:col-span-2">
1013
308
  <dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
1014
309
  <i className="fa-light fa-chart-line text-[10px]" aria-hidden="true" /> Progress
@@ -1041,12 +336,12 @@ function PlacementFinderDetail({
1041
336
  // ─── Status groups for FinderPanelView ───────────────────────────────────────
1042
337
 
1043
338
  const STATUS_GROUPS: Array<{ id: Status | "all"; label: string; accent: string }> = [
1044
- { id: "all", label: "All", accent: "bg-muted-foreground" },
1045
- { id: "confirmed", label: "Confirmed", accent: "bg-success" },
1046
- { id: "pending", label: "Pending", accent: "bg-warning" },
339
+ { id: "all", label: "All", accent: "bg-muted-foreground" },
340
+ { id: "confirmed", label: "Confirmed", accent: "bg-success" },
341
+ { id: "pending", label: "Pending", accent: "bg-warning" },
1047
342
  { id: "under-review", label: "Under Review", accent: "bg-brand" },
1048
- { id: "rejected", label: "Rejected", accent: "bg-destructive" },
1049
- { id: "completed", label: "Completed", accent: "bg-muted-foreground/50" },
343
+ { id: "rejected", label: "Rejected", accent: "bg-destructive" },
344
+ { id: "completed", label: "Completed", accent: "bg-muted-foreground/50" },
1050
345
  ]
1051
346
 
1052
347
  function buildStatusGroups(rows: Placement[]): FinderGroup[] {
@@ -1058,166 +353,290 @@ function buildStatusGroups(rows: Placement[]): FinderGroup[] {
1058
353
  }))
1059
354
  }
1060
355
 
1061
- // ─── Panel view shell ────────────────────────────────────────────────────────
356
+ // ─── Tree-view body (its own selection state) ────────────────────────────────
357
+
358
+ function PlacementsTreeBody({
359
+ args,
360
+ }: {
361
+ args: HubTableRendererArgs<Placement>
362
+ }) {
363
+ const { state } = args
364
+ const listRows = state.rows as Placement[]
365
+ const boardColumns = state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")
366
+ const [selectedId, setSelectedId] = React.useState<number | null>(() => listRows[0]?.id ?? null)
367
+
368
+ React.useEffect(() => {
369
+ if (selectedId == null) {
370
+ setSelectedId(listRows[0]?.id ?? null)
371
+ return
372
+ }
373
+ if (!listRows.some(r => r.id === selectedId)) {
374
+ setSelectedId(listRows[0]?.id ?? null)
375
+ }
376
+ }, [listRows, selectedId])
377
+
378
+ const selected = listRows.find(r => r.id === selectedId) ?? null
379
+
380
+ return (
381
+ <ListPageTreePanelShell
382
+ resizableGroupId="data-list-tree"
383
+ ariaLabel="Record outline and details"
384
+ tree={
385
+ <div className="flex min-h-0 flex-1 flex-col">
386
+ <ListPageTreeColumnHeader title="Records" />
387
+ {listRows.length === 0 ? (
388
+ <p className="p-3 text-sm text-muted-foreground">{PLACEMENT_EMPTY_COPY}</p>
389
+ ) : (
390
+ <ul
391
+ role="tree"
392
+ aria-label="Demo records"
393
+ className="min-h-0 flex-1 list-none space-y-0.5 overflow-y-auto py-1"
394
+ >
395
+ {listRows.map(row => {
396
+ const isSel = selectedId === row.id
397
+ return (
398
+ <li key={row.id} role="none" className="py-0.5">
399
+ <button
400
+ type="button"
401
+ role="treeitem"
402
+ aria-selected={isSel}
403
+ tabIndex={isSel ? 0 : -1}
404
+ onClick={() => setSelectedId(row.id)}
405
+ className={cn(
406
+ "flex w-full min-h-8 items-center rounded-md px-3 py-2 text-left text-sm transition-colors duration-75",
407
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
408
+ isSel
409
+ ? "bg-accent font-medium text-accent-foreground"
410
+ : "text-foreground hover:bg-muted/50",
411
+ )}
412
+ >
413
+ <span className="min-w-0 truncate">{row.student}</span>
414
+ </button>
415
+ </li>
416
+ )
417
+ })}
418
+ </ul>
419
+ )}
420
+ </div>
421
+ }
422
+ details={
423
+ selected ? (
424
+ <div className="flex min-h-0 flex-1 flex-col overflow-hidden bg-card">
425
+ <ListPageTreeColumnHeader title="Details" />
426
+ <div className="min-h-0 flex-1 overflow-y-auto">
427
+ <PlacementFinderDetail
428
+ row={selected}
429
+ hiddenColKeys={state.hiddenCols}
430
+ boardColumns={boardColumns}
431
+ />
432
+ </div>
433
+ </div>
434
+ ) : (
435
+ <ListPageSplitDetailsPlaceholder title="Nothing selected" />
436
+ )
437
+ }
438
+ />
439
+ )
440
+ }
441
+
442
+ // ─── Dashboard body (its own layout state) ───────────────────────────────────
1062
443
 
1063
- /** Finder-style panel view shell with groups, list, and detail pane */
1064
- function DataListPanelShell({
1065
- state,
1066
- openDrawerRef,
1067
- tableData,
444
+ function PlacementsDashboardBody({
445
+ args,
1068
446
  columns,
1069
- lifecycleTabId,
1070
- view,
1071
- onViewChange,
1072
- pagination,
1073
- onPaginationChange,
1074
- conditionalRules,
1075
- onAddConditionalRule,
1076
- onRemoveConditionalRule,
1077
- onUpdateConditionalRule,
1078
- filterFields,
1079
- lifecycleDrawerLabel,
1080
- fieldDefinitionsForDrawer,
1081
- resolveColumnLabel,
1082
- renderFilterOptionValue,
1083
- displayOptions,
1084
- onDisplayOptionsChange,
1085
- listRows,
1086
- emptyTableCopy,
1087
- panelGroupsBuilder,
1088
- panelRenderListRow,
1089
- panelRenderDetail,
1090
447
  }: {
1091
- state: ReturnType<typeof useTableState<Placement>>
1092
- openDrawerRef: React.MutableRefObject<() => void>
1093
- tableData: Placement[]
448
+ args: HubTableRendererArgs<Placement>
1094
449
  columns: ColumnDef<Placement>[]
1095
- lifecycleTabId: PlacementLifecycleTabId
1096
- view: DataListViewType
1097
- onViewChange?: (view: DataListViewType) => void
1098
- pagination: boolean
1099
- onPaginationChange: (v: boolean) => void
1100
- conditionalRules: ConditionalRule[]
1101
- onAddConditionalRule: (rule: Omit<ConditionalRule, "id">) => void
1102
- onRemoveConditionalRule: (id: string) => void
1103
- onUpdateConditionalRule: (id: string, patch: Partial<ConditionalRule>) => void
1104
- filterFields: FilterFieldDef[]
1105
- lifecycleDrawerLabel: string
1106
- fieldDefinitionsForDrawer: { key: string; label: string; sortable?: boolean }[]
1107
- resolveColumnLabel: (key: string) => string
1108
- renderFilterOptionValue: (fieldKey: string, value: string) => React.ReactNode
1109
- displayOptions: DataListDisplayOptions
1110
- onDisplayOptionsChange: (patch: Partial<DataListDisplayOptions>) => void
1111
- listRows: Placement[]
1112
- emptyTableCopy: string
1113
- panelGroupsBuilder?: (rows: Placement[]) => FinderGroup[]
1114
- panelRenderListRow?: (row: Placement, isSelected: boolean) => React.ReactNode
1115
- panelRenderDetail?: (row: Placement) => React.ReactNode
1116
450
  }) {
1117
- // Stable "open properties drawer" callback ref — see top of this file.
1118
- React.useEffect(() => {
1119
- openDrawerRef.current = () => state.setSheetOpen(true)
1120
- // eslint-disable-next-line react-hooks/exhaustive-deps
1121
- }, [openDrawerRef, state.setSheetOpen])
451
+ const { state, drawerToolbarProps, displayOptions } = args
452
+ const rows = state.rows as Placement[]
1122
453
 
1123
- const boardColumns = state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")
1124
- const groups = React.useMemo(
1125
- () => panelGroupsBuilder ? panelGroupsBuilder(listRows) : buildStatusGroups(listRows),
1126
- [listRows, panelGroupsBuilder],
454
+ const dashboardKpi = React.useMemo(
455
+ () => ({
456
+ metrics: placementKpiMetricsFromRows(rows),
457
+ insight: placementKpiInsightFromRows(rows),
458
+ }),
459
+ [rows],
1127
460
  )
1128
461
 
462
+ const [visibleCards, setVisibleCards] = React.useState<string[]>(DEFAULT_VISIBLE_CARDS)
463
+ const [cardOrder, setCardOrder] = React.useState<string[]>(ALL_DASHBOARD_CARDS.map(c => c.id))
464
+ const [cardSpans, setCardSpans] = React.useState<Record<string, 1 | 2>>(() => ({ ...DEFAULT_SPANS }))
465
+ const [cardChartTypes, setCardChartTypes] = React.useState<Record<string, ChartType>>(() => ({ ...DEFAULT_CHART_TYPES }))
466
+ const [keyMetricsKpiCount, setKeyMetricsKpiCount] = React.useState<number>(KEY_METRICS_KPI_COUNT_DEFAULT)
467
+ const [layoutEdit, setLayoutEdit] = React.useState(false)
468
+ const hydrated = React.useRef(false)
469
+ const baselineRef = React.useRef<DashboardLayout | null>(null)
470
+
471
+ React.useEffect(() => {
472
+ const saved = loadDashboardLayout()
473
+ const m = mergeDashboardLayout(saved)
474
+ setVisibleCards(m.visible)
475
+ setCardOrder(m.order)
476
+ setCardSpans(m.spans ?? { ...DEFAULT_SPANS })
477
+ setCardChartTypes(m.chartTypes ?? { ...DEFAULT_CHART_TYPES })
478
+ setKeyMetricsKpiCount(m.keyMetricsKpiCount ?? KEY_METRICS_KPI_COUNT_DEFAULT)
479
+ hydrated.current = true
480
+ }, [])
481
+
482
+ React.useEffect(() => {
483
+ if (!hydrated.current) return
484
+ saveDashboardLayout({
485
+ visible: visibleCards,
486
+ order: cardOrder,
487
+ spans: cardSpans,
488
+ chartTypes: cardChartTypes,
489
+ keyMetricsKpiCount,
490
+ })
491
+ }, [visibleCards, cardOrder, cardSpans, cardChartTypes, keyMetricsKpiCount])
492
+
493
+ const onResetLayout = React.useCallback(() => {
494
+ setVisibleCards(ALL_DASHBOARD_CARDS.map(c => c.id))
495
+ setCardOrder(ALL_DASHBOARD_CARDS.map(c => c.id))
496
+ setCardSpans({ ...DEFAULT_SPANS })
497
+ setCardChartTypes({ ...DEFAULT_CHART_TYPES })
498
+ setKeyMetricsKpiCount(KEY_METRICS_KPI_COUNT_DEFAULT)
499
+ }, [])
500
+
501
+ const onLayoutEditStart = React.useCallback(() => {
502
+ baselineRef.current = {
503
+ visible: [...visibleCards],
504
+ order: [...cardOrder],
505
+ spans: { ...cardSpans },
506
+ chartTypes: { ...cardChartTypes },
507
+ keyMetricsKpiCount,
508
+ }
509
+ setLayoutEdit(true)
510
+ }, [visibleCards, cardOrder, cardSpans, cardChartTypes, keyMetricsKpiCount])
511
+
512
+ const onLayoutEditCancel = React.useCallback(() => {
513
+ const b = baselineRef.current
514
+ if (b) {
515
+ setVisibleCards(b.visible)
516
+ setCardOrder(b.order)
517
+ setCardSpans(b.spans ?? { ...DEFAULT_SPANS })
518
+ setCardChartTypes(b.chartTypes ?? { ...DEFAULT_CHART_TYPES })
519
+ setKeyMetricsKpiCount(b.keyMetricsKpiCount ?? KEY_METRICS_KPI_COUNT_DEFAULT)
520
+ }
521
+ setLayoutEdit(false)
522
+ }, [])
523
+
524
+ const coach = useCoachMark({
525
+ flowId: "data-list-dashboard-customize",
526
+ steps: DASHBOARD_CUSTOMIZE_COACH_STEPS,
527
+ delay: 700,
528
+ dependsOnDismissedFlowId: "data-list-views-tour",
529
+ })
530
+
1129
531
  return (
1130
- <div className="flex min-h-0 flex-1 flex-col">
1131
- <DataTableToolbar
1132
- state={state}
1133
- columns={columns}
1134
- searchable
1135
- renderFilterOptionValue={renderFilterOptionValue}
1136
- searchAriaLabel="Search rows"
1137
- toolbarSlot={s => (
1138
- <TablePropertiesDrawerButton
1139
- state={s}
1140
- totalRows={tableData.length}
1141
- pagination={pagination}
1142
- onPaginationChange={onPaginationChange}
1143
- conditionalRules={conditionalRules}
1144
- onAddConditionalRule={onAddConditionalRule}
1145
- onRemoveConditionalRule={onRemoveConditionalRule}
1146
- onUpdateConditionalRule={onUpdateConditionalRule}
1147
- filterFields={filterFields}
1148
- currentView={view}
1149
- onViewChange={onViewChange}
1150
- lifecycleTabLabel={lifecycleDrawerLabel}
1151
- fieldDefinitions={fieldDefinitionsForDrawer}
1152
- resolveColumnLabel={resolveColumnLabel}
1153
- displayOptions={displayOptions}
1154
- onDisplayOptionsChange={onDisplayOptionsChange}
1155
- renderFilterOptionValue={renderFilterOptionValue}
1156
- />
1157
- )}
1158
- />
1159
- <ListPageSplitHubChrome aria-label={lifecycleDrawerLabel}>
1160
- <FinderPanelView<Placement>
1161
- embedded
1162
- groupsColumnTitle="Status"
1163
- groups={groups}
1164
- rows={listRows}
1165
- getRowId={r => r.id}
1166
- getRowGroupId={r => r.status}
1167
- defaultGroupId="all"
1168
- autoSaveId="finder-panel-view"
1169
- ariaLabel="Demo panel view"
1170
- emptyList={<p>{emptyTableCopy}</p>}
1171
- renderListRow={
1172
- panelRenderListRow
1173
- ? panelRenderListRow
1174
- : (row, isSelected) => (
1175
- <PlacementFinderListRow
1176
- row={row}
1177
- isSelected={isSelected}
1178
- tab={lifecycleTabId as BoardCardLifecycleTabId}
1179
- hiddenColKeys={state.hiddenCols}
1180
- boardColumns={boardColumns}
1181
- conditionalRules={conditionalRules}
1182
- />
1183
- )
1184
- }
1185
- renderDetail={
1186
- panelRenderDetail
1187
- ? panelRenderDetail
1188
- : row => (
1189
- <PlacementFinderDetail
1190
- row={row}
1191
- tab={lifecycleTabId as BoardCardLifecycleTabId}
1192
- hiddenColKeys={state.hiddenCols}
1193
- boardColumns={boardColumns}
1194
- />
1195
- )
1196
- }
532
+ <>
533
+ <CoachMark state={coach} />
534
+ {!layoutEdit ? (
535
+ <DataTableToolbar
536
+ state={state}
537
+ columns={columns}
538
+ searchable={displayOptions.showToolbarSearch}
539
+ renderFilterOptionValue={drawerToolbarProps.renderFilterOptionValue}
540
+ searchAriaLabel="Search rows"
541
+ toolbarSlot={s => (
542
+ <TablePropertiesDrawerButton
543
+ {...drawerToolbarProps}
544
+ state={s}
545
+ extraActions={
546
+ <Tip side="bottom" label="Edit dashboard layout on canvas">
547
+ <Button
548
+ type="button"
549
+ variant="ghost"
550
+ size="icon-sm"
551
+ aria-label="Edit dashboard layout"
552
+ onClick={onLayoutEditStart}
553
+ className="text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover"
554
+ >
555
+ <i className="fa-light fa-pen-ruler text-[13px]" aria-hidden="true" />
556
+ </Button>
557
+ </Tip>
558
+ }
559
+ />
560
+ )}
1197
561
  />
1198
- </ListPageSplitHubChrome>
1199
- </div>
562
+ ) : null}
563
+ <PlacementsDashboardChartsSection
564
+ placements={rows}
565
+ keyMetrics={dashboardKpi}
566
+ visibleCards={visibleCards}
567
+ cardOrder={cardOrder}
568
+ cardSpans={cardSpans}
569
+ cardChartTypes={cardChartTypes}
570
+ keyMetricsKpiCount={keyMetricsKpiCount}
571
+ layoutEditMode={layoutEdit}
572
+ onVisibleChange={setVisibleCards}
573
+ onOrderChange={setCardOrder}
574
+ onSpanChange={(id, span) => setCardSpans(prev => ({ ...prev, [id]: span }))}
575
+ onChartTypeChange={(id, t) => setCardChartTypes(prev => ({ ...prev, [id]: t }))}
576
+ onKeyMetricsKpiCountChange={setKeyMetricsKpiCount}
577
+ onResetLayout={onResetLayout}
578
+ onLayoutEditDone={() => setLayoutEdit(false)}
579
+ onLayoutEditCancel={onLayoutEditCancel}
580
+ />
581
+ </>
582
+ )
583
+ }
584
+
585
+ // ─── Board renderer body ─────────────────────────────────────────────────────
586
+
587
+ function PlacementsBoardBody({
588
+ args,
589
+ }: {
590
+ args: HubTableRendererArgs<Placement>
591
+ }) {
592
+ const { state, displayOptions, drawerToolbarProps } = args
593
+ const columns = state.displayCols
594
+ const boardColumnMenu: PlacementsBoardColumnMenu = React.useMemo(
595
+ () => ({
596
+ filterableColumns: columns.filter(c => c.filter).map(c => ({ key: c.key, label: c.label })),
597
+ sortableColumns: columns.filter(c => c.sortable && c.sortKey).map(c => ({ key: c.key, label: c.label })),
598
+ groupableColumns: columns.filter(c => c.key !== "select" && c.key !== "actions").map(c => ({ key: c.key, label: c.label })),
599
+ groupBy: state.groupBy,
600
+ onAddFilter: state.addFilter,
601
+ onSortByField: (fieldKey, direction) => {
602
+ state.setSortRules(prev => {
603
+ const filtered = prev.filter(r => r.fieldKey !== fieldKey)
604
+ return [{ id: `sort-${Date.now()}`, fieldKey, direction }, ...filtered]
605
+ })
606
+ },
607
+ onToggleGroupBy: (fieldKey: string) => {
608
+ state.setGroupBy(prev => (prev === fieldKey ? null : fieldKey))
609
+ },
610
+ onOpenProperties: () => state.setSheetOpen(true),
611
+ }),
612
+ [columns, state],
613
+ )
614
+
615
+ return (
616
+ <PlacementsBoardView
617
+ placements={state.rows as Placement[]}
618
+ boardColumnMenu={boardColumnMenu}
619
+ boardDisplay={{
620
+ lineCount: displayOptions.boardLineCount,
621
+ showColumnLabels: displayOptions.showColumnLabels,
622
+ showColumnCounts: displayOptions.showBoardColumnCounts,
623
+ newCardAbove: displayOptions.boardNewCardAbove,
624
+ }}
625
+ hiddenColKeys={state.hiddenCols}
626
+ conditionalRules={drawerToolbarProps.conditionalRules}
627
+ boardColumns={state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")}
628
+ />
1200
629
  )
1201
630
  }
1202
631
 
1203
- // ─────────────────────────────────────────────────────────────────────────────
1204
- // Props
1205
- // ─────────────────────────────────────────────────────────────────────────────
632
+ // ─── Props ───────────────────────────────────────────────────────────────────
1206
633
 
1207
634
  export interface PlacementsTableProps {
1208
635
  view?: DataListViewType
1209
636
  onViewChange?: (view: DataListViewType) => void
1210
- /** Demo row segment: drives filtered rows + column set (`all` | `upcoming` | `ongoing` | `completed`). */
1211
- lifecycleTabId?: PlacementLifecycleTabId
1212
637
  /** Shared display options (persist at page level — all view types). */
1213
638
  displayOptions?: DataListDisplayOptions
1214
639
  onDisplayOptionsChange?: (patch: Partial<DataListDisplayOptions>) => void
1215
- /** Lifecycle column set from the placements page (e.g. `getPlacementColumnsForLifecycle`). */
1216
- getColumnsForLifecycle: (tab: PlacementLifecycleTabId) => ColumnDef<Placement>[]
1217
- /** Empty-state copy for the active lifecycle tab — from the page. */
1218
- emptyTableCopy: string
1219
- /** Table Properties drawer lifecycle label — from the page. */
1220
- lifecycleDrawerLabel: string
1221
640
  /** Panel view: custom groups builder. If not provided, uses default placement status groups. */
1222
641
  panelGroupsBuilder?: (rows: Placement[]) => FinderGroup[]
1223
642
  /** Panel view: custom list row renderer. If not provided, uses default placement row rendering. */
@@ -1227,86 +646,30 @@ export interface PlacementsTableProps {
1227
646
  }
1228
647
 
1229
648
  /** Imperative handle — open Table Properties (table view only). */
1230
- export type PlacementsTableHandle = OpenTablePropertiesHandle
649
+ export type PlacementsTableHandle = HubTableHandle
1231
650
 
1232
- // ─────────────────────────────────────────────────────────────────────────────
1233
- // Main component
1234
- // ─────────────────────────────────────────────────────────────────────────────
651
+ // ─── Public component ───────────────────────────────────────────────────────
1235
652
 
1236
- export const PlacementsTable = React.forwardRef<PlacementsTableHandle, PlacementsTableProps>(function PlacementsTable({
1237
- view = "table",
1238
- onViewChange,
1239
- lifecycleTabId = "all",
1240
- displayOptions: displayOptionsProp,
1241
- onDisplayOptionsChange,
1242
- getColumnsForLifecycle,
1243
- emptyTableCopy,
1244
- lifecycleDrawerLabel,
1245
- panelGroupsBuilder,
1246
- panelRenderListRow,
1247
- panelRenderDetail,
1248
- }, ref) {
653
+ export const PlacementsTable = React.forwardRef<PlacementsTableHandle, PlacementsTableProps>(function PlacementsTable(
654
+ {
655
+ view = "table",
656
+ onViewChange,
657
+ displayOptions: displayOptionsProp,
658
+ onDisplayOptionsChange,
659
+ panelGroupsBuilder,
660
+ panelRenderListRow,
661
+ panelRenderDetail,
662
+ },
663
+ ref,
664
+ ) {
665
+ const router = useRouter()
1249
666
  const displayOptions = React.useMemo(
1250
667
  () => ({ ...DEFAULT_DATA_LIST_DISPLAY_OPTIONS, ...displayOptionsProp }),
1251
668
  [displayOptionsProp],
1252
669
  )
1253
670
 
1254
- const patchDisplayOptions = React.useCallback(
1255
- (patch: Partial<DataListDisplayOptions>) => {
1256
- onDisplayOptionsChange?.(patch)
1257
- },
1258
- [onDisplayOptionsChange],
1259
- )
1260
- const openDrawerRef = React.useRef<() => void>(() => {})
1261
-
1262
- React.useImperativeHandle(ref, () => ({
1263
- openPropertiesDrawer: () => {
1264
- openDrawerRef.current()
1265
- },
1266
- }), [])
1267
-
1268
- const router = useRouter()
1269
- const [pagination, setPagination] = React.useState(false)
1270
-
1271
- const columns = React.useMemo(
1272
- () => getColumnsForLifecycle(lifecycleTabId),
1273
- [getColumnsForLifecycle, lifecycleTabId],
1274
- )
1275
-
1276
- const tableData = React.useMemo(
1277
- () => placementsForPhase(lifecycleTabId),
1278
- [lifecycleTabId],
1279
- )
1280
-
1281
- const filterFields = React.useMemo(() => columnsToFilterFields(columns), [columns])
1282
-
1283
- const fieldDefinitionsForDrawer = React.useMemo(
1284
- () => columns
1285
- .filter(c => c.key !== "select" && c.key !== "actions")
1286
- .map(c => ({
1287
- key: c.key,
1288
- label: c.label,
1289
- sortable: !!(c.sortable && c.sortKey),
1290
- })),
1291
- [columns],
1292
- )
1293
-
1294
- const resolveColumnLabel = React.useCallback(
1295
- (key: string) => columns.find(c => c.key === key)?.label ?? key,
1296
- [columns],
1297
- )
1298
-
1299
- const [conditionalRules, setConditionalRules] = React.useState<ConditionalRule[]>([])
1300
-
1301
- function addConditionalRule(rule: Omit<ConditionalRule, "id">) {
1302
- setConditionalRules(prev => [...prev, { ...rule, id: `cr-${Date.now()}` }])
1303
- }
1304
- function removeConditionalRule(id: string) {
1305
- setConditionalRules(prev => prev.filter(r => r.id !== id))
1306
- }
1307
- function updateConditionalRule(id: string, patch: Partial<ConditionalRule>) {
1308
- setConditionalRules(prev => prev.map(r => r.id === id ? { ...r, ...patch } : r))
1309
- }
671
+ const columns = React.useMemo(() => getPlacementColumns(), [])
672
+ const tableData = ALL_PLACEMENTS
1310
673
 
1311
674
  const renderFilterOptionValue = React.useCallback(
1312
675
  (fieldKey: string, value: string): React.ReactNode => {
@@ -1318,6 +681,8 @@ export const PlacementsTable = React.forwardRef<PlacementsTableHandle, Placement
1318
681
  [columns],
1319
682
  )
1320
683
 
684
+ // ─ Pagination chrome (only TABLE + LIST views) ────────────────────────────
685
+ const [pagination, setPagination] = React.useState(false)
1321
686
  const [paginationPage, setPaginationPage] = React.useState(1)
1322
687
  const [paginationPageSize, setPaginationPageSize] = React.useState(10)
1323
688
  const [filteredCount, setFilteredCount] = React.useState(tableData.length)
@@ -1328,171 +693,196 @@ export const PlacementsTable = React.forwardRef<PlacementsTableHandle, Placement
1328
693
 
1329
694
  const totalPages = Math.max(1, Math.ceil(filteredCount / Math.max(1, paginationPageSize)))
1330
695
  const safePage = Math.min(paginationPage, totalPages)
1331
- const paginationOverride =
1332
- pagination && view !== "board" && view !== "dashboard" && view !== "folder" && view !== "panel" && view !== "tree-panel"
1333
- ? { page: safePage, pageSize: paginationPageSize }
1334
- : undefined
1335
696
 
1336
- const tableState = useTableState(tableData, columns, { key: "student", dir: "asc" }, paginationOverride)
697
+ // Pagination only applies to TABLE + LIST views (board/dashboard/folder/panel/tree-panel
698
+ // are not paged). Cards/boards consume `state.rows` directly.
699
+ const paginationEligible = view === "table" || view === "list"
700
+ const paginationOverride =
701
+ pagination && paginationEligible ? { page: safePage, pageSize: paginationPageSize } : undefined
1337
702
 
1338
- // Stable "open properties drawer" callback ref — see top of this file.
1339
- React.useEffect(() => {
1340
- openDrawerRef.current = () => tableState.setSheetOpen(true)
1341
- // eslint-disable-next-line react-hooks/exhaustive-deps
1342
- }, [openDrawerRef, tableState.setSheetOpen])
703
+ const onPageSizeChange = React.useCallback((n: number) => {
704
+ setPaginationPageSize(n)
705
+ setPaginationPage(1)
706
+ }, [])
1343
707
 
1344
- function buildToolbarSlot(
1345
- s: ReturnType<typeof useTableState<Placement>>,
1346
- ): React.ReactNode {
1347
- return (
1348
- <TablePropertiesDrawerButton
1349
- state={s}
1350
- totalRows={tableData.length}
1351
- pagination={pagination}
1352
- onPaginationChange={setPagination}
1353
- conditionalRules={conditionalRules}
1354
- onAddConditionalRule={addConditionalRule}
1355
- onRemoveConditionalRule={removeConditionalRule}
1356
- onUpdateConditionalRule={updateConditionalRule}
1357
- filterFields={filterFields}
1358
- currentView={view}
1359
- onViewChange={onViewChange}
1360
- lifecycleTabLabel={lifecycleDrawerLabel}
1361
- fieldDefinitions={fieldDefinitionsForDrawer}
1362
- resolveColumnLabel={resolveColumnLabel}
1363
- displayOptions={displayOptions}
1364
- onDisplayOptionsChange={patchDisplayOptions}
1365
- renderFilterOptionValue={renderFilterOptionValue}
1366
- />
1367
- )
708
+ // Renderers --------------------------------------------------------------
709
+ const renderers: HubTableRenderers<Placement> = {
710
+ "board-with-toolbar": (args) =>
711
+ args.toolbarShell(<PlacementsBoardBody args={args} />),
712
+ "list-with-toolbar": (args) => {
713
+ const { state, toolbarShell } = args
714
+ const listRows = pagination ? (state.pagedRows as Placement[]) : (state.rows as Placement[])
715
+ const boardColumns = state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")
716
+ return (
717
+ <>
718
+ {pagination ? (
719
+ <CountSyncer
720
+ count={state.rows.length}
721
+ onSync={setFilteredCount}
722
+ onReset={() => setPaginationPage(1)}
723
+ />
724
+ ) : null}
725
+ {toolbarShell(
726
+ <>
727
+ <DataRowList<Placement>
728
+ rows={listRows}
729
+ getRowId={row => row.id}
730
+ ariaLabel="Placements"
731
+ emptyState={PLACEMENT_EMPTY_COPY}
732
+ virtualizeThreshold={PLACEMENT_LIST_VIRTUAL_ROWS_THRESHOLD}
733
+ estimatedRowHeight={PLACEMENT_LIST_ESTIMATE_ROW_PX}
734
+ renderRow={row => (
735
+ <PlacementListRowContent
736
+ row={row}
737
+ hiddenColKeys={state.hiddenCols}
738
+ boardColumns={boardColumns}
739
+ conditionalRules={args.drawerToolbarProps.conditionalRules}
740
+ onOpen={id => router.push(`/data-list/${id}`)}
741
+ />
742
+ )}
743
+ />
744
+ {pagination ? (
745
+ <div className="mx-4 lg:mx-6 border-x border-b border-border rounded-b-lg overflow-hidden">
746
+ <PaginationBar
747
+ page={safePage}
748
+ pageSize={paginationPageSize}
749
+ total={filteredCount}
750
+ pageSizeOptions={[10, 25, 50, 100]}
751
+ onPageChange={setPaginationPage}
752
+ onPageSizeChange={onPageSizeChange}
753
+ />
754
+ </div>
755
+ ) : null}
756
+ </>,
757
+ )}
758
+ </>
759
+ )
760
+ },
761
+ "folder-with-toolbar": (args) => {
762
+ const { state, toolbarShell } = args
763
+ const boardColumns = state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")
764
+ return toolbarShell(
765
+ <FolderGridView<Placement>
766
+ rows={state.rows as Placement[]}
767
+ getRowId={r => r.id}
768
+ ariaLabel="Demo folder view"
769
+ emptyContent={<p>{PLACEMENT_EMPTY_COPY}</p>}
770
+ renderTile={row => (
771
+ <PlacementFolderTile
772
+ row={row}
773
+ hiddenColKeys={state.hiddenCols}
774
+ boardColumns={boardColumns}
775
+ onClick={() => router.push(`/data-list/${row.id}`)}
776
+ />
777
+ )}
778
+ />,
779
+ )
780
+ },
781
+ "tree-panel-with-toolbar": (args) =>
782
+ args.toolbarShell(<PlacementsTreeBody args={args} />),
783
+ "panel-with-toolbar": (args) => {
784
+ const { state, toolbarShell } = args
785
+ const listRows = state.rows as Placement[]
786
+ const boardColumns = state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")
787
+ const groups = panelGroupsBuilder ? panelGroupsBuilder(listRows) : buildStatusGroups(listRows)
788
+ return toolbarShell(
789
+ <ListPageSplitHubChrome aria-label={PLACEMENT_DRAWER_LABEL}>
790
+ <FinderPanelView<Placement>
791
+ embedded
792
+ groupsColumnTitle="Status"
793
+ groups={groups}
794
+ rows={listRows}
795
+ getRowId={r => r.id}
796
+ getRowGroupId={r => r.status}
797
+ defaultGroupId="all"
798
+ autoSaveId="finder-panel-view"
799
+ ariaLabel="Demo panel view"
800
+ emptyList={<p>{PLACEMENT_EMPTY_COPY}</p>}
801
+ renderListRow={
802
+ panelRenderListRow
803
+ ? panelRenderListRow
804
+ : (row, isSelected) => (
805
+ <PlacementFinderListRow
806
+ row={row}
807
+ isSelected={isSelected}
808
+ hiddenColKeys={state.hiddenCols}
809
+ boardColumns={boardColumns}
810
+ />
811
+ )
812
+ }
813
+ renderDetail={
814
+ panelRenderDetail
815
+ ? panelRenderDetail
816
+ : row => (
817
+ <PlacementFinderDetail
818
+ row={row}
819
+ hiddenColKeys={state.hiddenCols}
820
+ boardColumns={boardColumns}
821
+ />
822
+ )
823
+ }
824
+ />
825
+ </ListPageSplitHubChrome>,
826
+ )
827
+ },
828
+ "dashboard-with-toolbar": (args) => <PlacementsDashboardBody args={args} columns={columns} />,
1368
829
  }
1369
830
 
1370
- function bulkActionsSlot(selected: Set<string | number>, _rows: Placement[]): React.ReactNode {
1371
- const count = selected.size
1372
- const contextId = "bulk-selection-context"
831
+ // Custom `tableRenderer` so pagination chrome (CountSyncer + PaginationBar) wraps the
832
+ // default DataTable when `pagination` is on. When off, falls back to a plain DataTable.
833
+ const tableRenderer = (args: HubTableRendererArgs<Placement>) => {
834
+ const { state } = args
1373
835
  return (
1374
836
  <>
1375
- <span id={contextId} className="sr-only">
1376
- {count} {count === 1 ? "row" : "rows"} selected
1377
- </span>
1378
- <Button size="sm" variant="default" aria-describedby={contextId}>
1379
- <i className="fa-light fa-circle-check" aria-hidden="true" /> Confirm
1380
- </Button>
1381
- <Button size="sm" variant="outline" aria-describedby={contextId}>
1382
- <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" /> Export
1383
- </Button>
1384
- <Button size="sm" variant="destructive" aria-describedby={contextId}>
1385
- <i className="fa-light fa-trash" aria-hidden="true" /> Delete
1386
- </Button>
1387
- </>
1388
- )
1389
- }
1390
-
1391
- const tableProps: DataTableExtendedProps<Placement> = {
1392
- data: tableData,
1393
- columns,
1394
- getRowId: (row: Placement) => row.id,
1395
- getRowSelectionLabel: (row: Placement) => row.student,
1396
- selectable: true,
1397
- searchable: displayOptions.showToolbarSearch,
1398
- showColumnHeaders: displayOptions.showColumnLabels,
1399
- defaultSort: { key: "student" as const, dir: "asc" as const },
1400
- emptyState: emptyTableCopy,
1401
- toolbarSlot: buildToolbarSlot,
1402
- bulkActionsSlot,
1403
- renderFilterOptionValue,
1404
- conditionalRules,
1405
- onRowClick: (row: Placement) => router.push(`/data-list/${row.id}`),
1406
- state: tableState,
1407
- }
1408
-
1409
- if (view === "board") {
1410
- return (
1411
- <DataListBoardShell
1412
- state={tableState}
1413
- openDrawerRef={openDrawerRef}
1414
- tableData={tableData}
1415
- columns={columns}
1416
- lifecycleTabId={lifecycleTabId}
1417
- view={view}
1418
- onViewChange={onViewChange}
1419
- pagination={pagination}
1420
- onPaginationChange={setPagination}
1421
- conditionalRules={conditionalRules}
1422
- onAddConditionalRule={addConditionalRule}
1423
- onRemoveConditionalRule={removeConditionalRule}
1424
- onUpdateConditionalRule={updateConditionalRule}
1425
- filterFields={filterFields}
1426
- lifecycleDrawerLabel={lifecycleDrawerLabel}
1427
- fieldDefinitionsForDrawer={fieldDefinitionsForDrawer}
1428
- resolveColumnLabel={resolveColumnLabel}
1429
- renderFilterOptionValue={renderFilterOptionValue}
1430
- displayOptions={displayOptions}
1431
- onDisplayOptionsChange={patchDisplayOptions}
1432
- />
1433
- )
1434
- }
1435
-
1436
- if (view === "dashboard") {
1437
- return (
1438
- <DataListDashboardShell
1439
- state={tableState}
1440
- openDrawerRef={openDrawerRef}
1441
- tableData={tableData}
1442
- columns={columns}
1443
- view={view}
1444
- onViewChange={onViewChange}
1445
- pagination={pagination}
1446
- onPaginationChange={setPagination}
1447
- conditionalRules={conditionalRules}
1448
- onAddConditionalRule={addConditionalRule}
1449
- onRemoveConditionalRule={removeConditionalRule}
1450
- onUpdateConditionalRule={updateConditionalRule}
1451
- filterFields={filterFields}
1452
- lifecycleDrawerLabel={lifecycleDrawerLabel}
1453
- fieldDefinitionsForDrawer={fieldDefinitionsForDrawer}
1454
- resolveColumnLabel={resolveColumnLabel}
1455
- renderFilterOptionValue={renderFilterOptionValue}
1456
- displayOptions={displayOptions}
1457
- onDisplayOptionsChange={patchDisplayOptions}
1458
- />
1459
- )
1460
- }
1461
-
1462
- if (view === "list") {
1463
- return (
1464
- <React.Fragment key={lifecycleTabId}>
1465
837
  {pagination ? (
1466
838
  <CountSyncer
1467
- count={tableState.rows.length}
839
+ count={state.rows.length}
1468
840
  onSync={setFilteredCount}
1469
841
  onReset={() => setPaginationPage(1)}
1470
842
  />
1471
843
  ) : null}
1472
- <DataListListShell
1473
- state={tableState}
1474
- openDrawerRef={openDrawerRef}
1475
- tableData={tableData}
1476
- columns={columns}
1477
- lifecycleTabId={lifecycleTabId}
1478
- view={view}
1479
- onViewChange={onViewChange}
1480
- pagination={pagination}
1481
- onPaginationChange={setPagination}
1482
- conditionalRules={conditionalRules}
1483
- onAddConditionalRule={addConditionalRule}
1484
- onRemoveConditionalRule={removeConditionalRule}
1485
- onUpdateConditionalRule={updateConditionalRule}
1486
- filterFields={filterFields}
1487
- lifecycleDrawerLabel={lifecycleDrawerLabel}
1488
- fieldDefinitionsForDrawer={fieldDefinitionsForDrawer}
1489
- resolveColumnLabel={resolveColumnLabel}
1490
- renderFilterOptionValue={renderFilterOptionValue}
1491
- displayOptions={displayOptions}
1492
- onDisplayOptionsChange={patchDisplayOptions}
1493
- listRows={pagination ? tableState.pagedRows : tableState.rows}
1494
- emptyTableCopy={emptyTableCopy}
1495
- />
844
+ <div className="pb-6">
845
+ <DataTable<Placement>
846
+ data={tableData}
847
+ columns={columns}
848
+ getRowId={row => row.id}
849
+ getRowSelectionLabel={row => row.student}
850
+ selectable
851
+ searchable={displayOptions.showToolbarSearch}
852
+ showColumnHeaders={displayOptions.showColumnLabels}
853
+ defaultSort={{ key: "student" as const, dir: "asc" as const }}
854
+ emptyState={PLACEMENT_EMPTY_COPY}
855
+ renderFilterOptionValue={renderFilterOptionValue}
856
+ conditionalRules={args.drawerToolbarProps.conditionalRules}
857
+ onRowClick={row => router.push(`/data-list/${row.id}`)}
858
+ state={state}
859
+ hasFooter={pagination}
860
+ toolbarSlot={s => (
861
+ <TablePropertiesDrawerButton {...args.drawerToolbarProps} state={s} />
862
+ )}
863
+ bulkActionsSlot={(selected) => {
864
+ const count = selected.size
865
+ if (count === 0) return null
866
+ const contextId = "bulk-selection-context"
867
+ return (
868
+ <>
869
+ <span id={contextId} className="sr-only">
870
+ {count} {count === 1 ? "row" : "rows"} selected
871
+ </span>
872
+ <Button size="sm" variant="default" aria-describedby={contextId}>
873
+ <i className="fa-light fa-circle-check" aria-hidden="true" /> Confirm
874
+ </Button>
875
+ <Button size="sm" variant="outline" aria-describedby={contextId}>
876
+ <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" /> Export
877
+ </Button>
878
+ <Button size="sm" variant="destructive" aria-describedby={contextId}>
879
+ <i className="fa-light fa-trash" aria-hidden="true" /> Delete
880
+ </Button>
881
+ </>
882
+ )
883
+ }}
884
+ />
885
+ </div>
1496
886
  {pagination ? (
1497
887
  <div className="mx-4 lg:mx-6 border-x border-b border-border rounded-b-lg overflow-hidden">
1498
888
  <PaginationBar
@@ -1501,142 +891,44 @@ export const PlacementsTable = React.forwardRef<PlacementsTableHandle, Placement
1501
891
  total={filteredCount}
1502
892
  pageSizeOptions={[10, 25, 50, 100]}
1503
893
  onPageChange={setPaginationPage}
1504
- onPageSizeChange={n => {
1505
- setPaginationPageSize(n)
1506
- setPaginationPage(1)
1507
- }}
894
+ onPageSizeChange={onPageSizeChange}
1508
895
  />
1509
896
  </div>
1510
897
  ) : null}
1511
- </React.Fragment>
1512
- )
1513
- }
1514
-
1515
- if (view === "folder") {
1516
- return (
1517
- <DataListFolderShell
1518
- key={lifecycleTabId}
1519
- state={tableState}
1520
- openDrawerRef={openDrawerRef}
1521
- tableData={tableData}
1522
- columns={columns}
1523
- lifecycleTabId={lifecycleTabId}
1524
- view={view}
1525
- onViewChange={onViewChange}
1526
- pagination={pagination}
1527
- onPaginationChange={setPagination}
1528
- conditionalRules={conditionalRules}
1529
- onAddConditionalRule={addConditionalRule}
1530
- onRemoveConditionalRule={removeConditionalRule}
1531
- onUpdateConditionalRule={updateConditionalRule}
1532
- filterFields={filterFields}
1533
- lifecycleDrawerLabel={lifecycleDrawerLabel}
1534
- fieldDefinitionsForDrawer={fieldDefinitionsForDrawer}
1535
- resolveColumnLabel={resolveColumnLabel}
1536
- renderFilterOptionValue={renderFilterOptionValue}
1537
- displayOptions={displayOptions}
1538
- onDisplayOptionsChange={patchDisplayOptions}
1539
- listRows={tableState.rows}
1540
- emptyTableCopy={emptyTableCopy}
1541
- />
1542
- )
1543
- }
1544
-
1545
- if (view === "tree-panel") {
1546
- return (
1547
- <DataListTreeShell
1548
- key={lifecycleTabId}
1549
- state={tableState}
1550
- openDrawerRef={openDrawerRef}
1551
- tableData={tableData}
1552
- columns={columns}
1553
- lifecycleTabId={lifecycleTabId}
1554
- view={view}
1555
- onViewChange={onViewChange}
1556
- pagination={pagination}
1557
- onPaginationChange={setPagination}
1558
- conditionalRules={conditionalRules}
1559
- onAddConditionalRule={addConditionalRule}
1560
- onRemoveConditionalRule={removeConditionalRule}
1561
- onUpdateConditionalRule={updateConditionalRule}
1562
- filterFields={filterFields}
1563
- lifecycleDrawerLabel={lifecycleDrawerLabel}
1564
- fieldDefinitionsForDrawer={fieldDefinitionsForDrawer}
1565
- resolveColumnLabel={resolveColumnLabel}
1566
- renderFilterOptionValue={renderFilterOptionValue}
1567
- displayOptions={displayOptions}
1568
- onDisplayOptionsChange={patchDisplayOptions}
1569
- listRows={tableState.rows}
1570
- emptyTableCopy={emptyTableCopy}
1571
- />
1572
- )
1573
- }
1574
-
1575
- if (view === "panel") {
1576
- return (
1577
- <DataListPanelShell
1578
- key={lifecycleTabId}
1579
- state={tableState}
1580
- openDrawerRef={openDrawerRef}
1581
- tableData={tableData}
1582
- columns={columns}
1583
- lifecycleTabId={lifecycleTabId}
1584
- view={view}
1585
- onViewChange={onViewChange}
1586
- pagination={pagination}
1587
- onPaginationChange={setPagination}
1588
- conditionalRules={conditionalRules}
1589
- onAddConditionalRule={addConditionalRule}
1590
- onRemoveConditionalRule={removeConditionalRule}
1591
- onUpdateConditionalRule={updateConditionalRule}
1592
- filterFields={filterFields}
1593
- lifecycleDrawerLabel={lifecycleDrawerLabel}
1594
- fieldDefinitionsForDrawer={fieldDefinitionsForDrawer}
1595
- resolveColumnLabel={resolveColumnLabel}
1596
- renderFilterOptionValue={renderFilterOptionValue}
1597
- displayOptions={displayOptions}
1598
- onDisplayOptionsChange={patchDisplayOptions}
1599
- listRows={tableState.rows}
1600
- emptyTableCopy={emptyTableCopy}
1601
- panelGroupsBuilder={panelGroupsBuilder}
1602
- panelRenderListRow={panelRenderListRow}
1603
- panelRenderDetail={panelRenderDetail}
1604
- />
1605
- )
1606
- }
1607
-
1608
- if (pagination) {
1609
- return (
1610
- <React.Fragment key={lifecycleTabId}>
1611
- <CountSyncer
1612
- count={tableState.rows.length}
1613
- onSync={setFilteredCount}
1614
- onReset={() => setPaginationPage(1)}
1615
- />
1616
- <DataTable<Placement> {...tableProps} hasFooter />
1617
- <div className="mx-4 lg:mx-6 border-x border-b border-border rounded-b-lg overflow-hidden">
1618
- <PaginationBar
1619
- page={safePage}
1620
- pageSize={paginationPageSize}
1621
- total={filteredCount}
1622
- pageSizeOptions={[10, 25, 50, 100]}
1623
- onPageChange={setPaginationPage}
1624
- onPageSizeChange={n => {
1625
- setPaginationPageSize(n)
1626
- setPaginationPage(1)
1627
- }}
1628
- />
1629
- </div>
1630
- </React.Fragment>
898
+ </>
1631
899
  )
1632
900
  }
1633
901
 
1634
- return <DataTable<Placement> key={lifecycleTabId} {...tableProps} />
902
+ return (
903
+ <HubTable<Placement>
904
+ rows={tableData}
905
+ columns={columns}
906
+ view={view}
907
+ onViewChange={onViewChange}
908
+ supportedViewTypes={PLACEMENTS_SUPPORTED_VIEWS}
909
+ hubLabel={PLACEMENT_DRAWER_LABEL}
910
+ lifecycleTabLabel={PLACEMENT_DRAWER_LABEL}
911
+ searchAriaLabel="Search rows"
912
+ getRowId={row => row.id}
913
+ getRowSelectionLabel={row => row.student}
914
+ defaultSort={{ key: "student", dir: "asc" }}
915
+ emptyState={PLACEMENT_EMPTY_COPY}
916
+ onRowClick={row => router.push(`/data-list/${row.id}`)}
917
+ displayOptions={displayOptions}
918
+ onDisplayOptionsChange={onDisplayOptionsChange}
919
+ pagination={pagination}
920
+ onPaginationChange={setPagination}
921
+ paginationOverride={paginationOverride}
922
+ renderFilterOptionValue={renderFilterOptionValue}
923
+ renderers={renderers}
924
+ tableRenderer={tableRenderer}
925
+ handleRef={ref}
926
+ />
927
+ )
1635
928
  })
1636
929
 
1637
930
  PlacementsTable.displayName = "PlacementsTable"
1638
931
 
1639
-
1640
932
  export type { DataListViewType } from "@/lib/data-list-view"
1641
933
  export { DATA_LIST_VIEW_TILES } from "@/lib/data-list-view"
1642
934
  export type { DataListDisplayOptions } from "@/lib/data-list-display-options"