@pilotiq/pilotiq 0.24.1 → 0.24.2

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