@pilotiq/pilotiq 0.24.1 → 0.24.2

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 (480) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/boost/guidelines.md +566 -0
  3. package/boost/skills/pilotiq-fields/SKILL.md +47 -0
  4. package/boost/skills/pilotiq-fields/rules/field-catalog.md +288 -0
  5. package/boost/skills/pilotiq-fields/rules/reactive-fields.md +199 -0
  6. package/boost/skills/pilotiq-fields/rules/validation.md +198 -0
  7. package/boost/skills/pilotiq-relations/SKILL.md +47 -0
  8. package/boost/skills/pilotiq-relations/rules/relation-managers.md +256 -0
  9. package/boost/skills/pilotiq-relations/rules/repeater-relationship.md +177 -0
  10. package/boost/skills/pilotiq-resource/SKILL.md +61 -0
  11. package/boost/skills/pilotiq-resource/rules/authorization.md +242 -0
  12. package/boost/skills/pilotiq-resource/rules/defining-resources.md +228 -0
  13. package/boost/skills/pilotiq-resource/rules/page-overrides.md +296 -0
  14. package/package.json +6 -1
  15. package/.turbo/turbo-build.log +0 -8
  16. package/CLAUDE.md +0 -265
  17. package/src/Cluster.test.ts +0 -283
  18. package/src/Cluster.ts +0 -83
  19. package/src/Column.test.ts +0 -199
  20. package/src/Column.ts +0 -710
  21. package/src/Global.test.ts +0 -367
  22. package/src/Global.ts +0 -169
  23. package/src/Page.test.ts +0 -114
  24. package/src/Page.ts +0 -208
  25. package/src/Pilotiq.perf.test.ts +0 -252
  26. package/src/Pilotiq.test.ts +0 -129
  27. package/src/Pilotiq.ts +0 -1158
  28. package/src/PilotiqRegistry.ts +0 -36
  29. package/src/PilotiqServiceProvider.ts +0 -121
  30. package/src/RelationManager.test.ts +0 -400
  31. package/src/RelationManager.ts +0 -527
  32. package/src/RenderHook.test.ts +0 -252
  33. package/src/RenderHook.ts +0 -242
  34. package/src/Resource.test.ts +0 -284
  35. package/src/Resource.ts +0 -526
  36. package/src/RightPanel.test.ts +0 -202
  37. package/src/RightPanel.ts +0 -132
  38. package/src/Tab.test.ts +0 -91
  39. package/src/Tab.ts +0 -156
  40. package/src/UserMenuItem.ts +0 -145
  41. package/src/actions/Action.test.ts +0 -2526
  42. package/src/actions/Action.ts +0 -1515
  43. package/src/actions/ActionGroup.test.ts +0 -112
  44. package/src/actions/ActionGroup.ts +0 -173
  45. package/src/actions/attachFactory.ts +0 -172
  46. package/src/actions/bulkFactories.ts +0 -168
  47. package/src/actions/crudFactories.ts +0 -220
  48. package/src/actions/exportFactory.ts +0 -225
  49. package/src/actions/factoryHelpers.ts +0 -177
  50. package/src/actions/importFactory.ts +0 -243
  51. package/src/actions/index.ts +0 -17
  52. package/src/actions/m2mFactories.ts +0 -193
  53. package/src/actions/relationFactories.ts +0 -372
  54. package/src/applyPageHooks.test.ts +0 -463
  55. package/src/applyPageHooks.ts +0 -330
  56. package/src/authorization.test.ts +0 -483
  57. package/src/breadcrumbs.test.ts +0 -238
  58. package/src/cells/coerce.test.ts +0 -85
  59. package/src/cells/coerce.ts +0 -84
  60. package/src/clusterPaths.ts +0 -35
  61. package/src/columns/BadgeColumn.test.ts +0 -54
  62. package/src/columns/BadgeColumn.ts +0 -32
  63. package/src/columns/BooleanColumn.test.ts +0 -41
  64. package/src/columns/BooleanColumn.ts +0 -18
  65. package/src/columns/ColorColumn.test.ts +0 -37
  66. package/src/columns/ColorColumn.ts +0 -38
  67. package/src/columns/IconColumn.test.ts +0 -54
  68. package/src/columns/IconColumn.ts +0 -37
  69. package/src/columns/ImageColumn.test.ts +0 -41
  70. package/src/columns/ImageColumn.ts +0 -28
  71. package/src/columns/SelectColumn.ts +0 -98
  72. package/src/columns/TextColumn.test.ts +0 -190
  73. package/src/columns/TextColumn.ts +0 -20
  74. package/src/columns/TextInputColumn.ts +0 -68
  75. package/src/columns/ToggleColumn.ts +0 -46
  76. package/src/columns/editableColumns.test.ts +0 -238
  77. package/src/columns/index.ts +0 -9
  78. package/src/defaultGlobalPages.ts +0 -95
  79. package/src/defaultPages.test.ts +0 -634
  80. package/src/defaultPages.ts +0 -617
  81. package/src/defaultViewPage.test.ts +0 -147
  82. package/src/elements/Form.test.ts +0 -223
  83. package/src/elements/Form.ts +0 -416
  84. package/src/elements/ListTabs.ts +0 -28
  85. package/src/elements/Table.test.ts +0 -422
  86. package/src/elements/Table.ts +0 -850
  87. package/src/elements/TableGroup.test.ts +0 -260
  88. package/src/elements/TableGroup.ts +0 -334
  89. package/src/elements/dispatchAction.test.ts +0 -463
  90. package/src/elements/dispatchAction.ts +0 -355
  91. package/src/elements/dispatchForm.test.ts +0 -477
  92. package/src/elements/dispatchForm.ts +0 -1993
  93. package/src/elements/dispatchTable.test.ts +0 -1514
  94. package/src/elements/dispatchTable.ts +0 -745
  95. package/src/elements/index.ts +0 -21
  96. package/src/entries/BadgeEntry.ts +0 -39
  97. package/src/entries/CodeEntry.test.ts +0 -40
  98. package/src/entries/CodeEntry.ts +0 -52
  99. package/src/entries/ColorEntry.ts +0 -63
  100. package/src/entries/ComponentEntry.test.ts +0 -173
  101. package/src/entries/ComponentEntry.ts +0 -95
  102. package/src/entries/Entry.ts +0 -304
  103. package/src/entries/IconEntry.ts +0 -49
  104. package/src/entries/ImageEntry.ts +0 -61
  105. package/src/entries/KeyValueEntry.ts +0 -47
  106. package/src/entries/RepeatableEntry.test.ts +0 -239
  107. package/src/entries/RepeatableEntry.ts +0 -173
  108. package/src/entries/TextEntry.test.ts +0 -394
  109. package/src/entries/TextEntry.ts +0 -60
  110. package/src/entries/index.ts +0 -12
  111. package/src/entries/leaves.test.ts +0 -306
  112. package/src/entries/registry.ts +0 -54
  113. package/src/fields/BuilderField.test.ts +0 -1188
  114. package/src/fields/BuilderField.ts +0 -605
  115. package/src/fields/BuilderRelationship.test.ts +0 -811
  116. package/src/fields/CheckboxField.test.ts +0 -44
  117. package/src/fields/CheckboxField.ts +0 -27
  118. package/src/fields/CheckboxListField.test.ts +0 -99
  119. package/src/fields/CheckboxListField.ts +0 -66
  120. package/src/fields/ColorPickerField.test.ts +0 -33
  121. package/src/fields/ColorPickerField.ts +0 -25
  122. package/src/fields/DateField.ts +0 -54
  123. package/src/fields/DateTimeField.test.ts +0 -55
  124. package/src/fields/EmailField.ts +0 -16
  125. package/src/fields/Field.test.ts +0 -654
  126. package/src/fields/Field.ts +0 -817
  127. package/src/fields/FileUploadField.test.ts +0 -143
  128. package/src/fields/FileUploadField.ts +0 -159
  129. package/src/fields/HiddenField.test.ts +0 -27
  130. package/src/fields/HiddenField.ts +0 -28
  131. package/src/fields/KeyValueField.test.ts +0 -105
  132. package/src/fields/KeyValueField.ts +0 -55
  133. package/src/fields/MarkdownField.test.ts +0 -167
  134. package/src/fields/MarkdownField.ts +0 -162
  135. package/src/fields/NumberField.ts +0 -33
  136. package/src/fields/RadioField.test.ts +0 -94
  137. package/src/fields/RadioField.ts +0 -67
  138. package/src/fields/RepeaterField.test.ts +0 -1806
  139. package/src/fields/RepeaterField.ts +0 -939
  140. package/src/fields/RepeaterRelationship.test.ts +0 -1923
  141. package/src/fields/RepeaterSimple.test.ts +0 -248
  142. package/src/fields/RowButton.test.ts +0 -219
  143. package/src/fields/RowButton.ts +0 -135
  144. package/src/fields/SelectField.test.ts +0 -192
  145. package/src/fields/SelectField.ts +0 -235
  146. package/src/fields/SliderField.test.ts +0 -50
  147. package/src/fields/SliderField.ts +0 -53
  148. package/src/fields/SlugField.ts +0 -24
  149. package/src/fields/TagsInputField.test.ts +0 -154
  150. package/src/fields/TagsInputField.ts +0 -133
  151. package/src/fields/TextField.test.ts +0 -213
  152. package/src/fields/TextField.ts +0 -177
  153. package/src/fields/TextareaField.test.ts +0 -58
  154. package/src/fields/TextareaField.ts +0 -59
  155. package/src/fields/ToggleButtonsField.test.ts +0 -106
  156. package/src/fields/ToggleButtonsField.ts +0 -59
  157. package/src/fields/ToggleField.ts +0 -16
  158. package/src/fields/disableOptionsWhenSelectedInSiblingRepeaterItems.test.ts +0 -319
  159. package/src/fields/optionsResolver.ts +0 -95
  160. package/src/fields/resolveField.ts +0 -28
  161. package/src/filters/BooleanFilter.ts +0 -35
  162. package/src/filters/DateRangeFilter.test.ts +0 -194
  163. package/src/filters/DateRangeFilter.ts +0 -148
  164. package/src/filters/Filter.test.ts +0 -268
  165. package/src/filters/Filter.ts +0 -184
  166. package/src/filters/FormFilter.test.ts +0 -238
  167. package/src/filters/FormFilter.ts +0 -215
  168. package/src/filters/MultiSelectFilter.test.ts +0 -119
  169. package/src/filters/MultiSelectFilter.ts +0 -78
  170. package/src/filters/QueryBuilderFilter.test.ts +0 -662
  171. package/src/filters/QueryBuilderFilter.ts +0 -398
  172. package/src/filters/SelectFilter.ts +0 -46
  173. package/src/filters/TernaryFilter.test.ts +0 -160
  174. package/src/filters/TernaryFilter.ts +0 -72
  175. package/src/filters/TrashedFilter.test.ts +0 -149
  176. package/src/filters/TrashedFilter.ts +0 -55
  177. package/src/filters/queryBuilder/BooleanConstraint.ts +0 -31
  178. package/src/filters/queryBuilder/Constraint.ts +0 -115
  179. package/src/filters/queryBuilder/DateConstraint.ts +0 -69
  180. package/src/filters/queryBuilder/NumberConstraint.ts +0 -66
  181. package/src/filters/queryBuilder/SelectConstraint.ts +0 -72
  182. package/src/filters/queryBuilder/TextConstraint.ts +0 -64
  183. package/src/filters/queryBuilder/index.ts +0 -12
  184. package/src/icons/index.ts +0 -2
  185. package/src/icons/lucide.ts +0 -204
  186. package/src/icons/registry.test.ts +0 -56
  187. package/src/icons/registry.ts +0 -41
  188. package/src/icons/types.ts +0 -47
  189. package/src/index.ts +0 -525
  190. package/src/io/csv.test.ts +0 -142
  191. package/src/io/csv.ts +0 -170
  192. package/src/nestedRelationManagerData.test.ts +0 -547
  193. package/src/notifications/Notification.test.ts +0 -210
  194. package/src/notifications/Notification.ts +0 -354
  195. package/src/notifications/broadcast.test.ts +0 -110
  196. package/src/notifications/broadcast.ts +0 -95
  197. package/src/notifications/database.test.ts +0 -383
  198. package/src/notifications/database.ts +0 -398
  199. package/src/notifications/databaseNotifications.test.ts +0 -187
  200. package/src/notifications/dispatchNotificationAction.test.ts +0 -341
  201. package/src/notifications/dispatchNotificationAction.ts +0 -142
  202. package/src/notifications/flash.test.ts +0 -89
  203. package/src/notifications/flash.ts +0 -71
  204. package/src/notifications/index.ts +0 -45
  205. package/src/notifications/registerBroadcastAuth.test.ts +0 -134
  206. package/src/notifications/registerBroadcastAuth.ts +0 -100
  207. package/src/notifications/resolveSavedNotification.test.ts +0 -82
  208. package/src/notifications/resolveSavedNotification.ts +0 -59
  209. package/src/notifications/types.ts +0 -93
  210. package/src/orm/m2mAccessor.ts +0 -66
  211. package/src/orm/modelDefaults.test.ts +0 -633
  212. package/src/orm/modelDefaults.ts +0 -666
  213. package/src/pageData/breadcrumbs.ts +0 -288
  214. package/src/pageData/forms.ts +0 -578
  215. package/src/pageData/helpers.ts +0 -857
  216. package/src/pageData/misc.ts +0 -347
  217. package/src/pageData/navigation.ts +0 -842
  218. package/src/pageData/relationPages.ts +0 -1248
  219. package/src/pageData/relationTabs.ts +0 -286
  220. package/src/pageData/resourcePages.ts +0 -609
  221. package/src/pageData.test.ts +0 -1545
  222. package/src/pageData.ts +0 -341
  223. package/src/plugins/index.ts +0 -8
  224. package/src/plugins/themeEditor.test.ts +0 -36
  225. package/src/plugins/themeEditor.ts +0 -45
  226. package/src/react/AppShell.tsx +0 -251
  227. package/src/react/CollabExtensionFactoryRegistry.ts +0 -55
  228. package/src/react/CollabRoomContext.ts +0 -98
  229. package/src/react/CollabTextRendererRegistry.ts +0 -102
  230. package/src/react/CommandPalette.tsx +0 -375
  231. package/src/react/CurrentUserContext.tsx +0 -50
  232. package/src/react/CustomPageWrapperGate.tsx +0 -69
  233. package/src/react/CustomPageWrapperRegistry.ts +0 -45
  234. package/src/react/FieldFocusReporterRegistry.ts +0 -37
  235. package/src/react/FieldLabelSlotRegistry.ts +0 -30
  236. package/src/react/FieldPresenceRegistry.ts +0 -46
  237. package/src/react/FormCollabBindingRegistry.ts +0 -242
  238. package/src/react/FormStateContext.tsx +0 -591
  239. package/src/react/HeadHooks.tsx +0 -126
  240. package/src/react/MarkdownEditorRegistry.test.ts +0 -38
  241. package/src/react/MarkdownEditorRegistry.ts +0 -107
  242. package/src/react/NotificationActionStrip.tsx +0 -263
  243. package/src/react/NotificationBell.tsx +0 -426
  244. package/src/react/PendingSuggestionApplierRegistry.test.ts +0 -97
  245. package/src/react/PendingSuggestionApplierRegistry.ts +0 -98
  246. package/src/react/PendingSuggestionOverlayRegistry.ts +0 -54
  247. package/src/react/PendingSuggestionsContext.tsx +0 -172
  248. package/src/react/RecordWrapperGate.tsx +0 -58
  249. package/src/react/RecordWrapperRegistry.ts +0 -39
  250. package/src/react/RenderHookSlot.tsx +0 -32
  251. package/src/react/RightSidebar.tsx +0 -257
  252. package/src/react/RightSidebarContext.tsx +0 -234
  253. package/src/react/RightSidebarTrigger.tsx +0 -53
  254. package/src/react/RowCoordsContext.tsx +0 -23
  255. package/src/react/SchemaRenderer.tsx +0 -549
  256. package/src/react/SearchTrigger.tsx +0 -46
  257. package/src/react/ThemeProvider.tsx +0 -93
  258. package/src/react/ThemeSettingsPage.tsx +0 -579
  259. package/src/react/ThemeToggle.tsx +0 -20
  260. package/src/react/Toaster.tsx +0 -158
  261. package/src/react/UserMenu.tsx +0 -196
  262. package/src/react/WidgetDataContext.tsx +0 -157
  263. package/src/react/cells/EditableCell.tsx +0 -389
  264. package/src/react/component-slots.test.ts +0 -103
  265. package/src/react/component-slots.ts +0 -116
  266. package/src/react/fieldJsHandler.test.ts +0 -166
  267. package/src/react/fieldJsHandler.ts +0 -79
  268. package/src/react/fields/BuilderInput.tsx +0 -1078
  269. package/src/react/fields/CheckboxInput.tsx +0 -39
  270. package/src/react/fields/CheckboxListInput.tsx +0 -102
  271. package/src/react/fields/ColorInput.tsx +0 -71
  272. package/src/react/fields/DateFieldInput.tsx +0 -70
  273. package/src/react/fields/DateTimeInput.tsx +0 -62
  274. package/src/react/fields/FieldShell.tsx +0 -348
  275. package/src/react/fields/FileUploadInput.tsx +0 -639
  276. package/src/react/fields/HiddenInput.tsx +0 -17
  277. package/src/react/fields/KeyValueInput.tsx +0 -230
  278. package/src/react/fields/MarkdownInput.tsx +0 -560
  279. package/src/react/fields/RadioInput.tsx +0 -81
  280. package/src/react/fields/RepeaterInput.test.ts +0 -116
  281. package/src/react/fields/RepeaterInput.tsx +0 -1420
  282. package/src/react/fields/SelectFieldInput.tsx +0 -280
  283. package/src/react/fields/SliderInput.tsx +0 -81
  284. package/src/react/fields/TagsInput.tsx +0 -283
  285. package/src/react/fields/TextLikeInput.tsx +0 -256
  286. package/src/react/fields/ToggleButtonsInput.tsx +0 -60
  287. package/src/react/fields/ToggleFieldInput.tsx +0 -56
  288. package/src/react/fields/relationshipRenameDispatch.test.ts +0 -106
  289. package/src/react/fields/relationshipRenameDispatch.ts +0 -97
  290. package/src/react/fields/repeaterReconcile.test.ts +0 -114
  291. package/src/react/fields/repeaterReconcile.ts +0 -104
  292. package/src/react/fields/rowChromeButton.tsx +0 -336
  293. package/src/react/fields/rowState.ts +0 -106
  294. package/src/react/fields/syncRowGates.test.ts +0 -202
  295. package/src/react/fields/syncRowGates.ts +0 -66
  296. package/src/react/fields/textInputControls.tsx +0 -238
  297. package/src/react/fields/useRowReorderDnd.ts +0 -78
  298. package/src/react/formStateHelpers.test.ts +0 -508
  299. package/src/react/formStateHelpers.ts +0 -381
  300. package/src/react/hooks/use-mobile.ts +0 -19
  301. package/src/react/icon-context.tsx +0 -60
  302. package/src/react/index.ts +0 -194
  303. package/src/react/layouts/SidebarLayout.tsx +0 -250
  304. package/src/react/layouts/TopbarLayout.tsx +0 -258
  305. package/src/react/navigate.tsx +0 -37
  306. package/src/react/onProviderSynced.test.ts +0 -90
  307. package/src/react/parseRecordEditUrl.test.ts +0 -122
  308. package/src/react/parseRecordEditUrl.ts +0 -94
  309. package/src/react/persistedState.ts +0 -40
  310. package/src/react/registry.ts +0 -48
  311. package/src/react/right-panel-registry.tsx +0 -47
  312. package/src/react/schemaRenderer/AlertRenderer.tsx +0 -112
  313. package/src/react/schemaRenderer/EntryRenderer.tsx +0 -501
  314. package/src/react/schemaRenderer/SectionRenderer.tsx +0 -120
  315. package/src/react/schemaRenderer/SimpleElements.tsx +0 -306
  316. package/src/react/schemaRenderer/TabsRenderer.tsx +0 -62
  317. package/src/react/schemaRenderer/WizardRenderer.tsx +0 -338
  318. package/src/react/schemaRenderer/action/ActionGroupTrigger.tsx +0 -177
  319. package/src/react/schemaRenderer/action/ActionModalDialog.tsx +0 -273
  320. package/src/react/schemaRenderer/action/ConfirmActionDialog.tsx +0 -61
  321. package/src/react/schemaRenderer/action/HandlerActionButton.tsx +0 -43
  322. package/src/react/schemaRenderer/action/MethodActionButton.tsx +0 -64
  323. package/src/react/schemaRenderer/action/buttons.tsx +0 -99
  324. package/src/react/schemaRenderer/action/helpers.ts +0 -140
  325. package/src/react/schemaRenderer/action/renderAction.tsx +0 -245
  326. package/src/react/schemaRenderer/columnFormat.ts +0 -65
  327. package/src/react/schemaRenderer/constants.ts +0 -50
  328. package/src/react/schemaRenderer/form/FormRenderer.tsx +0 -274
  329. package/src/react/schemaRenderer/form/renderField.tsx +0 -511
  330. package/src/react/schemaRenderer/helpers.tsx +0 -81
  331. package/src/react/schemaRenderer/table/CardsLayoutBody.tsx +0 -308
  332. package/src/react/schemaRenderer/table/TableRenderer.tsx +0 -123
  333. package/src/react/schemaRenderer/table/TableRendererBody.tsx +0 -974
  334. package/src/react/schemaRenderer/table/filters.tsx +0 -1233
  335. package/src/react/schemaRenderer/table/formatCell.tsx +0 -264
  336. package/src/react/schemaRenderer/table/links.tsx +0 -112
  337. package/src/react/schemaRenderer/table/renderRowActions.tsx +0 -52
  338. package/src/react/schemaRenderer/table/url.tsx +0 -143
  339. package/src/react/theme-preview/apply.ts +0 -99
  340. package/src/react/theme-preview/build-html.ts +0 -436
  341. package/src/react/ui/button.tsx +0 -51
  342. package/src/react/ui/calendar.tsx +0 -67
  343. package/src/react/ui/checkbox.tsx +0 -29
  344. package/src/react/ui/dialog.tsx +0 -108
  345. package/src/react/ui/dropdown-menu.tsx +0 -97
  346. package/src/react/ui/input.tsx +0 -20
  347. package/src/react/ui/label.tsx +0 -21
  348. package/src/react/ui/popover.tsx +0 -50
  349. package/src/react/ui/select.tsx +0 -169
  350. package/src/react/ui/separator.tsx +0 -25
  351. package/src/react/ui/sheet.tsx +0 -136
  352. package/src/react/ui/sidebar.tsx +0 -723
  353. package/src/react/ui/skeleton.tsx +0 -13
  354. package/src/react/ui/slider.tsx +0 -34
  355. package/src/react/ui/switch.tsx +0 -28
  356. package/src/react/ui/table.tsx +0 -105
  357. package/src/react/ui/tabs.tsx +0 -63
  358. package/src/react/ui/textarea.tsx +0 -18
  359. package/src/react/ui/tooltip.tsx +0 -64
  360. package/src/react/useResizableWidth.ts +0 -139
  361. package/src/react/utils.ts +0 -6
  362. package/src/react/widgetRegistry.test.ts +0 -43
  363. package/src/react/widgetRegistry.ts +0 -50
  364. package/src/react/widgets/StatsOverviewRenderer.tsx +0 -232
  365. package/src/react/widgets/TableWidgetRenderer.tsx +0 -231
  366. package/src/react/widgets/ViewRenderer.tsx +0 -71
  367. package/src/relationManagerData.test.ts +0 -1595
  368. package/src/richtext/index.ts +0 -8
  369. package/src/richtext/registry.ts +0 -89
  370. package/src/routes/globals.ts +0 -148
  371. package/src/routes/guard.test.ts +0 -325
  372. package/src/routes/helpers.ts +0 -704
  373. package/src/routes/pages.ts +0 -175
  374. package/src/routes/panel.ts +0 -204
  375. package/src/routes/relations.ts +0 -1243
  376. package/src/routes/resources.ts +0 -781
  377. package/src/routes/theme.ts +0 -91
  378. package/src/routes-nested-relations.test.ts +0 -676
  379. package/src/routes-relations.test.ts +0 -972
  380. package/src/routes.test.ts +0 -2027
  381. package/src/routes.ts +0 -303
  382. package/src/schema/Alert.test.ts +0 -109
  383. package/src/schema/Alert.ts +0 -131
  384. package/src/schema/Block.ts +0 -169
  385. package/src/schema/Breadcrumbs.ts +0 -40
  386. package/src/schema/Card.ts +0 -35
  387. package/src/schema/Divider.ts +0 -20
  388. package/src/schema/Element.ts +0 -219
  389. package/src/schema/EmptyState.test.ts +0 -37
  390. package/src/schema/EmptyState.ts +0 -63
  391. package/src/schema/Fieldset.ts +0 -43
  392. package/src/schema/Grid.ts +0 -43
  393. package/src/schema/Group.ts +0 -30
  394. package/src/schema/Heading.ts +0 -39
  395. package/src/schema/Html.ts +0 -67
  396. package/src/schema/Icon.ts +0 -54
  397. package/src/schema/Image.ts +0 -57
  398. package/src/schema/LinkTag.ts +0 -41
  399. package/src/schema/Markdown.ts +0 -85
  400. package/src/schema/MetaTag.ts +0 -41
  401. package/src/schema/RelationTabs.ts +0 -71
  402. package/src/schema/ScriptTag.ts +0 -55
  403. package/src/schema/Section.ts +0 -160
  404. package/src/schema/ServerDataElement.test.ts +0 -140
  405. package/src/schema/ServerDataElement.ts +0 -156
  406. package/src/schema/SlotComponent.test.ts +0 -77
  407. package/src/schema/SlotComponent.ts +0 -71
  408. package/src/schema/Split.ts +0 -50
  409. package/src/schema/Stat.test.ts +0 -118
  410. package/src/schema/Stat.ts +0 -154
  411. package/src/schema/StatsOverview.test.ts +0 -141
  412. package/src/schema/StatsOverview.ts +0 -119
  413. package/src/schema/StyleTag.ts +0 -35
  414. package/src/schema/TableWidget.test.ts +0 -297
  415. package/src/schema/TableWidget.ts +0 -289
  416. package/src/schema/Tabs.ts +0 -79
  417. package/src/schema/Text.ts +0 -58
  418. package/src/schema/UnorderedList.ts +0 -49
  419. package/src/schema/View.test.ts +0 -111
  420. package/src/schema/View.ts +0 -127
  421. package/src/schema/Wizard.ts +0 -220
  422. package/src/schema/containers.test.ts +0 -564
  423. package/src/schema/headTags.test.ts +0 -134
  424. package/src/schema/index.ts +0 -40
  425. package/src/schema/primes.test.ts +0 -269
  426. package/src/schema/resolveSchema.test.ts +0 -379
  427. package/src/schema/resolveSchema.ts +0 -917
  428. package/src/schema/sanitize.ts +0 -58
  429. package/src/search.test.ts +0 -446
  430. package/src/search.ts +0 -178
  431. package/src/sessionFilters.test.ts +0 -375
  432. package/src/sessionFilters.ts +0 -143
  433. package/src/slot-components/index.ts +0 -10
  434. package/src/slot-components/registry.ts +0 -56
  435. package/src/styles/file-upload.css +0 -13
  436. package/src/summarizers/Summarizer.test.ts +0 -84
  437. package/src/summarizers/Summarizer.ts +0 -123
  438. package/src/summarizers/index.ts +0 -11
  439. package/src/theme/base-colors.ts +0 -68
  440. package/src/theme/chart-colors.ts +0 -50
  441. package/src/theme/colors.ts +0 -447
  442. package/src/theme/generate-css.test.ts +0 -139
  443. package/src/theme/generate-css.ts +0 -44
  444. package/src/theme/generate-scale.test.ts +0 -106
  445. package/src/theme/generate-scale.ts +0 -97
  446. package/src/theme/icon-map.ts +0 -42
  447. package/src/theme/index.ts +0 -34
  448. package/src/theme/migrate.test.ts +0 -178
  449. package/src/theme/migrate.ts +0 -81
  450. package/src/theme/presets.ts +0 -135
  451. package/src/theme/radius.ts +0 -18
  452. package/src/theme/resolve.test.ts +0 -238
  453. package/src/theme/resolve.ts +0 -96
  454. package/src/theme/spacing.ts +0 -18
  455. package/src/theme/storage.test.ts +0 -126
  456. package/src/theme/storage.ts +0 -106
  457. package/src/theme/theme-colors.ts +0 -88
  458. package/src/theme/types.ts +0 -125
  459. package/src/uploads/UploadAdapter.ts +0 -35
  460. package/src/uploads/index.ts +0 -2
  461. package/src/uploads/localUpload.test.ts +0 -70
  462. package/src/uploads/localUpload.ts +0 -84
  463. package/src/validation/Validator.ts +0 -49
  464. package/src/validation/index.ts +0 -28
  465. package/src/validation/rules.ts +0 -78
  466. package/src/validation/runValidators.ts +0 -435
  467. package/src/validation/uniqueValidator.test.ts +0 -196
  468. package/src/validation/uniqueValidator.ts +0 -133
  469. package/src/validation/validators.test.ts +0 -268
  470. package/src/vite.test.ts +0 -184
  471. package/src/vite.ts +0 -787
  472. package/src/widgets/index.ts +0 -10
  473. package/src/widgets/registry.ts +0 -45
  474. package/src/widgets.test.ts +0 -592
  475. package/tsconfig.build.json +0 -11
  476. package/tsconfig.json +0 -4
  477. package/tsconfig.test.json +0 -10
  478. package/views/react/Dashboard.tsx +0 -27
  479. package/views/react/Resources/Form.tsx +0 -102
  480. package/views/react/Resources/Index.tsx +0 -49
@@ -1,1233 +0,0 @@
1
- import React, { useRef, useState } from 'react'
2
- import { CheckIcon, Columns3Icon, FilterIcon } from 'lucide-react'
3
- import type { ElementMeta } from '../../../schema/Element.js'
4
- import { Checkbox } from '../../ui/checkbox.js'
5
- import { Input } from '../../ui/input.js'
6
- import { Popover, PopoverContent, PopoverTrigger } from '../../ui/popover.js'
7
- import {
8
- Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
9
- } from '../../ui/select.js'
10
- import {
11
- DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
12
- } from '../../ui/dropdown-menu.js'
13
- import { useNavigate } from '../../navigate.js'
14
- import {
15
- parseDateRangeValue, encodeDateRangeValue,
16
- } from '../../../filters/DateRangeFilter.js'
17
- import {
18
- parseMultiSelectValue, encodeMultiSelectValue,
19
- } from '../../../filters/MultiSelectFilter.js'
20
- import { encodeFormFilterValue } from '../../../filters/FormFilter.js'
21
- import {
22
- parseQueryBuilderValue, encodeQueryBuilderValue, isQueryBuilderTree,
23
- type QueryBuilderRule, type QueryBuilderTree, type QueryBuilderTreeChild,
24
- } from '../../../filters/QueryBuilderFilter.js'
25
- import type {
26
- ConstraintMeta, ConstraintOperator, ConstraintOperatorName, ConstraintValueKind,
27
- } from '../../../filters/queryBuilder/Constraint.js'
28
- import {
29
- COLUMN_COLOR_CLASSES,
30
- } from '../constants.js'
31
- import { resolveIcon } from '../helpers.js'
32
- import { patchFilterUrl } from './url.js'
33
-
34
- // ─── Filter chrome + table-toolbar dropdowns ────────────────
35
- //
36
- // Every filter UI piece — from the per-filter widget (Select / MultiSelect
37
- // / DateRange / Form / QueryBuilder) to the wrapping Popover / inline
38
- // strip / collapsible toggle — plus toolbar siblings (SortByPicker,
39
- // ColumnsToggleDropdown, TableGroupPicker) and the renderFilterControl
40
- // switch that picks the right widget per filter kind.
41
-
42
-
43
- /**
44
- * Active-filters bar — pill row above the table summarising every filter
45
- * with a current value. Each pill shows the filter's `indicator` text
46
- * (server-formatted via `Filter.indicator()` / per-subclass defaults) and
47
- * an `×` button that clears that filter's URL key in place. Clicking ×
48
- * also drops `?page` so users land on the first page of the relaxed set.
49
- *
50
- * Renders nothing when no filter has an indicator.
51
- */
52
- export function ActiveFiltersBar({ filters, prefix }: { filters: ElementMeta[]; prefix?: string | undefined }) {
53
- const navigate = useNavigate()
54
- const active = filters.filter(f => typeof f['indicator'] === 'string' && f['indicator'] !== '')
55
- if (active.length === 0) return null
56
-
57
- const clear = (name: string): void => {
58
- patchFilterUrl(navigate, prefix, { [name]: null })
59
- }
60
-
61
- const clearAll = (): void => {
62
- const patches: Record<string, string | null> = {}
63
- for (const f of active) patches[String(f['name'] ?? '')] = null
64
- patchFilterUrl(navigate, prefix, patches)
65
- }
66
-
67
- return (
68
- <div className="flex flex-wrap items-center gap-2 text-xs">
69
- {active.map((f, i) => {
70
- const name = String(f['name'] ?? '')
71
- const indicator = String(f['indicator'] ?? '')
72
- return (
73
- <span
74
- key={i}
75
- className="inline-flex items-center gap-1 rounded-full border border-border bg-muted/40 pl-2.5 pr-1 py-0.5"
76
- >
77
- <span>{indicator}</span>
78
- <button
79
- type="button"
80
- onClick={() => clear(name)}
81
- aria-label={`Clear filter ${indicator}`}
82
- className="inline-flex size-4 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground"
83
- >
84
- ×
85
- </button>
86
- </span>
87
- )
88
- })}
89
- {active.length > 1 && (
90
- <button
91
- type="button"
92
- onClick={clearAll}
93
- className="text-muted-foreground hover:text-foreground underline-offset-2 hover:underline"
94
- >
95
- Clear all
96
- </button>
97
- )}
98
- </div>
99
- )
100
- }
101
-
102
- /**
103
- * Filter icon button + Popover containing every filter control.
104
- * Opens on click; the inner Selects don't dismiss the outer Popover when
105
- * an option is chosen (Base UI Popover doesn't auto-close on inner clicks).
106
- *
107
- * Each FilterSelect navigates the page on change (window.location), so the
108
- * filter form is no longer needed — keeps the search input in its own
109
- * lightweight form for native Enter-to-submit.
110
- */
111
- export function FilterPopover({ filters, prefix, renderFormChild }: {
112
- filters: ElementMeta[]
113
- prefix?: string | undefined
114
- renderFormChild: (child: ElementMeta, index: number, values: Record<string, unknown>, errors: Record<string, string[]>) => React.ReactNode
115
- }) {
116
- const activeCount = filters.filter(f => {
117
- const v = f['value']
118
- return typeof v === 'string' && v !== ''
119
- }).length
120
-
121
- return (
122
- <Popover>
123
- <PopoverTrigger
124
- render={(props) => (
125
- <button
126
- {...props}
127
- type="button"
128
- aria-label="Filters"
129
- className="relative inline-flex h-9 items-center justify-center gap-1.5 rounded-md border border-input bg-background px-3 text-sm font-medium hover:bg-accent hover:text-accent-foreground"
130
- >
131
- <FilterIcon className="size-4" />
132
- <span>Filters</span>
133
- {activeCount > 0 && (
134
- <span className="ml-1 inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-xs font-medium text-primary-foreground">
135
- {activeCount}
136
- </span>
137
- )}
138
- </button>
139
- )}
140
- />
141
- <PopoverContent align="start" className={
142
- filters.some(f => f['kind'] === 'queryBuilder')
143
- ? 'w-[36rem] max-w-[calc(100vw-2rem)] p-3'
144
- : 'w-72 p-3'
145
- }>
146
- <div className="flex flex-col gap-3">
147
- {filters.map((f, i) => renderFilterControl(f, i, prefix, renderFormChild))}
148
- </div>
149
- </PopoverContent>
150
- </Popover>
151
- )
152
- }
153
-
154
- /**
155
- * Inline strip of filter controls — used by `Table.filtersLayout('above-content'
156
- * | 'above-content-collapsible' | 'below-content')`. Mirrors `FilterPopover`'s
157
- * inner body but lays the controls out in a wrapping row instead of a
158
- * vertical stack inside a popover.
159
- */
160
- export function FilterStrip({ filters, prefix, renderFormChild }: {
161
- filters: ElementMeta[]
162
- prefix?: string | undefined
163
- renderFormChild: (child: ElementMeta, index: number, values: Record<string, unknown>, errors: Record<string, string[]>) => React.ReactNode
164
- }) {
165
- if (filters.length === 0) return null
166
- return (
167
- <div className="flex flex-col gap-3 rounded-md border bg-muted/30 p-3 sm:flex-row sm:flex-wrap sm:items-end">
168
- {filters.map((f, i) => (
169
- <div key={i} className="min-w-[12rem] flex-1 sm:max-w-xs">
170
- {renderFilterControl(f, i, prefix, renderFormChild)}
171
- </div>
172
- ))}
173
- </div>
174
- )
175
- }
176
-
177
- /**
178
- * Toolbar button paired with `FilterStrip` for `Table.filtersLayout(
179
- * 'above-content-collapsible')`. Visually matches the modal-mode trigger
180
- * (filter icon + "Filters" label + active-count badge) but flips a parent-
181
- * owned `open` state instead of opening a Popover.
182
- */
183
- export function FilterStripToggle({
184
- filters, open, onToggle,
185
- }: {
186
- filters: ElementMeta[]
187
- open: boolean
188
- onToggle: () => void
189
- }) {
190
- const activeCount = filters.filter(f => {
191
- const v = f['value']
192
- return typeof v === 'string' && v !== ''
193
- }).length
194
- return (
195
- <button
196
- type="button"
197
- aria-label="Filters"
198
- aria-expanded={open}
199
- onClick={onToggle}
200
- className="relative inline-flex h-9 items-center justify-center gap-1.5 rounded-md border border-input bg-background px-3 text-sm font-medium hover:bg-accent hover:text-accent-foreground"
201
- >
202
- <FilterIcon className="size-4" />
203
- <span>Filters</span>
204
- {activeCount > 0 && (
205
- <span className="ml-1 inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-xs font-medium text-primary-foreground">
206
- {activeCount}
207
- </span>
208
- )}
209
- </button>
210
- )
211
- }
212
-
213
- /**
214
- * Filter dropdown that updates the URL directly on change. We don't rely
215
- * on a wrapping `<form>` because filters now live inside a portaled
216
- * Popover (the search input keeps its own form for Enter-to-submit).
217
- *
218
- * Empty value (`''`) is the "All" sentinel — the param is removed from
219
- * the URL rather than serialized as `&name=`.
220
- */
221
- export function FilterSelect({
222
- name, label, defaultValue, placeholder, options, prefix,
223
- }: {
224
- name: string
225
- label: string
226
- defaultValue: string
227
- placeholder: string
228
- options: Array<{ value: string; label: string }>
229
- prefix?: string | undefined
230
- }) {
231
- const [value, setValue] = useState(defaultValue)
232
- const navigate = useNavigate()
233
-
234
- const onChange = (next: unknown) => {
235
- const v = typeof next === 'string' ? next : ''
236
- setValue(v)
237
- patchFilterUrl(navigate, prefix, { [name]: v })
238
- }
239
-
240
- return (
241
- <div className="flex flex-col gap-1 text-xs">
242
- <span className="text-muted-foreground">{label}</span>
243
- <Select value={value} onValueChange={onChange}>
244
- <SelectTrigger size="sm" className="w-full">
245
- <SelectValue placeholder={placeholder} />
246
- </SelectTrigger>
247
- <SelectContent>
248
- <SelectItem value="">{placeholder}</SelectItem>
249
- {options.map(o => (
250
- <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>
251
- ))}
252
- </SelectContent>
253
- </Select>
254
- </div>
255
- )
256
- }
257
-
258
- /**
259
- * Heading-row text for a group band. Shows `<label>: <value-or-title>`
260
- * with an optional description below. Reused for both collapsible and
261
- * static heading rows.
262
- */
263
- export function GroupHeaderText({
264
- label, value, title, description,
265
- }: {
266
- label?: string | undefined
267
- value?: string | undefined
268
- title?: string | undefined
269
- description?: string | undefined
270
- }) {
271
- const display = title ?? value ?? ''
272
- return (
273
- <span className="flex flex-col gap-0.5">
274
- <span>
275
- {label && <span className="text-muted-foreground/70">{label}: </span>}
276
- <span className="text-foreground">{display || 'Ungrouped'}</span>
277
- </span>
278
- {description && (
279
- <span className="text-[10px] font-normal normal-case text-muted-foreground/80">
280
- {description}
281
- </span>
282
- )}
283
- </span>
284
- )
285
- }
286
-
287
- /**
288
- * "Group by" dropdown rendered above the table when 2+ TableGroups
289
- * are registered (or 1 group with rich metadata). Selecting "None"
290
- * sets `?group=` (empty) which explicitly overrides `defaultGroup`.
291
- *
292
- * URL-driven — `onChange` builds the next href via `buildTableQuery`
293
- * and SPA-navigates; the page re-renders with the new active group.
294
- */
295
- export function TableGroupPicker({
296
- options, active, onChange,
297
- }: {
298
- options: Array<{ column: string; label: string }>
299
- active: string | undefined
300
- onChange: (column: string) => void
301
- }) {
302
- const value = active ?? ''
303
- return (
304
- <Select value={value} onValueChange={(v) => onChange(typeof v === 'string' ? v : '')}>
305
- <SelectTrigger size="sm" className="h-9 w-44">
306
- <SelectValue placeholder="Group by…" />
307
- </SelectTrigger>
308
- <SelectContent>
309
- <SelectItem value="">No grouping</SelectItem>
310
- {options.map(o => (
311
- <SelectItem key={o.column} value={o.column}>{o.label}</SelectItem>
312
- ))}
313
- </SelectContent>
314
- </Select>
315
- )
316
- }
317
-
318
- /**
319
- * Pair-of-date-inputs filter for `kind === 'dateRange'`. Each side
320
- * navigates the URL on change, encoding the pair as `from..to` keyed
321
- * off the filter name. Empty pair drops the URL key.
322
- */
323
- export function FilterDateRange({
324
- name, label, defaultValue, placeholder, includesTime, minDate, maxDate, prefix,
325
- }: {
326
- name: string
327
- label: string
328
- defaultValue: string
329
- placeholder: string
330
- includesTime: boolean
331
- minDate?: string
332
- maxDate?: string
333
- prefix?: string | undefined
334
- }) {
335
- const initial = parseDateRangeValue(defaultValue)
336
- const [from, setFrom] = useState(initial.from ?? '')
337
- const [to, setTo] = useState(initial.to ?? '')
338
- const navigate = useNavigate()
339
-
340
- const inputType = includesTime ? 'datetime-local' : 'date'
341
-
342
- const navigateTo = (nextFrom: string, nextTo: string): void => {
343
- patchFilterUrl(navigate, prefix, {
344
- [name]: encodeDateRangeValue({ from: nextFrom, to: nextTo }),
345
- })
346
- }
347
-
348
- const onFromChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
349
- const v = e.target.value
350
- setFrom(v)
351
- navigateTo(v, to)
352
- }
353
- const onToChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
354
- const v = e.target.value
355
- setTo(v)
356
- navigateTo(from, v)
357
- }
358
- const onClear = (): void => {
359
- setFrom('')
360
- setTo('')
361
- navigateTo('', '')
362
- }
363
-
364
- const hasValue = from !== '' || to !== ''
365
-
366
- return (
367
- <div className="flex flex-col gap-1 text-xs">
368
- <span className="text-muted-foreground">{label}</span>
369
- <div className="flex items-center gap-1">
370
- <Input
371
- type={inputType}
372
- value={from}
373
- onChange={onFromChange}
374
- placeholder={placeholder}
375
- aria-label={`${label} from`}
376
- {...(minDate !== undefined ? { min: minDate } : {})}
377
- {...(maxDate !== undefined ? { max: maxDate } : {})}
378
- className="h-8 text-xs"
379
- />
380
- <span className="text-muted-foreground">→</span>
381
- <Input
382
- type={inputType}
383
- value={to}
384
- onChange={onToChange}
385
- placeholder={placeholder}
386
- aria-label={`${label} to`}
387
- {...(minDate !== undefined ? { min: minDate } : {})}
388
- {...(maxDate !== undefined ? { max: maxDate } : {})}
389
- className="h-8 text-xs"
390
- />
391
- {hasValue && (
392
- <button
393
- type="button"
394
- onClick={onClear}
395
- aria-label={`Clear ${label}`}
396
- className="text-muted-foreground hover:text-foreground px-1"
397
- >
398
- ×
399
- </button>
400
- )}
401
- </div>
402
- </div>
403
- )
404
- }
405
-
406
- /**
407
- * Multi-value filter for `kind === 'multiSelect'`. Renders a checkbox
408
- * stack inside the popover; toggling a box patches the comma-separated
409
- * URL value for the filter's name. Empty selection drops the URL key.
410
- */
411
- export function FilterMultiSelect({
412
- name, label, defaultValue, options, prefix,
413
- }: {
414
- name: string
415
- label: string
416
- defaultValue: string
417
- options: Array<{ value: string; label: string }>
418
- prefix?: string | undefined
419
- }) {
420
- const [selected, setSelected] = useState<string[]>(() => parseMultiSelectValue(defaultValue))
421
- const navigate = useNavigate()
422
-
423
- const apply = (next: string[]): void => {
424
- setSelected(next)
425
- patchFilterUrl(navigate, prefix, { [name]: encodeMultiSelectValue(next) })
426
- }
427
-
428
- const toggle = (value: string, checked: boolean): void => {
429
- const next = checked
430
- ? [...selected.filter(v => v !== value), value]
431
- : selected.filter(v => v !== value)
432
- apply(next)
433
- }
434
-
435
- return (
436
- <div className="flex flex-col gap-1 text-xs">
437
- <span className="text-muted-foreground">{label}</span>
438
- <div className="flex flex-col gap-1.5">
439
- {options.map(o => {
440
- const checked = selected.includes(o.value)
441
- return (
442
- <label
443
- key={o.value}
444
- className="flex items-center gap-2 text-sm cursor-pointer"
445
- >
446
- <Checkbox
447
- checked={checked}
448
- onCheckedChange={(c: boolean | 'indeterminate') => toggle(o.value, c === true)}
449
- />
450
- <span>{o.label}</span>
451
- </label>
452
- )
453
- })}
454
- </div>
455
- </div>
456
- )
457
- }
458
-
459
- /**
460
- * Multi-field filter for `kind === 'form'`. The popover renders an inner
461
- * sub-form with the user-declared schema; submitting bundles all named
462
- * inputs into a `Record<string, unknown>`, JSON-encodes the non-empty
463
- * subset under the filter's URL key, and SPA-navigates. Empty submit
464
- * drops the URL key entirely.
465
- *
466
- * The fields' `defaultValue` were pre-hydrated server-side from the
467
- * active URL value (see `FormFilter.toMeta`), so an existing filter
468
- * round-trips into the form on render. Inputs are uncontrolled — we
469
- * read state via `new FormData(form)` on submit, matching how the
470
- * outer page-level Form works on full submit.
471
- */
472
- export function FilterForm({
473
- name, label, defaultValue, formSchema, prefix, renderFormChild,
474
- }: {
475
- name: string
476
- label: string
477
- defaultValue: string
478
- formSchema: ElementMeta[]
479
- prefix?: string | undefined
480
- // Injected from the top-level dispatch — `renderFormChild` lives in
481
- // the form layer (Phase 4) and can't be imported directly without
482
- // a cycle through `SchemaRenderer.tsx`.
483
- renderFormChild: (child: ElementMeta, index: number, values: Record<string, unknown>, errors: Record<string, string[]>) => React.ReactNode
484
- }) {
485
- const formRef = useRef<HTMLFormElement>(null)
486
- const navigate = useNavigate()
487
- const hasValue = defaultValue !== '' && defaultValue !== '{}'
488
-
489
- const onApply = (e?: React.FormEvent | React.MouseEvent): void => {
490
- e?.preventDefault()
491
- if (!formRef.current) return
492
- const fd = new FormData(formRef.current)
493
- const values: Record<string, unknown> = {}
494
- for (const [key, val] of fd.entries()) {
495
- const existing = values[key]
496
- if (existing === undefined) {
497
- values[key] = val
498
- } else if (Array.isArray(existing)) {
499
- (existing as unknown[]).push(val)
500
- } else {
501
- values[key] = [existing, val]
502
- }
503
- }
504
- patchFilterUrl(navigate, prefix, { [name]: encodeFormFilterValue(values) })
505
- }
506
-
507
- const onClear = (): void => {
508
- patchFilterUrl(navigate, prefix, { [name]: null })
509
- }
510
-
511
- return (
512
- <div className="flex flex-col gap-2">
513
- <span className="text-muted-foreground text-xs">{label}</span>
514
- <form ref={formRef} onSubmit={onApply} className="flex flex-col gap-2">
515
- {formSchema.map((child, i) => renderFormChild(child, i, {}, {}))}
516
- <div className="flex gap-2 pt-1">
517
- <button
518
- type="submit"
519
- className="inline-flex h-8 items-center justify-center rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90"
520
- >
521
- Apply
522
- </button>
523
- {hasValue && (
524
- <button
525
- type="button"
526
- onClick={onClear}
527
- className="inline-flex h-8 items-center justify-center rounded-md border border-input bg-background px-3 text-xs font-medium hover:bg-accent hover:text-accent-foreground"
528
- >
529
- Clear
530
- </button>
531
- )}
532
- </div>
533
- </form>
534
- </div>
535
- )
536
- }
537
-
538
- /**
539
- * Composable advanced filter for `kind === 'queryBuilder'`. v2 emits a
540
- * full tree — root AND/OR connector + nested groups arbitrarily deep —
541
- * JSON-encoded into a single URL key on Apply (see
542
- * `encodeQueryBuilderValue`).
543
- *
544
- * State is local — typing into a value input doesn't navigate. Only the
545
- * Apply button writes the URL. This mirrors `FilterForm`'s behavior and
546
- * keeps the popover quiet under the cursor.
547
- */
548
- export function FilterQueryBuilder({
549
- name, label, defaultValue, constraints, prefix,
550
- }: {
551
- name: string
552
- label: string
553
- defaultValue: string
554
- constraints: ConstraintMeta[]
555
- prefix?: string | undefined
556
- }) {
557
- const navigate = useNavigate()
558
- const initialTree = parseQueryBuilderValue(defaultValue)
559
- const [tree, setTree] = useState<QueryBuilderTree>(initialTree)
560
- const hasValue = defaultValue !== '' && initialTree.rules.length > 0
561
-
562
- const onApply = (e?: React.FormEvent | React.MouseEvent): void => {
563
- e?.preventDefault()
564
- patchFilterUrl(navigate, prefix, { [name]: encodeQueryBuilderValue(tree) })
565
- }
566
-
567
- const onClear = (): void => {
568
- setTree({ operator: 'and', rules: [] })
569
- patchFilterUrl(navigate, prefix, { [name]: null })
570
- }
571
-
572
- if (constraints.length === 0) {
573
- return (
574
- <div className="text-muted-foreground text-xs">
575
- {label}: no constraints declared.
576
- </div>
577
- )
578
- }
579
-
580
- return (
581
- <div className="flex flex-col gap-2 min-w-[24rem]">
582
- <span className="text-muted-foreground text-xs">{label}</span>
583
- <form onSubmit={onApply} className="flex flex-col gap-2">
584
- <QueryBuilderGroup
585
- tree={tree}
586
- constraints={constraints}
587
- isRoot={true}
588
- onChange={setTree}
589
- />
590
- <div className="flex items-center gap-2 pt-1">
591
- <div className="flex-1" />
592
- <button
593
- type="submit"
594
- className="inline-flex h-8 items-center justify-center rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90"
595
- >
596
- Apply
597
- </button>
598
- {(hasValue || tree.rules.length > 0) && (
599
- <button
600
- type="button"
601
- onClick={onClear}
602
- className="inline-flex h-8 items-center justify-center rounded-md border border-input bg-background px-3 text-xs font-medium hover:bg-accent hover:text-accent-foreground"
603
- >
604
- Clear
605
- </button>
606
- )}
607
- </div>
608
- </form>
609
- </div>
610
- )
611
- }
612
-
613
- /**
614
- * Recursive group renderer — emits a connector picker (AND / OR) at the
615
- * top, a vertical stack of children (rules and sub-groups), and footer
616
- * buttons for "+ Add condition" and "+ Add group". Calls `onChange` with
617
- * the updated sub-tree so parents can splice it back into their own
618
- * `rules` array. Root groups skip the outer border so the popover doesn't
619
- * carry a redundant frame; nested groups draw a faint left rule + soft
620
- * background so the nesting is visible without blowing up the width.
621
- */
622
- export function QueryBuilderGroup({
623
- tree, constraints, isRoot, onChange, onRemove,
624
- }: {
625
- tree: QueryBuilderTree
626
- constraints: ConstraintMeta[]
627
- isRoot: boolean
628
- onChange: (next: QueryBuilderTree) => void
629
- onRemove?: () => void
630
- }) {
631
- const constraintMap = new Map<string, ConstraintMeta>()
632
- for (const c of constraints) constraintMap.set(c.name, c)
633
-
634
- const setOperator = (op: 'and' | 'or'): void => {
635
- onChange({ ...tree, operator: op })
636
- }
637
-
638
- const updateChildAt = (index: number, next: QueryBuilderTreeChild): void => {
639
- onChange({ ...tree, rules: tree.rules.map((r, i) => i === index ? next : r) })
640
- }
641
-
642
- const removeChildAt = (index: number): void => {
643
- onChange({ ...tree, rules: tree.rules.filter((_, i) => i !== index) })
644
- }
645
-
646
- const addRule = (): void => {
647
- const first = constraints[0]
648
- if (!first) return
649
- onChange({
650
- ...tree,
651
- rules: [...tree.rules, {
652
- constraint: first.name,
653
- operator: first.defaultOperator ?? first.operators[0]?.name ?? 'equals',
654
- value: undefined,
655
- }],
656
- })
657
- }
658
-
659
- const addGroup = (): void => {
660
- onChange({
661
- ...tree,
662
- rules: [...tree.rules, { operator: 'and', rules: [] }],
663
- })
664
- }
665
-
666
- const wrapper = isRoot
667
- ? 'flex flex-col gap-2'
668
- : 'flex flex-col gap-2 rounded-md border-l-2 border-primary/40 bg-muted/30 pl-2 py-2 pr-2'
669
-
670
- return (
671
- <div className={wrapper}>
672
- <div className="flex items-center gap-2">
673
- <ConnectorToggle value={tree.operator} onChange={setOperator} />
674
- <span className="text-muted-foreground text-[11px]">
675
- {tree.operator === 'and' ? 'Match all of the following' : 'Match any of the following'}
676
- </span>
677
- {!isRoot && onRemove && (
678
- <>
679
- <div className="flex-1" />
680
- <button
681
- type="button"
682
- onClick={onRemove}
683
- aria-label="Remove group"
684
- className="inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground"
685
- >
686
- ×
687
- </button>
688
- </>
689
- )}
690
- </div>
691
-
692
- {tree.rules.length === 0 && (
693
- <div className="text-muted-foreground text-xs italic">No conditions yet.</div>
694
- )}
695
-
696
- {tree.rules.map((child, i) => {
697
- if (isQueryBuilderTree(child)) {
698
- return (
699
- <QueryBuilderGroup
700
- key={i}
701
- tree={child}
702
- constraints={constraints}
703
- isRoot={false}
704
- onChange={(next) => updateChildAt(i, next)}
705
- onRemove={() => removeChildAt(i)}
706
- />
707
- )
708
- }
709
- return (
710
- <QueryBuilderRow
711
- key={i}
712
- rule={child}
713
- constraints={constraints}
714
- constraintMeta={constraintMap.get(child.constraint)}
715
- onConstraintChange={(v) => {
716
- const c = constraintMap.get(v)
717
- if (!c) return
718
- updateChildAt(i, {
719
- constraint: v,
720
- operator: c.defaultOperator ?? c.operators[0]?.name ?? 'equals',
721
- value: undefined,
722
- })
723
- }}
724
- onOperatorChange={(v) => {
725
- updateChildAt(i, {
726
- ...child,
727
- operator: v as ConstraintOperatorName,
728
- value: undefined,
729
- })
730
- }}
731
- onValueChange={(v) => updateChildAt(i, { ...child, value: v })}
732
- onRemove={() => removeChildAt(i)}
733
- />
734
- )
735
- })}
736
-
737
- <div className="flex flex-wrap items-center gap-2">
738
- <button
739
- type="button"
740
- onClick={addRule}
741
- className="inline-flex h-8 items-center justify-center rounded-md border border-dashed border-input bg-background px-3 text-xs font-medium hover:bg-accent hover:text-accent-foreground"
742
- >
743
- + Add condition
744
- </button>
745
- <button
746
- type="button"
747
- onClick={addGroup}
748
- className="inline-flex h-8 items-center justify-center rounded-md border border-dashed border-input bg-background px-3 text-xs font-medium hover:bg-accent hover:text-accent-foreground"
749
- >
750
- + Add group
751
- </button>
752
- </div>
753
- </div>
754
- )
755
- }
756
-
757
- /**
758
- * Compact AND/OR segmented control used at the head of every group. Pure
759
- * presentation — the parent owns the value.
760
- */
761
- export function ConnectorToggle({
762
- value, onChange,
763
- }: {
764
- value: 'and' | 'or'
765
- onChange: (next: 'and' | 'or') => void
766
- }) {
767
- const base = 'inline-flex h-7 items-center px-2 text-[11px] font-medium uppercase tracking-wide transition'
768
- const on = 'bg-primary text-primary-foreground'
769
- const off = 'bg-background text-muted-foreground hover:bg-accent hover:text-accent-foreground'
770
- return (
771
- <div className="inline-flex overflow-hidden rounded-md border border-input">
772
- <button
773
- type="button"
774
- onClick={() => onChange('and')}
775
- className={`${base} ${value === 'and' ? on : off}`}
776
- aria-pressed={value === 'and'}
777
- >
778
- AND
779
- </button>
780
- <button
781
- type="button"
782
- onClick={() => onChange('or')}
783
- className={`${base} ${value === 'or' ? on : off}`}
784
- aria-pressed={value === 'or'}
785
- >
786
- OR
787
- </button>
788
- </div>
789
- )
790
- }
791
-
792
- /**
793
- * One condition row inside `FilterQueryBuilder`. Three controls
794
- * left-to-right: constraint picker, operator picker, value input. The
795
- * value input dispatches off the operator's `valueKind` — `none` hides
796
- * it entirely, `numberRange` / `dateRange` mount a pair, otherwise a
797
- * single typed input.
798
- */
799
- export function QueryBuilderRow({
800
- rule, constraints, constraintMeta,
801
- onConstraintChange, onOperatorChange, onValueChange, onRemove,
802
- }: {
803
- rule: QueryBuilderRule
804
- constraints: ConstraintMeta[]
805
- constraintMeta: ConstraintMeta | undefined
806
- onConstraintChange: (name: string) => void
807
- onOperatorChange: (name: string) => void
808
- onValueChange: (value: unknown) => void
809
- onRemove: () => void
810
- }) {
811
- const operators: ConstraintOperator[] = constraintMeta?.operators ?? []
812
- const activeOp = operators.find(o => o.name === rule.operator)
813
- const valueKind: ConstraintValueKind = activeOp?.valueKind ?? 'text'
814
-
815
- return (
816
- <div className="flex items-start gap-1.5 rounded-md border border-input bg-background p-2">
817
- <div className="flex flex-1 flex-wrap items-center gap-1.5">
818
- <Select value={rule.constraint} onValueChange={(v) => onConstraintChange(typeof v === 'string' ? v : '')}>
819
- <SelectTrigger size="sm" className="h-8 w-36 text-xs">
820
- <SelectValue />
821
- </SelectTrigger>
822
- <SelectContent>
823
- {constraints.map(c => (
824
- <SelectItem key={c.name} value={c.name}>{c.label}</SelectItem>
825
- ))}
826
- </SelectContent>
827
- </Select>
828
-
829
- <Select value={rule.operator} onValueChange={(v) => onOperatorChange(typeof v === 'string' ? v : '')}>
830
- <SelectTrigger size="sm" className="h-8 w-32 text-xs">
831
- <SelectValue />
832
- </SelectTrigger>
833
- <SelectContent>
834
- {operators.map(o => (
835
- <SelectItem key={o.name} value={o.name}>{o.label}</SelectItem>
836
- ))}
837
- </SelectContent>
838
- </Select>
839
-
840
- <QueryBuilderValueInput
841
- kind={valueKind}
842
- value={rule.value}
843
- options={constraintMeta?.options}
844
- onChange={onValueChange}
845
- />
846
- </div>
847
- <button
848
- type="button"
849
- onClick={onRemove}
850
- aria-label="Remove condition"
851
- className="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground"
852
- >
853
- ×
854
- </button>
855
- </div>
856
- )
857
- }
858
-
859
- /**
860
- * Operator-aware value control. Switches over the constraint operator's
861
- * `valueKind` and mounts the matching input. Value shapes:
862
- * - `text / number / date / dateTime / select` → scalar
863
- * - `multiSelect` → string[]
864
- * - `numberRange / dateRange` → [string, string]
865
- * - `boolean / none` → null / undefined
866
- */
867
- export function QueryBuilderValueInput({
868
- kind, value, options, onChange,
869
- }: {
870
- kind: ConstraintValueKind
871
- value: unknown
872
- options: Array<{ value: string; label: string }> | undefined
873
- onChange: (next: unknown) => void
874
- }) {
875
- if (kind === 'none' || kind === 'boolean') return null
876
-
877
- if (kind === 'select') {
878
- const opts = options ?? []
879
- const v = value === undefined || value === null ? '' : String(value)
880
- return (
881
- <Select value={v} onValueChange={(next) => onChange(typeof next === 'string' ? next : '')}>
882
- <SelectTrigger size="sm" className="h-8 min-w-32 text-xs">
883
- <SelectValue placeholder="Pick…" />
884
- </SelectTrigger>
885
- <SelectContent>
886
- {opts.map(o => (
887
- <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>
888
- ))}
889
- </SelectContent>
890
- </Select>
891
- )
892
- }
893
-
894
- if (kind === 'multiSelect') {
895
- const opts = options ?? []
896
- const list = Array.isArray(value) ? value.map(v => String(v)) : []
897
- const toggle = (val: string): void => {
898
- if (list.includes(val)) onChange(list.filter(v => v !== val))
899
- else onChange([...list, val])
900
- }
901
- return (
902
- <div className="flex flex-wrap items-center gap-1">
903
- {opts.map(o => {
904
- const active = list.includes(o.value)
905
- return (
906
- <button
907
- key={o.value}
908
- type="button"
909
- onClick={() => toggle(o.value)}
910
- className={
911
- 'inline-flex h-7 items-center rounded-md border px-2 text-xs ' +
912
- (active
913
- ? 'border-primary bg-primary text-primary-foreground'
914
- : 'border-input bg-background hover:bg-accent')
915
- }
916
- >
917
- {o.label}
918
- </button>
919
- )
920
- })}
921
- </div>
922
- )
923
- }
924
-
925
- if (kind === 'numberRange') {
926
- const [min, max] = Array.isArray(value) ? [value[0], value[1]] : [undefined, undefined]
927
- return (
928
- <div className="flex items-center gap-1">
929
- <Input
930
- type="number"
931
- className="h-8 w-24 text-xs"
932
- value={min === undefined || min === null ? '' : String(min)}
933
- onChange={(e) => onChange([e.target.value, max ?? ''])}
934
- placeholder="Min"
935
- />
936
- <span className="text-muted-foreground text-xs">–</span>
937
- <Input
938
- type="number"
939
- className="h-8 w-24 text-xs"
940
- value={max === undefined || max === null ? '' : String(max)}
941
- onChange={(e) => onChange([min ?? '', e.target.value])}
942
- placeholder="Max"
943
- />
944
- </div>
945
- )
946
- }
947
-
948
- if (kind === 'dateRange') {
949
- const [from, to] = Array.isArray(value) ? [value[0], value[1]] : [undefined, undefined]
950
- return (
951
- <div className="flex items-center gap-1">
952
- <Input
953
- type="date"
954
- className="h-8 w-36 text-xs"
955
- value={from === undefined || from === null ? '' : String(from)}
956
- onChange={(e) => onChange([e.target.value, to ?? ''])}
957
- />
958
- <span className="text-muted-foreground text-xs">→</span>
959
- <Input
960
- type="date"
961
- className="h-8 w-36 text-xs"
962
- value={to === undefined || to === null ? '' : String(to)}
963
- onChange={(e) => onChange([from ?? '', e.target.value])}
964
- />
965
- </div>
966
- )
967
- }
968
-
969
- if (kind === 'date' || kind === 'dateTime') {
970
- const v = value === undefined || value === null ? '' : String(value)
971
- return (
972
- <Input
973
- type={kind === 'dateTime' ? 'datetime-local' : 'date'}
974
- className="h-8 w-44 text-xs"
975
- value={v}
976
- onChange={(e) => onChange(e.target.value)}
977
- />
978
- )
979
- }
980
-
981
- if (kind === 'number') {
982
- const v = value === undefined || value === null ? '' : String(value)
983
- return (
984
- <Input
985
- type="number"
986
- className="h-8 w-32 text-xs"
987
- value={v}
988
- onChange={(e) => onChange(e.target.value)}
989
- placeholder="Value"
990
- />
991
- )
992
- }
993
-
994
- // Default: text
995
- const v = value === undefined || value === null ? '' : String(value)
996
- return (
997
- <Input
998
- type="text"
999
- className="h-8 min-w-32 flex-1 text-xs"
1000
- value={v}
1001
- onChange={(e) => onChange(e.target.value)}
1002
- placeholder="Value"
1003
- />
1004
- )
1005
- }
1006
-
1007
- export function renderFilterControl(
1008
- el: ElementMeta,
1009
- index: number,
1010
- prefix: string | undefined,
1011
- renderFormChild: (child: ElementMeta, index: number, values: Record<string, unknown>, errors: Record<string, string[]>) => React.ReactNode,
1012
- ): React.ReactNode {
1013
- const name = String(el['name'] ?? '')
1014
- const label = String(el['label'] ?? name)
1015
- const kind = String(el['kind'] ?? 'select')
1016
- const value = el['value'] ? String(el['value']) : ''
1017
- const placeholder = el['placeholder'] ? String(el['placeholder']) : 'All'
1018
-
1019
- if (kind === 'queryBuilder') {
1020
- const constraints = (el['constraints'] as ConstraintMeta[] | undefined) ?? []
1021
- return (
1022
- <FilterQueryBuilder
1023
- key={index}
1024
- name={name}
1025
- label={label}
1026
- defaultValue={value}
1027
- constraints={constraints}
1028
- prefix={prefix}
1029
- />
1030
- )
1031
- }
1032
-
1033
- if (kind === 'form') {
1034
- const formSchema = (el['formSchema'] as ElementMeta[] | undefined) ?? []
1035
- return (
1036
- <FilterForm
1037
- key={index}
1038
- name={name}
1039
- label={label}
1040
- defaultValue={value}
1041
- formSchema={formSchema}
1042
- prefix={prefix}
1043
- renderFormChild={renderFormChild}
1044
- />
1045
- )
1046
- }
1047
-
1048
- if (kind === 'boolean') {
1049
- return (
1050
- <FilterSelect
1051
- key={index}
1052
- name={name}
1053
- label={label}
1054
- defaultValue={value}
1055
- placeholder={placeholder}
1056
- options={[{ value: '1', label: 'Yes' }, { value: '0', label: 'No' }]}
1057
- prefix={prefix}
1058
- />
1059
- )
1060
- }
1061
-
1062
- if (kind === 'multiSelect') {
1063
- const options = (el['options'] as Array<{ value: string; label: string }> | undefined) ?? []
1064
- return (
1065
- <FilterMultiSelect
1066
- key={index}
1067
- name={name}
1068
- label={label}
1069
- defaultValue={value}
1070
- options={options}
1071
- prefix={prefix}
1072
- />
1073
- )
1074
- }
1075
-
1076
- if (kind === 'dateRange') {
1077
- const includesTime = Boolean(el['includesTime'])
1078
- const minDate = el['minDate'] ? String(el['minDate']) : undefined
1079
- const maxDate = el['maxDate'] ? String(el['maxDate']) : undefined
1080
- return (
1081
- <FilterDateRange
1082
- key={index}
1083
- name={name}
1084
- label={label}
1085
- defaultValue={value}
1086
- placeholder={placeholder}
1087
- includesTime={includesTime}
1088
- prefix={prefix}
1089
- {...(minDate !== undefined ? { minDate } : {})}
1090
- {...(maxDate !== undefined ? { maxDate } : {})}
1091
- />
1092
- )
1093
- }
1094
-
1095
- // 'ternary' and 'select' both render as a single-select dropdown,
1096
- // differing only in their server-supplied option set.
1097
- const options = (el['options'] as Array<{ value: string; label: string }> | undefined) ?? []
1098
- return (
1099
- <FilterSelect
1100
- key={index}
1101
- name={name}
1102
- label={label}
1103
- defaultValue={value}
1104
- placeholder={placeholder}
1105
- options={options}
1106
- prefix={prefix}
1107
- />
1108
- )
1109
- }
1110
-
1111
- /**
1112
- * Resolve the record URL for a single data cell. Column-level override
1113
- * (`Column.recordUrl(fn)` → `_columnRecordUrls[name]`) wins over the
1114
- * table-level `Table.recordUrl(fn)` (`_recordUrl`). Explicit per-column
1115
- * opt-out (`Column.recordUrl(false)` → `meta.recordUrl === false`)
1116
- * suppresses the link entirely. Returns `undefined` when the cell is
1117
- * not linkable, in which case the renderer leaves it unwrapped.
1118
- */
1119
- export function resolveColumnUrl(
1120
- col: ElementMeta,
1121
- tableUrl: string | undefined,
1122
- colUrls: Record<string, string>,
1123
- ): string | undefined {
1124
- if (col['recordUrl'] === false) return undefined
1125
- const own = colUrls[String(col['name'] ?? '')]
1126
- if (own !== undefined) return own
1127
- return tableUrl
1128
- }
1129
-
1130
- /**
1131
- * Toolbar "Sort by" picker — surfaces in cards layout where there's no
1132
- * column-header click affordance to flip sort order. In standard table
1133
- * mode, this picker appears in the top bar instead. Each `Column` flagged
1134
- * `.sortable()` contributes two options — ascending and descending —
1135
- * yielding "Title (A→Z) / Title (Z→A) / Date (oldest first) / Date (newest
1136
- * first)" style entries. Selecting an option resets `?page=1`.
1137
- */
1138
- export function SortByPicker({
1139
- columns, active, onChange,
1140
- }: {
1141
- columns: ElementMeta[]
1142
- active: { column: string; direction: 'asc' | 'desc' } | undefined
1143
- onChange: (column: string, direction: 'asc' | 'desc') => void
1144
- }) {
1145
- const sortable = columns.filter(c => Boolean(c['sortable']))
1146
- if (sortable.length === 0) return null
1147
- const value = active ? `${active.column}:${active.direction}` : ''
1148
- return (
1149
- <Select
1150
- value={value}
1151
- onValueChange={(v) => {
1152
- if (typeof v !== 'string' || v === '') return
1153
- const idx = v.indexOf(':')
1154
- if (idx < 0) return
1155
- const col = v.slice(0, idx)
1156
- const dir = v.slice(idx + 1) === 'desc' ? 'desc' : 'asc'
1157
- onChange(col, dir as 'asc' | 'desc')
1158
- }}
1159
- >
1160
- <SelectTrigger size="sm" className="h-9 w-44">
1161
- <SelectValue placeholder="Sort by…" />
1162
- </SelectTrigger>
1163
- <SelectContent>
1164
- {sortable.map(col => {
1165
- const name = String(col['name'] ?? '')
1166
- const label = String(col['label'] ?? name)
1167
- return (
1168
- <React.Fragment key={name}>
1169
- <SelectItem value={`${name}:asc`}>{label} (A→Z)</SelectItem>
1170
- <SelectItem value={`${name}:desc`}>{label} (Z→A)</SelectItem>
1171
- </React.Fragment>
1172
- )
1173
- })}
1174
- </SelectContent>
1175
- </Select>
1176
- )
1177
- }
1178
-
1179
- /**
1180
- * Toolbar dropdown for `Column.toggleable()` columns. Lists every
1181
- * toggleable column with a checkbox; toggling writes through to a
1182
- * caller-supplied `onToggle` (the `TableRendererBody` owns the state
1183
- * + the localStorage round-trip). Mounted only when at least one
1184
- * column is toggleable.
1185
- */
1186
- export function ColumnsToggleDropdown({
1187
- columns, hidden, onToggle,
1188
- }: {
1189
- columns: ElementMeta[]
1190
- hidden: Set<string>
1191
- onToggle: (name: string, nextHidden: boolean) => void
1192
- }) {
1193
- if (columns.length === 0) return null
1194
- return (
1195
- <DropdownMenu>
1196
- <DropdownMenuTrigger
1197
- render={(props) => (
1198
- <button
1199
- {...props}
1200
- type="button"
1201
- className="inline-flex h-9 items-center gap-1.5 rounded-md border border-input bg-background px-3 text-sm font-medium text-foreground hover:bg-accent"
1202
- aria-label="Show or hide columns"
1203
- >
1204
- <Columns3Icon className="h-4 w-4" aria-hidden="true" />
1205
- <span>Columns</span>
1206
- </button>
1207
- )}
1208
- />
1209
- <DropdownMenuContent align="end" className="min-w-[12rem]">
1210
- {columns.map((col, i) => {
1211
- const name = String(col['name'] ?? '')
1212
- const label = String(col['label'] ?? name)
1213
- const isHidden = hidden.has(name)
1214
- return (
1215
- <DropdownMenuItem
1216
- key={i}
1217
- // Suppress menu-close so users can toggle multiple columns
1218
- // without re-opening the dropdown.
1219
- closeOnClick={false}
1220
- onClick={() => onToggle(name, !isHidden)}
1221
- >
1222
- <span className="inline-flex w-4 items-center justify-center">
1223
- {!isHidden && <CheckIcon className="h-4 w-4" aria-hidden="true" />}
1224
- </span>
1225
- <span>{label}</span>
1226
- </DropdownMenuItem>
1227
- )
1228
- })}
1229
- </DropdownMenuContent>
1230
- </DropdownMenu>
1231
- )
1232
- }
1233
-