torch-glare 2.5.6 → 2.6.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 (362) hide show
  1. package/README.md +4 -3
  2. package/dist/bin/index.js +9 -24
  3. package/dist/bin/index.js.map +1 -1
  4. package/dist/src/commands/add.d.ts +2 -2
  5. package/dist/src/commands/add.d.ts.map +1 -1
  6. package/dist/src/commands/add.js +4 -70
  7. package/dist/src/commands/add.js.map +1 -1
  8. package/dist/src/commands/hook.d.ts +5 -4
  9. package/dist/src/commands/hook.d.ts.map +1 -1
  10. package/dist/src/commands/hook.js +7 -75
  11. package/dist/src/commands/hook.js.map +1 -1
  12. package/dist/src/commands/init.js +1 -1
  13. package/dist/src/commands/init.js.map +1 -1
  14. package/dist/src/commands/layout.d.ts +5 -4
  15. package/dist/src/commands/layout.d.ts.map +1 -1
  16. package/dist/src/commands/layout.js +7 -75
  17. package/dist/src/commands/layout.js.map +1 -1
  18. package/dist/src/commands/provider.d.ts +5 -4
  19. package/dist/src/commands/provider.d.ts.map +1 -1
  20. package/dist/src/commands/provider.js +7 -75
  21. package/dist/src/commands/provider.js.map +1 -1
  22. package/dist/src/commands/update.d.ts.map +1 -1
  23. package/dist/src/commands/update.js +78 -43
  24. package/dist/src/commands/update.js.map +1 -1
  25. package/dist/src/commands/utils.d.ts +5 -4
  26. package/dist/src/commands/utils.d.ts.map +1 -1
  27. package/dist/src/commands/utils.js +7 -80
  28. package/dist/src/commands/utils.js.map +1 -1
  29. package/dist/src/shared/addFromRegistry.d.ts +16 -0
  30. package/dist/src/shared/addFromRegistry.d.ts.map +1 -0
  31. package/dist/src/shared/addFromRegistry.js +88 -0
  32. package/dist/src/shared/addFromRegistry.js.map +1 -0
  33. package/dist/src/shared/getInstallCommand.d.ts.map +1 -1
  34. package/dist/src/shared/getInstallCommand.js +5 -0
  35. package/dist/src/shared/getInstallCommand.js.map +1 -1
  36. package/dist/src/shared/installDependencies.d.ts +0 -10
  37. package/dist/src/shared/installDependencies.d.ts.map +1 -1
  38. package/dist/src/shared/installDependencies.js +0 -17
  39. package/dist/src/shared/installDependencies.js.map +1 -1
  40. package/dist/src/shared/installFromPlan.d.ts +7 -5
  41. package/dist/src/shared/installFromPlan.d.ts.map +1 -1
  42. package/dist/src/shared/installFromPlan.js +103 -36
  43. package/dist/src/shared/installFromPlan.js.map +1 -1
  44. package/dist/src/shared/loadRegistry.d.ts +12 -3
  45. package/dist/src/shared/loadRegistry.d.ts.map +1 -1
  46. package/dist/src/shared/loadRegistry.js +19 -20
  47. package/dist/src/shared/loadRegistry.js.map +1 -1
  48. package/dist/src/shared/registryClient.d.ts +19 -0
  49. package/dist/src/shared/registryClient.d.ts.map +1 -0
  50. package/dist/src/shared/registryClient.js +128 -0
  51. package/dist/src/shared/registryClient.js.map +1 -0
  52. package/dist/src/shared/resolveEntry.d.ts +10 -7
  53. package/dist/src/shared/resolveEntry.d.ts.map +1 -1
  54. package/dist/src/shared/resolveEntry.js +18 -13
  55. package/dist/src/shared/resolveEntry.js.map +1 -1
  56. package/dist/src/shared/suggestOtherCommand.d.ts +7 -1
  57. package/dist/src/shared/suggestOtherCommand.d.ts.map +1 -1
  58. package/dist/src/shared/suggestOtherCommand.js +29 -23
  59. package/dist/src/shared/suggestOtherCommand.js.map +1 -1
  60. package/dist/src/types/main.d.ts +31 -2
  61. package/dist/src/types/main.d.ts.map +1 -1
  62. package/package.json +6 -11
  63. package/apps/lib/components/ActionButton.tsx +0 -93
  64. package/apps/lib/components/ActionsGroup.tsx +0 -27
  65. package/apps/lib/components/AlertDialog.tsx +0 -204
  66. package/apps/lib/components/Avatar.tsx +0 -46
  67. package/apps/lib/components/Badge.tsx +0 -250
  68. package/apps/lib/components/BadgeField.tsx +0 -403
  69. package/apps/lib/components/Breadcrumb.tsx +0 -280
  70. package/apps/lib/components/Button.tsx +0 -325
  71. package/apps/lib/components/ButtonGroup.tsx +0 -190
  72. package/apps/lib/components/Calendar.tsx +0 -115
  73. package/apps/lib/components/Card.tsx +0 -95
  74. package/apps/lib/components/Checkbox.tsx +0 -42
  75. package/apps/lib/components/ColorPicker.tsx +0 -441
  76. package/apps/lib/components/ConclusionHeader.tsx +0 -148
  77. package/apps/lib/components/ContextMenu.tsx +0 -571
  78. package/apps/lib/components/CountBadge.tsx +0 -52
  79. package/apps/lib/components/DataTable.tsx +0 -213
  80. package/apps/lib/components/DataViews/badge.ts +0 -49
  81. package/apps/lib/components/DataViews/cell.tsx +0 -324
  82. package/apps/lib/components/DataViews/context.ts +0 -144
  83. package/apps/lib/components/DataViews/data-views.tsx +0 -395
  84. package/apps/lib/components/DataViews/filters/children.tsx +0 -98
  85. package/apps/lib/components/DataViews/filters/custom.tsx +0 -34
  86. package/apps/lib/components/DataViews/filters/filters.tsx +0 -157
  87. package/apps/lib/components/DataViews/filters/index.ts +0 -4
  88. package/apps/lib/components/DataViews/filters/labelled.tsx +0 -20
  89. package/apps/lib/components/DataViews/filters/presets.tsx +0 -65
  90. package/apps/lib/components/DataViews/filters/sync.tsx +0 -35
  91. package/apps/lib/components/DataViews/filters/values.ts +0 -173
  92. package/apps/lib/components/DataViews/header.tsx +0 -220
  93. package/apps/lib/components/DataViews/hooks/index.ts +0 -5
  94. package/apps/lib/components/DataViews/hooks/useActiveRow.ts +0 -22
  95. package/apps/lib/components/DataViews/hooks/useControllable.ts +0 -52
  96. package/apps/lib/components/DataViews/index.ts +0 -88
  97. package/apps/lib/components/DataViews/panel/columns.tsx +0 -153
  98. package/apps/lib/components/DataViews/panel/controls.tsx +0 -106
  99. package/apps/lib/components/DataViews/panel/index.ts +0 -3
  100. package/apps/lib/components/DataViews/panel/panel.tsx +0 -164
  101. package/apps/lib/components/DataViews/panel/saved-views.tsx +0 -67
  102. package/apps/lib/components/DataViews/panel/section.tsx +0 -81
  103. package/apps/lib/components/DataViews/panel/sort.tsx +0 -42
  104. package/apps/lib/components/DataViews/panel/tab.tsx +0 -31
  105. package/apps/lib/components/DataViews/slots.ts +0 -72
  106. package/apps/lib/components/DataViews/states.tsx +0 -73
  107. package/apps/lib/components/DataViews/types.ts +0 -510
  108. package/apps/lib/components/DataViews/views/board-view.tsx +0 -381
  109. package/apps/lib/components/DataViews/views/card-rows.tsx +0 -36
  110. package/apps/lib/components/DataViews/views/inbox-view.tsx +0 -260
  111. package/apps/lib/components/DataViews/views/pane-views.tsx +0 -196
  112. package/apps/lib/components/DataViews/views/table-view.tsx +0 -493
  113. package/apps/lib/components/DataViews/views/tree-view.tsx +0 -367
  114. package/apps/lib/components/DatePicker.tsx +0 -232
  115. package/apps/lib/components/Dialog.tsx +0 -116
  116. package/apps/lib/components/Divider.tsx +0 -28
  117. package/apps/lib/components/Drawer.tsx +0 -451
  118. package/apps/lib/components/DropdownMenu.tsx +0 -554
  119. package/apps/lib/components/FieldHint.tsx +0 -69
  120. package/apps/lib/components/Form.tsx +0 -182
  121. package/apps/lib/components/FormBuilder/context.ts +0 -85
  122. package/apps/lib/components/FormBuilder/field-kind.ts +0 -28
  123. package/apps/lib/components/FormBuilder/fields/ChoiceFields.tsx +0 -60
  124. package/apps/lib/components/FormBuilder/fields/ColorField.tsx +0 -59
  125. package/apps/lib/components/FormBuilder/fields/CustomField.tsx +0 -11
  126. package/apps/lib/components/FormBuilder/fields/DateField.tsx +0 -32
  127. package/apps/lib/components/FormBuilder/fields/FieldArray.tsx +0 -74
  128. package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +0 -168
  129. package/apps/lib/components/FormBuilder/fields/FileField.tsx +0 -39
  130. package/apps/lib/components/FormBuilder/fields/OptionListFields.tsx +0 -132
  131. package/apps/lib/components/FormBuilder/fields/OtpField.tsx +0 -31
  132. package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +0 -109
  133. package/apps/lib/components/FormBuilder/fields/RichTextEditorField.tsx +0 -31
  134. package/apps/lib/components/FormBuilder/fields/SelectField.tsx +0 -114
  135. package/apps/lib/components/FormBuilder/fields/SignatureField.tsx +0 -157
  136. package/apps/lib/components/FormBuilder/fields/SliderField.tsx +0 -67
  137. package/apps/lib/components/FormBuilder/fields/SwitchBoxField.tsx +0 -41
  138. package/apps/lib/components/FormBuilder/fields/TableField.tsx +0 -336
  139. package/apps/lib/components/FormBuilder/fields/TextField.tsx +0 -213
  140. package/apps/lib/components/FormBuilder/fields/TreeSelectField.tsx +0 -43
  141. package/apps/lib/components/FormBuilder/fields/countries.ts +0 -303
  142. package/apps/lib/components/FormBuilder/fields/index.ts +0 -25
  143. package/apps/lib/components/FormBuilder/form-builder.tsx +0 -198
  144. package/apps/lib/components/FormBuilder/index.ts +0 -31
  145. package/apps/lib/components/FormBuilder/numberFormat.ts +0 -16
  146. package/apps/lib/components/FormBuilder/submit.tsx +0 -67
  147. package/apps/lib/components/FormBuilder/types.ts +0 -316
  148. package/apps/lib/components/FormRenderer/FormDrawer.tsx +0 -250
  149. package/apps/lib/components/FormRenderer/detail.tsx +0 -256
  150. package/apps/lib/components/FormRenderer/form-renderer.tsx +0 -358
  151. package/apps/lib/components/FormRenderer/header.tsx +0 -88
  152. package/apps/lib/components/FormRenderer/index.ts +0 -17
  153. package/apps/lib/components/FormRenderer/notch-action.tsx +0 -64
  154. package/apps/lib/components/FormRenderer/section.tsx +0 -39
  155. package/apps/lib/components/FormRenderer/stepper.tsx +0 -393
  156. package/apps/lib/components/FormRenderer/types.ts +0 -115
  157. package/apps/lib/components/FormSummary.tsx +0 -282
  158. package/apps/lib/components/HeaderBar.tsx +0 -117
  159. package/apps/lib/components/ImageAttachment.tsx +0 -209
  160. package/apps/lib/components/InnerLabelField.tsx +0 -154
  161. package/apps/lib/components/Input.tsx +0 -224
  162. package/apps/lib/components/InputField.tsx +0 -147
  163. package/apps/lib/components/InputOTP.tsx +0 -84
  164. package/apps/lib/components/Label.tsx +0 -129
  165. package/apps/lib/components/LabelField.tsx +0 -76
  166. package/apps/lib/components/LabeledCheckBox.tsx +0 -51
  167. package/apps/lib/components/LabeledRadio.tsx +0 -51
  168. package/apps/lib/components/LinkButton.tsx +0 -94
  169. package/apps/lib/components/LoginButton.tsx +0 -51
  170. package/apps/lib/components/PasswordLevel.tsx +0 -58
  171. package/apps/lib/components/Popover.tsx +0 -312
  172. package/apps/lib/components/ProfileMenu.tsx +0 -85
  173. package/apps/lib/components/Radio.tsx +0 -59
  174. package/apps/lib/components/RadioCard.tsx +0 -50
  175. package/apps/lib/components/ScrollArea.tsx +0 -54
  176. package/apps/lib/components/SearchField.tsx +0 -62
  177. package/apps/lib/components/SearchableSelect.tsx +0 -318
  178. package/apps/lib/components/SearchableTable.tsx +0 -344
  179. package/apps/lib/components/SearchableTree.tsx +0 -485
  180. package/apps/lib/components/SearchableTreeDialog.tsx +0 -502
  181. package/apps/lib/components/SectionBlock.tsx +0 -168
  182. package/apps/lib/components/Select.tsx +0 -347
  183. package/apps/lib/components/SimpleSelect.tsx +0 -225
  184. package/apps/lib/components/Skeleton.tsx +0 -12
  185. package/apps/lib/components/SlideDatePicker.tsx +0 -255
  186. package/apps/lib/components/SpinLoading.tsx +0 -174
  187. package/apps/lib/components/Stepper.tsx +0 -480
  188. package/apps/lib/components/Switch.tsx +0 -82
  189. package/apps/lib/components/TabFormItem.tsx +0 -153
  190. package/apps/lib/components/TabSwitch.tsx +0 -172
  191. package/apps/lib/components/Table.tsx +0 -597
  192. package/apps/lib/components/TextEditor/ChartBlockTool.ts +0 -676
  193. package/apps/lib/components/TextEditor/RichTextField.tsx +0 -46
  194. package/apps/lib/components/TextEditor/TableDnDWrapper.ts +0 -462
  195. package/apps/lib/components/TextEditor/TextEditor.tsx +0 -1164
  196. package/apps/lib/components/TextEditor/TextEditorToolbar.tsx +0 -429
  197. package/apps/lib/components/TextEditor/editor-tools/AlignmentTune.ts +0 -70
  198. package/apps/lib/components/TextEditor/editor-tools/ColorInlineTool.ts +0 -50
  199. package/apps/lib/components/TextEditor/editor-tools/StrikethroughInlineTool.ts +0 -48
  200. package/apps/lib/components/TextEditor/editor-tools/inlineFormat.ts +0 -98
  201. package/apps/lib/components/TextEditor/editorjs.d.ts +0 -60
  202. package/apps/lib/components/TextEditor/index.ts +0 -7
  203. package/apps/lib/components/TextEditor/markdownParser.ts +0 -346
  204. package/apps/lib/components/Textarea.tsx +0 -117
  205. package/apps/lib/components/Timeline.tsx +0 -263
  206. package/apps/lib/components/Toast.tsx +0 -87
  207. package/apps/lib/components/Toggle.tsx +0 -125
  208. package/apps/lib/components/ToggleButton.tsx +0 -140
  209. package/apps/lib/components/Tooltip.tsx +0 -116
  210. package/apps/lib/components/TransparentLabel.tsx +0 -71
  211. package/apps/lib/components/TreeDropDown.tsx +0 -115
  212. package/apps/lib/components/TreeFolder/TreeFolder.tsx +0 -380
  213. package/apps/lib/components/TreeFolder/TreeFolderBreadcrumb.tsx +0 -75
  214. package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +0 -354
  215. package/apps/lib/components/TreeFolder/TreeFolderStyles.tsx +0 -60
  216. package/apps/lib/components/TreeFolder/icons.tsx +0 -60
  217. package/apps/lib/components/TreeFolder/index.ts +0 -24
  218. package/apps/lib/components/TreeFolder/treeFolderUtils.ts +0 -109
  219. package/apps/lib/components/TreeFolder/types.ts +0 -77
  220. package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +0 -118
  221. package/apps/lib/hooks/useActiveTreeItem.ts +0 -61
  222. package/apps/lib/hooks/useClickOutside.ts +0 -27
  223. package/apps/lib/hooks/useDragDrop.tsx +0 -365
  224. package/apps/lib/hooks/useHtmlDir.ts +0 -31
  225. package/apps/lib/hooks/useInfiniteScroll.ts +0 -108
  226. package/apps/lib/hooks/useIsMobile.ts +0 -21
  227. package/apps/lib/hooks/useResize.ts +0 -67
  228. package/apps/lib/hooks/useTagSelection.ts +0 -300
  229. package/apps/lib/layouts/CNLayout.tsx +0 -372
  230. package/apps/lib/layouts/DataViewCard.tsx +0 -62
  231. package/apps/lib/layouts/FieldSection.tsx +0 -91
  232. package/apps/lib/layouts/TreeSubLayout.tsx +0 -173
  233. package/apps/lib/providers/ThemeProvider.tsx +0 -84
  234. package/apps/lib/registry.json +0 -1217
  235. package/apps/lib/tsconfig.json +0 -9
  236. package/apps/lib/tsconfig.tsbuildinfo +0 -1
  237. package/apps/lib/utils/cn.ts +0 -6
  238. package/apps/lib/utils/color.ts +0 -175
  239. package/apps/lib/utils/dataViews/path.ts +0 -67
  240. package/apps/lib/utils/dataViews/query.ts +0 -73
  241. package/apps/lib/utils/dataViews/types.ts +0 -187
  242. package/apps/lib/utils/dateFormat.ts +0 -108
  243. package/apps/lib/utils/resize.ts +0 -31
  244. package/apps/lib/utils/scroller.ts +0 -26
  245. package/apps/lib/utils/types.ts +0 -16
  246. package/docs/BLOCKS.md +0 -129
  247. package/docs/CHANGELOG-1.1.16.md +0 -108
  248. package/docs/Cover.png +0 -0
  249. package/docs/README.md +0 -215
  250. package/docs/components/action-button.md +0 -638
  251. package/docs/components/actions-group.md +0 -708
  252. package/docs/components/alert-dialog.md +0 -920
  253. package/docs/components/avatar.md +0 -709
  254. package/docs/components/badge-field.md +0 -738
  255. package/docs/components/badge.md +0 -301
  256. package/docs/components/breadcrumb.md +0 -930
  257. package/docs/components/button-group.md +0 -650
  258. package/docs/components/button.md +0 -639
  259. package/docs/components/calendar.md +0 -555
  260. package/docs/components/card.md +0 -622
  261. package/docs/components/chart-block-tool.md +0 -129
  262. package/docs/components/checkbox.md +0 -699
  263. package/docs/components/cn-layout.md +0 -854
  264. package/docs/components/color-picker.md +0 -101
  265. package/docs/components/conclusion-header.md +0 -80
  266. package/docs/components/context-menu.md +0 -483
  267. package/docs/components/count-badge.md +0 -560
  268. package/docs/components/data-table.md +0 -854
  269. package/docs/components/data-views/backend-response.md +0 -324
  270. package/docs/components/data-views/examples/a11y-rtl.md +0 -250
  271. package/docs/components/data-views/examples/api-orders-route.md +0 -130
  272. package/docs/components/data-views/examples/fields.md +0 -362
  273. package/docs/components/data-views/examples/filters.md +0 -307
  274. package/docs/components/data-views/examples/inbox-routing.md +0 -218
  275. package/docs/components/data-views/examples/index.md +0 -29
  276. package/docs/components/data-views/examples/overview.md +0 -244
  277. package/docs/components/data-views/examples/panel.md +0 -212
  278. package/docs/components/data-views/examples/scale.md +0 -231
  279. package/docs/components/data-views/examples/server-side.md +0 -210
  280. package/docs/components/data-views/examples/state.md +0 -250
  281. package/docs/components/data-views/examples/tree-custom.md +0 -388
  282. package/docs/components/data-views/examples/view-registry.md +0 -313
  283. package/docs/components/data-views/examples/views.md +0 -534
  284. package/docs/components/data-views/guide.md +0 -405
  285. package/docs/components/data-views/index.md +0 -1521
  286. package/docs/components/data-views/migration.md +0 -81
  287. package/docs/components/date-picker.md +0 -857
  288. package/docs/components/dialog.md +0 -929
  289. package/docs/components/divider.md +0 -766
  290. package/docs/components/drawer.md +0 -654
  291. package/docs/components/dropdown-menu.md +0 -668
  292. package/docs/components/field-hint.md +0 -898
  293. package/docs/components/field-section.md +0 -781
  294. package/docs/components/form-builder.md +0 -284
  295. package/docs/components/form-renderer.md +0 -396
  296. package/docs/components/form-summary.md +0 -138
  297. package/docs/components/form.md +0 -785
  298. package/docs/components/header-bar.md +0 -188
  299. package/docs/components/image-attachment.md +0 -906
  300. package/docs/components/inner-label-field.md +0 -819
  301. package/docs/components/input-field.md +0 -623
  302. package/docs/components/input-otp.md +0 -756
  303. package/docs/components/input.md +0 -634
  304. package/docs/components/label-field.md +0 -770
  305. package/docs/components/label.md +0 -761
  306. package/docs/components/labeled-check-box.md +0 -642
  307. package/docs/components/labeled-radio.md +0 -796
  308. package/docs/components/link-button.md +0 -685
  309. package/docs/components/login-button.md +0 -872
  310. package/docs/components/password-level.md +0 -803
  311. package/docs/components/popover.md +0 -649
  312. package/docs/components/profile-menu.md +0 -656
  313. package/docs/components/radio-card.md +0 -801
  314. package/docs/components/radio.md +0 -735
  315. package/docs/components/scroll-area.md +0 -691
  316. package/docs/components/search-field.md +0 -702
  317. package/docs/components/searchable-select.md +0 -368
  318. package/docs/components/searchable-table.md +0 -438
  319. package/docs/components/searchable-tree-dialog.md +0 -190
  320. package/docs/components/searchable-tree.md +0 -200
  321. package/docs/components/section-block.md +0 -483
  322. package/docs/components/select.md +0 -709
  323. package/docs/components/simple-select.md +0 -741
  324. package/docs/components/skeleton.md +0 -706
  325. package/docs/components/slide-date-picker.md +0 -837
  326. package/docs/components/spin-loading.md +0 -696
  327. package/docs/components/stepper.md +0 -316
  328. package/docs/components/switch.md +0 -770
  329. package/docs/components/tab-form-item.md +0 -773
  330. package/docs/components/tab-switch.md +0 -170
  331. package/docs/components/table-dnd-wrapper.md +0 -107
  332. package/docs/components/table.md +0 -1088
  333. package/docs/components/text-editor.md +0 -735
  334. package/docs/components/textarea.md +0 -681
  335. package/docs/components/timeline.md +0 -254
  336. package/docs/components/toast.md +0 -773
  337. package/docs/components/toggle-button.md +0 -643
  338. package/docs/components/toggle.md +0 -882
  339. package/docs/components/tooltip.md +0 -513
  340. package/docs/components/transparent-label.md +0 -721
  341. package/docs/components/tree-drop-down.md +0 -740
  342. package/docs/components/tree-folder.md +0 -110
  343. package/docs/components/tree-sub-layout.md +0 -707
  344. package/docs/explanation/architecture.md +0 -54
  345. package/docs/explanation/design-system.md +0 -52
  346. package/docs/how-to/form-and-list-recipes.md +0 -386
  347. package/docs/how-to/forms-with-form-builder.md +0 -472
  348. package/docs/how-to/guides.md +0 -1027
  349. package/docs/migration/changelog.md +0 -60
  350. package/docs/migration/form-builder-2.5.2.md +0 -113
  351. package/docs/reference/cli.md +0 -112
  352. package/docs/reference/components.md +0 -572
  353. package/docs/reference/hooks.md +0 -1523
  354. package/docs/reference/providers.md +0 -866
  355. package/docs/reference/tailwind-plugins.md +0 -942
  356. package/docs/reference/theme.md +0 -85
  357. package/docs/reference/types.md +0 -1016
  358. package/docs/reference/utilities.md +0 -726
  359. package/docs/tutorials/building-first-form.md +0 -846
  360. package/docs/tutorials/component-composition.md +0 -882
  361. package/docs/tutorials/getting-started.md +0 -302
  362. package/docs/tutorials/theming-basics.md +0 -785
@@ -1,1521 +0,0 @@
1
- ---
2
- title: DataViews
3
- description: One dataset shown as a table, kanban board, inbox or tree — with a shared header, view switcher, filters, settings rail, scroll loading and drag-and-drop.
4
- group: Data Display
5
- keywords: [data-views, dataviews, table, board, kanban, inbox, tree, views, filters, panel, columns, saved-views, infinite-scroll, virtualization, drag-drop, server-side]
6
- ---
7
-
8
- # DataViews
9
-
10
- > One dataset, several ways to look at it — table, board, tree, inbox — behind a shared header,
11
- > filter set and settings rail. It is pure UI: you query, it paints.
12
-
13
- ## Installation
14
-
15
- TORCH Glare is a copy-in library: the CLI copies this component's source into your project
16
- (you do **not** install it from the npm package). Run `init` once, then `add`:
17
-
18
- ```bash
19
- npx torch-glare@latest init
20
- npx torch-glare@latest add DataViews
21
- ```
22
-
23
- `DataViews` is a folder component — the CLI copies the whole directory, plus everything it depends
24
- on: `Table`, `Checkbox`, `Badge`, `Button`, `Switch`, `Divider`, `Skeleton`, `TabSwitch`, `Avatar`,
25
- `FormBuilder`, `TreeFolder`, the `DataViewCard` layout, the `useDragDrop` and `useInfiniteScroll`
26
- hooks, and the `dataViews` utilities.
27
-
28
- ## Import
29
-
30
- Import from your project's local path — the alias configured in `glare.json` (e.g. `@/*`).
31
- **One path covers everything**: the component, the hooks, the query helpers, and the types you
32
- construct.
33
-
34
- ```tsx
35
- import {
36
- DataViews, // the compound root — every part hangs off it
37
- emptyQuery, queryToParams, parseQuery, // the query's wire format, both directions
38
- useDataViewsData, useDataViewsView, // read the component's state from your own part
39
- useDataViewsFilters, useDataViewsPanel, useDataViewsPanelTabs,
40
- useActiveRow, // the row behind `activeId`
41
- Cell, // paint one field the way the views paint it
42
- markView, markHeader, markPanel, markEmpty, // register a part of your own
43
- SkeletonBar, skeletonKeys, // the loading pieces every view is built from
44
- getByPath, formatPathLabel, defaultGetRowId, // read a value by dotted path
45
- buildCardRows, resolveBadgeVariant,
46
- } from "@/components/DataViews";
47
-
48
- import type {
49
- Row, FieldConfig, FieldType, DataViewsQuery, // the shapes you construct
50
- RowGroup, TreeNode, MoveIntent, Sort, ColumnState,
51
- FilterState, FilterValue, Preset, SavedView,
52
- } from "@/components/DataViews";
53
- ```
54
-
55
- Two things come from elsewhere, because they are not DataViews' own:
56
-
57
- ```tsx
58
- import { DataViewCard } from "@/layouts/DataViewCard"; // the card the board and pane paint
59
- import { useInfiniteScroll } from "@/hooks/useInfiniteScroll";
60
- import { useDragDrop } from "@/hooks/useDragDrop";
61
- ```
62
-
63
- ## Quick Examples
64
-
65
- Four screens, each a complete page that ships with the docs. Start from
66
- [`overview`](./examples/overview.md) — it is every part at once.
67
-
68
- ```tsx
69
- const [query, setQuery] = useState(emptyQuery());
70
- const { data, isPending } = useQuery({
71
- queryKey: ["orders", query],
72
- queryFn: () => fetch(`/api/orders?${queryToParams(query)}`).then((r) => r.json()),
73
- });
74
-
75
- <DataViews
76
- rows={data?.rows ?? []}
77
- total={data?.total ?? 0}
78
- fields={fields}
79
- loading={isPending}
80
- onQueryChange={setQuery}
81
- >
82
- <DataViews.Header title="Orders">
83
- <DataViews.ViewSwitch />
84
- <DataViews.Search />
85
- <DataViews.PanelToggle />
86
- </DataViews.Header>
87
-
88
- <DataViews.Table selectable />
89
- <DataViews.Board groups={groups} onRowMove={move} />
90
-
91
- <DataViews.Panel>
92
- <DataViews.Panel.Tab value="config" label="Config." icon={<Settings />}>
93
- <DataViews.Panel.SavedViews views={saved} onSave={persist} />
94
- <DataViews.Panel.Columns />
95
- <DataViews.Panel.Sort />
96
- </DataViews.Panel.Tab>
97
- <DataViews.Panel.Tab value="filters" label="Filters" icon={<Filter />}>
98
- <DataViews.Filters title={null} className="border-b-0 p-0">
99
- <FormBuilder.MultiSelect name="status" label="Status" options={STATUS} />
100
- <FormBuilder.Slider name="total" label="Total" range min={0} max={15000} />
101
- </DataViews.Filters>
102
- </DataViews.Panel.Tab>
103
- </DataViews.Panel>
104
- </DataViews>
105
- ```
106
-
107
- ## The three rules
108
-
109
- **A part exists because you rendered it.** There is no `views={{ table: true }}` map and no
110
- `showFilters` flag. Render `<DataViews.Tree/>` and a Tree tab appears; wrap it in
111
- `{canSeeTree && …}` and it disappears, switching away from it if it was open. The same goes for
112
- panel tabs.
113
-
114
- **It is pure UI.** DataViews never filters, searches, sorts, groups, paginates, builds a tree,
115
- infers a schema or mutates a row. It paints the `rows` you hand it, in the order you hand them.
116
- Nothing on screen moves until you hand back different data — which is the point: a drag that fails
117
- to save leaves the board showing the truth. So you supply `rows` (already queried), `total`,
118
- `fields`, filter options and slider bounds, board `groups` and tree `nodes`.
119
-
120
- **Only the query leaves.** Search, filters, sort, page and page size are one object reported
121
- through `onQueryChange`, because they are one question — and because a query with a new filter and
122
- a stale page number is not a query anyone meant to ask. Everything else the user can change (which
123
- view is showing, which tab is open, how columns are arranged, what is selected, which row is open)
124
- changes nothing but the picture, so the component keeps it.
125
-
126
- | The user… | You get |
127
- | --- | --- |
128
- | types, filters, sorts | `onQueryChange(query)` → go and fetch |
129
- | reaches the end of a list | `onLoadMore()` → fetch the next page and append it |
130
- | drags a card, a row or a node | `onRowMove` / `onNodeMove` → save, hand back updated rows |
131
- | selects rows / opens one | `onSelectionChange` / `onActiveIdChange` — told, not asked |
132
-
133
- Changing a filter resets `page` to 1 internally. Only the component can do that: by the time you
134
- see the change, "new filter" and "new page" have already become one object.
135
-
136
- ## Empty and loading
137
-
138
- **Loading is not a part you render.** While `loading` is set, each view paints a **skeleton in its
139
- own shape**: the table shimmers rows at the real row height and column widths, the board shimmers
140
- cards inside its columns.
141
-
142
- **Empty is opt-in.** By default the view simply shows nothing — the table keeps its header band and
143
- has no rows, the board keeps its columns and has no cards. That default is deliberate: a centred
144
- message in place of the view throws away the chrome, and it cannot tell "no results" from "not
145
- fetched yet".
146
-
147
- When you do want something there, render `DataViews.Empty` anywhere among the children. It is a
148
- passthrough with a marker — it holds no opinion about what an empty state looks like, it only tells
149
- the root to put its content where the view goes. The root swaps it in when the query has settled and
150
- returned nothing (`!loading && rows.length === 0`):
151
-
152
- ```tsx
153
- <DataViews rows={rows} fields={fields} loading={loading}>
154
- <DataViews.Table />
155
- <DataViews.Empty>
156
- <div className="flex flex-1 flex-col items-center justify-center gap-2">
157
- <p>No invoices match these filters.</p>
158
- <Button onClick={clearFilters}>Clear filters</Button>
159
- </div>
160
- </DataViews.Empty>
161
- </DataViews>
162
- ```
163
-
164
- Your content is what receives the body slot's height, so give it `flex-1` if it should centre.
165
- Wrapping `DataViews.Empty` in a component of your own? Mark that wrapper with `markEmpty` so the
166
- root still recognises it, the same way `markView` / `markHeader` / `markPanel` work.
167
-
168
- ## Large datasets
169
-
170
- Rows load **as you scroll**; there is no pager. Hand over `onLoadMore` and append each page to
171
- `rows`. Whether there is more is not a prop — it is `rows.length < total`, which the component
172
- already knows.
173
-
174
- ```tsx
175
- const { data, fetchNextPage, isPending, isFetchingNextPage } = useInfiniteQuery({
176
- queryKey: ["orders", { ...query, page: undefined }], // not `page` — that is what pageParam drives
177
- initialPageParam: 1,
178
- queryFn: ({ pageParam }) =>
179
- fetch(`/api/orders?${queryToParams({ ...query, page: pageParam })}`).then((r) => r.json()),
180
- getNextPageParam: (last, pages) => {
181
- const loaded = pages.reduce((n, p) => n + p.rows.length, 0);
182
- return loaded < last.total ? pages.length + 1 : undefined;
183
- },
184
- });
185
-
186
- <DataViews
187
- rows={data?.pages.flatMap((p) => p.rows) ?? []}
188
- total={data?.pages[0]?.total ?? 0}
189
- loading={isPending}
190
- onLoadMore={fetchNextPage}
191
- loadingMore={isFetchingNextPage}
192
- fields={fields}
193
- onQueryChange={setQuery}
194
- />
195
- ```
196
-
197
- The table also **virtualizes past 300 rows** — below that it renders every row, so small tables,
198
- row drag and column resize behave exactly as before; above it the DOM holds a window of rows
199
- however many are loaded. The board and inbox load on scroll but do not virtualize. The **tree does
200
- neither**: a tree wants its children fetched when a node is expanded, not its siblings paged in,
201
- and that is not built.
202
-
203
- ## Views
204
-
205
- | Part | What it renders | You supply | Runnable |
206
- | --- | --- | --- | --- |
207
- | `DataViews.Table` | rows and columns, virtualized past 300 | nothing beyond `rows` | [`views`](./examples/views.md) |
208
- | `DataViews.Board` | a kanban board | `groups` — it never groups rows itself | [`views`](./examples/views.md) |
209
- | `DataViews.Inbox` | a master list beside a detail pane | the pane, as `children` | [`inbox-routing`](./examples/inbox-routing.md) |
210
- | `DataViews.Tree` | a hierarchy, optionally beside a pane | `nodes` — it never builds one | [`tree-custom`](./examples/tree-custom.md) |
211
- | `DataViews.Empty` | your content, in place of the view, once a settled query returns no rows | the content — it holds no opinion about what empty looks like | — |
212
-
213
- Each takes `id`, `label` and `icon` to control how it appears in the switcher, so the same view can
214
- be registered twice with different data. Full props are under
215
- [API Reference](#api-reference) — one heading per part.
216
-
217
- Two things `DataViews.Table` does for you that you would otherwise wire by hand: its column header
218
- **stays put while the rows scroll under it**, and the view draws **its own border and radius**, so it
219
- reads as a separated surface like the inbox and tree panels rather than filling the shell edge to
220
- edge. Long column labels truncate with an ellipsis instead of wrapping the header row onto a second
221
- line. Inside `DataViews.Tree`, the same table drops that border — the tree's pane already draws one,
222
- and two would nest a pixel apart.
223
-
224
- ### The tree's pane
225
-
226
- Pick a node and the pane beside it lists what that node holds. Its header names the selected node
227
- and counts its records, and it stays up before you have picked anything, so the tabs are always
228
- reachable.
229
-
230
- **The pane's tabs are children**, the same bargain the component's own views strike: a tab exists
231
- because you rendered it, and the switch shows exactly what you passed. Pass one and there is no
232
- switch at all — a switch with a single option is a label. Pass **none** and there is no pane
233
- either; the tree is then a hierarchy and nothing else, and the rail takes the whole width:
234
-
235
- ```tsx
236
- <DataViews.Tree nodes={nodes} labelPath="status" /> // a rail, no pane
237
- ```
238
-
239
- ```tsx
240
- <DataViews.Tree nodes={nodes} labelPath="status">
241
- <DataViews.Tree.Table selectable onRowClick={open} renderCell={cell} />
242
- <DataViews.Tree.Cards renderCard={card} />
243
- <DataViews.Tree.Tab value="timeline" label="Timeline" icon={<Clock />}>
244
- <Timeline />
245
- </DataViews.Tree.Tab>
246
- </DataViews.Tree>
247
- ```
248
-
249
- | Part | What it is |
250
- | --- | --- |
251
- | `DataViews.Tree.Table` | the **real `DataViews.Table`** over the node's rows — `selectable`, `onRowClick`, `renderCell`, `onRowMove`, `onAddRow`, virtualization past 300 rows |
252
- | `DataViews.Tree.Cards` | the board's `DataViewCard`; `renderCard` replaces it, `className` replaces the grid |
253
- | `DataViews.Tree.Tab` | a mode of your own — its `children` are the pane while it is selected |
254
-
255
- All three take `value`, `label` and `icon` to name themselves in the switch, exactly as the
256
- component's views take `id`, `label` and `icon`. The two built-in ones fill those in, so you pass
257
- them only to rename a tab or to register the same view twice.
258
-
259
- **A tab sees the node's rows.** Everything inside the pane runs in a data scope whose `rows` are
260
- the ones the pane lists, so a tab of your own reads them the same way any other part does — no
261
- props to thread:
262
-
263
- ```tsx
264
- function Timeline() {
265
- const { rows } = useDataViewsData(); // the selected node's rows, already narrowed by paneRows
266
- return <ol>{rows.map((row) => <li key={String(row.id)}>{String(row.createdAt)}</li>)}</ol>;
267
- }
268
- ```
269
-
270
- **Which rows.** By default a node shows its **descendants** — a branch lists what is under it, a
271
- leaf lists itself. A synthetic grouping branch has no meaningful row of its own, which is why
272
- descendants are the default. Override with `paneRows` when the tree and the pane hold different
273
- things — a tree of categories whose pane must list that category's items:
274
-
275
- ```tsx
276
- <DataViews.Tree nodes={categories} paneRows={(node) => itemsUnder(node.id)} />
277
- ```
278
-
279
- `paneRows` is also where sorting belongs if you want the pane self-contained. The pane's table
280
- headers sort by writing to the **query**, like every other sort in this component — so a pane sort
281
- re-fetches the dataset and only reorders the pane once re-sorted nodes come back.
282
-
283
- **Mode.** `defaultPaneMode` seeds it, `onPaneModeChange` reports every switch, `paneMode` takes it
284
- over entirely. The value is a tab's `value` — `"table"`, `"cards"`, or your own. Seed and persist
285
- is the round-trip:
286
-
287
- ```tsx
288
- <DataViews.Tree
289
- nodes={nodes}
290
- defaultPaneMode={loadPref() ?? "table"}
291
- onPaneModeChange={savePref}
292
- />
293
- ```
294
-
295
- **The header** takes your markup through `paneActions` — an Add button, a menu, a count of your
296
- own. It sits between the record count and the switch.
297
-
298
- **And when none of it fits**, any child that is *not* one of those three tabs **is** the pane:
299
- header, switch and all. That is the escape hatch, and what every tree written before these tabs
300
- existed passes.
301
-
302
- ```tsx
303
- <DataViews.Tree nodes={nodes}>
304
- <MyOwnPane />
305
- </DataViews.Tree>
306
- ```
307
-
308
- A full working example of every seam above — `renderNode`, `paneRows`, a custom cell, a custom
309
- card, a custom tab, `paneActions` and a whole-pane override — is `app/data-views/tree-custom`.
310
-
311
- **`DataViews.Detail`** is the ready-made detail pane for the inbox and tree. Drop it in as
312
- `children` and it renders every visible field of whatever row is open, as a `<dl>`, painted through
313
- the same `Cell` the views use. It renders **nothing** when no row is open — including when the open
314
- id belongs to a node that is not a row (see [One caveat](#one-caveat)).
315
-
316
- ```tsx
317
- <DataViews.Inbox titlePath="subject" datePath="createdAt">
318
- <DataViews.Detail />
319
- </DataViews.Inbox>
320
- ```
321
-
322
- ## Header, panel and filters
323
-
324
- `DataViews.Header` holds `ViewSwitch`, `Search`, `Actions` and `PanelToggle`. The bar is always
325
- dark, scoped with `data-theme="dark"`, so it reads correctly whatever theme the host app runs in.
326
-
327
- `DataViews.Panel` is the 260px settings rail: `Panel.Tab` for each tab, with `Panel.SavedViews`,
328
- `Panel.Columns` (show/hide and drag-reorder) and `Panel.Sort` inside.
329
-
330
- `DataViews.Filters` takes **FormBuilder fields as children** — not a config array describing
331
- fields, the fields themselves. The `<FormBuilder>` lives inside `Filters`; you supply only its
332
- fields, and what each one *means* comes from the field, never from the rows.
333
-
334
- Five field kinds become filters. Anything else renders but filters nothing — a checkbox is not a
335
- query, and a file is not a value you can filter by.
336
-
337
- **Filters are not derived from `fields`.** There is no `filterable`, `filterMode`, `filterVariant`,
338
- `filterOptions` or `filterLabel` — a filter exists because you wrote the control, and what it
339
- *means* comes from that field's `FieldKind`. Hiding a column (`visible: false`, `type: "hidden"`)
340
- has no effect on filters at all; they are independent surfaces.
341
-
342
- ### Choosing a control
343
-
344
- Two questions pick it, and **the first is about the data, not the UI**:
345
-
346
- 1. **Can the option set grow?** Not how many values it has today — whether it can scale. A set the
347
- system's own model fixes (four order statuses) will never grow without a release. A set the org
348
- configures, or a workflow editor can extend, is **dynamic** — and a dynamic set with four values
349
- today can have sixteen next quarter.
350
- 2. **Can the user pick more than one?**
351
-
352
- | | **Fixed** — bounded by the model | **Dynamic** — org-configurable |
353
- | --- | --- | --- |
354
- | **Multi-pick** | `CheckboxGroup` | `MultiSelect` / `Tags` |
355
- | **Single-pick** | `RadioList` | `SearchableSelect` |
356
-
357
- A closed lifecycle (`Draft · Active · Discontinued`) is fixed. Brand, Owner, Vendor, Assignee, Tags
358
- and Category are dynamic unconditionally. A *status* can be either — if an org can extend it
359
- through an approval chain, treat it as dynamic, because sixteen stacked checkboxes is not a control.
360
-
361
- ### The section types
362
-
363
- | Control | Use when | Renders | Lands in `FilterState` as |
364
- | --- | --- | --- | --- |
365
- | `FormBuilder.CheckboxGroup` | fixed set, multi-pick | checkbox list | `string[]` |
366
- | `FormBuilder.RadioList` | fixed set, single-pick | radio list | one-element `string[]` |
367
- | `FormBuilder.SearchableSelect` | dynamic set, single-pick | searchable combobox | one-element `string[]` |
368
- | `FormBuilder.MultiSelect` · `FormBuilder.Tags` | dynamic set, multi-pick | **`BadgeField`** — search + chips | `string[]` |
369
- | `FormBuilder.Slider range` | numeric | range slider | `{ kind: "number", min, max }` |
370
- | `FormBuilder.DateRange` | date | date pair | `{ kind: "date", from, to }` |
371
- | `FormBuilder.Text` | free text | text input | one-element `string[]` |
372
- | ~~toggle / switch~~ | boolean | — | **not supported** |
373
-
374
- `MultiSelect` and `Tags` are the **same field**: both render `BadgeField`, so the searchable
375
- multi-select case needs nothing extra.
376
-
377
- **Booleans do not filter yet.** `FormBuilder.SwitchBox` and `FormBuilder.Checkbox` are stamped
378
- `boolean`, but the filter map (`filters/children.tsx`) recognises only `text · choice ·
379
- multiChoice · date · slider`. A switch placed in `Filters` renders and writes nothing. Until that
380
- kind is added, express an on/off narrowing with `Filters.Custom`, which can write any shape you
381
- like to one path.
382
-
383
- Every control here is live on `app/data-views/filters/page.tsx`:
384
-
385
- ```tsx
386
- <DataViews.Filters>
387
- {/* fixed set, multi-pick — the four statuses are fixed by the model */}
388
- <FormBuilder.CheckboxGroup name="status" label="Status" options={STATUS_OPTIONS} />
389
-
390
- {/* fixed set, single-pick — High/Medium/Low are mutually exclusive */}
391
- <FormBuilder.RadioList name="priority" label="Priority" options={PRIORITY_OPTIONS} />
392
-
393
- {/* dynamic set, single-pick — customers are data-fed, one per record */}
394
- <FormBuilder.SearchableSelect name="customer.name" label="Customer" options={CUSTOMER_OPTIONS} />
395
-
396
- {/* dynamic set, multi-pick — renders BadgeField: search *and* several values, as chips */}
397
- <FormBuilder.MultiSelect name="brand.name" label="Brand" options={BRAND_OPTIONS} />
398
-
399
- <FormBuilder.Slider name="total" label="Total" range min={0} max={15000} step={100} />
400
- <FormBuilder.DateRange name="createdAt" label="Created" />
401
- </DataViews.Filters>
402
- ```
403
-
404
- **One rule you cannot guess from the markup.** react-hook-form reads `.` as object nesting, so a
405
- filter on `customer.name` is registered as `customer__name` — you write the real path and `Filters`
406
- escapes it, but anything calling `setValue` yourself must use the escaped name. (The other, that a
407
- control at its neutral position emits no key at all, is under
408
- [Questions this design gets asked](#questions-this-design-gets-asked).)
409
-
410
- ### Presets, custom filters and the summary
411
-
412
- ```tsx
413
- <DataViews.Filters>
414
- <FormBuilder.Slider name="total" label="Total" range min={0} max={15000} />
415
- {/* Quick-set chips for one field. Number presets take min/max, date presets from/to. */}
416
- <DataViews.Filters.Presets
417
- for="total"
418
- items={[
419
- { label: "Under $500", max: 500 },
420
- { label: "$5k+", min: 5000 },
421
- ]}
422
- />
423
-
424
- {/* The escape hatch: any control you like, driving one filter path yourself. */}
425
- <DataViews.Filters.Custom
426
- path="items"
427
- label="Item count"
428
- render={({ value, setValue }) => (
429
- <ItemsPicker value={value} onChange={(next) => setValue(next)} />
430
- )}
431
- />
432
- </DataViews.Filters>
433
- ```
434
-
435
- ### Questions this design gets asked
436
-
437
- | Question | Answer |
438
- | --- | --- |
439
- | Is there an in-view filter panel *and* a Filters tab — which is canonical? | **One surface.** `DataViews.Filters` is a single component. Render it inside a `Panel.Tab` or as a standalone bar; author against the component, not against a tab. |
440
- | What orders the sections? | **The order you write the children.** There is no `order` prop for filters. |
441
- | Is there an applied-count badge or a chip summary of active filters? | **No.** `PanelToggle` carries no count, and there is no summary component — the controls themselves show what is set. Render your own above the rows if you want one. |
442
- | Do `BadgeField` chip colours and `FieldConfig.variants` share a token set? | **They never meet.** Chips come from the field's own `options`; `variants` (`BadgeVariant`) styles `enum-badge` **columns**. Filters and columns are independent. |
443
-
444
- And the behaviours worth stating because they are easy to assume wrongly:
445
-
446
- - **Empty is unconstrained.** A control at its neutral position emits **no key** — a slider dragged
447
- back to exactly `[min, max]` removes its filter rather than sending "everything". Filters never
448
- narrow to zero rows because a section was touched and cleared.
449
- - **Filter state is one object.** `onQueryChange` receives the whole next query, not a per-field
450
- delta.
451
- - **Filters survive a view switch.** Table, board, inbox and tree share one query.
452
- - **Persistence is yours.** The component holds the query only as long as it is mounted.
453
- - **How constraints combine is *your* matcher's business,** not the component's — it reports what
454
- the user asked for and nothing more. The reference endpoint in `app/api/_lib/query.ts` intersects
455
- across fields (AND) and unions within one field (OR), which is what most callers want.
456
-
457
- ### Filters outside the settings rail
458
-
459
- Nothing requires `Filters` to live in a `Panel.Tab`. Rendered as a plain child it becomes a bar
460
- above the views, inside the light surface — which is what you want when filtering is the main task
461
- rather than a setting:
462
-
463
- ```tsx
464
- <DataViews rows={rows} fields={fields} total={total} onQueryChange={setQuery}>
465
- <DataViews.Header title="Orders">
466
- <DataViews.ViewSwitch />
467
- </DataViews.Header>
468
-
469
- {/* A bar, not a tab. `title` and the bottom border are on by default here. */}
470
- <DataViews.Filters title="Filters">
471
- <FormBuilder.MultiSelect name="status" label="Status" options={STATUS} />
472
- <FormBuilder.DateRange name="createdAt" label="Created" />
473
- </DataViews.Filters>
474
-
475
- <DataViews.Table />
476
- </DataViews>
477
- ```
478
-
479
- Inside a tab you normally turn that chrome off — `<DataViews.Filters title={null}
480
- className="border-b-0 p-0">` — because the tab already provides it.
481
-
482
- ## API Reference
483
-
484
- ### `<DataViews>` (root)
485
-
486
- | Prop | Type | Default | Description |
487
- | --- | --- | --- | --- |
488
- | `rows` * | `readonly Row[]` | — | The rows to paint, already queried, in the order they should appear. |
489
- | `fields` * | `readonly FieldConfig[]` | — | How to paint each field. Order here is the default column order. |
490
- | `children` * | `ReactNode` | — | The parts to render — the header, views, panel, filters. |
491
- | `getRowId` | `(row, index) => string` | `row.id ?? index` | Stable identity. Selection, drag and the active row all key off it. |
492
- | `total` | `number` | `0` | Total matching rows on the server. `hasMore` is derived from `rows.length < total`. |
493
- | `loading` | `boolean` | `false` | First load. Each view paints its own skeleton. |
494
- | `onLoadMore` | `() => void` | — | A view reached its end. Fetch the next page and **append** it to `rows`. |
495
- | `loadingMore` | `boolean` | `false` | That next page is in flight — distinct from `loading`. |
496
- | `query` | `DataViewsQuery` | — | Controlled query. Omit to let the component hold it. |
497
- | `onQueryChange` | `(query) => void` | — | Search, filters, sort, page or page size changed. Go and fetch. |
498
- | `defaultQuery` | `Partial<DataViewsQuery>` | — | Seed for the uncontrolled query — e.g. a starting `pageSize`. |
499
- | `defaultView` | `string` | first registered | Which view opens. |
500
- | `defaultPanelOpen` | `boolean` | `false` | Start with the settings rail open. |
501
- | `onViewChange` | `(view: string) => void` | — | Told which view is showing; the component still owns it. |
502
- | `onSelectionChange` | `(ids: readonly string[]) => void` | — | Told what is selected. |
503
- | `onActiveIdChange` | `(id: string \| null) => void` | — | Told which row is open. |
504
- | `theme` | `Themes` | — | `data-theme` for the **content**. The chrome stays dark regardless. |
505
- | `className` | `string` | — | |
506
-
507
- `page` resets to 1 internally whenever the search, filters, sort or page size change — a new
508
- result set has no page 4 to stay on.
509
-
510
- Every part below has its own heading, so you can ask for one on its own — with the MCP server,
511
- `get-component-api DataViews part="Board"`. Columns are the same throughout:
512
- **Prop · Type · Default · Required · Notes**.
513
-
514
- ### DataViews.Header
515
-
516
- The dark bar across the top. Its children are the four parts below, in whatever order you write
517
- them.
518
-
519
- | Prop | Type | Default | Required | Notes |
520
- | --- | --- | --- | --- | --- |
521
- | `title` | `ReactNode` | — | no | The uppercase title pill. |
522
- | `children` | `ReactNode` | — | no | `ViewSwitch`, `Search`, `Actions`, `PanelToggle`. |
523
- | `className` | `string` | — | no | |
524
-
525
- ### DataViews.ViewSwitch
526
-
527
- | Prop | Type | Default | Required | Notes |
528
- | --- | --- | --- | --- | --- |
529
- | `className` | `string` | — | no | |
530
-
531
- Renders a spacer instead of a switcher when fewer than two views are registered — one option is a
532
- label, not a choice.
533
-
534
- ### DataViews.Search
535
-
536
- | Prop | Type | Default | Required | Notes |
537
- | --- | --- | --- | --- | --- |
538
- | `placeholder` | `string` | `"Search..."` | no | |
539
- | `className` | `string` | — | no | |
540
-
541
- An icon button that expands into a field. It writes `search` into the query and matches nothing
542
- itself; it collapses on an outside click only while empty, so an active term is never thrown away.
543
-
544
- ### DataViews.Actions
545
-
546
- | Prop | Type | Default | Required | Notes |
547
- | --- | --- | --- | --- | --- |
548
- | `children` | `ReactNode` | — | no | Your buttons, pushed to the end of the bar. |
549
- | `className` | `string` | — | no | |
550
-
551
- ### DataViews.PanelToggle
552
-
553
- | Prop | Type | Default | Required | Notes |
554
- | --- | --- | --- | --- | --- |
555
- | `children` | `ReactNode` | `"Filter & Config."` | no | The label beside the gear. |
556
- | `className` | `string` | — | no | |
557
-
558
- Renders `null` while the rail is open — the rail carries its own close button.
559
-
560
- ### DataViews.Table
561
-
562
- Rows and columns. Virtualizes past 300 rows.
563
- Example: [`views`](./examples/views.md).
564
-
565
- | Prop | Type | Default | Required | Notes |
566
- | --- | --- | --- | --- | --- |
567
- | `selectable` | `boolean` | `false` | no | Per-row checkboxes and select-all. |
568
- | `onRowClick` | `(row: Row, id: string) => void` | — | no | Also makes rows keyboard-reachable. |
569
- | `onRowMove` | `(intent: MoveIntent) => void` | — | no | Row reordering. Adds a grip column; moves nothing itself. |
570
- | `onAddRow` | `() => void` | — | no | Shows the `+ Add New` row at the foot of the table. |
571
- | `addRowLabel` | `string` | `"Add New"` | no | |
572
- | `renderCell` | `(args: RowRenderArgs & { field: FieldConfig }) => ReactNode` | — | no | Return `undefined` to fall through to the default cell. |
573
- | `id` · `label` · `icon` · `className` | `ViewBaseProps` | `"table"` · `"List"` | no | How it appears in the switcher. |
574
-
575
- ### DataViews.Board
576
-
577
- A kanban board. **You** build the columns — the board never groups rows itself.
578
- Example: [`views`](./examples/views.md).
579
-
580
- | Prop | Type | Default | Required | Notes |
581
- | --- | --- | --- | --- | --- |
582
- | `groups` | `readonly RowGroup[]` | — | **yes** | The columns, in order. |
583
- | `titlePath` | `string` | first visible field | no | Which field is the card title. |
584
- | `renderCard` | `(args: RowRenderArgs & { group: RowGroup; isActive: boolean; isDragging: boolean }) => ReactNode` | — | no | Replaces the card; the wrapper keeps the drag. |
585
- | `onRowMove` | `(intent: MoveIntent) => void` | — | no | A card was dropped. `intent.to` is the group id, or `null` outside any column. |
586
- | `onColumnAction` | `(groupId: string) => void` | — | no | The per-column action button in its header. |
587
- | `id` · `label` · `icon` · `className` | `ViewBaseProps` | `"board"` · `"Board"` | no | |
588
-
589
- ### DataViews.Inbox
590
-
591
- A master list beside a detail pane.
592
- Example: [`inbox-routing`](./examples/inbox-routing.md), which drives the
593
- pane from the URL.
594
-
595
- | Prop | Type | Default | Required | Notes |
596
- | --- | --- | --- | --- | --- |
597
- | `children` | `ReactNode` | — | no | The detail pane — often `<DataViews.Detail/>`. |
598
- | `renderItem` | `(args: RowRenderArgs & { isActive: boolean }) => ReactNode` | — | no | Replaces the list item. |
599
- | `titlePath` | `string` | — | no | Which field leads the item. |
600
- | `datePath` | `string` | first `date`/`date-format` field | no | Which field shows as the date chip. |
601
- | `itemHref` | `(row: Row, id: string) => string` | — | no | Makes items links rather than buttons — that is what gives them a back button and a shareable URL. |
602
- | `linkComponent` | `React.ElementType` | `"a"` | no | Your router's link — e.g. `next/link`. |
603
- | `placeholder` | `ReactNode` | built-in empty pane | no | Shown while nothing is open. |
604
- | `id` · `label` · `icon` · `className` | `ViewBaseProps` | `"inbox"` · `"Inbox"` | no | |
605
-
606
- ### DataViews.Tree
607
-
608
- A hierarchy beside a pane. **You** build `nodes` — which field is the parent key, whether orphans
609
- become roots, how cycles are handled are decisions only you can make correctly.
610
- Example: [`tree-custom`](./examples/tree-custom.md).
611
-
612
- | Prop | Type | Default | Required | Notes |
613
- | --- | --- | --- | --- | --- |
614
- | `nodes` | `readonly TreeNode[]` | — | **yes** | The hierarchy. |
615
- | `labelPath` | `string` | first visible field | no | Which field labels a node. |
616
- | `renderNode` | `(args: { node: TreeNode; row: Row; fields: readonly FieldConfig[] }) => { name?: string; icon?: ReactNode; meta?: ReactNode }` | — | no | Returns parts, not markup — `TreeFolder` owns the row. |
617
- | `expanded` | `readonly string[]` | root nodes that have children | no | Controlled expansion. |
618
- | `onExpandedChange` | `(ids: readonly string[]) => void` | — | no | |
619
- | `onNodeMove` | `(intent: MoveIntent) => void` | — | no | A node was dropped into a new parent. Passing it is what turns drag on. |
620
- | `paneMode` | `TreePaneMode` | — | no | Controls the pane's tab. Omit to let the view hold it. |
621
- | `defaultPaneMode` | `TreePaneMode` | **the first tab's `value`** | no | Not a hardcoded `"table"` — a pane whose only tab is yours opens on it. |
622
- | `onPaneModeChange` | `(mode: TreePaneMode) => void` | — | no | The user switched. Persist it and seed `defaultPaneMode` back. |
623
- | `paneRows` | `(node: TreeNode) => readonly Row[]` | the node's descendants; a leaf yields itself | no | Which rows the pane lists. |
624
- | `paneActions` | `ReactNode` | — | no | Your markup in the pane's header, before the tab switch. |
625
- | `children` | `ReactNode` | — | no | The pane's tabs. **None means no pane**; anything that is not a tab **is** the pane. |
626
- | `id` · `label` · `icon` · `className` | `ViewBaseProps` | `"tree"` · `"Tree"` | no | |
627
-
628
- `TreePaneMode` is `"table" | "cards" | (string & {})` — any string, because a `Tree.Tab` of yours
629
- names its own mode.
630
-
631
- ### DataViews.Tree.Table
632
-
633
- The pane as a table. It **is** `DataViews.Table`, rendered over the selected node's rows, so it
634
- keeps sortable headers, selection, `renderCell`, the grip, `+ Add New` and virtualization.
635
-
636
- | Prop | Type | Default | Required | Notes |
637
- | --- | --- | --- | --- | --- |
638
- | `value` | `string` | `"table"` | no | What `paneMode` becomes while this tab shows. |
639
- | `label` | `string` | `"List"` | no | |
640
- | `icon` | `ReactNode` | `<Table2/>` | no | |
641
- | *…all of `DataViews.Table`'s* | | | no | `selectable`, `onRowClick`, `onRowMove`, `onAddRow`, `addRowLabel`, `renderCell`. |
642
-
643
- **Gotcha.** Its headers sort by writing to the **query**, like every other sort here — so a pane
644
- sort re-fetches the dataset and only reorders the pane once re-sorted nodes come back. Sort inside
645
- `paneRows` if you want the pane self-contained. It takes no `className` (that is a `ViewBaseProps`
646
- key, omitted).
647
-
648
- ### DataViews.Tree.Cards
649
-
650
- The pane as a grid of cards — the same `DataViewCard` the board paints.
651
-
652
- | Prop | Type | Default | Required | Notes |
653
- | --- | --- | --- | --- | --- |
654
- | `value` | `string` | `"cards"` | no | |
655
- | `label` | `string` | `"Cards"` | no | |
656
- | `icon` | `ReactNode` | `<LayoutGrid/>` | no | |
657
- | `renderCard` | `(args: RowRenderArgs) => ReactNode` | — | no | Replaces `DataViewCard` outright. |
658
- | `className` | `string` | `grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 p-4` | no | **Replaces** the grid classes; it is not merged. |
659
-
660
- ### DataViews.Tree.Tab
661
-
662
- A mode of your own. Its `children` are the pane while it is selected, and nothing while it is not.
663
-
664
- | Prop | Type | Default | Required | Notes |
665
- | --- | --- | --- | --- | --- |
666
- | `value` | `string` | — | **yes** | A tab only you know about has no default name. |
667
- | `label` | `string` | — | **yes** | |
668
- | `icon` | `ReactNode` | — | no | |
669
- | `children` | `ReactNode` | — | no | Reads the node's rows from `useDataViewsData()` — the pane scopes them. |
670
-
671
- ### DataViews.Detail
672
-
673
- The ready-made detail pane for the inbox and tree: every visible field of the open row, as a `<dl>`,
674
- painted through `Cell`.
675
-
676
- | Prop | Type | Default | Required | Notes |
677
- | --- | --- | --- | --- | --- |
678
- | `className` | `string` | — | no | |
679
-
680
- Renders **nothing** when no row is open — including when the open id belongs to a node that is not
681
- a row (see [One caveat](#one-caveat)).
682
-
683
- ### DataViews.Panel
684
-
685
- The 260px settings rail, opened by `PanelToggle`. Always dark.
686
- Example: [`panel`](./examples/panel.md).
687
-
688
- | Prop | Type | Default | Required | Notes |
689
- | --- | --- | --- | --- | --- |
690
- | `children` | `ReactNode` | — | no | `Panel.Tab`s. |
691
- | `defaultTab` | `string` | first rendered tab | no | |
692
- | `title` | `ReactNode` | the single tab's label | no | Shown only when there is one tab or none. |
693
- | `className` | `string` | — | no | |
694
-
695
- ### DataViews.Panel.Tab
696
-
697
- | Prop | Type | Default | Required | Notes |
698
- | --- | --- | --- | --- | --- |
699
- | `value` | `string` | — | **yes** | |
700
- | `label` | `string` | — | **yes** | |
701
- | `icon` | `ReactNode` | — | no | |
702
- | `children` | `ReactNode` | — | no | Rendered only while this tab is open. |
703
-
704
- The strip disappears when there is only one tab.
705
-
706
- ### DataViews.Panel.Section
707
-
708
- A titled, collapsible group inside a tab — the same `ConclusionHeader` `FormSummary` uses.
709
-
710
- | Prop | Type | Default | Required | Notes |
711
- | --- | --- | --- | --- | --- |
712
- | `title` | `ReactNode` | — | no | Without one there is no header and no fold. |
713
- | `description` | `ReactNode` | — | no | Sits with the header, outside the fold, so it still reads when shut. |
714
- | `collapsible` | `boolean` | `true` | no | |
715
- | `defaultOpen` | `boolean` | `true` | no | Nothing starts folded. |
716
- | `children` · `className` | | — | no | |
717
-
718
- A collapsed body keeps its controls in the DOM but marks them `inert`, so focus cannot reach them.
719
-
720
- ### DataViews.Panel.Columns
721
-
722
- Show/hide and drag-reorder the columns. Takes no `value`/`onValueChange` — the column list is the
723
- root's, reached through `useDataViewsView()`.
724
-
725
- | Prop | Type | Default | Required | Notes |
726
- | --- | --- | --- | --- | --- |
727
- | `title` | `ReactNode` | `"Table Columns"` | no | |
728
- | `description` | `ReactNode` | `"Show or hide columns in table view"` | no | |
729
- | `className` | `string` | — | no | |
730
-
731
- Hiding a column here changes `visibleFields` everywhere — it also retitles the board's cards.
732
-
733
- ### DataViews.Panel.Sort
734
-
735
- The default-sort picker, for views that have no column headers. Writes into the query.
736
-
737
- | Prop | Type | Default | Required | Notes |
738
- | --- | --- | --- | --- | --- |
739
- | `title` | `ReactNode` | `"Default Sort"` | no | |
740
- | `className` | `string` | — | no | |
741
-
742
- ### DataViews.Panel.SavedViews
743
-
744
- | Prop | Type | Default | Required | Notes |
745
- | --- | --- | --- | --- | --- |
746
- | `views` | `readonly SavedView[]` | `[]` | no | The list you persisted. |
747
- | `onValueChange` | `(id: string) => void` | — | no | A saved view was picked. |
748
- | `onSave` | `(snapshot: SavedViewSnapshot) => void` | — | no | The Save button appears only when you pass this. |
749
- | `saveLabel` | `ReactNode` | `"Save a New View"` | no | |
750
- | `title` | `ReactNode` | `"Saved View"` | no | |
751
- | `className` | `string` | — | no | |
752
-
753
- Saving is yours — a view outlives the component. Restoring is not: hand the snapshot back and
754
- selecting it puts filters, sort and columns back internally.
755
-
756
- ### DataViews.Filters
757
-
758
- The filter controls, written as a form. Takes **no** `value`/`onValueChange`: it reads and writes
759
- the root's query through `useDataViewsFilters()`, and what leaves the component is `onQueryChange`.
760
- Example: [`filters`](./examples/filters.md).
761
-
762
- | Prop | Type | Default | Required | Notes |
763
- | --- | --- | --- | --- | --- |
764
- | `children` | `ReactNode` | — | no | **FormBuilder fields.** The `<FormBuilder>` is inside. |
765
- | `title` | `ReactNode` | `"Filters"` | no | Pass `null` to drop the header row. |
766
- | `description` | `ReactNode` | — | no | A small line under the title, above the fields. |
767
- | `clearLabel` | `ReactNode` | `"Clear"` | no | The Clear button appears only while a filter is set. |
768
- | `collapsible` | `boolean` | `true` | no | |
769
- | `defaultOpen` | `boolean` | `true` | no | |
770
- | `className` | `string` | — | no | |
771
-
772
- ### DataViews.Filters.Presets
773
-
774
- Quick-set chips for one numeric or date field.
775
-
776
- | Prop | Type | Default | Required | Notes |
777
- | --- | --- | --- | --- | --- |
778
- | `for` | `string` | — | **yes** | The field's path. Renders `null` if no field there. |
779
- | `items` | `readonly Preset[]` | — | **yes** | `{ label, min?, max? }` or `{ label, from?, to? }`. |
780
- | `className` | `string` | — | no | |
781
-
782
- ### DataViews.Filters.Custom
783
-
784
- A filter no FormBuilder field covers.
785
-
786
- | Prop | Type | Default | Required | Notes |
787
- | --- | --- | --- | --- | --- |
788
- | `path` | `string` | — | **yes** | The key it writes into `filters`. |
789
- | `render` | `(args: { value: FilterValue \| undefined; setValue: (v: FilterValue \| undefined) => void }) => ReactNode` | — | **yes** | |
790
- | `label` | `ReactNode` | derived from `path` | no | |
791
-
792
- ### Cell
793
-
794
- Paint one field of one row exactly as the views paint it.
795
-
796
- | Prop | Type | Default | Required | Notes |
797
- | --- | --- | --- | --- | --- |
798
- | `field` | `FieldConfig` | — | **yes** | `field.render` wins over `field.type`. |
799
- | `row` | `Row` | — | **yes** | |
800
- | `className` | `string` | — | no | |
801
-
802
- `type: "hidden"` renders `null`; a blank value renders `-`, except for `boolean` and
803
- `progress-bar`, which have a meaningful zero.
804
-
805
- ### Hooks
806
-
807
- All five throw if called outside `<DataViews>`.
808
-
809
- | Hook | Returns |
810
- | --- | --- |
811
- | `useDataViewsData()` | `{ rows, fields, visibleFields, getRowId, loading, loadingMore, hasMore, onLoadMore? }` |
812
- | `useDataViewsView()` | `{ view, setView, views, search, setSearch, sort, setSort, selection, setSelection, activeId, setActiveId, columns, setColumns }` |
813
- | `useDataViewsPanel()` | `{ open, setOpen }` |
814
- | `useDataViewsPanelTabs()` | `{ tab, setTab, tabs }` |
815
- | `useDataViewsFilters()` | `{ filters, setFilters, filterFields }` |
816
- | `useActiveRow()` | `Row \| null` — the row behind `activeId` |
817
-
818
- Inside the tree's pane, `useDataViewsData().rows` is **the selected node's rows**, not the whole
819
- set: the pane scopes the context. That is what lets a `Tree.Tab` of yours read them with no props.
820
-
821
- ### Utilities
822
-
823
- | Export | Signature | What it is for |
824
- | --- | --- | --- |
825
- | `emptyQuery` | `(overrides?: Partial<DataViewsQuery>) => DataViewsQuery` | `{ search: "", filters: {}, sort: null, page: 1, pageSize: 10 }`. |
826
- | `queryToParams` | `(query: DataViewsQuery) => URLSearchParams` | `search` · `filters` (JSON) · `sort` (`"path:direction"`) · `page` · `pageSize`. |
827
- | `parseQuery` | `(url: URL) => DataViewsQuery` | The server half. Malformed filters → `{}`; `pageSize` clamped to 1–500. |
828
- | `getByPath` | `(obj: unknown, path?: string) => unknown` | Reads `"customer.name"`. |
829
- | `getString` | `(obj: unknown, path: string) => string` | |
830
- | `formatPathLabel` | `(path: string) => string` | `"created_at"` → `"Created At"`. |
831
- | `defaultGetRowId` | `(row: Row, index: number) => string` | `id ?? _id ?? uuid ?? index`. |
832
- | `buildCardRows` | `(fields: readonly FieldConfig[], row: Row) => DataViewCardRow[]` | The card body, paired two per row. |
833
- | `resolveBadgeVariant` | `(variant?: BadgeVariant) => { color, badgeStyle }` | |
834
- | `SkeletonBar` · `skeletonKeys` | `({ className })` · `(n: number) => number[]` | The pieces every view's skeleton is built from. |
835
- | `markView` · `markHeader` · `markPanel` | `(component, meta?) => component` | Register a part of your own. |
836
-
837
- ## TypeScript
838
-
839
- Every type below is exported from `@/components/DataViews`.
840
-
841
- ### Row
842
-
843
- ```ts
844
- type Row = Record<string, unknown>;
845
- ```
846
-
847
- Your object, untouched. DataViews never reshapes it — dotted paths are read on the way out.
848
-
849
- ### FieldConfig
850
-
851
- One entry per field. The order of the array is the default column order.
852
-
853
- ```ts
854
- type FieldConfig = {
855
- path: string; // "customer.name" — dotted paths are read for you
856
- label?: string; // defaults to a title-cased `path`
857
- type?: FieldType; // how to paint it — see the table below
858
- visible?: boolean; // false hides it from `visibleFields`
859
- render?: (value: unknown, row: Row) => ReactNode; // wins over `type`; the widest per-field seam
860
- // …plus the per-type keys below
861
- };
862
- ```
863
-
864
- ### Field types
865
-
866
- All seventeen, with the extra keys each one reads. Anything unrecognised falls back to `text`.
867
-
868
- | `type` | Extra keys it reads | Default when omitted |
869
- | --- | --- | --- |
870
- | `text` | — | `String(value)`; also the fallback for any unknown type |
871
- | `number` | — | `value.toLocaleString()` |
872
- | `date` | — | the raw string |
873
- | `date-format` | `dateFormat` — token string (`YYYY MM DD HH mm ss`) or `Intl.DateTimeFormatOptions` | `{ year: "numeric", month: "short", day: "numeric" }` |
874
- | `boolean` | `trueLabel` · `falseLabel` · `trueVariant` · `falseVariant` | `"Yes"`/`"No"`, green/gray. Never shows the `-` placeholder |
875
- | `enum-badge` | `variants` (value → colour) · `defaultVariant` | `"gray"`; badge size `S` |
876
- | `badge-array` | `variant` · `limit` | `"blue"`, no limit; the overflow chip is `+N` in gray, size `XS` |
877
- | `currency` | `currency` — `"USD"` or `{ symbol, locale, decimals, code }` | symbol `"$"`; `Intl` currency style when `code` is set |
878
- | `number-format` | `format: Intl.NumberFormatOptions` | plain `Intl.NumberFormat` |
879
- | `progress-bar` | `thresholds: [warn, ok]` | `[40, 70]`; clamped 0–100; never shows the placeholder |
880
- | `star-rating` | `max` | `5` |
881
- | `icon-text` | `icon` (a `ri-*` class, else literal text) · `iconPosition` | `"before"` |
882
- | `two-line` | `secondaryPath` | — |
883
- | `avatar` | `fallbackPath` — where the initials come from | initials `"?"` |
884
- | `link` | `linkType: "mailto" \| "tel" \| "url"` | `url` opens in a new tab with `rel="noopener noreferrer"`; `mailto:`/`tel:` are prefixed if absent |
885
- | `image` | — | 40×40 rounded |
886
- | `hidden` | — | renders `null` **and** is dropped from `visibleFields` |
887
-
888
- `BadgeVariant` is one of `green · greenLight · cocktailGreen · yellow · redOrange · redLight ·
889
- rose · purple · bluePurple · blue · navy · gray · highlight`.
890
-
891
- ### DataViewsQuery
892
-
893
- The only state that leaves the component.
894
-
895
- ```ts
896
- interface DataViewsQuery {
897
- search: string;
898
- filters: FilterState; // Record<path, FilterValue>
899
- sort: { path: string; direction: "asc" | "desc" } | null;
900
- page: number; // 1-based
901
- pageSize: number;
902
- }
903
- ```
904
-
905
- ### FilterValue
906
-
907
- Three kinds, and which one a field produces is decided by the FormBuilder field you rendered — see
908
- [The section types](#the-section-types).
909
-
910
- ```ts
911
- type FilterValue =
912
- | string[] // choice / multi-choice / text
913
- | { kind: "number"; min?: number; max?: number } // slider
914
- | { kind: "date"; from?: string; to?: string }; // date range, ISO YYYY-MM-DD, local time
915
-
916
- type FilterState = Record<string, FilterValue>;
917
- ```
918
-
919
- A cleared filter is **removed** from the object, never set to an empty value — so
920
- `Object.keys(filters).length` is a truthful "is anything filtered".
921
-
922
- ### The rest
923
-
924
- ```ts
925
- type RowGroup = { id: string; label: string; color?: ColumnColor; rows: Row[] };
926
- type TreeNode = { id: string; row: Row; children: TreeNode[]; depth: number };
927
- type MoveIntent = { id: string; from: string | null; to: string | null; index?: number };
928
- type ColumnState = { path: string; label: string; visible: boolean };
929
- type Sort = { path: string; direction: "asc" | "desc" } | null;
930
- type Preset = { label: string; min?: number; max?: number }
931
- | { label: string; from?: string; to?: string };
932
- type SavedViewSnapshot = { filters: FilterState; sort: Sort; columns: readonly ColumnState[] };
933
- type SavedView = { id: string; label: string; snapshot?: SavedViewSnapshot };
934
- type ColumnColor = "gray" | "purple" | "orange" | "blue" | "green" | "red";
935
- type TreePaneMode = "table" | "cards" | (string & {});
936
- type RowRenderArgs = { row: Row; id: string; index: number; fields: readonly FieldConfig[] };
937
- ```
938
-
939
- `TreeNode.depth` is required by the type but the view does not read it — it measures depth from the
940
- nesting. Pass `0` and forget it.
941
-
942
- `parseQuery` is the server half of `queryToParams` — a route handler imports it so the encoder and
943
- the decoder cannot drift.
944
- ## Custom rendering
945
-
946
- Every view can be repainted, and each seam hands you the same `{ row, id, index, fields }` — plus
947
- whatever that view knows. `fields` is what the panel left **visible, in order**, and `Cell` paints
948
- one the way every other view paints it, so a custom UI keeps the currency, badge colours and date
949
- formats the rest of the component uses.
950
-
951
- | Want | Use |
952
- | --- | --- |
953
- | one field, everywhere it appears | `FieldConfig.render(value, row)` |
954
- | one cell, in the table only | `renderCell` — return `undefined` to fall through |
955
- | the board's card | `renderCard` |
956
- | the inbox list row | `renderItem` |
957
- | a tree node's label / icon / meta | `renderNode` |
958
- | a cell or card **inside the tree's pane** | `DataViews.Tree.Table renderCell` · `DataViews.Tree.Cards renderCard` |
959
- | a whole extra mode in the tree's pane | `DataViews.Tree.Tab` |
960
- | something in the pane's header | `paneActions` |
961
- | the inbox's detail pane, or the tree's pane entirely | `children` |
962
- | a whole new view | `markView(MyView, { defaultId, defaultLabel })` |
963
-
964
- ### A table cell
965
-
966
- Return `undefined` for anything you do not want to touch — that cell falls through to the default,
967
- so you only describe the exception.
968
-
969
- ```tsx
970
- <DataViews.Table
971
- renderCell={({ field, row }) =>
972
- field.path === "total" ? (
973
- <span className="flex items-center gap-2">
974
- <Cell field={field} row={row} />
975
- {Number(row.total) > 5000 && (
976
- <Badge label="large" color="purple" badgeStyle="subtle" showIcon={false} />
977
- )}
978
- </span>
979
- ) : undefined
980
- }
981
- />
982
- ```
983
-
984
- `undefined` means "you paint it" and falls through to the default cell. `null` does not — it is a
985
- deliberate blank, which is how you hide a value without hiding the column. One `renderCell` can
986
- handle several fields:
987
-
988
- ```tsx
989
- renderCell={({ field, row }) => {
990
- if (field.path === "status" && row.archived) return null; // deliberately empty
991
- if (field.path === "customer.name")
992
- return (
993
- <span className="flex items-center gap-2">
994
- <Avatar src={String(row.avatar ?? "")} size="S" />
995
- <Cell field={field} row={row} />
996
- </span>
997
- );
998
- return undefined; // everything else: default
999
- }}
1000
- ```
1001
-
1002
- ### A board card
1003
-
1004
- `renderCard` replaces the card's **content**; the board keeps the wrapper, so dragging, selection
1005
- and the click target keep working without wiring any of it. It also receives `group` and
1006
- `isDragging`.
1007
-
1008
- ```tsx
1009
- <DataViews.Board
1010
- groups={groups}
1011
- renderCard={({ row, fields, isActive }) => (
1012
- <div
1013
- className={cn(
1014
- "bg-background-presentation-form-base flex flex-col gap-1 rounded-[10px] border p-3",
1015
- isActive ? "border-border-presentation-state-focus" : "border-border-presentation-global-primary",
1016
- )}
1017
- >
1018
- <span className="typography-headers-large-semibold">
1019
- <Cell field={fields[1]} row={row} />
1020
- </span>
1021
- <Cell field={fields[2]} row={row} />
1022
- </div>
1023
- )}
1024
- />
1025
- ```
1026
-
1027
- It also receives `group` and `isDragging` — the column the card is in, and whether this card is the
1028
- one being dragged:
1029
-
1030
- ```tsx
1031
- renderCard={({ row, fields, group, isDragging }) => (
1032
- <div className={cn("rounded-[10px] border p-3", isDragging && "opacity-40")}>
1033
- <Cell field={fields[1]} row={row} />
1034
- <span className="typography-body-small-regular">{group.label}</span>
1035
- </div>
1036
- )}
1037
- ```
1038
-
1039
- ### An inbox row
1040
-
1041
- Same idea: the row keeps its own hover, selected and link behaviour, and `renderItem` fills it.
1042
-
1043
- ```tsx
1044
- <DataViews.Inbox
1045
- renderItem={({ row, fields, isActive }) => (
1046
- <div className="flex items-center justify-between gap-2">
1047
- <Cell field={fields[1]} row={row} />
1048
- <span className="flex items-center gap-2">
1049
- <Cell field={fields[2]} row={row} />
1050
- {isActive && <i className="ri-arrow-right-line" aria-hidden />}
1051
- </span>
1052
- </div>
1053
- )}
1054
- />
1055
- ```
1056
-
1057
- `isActive` is the open row, which is what you hang a read/unread treatment on. With `itemHref` the
1058
- item becomes a link, and `linkComponent` makes it your router's:
1059
-
1060
- ```tsx
1061
- <DataViews.Inbox
1062
- itemHref={(row, id) => `/orders/${id}`}
1063
- linkComponent={Link}
1064
- placeholder={<p className="p-6">Pick an order.</p>}
1065
- renderItem={({ row, fields, isActive }) => (
1066
- <div className={cn("flex justify-between", !isActive && !row.read && "font-semibold")}>
1067
- <Cell field={fields[1]} row={row} />
1068
- <Cell field={fields[2]} row={row} />
1069
- </div>
1070
- )}
1071
- />
1072
- ```
1073
-
1074
- ### A tree node
1075
-
1076
- `renderNode` is the one that does **not** return markup. `TreeFolder` owns the row — the indent,
1077
- the connector lines, the selection band, the drag grip — so it returns only the pieces that can
1078
- vary, and anything richer belongs in the pane beside it.
1079
-
1080
- ```tsx
1081
- <DataViews.Tree
1082
- nodes={nodes}
1083
- renderNode={({ row }) =>
1084
- row.status ? { meta: <Badge label={String(row.status)} color="blue" badgeStyle="subtle" /> } : {}
1085
- }
1086
- />
1087
- ```
1088
-
1089
- It may return `name`, `icon` and `meta` — anything omitted keeps the default, and returning `{}`
1090
- leaves the node entirely alone:
1091
-
1092
- ```tsx
1093
- renderNode={({ row, node }) => ({
1094
- name: `${row.customer.name} (${node.children.length})`,
1095
- icon: <i className={row.status === "Delivered" ? "ri-check-line" : "ri-time-line"} />,
1096
- meta: <Badge label={String(row.status)} color="blue" badgeStyle="subtle" showIcon={false} />,
1097
- })}
1098
- ```
1099
-
1100
- ### A pane tab
1101
-
1102
- The tree's pane takes a mode of your own beside List and Cards. A tab renders its children only
1103
- while it is selected, and reads the **selected node's** rows — the pane scopes the data context, so
1104
- nothing is threaded through props:
1105
-
1106
- ```tsx
1107
- function Timeline() {
1108
- const { rows } = useDataViewsData(); // the node's rows, already narrowed by `paneRows`
1109
- return (
1110
- <ol className="p-6">
1111
- {rows.map((row) => (
1112
- <li key={String(row.id)}>{String(row.createdAt)} — {String(row.customer.name)}</li>
1113
- ))}
1114
- </ol>
1115
- );
1116
- }
1117
-
1118
- <DataViews.Tree nodes={nodes} labelPath="name">
1119
- <DataViews.Tree.Table selectable />
1120
- <DataViews.Tree.Cards renderCard={({ row }) => <OrderCard row={row} />} />
1121
- <DataViews.Tree.Tab value="timeline" label="Timeline" icon={<Clock />}>
1122
- <Timeline />
1123
- </DataViews.Tree.Tab>
1124
- </DataViews.Tree>
1125
- ```
1126
-
1127
- The switch shows exactly what you rendered. Render one tab and there is no switch; render none and
1128
- there is no pane. See [`tree-custom`](./examples/tree-custom.md).
1129
-
1130
- ### The detail pane
1131
-
1132
- The `children` of `Inbox` and `Tree` **are** the pane. `DataViews.Detail` is a sensible default,
1133
- not a requirement — write your own and `useActiveRow()` resolves whatever is open:
1134
-
1135
- ```tsx
1136
- function OrderDetail() {
1137
- const row = useActiveRow();
1138
- const { visibleFields } = useDataViewsData();
1139
- if (!row) return <p className="p-6">Select an order.</p>;
1140
- return (
1141
- <div className="flex flex-col gap-3 p-6">
1142
- {visibleFields.map((field, i) => (
1143
- <Cell key={`${field.path}-${i}`} field={field} row={row} />
1144
- ))}
1145
- </div>
1146
- );
1147
- }
1148
-
1149
- <DataViews.Inbox>
1150
- <OrderDetail />
1151
- </DataViews.Inbox>
1152
- ```
1153
-
1154
- In a tree, `useActiveRow()` returns nothing when the selected node is a synthetic branch rather
1155
- than a row — say so in the pane rather than rendering an empty shell. See
1156
- [One caveat](#one-caveat).
1157
-
1158
- ### A whole new view
1159
-
1160
- `markView` registers it in the switcher beside the built-in four. A view is not a decoration — it
1161
- is handed the same context they are, and is expected to honour the same contract: paint the fields
1162
- the panel left visible, key rows by `getRowId`, show the house skeleton while `loading`, set
1163
- `activeId` when one is opened, and ask for more when it runs out.
1164
-
1165
- Here is a complete one — a timeline grouped by date, built on the library's own `Timeline`:
1166
-
1167
- ```tsx
1168
- import {
1169
- Cell, markView, skeletonKeys, SkeletonBar,
1170
- useDataViewsData, useDataViewsView, type ViewBaseProps,
1171
- } from "@/components/DataViews";
1172
- import {
1173
- Timeline, TimelineItem, TimelineIndicator,
1174
- TimelineSeparator, TimelineConnector, TimelineContent, TimelineHeading,
1175
- } from "@/components/Timeline";
1176
- import { useInfiniteScroll } from "@/hooks/useInfiniteScroll"; // not on the DataViews barrel
1177
- import { getByPath } from "@/utils/dataViews/path";
1178
- import { cn } from "@/utils/cn";
1179
-
1180
- export const TimelineView = markView(
1181
- function TimelineView({ className }: ViewBaseProps) {
1182
- const { rows, visibleFields, getRowId, loading, loadingMore, hasMore, onLoadMore } =
1183
- useDataViewsData();
1184
- const { activeId, setActiveId } = useDataViewsView();
1185
-
1186
- // Asks for the next page as the list nears its end. `hasMore` is derived by the root.
1187
- const { sentinelRef } = useInfiniteScroll({
1188
- onLoadMore,
1189
- hasMore,
1190
- loading: loading || loadingMore,
1191
- });
1192
-
1193
- const [title, ...rest] = visibleFields;
1194
-
1195
- // One bucket per day, in the order the rows arrived — the component never sorts.
1196
- const byDay = new Map<string, typeof rows>();
1197
- for (const row of rows) {
1198
- const day = String(getByPath(row, "createdAt") ?? "—").slice(0, 10);
1199
- byDay.set(day, [...(byDay.get(day) ?? []), row]);
1200
- }
1201
-
1202
- if (loading) {
1203
- return (
1204
- <div className={cn("bg-background-presentation-form-base flex flex-col gap-4 p-6", className)}>
1205
- {skeletonKeys(6).map((i) => (
1206
- <div key={i} className="flex items-center gap-3">
1207
- <SkeletonBar className="h-[14px] w-[14px] shrink-0 rounded-full" />
1208
- <SkeletonBar className={i % 2 ? "w-[45%]" : "w-[65%]"} />
1209
- </div>
1210
- ))}
1211
- </div>
1212
- );
1213
- }
1214
-
1215
- return (
1216
- <div className={cn("bg-background-presentation-form-base h-full overflow-y-auto p-6", className)}>
1217
- {[...byDay].map(([day, dayRows]) => (
1218
- <section key={day}>
1219
- <h3 className="typography-body-small-semibold text-content-presentation-global-secondary py-2">
1220
- {day}
1221
- </h3>
1222
- <Timeline>
1223
- {dayRows.map((row, index) => {
1224
- const id = getRowId(row, index);
1225
- return (
1226
- <TimelineItem key={id}>
1227
- <TimelineSeparator>
1228
- <TimelineIndicator variant={activeId === id ? "active" : "default"} />
1229
- <TimelineConnector />
1230
- </TimelineSeparator>
1231
- <TimelineContent
1232
- role="button"
1233
- tabIndex={0}
1234
- onClick={() => setActiveId(activeId === id ? null : id)}
1235
- className="cursor-pointer"
1236
- >
1237
- <TimelineHeading>
1238
- {title && <Cell field={title} row={row} />}
1239
- </TimelineHeading>
1240
- <div className="flex items-center gap-2">
1241
- {rest.slice(0, 2).map((field, i) => (
1242
- <Cell key={`${field.path}-${i}`} field={field} row={row} />
1243
- ))}
1244
- </div>
1245
- </TimelineContent>
1246
- </TimelineItem>
1247
- );
1248
- })}
1249
- </Timeline>
1250
- </section>
1251
- ))}
1252
-
1253
- {/* The trigger, inside the scroller the rows live in. */}
1254
- {hasMore && <div ref={sentinelRef as React.Ref<HTMLDivElement>} className="h-px" />}
1255
- </div>
1256
- );
1257
- },
1258
- { defaultId: "timeline", defaultLabel: "Timeline" },
1259
- );
1260
- ```
1261
-
1262
- Render it like any other view — it appears in the switcher, and disappears if you stop rendering it:
1263
-
1264
- ```tsx
1265
- <DataViews rows={rows} fields={fields} total={total} onLoadMore={fetchNextPage}>
1266
- <DataViews.Header title="Orders">
1267
- <DataViews.ViewSwitch />
1268
- </DataViews.Header>
1269
- <DataViews.Table />
1270
- <TimelineView icon={<i className="ri-time-line" />} />
1271
- </DataViews>
1272
- ```
1273
-
1274
- Accept `ViewBaseProps` so the caller keeps `id`, `label`, `icon` and `className`.
1275
-
1276
- ### The chrome
1277
-
1278
- Buttons you put in `Actions` are `<Button variant="BluColStyle" size="M">` by convention — the
1279
- solid blue Figma uses for the bar's action, with `Search` and `PanelToggle` left ghost beside them.
1280
-
1281
- Every titled group in the rail folds: `Panel.Columns`, `Panel.Sort` and `Panel.SavedViews` all
1282
- render through `Panel.Section`, and `Filters` folds its fields the same way. Pass
1283
- `collapsible={false}` to pin one open, or `defaultOpen={false}` to start it closed.
1284
-
1285
- The surrounding parts take content too: `Header`'s `title` and `Actions`/`PanelToggle` children,
1286
- any markup inside a `Panel.Tab` (with `Panel.Section` to group it), `Filters.Custom` for a filter
1287
- no FormBuilder field covers, and `Inbox`'s `placeholder` for the empty pane.
1288
-
1289
- ## Dragging
1290
-
1291
- Four surfaces drag — board cards between columns, table rows into a manual order, the rail's
1292
- column list, and tree nodes into a new parent — and all four **work with a finger and with the
1293
- keyboard**: hold to pick up and swipe to scroll, or Space, arrows, Space. Each is opt-in by handing
1294
- over a callback, and none of them move anything on their own; the item settles where it landed only
1295
- once you hand back reordered data.
1296
-
1297
- A table's manual order and a sort are two different orders, and the component cannot know which one
1298
- you meant. It reports the drop; deciding is yours.
1299
-
1300
- ## Two colour worlds
1301
-
1302
- The chrome is always dark and the content is not. `data-theme="dark"` is scoped to the header bar
1303
- and the settings rail so their literals resolve correctly no matter what theme the host app runs
1304
- in. The views inside keep the host theme, which is why the table reads white and the board grey.
1305
- The Master Container carries the surface — a form base, a 1px global border and a 16px radius — so
1306
- anything positioned around a view inherits it.
1307
-
1308
- ## Accessibility
1309
-
1310
- What is actually implemented, rather than what a data grid usually claims:
1311
-
1312
- - **Sorting is buttons.** Each column header is a real button carrying its own `sortLabel`, so a
1313
- screen reader hears "Customer, sort ascending" rather than one identical "Sort ascending" per
1314
- column. Clicking cycles asc → desc → unsorted, so a third press returns the server's own order.
1315
- - **Rows are only focusable when they do something.** `<tr>` has no implicit role, so a row gains
1316
- `role="button"`, `tabIndex` and Enter/Space handling **only** when `onRowClick` is passed —
1317
- otherwise it stays out of the tab order instead of being an announced control that does nothing.
1318
- - **Dragging works without a pointer.** Space lifts, arrows move, Space drops, Escape cancels — on
1319
- all four draggable surfaces. Grips are labelled (`"Reorder row"`), and touch drags start on a
1320
- 200ms hold so a swipe still scrolls the list.
1321
- - **The view switcher is a tablist** (`role="tab"` / `aria-selected`), and the tree exposes
1322
- `aria-expanded` on branches and `aria-current` on the open node.
1323
- - **RTL** works from logical properties (`ms-*`, `ps-*`, `start-*`) rather than left/right, so the
1324
- whole component mirrors under `dir="rtl"` — including the panel rail, the filter controls and the
1325
- tree's indentation. `app/data-views/a11y-rtl/page.tsx` toggles it live.
1326
- - **Decorative things are hidden.** Connector lines, drag grips, skeletons and layout spacers all
1327
- carry `aria-hidden`, so the reading order is the data.
1328
-
1329
- The one thing to supply yourself: `fields[].label`. Without it a column falls back to its `path`,
1330
- and `customer.name` is what the sort control will announce.
1331
-
1332
- ## Common Patterns
1333
-
1334
- ### Fetch on every query change
1335
-
1336
- The whole component in one line of plumbing: the query is the query key, so TanStack refetches when
1337
- it changes and discards a superseded response.
1338
-
1339
- ```tsx
1340
- const [query, setQuery] = useState(emptyQuery());
1341
- const { data, isPending } = useQuery({
1342
- queryKey: ["orders", query],
1343
- queryFn: () => fetch(`/api/orders?${queryToParams(query)}`).then((r) => r.json()),
1344
- });
1345
-
1346
- <DataViews rows={data?.rows ?? []} total={data?.total ?? 0} fields={FIELDS}
1347
- loading={isPending} onQueryChange={setQuery}>…</DataViews>
1348
- ```
1349
-
1350
- ### Decode it on the server
1351
-
1352
- `parseQuery` is the other half, so the two cannot drift:
1353
-
1354
- ```ts
1355
- // app/api/orders/route.ts
1356
- export async function GET(request: Request) {
1357
- const { search, filters, sort, page, pageSize } = parseQuery(new URL(request.url));
1358
- // …your matcher, your ORM. Return { rows, total }.
1359
- }
1360
- ```
1361
-
1362
- ### Persist what the user chose
1363
-
1364
- Only the query leaves, so persistence is a two-line round trip — seed with a `default*` prop, save
1365
- from the `on*Change`:
1366
-
1367
- ```tsx
1368
- <DataViews defaultQuery={loadQuery()} onQueryChange={(q) => { save(q); setQuery(q); }}>
1369
- <DataViews.Tree defaultPaneMode={loadPref()} onPaneModeChange={savePref} />
1370
- ```
1371
-
1372
- Saved views are the same shape at a larger grain: `Panel.SavedViews` hands you a
1373
- `SavedViewSnapshot` on save, and restores whatever you hand back.
1374
-
1375
- ### Drag that survives a failed save
1376
-
1377
- `onRowMove` reports intent; nothing moves until you hand back rows that agree. That is what makes a
1378
- failed save leave the board showing the truth:
1379
-
1380
- ```tsx
1381
- <DataViews.Board groups={groups} onRowMove={(intent) => {
1382
- if (intent.to) move.mutate({ id: Number(intent.id), status: intent.to });
1383
- }} />
1384
- ```
1385
-
1386
- ### One dataset, two tabs of the same view
1387
-
1388
- Views are registered by rendering them, and `id`/`label` name them — so the same view twice is just
1389
- two elements:
1390
-
1391
- ```tsx
1392
- <DataViews.Board id="by-status" label="Status" groups={byStatus} />
1393
- <DataViews.Board id="by-owner" label="Owner" groups={byOwner} />
1394
- ```
1395
-
1396
- ## Testing
1397
-
1398
- The component is pure UI, so the assertions worth writing are about **what left** and **what was
1399
- painted** — never about internal state.
1400
-
1401
- ```tsx
1402
- it("reports a filter through onQueryChange", async () => {
1403
- const onQueryChange = vi.fn();
1404
- render(<Orders onQueryChange={onQueryChange} />);
1405
- await userEvent.click(screen.getByRole("button", { name: /filter & config/i }));
1406
- await userEvent.click(screen.getByRole("checkbox", { name: "Shipped" }));
1407
-
1408
- expect(onQueryChange).toHaveBeenLastCalledWith(
1409
- expect.objectContaining({ filters: { status: ["Shipped"] }, page: 1 }), // page reset
1410
- );
1411
- });
1412
-
1413
- it("paints nothing when there is nothing", () => {
1414
- render(<DataViews rows={[]} total={0} fields={FIELDS}><DataViews.Table /></DataViews>);
1415
- expect(screen.getByRole("columnheader", { name: "Order #" })).toBeInTheDocument();
1416
- expect(screen.queryAllByRole("row")).toHaveLength(1); // the header band only
1417
- });
1418
- ```
1419
-
1420
- Notes that save time:
1421
-
1422
- - The search box is an **input inside an expanding button** — click the button first.
1423
- - A combobox filter renders as `role="combobox"` on an `<input>`, not a `<button>`.
1424
- - Sortable column headers are **buttons inside the `columnheader`**.
1425
- - The tree's pane has its own `role="tablist"`, distinct from the header's view switcher — scope
1426
- the query or you will assert against the wrong one.
1427
- - Drag is `@dnd-kit`: it needs pointer events with an 8px move, not `fireEvent.dragStart`. Keyboard
1428
- drag (Space, arrows, Space) is usually the cheaper test.
1429
-
1430
- ## Performance
1431
-
1432
- | Concern | What the component already does | What is yours |
1433
- | --- | --- | --- |
1434
- | Large row counts | The table renders a window past **300 rows**; below that every row is real, which is what keeps row drag and column resize simple | Keep `rows` a stable reference — `data?.rows ?? []` is a new array every render and will re-run every memo downstream |
1435
- | Paging | `onLoadMore` fires once per arrival at the end, latched until the sentinel leaves | Append pages; never replace |
1436
- | Re-renders | `visibleFields`, `groups` and `nodes` are read straight through | `useMemo` your `groups`/`nodes` builders — they run on every render otherwise |
1437
- | Query churn | `page` resets internally when the query narrows | Exclude `page` from your query key when using `useInfiniteQuery`, or every page refetches the lot |
1438
- | Cell cost | `Cell` is a switch on `type` | A `render` that mounts a heavy subtree runs per visible cell — keep it cheap or memoize it |
1439
-
1440
- The board and inbox load on scroll but do **not** virtualize. The tree does neither: a tree wants
1441
- its children fetched when a node expands, not its siblings paged in, and that is not built.
1442
-
1443
- ## Styling
1444
-
1445
- `className` on any part is merged through `cn`, so a Tailwind class wins over the default. Two
1446
- things are worth knowing before you reach for it.
1447
-
1448
- **The chrome is always dark.** The header bar and the settings rail carry `data-theme="dark"`
1449
- regardless of the `theme` you pass; `theme` themes the **content**. See
1450
- [Two colour worlds](#two-colour-worlds).
1451
-
1452
- **Colour comes from tokens, never literals.** Use `presentation` tokens
1453
- (`bg-background-presentation-*`, `text-content-presentation-*`, `border-border-presentation-*`) so a
1454
- part sits correctly in both worlds. Never use `system` tokens or `variant="SystemStyle"`.
1455
-
1456
- Two `className`s **replace** rather than merge, because they are layout, not decoration:
1457
- `DataViews.Tree.Cards`'s grid classes, and `Filters`'s when you pass `title={null}`.
1458
-
1459
- ## Known Limitations
1460
-
1461
- | Limitation | Why | What to do |
1462
- | --- | --- | --- |
1463
- | No boolean filter section | `AS_FILTER` maps FormBuilder kinds to `text · choice · multiChoice · date · slider`; a checkbox has no filter meaning | Use a `RadioList` of Yes/No, or `Filters.Custom` |
1464
- | The tree does not virtualize or page | Trees want lazy children, not paged siblings | Fetch a node's children on expand and hand back new `nodes` |
1465
- | `Detail` is row-keyed, not node-keyed | It resolves `activeId` against `rows` | See [One caveat](#one-caveat) |
1466
- | A pane sort re-queries | The pane's table writes sort into the shared query | Sort inside `paneRows` for a self-contained pane |
1467
- | Filters cannot express OR | The component reports what was chosen; combining is your matcher's business | Interpret `FilterState` however you like server-side |
1468
- | Two views of one dataset share one query | That is the design — a switch must not lose the user's filters | Mount two `DataViews` if they genuinely need separate queries |
1469
-
1470
- ## Troubleshooting
1471
-
1472
- | Symptom | Cause | Fix |
1473
- | --- | --- | --- |
1474
- | Nothing renders, or "DataViews parts must be rendered inside `<DataViews>`" | A part is outside the root, or wrapped in a component of your own | Wrap the wrapper with `markView`/`markHeader`/`markPanel` |
1475
- | A view has no tab | The element is not rendered, or is behind a falsy condition | A part exists because you rendered it |
1476
- | Filtering does nothing | You are waiting for the component to filter | It does not. Fetch with the new query and hand back new `rows` |
1477
- | A filter never appears in `filters` | The field's kind has no filter meaning (`Checkbox`, `File`, `Custom`) | Use a supported control or `Filters.Custom` |
1478
- | `setValue("customer.name", …)` does nothing | react-hook-form reads `.` as nesting | Use the escaped name — `customer__name` |
1479
- | Scroll loading fires repeatedly | `hasMore` never goes false | It is derived from `rows.length < total` — check `total` |
1480
- | Scroll loading never fires | Rows were replaced instead of appended | Append each page |
1481
- | The tree shows no pane | No tabs were passed | Render `<DataViews.Tree.Table/>` / `.Cards`, or anything else as the pane |
1482
- | A pane tab shows the whole dataset | It read rows from outside the pane | Read `useDataViewsData()` **inside** the tab — the pane scopes it |
1483
- | Drag does nothing on a phone | Nothing is wrong; hold 200ms first | A shorter delay cannot be told apart from a scroll |
1484
- | Types will not import | Reaching into `@/utils/dataViews/types` | Everything is re-exported from `@/components/DataViews` |
1485
-
1486
- ## Example pages
1487
-
1488
- Every one is a complete, runnable page — generated into the docs from the app, so the code below
1489
- travels with the package rather than living in a repo you may not have.
1490
-
1491
- | Page | Shows |
1492
- | --- | --- |
1493
- | [`overview`](./examples/overview.md) | Every part at once — the fastest way to see the whole shape |
1494
- | [`views`](./examples/views.md) | All four views over one dataset, with drag round-trips |
1495
- | [`tree-custom`](./examples/tree-custom.md) | Every custom-UI seam of the tree: `renderNode`, `paneRows`, a custom cell, card, tab, `paneActions`, and a whole-pane override |
1496
- | [`inbox-routing`](./examples/inbox-routing.md) | `itemHref` + `linkComponent` — the pane driven by the URL |
1497
- | [`fields`](./examples/fields.md) | The field types, painted |
1498
- | [`filters`](./examples/filters.md) | Every filter control, presets, custom filters, the summary |
1499
- | [`server-side`](./examples/server-side.md) | `queryToParams` out, `parseQuery` in |
1500
- | [`scale`](./examples/scale.md) | Virtualization and scroll loading at size |
1501
- | [`panel`](./examples/panel.md) | The rail: saved views, columns, sort — and the pane-mode round trip |
1502
- | [`state`](./examples/state.md) | Controlled vs uncontrolled query |
1503
- | [`view-registry`](./examples/view-registry.md) | A view of your own via `markView`, beside the built-in four |
1504
- | [`a11y-rtl`](./examples/a11y-rtl.md) | Keyboard paths and the RTL mirror |
1505
-
1506
- ## Related Components
1507
-
1508
- | Component | When |
1509
- | --- | --- |
1510
- | [`Table`](../table.md) | You need one table, not several views of one dataset. `DataViews.Table` composes it. |
1511
- | [`DataTable`](../data-table.md) | A self-contained TanStack table that sorts, filters and pages **client-side**. Reach for it when the data is already in the browser; reach for `DataViews` when the server owns the query. |
1512
- | [`TreeFolder`](../tree-folder.md) | The tree on its own, without the surrounding views. |
1513
- | [`FormBuilder`](../form-builder.md) | Writes the filter controls — `DataViews.Filters` takes its fields as children. |
1514
- | `DataViewCard` | The card the board renders (a layout, `@/layouts/DataViewCard`), usable directly. |
1515
-
1516
- ## One caveat
1517
-
1518
- `DataViews.Detail` resolves `activeId` against `rows` via `getRowId`. The tree selects a **node**
1519
- id, so `Detail` fills in only when the node's id is also a row id. Select a synthetic grouping node
1520
- and there is no matching row, so it renders nothing. Either key your leaf nodes by row id, or
1521
- render your own pane.