@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,19 +1,27 @@
1
1
  "use client"
2
2
 
3
3
  /**
4
- * Question bank — DataTable + TablePropertiesDrawer + list/board/dashboard (KPI + charts on dashboard).
4
+ * Question bank — thin wrapper around the centralized `<HubTable>`. Owns column defs,
5
+ * folder/panel/tree-panel custom views, the new-folder + customize-folder sheet, and
6
+ * forwards URL search via `HubTable.syncedSearchFromUrl`.
7
+ *
8
+ * Single dataset rule: `HubTable` runs one `useTableState(tableSourceItems, columns, …)`.
9
+ * Every non-table renderer (list, board, folder, panel, tree-panel, dashboard) reads
10
+ * `state.rows` — the same filtered/sorted/searched bag as the grid.
5
11
  */
6
12
 
7
13
  import * as React from "react"
8
14
  import dynamic from "next/dynamic"
9
15
  import { mailtoHref } from "@/lib/mailto"
10
- import { DataTable, DataTableToolbar } from "@/components/data-table"
11
16
  import type { DataListViewType } from "@/lib/data-list-view"
12
- import type { OpenTablePropertiesHandle } from "@/lib/list-page-table-properties"
13
17
  import type { ColumnDef } from "@/components/data-table/types"
14
- import { useTableState } from "@/components/data-table/use-table-state"
15
- import { TablePropertiesDrawerButton } from "@/components/table-properties"
16
- import type { ConditionalRule, FilterFieldDef, FilterOperator } from "@/components/table-properties/types"
18
+ import {
19
+ HubTable,
20
+ type HubTableHandle,
21
+ type HubTableRenderers,
22
+ type HubTableRendererArgs,
23
+ } from "@/components/data-views"
24
+ import { QUESTION_BANK_SUPPORTED_VIEWS } from "@/lib/question-bank-supported-views"
17
25
  import { Button } from "@/components/ui/button"
18
26
  import {
19
27
  DropdownMenu,
@@ -41,7 +49,7 @@ import {
41
49
  } from "@/components/data-views/list-page-split-hub-tokens"
42
50
  import { ListPageTreeColumnHeader } from "@/components/data-views/list-page-tree-column-header"
43
51
  import { QuestionBankBoardView, QUESTION_BANK_BOARD_GROUP_OPTIONS } from "@/components/question-bank-board-view"
44
- import { QuestionBankListView } from "@/components/question-bank-list-view"
52
+ import { ListPageBoardCard } from "@/components/data-views/list-page-board-card"
45
53
  import {
46
54
  QuestionBankFavoriteButton,
47
55
  QUESTION_BANK_FAVORITE_HOVER_GROUP,
@@ -60,17 +68,19 @@ import {
60
68
  type QuestionBankItem,
61
69
  type QuestionBankType,
62
70
  } from "@/lib/mock/question-bank"
63
- import { type QuestionBankFolder, QUESTION_BANK_FOLDER_COLOR_STYLES, QUESTION_BANK_FOLDER_ICON_COLORS } from "@/lib/mock/question-bank-folders"
71
+ import {
72
+ type QuestionBankFolder,
73
+ QUESTION_BANK_FOLDER_COLOR_STYLES,
74
+ QUESTION_BANK_FOLDER_ICON_COLORS,
75
+ } from "@/lib/mock/question-bank-folders"
64
76
  import {
65
77
  toggleQuestionBankItemFavorite,
66
78
  applyQuestionBankHubDisplayFilters,
67
79
  type QuestionBankLandingFilterState,
68
80
  type QuestionBankNavState,
69
81
  } from "@/lib/question-bank-nav"
70
- import {
71
- DEFAULT_DATA_LIST_DISPLAY_OPTIONS,
72
- type DataListDisplayOptions,
73
- } from "@/lib/data-list-display-options"
82
+
83
+ // ─── Dynamic dashboard charts section ────────────────────────────────────────
74
84
 
75
85
  const QuestionBankDashboardChartsSection = dynamic(
76
86
  () =>
@@ -102,6 +112,8 @@ const QuestionBankDashboardChartsSection = dynamic(
102
112
  },
103
113
  )
104
114
 
115
+ // ─── Constants ───────────────────────────────────────────────────────────────
116
+
105
117
  const TYPE_LABEL: Record<QuestionBankType, string> = {
106
118
  multiple_choice: "Multiple choice",
107
119
  true_false: "True / false",
@@ -114,21 +126,6 @@ const DIFFICULTY_LABEL: Record<QuestionBankDifficulty, string> = {
114
126
  hard: "Hard",
115
127
  }
116
128
 
117
- function newQuestionBankItemId() {
118
- return `q-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`
119
- }
120
-
121
- /** Folder id to use when adding a question from the root column (`parentId` null). */
122
- function defaultFolderIdForColumnParent(parentId: string | null, folders: QuestionBankFolder[]): string | null {
123
- if (parentId !== null) return parentId
124
- const roots = [...folders].filter(f => f.parentId === null).sort((a, b) => a.name.localeCompare(b.name))
125
- return roots[0]?.id ?? null
126
- }
127
-
128
- function uniqueTopics(items: QuestionBankItem[]) {
129
- return [...new Set(items.map(i => i.topic))].sort().map(t => ({ value: t, label: t }))
130
- }
131
-
132
129
  const TYPE_FILTER_OPTS = (Object.keys(TYPE_LABEL) as QuestionBankType[]).map(k => ({
133
130
  value: k,
134
131
  label: TYPE_LABEL[k],
@@ -139,26 +136,18 @@ const DIFFICULTY_FILTER_OPTS = (Object.keys(DIFFICULTY_LABEL) as QuestionBankDif
139
136
  label: DIFFICULTY_LABEL[k],
140
137
  }))
141
138
 
142
- function columnToFilterFieldDef(c: ColumnDef<QuestionBankItem>): FilterFieldDef | null {
143
- if (!c.filter) return null
144
- const f = c.filter
145
- const defaultOps: FilterOperator[] =
146
- f.type === "select" || f.type === "date"
147
- ? ["is", "is_not"]
148
- : ["contains", "not_contains"]
149
- return {
150
- key: c.key,
151
- label: c.label,
152
- icon: f.icon ?? "fa-filter",
153
- type: f.type,
154
- operators: (f.operators ?? defaultOps) as FilterOperator[],
155
- options: f.options,
156
- ...(f.textMask ? { textMask: f.textMask } : {}),
157
- }
139
+ function newQuestionBankItemId() {
140
+ return `q-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`
158
141
  }
159
142
 
160
- function columnsToFilterFields(cols: ColumnDef<QuestionBankItem>[]) {
161
- return cols.map(columnToFilterFieldDef).filter((x): x is FilterFieldDef => x !== null)
143
+ function defaultFolderIdForColumnParent(parentId: string | null, folders: QuestionBankFolder[]): string | null {
144
+ if (parentId !== null) return parentId
145
+ const roots = [...folders].filter(f => f.parentId === null).sort((a, b) => a.name.localeCompare(b.name))
146
+ return roots[0]?.id ?? null
147
+ }
148
+
149
+ function uniqueTopics(items: QuestionBankItem[]) {
150
+ return [...new Set(items.map(i => i.topic))].sort().map(t => ({ value: t, label: t }))
162
151
  }
163
152
 
164
153
  function buildQuestionBankColumns(
@@ -167,18 +156,8 @@ function buildQuestionBankColumns(
167
156
  ): ColumnDef<QuestionBankItem>[] {
168
157
  const topicOpts = uniqueTopics(items)
169
158
  const { onToggleFavorite } = opts
170
-
171
- const COLUMN_SELECT: ColumnDef<QuestionBankItem> = {
172
- key: "select",
173
- label: "",
174
- width: 40,
175
- minWidth: 40,
176
- defaultPin: "left",
177
- lockPin: true,
178
- }
179
-
180
- const cols: ColumnDef<QuestionBankItem>[] = [COLUMN_SELECT]
181
- cols.push(
159
+ return [
160
+ { key: "select", label: "", width: 40, minWidth: 40, defaultPin: "left", lockPin: true },
182
161
  {
183
162
  key: "stem",
184
163
  label: "Question",
@@ -187,14 +166,10 @@ function buildQuestionBankColumns(
187
166
  sortable: true,
188
167
  sortKey: "stem",
189
168
  defaultPin: "left",
190
- filter: {
191
- type: "text",
192
- icon: "fa-file-lines",
193
- operators: ["contains", "not_contains"],
194
- },
169
+ filter: { type: "text", icon: "fa-file-lines", operators: ["contains", "not_contains"] },
195
170
  cell: row => (
196
171
  <div className={cn(QUESTION_BANK_FAVORITE_HOVER_GROUP, "flex min-w-0 items-start gap-2")}>
197
- <div className="flex min-w-0 flex-1 flex-col gap-0.5 pr-1">
172
+ <div className="flex min-w-0 flex-1 flex-col gap-0.5 pe-1">
198
173
  <span className="line-clamp-2 text-sm font-medium text-foreground">{row.stem}</span>
199
174
  <span className="font-mono text-xs text-muted-foreground">{row.questionId}</span>
200
175
  </div>
@@ -209,12 +184,7 @@ function buildQuestionBankColumns(
209
184
  minWidth: 120,
210
185
  sortable: true,
211
186
  sortKey: "topic",
212
- filter: {
213
- type: "select",
214
- icon: "fa-layer-group",
215
- operators: ["is", "is_not"],
216
- options: topicOpts,
217
- },
187
+ filter: { type: "select", icon: "fa-layer-group", operators: ["is", "is_not"], options: topicOpts },
218
188
  cell: row => <span className="text-sm text-foreground/90">{row.topic}</span>,
219
189
  },
220
190
  {
@@ -224,12 +194,7 @@ function buildQuestionBankColumns(
224
194
  minWidth: 120,
225
195
  sortable: true,
226
196
  sortKey: "type",
227
- filter: {
228
- type: "select",
229
- icon: "fa-list-check",
230
- operators: ["is", "is_not"],
231
- options: TYPE_FILTER_OPTS,
232
- },
197
+ filter: { type: "select", icon: "fa-list-check", operators: ["is", "is_not"], options: TYPE_FILTER_OPTS },
233
198
  cell: row => <span className="text-sm text-foreground/90">{TYPE_LABEL[row.type]}</span>,
234
199
  },
235
200
  {
@@ -239,15 +204,8 @@ function buildQuestionBankColumns(
239
204
  minWidth: 96,
240
205
  sortable: true,
241
206
  sortKey: "difficulty",
242
- filter: {
243
- type: "select",
244
- icon: "fa-signal",
245
- operators: ["is", "is_not"],
246
- options: DIFFICULTY_FILTER_OPTS,
247
- },
248
- cell: row => (
249
- <span className="text-sm text-foreground/90">{DIFFICULTY_LABEL[row.difficulty]}</span>
250
- ),
207
+ filter: { type: "select", icon: "fa-signal", operators: ["is", "is_not"], options: DIFFICULTY_FILTER_OPTS },
208
+ cell: row => <span className="text-sm text-foreground/90">{DIFFICULTY_LABEL[row.difficulty]}</span>,
251
209
  },
252
210
  {
253
211
  key: "updatedAt",
@@ -268,11 +226,7 @@ function buildQuestionBankColumns(
268
226
  minWidth: 200,
269
227
  sortable: true,
270
228
  sortKey: "author",
271
- filter: {
272
- type: "text",
273
- icon: "fa-user",
274
- operators: ["contains", "not_contains"],
275
- },
229
+ filter: { type: "text", icon: "fa-user", operators: ["contains", "not_contains"] },
276
230
  cell: row => {
277
231
  const initials = initialsFromDisplayName(row.author)
278
232
  return (
@@ -323,11 +277,11 @@ function buildQuestionBankColumns(
323
277
  </div>
324
278
  ),
325
279
  },
326
- )
327
-
328
- return cols
280
+ ]
329
281
  }
330
282
 
283
+ // ─── Folder columns panel (custom multi-column miller view) ─────────────────
284
+
331
285
  interface HubFolderColumnsPanelProps {
332
286
  folders: QuestionBankFolder[]
333
287
  rows: QuestionBankItem[]
@@ -340,14 +294,13 @@ interface HubFolderColumnsPanelProps {
340
294
  type HierarchyItem = QuestionBankFolder | QuestionBankItem
341
295
 
342
296
  function isFolder(item: HierarchyItem): item is QuestionBankFolder {
343
- return 'parentId' in item
297
+ return "parentId" in item
344
298
  }
345
299
 
346
300
  function isQuestion(item: HierarchyItem): item is QuestionBankItem {
347
- return 'stem' in item
301
+ return "stem" in item
348
302
  }
349
303
 
350
- /** **Panel view** — multi-column folder explorer + optional detail column (Finder-style). */
351
304
  function HubFolderColumnsPanel({
352
305
  folders,
353
306
  rows,
@@ -356,37 +309,25 @@ function HubFolderColumnsPanel({
356
309
  onAddQuestion,
357
310
  onCustomizeFolder,
358
311
  }: HubFolderColumnsPanelProps) {
359
- // Track the selected path through the hierarchy
360
- // Initialize with first folder selected by default
361
312
  const [selectedPath, setSelectedPath] = React.useState<HierarchyItem[]>(() => {
362
313
  const rootFolders = folders
363
314
  .filter(f => f.parentId === null)
364
315
  .sort((a, b) => a.name.localeCompare(b.name))
365
- if (rootFolders.length > 0) {
366
- return [rootFolders[0]]
367
- }
316
+ if (rootFolders.length > 0) return [rootFolders[0]]
368
317
  return []
369
318
  })
370
319
 
371
- // Track if this is the first render for auto-selection
372
320
  const isFirstRenderRef = React.useRef(true)
373
321
 
374
- // Get root items (top-level folders)
375
- const rootFolders = React.useMemo(() => {
376
- return folders
377
- .filter(f => f.parentId === null)
378
- .sort((a, b) => a.name.localeCompare(b.name))
379
- }, [folders])
322
+ const rootFolders = React.useMemo(
323
+ () => folders.filter(f => f.parentId === null).sort((a, b) => a.name.localeCompare(b.name)),
324
+ [folders],
325
+ )
380
326
 
381
- // Handle selection at any depth
382
327
  const handleSelect = (item: HierarchyItem, depth: number) => {
383
328
  setSelectedPath(prev => [...prev.slice(0, depth), item])
384
329
  }
385
330
 
386
- // Auto-select first item at each level (only on first render). Intentional
387
- // empty deps: we want this to run exactly once on mount; depending on the
388
- // referenced values (folders / rows / selectedPath) would re-run on every
389
- // edit and keep re-seeding the selection, undoing the user's choice.
390
331
  React.useEffect(() => {
391
332
  if (isFirstRenderRef.current && selectedPath.length > 0) {
392
333
  const lastItem = selectedPath[selectedPath.length - 1]
@@ -395,7 +336,6 @@ function HubFolderColumnsPanel({
395
336
  const subfolders = folders.filter(f => f.parentId === folder.id).sort((a, b) => a.name.localeCompare(b.name))
396
337
  const questionsInFolder = rows.filter(r => r.folderId === folder.id)
397
338
  const items: HierarchyItem[] = [...subfolders, ...questionsInFolder]
398
-
399
339
  if (items.length > 0 && !selectedPath[selectedPath.length + 1]) {
400
340
  setSelectedPath(prev => [...prev, items[0]])
401
341
  isFirstRenderRef.current = false
@@ -405,53 +345,31 @@ function HubFolderColumnsPanel({
405
345
  // eslint-disable-next-line react-hooks/exhaustive-deps
406
346
  }, [])
407
347
 
408
- // Build columns dynamically based on selected path
409
348
  const columns: Array<{ items: HierarchyItem[]; depth: number; parentId?: string | null }> = React.useMemo(() => {
410
349
  const cols: Array<{ items: HierarchyItem[]; depth: number; parentId?: string | null }> = [
411
350
  { items: rootFolders, depth: 0, parentId: null },
412
351
  ]
413
-
414
- // For each selected folder in the path, add a column with its children
415
352
  for (let i = 0; i < selectedPath.length; i++) {
416
353
  const item = selectedPath[i]
417
354
  if (isFolder(item)) {
418
- // Get subfolders
419
- const subfolders = folders
420
- .filter(f => f.parentId === item.id)
421
- .sort((a, b) => a.name.localeCompare(b.name))
422
-
423
- // Get questions in this folder
355
+ const subfolders = folders.filter(f => f.parentId === item.id).sort((a, b) => a.name.localeCompare(b.name))
424
356
  const questionsInFolder = rows.filter(r => r.folderId === item.id)
425
-
426
- // Combine folders and questions
427
357
  const items: HierarchyItem[] = [...subfolders, ...questionsInFolder]
428
-
429
- if (items.length > 0) {
430
- cols.push({ items, depth: i + 1, parentId: item.id })
431
- }
358
+ if (items.length > 0) cols.push({ items, depth: i + 1, parentId: item.id })
432
359
  }
433
360
  }
434
-
435
361
  return cols
436
362
  }, [selectedPath, rootFolders, folders, rows])
437
363
 
438
364
  const selectedLeaf = selectedPath.length > 0 ? selectedPath.at(-1)! : null
439
- const selectedQuestion =
440
- selectedLeaf && isQuestion(selectedLeaf) ? (selectedLeaf as QuestionBankItem) : null
441
- const selectedFolderLeaf =
442
- selectedLeaf && isFolder(selectedLeaf) ? (selectedLeaf as QuestionBankFolder) : null
365
+ const selectedQuestion = selectedLeaf && isQuestion(selectedLeaf) ? (selectedLeaf as QuestionBankItem) : null
366
+ const selectedFolderLeaf = selectedLeaf && isFolder(selectedLeaf) ? (selectedLeaf as QuestionBankFolder) : null
443
367
 
444
368
  return (
445
- <ResizablePanelGroup
446
- direction="horizontal"
447
- className="flex h-full min-h-0 w-full flex-1 overflow-hidden"
448
- >
449
- {/* Render all columns with handles between them */}
369
+ <ResizablePanelGroup direction="horizontal" className="flex h-full min-h-0 w-full flex-1 overflow-hidden">
450
370
  {columns.map(({ items, depth, parentId }, columnIdx) => (
451
371
  <React.Fragment key={`col-${depth}`}>
452
- {columnIdx > 0 && (
453
- <ResizableHandle withHandle className={LIST_PAGE_SPLIT_RESIZABLE_HANDLE_CLASS} />
454
- )}
372
+ {columnIdx > 0 && <ResizableHandle withHandle className={LIST_PAGE_SPLIT_RESIZABLE_HANDLE_CLASS} />}
455
373
  <ResizablePanel
456
374
  id={`col-${depth}`}
457
375
  defaultSize={columnIdx === 0 ? 35 : columnIdx === 1 ? 35 : 30}
@@ -468,9 +386,7 @@ function HubFolderColumnsPanel({
468
386
  }
469
387
  trailing={
470
388
  <>
471
- <span className="shrink-0 text-xs font-medium text-muted-foreground tabular-nums">
472
- {items.length}
473
- </span>
389
+ <span className="shrink-0 text-xs font-medium text-muted-foreground tabular-nums">{items.length}</span>
474
390
  {depth < columns.length - 1 && items.length > 0 ? (
475
391
  <div className="flex shrink-0 items-center gap-0.5">
476
392
  <Tooltip>
@@ -508,38 +424,23 @@ function HubFolderColumnsPanel({
508
424
  </>
509
425
  }
510
426
  />
511
-
512
- {/* Scrollable Items List */}
513
427
  <div className="min-h-0 flex-1 overflow-y-auto py-1">
514
428
  {items.map(item => {
515
429
  const isSelected = selectedPath[depth]?.id === item.id
516
430
  const isFolder_ = isFolder(item)
517
431
  const folder = isFolder_ ? item : null
518
432
  const question = isQuestion(item) ? item : null
519
-
520
- // Get count for folders
521
- const subfolderCount = isFolder_
522
- ? folders.filter(f => f.parentId === item.id).length
523
- : 0
524
- const questionCount = isFolder_
525
- ? rows.filter(r => r.folderId === item.id).length
526
- : 0
433
+ const subfolderCount = isFolder_ ? folders.filter(f => f.parentId === item.id).length : 0
434
+ const questionCount = isFolder_ ? rows.filter(r => r.folderId === item.id).length : 0
527
435
  const itemCount = subfolderCount + questionCount
528
-
529
436
  return (
530
- <div
531
- key={item.id}
532
- className="group flex items-center hover:bg-muted/50"
533
- >
437
+ <div key={item.id} className="group flex items-center hover:bg-muted/50">
534
438
  <button
535
439
  onClick={() => handleSelect(item, depth)}
536
440
  className={cn(
537
441
  "flex flex-1 items-center gap-3 px-3 py-2 text-left text-sm transition-colors duration-75",
538
442
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
539
- isSelected
540
- ? "bg-accent text-accent-foreground"
541
- : "text-foreground",
542
- // Apply folder background color if it's a folder and not selected, but NOT in the first column
443
+ isSelected ? "bg-accent text-accent-foreground" : "text-foreground",
543
444
  isFolder_ && !isSelected && folder?.colorKey && depth > 0
544
445
  ? QUESTION_BANK_FOLDER_COLOR_STYLES[folder.colorKey]?.tile
545
446
  : "",
@@ -547,36 +448,34 @@ function HubFolderColumnsPanel({
547
448
  aria-selected={isSelected}
548
449
  role="option"
549
450
  >
550
- {/* Icon - show for folders and questions */}
551
451
  {isFolder_ ? (
552
- <i className={cn(
553
- "fa-folder text-sm shrink-0",
554
- isSelected ? "fa-solid" : "fa-light",
555
- // Apply folder color from customization (for both selected and unselected)
556
- folder?.colorKey && QUESTION_BANK_FOLDER_ICON_COLORS[folder.colorKey]
557
- )} aria-hidden="true" />
452
+ <i
453
+ className={cn(
454
+ "fa-folder text-sm shrink-0",
455
+ isSelected ? "fa-solid" : "fa-light",
456
+ folder?.colorKey && QUESTION_BANK_FOLDER_ICON_COLORS[folder.colorKey],
457
+ )}
458
+ aria-hidden="true"
459
+ />
558
460
  ) : (
559
461
  <i className={cn("fa-file text-sm shrink-0", isSelected ? "fa-solid" : "fa-light")} aria-hidden="true" />
560
462
  )}
561
-
562
- {/* Name */}
563
- <span className={cn(
564
- "min-w-0 flex-1 truncate leading-tight",
565
- isSelected && "font-medium"
566
- )}>
463
+ <span className={cn("min-w-0 flex-1 truncate leading-tight", isSelected && "font-medium")}>
567
464
  {isFolder_ ? folder?.name : question?.stem}
568
465
  </span>
569
-
570
- {/* Count or metadata */}
571
- <span className={cn(
572
- "shrink-0 tabular-nums text-xs ml-auto",
573
- isSelected ? "text-accent-foreground/70" : "text-muted-foreground",
574
- )}>
575
- {isFolder_ ? itemCount : (question?.type === 'multiple_choice' ? 'MCQ' : question?.difficulty?.charAt(0).toUpperCase())}
466
+ <span
467
+ className={cn(
468
+ "shrink-0 tabular-nums text-xs ms-auto",
469
+ isSelected ? "text-accent-foreground/70" : "text-muted-foreground",
470
+ )}
471
+ >
472
+ {isFolder_
473
+ ? itemCount
474
+ : question?.type === "multiple_choice"
475
+ ? "MCQ"
476
+ : question?.difficulty?.charAt(0).toUpperCase()}
576
477
  </span>
577
478
  </button>
578
-
579
- {/* Folder actions menu - only for folders */}
580
479
  {isFolder_ && folder && (
581
480
  <DropdownMenu>
582
481
  <DropdownMenuTrigger asChild>
@@ -605,8 +504,6 @@ function HubFolderColumnsPanel({
605
504
  </ResizablePanel>
606
505
  </React.Fragment>
607
506
  ))}
608
-
609
- {/* Details panel — question (summary) or folder (aggregates) */}
610
507
  {(selectedQuestion || selectedFolderLeaf) && (
611
508
  <>
612
509
  <ResizableHandle withHandle className={LIST_PAGE_SPLIT_RESIZABLE_HANDLE_CLASS} />
@@ -620,11 +517,7 @@ function HubFolderColumnsPanel({
620
517
  </>
621
518
  ) : selectedFolderLeaf ? (
622
519
  <div className="min-h-0 flex-1 overflow-hidden">
623
- <FolderDetailsShell
624
- folder={selectedFolderLeaf}
625
- folders={folders}
626
- questions={rows}
627
- />
520
+ <FolderDetailsShell folder={selectedFolderLeaf} folders={folders} questions={rows} />
628
521
  </div>
629
522
  ) : null}
630
523
  </ResizablePanel>
@@ -634,160 +527,10 @@ function HubFolderColumnsPanel({
634
527
  )
635
528
  }
636
529
 
637
- export type QuestionBankTableHandle = OpenTablePropertiesHandle
638
-
639
- export const QuestionBankTable = React.forwardRef<
640
- QuestionBankTableHandle,
641
- {
642
- items: QuestionBankItem[]
643
- /** When set, table / board / tree rows are limited to this nav scope (secondary sidebar). */
644
- navState?: QuestionBankNavState
645
- /** URL toolbar search binding (`?q=`) — omit on search landing so hub `q` does not pre-fill the grid search. */
646
- urlListSearch?: string
647
- /** When true, dedicated search shell: hub landing row filters; table toolbar search stays independent of URL `q`. */
648
- searchLanding?: boolean
649
- /** Applied with nav filters before `useTableState` when {@link searchLanding} is true. */
650
- landingFilters?: QuestionBankLandingFilterState | null
651
- view?: DataListViewType
652
- onViewChange?: (v: DataListViewType) => void
653
- folders: QuestionBankFolder[]
654
- onFoldersChange: React.Dispatch<React.SetStateAction<QuestionBankFolder[]>>
655
- onItemsChange: React.Dispatch<React.SetStateAction<QuestionBankItem[]>>
656
- }
657
- >(function QuestionBankTable(
658
- {
659
- items,
660
- navState,
661
- urlListSearch,
662
- searchLanding,
663
- landingFilters,
664
- view = "table",
665
- onViewChange,
666
- folders,
667
- onFoldersChange,
668
- onItemsChange,
669
- },
670
- ref,
671
- ) {
672
- const tableSourceItems = React.useMemo(() => {
673
- const nav = navState ?? { scope: "all" as const, folderId: null }
674
- const landing = searchLanding ? (landingFilters ?? null) : null
675
- return applyQuestionBankHubDisplayFilters(items, folders, nav, landing)
676
- }, [items, folders, navState, searchLanding, landingFilters])
677
-
678
- const toggleFavorite = React.useCallback(
679
- (row: QuestionBankItem) => {
680
- onItemsChange(prev => prev.map(r => (r.id === row.id ? toggleQuestionBankItemFavorite(r) : r)))
681
- },
682
- [onItemsChange],
683
- )
684
-
685
- const columns = React.useMemo(
686
- () => buildQuestionBankColumns(tableSourceItems, { onToggleFavorite: toggleFavorite }),
687
- [tableSourceItems, toggleFavorite],
688
- )
689
- const filterFields = React.useMemo(() => columnsToFilterFields(columns), [columns])
690
- const fieldDefinitionsForDrawer = React.useMemo(
691
- () =>
692
- columns
693
- .filter(c => c.key !== "select" && c.key !== "favorite" && c.key !== "actions")
694
- .map(c => ({ key: c.key, label: c.label, sortable: !!(c.sortable && (c.sortKey ?? c.key)) })),
695
- [columns],
696
- )
697
-
698
- const resolveColumnLabel = React.useCallback(
699
- (key: string) => columns.find(c => c.key === key)?.label ?? key,
700
- [columns],
701
- )
702
-
703
- const [displayOptions, setDisplayOptions] = React.useState<DataListDisplayOptions>(DEFAULT_DATA_LIST_DISPLAY_OPTIONS)
704
- const patchDisplay = React.useCallback((patch: Partial<DataListDisplayOptions>) => {
705
- setDisplayOptions(prev => ({ ...prev, ...patch }))
706
- }, [])
707
-
708
- const [newFolderOpen, setNewFolderOpen] = React.useState(false)
709
- const [newFolderParentId, setNewFolderParentId] = React.useState<string | null>(null)
710
- const [customizingFolder, setCustomizingFolder] = React.useState<QuestionBankFolder | null>(null)
711
-
712
- const [conditionalRules, setConditionalRules] = React.useState<ConditionalRule[]>([])
713
- const addConditionalRule = React.useCallback((rule: Omit<ConditionalRule, "id">) => {
714
- setConditionalRules(prev => [...prev, { ...rule, id: `cr-${Date.now()}` }])
715
- }, [])
716
- const removeConditionalRule = React.useCallback((id: string) => {
717
- setConditionalRules(prev => prev.filter(r => r.id !== id))
718
- }, [])
719
- const updateConditionalRule = React.useCallback((id: string, patch: Partial<ConditionalRule>) => {
720
- setConditionalRules(prev => prev.map(r => r.id === id ? { ...r, ...patch } : r))
721
- }, [])
722
-
723
- const tableState = useTableState(
724
- tableSourceItems,
725
- columns,
726
- { key: "updatedAt", dir: "desc" },
727
- undefined,
728
- searchLanding ? undefined : urlListSearch,
729
- )
730
-
731
- const openNewFolderForColumn = React.useCallback((parentId: string | null) => {
732
- setNewFolderParentId(parentId)
733
- setCustomizingFolder(null)
734
- setNewFolderOpen(true)
735
- }, [])
736
-
737
- const openCustomizeFolderSheet = React.useCallback((folder: QuestionBankFolder) => {
738
- setCustomizingFolder(folder)
739
- setNewFolderOpen(true)
740
- }, [])
741
-
742
- const addQuestionInColumn = React.useCallback(
743
- (parentId: string | null) => {
744
- const folderId = defaultFolderIdForColumnParent(parentId, folders)
745
- if (!folderId) return
746
- const today = new Date()
747
- const y = today.getFullYear()
748
- const m = String(today.getMonth() + 1).padStart(2, "0")
749
- const d = String(today.getDate()).padStart(2, "0")
750
- onItemsChange(prev => [
751
- ...prev,
752
- {
753
- id: newQuestionBankItemId(),
754
- questionId: newQuestionBankQuestionId(),
755
- stem: "New question",
756
- topic: "General",
757
- type: "short_answer",
758
- difficulty: "medium",
759
- author: "Demo user",
760
- authorEmail: "demo.user@demo.exxat.io",
761
- updatedAt: `${y}-${m}-${d}`,
762
- folderId,
763
- },
764
- ])
765
- },
766
- [folders, onItemsChange],
767
- )
768
-
769
- const renderFilterOptionValue = React.useCallback(
770
- (fieldKey: string, value: string): React.ReactNode => {
771
- const col = columns.find(c => c.key === fieldKey)
772
- const opt = col?.filter?.options?.find(o => o.value === value)
773
- return <span className="text-foreground">{opt?.label ?? value}</span>
774
- },
775
- [columns],
776
- )
777
-
778
- React.useImperativeHandle(ref, () => ({
779
- openPropertiesDrawer: () => {
780
- tableState.setSheetOpen(true)
781
- },
782
- }), [tableState])
783
-
784
- const questionBankBoardGroupKey = QUESTION_BANK_BOARD_GROUP_OPTIONS.some(
785
- o => o.key === displayOptions.boardGroupByColumnKey,
786
- )
787
- ? displayOptions.boardGroupByColumnKey
788
- : "topic"
530
+ // ─── Detail renderer reused by panel + tree-panel ───────────────────────────
789
531
 
790
- const panelRenderDetail = (row: QuestionBankItem) => (
532
+ function questionBankPanelDetail(row: QuestionBankItem) {
533
+ return (
791
534
  <div className="flex min-w-0 flex-col gap-4">
792
535
  <div>
793
536
  <h3 className="text-sm font-semibold text-foreground mb-2">Question</h3>
@@ -819,14 +562,11 @@ export const QuestionBankTable = React.forwardRef<
819
562
  <span className="text-xs font-medium text-muted-foreground mt-0.5 shrink-0">
820
563
  {String.fromCharCode(65 + idx)}.
821
564
  </span>
822
- <span className={cn(
823
- "text-sm",
824
- option.isCorrect ? "text-foreground font-medium" : "text-foreground/80"
825
- )}>
565
+ <span className={cn("text-sm", option.isCorrect ? "text-foreground font-medium" : "text-foreground/80")}>
826
566
  {option.text}
827
567
  </span>
828
568
  {option.isCorrect && (
829
- <i className="fa-light fa-check text-emerald-600 text-sm ml-auto shrink-0" aria-hidden="true" />
569
+ <i className="fa-light fa-check text-emerald-600 text-sm ms-auto shrink-0" aria-hidden="true" />
830
570
  )}
831
571
  </div>
832
572
  ))}
@@ -835,190 +575,232 @@ export const QuestionBankTable = React.forwardRef<
835
575
  )}
836
576
  </div>
837
577
  )
578
+ }
838
579
 
839
- const drawerToolbarProps = {
840
- totalRows: tableSourceItems.length,
841
- filterFields,
842
- fieldDefinitions: fieldDefinitionsForDrawer,
843
- resolveColumnLabel,
844
- displayOptions,
845
- onDisplayOptionsChange: patchDisplay,
846
- conditionalRules,
847
- onAddConditionalRule: addConditionalRule,
848
- onRemoveConditionalRule: removeConditionalRule,
849
- onUpdateConditionalRule: updateConditionalRule,
850
- currentView: view,
851
- onViewChange,
852
- lifecycleTabLabel: "Question bank",
853
- boardGroupByColumnOptions: [...QUESTION_BANK_BOARD_GROUP_OPTIONS],
854
- renderFilterOptionValue,
855
- }
580
+ // ─── Public component ───────────────────────────────────────────────────────
581
+
582
+ export type QuestionBankTableHandle = HubTableHandle
583
+
584
+ export interface QuestionBankTableProps {
585
+ items: QuestionBankItem[]
586
+ /** When set, table / board / tree rows are limited to this nav scope (secondary sidebar). */
587
+ navState?: QuestionBankNavState
588
+ /** URL toolbar search binding (`?q=`) — omit on search landing so hub `q` does not pre-fill the grid search. */
589
+ urlListSearch?: string
590
+ /** When true, dedicated search shell: hub landing row filters; table toolbar search stays independent of URL `q`. */
591
+ searchLanding?: boolean
592
+ /** Applied with nav filters before `useTableState` when {@link searchLanding} is true. */
593
+ landingFilters?: QuestionBankLandingFilterState | null
594
+ view?: DataListViewType
595
+ onViewChange?: (v: DataListViewType) => void
596
+ folders: QuestionBankFolder[]
597
+ onFoldersChange: React.Dispatch<React.SetStateAction<QuestionBankFolder[]>>
598
+ onItemsChange: React.Dispatch<React.SetStateAction<QuestionBankItem[]>>
599
+ }
856
600
 
857
- const tableProps = {
858
- data: tableSourceItems,
859
- columns,
860
- getRowId: (row: QuestionBankItem) => row.id,
861
- getRowSelectionLabel: (row: QuestionBankItem) => row.stem,
862
- selectable: true,
863
- searchable: displayOptions.showToolbarSearch,
864
- showColumnHeaders: displayOptions.showColumnLabels,
865
- groupable: true,
866
- defaultSort: { key: "updatedAt", dir: "desc" as const },
867
- emptyState: <p className="text-sm text-muted-foreground">No questions in the bank.</p>,
868
- conditionalRules,
869
- state: tableState,
870
- renderFilterOptionValue,
871
- toolbarSlot: (s: ReturnType<typeof useTableState<QuestionBankItem>>) => (
872
- <TablePropertiesDrawerButton {...drawerToolbarProps} state={s} />
873
- ),
874
- bulkActionsSlot: (selected: Set<string | number>) => {
875
- const n = selected.size
876
- if (n === 0) return null
877
- return (
878
- <>
879
- <span className="sr-only">{n} selected</span>
880
- <Tip label="Export selection (demo)">
881
- <Button size="sm" variant="outline" type="button">
882
- <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
883
- Export
884
- </Button>
885
- </Tip>
886
- </>
887
- )
601
+ export const QuestionBankTable = React.forwardRef<QuestionBankTableHandle, QuestionBankTableProps>(
602
+ function QuestionBankTable(
603
+ {
604
+ items,
605
+ navState,
606
+ urlListSearch,
607
+ searchLanding,
608
+ landingFilters,
609
+ view = "table",
610
+ onViewChange,
611
+ folders,
612
+ onFoldersChange,
613
+ onItemsChange,
888
614
  },
889
- }
890
-
891
- const sharedToolbar = (
892
- <DataTableToolbar
893
- state={tableState}
894
- columns={columns}
895
- searchable={displayOptions.showToolbarSearch}
896
- searchAriaLabel="Search questions"
897
- renderFilterOptionValue={renderFilterOptionValue}
898
- toolbarSlot={s => <TablePropertiesDrawerButton {...drawerToolbarProps} state={s} />}
899
- />
900
- )
901
-
902
- if (view === "table") {
903
- return (
904
- <div className="pb-6">
905
- <DataTable<QuestionBankItem> {...tableProps} />
906
- </div>
615
+ ref,
616
+ ) {
617
+ const tableSourceItems = React.useMemo(() => {
618
+ const nav = navState ?? { scope: "all" as const, folderId: null }
619
+ const landing = searchLanding ? (landingFilters ?? null) : null
620
+ return applyQuestionBankHubDisplayFilters(items, folders, nav, landing)
621
+ }, [items, folders, navState, searchLanding, landingFilters])
622
+
623
+ const toggleFavorite = React.useCallback(
624
+ (row: QuestionBankItem) => {
625
+ onItemsChange(prev => prev.map(r => (r.id === row.id ? toggleQuestionBankItemFavorite(r) : r)))
626
+ },
627
+ [onItemsChange],
907
628
  )
908
- }
909
629
 
910
- if (view === "list") {
911
- return (
912
- <div className="flex min-h-0 flex-1 flex-col">
913
- {sharedToolbar}
914
- <QuestionBankListView
915
- rows={tableState.rows as QuestionBankItem[]}
916
- onToggleFavorite={toggleFavorite}
917
- onRowActivate={row => tableState.toggleRow(row.id)}
918
- />
919
- </div>
630
+ const columns = React.useMemo(
631
+ () => buildQuestionBankColumns(tableSourceItems, { onToggleFavorite: toggleFavorite }),
632
+ [tableSourceItems, toggleFavorite],
920
633
  )
921
- }
922
634
 
923
- if (view === "board") {
924
- return (
925
- <div className="flex min-h-0 flex-1 flex-col">
926
- {sharedToolbar}
927
- <QuestionBankBoardView
928
- rows={tableState.rows as QuestionBankItem[]}
929
- groupByColumnKey={questionBankBoardGroupKey}
930
- onToggleFavorite={toggleFavorite}
931
- onRowActivate={row => tableState.toggleRow(row.id)}
932
- />
933
- </div>
635
+ // ─ New-folder / customize-folder modal state (shared by panel + tree-panel) ────
636
+ const [newFolderOpen, setNewFolderOpen] = React.useState(false)
637
+ const [newFolderParentId, setNewFolderParentId] = React.useState<string | null>(null)
638
+ const [customizingFolder, setCustomizingFolder] = React.useState<QuestionBankFolder | null>(null)
639
+
640
+ const openNewFolderForColumn = React.useCallback((parentId: string | null) => {
641
+ setNewFolderParentId(parentId)
642
+ setCustomizingFolder(null)
643
+ setNewFolderOpen(true)
644
+ }, [])
645
+
646
+ const openCustomizeFolderSheet = React.useCallback((folder: QuestionBankFolder) => {
647
+ setCustomizingFolder(folder)
648
+ setNewFolderOpen(true)
649
+ }, [])
650
+
651
+ const addQuestionInColumn = React.useCallback(
652
+ (parentId: string | null) => {
653
+ const folderId = defaultFolderIdForColumnParent(parentId, folders)
654
+ if (!folderId) return
655
+ const today = new Date()
656
+ const y = today.getFullYear()
657
+ const m = String(today.getMonth() + 1).padStart(2, "0")
658
+ const d = String(today.getDate()).padStart(2, "0")
659
+ onItemsChange(prev => [
660
+ ...prev,
661
+ {
662
+ id: newQuestionBankItemId(),
663
+ questionId: newQuestionBankQuestionId(),
664
+ stem: "New question",
665
+ topic: "General",
666
+ type: "short_answer",
667
+ difficulty: "medium",
668
+ author: "Demo user",
669
+ authorEmail: "demo.user@demo.exxat.io",
670
+ updatedAt: `${y}-${m}-${d}`,
671
+ folderId,
672
+ },
673
+ ])
674
+ },
675
+ [folders, onItemsChange],
934
676
  )
935
- }
936
677
 
937
- if (view === "folder") {
938
- return (
939
- <div className="flex min-h-0 flex-1 flex-col">
940
- {sharedToolbar}
941
- <QuestionBankOsFolderView
942
- folders={folders}
943
- onFoldersChange={onFoldersChange}
944
- questions={tableState.rows as QuestionBankItem[]}
945
- onQuestionsChange={onItemsChange}
946
- />
947
- </div>
678
+ const renderFilterOptionValue = React.useCallback(
679
+ (fieldKey: string, value: string): React.ReactNode => {
680
+ const col = columns.find(c => c.key === fieldKey)
681
+ const opt = col?.filter?.options?.find(o => o.value === value)
682
+ return <span className="text-foreground">{opt?.label ?? value}</span>
683
+ },
684
+ [columns],
948
685
  )
949
- }
950
686
 
951
- if (view === "panel") {
952
- return (
953
- <>
954
- <div className="flex min-h-0 flex-1 flex-col">
955
- {sharedToolbar}
687
+ // Renderers ──────────────────────────────────────────────────────────────
688
+ const renderers: HubTableRenderers<QuestionBankItem> = {
689
+ "board-with-toolbar": ({ state, toolbarShell, displayOptions }) => {
690
+ const boardGroupKey = QUESTION_BANK_BOARD_GROUP_OPTIONS.some(
691
+ o => o.key === displayOptions.boardGroupByColumnKey,
692
+ )
693
+ ? displayOptions.boardGroupByColumnKey
694
+ : "topic"
695
+ return toolbarShell(
696
+ <QuestionBankBoardView
697
+ rows={state.rows as QuestionBankItem[]}
698
+ groupByColumnKey={boardGroupKey}
699
+ onToggleFavorite={toggleFavorite}
700
+ onRowActivate={row => state.toggleRow(row.id)}
701
+ />,
702
+ )
703
+ },
704
+ "folder-with-toolbar": ({ state, toolbarShell }) =>
705
+ toolbarShell(
706
+ <QuestionBankOsFolderView
707
+ folders={folders}
708
+ onFoldersChange={onFoldersChange}
709
+ questions={state.rows as QuestionBankItem[]}
710
+ onQuestionsChange={onItemsChange}
711
+ />,
712
+ ),
713
+ "panel-with-toolbar": ({ state, toolbarShell }) =>
714
+ toolbarShell(
956
715
  <ListPageSplitHubChrome aria-label="Question bank folder columns">
957
716
  <HubFolderColumnsPanel
958
717
  folders={folders}
959
- rows={tableState.rows as QuestionBankItem[]}
960
- panelRenderDetail={panelRenderDetail}
718
+ rows={state.rows as QuestionBankItem[]}
719
+ panelRenderDetail={questionBankPanelDetail}
961
720
  onAddFolder={openNewFolderForColumn}
962
721
  onAddQuestion={addQuestionInColumn}
963
722
  onCustomizeFolder={openCustomizeFolderSheet}
964
723
  />
965
- </ListPageSplitHubChrome>
966
- </div>
967
- <QuestionBankNewFolderSheet
968
- open={newFolderOpen}
969
- onOpenChange={setNewFolderOpen}
970
- parentFolderId={customizingFolder?.parentId ?? newFolderParentId}
971
- customizingFolder={customizingFolder}
972
- onCreated={(newFolder) => {
973
- if (customizingFolder) {
974
- // Update existing folder
975
- onFoldersChange(prev =>
976
- prev.map(f =>
977
- f.id === customizingFolder.id
978
- ? {
979
- ...f,
980
- name: newFolder.name,
981
- icon: newFolder.icon,
982
- colorKey: newFolder.colorKey,
983
- }
984
- : f,
985
- ),
986
- )
987
- setCustomizingFolder(null)
988
- } else {
989
- // Create new folder
990
- onFoldersChange(prev => [
991
- ...prev,
992
- {
993
- id: `fld-${Date.now()}`,
994
- name: newFolder.name,
995
- icon: newFolder.icon,
996
- colorKey: newFolder.colorKey,
997
- parentId: newFolder.parentId,
998
- },
999
- ])
1000
- }
1001
- setNewFolderOpen(false)
1002
- }}
1003
- />
1004
- </>
1005
- )
1006
- }
1007
-
1008
- if (view === "tree-panel") {
1009
- return (
1010
- <>
1011
- <div className="flex min-h-0 flex-1 flex-col">
1012
- {sharedToolbar}
724
+ </ListPageSplitHubChrome>,
725
+ ),
726
+ "tree-panel-with-toolbar": ({ state, toolbarShell }) =>
727
+ toolbarShell(
1013
728
  <div className="flex min-h-0 flex-1 flex-col">
1014
729
  <HubTreePanelView
1015
- items={tableState.rows as QuestionBankItem[]}
730
+ items={state.rows as QuestionBankItem[]}
1016
731
  folders={folders}
1017
732
  onItemsChange={onItemsChange}
1018
733
  onFoldersChange={onFoldersChange}
1019
734
  />
1020
- </div>
735
+ </div>,
736
+ ),
737
+ "dashboard-with-toolbar": ({ state, toolbar }) => (
738
+ <div className="flex min-h-0 flex-1 flex-col">
739
+ {toolbar}
740
+ <QuestionBankDashboardChartsSection rows={state.rows as QuestionBankItem[]} />
1021
741
  </div>
742
+ ),
743
+ }
744
+
745
+ return (
746
+ <>
747
+ <HubTable<QuestionBankItem>
748
+ rows={tableSourceItems}
749
+ columns={columns}
750
+ view={view}
751
+ onViewChange={onViewChange}
752
+ supportedViewTypes={QUESTION_BANK_SUPPORTED_VIEWS}
753
+ hubLabel="Question bank"
754
+ lifecycleTabLabel="Question bank"
755
+ searchAriaLabel="Search questions"
756
+ getRowId={row => row.id}
757
+ getRowSelectionLabel={row => row.stem}
758
+ defaultSort={{ key: "updatedAt", dir: "desc" }}
759
+ emptyState={<p className="text-sm text-muted-foreground">No questions in the bank.</p>}
760
+ boardGroupByColumnOptions={[...QUESTION_BANK_BOARD_GROUP_OPTIONS]}
761
+ renderFilterOptionValue={renderFilterOptionValue}
762
+ syncedSearchFromUrl={searchLanding ? undefined : urlListSearch}
763
+ listAriaLabel="Questions"
764
+ listEmptyState="No questions match your filters."
765
+ renderListRow={row => (
766
+ <ListPageBoardCard
767
+ className={QUESTION_BANK_FAVORITE_HOVER_GROUP}
768
+ layout="row"
769
+ rowContainerClassName="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:gap-4"
770
+ rowEnd={
771
+ <div className="flex shrink-0 items-center gap-1">
772
+ <QuestionBankFavoriteButton row={row} onToggleFavorite={toggleFavorite} />
773
+ <i className="fa-light fa-chevron-right text-xs text-muted-foreground" aria-hidden="true" />
774
+ </div>
775
+ }
776
+ >
777
+ <div className="space-y-0.5">
778
+ <p className="line-clamp-2 text-sm font-semibold text-foreground">{row.stem}</p>
779
+ <p className="font-mono text-xs text-muted-foreground">{row.questionId}</p>
780
+ <p className="text-xs text-muted-foreground">
781
+ {row.topic} · Updated {formatDateUS(row.updatedAt)}
782
+ </p>
783
+ <p className="text-xs text-muted-foreground">{row.author}</p>
784
+ </div>
785
+ </ListPageBoardCard>
786
+ )}
787
+ bulkActionsSlot={selected => {
788
+ if (selected.size === 0) return null
789
+ return (
790
+ <>
791
+ <span className="sr-only">{selected.size} selected</span>
792
+ <Tip label="Export selection (demo)">
793
+ <Button size="sm" variant="outline" type="button">
794
+ <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
795
+ Export
796
+ </Button>
797
+ </Tip>
798
+ </>
799
+ )
800
+ }}
801
+ renderers={renderers}
802
+ handleRef={ref}
803
+ />
1022
804
  <QuestionBankNewFolderSheet
1023
805
  open={newFolderOpen}
1024
806
  onOpenChange={setNewFolderOpen}
@@ -1026,22 +808,15 @@ export const QuestionBankTable = React.forwardRef<
1026
808
  customizingFolder={customizingFolder}
1027
809
  onCreated={(newFolder) => {
1028
810
  if (customizingFolder) {
1029
- // Update existing folder
1030
811
  onFoldersChange(prev =>
1031
812
  prev.map(f =>
1032
813
  f.id === customizingFolder.id
1033
- ? {
1034
- ...f,
1035
- name: newFolder.name,
1036
- icon: newFolder.icon,
1037
- colorKey: newFolder.colorKey,
1038
- }
814
+ ? { ...f, name: newFolder.name, icon: newFolder.icon, colorKey: newFolder.colorKey }
1039
815
  : f,
1040
816
  ),
1041
817
  )
1042
818
  setCustomizingFolder(null)
1043
819
  } else {
1044
- // Create new folder
1045
820
  onFoldersChange(prev => [
1046
821
  ...prev,
1047
822
  {
@@ -1058,14 +833,7 @@ export const QuestionBankTable = React.forwardRef<
1058
833
  />
1059
834
  </>
1060
835
  )
1061
- }
1062
-
1063
- return (
1064
- <div className="flex min-h-0 flex-1 flex-col">
1065
- {sharedToolbar}
1066
- <QuestionBankDashboardChartsSection rows={tableState.rows as QuestionBankItem[]} />
1067
- </div>
1068
- )
1069
- })
836
+ },
837
+ )
1070
838
 
1071
839
  QuestionBankTable.displayName = "QuestionBankTable"