@pilotiq/pilotiq 0.23.1 → 0.24.2

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