@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
package/src/Pilotiq.ts DELETED
@@ -1,1158 +0,0 @@
1
- import type * as React from 'react'
2
- import type { Router } from '@rudderjs/router'
3
- import type { ResourceClass } from './Resource.js'
4
- import type { GlobalClass } from './Global.js'
5
- import type { ClusterClass } from './Cluster.js'
6
- import type { Page } from './Page.js'
7
- import type { SchemaDefinition } from './schema/resolveSchema.js'
8
- import type { ThemeConfig } from './theme/types.js'
9
- import type { ThemeStorageAdapter } from './theme/storage.js'
10
- import type { UploadAdapter } from './uploads/UploadAdapter.js'
11
- import type { UserMenuItem } from './UserMenuItem.js'
12
- import type { NavigationBadgeColor } from './Resource.js'
13
- import type {
14
- RenderHookEntry,
15
- RenderHookFn,
16
- RenderHookName,
17
- RenderHookScope,
18
- } from './RenderHook.js'
19
- import type { NotificationActionHandler } from './notifications/types.js'
20
- import {
21
- validateRightPanel,
22
- findDuplicateRightPanelId,
23
- type RightPanelContribution,
24
- } from './RightPanel.js'
25
- import type {
26
- NavComponentProps,
27
- HeaderComponentProps,
28
- FooterComponentProps,
29
- } from './react/component-slots.js'
30
-
31
- export type PilotiqLayout = 'sidebar' | 'topbar'
32
-
33
- /** Plugin interface for extending Pilotiq panels. */
34
- export interface PilotiqPlugin {
35
- name: string
36
- /** Called when `.use()` / `.plugins([…])` runs at panel-build time —
37
- * mutate config as needed (register a right-sidebar contribution,
38
- * register a field renderer, seed an internal registry, install a
39
- * `Field.prototype` augment, etc). Idempotent under HMR / re-mount. */
40
- register(panel: Pilotiq): void
41
- /**
42
- * Optional — called by `registerPilotiqRoutes(router, pilotiq)` AFTER
43
- * the core routes are mounted, so plugins can register their own
44
- * HTTP surface alongside the panel's. The mount order matches the
45
- * order plugins were registered via `.use()` / `.plugins([…])`.
46
- *
47
- * Use this for plugin-owned endpoints (`POST {base}/_chat`,
48
- * `POST {base}/_collab`, …) — the path prefix should mirror
49
- * pilotiq's own underscore-prefixed precedent (`_search`,
50
- * `_uploads`, `_widget`, `_notifications`).
51
- *
52
- * Skip this hook for plugins that only mutate panel config; nothing
53
- * inside core walks plugins outside `register()` / `registerRoutes()`.
54
- */
55
- registerRoutes?(router: Router, pilotiq: Pilotiq): void
56
- }
57
-
58
- /**
59
- * User resolver — receives the request and returns the current user (or
60
- * null). Pilotiq treats the user object as opaque; whatever the resolver
61
- * returns is forwarded into `Resource.canX(user, …)` / `Global.canX(...)` /
62
- * `Page.canAccess(user)` and into `Action` visibility rules. Sync or async.
63
- *
64
- * Apps using `@rudderjs/auth` typically pass `req => Auth.user()`. The
65
- * resolver is optional — when unset, every `can*` predicate runs with
66
- * `user === null` and the defaults (which return `true`) keep the panel
67
- * working with no auth wired up.
68
- */
69
- export type UserResolver = (req: unknown) => unknown | null | Promise<unknown | null>
70
-
71
- /**
72
- * Server-side hook handed to `Pilotiq.editPageHydrator(fn)`. The
73
- * resource-edit data builder calls every registered hydrator after the
74
- * standard fill pipeline (`loadRecord` → `mutateFormDataBeforeFill` →
75
- * `fillFromRecord` → `mutateFormDataAfterFill` →
76
- * `applyRelationshipRepeaterFill` → `applyRelationshipBuilderFill`) and
77
- * merges each non-null return onto the form's default values.
78
- *
79
- * Multiple hydrators are walked in registration order; later hydrators
80
- * override keys produced by earlier ones. A hydrator returning `null`
81
- * (or throwing) is a pass-through — the DB row values survive intact.
82
- *
83
- * Designed for the SSR-from-Y.Doc consumer in `@pilotiq-pro/collab`:
84
- * read persisted Y.Text + Y.Map values for the record and override the
85
- * matching field defaults, killing the DB → Y.Doc value flicker on
86
- * hydration. Other consumers (per-tenant overrides, A/B experiments,
87
- * draft-snapshot resume) compose the same way.
88
- *
89
- * Pilotiq core stays Yjs-free — the hook's `Record<string, unknown>`
90
- * return is opaque, so the collab consumer's Yjs imports stay confined
91
- * to its own `/server` subpath.
92
- */
93
- export interface EditPageHydratorContext {
94
- /** The Resource class being edited (server-side reference, not the
95
- * serialized meta — gives access to `model`, `getSlug()`, etc.). */
96
- resource: ResourceClass
97
- /** Record id from the URL, stringified. */
98
- recordId: string
99
- /** Values produced by the fill pipeline so far — DB row → hooks →
100
- * relationship fills. Hydrators read this to make merge decisions
101
- * (e.g. "only override fields that have content in Y.Doc"). */
102
- currentValues: Record<string, unknown>
103
- }
104
-
105
- export type EditPageHydrator = (
106
- ctx: EditPageHydratorContext,
107
- ) => Record<string, unknown> | null | Promise<Record<string, unknown> | null>
108
-
109
- /**
110
- * Upload configuration. Apps register an adapter via `Pilotiq.uploads({
111
- * adapter })`; the `_uploads` route hands every incoming file to it.
112
- * Without an adapter, `FileUpload` fields render but the upload POST
113
- * fails with a clear "no upload adapter configured" error.
114
- */
115
- export interface UploadConfig {
116
- adapter: UploadAdapter
117
- }
118
-
119
- /**
120
- * Database notifications configuration. Wired through
121
- * `Pilotiq.databaseNotifications(opts?)`.
122
- *
123
- * Pilotiq stores rows on the `notification` table shipped by
124
- * `@rudderjs/notification` (`prisma/schema/notification.prisma`). The
125
- * panel consumes that table directly via `@rudderjs/orm`'s
126
- * `ModelRegistry` adapter so apps that already have rudder's
127
- * `NotificationProvider` running can read existing rows alongside ones
128
- * authored by `Notification.make(...).sendToDatabase(recipient)`.
129
- *
130
- * `position` chooses where the bell mounts in the panel chrome.
131
- * `'topbar'` (default) sits between `<ThemeToggle>` and `<UserMenu>` in
132
- * both the sidebar and topbar layouts; `'sidebar'` moves it into the
133
- * sidebar footer (parity with Filament's
134
- * `DatabaseNotificationsPosition::Sidebar`).
135
- *
136
- * `polling` is the seconds between background refetches. `null`
137
- * disables auto-polling — the bell fetches once on mount and after
138
- * mark-read mutations. Filament default is 30 seconds; we mirror that.
139
- *
140
- * `pageSize` caps the list endpoint's page size (default 25).
141
- *
142
- * `badgeColor` recolors the unread-count pill on the bell trigger.
143
- * Default `'primary'`. Reuses the same `NavigationBadgeColor` palette
144
- * that the sidebar nav badges use.
145
- *
146
- * `trigger.icon` overrides the bell icon (registry name); `trigger.label`
147
- * overrides the trigger's `aria-label`.
148
- *
149
- * `notifiableType` is the value pilotiq writes/reads against the
150
- * `notifiable_type` column. Defaults to `'users'` to match
151
- * `@rudderjs/notification`'s `DatabaseChannel`. Override when your app
152
- * stores rows tagged for a different notifiable.
153
- */
154
- export interface DatabaseNotificationsConfig {
155
- /** Where the bell mounts. Default `'topbar'`. */
156
- position?: 'topbar' | 'sidebar'
157
- /** Auto-poll interval in seconds. `null` disables. Default `30`. */
158
- polling?: number | null
159
- /** Max rows the list endpoint returns. Default `25`. */
160
- pageSize?: number
161
- /** Unread badge color on the bell trigger. Default `'primary'`. */
162
- badgeColor?: NavigationBadgeColor
163
- /** Bell trigger overrides. */
164
- trigger?: { icon?: string; label?: string }
165
- /** `notifiable_type` column value. Default `'users'`. */
166
- notifiableType?: string
167
- /**
168
- * Phase 2 — push new rows over WebSocket on
169
- * `private-pilotiq-notifications.${user.id}` so the bell client can
170
- * refetch immediately instead of waiting for the next poll. Requires
171
- * `@rudderjs/broadcast` (the `BroadcastingProvider` must be in the
172
- * app's providers list and the `RudderSocket` client must be vendored).
173
- *
174
- * Soft-fails when broadcast isn't installed — pilotiq still works,
175
- * the bell just falls back to polling.
176
- *
177
- * Pass a string to override the WebSocket URL the client connects to
178
- * (default: same-origin `ws://…/ws`). Most apps leave this on the
179
- * default — `@rudderjs/broadcast`'s `BroadcastConfig.path` already
180
- * mounts the upgrade handler at `/ws`.
181
- */
182
- broadcast?: boolean | { wsUrl?: string }
183
- }
184
-
185
- /**
186
- * Sign-out configuration. Wired through `Pilotiq.signOut(url | config)`.
187
- *
188
- * The user menu renders the sign-out entry as a `<form method="POST"
189
- * action={url}>` so CSRF middleware downstream can validate the request
190
- * (a bare `<a href>` would be GET-only). Set `method: 'GET'` for
191
- * traditional logout endpoints that redirect on a normal navigation.
192
- */
193
- export interface SignOutConfig {
194
- url: string
195
- label?: string
196
- method?: 'POST' | 'GET'
197
- }
198
-
199
- export interface PilotiqConfig {
200
- name: string
201
- path: string
202
- layout: PilotiqLayout
203
- resources: ResourceClass[]
204
- globals: GlobalClass[]
205
- pages: (typeof Page)[]
206
- clusters: ClusterClass[]
207
- branding: { title?: string; logo?: string }
208
- schema?: SchemaDefinition
209
- /**
210
- * Plan #15 — homepage page class. When set, `GET ${base}` resolves
211
- * this Page's `static schema()` instead of the builder-level
212
- * `schema()`. Set via `panel.dashboard(P)`. The page is also added
213
- * to `cfg.pages` for nav + canAccess gating, with its nav URL
214
- * collapsed back to `${base}` so the sidebar entry deep-links to
215
- * the panel root.
216
- */
217
- dashboardPage?: typeof Page
218
- /**
219
- * Profile page — when set, the user-menu dropdown auto-prepends an
220
- * entry pointing at this Page's URL. The Page is registered in
221
- * `cfg.pages` for routing + nav + canAccess gating. The menu entry's
222
- * label and icon read from the Page's `static label` / `static icon`
223
- * (with `'Edit profile'` and `'user-circle'` defaults).
224
- *
225
- * The Page is otherwise a normal `Page` subclass — author the form
226
- * (`static schema()`) against your own auth model since pilotiq
227
- * treats the user object as opaque.
228
- */
229
- profilePage?: typeof Page
230
- theme?: ThemeConfig
231
- themeEditor?: boolean
232
- /**
233
- * Theme override persistence adapter — wired via
234
- * `themeEditor({ storage })`. Reads/writes the JSON blob the editor
235
- * page produces. Without this, the service provider falls back to
236
- * the implicit Prisma adapter (auto-resolved via
237
- * `app.make('prisma')`) for back-compat — that fallback is
238
- * deprecated and will be removed in a future minor; pass `storage`
239
- * explicitly.
240
- */
241
- themeStorage?: ThemeStorageAdapter
242
- guard?: (req: unknown) => boolean | Promise<boolean>
243
- user?: UserResolver
244
- uploads?: UploadConfig
245
- /**
246
- * Top-right user-menu entries (between user identity and sign-out).
247
- * Order: items with explicit `.sort(n)` ascending, ties → registration
248
- * order; unsorted items follow in registration order.
249
- */
250
- userMenuItems?: UserMenuItem[]
251
- /** Sign-out endpoint config — when set, the user menu appends a
252
- * separator + Sign-out entry. Without this, the menu shows custom
253
- * items (if any) and the user identity, but no sign-out affordance. */
254
- signOut?: SignOutConfig
255
- /** Server-side hydrators applied after the fill pipeline on every
256
- * resource edit page. Empty / unset → behaviour unchanged. See
257
- * `EditPageHydrator` for the contract; plugins register via
258
- * `panel.editPageHydrator(fn)`. */
259
- editPageHydrators?: EditPageHydrator[]
260
- /** Database notifications — opt-in. Mounts the bell + 4 endpoints
261
- * (`_notifications` list / `:id/read` / `:id/unread` / `read-all`).
262
- * Reads rows from `@rudderjs/notification`'s `notification` table via
263
- * `@rudderjs/orm`'s `ModelRegistry` adapter, scoped to
264
- * `pilotiq.resolveUser(req).id`. */
265
- databaseNotifications?: DatabaseNotificationsConfig & { enabled: true }
266
- /**
267
- * Named notification-action handlers. Registered via
268
- * `Pilotiq.notificationHandlers({ name: fn })`. Looked up by the
269
- * `_notifications/:id/_action/:actionName` route at request time when
270
- * a stored action carries `handler: 'name'`.
271
- *
272
- * The closure-handler path on Action (`.handler(async ctx => …)`)
273
- * works for transient toasts but doesn't survive a `data` JSON
274
- * round-trip — this registry is the persistence-safe escape hatch.
275
- */
276
- notificationHandlers?: Record<string, NotificationActionHandler>
277
- /**
278
- * Render-hook entries registered via `Pilotiq.renderHook(name, fn,
279
- * scope?)`. Resolved per-request by `panelInfo()` (chrome slots) and
280
- * the per-page-role data builders (page-role slots).
281
- */
282
- renderHooks?: RenderHookEntry[]
283
- /**
284
- * Right-sidebar plugin contributions registered via
285
- * `Pilotiq.rightPanel(c)` / `Pilotiq.rightPanels([c, …])`. Each entry
286
- * is auth-gated, sorted, and serialized into `panel.rightSidebar`
287
- * under `panelInfo()`. Empty / all-gated → `panel.rightSidebar` is
288
- * absent and the chrome doesn't mount.
289
- */
290
- rightPanels?: RightPanelContribution[]
291
- /**
292
- * Layout-level provider components registered via
293
- * `Pilotiq.layoutProvider(C)` / `Pilotiq.layoutProviders([…])`. Plugins
294
- * register React providers (e.g. an AI chat queue context, a tenant
295
- * theme switcher) that wrap the panel's `<AppShell>` children — so
296
- * the providers are in scope for every page in the panel, not just
297
- * specific component slots. Order matters: the first registered
298
- * provider sits closest to the root (outermost wrap); the last sits
299
- * closest to the page tree (innermost wrap).
300
- */
301
- layoutProviders?: LayoutProviderComponent[]
302
- /**
303
- * Build-time component overrides for panel chrome slots. Registered
304
- * via `Pilotiq.components({ nav })`. The Vite plugin harvests the
305
- * actual React refs into `pages/(pilotiq)/_components.ts`; component
306
- * refs never travel over the wire.
307
- *
308
- * v1 ships only the `nav` slot — full replacement of the sidebar's
309
- * `<SidebarContent>` body and the topbar's `<nav>` element. Other
310
- * slots (`header`, `footer`, …) will land when a concrete consumer
311
- * asks; the shape is open-ended so additions don't break this
312
- * surface.
313
- */
314
- components?: ComponentSlots
315
- /**
316
- * AI suggestion mode — controls what happens when an AI agent calls a
317
- * write tool against a form field.
318
- *
319
- * - `'auto'` (default): the write applies immediately to the form state.
320
- * Existing behavior — agents take effect as soon as the tool returns.
321
- * - `'review'`: the write is staged as a `PendingSuggestion` and the
322
- * field shows an inline diff (text fields) or current → suggested
323
- * comparison (other types) with Approve / Reject buttons. Approve
324
- * runs the field's registered applier; Reject discards.
325
- *
326
- * VS Code-style review flow. Plan: `docs/plans/ai-review-mode.md`.
327
- */
328
- aiSuggestionsMode?: 'auto' | 'review'
329
- /** @internal Runtime theme overrides from DB. */
330
- _themeOverrides?: Partial<ThemeConfig>
331
- /**
332
- * TTL (milliseconds) for the per-user navigation badge cache. Set to
333
- * `0` (or `null` via the builder) to disable caching. Default 30000.
334
- */
335
- navigationBadgeTtlMs?: number
336
- }
337
-
338
- /**
339
- * Shape a layout-provider component must implement. Receives the
340
- * standard layout context (`basePath`, panel children) so providers can
341
- * thread panel-aware values into their context — e.g. an AI chat
342
- * provider needs `basePath` to know where to POST chat requests.
343
- */
344
- export type LayoutProviderComponent = React.ComponentType<{
345
- children: React.ReactNode
346
- basePath?: string
347
- }>
348
-
349
- /**
350
- * Chrome-slot overrides registered through `Pilotiq.components({ … })`.
351
- *
352
- * Three slots ship today:
353
- *
354
- * - `nav` — replaces the default nav tree (`<SidebarMenu>` body in
355
- * `SidebarLayout`, the `<nav>` cluster in `TopbarLayout`). Surrounding
356
- * chrome (branding header, render-hook splices, footer, sign-out
357
- * menu) stays.
358
- * - `header` — replaces the whole `<header>` chrome bar. In
359
- * `SidebarLayout` that's the top bar with search / theme / bell /
360
- * user menu; in `TopbarLayout` that's the whole top region including
361
- * the brand cluster AND the nav (setting `header` makes `nav`
362
- * irrelevant in `TopbarLayout`). Render hooks that splice INSIDE the
363
- * default header (`panels::topbar.*`, `panels::user-menu.*`) don't
364
- * fire when the header is replaced — the surrounding container is
365
- * gone. The consumer reimplements the chrome controls they want
366
- * (import `<SearchTrigger>`, `<ThemeToggle>`, `<NotificationBell>`,
367
- * `<RightSidebarTrigger>`, `<UserMenu>` from `@pilotiq/pilotiq/react`).
368
- * - `footer` — mounts a `<footer>` element BELOW the main content area
369
- * in both layouts (outside the scroll region). Separate from the
370
- * `panels::footer` render hook, which keeps firing inside the content
371
- * area for per-page trailing chrome.
372
- *
373
- * The shape is open-ended so additional slots can land without a
374
- * breaking change when a concrete consumer asks for them.
375
- */
376
- export interface ComponentSlots {
377
- /** Component rendered in place of the default nav tree. */
378
- nav?: React.ComponentType<NavComponentProps>
379
- /** Component rendered in place of the default `<header>` chrome. */
380
- header?: React.ComponentType<HeaderComponentProps>
381
- /** Component rendered as the panel footer, below the main content. */
382
- footer?: React.ComponentType<FooterComponentProps>
383
- }
384
-
385
- export class Pilotiq {
386
- private config: PilotiqConfig
387
- private installedPlugins: PilotiqPlugin[] = []
388
- /** Lazy slug-indexed caches. Built on first lookup; invalidated when
389
- * the underlying setter mutates the matching array. Resources /
390
- * globals / pages are looked up by slug 16+ times per request across
391
- * the page-data builders — the linear `Array.find` adds up around 50+
392
- * resources. */
393
- private _resourceBySlug?: Map<string, ResourceClass>
394
- private _globalBySlug?: Map<string, GlobalClass>
395
- private _pageBySlug?: Map<string, typeof Page>
396
- /**
397
- * Per-user navigation badge cache. Keyed by `${ownerName}|${userKey}`
398
- * — `userKey` derived from `user.id` (or the primitive user / JSON
399
- * fallback / `''` for anon). Each entry expires after
400
- * `getNavigationBadgeTtl()` ms.
401
- */
402
- private _navigationBadgeCache: Map<string, { value: string | undefined; expires: number }> = new Map()
403
-
404
- private constructor(name: string) {
405
- this.config = {
406
- name,
407
- path: '/admin',
408
- layout: 'sidebar',
409
- resources: [],
410
- globals: [],
411
- pages: [],
412
- clusters: [],
413
- branding: {},
414
- }
415
- }
416
-
417
- static make(name: string): Pilotiq {
418
- return new Pilotiq(name)
419
- }
420
-
421
- path(p: string): this {
422
- this.config.path = `/${p.replace(/^\/+/, '')}`
423
- return this
424
- }
425
-
426
- branding(b: { title?: string; logo?: string }): this {
427
- this.config.branding = b
428
- return this
429
- }
430
-
431
- resources(r: ResourceClass[]): this {
432
- this.config.resources = r
433
- delete this._resourceBySlug
434
- return this
435
- }
436
-
437
- globals(g: GlobalClass[]): this {
438
- this.config.globals = g
439
- delete this._globalBySlug
440
- return this
441
- }
442
-
443
- pages(p: (typeof Page)[]): this {
444
- this.config.pages = p
445
- delete this._pageBySlug
446
- return this
447
- }
448
-
449
- clusters(c: ClusterClass[]): this {
450
- this.config.clusters = c
451
- return this
452
- }
453
-
454
- schema(def: SchemaDefinition): this {
455
- this.config.schema = def
456
- return this
457
- }
458
-
459
- /**
460
- * Plan #15 — mark a Page as the panel-root entry. Sugar for "render
461
- * this page's `schema()` at `${base}` instead of the default
462
- * dashboard layout."
463
- *
464
- * Effects:
465
- * 1. `GET ${base}` resolves the page's schema (instead of the
466
- * builder-level `schema()` definition).
467
- * 2. The page is registered in `cfg.pages` if not already there
468
- * (so canAccess + the action / form-state routes wire up).
469
- * 3. Navigation collapses the page's nav URL to `${base}` (no
470
- * trailing slug segment) so the sidebar entry deep-links to
471
- * the panel root.
472
- *
473
- * The page subclass is plain — no special Page subclass required.
474
- * The convention is `static slug = ''` so the regular slug-route
475
- * doesn't also catch it; `panel.dashboard()` enforces this by
476
- * skipping the page in the slug-route registration.
477
- *
478
- * @example
479
- * class MyDashboard extends Page {
480
- * static slug = ''
481
- * static label = 'Dashboard'
482
- * static schema() { return [Heading.make('Welcome')] }
483
- * }
484
- * panel.dashboard(MyDashboard)
485
- */
486
- dashboard(P: typeof Page): this {
487
- this.config.dashboardPage = P
488
- if (!this.config.pages.includes(P)) {
489
- this.config.pages = [...this.config.pages, P]
490
- delete this._pageBySlug
491
- }
492
- return this
493
- }
494
-
495
- /**
496
- * Mark a Page as the panel's profile page. The page is auto-added to
497
- * `cfg.pages` (routing + canAccess wired up) and the user-menu
498
- * dropdown prepends an "Edit profile" entry pointing at it.
499
- *
500
- * class ProfilePage extends Page {
501
- * static slug = 'profile'
502
- * static label = 'My profile'
503
- * static icon = 'user-circle'
504
- * static schema(ctx) {
505
- * return [Form.make().schema([TextField.make('name')…])]
506
- * }
507
- * }
508
- * panel.profile(ProfilePage)
509
- */
510
- profile(P: typeof Page): this {
511
- this.config.profilePage = P
512
- if (!this.config.pages.includes(P)) {
513
- this.config.pages = [...this.config.pages, P]
514
- delete this._pageBySlug
515
- }
516
- return this
517
- }
518
-
519
- layout(l: PilotiqLayout): this {
520
- this.config.layout = l
521
- return this
522
- }
523
-
524
- theme(config: ThemeConfig): this {
525
- this.config.theme = config
526
- return this
527
- }
528
-
529
- guard(fn: (req: unknown) => boolean | Promise<boolean>): this {
530
- this.config.guard = fn
531
- return this
532
- }
533
-
534
- /**
535
- * Configure the current-user resolver. Pilotiq calls `fn(req)` once per
536
- * request and forwards the return value into every `Resource.canX(...)`,
537
- * `Global.canX(...)`, `Page.canAccess(...)`, and `Action.visible(({ user })
538
- * => ...)` callback. The user object is opaque to pilotiq.
539
- *
540
- * Apps using `@rudderjs/auth`:
541
- *
542
- * import { Auth } from '@rudderjs/auth'
543
- * Pilotiq.make('admin').user(() => Auth.user())
544
- *
545
- * Apps with custom auth pass whatever resolves their user. When unset,
546
- * `resolveUser` returns `null` and the default `can*` predicates (which
547
- * ignore `user`) all resolve `true`.
548
- */
549
- user(fn: UserResolver): this {
550
- this.config.user = fn
551
- return this
552
- }
553
-
554
- /**
555
- * Configure file uploads. Pass an adapter implementing
556
- * `UploadAdapter`; `localUpload({ root, urlPrefix })` is bundled for
557
- * disk-backed storage. Apps using S3 / R2 / a custom storage backend
558
- * provide their own adapter conforming to the same interface.
559
- *
560
- * import { localUpload } from '@pilotiq/pilotiq/uploads'
561
- * Pilotiq.make('admin').uploads({
562
- * adapter: localUpload({ root: 'public/uploads', urlPrefix: '/uploads' })
563
- * })
564
- */
565
- uploads(config: UploadConfig): this {
566
- this.config.uploads = config
567
- return this
568
- }
569
-
570
- /**
571
- * Register entries for the panel's top-right user-menu dropdown.
572
- * Replaces any previously registered set (call once with the full
573
- * list). The dropdown only mounts when `Pilotiq.user(req => …)` is
574
- * configured AND resolves a non-null user; otherwise the items are
575
- * silently ignored.
576
- *
577
- * import { UserMenuItem } from '@pilotiq/pilotiq'
578
- * Pilotiq.make('admin')
579
- * .user(req => Auth.user())
580
- * .userMenuItems([
581
- * UserMenuItem.make('profile').label('My profile').url('/profile'),
582
- * UserMenuItem.make('billing').label('Billing').url('/billing').sort(10),
583
- * ])
584
- * .signOut('/logout')
585
- */
586
- userMenuItems(items: UserMenuItem[]): this {
587
- this.config.userMenuItems = items
588
- return this
589
- }
590
-
591
- /**
592
- * Configure the sign-out entry on the user menu. Pass a string for the
593
- * default POST shape, or a `SignOutConfig` to override label/method.
594
- *
595
- * .signOut('/logout')
596
- * .signOut({ url: '/auth/logout', method: 'GET', label: 'Log out' })
597
- */
598
- signOut(config: string | SignOutConfig): this {
599
- this.config.signOut = typeof config === 'string' ? { url: config } : config
600
- return this
601
- }
602
-
603
- /**
604
- * Register a server-side hydrator that runs after the fill pipeline
605
- * on every resource edit page. Each registered hydrator's non-null
606
- * return merges onto the form's default values; multiple registrations
607
- * are walked in registration order (later wins on key conflicts).
608
- *
609
- * panel.editPageHydrator(async ({ resource, recordId }) => {
610
- * // override defaults for this record from your alt source
611
- * return { title: await draftStore.read(resource.getSlug(), recordId) }
612
- * })
613
- *
614
- * Plugins typically register from inside their `register(panel)` hook.
615
- * Throwing or returning `null` is a pass-through — the DB row values
616
- * survive. See `EditPageHydrator` for the full contract.
617
- */
618
- editPageHydrator(fn: EditPageHydrator): this {
619
- if (!this.config.editPageHydrators) this.config.editPageHydrators = []
620
- this.config.editPageHydrators.push(fn)
621
- return this
622
- }
623
-
624
- /**
625
- * Enable persistent database-backed notifications. Mounts the bell in
626
- * the panel chrome + a small set of `_notifications` endpoints. The
627
- * bell only renders when a non-null user resolves from
628
- * `Pilotiq.user(req => …)` — anonymous requests never see the
629
- * affordance.
630
- *
631
- * Sends originate from `Notification.make('Saved')
632
- * .sendToDatabase(user)` (and the rudder `Notifier.send(user, ...)` /
633
- * `notify(user, ...)` paths when the app uses the upstream
634
- * notification class). Both write to the same `notification` table so
635
- * the bell surfaces both.
636
- *
637
- * Pilotiq.make('admin')
638
- * .user(req => Auth.user())
639
- * .databaseNotifications() // defaults
640
- * .databaseNotifications({ position: 'sidebar' }) // bell in sidebar
641
- * .databaseNotifications({ polling: null }) // disable polling
642
- * .databaseNotifications({ polling: 10, pageSize: 50 }) // custom
643
- */
644
- databaseNotifications(opts: DatabaseNotificationsConfig = {}): this {
645
- this.config.databaseNotifications = { ...opts, enabled: true }
646
- return this
647
- }
648
-
649
- /**
650
- * Sugar setter for the polling interval. `null` disables auto-poll.
651
- * Has no effect unless `.databaseNotifications()` was called.
652
- */
653
- databaseNotificationsPolling(seconds: number | null): this {
654
- if (!this.config.databaseNotifications) return this
655
- this.config.databaseNotifications = {
656
- ...this.config.databaseNotifications,
657
- polling: seconds,
658
- }
659
- return this
660
- }
661
-
662
- /**
663
- * Sugar setter for the bell mount position. Has no effect unless
664
- * `.databaseNotifications()` was called.
665
- */
666
- databaseNotificationsPosition(position: 'topbar' | 'sidebar'): this {
667
- if (!this.config.databaseNotifications) return this
668
- this.config.databaseNotifications = {
669
- ...this.config.databaseNotifications,
670
- position,
671
- }
672
- return this
673
- }
674
-
675
- /**
676
- * Phase 2 — enable broadcast push for the bell. Requires
677
- * `@rudderjs/broadcast` to be installed and `BroadcastingProvider`
678
- * registered. Pass `{ wsUrl }` to override the WebSocket URL (default:
679
- * same-origin `/ws`). Has no effect unless `.databaseNotifications()`
680
- * was called.
681
- *
682
- * .databaseNotifications().databaseNotificationsBroadcast()
683
- * .databaseNotifications().databaseNotificationsBroadcast({ wsUrl: 'wss://…/ws' })
684
- */
685
- databaseNotificationsBroadcast(opts: boolean | { wsUrl?: string } = true): this {
686
- if (!this.config.databaseNotifications) return this
687
- this.config.databaseNotifications = {
688
- ...this.config.databaseNotifications,
689
- broadcast: opts,
690
- }
691
- return this
692
- }
693
-
694
- /**
695
- * Register named handlers for `Notification.actions([…])` slots.
696
- * Calling this twice merges — later registrations override earlier
697
- * keys.
698
- *
699
- * panel.notificationHandlers({
700
- * 'archive-project': async (ctx) => {
701
- * const { projectId } = ctx.payload as { projectId: number }
702
- * await Project.update(projectId, { archivedAt: new Date() })
703
- * return { notify: { title: 'Archived', type: 'success' } }
704
- * },
705
- * })
706
- *
707
- * Names must match `^[A-Za-z0-9_-]+$` (URL-safe — they ride in the
708
- * action endpoint path). Empty / whitespace / non-conforming keys
709
- * throw at registration time so a typo fails fast instead of 404ing
710
- * three days later when a user clicks the action on an old row.
711
- *
712
- * Closures registered here survive a `data` JSON round-trip (only
713
- * the name does — the closure stays in memory). For transient toasts
714
- * authored against the current page, `Action.handler(async ctx => …)`
715
- * still works inline — the registry is only required for persisted
716
- * actions.
717
- */
718
- notificationHandlers(map: Record<string, NotificationActionHandler>): this {
719
- const namePattern = /^[A-Za-z0-9_-]+$/
720
- for (const key of Object.keys(map)) {
721
- if (!namePattern.test(key)) {
722
- throw new Error(
723
- `[Pilotiq] notificationHandlers: handler name "${key}" contains characters ` +
724
- `outside [A-Za-z0-9_-]. Names ride in the action URL path; pick a URL-safe key.`,
725
- )
726
- }
727
- }
728
- this.config.notificationHandlers = {
729
- ...(this.config.notificationHandlers ?? {}),
730
- ...map,
731
- }
732
- return this
733
- }
734
-
735
- /** @internal — looked up by the notification-action route. */
736
- getNotificationHandler(name: string): NotificationActionHandler | undefined {
737
- return this.config.notificationHandlers?.[name]
738
- }
739
-
740
- /**
741
- * Register a render hook — a callback that returns `Element[]` to
742
- * mount at a named slot in the panel chrome or page renderers. Multiple
743
- * hooks against the same name run in registration order; their outputs
744
- * concatenate.
745
- *
746
- * import { Alert } from '@pilotiq/pilotiq'
747
- *
748
- * Pilotiq.make('admin')
749
- * .renderHook('panels::topbar.start', ({ user }) => [
750
- * Alert.make(`Hi, ${(user as any)?.name ?? 'there'}`).info(),
751
- * ])
752
- * .renderHook(
753
- * 'panels::resource.pages.list-records.table.before',
754
- * () => [Heading.make('Bulk import')],
755
- * { resource: ArticleResource },
756
- * )
757
- *
758
- * Scope (optional) restricts the hook to a single resource / page /
759
- * global. Scope keys are OR'd within the object — passing
760
- * `{ resource: A, page: P }` fires when EITHER `A` OR `P` is the
761
- * active route's identifier.
762
- *
763
- * Throwing hooks fail closed: their slot's contribution drops; other
764
- * hooks at the same slot still ship.
765
- *
766
- * Plan + guide: docs/plans/render-hooks.md, docs/guide/render-hooks.md.
767
- */
768
- renderHook(name: RenderHookName, fn: RenderHookFn, scope?: RenderHookScope): this {
769
- if (!this.config.renderHooks) this.config.renderHooks = []
770
- const entry: RenderHookEntry = { name, fn }
771
- if (scope !== undefined) entry.scope = scope
772
- this.config.renderHooks.push(entry)
773
- return this
774
- }
775
-
776
- /**
777
- * Resolve the current user for a request. Internal helper called by
778
- * routes + `panelInfo()`. Returns `null` when the resolver is unset or
779
- * throws. Errors are swallowed deliberately — a failing user resolver
780
- * should fail closed (no user) rather than 500 the page.
781
- */
782
- async resolveUser(req?: unknown): Promise<unknown | null> {
783
- if (!this.config.user) return null
784
- try {
785
- const u = await this.config.user(req)
786
- return u ?? null
787
- } catch {
788
- return null
789
- }
790
- }
791
-
792
- use(plugin: PilotiqPlugin): this {
793
- this.installedPlugins.push(plugin)
794
- plugin.register(this)
795
- return this
796
- }
797
-
798
- /**
799
- * Register multiple plugins in a single call. Each plugin's `register()`
800
- * runs in array order — equivalent to chaining `.use(p)` per item.
801
- *
802
- * @example
803
- * ```ts
804
- * import { tiptap } from '@pilotiq/tiptap'
805
- * import { codeEditor } from '@pilotiq/codemirror'
806
- * import { recharts } from '@pilotiq/recharts'
807
- * import { json } from '@codemirror/lang-json'
808
- *
809
- * Pilotiq.make('Admin')
810
- * .plugins([
811
- * tiptap(),
812
- * codeEditor({ languages: { json } }),
813
- * recharts(),
814
- * ])
815
- * ```
816
- */
817
- plugins(list: PilotiqPlugin[]): this {
818
- for (const plugin of list) this.use(plugin)
819
- return this
820
- }
821
-
822
- /**
823
- * Register a single right-sidebar contribution. Plugins surface chat
824
- * boxes, presence panels, document outlines, etc. through this — pilotiq
825
- * core ships the sidebar chrome (collapse / resize / tab strip / mobile
826
- * sheet); each contribution provides only its body component.
827
- *
828
- * @example
829
- * ```ts
830
- * Pilotiq.make('Admin').rightPanel({
831
- * id: 'ai.chat',
832
- * label: 'AI Assistant',
833
- * icon: 'sparkles',
834
- * render: AiChatBody,
835
- * defaultWidth: 360,
836
- * })
837
- * ```
838
- *
839
- * Adapter packages typically call this from inside their plugin's
840
- * `register(panel)` so consumers wire everything via `.plugins([…])`.
841
- *
842
- * @throws when `id` is missing/invalid, when `render` isn't a
843
- * component, when `defaultWidth` is out of [240, 800], or when
844
- * the same `id` was already registered.
845
- */
846
- rightPanel(contribution: RightPanelContribution): this {
847
- validateRightPanel(contribution)
848
- const existing = this.config.rightPanels ?? []
849
- const dup = findDuplicateRightPanelId(existing, contribution)
850
- if (dup) {
851
- throw new Error(
852
- `[Pilotiq] rightPanel: contribution id "${contribution.id}" is already ` +
853
- `registered. Each right-sidebar contribution must use a unique id.`,
854
- )
855
- }
856
- this.config.rightPanels = [...existing, contribution]
857
- return this
858
- }
859
-
860
- /**
861
- * Bulk variant of {@link rightPanel}. Registers each contribution in
862
- * array order; throws on the first invalid or duplicate id.
863
- *
864
- * @example
865
- * ```ts
866
- * Pilotiq.make('Admin').rightPanels([
867
- * { id: 'ai.chat', render: AiChatBody },
868
- * { id: 'outline', render: OutlineBody, defaultWidth: 280 },
869
- * ])
870
- * ```
871
- */
872
- rightPanels(list: RightPanelContribution[]): this {
873
- for (const c of list) this.rightPanel(c)
874
- return this
875
- }
876
-
877
- /** @internal — `panelInfo()` reads this to build `RightSidebarMeta`. */
878
- getRightPanels(): readonly RightPanelContribution[] {
879
- return this.config.rightPanels ?? []
880
- }
881
-
882
- /**
883
- * Register a component that wraps the panel's `<AppShell>` children at
884
- * the layout root. Plugins use this to mount React providers (AI chat
885
- * queue context, tenant theme switcher, feature-flag overlay, …) so
886
- * they're in scope for every page in the panel without consumers
887
- * having to manually wrap their `+Layout.tsx`.
888
- *
889
- * Adapter packages typically call this from inside their plugin's
890
- * `register(panel)` so consumers wire everything via `.plugins([…])`.
891
- *
892
- * Provider components receive `{ children, basePath? }` props.
893
- * Registration order is preservation order: the first registered
894
- * provider sits OUTERMOST (closest to the layout root); the last sits
895
- * INNERMOST (closest to the page tree). When two providers depend on
896
- * each other, register the producer first.
897
- *
898
- * @example
899
- * ```ts
900
- * // In a plugin's register(panel):
901
- * panel.layoutProvider(({ children, basePath }) =>
902
- * <AiUiProvider panelPath={basePath}>{children}</AiUiProvider>
903
- * )
904
- * ```
905
- *
906
- * @throws when `provider` isn't a function (component).
907
- */
908
- layoutProvider(provider: LayoutProviderComponent): this {
909
- if (typeof provider !== 'function') {
910
- throw new Error(
911
- `[Pilotiq] layoutProvider: expected a React component, got ${typeof provider}.`,
912
- )
913
- }
914
- const existing = this.config.layoutProviders ?? []
915
- this.config.layoutProviders = [...existing, provider]
916
- return this
917
- }
918
-
919
- /**
920
- * Bulk variant of {@link layoutProvider}. Registers each provider in
921
- * array order.
922
- */
923
- layoutProviders(list: LayoutProviderComponent[]): this {
924
- for (const c of list) this.layoutProvider(c)
925
- return this
926
- }
927
-
928
- /** @internal — read by the Vite plugin's `_components.ts` emitter. */
929
- getLayoutProviders(): readonly LayoutProviderComponent[] {
930
- return this.config.layoutProviders ?? []
931
- }
932
-
933
- /**
934
- * Override one of pilotiq's built-in chrome slots with a custom React
935
- * component. Calling twice merges — the latest registration wins per
936
- * slot; unset keys preserve the prior value (so a plugin can override
937
- * `nav` without clearing a host app's `footer`).
938
- *
939
- * Three slots ship today: `nav` (replaces the default nav tree),
940
- * `header` (replaces the whole `<header>` chrome bar in both
941
- * layouts — in `TopbarLayout` this subsumes the nav), and `footer`
942
- * (mounts a `<footer>` below the main content area in both layouts).
943
- * See {@link ComponentSlots} for the per-slot semantics and which
944
- * render hooks compose vs. stop firing under each replacement.
945
- *
946
- * Component refs are harvested by the Vite plugin into
947
- * `pages/(pilotiq)/_components.ts` — they never travel over the wire,
948
- * so authoring inside the panel module (which is import-safe on both
949
- * server and client) is the supported entry point.
950
- *
951
- * @example
952
- * ```ts
953
- * import { MyCustomSidebar } from './MyCustomSidebar.js'
954
- *
955
- * Pilotiq.make('Admin').components({ nav: MyCustomSidebar })
956
- * ```
957
- *
958
- * The supplied component receives `{ navigation, basePath, currentPath }`
959
- * — the same shape `panelInfo()` produces for the default renderers.
960
- * Import `NavComponentProps` and `isNavItemActive` from
961
- * `@pilotiq/pilotiq/react` to author a typed component that reuses
962
- * the framework's longest-prefix active-link semantics.
963
- */
964
- components(slots: ComponentSlots): this {
965
- this.config.components = { ...(this.config.components ?? {}), ...slots }
966
- return this
967
- }
968
-
969
- /** @internal — read by the Vite plugin's `_components.ts` emitter. */
970
- getComponentSlots(): Readonly<ComponentSlots> {
971
- return this.config.components ?? {}
972
- }
973
-
974
- /**
975
- * Set the panel-wide AI suggestion mode.
976
- *
977
- * - `'auto'` (default) — agent writes apply immediately.
978
- * - `'review'` — agent writes stage as `PendingSuggestion`s; the user
979
- * approves/rejects via inline diff (text) or value-comparison panel
980
- * (non-text). Reuses the Phase 8.5 applier registry on approve.
981
- *
982
- * Plan: `docs/plans/ai-review-mode.md`.
983
- */
984
- aiSuggestionsMode(mode: 'auto' | 'review'): this {
985
- this.config.aiSuggestionsMode = mode
986
- return this
987
- }
988
-
989
- /** @internal — read by `panelInfo()` and stamped onto the wire shape so
990
- * the AI client tool knows which branch (apply vs queue) to take. */
991
- getAiSuggestionsMode(): 'auto' | 'review' {
992
- return this.config.aiSuggestionsMode ?? 'auto'
993
- }
994
-
995
- /** @internal */
996
- enableThemeEditor(): void {
997
- this.config.themeEditor = true
998
- }
999
-
1000
- /** @internal — assign the storage adapter resolved by the
1001
- * `themeEditor({ storage })` plugin OR by the service provider's
1002
- * back-compat Prisma fallback. Both writers funnel through this
1003
- * setter so the route handlers consume a single slot. */
1004
- _setThemeStorage(adapter: ThemeStorageAdapter | undefined): void {
1005
- if (adapter === undefined) {
1006
- delete this.config.themeStorage
1007
- } else {
1008
- this.config.themeStorage = adapter
1009
- }
1010
- }
1011
-
1012
- /** @internal — the active theme storage adapter (explicit or the
1013
- * boot-time Prisma fallback). Routes read from here. */
1014
- getThemeStorage(): ThemeStorageAdapter | undefined {
1015
- return this.config.themeStorage
1016
- }
1017
-
1018
- /** @internal */
1019
- setThemeOverrides(overrides: Partial<ThemeConfig> | undefined): void {
1020
- if (overrides === undefined) {
1021
- delete this.config._themeOverrides
1022
- } else {
1023
- this.config._themeOverrides = overrides
1024
- }
1025
- }
1026
-
1027
- /** @internal — returns code defaults merged with DB overrides. Returns an
1028
- * empty config when the theme editor is on so the built-in default preset
1029
- * still resolves and the editor can persist overrides on top. */
1030
- getMergedTheme(): ThemeConfig | undefined {
1031
- const base = this.config.theme
1032
- const overrides = this.config._themeOverrides
1033
- if (!base && !overrides && !this.config.themeEditor) return undefined
1034
- return { ...base, ...overrides }
1035
- }
1036
-
1037
- /**
1038
- * Slug-indexed lookup for resources. O(1) replacement for
1039
- * `cfg.resources.find(r => r.getSlug() === slug)`. Built lazily on
1040
- * first call; invalidated when `.resources([…])` is reassigned.
1041
- */
1042
- findResource(slug: string): ResourceClass | undefined {
1043
- if (!this._resourceBySlug) {
1044
- this._resourceBySlug = new Map(this.config.resources.map(r => [r.getSlug(), r]))
1045
- }
1046
- return this._resourceBySlug.get(slug)
1047
- }
1048
-
1049
- /** Slug-indexed lookup for globals. See `findResource`. */
1050
- findGlobal(slug: string): GlobalClass | undefined {
1051
- if (!this._globalBySlug) {
1052
- this._globalBySlug = new Map(this.config.globals.map(g => [g.getSlug(), g]))
1053
- }
1054
- return this._globalBySlug.get(slug)
1055
- }
1056
-
1057
- /** Slug-indexed lookup for pages. See `findResource`. */
1058
- findPage(slug: string): typeof Page | undefined {
1059
- if (!this._pageBySlug) {
1060
- this._pageBySlug = new Map(this.config.pages.map(p => [p.getSlug(), p]))
1061
- }
1062
- return this._pageBySlug.get(slug)
1063
- }
1064
-
1065
- /**
1066
- * TTL (milliseconds) for the per-user navigation badge cache. Badges
1067
- * resolve once per `(owner, userIdentity)` pair and serve from the
1068
- * in-memory cache until the TTL elapses; the cache covers the
1069
- * common case where a panel with N resources each running
1070
- * `Model.count()` for a sidebar badge would otherwise issue N queries
1071
- * on every page nav.
1072
- *
1073
- * Pass `0` (or `null`) to disable caching entirely. Default 30000.
1074
- */
1075
- navigationBadgeTtl(ms: number | null): this {
1076
- if (ms === null) {
1077
- delete this.config.navigationBadgeTtlMs
1078
- } else {
1079
- this.config.navigationBadgeTtlMs = Math.max(0, ms)
1080
- }
1081
- // Bust on reconfigure so the new TTL doesn't reuse stale slots.
1082
- this._navigationBadgeCache.clear()
1083
- return this
1084
- }
1085
-
1086
- /** @internal — resolved TTL in milliseconds. Default 30s. `0`
1087
- * disables caching (each request re-resolves). */
1088
- getNavigationBadgeTtl(): number {
1089
- return this.config.navigationBadgeTtlMs ?? 30_000
1090
- }
1091
-
1092
- /** @internal — cache key for one (owner, user) pair. */
1093
- navigationBadgeCacheKey(ownerName: string, user: unknown): string {
1094
- return `${ownerName}|${userIdentityKey(user)}`
1095
- }
1096
-
1097
- /** @internal — read-through cache for a single owner's badge value.
1098
- * Caller supplies the resolver; cache wraps it with the configured
1099
- * TTL. When TTL is 0 the resolver is invoked unconditionally and
1100
- * nothing is stored. */
1101
- async resolveNavigationBadge(
1102
- ownerName: string,
1103
- user: unknown,
1104
- resolver: () => Promise<string | undefined>,
1105
- ): Promise<string | undefined> {
1106
- const ttl = this.getNavigationBadgeTtl()
1107
- if (ttl <= 0) return resolver()
1108
-
1109
- const key = this.navigationBadgeCacheKey(ownerName, user)
1110
- const now = Date.now()
1111
- const hit = this._navigationBadgeCache.get(key)
1112
- if (hit && hit.expires > now) return hit.value
1113
-
1114
- const value = await resolver()
1115
- this._navigationBadgeCache.set(key, { value, expires: now + ttl })
1116
- return value
1117
- }
1118
-
1119
- /** @internal — test seam; clears the per-user badge cache. */
1120
- _clearNavigationBadgeCache(): void {
1121
- this._navigationBadgeCache.clear()
1122
- }
1123
-
1124
- /** @internal */
1125
- getConfig(): Readonly<PilotiqConfig> {
1126
- return this.config
1127
- }
1128
-
1129
- /** @internal */
1130
- getPlugins(): readonly PilotiqPlugin[] {
1131
- return this.installedPlugins
1132
- }
1133
- }
1134
-
1135
- /**
1136
- * Stable cache key derived from a user object. Pilotiq treats the user
1137
- * as opaque, so we sniff the common shapes:
1138
- *
1139
- * 1. `null` / `undefined` — anonymous request; everyone shares one slot.
1140
- * 2. Primitive (string / number / bigint / boolean) — stringify directly.
1141
- * 3. Object with `id` — `String(user.id)` (the 99% case for app-supplied users).
1142
- * 4. Other objects — `JSON.stringify` as a last resort; falls back to a
1143
- * sentinel if stringify throws (cycles).
1144
- *
1145
- * Two distinct users with the same `id` collide, but that's the same
1146
- * collision the rest of the framework already trusts.
1147
- */
1148
- function userIdentityKey(user: unknown): string {
1149
- if (user === null || user === undefined) return ''
1150
- const t = typeof user
1151
- if (t === 'string' || t === 'number' || t === 'bigint' || t === 'boolean') return String(user)
1152
- if (t === 'object') {
1153
- const u = user as { id?: unknown }
1154
- if (u.id !== undefined && u.id !== null) return String(u.id)
1155
- try { return JSON.stringify(user) } catch { return '__opaque__' }
1156
- }
1157
- return '__opaque__'
1158
- }