@pilotiq/pilotiq 0.24.1 → 0.24.3

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