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