@pilotiq/pilotiq 0.23.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 (500) hide show
  1. package/CHANGELOG.md +91 -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/dist/actions/exportFactory.d.ts +10 -0
  15. package/dist/actions/exportFactory.d.ts.map +1 -1
  16. package/dist/actions/exportFactory.js +10 -0
  17. package/dist/actions/exportFactory.js.map +1 -1
  18. package/dist/react/CollabRoomContext.d.ts +5 -5
  19. package/dist/react/index.d.ts +0 -1
  20. package/dist/react/index.d.ts.map +1 -1
  21. package/dist/react/index.js +0 -1
  22. package/dist/react/index.js.map +1 -1
  23. package/dist/routes/helpers.d.ts.map +1 -1
  24. package/dist/routes/helpers.js +6 -2
  25. package/dist/routes/helpers.js.map +1 -1
  26. package/dist/routes/relations.d.ts.map +1 -1
  27. package/dist/routes/relations.js +12 -0
  28. package/dist/routes/relations.js.map +1 -1
  29. package/package.json +6 -1
  30. package/.turbo/turbo-build.log +0 -8
  31. package/CLAUDE.md +0 -265
  32. package/dist/react/useCollabSeed.d.ts +0 -23
  33. package/dist/react/useCollabSeed.d.ts.map +0 -1
  34. package/dist/react/useCollabSeed.js +0 -82
  35. package/dist/react/useCollabSeed.js.map +0 -1
  36. package/src/Cluster.test.ts +0 -283
  37. package/src/Cluster.ts +0 -83
  38. package/src/Column.test.ts +0 -199
  39. package/src/Column.ts +0 -710
  40. package/src/Global.test.ts +0 -367
  41. package/src/Global.ts +0 -169
  42. package/src/Page.test.ts +0 -114
  43. package/src/Page.ts +0 -208
  44. package/src/Pilotiq.perf.test.ts +0 -252
  45. package/src/Pilotiq.test.ts +0 -129
  46. package/src/Pilotiq.ts +0 -1158
  47. package/src/PilotiqRegistry.ts +0 -36
  48. package/src/PilotiqServiceProvider.ts +0 -121
  49. package/src/RelationManager.test.ts +0 -400
  50. package/src/RelationManager.ts +0 -527
  51. package/src/RenderHook.test.ts +0 -252
  52. package/src/RenderHook.ts +0 -242
  53. package/src/Resource.test.ts +0 -284
  54. package/src/Resource.ts +0 -526
  55. package/src/RightPanel.test.ts +0 -202
  56. package/src/RightPanel.ts +0 -132
  57. package/src/Tab.test.ts +0 -91
  58. package/src/Tab.ts +0 -156
  59. package/src/UserMenuItem.ts +0 -145
  60. package/src/actions/Action.test.ts +0 -2526
  61. package/src/actions/Action.ts +0 -1515
  62. package/src/actions/ActionGroup.test.ts +0 -112
  63. package/src/actions/ActionGroup.ts +0 -173
  64. package/src/actions/attachFactory.ts +0 -172
  65. package/src/actions/bulkFactories.ts +0 -168
  66. package/src/actions/crudFactories.ts +0 -220
  67. package/src/actions/exportFactory.ts +0 -215
  68. package/src/actions/factoryHelpers.ts +0 -177
  69. package/src/actions/importFactory.ts +0 -243
  70. package/src/actions/index.ts +0 -17
  71. package/src/actions/m2mFactories.ts +0 -193
  72. package/src/actions/relationFactories.ts +0 -372
  73. package/src/applyPageHooks.test.ts +0 -463
  74. package/src/applyPageHooks.ts +0 -330
  75. package/src/authorization.test.ts +0 -483
  76. package/src/breadcrumbs.test.ts +0 -238
  77. package/src/cells/coerce.test.ts +0 -85
  78. package/src/cells/coerce.ts +0 -84
  79. package/src/clusterPaths.ts +0 -35
  80. package/src/columns/BadgeColumn.test.ts +0 -54
  81. package/src/columns/BadgeColumn.ts +0 -32
  82. package/src/columns/BooleanColumn.test.ts +0 -41
  83. package/src/columns/BooleanColumn.ts +0 -18
  84. package/src/columns/ColorColumn.test.ts +0 -37
  85. package/src/columns/ColorColumn.ts +0 -38
  86. package/src/columns/IconColumn.test.ts +0 -54
  87. package/src/columns/IconColumn.ts +0 -37
  88. package/src/columns/ImageColumn.test.ts +0 -41
  89. package/src/columns/ImageColumn.ts +0 -28
  90. package/src/columns/SelectColumn.ts +0 -98
  91. package/src/columns/TextColumn.test.ts +0 -190
  92. package/src/columns/TextColumn.ts +0 -20
  93. package/src/columns/TextInputColumn.ts +0 -68
  94. package/src/columns/ToggleColumn.ts +0 -46
  95. package/src/columns/editableColumns.test.ts +0 -238
  96. package/src/columns/index.ts +0 -9
  97. package/src/defaultGlobalPages.ts +0 -95
  98. package/src/defaultPages.test.ts +0 -634
  99. package/src/defaultPages.ts +0 -617
  100. package/src/defaultViewPage.test.ts +0 -147
  101. package/src/elements/Form.test.ts +0 -223
  102. package/src/elements/Form.ts +0 -416
  103. package/src/elements/ListTabs.ts +0 -28
  104. package/src/elements/Table.test.ts +0 -422
  105. package/src/elements/Table.ts +0 -850
  106. package/src/elements/TableGroup.test.ts +0 -260
  107. package/src/elements/TableGroup.ts +0 -334
  108. package/src/elements/dispatchAction.test.ts +0 -463
  109. package/src/elements/dispatchAction.ts +0 -355
  110. package/src/elements/dispatchForm.test.ts +0 -477
  111. package/src/elements/dispatchForm.ts +0 -1993
  112. package/src/elements/dispatchTable.test.ts +0 -1514
  113. package/src/elements/dispatchTable.ts +0 -745
  114. package/src/elements/index.ts +0 -21
  115. package/src/entries/BadgeEntry.ts +0 -39
  116. package/src/entries/CodeEntry.test.ts +0 -40
  117. package/src/entries/CodeEntry.ts +0 -52
  118. package/src/entries/ColorEntry.ts +0 -63
  119. package/src/entries/ComponentEntry.test.ts +0 -173
  120. package/src/entries/ComponentEntry.ts +0 -95
  121. package/src/entries/Entry.ts +0 -304
  122. package/src/entries/IconEntry.ts +0 -49
  123. package/src/entries/ImageEntry.ts +0 -61
  124. package/src/entries/KeyValueEntry.ts +0 -47
  125. package/src/entries/RepeatableEntry.test.ts +0 -239
  126. package/src/entries/RepeatableEntry.ts +0 -173
  127. package/src/entries/TextEntry.test.ts +0 -394
  128. package/src/entries/TextEntry.ts +0 -60
  129. package/src/entries/index.ts +0 -12
  130. package/src/entries/leaves.test.ts +0 -306
  131. package/src/entries/registry.ts +0 -54
  132. package/src/fields/BuilderField.test.ts +0 -1188
  133. package/src/fields/BuilderField.ts +0 -605
  134. package/src/fields/BuilderRelationship.test.ts +0 -811
  135. package/src/fields/CheckboxField.test.ts +0 -44
  136. package/src/fields/CheckboxField.ts +0 -27
  137. package/src/fields/CheckboxListField.test.ts +0 -99
  138. package/src/fields/CheckboxListField.ts +0 -66
  139. package/src/fields/ColorPickerField.test.ts +0 -33
  140. package/src/fields/ColorPickerField.ts +0 -25
  141. package/src/fields/DateField.ts +0 -54
  142. package/src/fields/DateTimeField.test.ts +0 -55
  143. package/src/fields/EmailField.ts +0 -16
  144. package/src/fields/Field.test.ts +0 -654
  145. package/src/fields/Field.ts +0 -817
  146. package/src/fields/FileUploadField.test.ts +0 -143
  147. package/src/fields/FileUploadField.ts +0 -159
  148. package/src/fields/HiddenField.test.ts +0 -27
  149. package/src/fields/HiddenField.ts +0 -28
  150. package/src/fields/KeyValueField.test.ts +0 -105
  151. package/src/fields/KeyValueField.ts +0 -55
  152. package/src/fields/MarkdownField.test.ts +0 -167
  153. package/src/fields/MarkdownField.ts +0 -162
  154. package/src/fields/NumberField.ts +0 -33
  155. package/src/fields/RadioField.test.ts +0 -94
  156. package/src/fields/RadioField.ts +0 -67
  157. package/src/fields/RepeaterField.test.ts +0 -1806
  158. package/src/fields/RepeaterField.ts +0 -939
  159. package/src/fields/RepeaterRelationship.test.ts +0 -1923
  160. package/src/fields/RepeaterSimple.test.ts +0 -248
  161. package/src/fields/RowButton.test.ts +0 -219
  162. package/src/fields/RowButton.ts +0 -135
  163. package/src/fields/SelectField.test.ts +0 -192
  164. package/src/fields/SelectField.ts +0 -235
  165. package/src/fields/SliderField.test.ts +0 -50
  166. package/src/fields/SliderField.ts +0 -53
  167. package/src/fields/SlugField.ts +0 -24
  168. package/src/fields/TagsInputField.test.ts +0 -154
  169. package/src/fields/TagsInputField.ts +0 -133
  170. package/src/fields/TextField.test.ts +0 -213
  171. package/src/fields/TextField.ts +0 -177
  172. package/src/fields/TextareaField.test.ts +0 -58
  173. package/src/fields/TextareaField.ts +0 -59
  174. package/src/fields/ToggleButtonsField.test.ts +0 -106
  175. package/src/fields/ToggleButtonsField.ts +0 -59
  176. package/src/fields/ToggleField.ts +0 -16
  177. package/src/fields/disableOptionsWhenSelectedInSiblingRepeaterItems.test.ts +0 -319
  178. package/src/fields/optionsResolver.ts +0 -95
  179. package/src/fields/resolveField.ts +0 -28
  180. package/src/filters/BooleanFilter.ts +0 -35
  181. package/src/filters/DateRangeFilter.test.ts +0 -194
  182. package/src/filters/DateRangeFilter.ts +0 -148
  183. package/src/filters/Filter.test.ts +0 -268
  184. package/src/filters/Filter.ts +0 -184
  185. package/src/filters/FormFilter.test.ts +0 -238
  186. package/src/filters/FormFilter.ts +0 -215
  187. package/src/filters/MultiSelectFilter.test.ts +0 -119
  188. package/src/filters/MultiSelectFilter.ts +0 -78
  189. package/src/filters/QueryBuilderFilter.test.ts +0 -662
  190. package/src/filters/QueryBuilderFilter.ts +0 -398
  191. package/src/filters/SelectFilter.ts +0 -46
  192. package/src/filters/TernaryFilter.test.ts +0 -160
  193. package/src/filters/TernaryFilter.ts +0 -72
  194. package/src/filters/TrashedFilter.test.ts +0 -149
  195. package/src/filters/TrashedFilter.ts +0 -55
  196. package/src/filters/queryBuilder/BooleanConstraint.ts +0 -31
  197. package/src/filters/queryBuilder/Constraint.ts +0 -115
  198. package/src/filters/queryBuilder/DateConstraint.ts +0 -69
  199. package/src/filters/queryBuilder/NumberConstraint.ts +0 -66
  200. package/src/filters/queryBuilder/SelectConstraint.ts +0 -72
  201. package/src/filters/queryBuilder/TextConstraint.ts +0 -64
  202. package/src/filters/queryBuilder/index.ts +0 -12
  203. package/src/icons/index.ts +0 -2
  204. package/src/icons/lucide.ts +0 -204
  205. package/src/icons/registry.test.ts +0 -56
  206. package/src/icons/registry.ts +0 -41
  207. package/src/icons/types.ts +0 -47
  208. package/src/index.ts +0 -525
  209. package/src/io/csv.test.ts +0 -142
  210. package/src/io/csv.ts +0 -170
  211. package/src/nestedRelationManagerData.test.ts +0 -547
  212. package/src/notifications/Notification.test.ts +0 -210
  213. package/src/notifications/Notification.ts +0 -354
  214. package/src/notifications/broadcast.test.ts +0 -110
  215. package/src/notifications/broadcast.ts +0 -95
  216. package/src/notifications/database.test.ts +0 -383
  217. package/src/notifications/database.ts +0 -398
  218. package/src/notifications/databaseNotifications.test.ts +0 -187
  219. package/src/notifications/dispatchNotificationAction.test.ts +0 -341
  220. package/src/notifications/dispatchNotificationAction.ts +0 -142
  221. package/src/notifications/flash.test.ts +0 -89
  222. package/src/notifications/flash.ts +0 -71
  223. package/src/notifications/index.ts +0 -45
  224. package/src/notifications/registerBroadcastAuth.test.ts +0 -134
  225. package/src/notifications/registerBroadcastAuth.ts +0 -100
  226. package/src/notifications/resolveSavedNotification.test.ts +0 -82
  227. package/src/notifications/resolveSavedNotification.ts +0 -59
  228. package/src/notifications/types.ts +0 -93
  229. package/src/orm/m2mAccessor.ts +0 -66
  230. package/src/orm/modelDefaults.test.ts +0 -633
  231. package/src/orm/modelDefaults.ts +0 -666
  232. package/src/pageData/breadcrumbs.ts +0 -288
  233. package/src/pageData/forms.ts +0 -578
  234. package/src/pageData/helpers.ts +0 -857
  235. package/src/pageData/misc.ts +0 -347
  236. package/src/pageData/navigation.ts +0 -842
  237. package/src/pageData/relationPages.ts +0 -1248
  238. package/src/pageData/relationTabs.ts +0 -286
  239. package/src/pageData/resourcePages.ts +0 -609
  240. package/src/pageData.test.ts +0 -1545
  241. package/src/pageData.ts +0 -341
  242. package/src/plugins/index.ts +0 -8
  243. package/src/plugins/themeEditor.test.ts +0 -36
  244. package/src/plugins/themeEditor.ts +0 -45
  245. package/src/react/AppShell.tsx +0 -251
  246. package/src/react/CollabExtensionFactoryRegistry.ts +0 -55
  247. package/src/react/CollabRoomContext.ts +0 -98
  248. package/src/react/CollabTextRendererRegistry.ts +0 -102
  249. package/src/react/CommandPalette.tsx +0 -375
  250. package/src/react/CurrentUserContext.tsx +0 -50
  251. package/src/react/CustomPageWrapperGate.tsx +0 -69
  252. package/src/react/CustomPageWrapperRegistry.ts +0 -45
  253. package/src/react/FieldFocusReporterRegistry.ts +0 -37
  254. package/src/react/FieldLabelSlotRegistry.ts +0 -30
  255. package/src/react/FieldPresenceRegistry.ts +0 -46
  256. package/src/react/FormCollabBindingRegistry.ts +0 -242
  257. package/src/react/FormStateContext.tsx +0 -591
  258. package/src/react/HeadHooks.tsx +0 -126
  259. package/src/react/MarkdownEditorRegistry.test.ts +0 -38
  260. package/src/react/MarkdownEditorRegistry.ts +0 -107
  261. package/src/react/NotificationActionStrip.tsx +0 -263
  262. package/src/react/NotificationBell.tsx +0 -426
  263. package/src/react/PendingSuggestionApplierRegistry.test.ts +0 -97
  264. package/src/react/PendingSuggestionApplierRegistry.ts +0 -98
  265. package/src/react/PendingSuggestionOverlayRegistry.ts +0 -54
  266. package/src/react/PendingSuggestionsContext.tsx +0 -172
  267. package/src/react/RecordWrapperGate.tsx +0 -58
  268. package/src/react/RecordWrapperRegistry.ts +0 -39
  269. package/src/react/RenderHookSlot.tsx +0 -32
  270. package/src/react/RightSidebar.tsx +0 -257
  271. package/src/react/RightSidebarContext.tsx +0 -234
  272. package/src/react/RightSidebarTrigger.tsx +0 -53
  273. package/src/react/RowCoordsContext.tsx +0 -23
  274. package/src/react/SchemaRenderer.tsx +0 -549
  275. package/src/react/SearchTrigger.tsx +0 -46
  276. package/src/react/ThemeProvider.tsx +0 -93
  277. package/src/react/ThemeSettingsPage.tsx +0 -579
  278. package/src/react/ThemeToggle.tsx +0 -20
  279. package/src/react/Toaster.tsx +0 -158
  280. package/src/react/UserMenu.tsx +0 -196
  281. package/src/react/WidgetDataContext.tsx +0 -157
  282. package/src/react/cells/EditableCell.tsx +0 -389
  283. package/src/react/component-slots.test.ts +0 -103
  284. package/src/react/component-slots.ts +0 -116
  285. package/src/react/fieldJsHandler.test.ts +0 -166
  286. package/src/react/fieldJsHandler.ts +0 -79
  287. package/src/react/fields/BuilderInput.tsx +0 -1078
  288. package/src/react/fields/CheckboxInput.tsx +0 -39
  289. package/src/react/fields/CheckboxListInput.tsx +0 -102
  290. package/src/react/fields/ColorInput.tsx +0 -71
  291. package/src/react/fields/DateFieldInput.tsx +0 -70
  292. package/src/react/fields/DateTimeInput.tsx +0 -62
  293. package/src/react/fields/FieldShell.tsx +0 -348
  294. package/src/react/fields/FileUploadInput.tsx +0 -639
  295. package/src/react/fields/HiddenInput.tsx +0 -17
  296. package/src/react/fields/KeyValueInput.tsx +0 -230
  297. package/src/react/fields/MarkdownInput.tsx +0 -560
  298. package/src/react/fields/RadioInput.tsx +0 -81
  299. package/src/react/fields/RepeaterInput.test.ts +0 -116
  300. package/src/react/fields/RepeaterInput.tsx +0 -1420
  301. package/src/react/fields/SelectFieldInput.tsx +0 -280
  302. package/src/react/fields/SliderInput.tsx +0 -81
  303. package/src/react/fields/TagsInput.tsx +0 -283
  304. package/src/react/fields/TextLikeInput.tsx +0 -256
  305. package/src/react/fields/ToggleButtonsInput.tsx +0 -60
  306. package/src/react/fields/ToggleFieldInput.tsx +0 -56
  307. package/src/react/fields/relationshipRenameDispatch.test.ts +0 -106
  308. package/src/react/fields/relationshipRenameDispatch.ts +0 -97
  309. package/src/react/fields/repeaterReconcile.test.ts +0 -114
  310. package/src/react/fields/repeaterReconcile.ts +0 -104
  311. package/src/react/fields/rowChromeButton.tsx +0 -336
  312. package/src/react/fields/rowState.ts +0 -106
  313. package/src/react/fields/syncRowGates.test.ts +0 -202
  314. package/src/react/fields/syncRowGates.ts +0 -66
  315. package/src/react/fields/textInputControls.tsx +0 -238
  316. package/src/react/fields/useRowReorderDnd.ts +0 -78
  317. package/src/react/formStateHelpers.test.ts +0 -508
  318. package/src/react/formStateHelpers.ts +0 -381
  319. package/src/react/hooks/use-mobile.ts +0 -19
  320. package/src/react/icon-context.tsx +0 -60
  321. package/src/react/index.ts +0 -195
  322. package/src/react/layouts/SidebarLayout.tsx +0 -250
  323. package/src/react/layouts/TopbarLayout.tsx +0 -258
  324. package/src/react/navigate.tsx +0 -37
  325. package/src/react/onProviderSynced.test.ts +0 -90
  326. package/src/react/parseRecordEditUrl.test.ts +0 -122
  327. package/src/react/parseRecordEditUrl.ts +0 -94
  328. package/src/react/persistedState.ts +0 -40
  329. package/src/react/registry.ts +0 -48
  330. package/src/react/right-panel-registry.tsx +0 -47
  331. package/src/react/schemaRenderer/AlertRenderer.tsx +0 -112
  332. package/src/react/schemaRenderer/EntryRenderer.tsx +0 -501
  333. package/src/react/schemaRenderer/SectionRenderer.tsx +0 -120
  334. package/src/react/schemaRenderer/SimpleElements.tsx +0 -306
  335. package/src/react/schemaRenderer/TabsRenderer.tsx +0 -62
  336. package/src/react/schemaRenderer/WizardRenderer.tsx +0 -338
  337. package/src/react/schemaRenderer/action/ActionGroupTrigger.tsx +0 -177
  338. package/src/react/schemaRenderer/action/ActionModalDialog.tsx +0 -273
  339. package/src/react/schemaRenderer/action/ConfirmActionDialog.tsx +0 -61
  340. package/src/react/schemaRenderer/action/HandlerActionButton.tsx +0 -43
  341. package/src/react/schemaRenderer/action/MethodActionButton.tsx +0 -64
  342. package/src/react/schemaRenderer/action/buttons.tsx +0 -99
  343. package/src/react/schemaRenderer/action/helpers.ts +0 -140
  344. package/src/react/schemaRenderer/action/renderAction.tsx +0 -245
  345. package/src/react/schemaRenderer/columnFormat.ts +0 -65
  346. package/src/react/schemaRenderer/constants.ts +0 -50
  347. package/src/react/schemaRenderer/form/FormRenderer.tsx +0 -274
  348. package/src/react/schemaRenderer/form/renderField.tsx +0 -511
  349. package/src/react/schemaRenderer/helpers.tsx +0 -81
  350. package/src/react/schemaRenderer/table/CardsLayoutBody.tsx +0 -308
  351. package/src/react/schemaRenderer/table/TableRenderer.tsx +0 -123
  352. package/src/react/schemaRenderer/table/TableRendererBody.tsx +0 -974
  353. package/src/react/schemaRenderer/table/filters.tsx +0 -1233
  354. package/src/react/schemaRenderer/table/formatCell.tsx +0 -264
  355. package/src/react/schemaRenderer/table/links.tsx +0 -112
  356. package/src/react/schemaRenderer/table/renderRowActions.tsx +0 -52
  357. package/src/react/schemaRenderer/table/url.tsx +0 -143
  358. package/src/react/theme-preview/apply.ts +0 -99
  359. package/src/react/theme-preview/build-html.ts +0 -436
  360. package/src/react/ui/button.tsx +0 -51
  361. package/src/react/ui/calendar.tsx +0 -67
  362. package/src/react/ui/checkbox.tsx +0 -29
  363. package/src/react/ui/dialog.tsx +0 -108
  364. package/src/react/ui/dropdown-menu.tsx +0 -97
  365. package/src/react/ui/input.tsx +0 -20
  366. package/src/react/ui/label.tsx +0 -21
  367. package/src/react/ui/popover.tsx +0 -50
  368. package/src/react/ui/select.tsx +0 -169
  369. package/src/react/ui/separator.tsx +0 -25
  370. package/src/react/ui/sheet.tsx +0 -136
  371. package/src/react/ui/sidebar.tsx +0 -723
  372. package/src/react/ui/skeleton.tsx +0 -13
  373. package/src/react/ui/slider.tsx +0 -34
  374. package/src/react/ui/switch.tsx +0 -28
  375. package/src/react/ui/table.tsx +0 -105
  376. package/src/react/ui/tabs.tsx +0 -63
  377. package/src/react/ui/textarea.tsx +0 -18
  378. package/src/react/ui/tooltip.tsx +0 -64
  379. package/src/react/useCollabSeed.ts +0 -86
  380. package/src/react/useResizableWidth.ts +0 -139
  381. package/src/react/utils.ts +0 -6
  382. package/src/react/widgetRegistry.test.ts +0 -43
  383. package/src/react/widgetRegistry.ts +0 -50
  384. package/src/react/widgets/StatsOverviewRenderer.tsx +0 -232
  385. package/src/react/widgets/TableWidgetRenderer.tsx +0 -231
  386. package/src/react/widgets/ViewRenderer.tsx +0 -71
  387. package/src/relationManagerData.test.ts +0 -1595
  388. package/src/richtext/index.ts +0 -8
  389. package/src/richtext/registry.ts +0 -89
  390. package/src/routes/globals.ts +0 -148
  391. package/src/routes/guard.test.ts +0 -325
  392. package/src/routes/helpers.ts +0 -700
  393. package/src/routes/pages.ts +0 -175
  394. package/src/routes/panel.ts +0 -204
  395. package/src/routes/relations.ts +0 -1227
  396. package/src/routes/resources.ts +0 -781
  397. package/src/routes/theme.ts +0 -91
  398. package/src/routes-nested-relations.test.ts +0 -676
  399. package/src/routes-relations.test.ts +0 -972
  400. package/src/routes.test.ts +0 -2027
  401. package/src/routes.ts +0 -303
  402. package/src/schema/Alert.test.ts +0 -109
  403. package/src/schema/Alert.ts +0 -131
  404. package/src/schema/Block.ts +0 -169
  405. package/src/schema/Breadcrumbs.ts +0 -40
  406. package/src/schema/Card.ts +0 -35
  407. package/src/schema/Divider.ts +0 -20
  408. package/src/schema/Element.ts +0 -219
  409. package/src/schema/EmptyState.test.ts +0 -37
  410. package/src/schema/EmptyState.ts +0 -63
  411. package/src/schema/Fieldset.ts +0 -43
  412. package/src/schema/Grid.ts +0 -43
  413. package/src/schema/Group.ts +0 -30
  414. package/src/schema/Heading.ts +0 -39
  415. package/src/schema/Html.ts +0 -67
  416. package/src/schema/Icon.ts +0 -54
  417. package/src/schema/Image.ts +0 -57
  418. package/src/schema/LinkTag.ts +0 -41
  419. package/src/schema/Markdown.ts +0 -85
  420. package/src/schema/MetaTag.ts +0 -41
  421. package/src/schema/RelationTabs.ts +0 -71
  422. package/src/schema/ScriptTag.ts +0 -55
  423. package/src/schema/Section.ts +0 -160
  424. package/src/schema/ServerDataElement.test.ts +0 -140
  425. package/src/schema/ServerDataElement.ts +0 -156
  426. package/src/schema/SlotComponent.test.ts +0 -77
  427. package/src/schema/SlotComponent.ts +0 -71
  428. package/src/schema/Split.ts +0 -50
  429. package/src/schema/Stat.test.ts +0 -118
  430. package/src/schema/Stat.ts +0 -154
  431. package/src/schema/StatsOverview.test.ts +0 -141
  432. package/src/schema/StatsOverview.ts +0 -119
  433. package/src/schema/StyleTag.ts +0 -35
  434. package/src/schema/TableWidget.test.ts +0 -297
  435. package/src/schema/TableWidget.ts +0 -289
  436. package/src/schema/Tabs.ts +0 -79
  437. package/src/schema/Text.ts +0 -58
  438. package/src/schema/UnorderedList.ts +0 -49
  439. package/src/schema/View.test.ts +0 -111
  440. package/src/schema/View.ts +0 -127
  441. package/src/schema/Wizard.ts +0 -220
  442. package/src/schema/containers.test.ts +0 -564
  443. package/src/schema/headTags.test.ts +0 -134
  444. package/src/schema/index.ts +0 -40
  445. package/src/schema/primes.test.ts +0 -269
  446. package/src/schema/resolveSchema.test.ts +0 -379
  447. package/src/schema/resolveSchema.ts +0 -917
  448. package/src/schema/sanitize.ts +0 -58
  449. package/src/search.test.ts +0 -446
  450. package/src/search.ts +0 -178
  451. package/src/sessionFilters.test.ts +0 -375
  452. package/src/sessionFilters.ts +0 -143
  453. package/src/slot-components/index.ts +0 -10
  454. package/src/slot-components/registry.ts +0 -56
  455. package/src/styles/file-upload.css +0 -13
  456. package/src/summarizers/Summarizer.test.ts +0 -84
  457. package/src/summarizers/Summarizer.ts +0 -123
  458. package/src/summarizers/index.ts +0 -11
  459. package/src/theme/base-colors.ts +0 -68
  460. package/src/theme/chart-colors.ts +0 -50
  461. package/src/theme/colors.ts +0 -447
  462. package/src/theme/generate-css.test.ts +0 -139
  463. package/src/theme/generate-css.ts +0 -44
  464. package/src/theme/generate-scale.test.ts +0 -106
  465. package/src/theme/generate-scale.ts +0 -97
  466. package/src/theme/icon-map.ts +0 -42
  467. package/src/theme/index.ts +0 -34
  468. package/src/theme/migrate.test.ts +0 -178
  469. package/src/theme/migrate.ts +0 -81
  470. package/src/theme/presets.ts +0 -135
  471. package/src/theme/radius.ts +0 -18
  472. package/src/theme/resolve.test.ts +0 -238
  473. package/src/theme/resolve.ts +0 -96
  474. package/src/theme/spacing.ts +0 -18
  475. package/src/theme/storage.test.ts +0 -126
  476. package/src/theme/storage.ts +0 -106
  477. package/src/theme/theme-colors.ts +0 -88
  478. package/src/theme/types.ts +0 -125
  479. package/src/uploads/UploadAdapter.ts +0 -35
  480. package/src/uploads/index.ts +0 -2
  481. package/src/uploads/localUpload.test.ts +0 -70
  482. package/src/uploads/localUpload.ts +0 -84
  483. package/src/validation/Validator.ts +0 -49
  484. package/src/validation/index.ts +0 -28
  485. package/src/validation/rules.ts +0 -78
  486. package/src/validation/runValidators.ts +0 -435
  487. package/src/validation/uniqueValidator.test.ts +0 -196
  488. package/src/validation/uniqueValidator.ts +0 -133
  489. package/src/validation/validators.test.ts +0 -268
  490. package/src/vite.test.ts +0 -184
  491. package/src/vite.ts +0 -787
  492. package/src/widgets/index.ts +0 -10
  493. package/src/widgets/registry.ts +0 -45
  494. package/src/widgets.test.ts +0 -592
  495. package/tsconfig.build.json +0 -11
  496. package/tsconfig.json +0 -4
  497. package/tsconfig.test.json +0 -10
  498. package/views/react/Dashboard.tsx +0 -27
  499. package/views/react/Resources/Form.tsx +0 -102
  500. 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
-