@pilotiq/pilotiq 0.7.2 → 0.8.1

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 (371) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/CHANGELOG.md +208 -0
  3. package/CLAUDE.md +59 -3
  4. package/dist/Pilotiq.d.ts +83 -0
  5. package/dist/Pilotiq.d.ts.map +1 -1
  6. package/dist/Pilotiq.js +39 -0
  7. package/dist/Pilotiq.js.map +1 -1
  8. package/dist/actions/Action.d.ts +27 -99
  9. package/dist/actions/Action.d.ts.map +1 -1
  10. package/dist/actions/Action.js +52 -754
  11. package/dist/actions/Action.js.map +1 -1
  12. package/dist/actions/bulkFactories.d.ts +46 -0
  13. package/dist/actions/bulkFactories.d.ts.map +1 -0
  14. package/dist/actions/bulkFactories.js +144 -0
  15. package/dist/actions/bulkFactories.js.map +1 -0
  16. package/dist/actions/crudFactories.d.ts +94 -0
  17. package/dist/actions/crudFactories.d.ts.map +1 -0
  18. package/dist/actions/crudFactories.js +209 -0
  19. package/dist/actions/crudFactories.js.map +1 -0
  20. package/dist/actions/factoryHelpers.d.ts +108 -0
  21. package/dist/actions/factoryHelpers.d.ts.map +1 -0
  22. package/dist/actions/factoryHelpers.js +138 -0
  23. package/dist/actions/factoryHelpers.js.map +1 -0
  24. package/dist/actions/m2mFactories.d.ts +47 -0
  25. package/dist/actions/m2mFactories.d.ts.map +1 -0
  26. package/dist/actions/m2mFactories.js +173 -0
  27. package/dist/actions/m2mFactories.js.map +1 -0
  28. package/dist/actions/relationFactories.d.ts +93 -0
  29. package/dist/actions/relationFactories.d.ts.map +1 -0
  30. package/dist/actions/relationFactories.js +321 -0
  31. package/dist/actions/relationFactories.js.map +1 -0
  32. package/dist/elements/dispatchForm.js +1 -1
  33. package/dist/elements/dispatchForm.js.map +1 -1
  34. package/dist/elements/dispatchTable.js +1 -1
  35. package/dist/elements/dispatchTable.js.map +1 -1
  36. package/dist/fields/Field.d.ts +31 -0
  37. package/dist/fields/Field.d.ts.map +1 -1
  38. package/dist/fields/Field.js +25 -0
  39. package/dist/fields/Field.js.map +1 -1
  40. package/dist/pageData/breadcrumbs.d.ts +42 -0
  41. package/dist/pageData/breadcrumbs.d.ts.map +1 -0
  42. package/dist/pageData/breadcrumbs.js +172 -0
  43. package/dist/pageData/breadcrumbs.js.map +1 -0
  44. package/dist/pageData/forms.d.ts +137 -0
  45. package/dist/pageData/forms.d.ts.map +1 -0
  46. package/dist/pageData/forms.js +427 -0
  47. package/dist/pageData/forms.js.map +1 -0
  48. package/dist/pageData/helpers.d.ts +239 -0
  49. package/dist/pageData/helpers.d.ts.map +1 -0
  50. package/dist/pageData/helpers.js +703 -0
  51. package/dist/pageData/helpers.js.map +1 -0
  52. package/dist/pageData/misc.d.ts +76 -0
  53. package/dist/pageData/misc.d.ts.map +1 -0
  54. package/dist/pageData/misc.js +263 -0
  55. package/dist/pageData/misc.js.map +1 -0
  56. package/dist/pageData/navigation.d.ts +292 -0
  57. package/dist/pageData/navigation.d.ts.map +1 -0
  58. package/dist/pageData/navigation.js +591 -0
  59. package/dist/pageData/navigation.js.map +1 -0
  60. package/dist/pageData/relationPages.d.ts +172 -0
  61. package/dist/pageData/relationPages.d.ts.map +1 -0
  62. package/dist/pageData/relationPages.js +867 -0
  63. package/dist/pageData/relationPages.js.map +1 -0
  64. package/dist/pageData/relationTabs.d.ts +65 -0
  65. package/dist/pageData/relationTabs.d.ts.map +1 -0
  66. package/dist/pageData/relationTabs.js +258 -0
  67. package/dist/pageData/relationTabs.js.map +1 -0
  68. package/dist/pageData/resourcePages.d.ts +48 -0
  69. package/dist/pageData/resourcePages.d.ts.map +1 -0
  70. package/dist/pageData/resourcePages.js +504 -0
  71. package/dist/pageData/resourcePages.js.map +1 -0
  72. package/dist/pageData.d.ts +12 -792
  73. package/dist/pageData.d.ts.map +1 -1
  74. package/dist/pageData.js +24 -3797
  75. package/dist/pageData.js.map +1 -1
  76. package/dist/react/AppShell.d.ts +8 -0
  77. package/dist/react/AppShell.d.ts.map +1 -1
  78. package/dist/react/AppShell.js +11 -1
  79. package/dist/react/AppShell.js.map +1 -1
  80. package/dist/react/CollabExtensionFactoryRegistry.d.ts +47 -0
  81. package/dist/react/CollabExtensionFactoryRegistry.d.ts.map +1 -0
  82. package/dist/react/CollabExtensionFactoryRegistry.js +14 -0
  83. package/dist/react/CollabExtensionFactoryRegistry.js.map +1 -0
  84. package/dist/react/CollabRoomContext.d.ts +37 -0
  85. package/dist/react/CollabRoomContext.d.ts.map +1 -0
  86. package/dist/react/CollabRoomContext.js +12 -0
  87. package/dist/react/CollabRoomContext.js.map +1 -0
  88. package/dist/react/FormCollabBindingRegistry.d.ts +62 -0
  89. package/dist/react/FormCollabBindingRegistry.d.ts.map +1 -0
  90. package/dist/react/FormCollabBindingRegistry.js +14 -0
  91. package/dist/react/FormCollabBindingRegistry.js.map +1 -0
  92. package/dist/react/FormStateContext.d.ts.map +1 -1
  93. package/dist/react/FormStateContext.js +87 -0
  94. package/dist/react/FormStateContext.js.map +1 -1
  95. package/dist/react/RecordWrapperGate.d.ts +25 -0
  96. package/dist/react/RecordWrapperGate.d.ts.map +1 -0
  97. package/dist/react/RecordWrapperGate.js +30 -0
  98. package/dist/react/RecordWrapperGate.js.map +1 -0
  99. package/dist/react/RecordWrapperRegistry.d.ts +31 -0
  100. package/dist/react/RecordWrapperRegistry.d.ts.map +1 -0
  101. package/dist/react/RecordWrapperRegistry.js +15 -0
  102. package/dist/react/RecordWrapperRegistry.js.map +1 -0
  103. package/dist/react/SchemaRenderer.d.ts +17 -23
  104. package/dist/react/SchemaRenderer.d.ts.map +1 -1
  105. package/dist/react/SchemaRenderer.js +71 -3647
  106. package/dist/react/SchemaRenderer.js.map +1 -1
  107. package/dist/react/component-slots.d.ts +103 -0
  108. package/dist/react/component-slots.d.ts.map +1 -0
  109. package/dist/react/component-slots.js +18 -0
  110. package/dist/react/component-slots.js.map +1 -0
  111. package/dist/react/fields/BuilderInput.d.ts.map +1 -1
  112. package/dist/react/fields/BuilderInput.js +21 -117
  113. package/dist/react/fields/BuilderInput.js.map +1 -1
  114. package/dist/react/fields/MarkdownInput.d.ts.map +1 -1
  115. package/dist/react/fields/MarkdownInput.js +1 -3
  116. package/dist/react/fields/MarkdownInput.js.map +1 -1
  117. package/dist/react/fields/RepeaterInput.d.ts.map +1 -1
  118. package/dist/react/fields/RepeaterInput.js +22 -127
  119. package/dist/react/fields/RepeaterInput.js.map +1 -1
  120. package/dist/react/fields/rowState.d.ts +40 -0
  121. package/dist/react/fields/rowState.d.ts.map +1 -0
  122. package/dist/react/fields/rowState.js +60 -0
  123. package/dist/react/fields/rowState.js.map +1 -0
  124. package/dist/react/fields/useRowReorderDnd.d.ts +28 -0
  125. package/dist/react/fields/useRowReorderDnd.d.ts.map +1 -0
  126. package/dist/react/fields/useRowReorderDnd.js +51 -0
  127. package/dist/react/fields/useRowReorderDnd.js.map +1 -0
  128. package/dist/react/index.d.ts +9 -0
  129. package/dist/react/index.d.ts.map +1 -1
  130. package/dist/react/index.js +8 -0
  131. package/dist/react/index.js.map +1 -1
  132. package/dist/react/layouts/SidebarLayout.d.ts +1 -1
  133. package/dist/react/layouts/SidebarLayout.d.ts.map +1 -1
  134. package/dist/react/layouts/SidebarLayout.js +10 -2
  135. package/dist/react/layouts/SidebarLayout.js.map +1 -1
  136. package/dist/react/layouts/TopbarLayout.d.ts +1 -1
  137. package/dist/react/layouts/TopbarLayout.d.ts.map +1 -1
  138. package/dist/react/layouts/TopbarLayout.js +19 -11
  139. package/dist/react/layouts/TopbarLayout.js.map +1 -1
  140. package/dist/react/parseRecordEditUrl.d.ts +29 -0
  141. package/dist/react/parseRecordEditUrl.d.ts.map +1 -0
  142. package/dist/react/parseRecordEditUrl.js +25 -0
  143. package/dist/react/parseRecordEditUrl.js.map +1 -0
  144. package/dist/react/persistedState.d.ts +19 -0
  145. package/dist/react/persistedState.d.ts.map +1 -0
  146. package/dist/react/persistedState.js +51 -0
  147. package/dist/react/persistedState.js.map +1 -0
  148. package/dist/react/schemaRenderer/AlertRenderer.d.ts +12 -0
  149. package/dist/react/schemaRenderer/AlertRenderer.d.ts.map +1 -0
  150. package/dist/react/schemaRenderer/AlertRenderer.js +61 -0
  151. package/dist/react/schemaRenderer/AlertRenderer.js.map +1 -0
  152. package/dist/react/schemaRenderer/EntryRenderer.d.ts +13 -0
  153. package/dist/react/schemaRenderer/EntryRenderer.d.ts.map +1 -0
  154. package/dist/react/schemaRenderer/EntryRenderer.js +277 -0
  155. package/dist/react/schemaRenderer/EntryRenderer.js.map +1 -0
  156. package/dist/react/schemaRenderer/SectionRenderer.d.ts +16 -0
  157. package/dist/react/schemaRenderer/SectionRenderer.d.ts.map +1 -0
  158. package/dist/react/schemaRenderer/SectionRenderer.js +62 -0
  159. package/dist/react/schemaRenderer/SectionRenderer.js.map +1 -0
  160. package/dist/react/schemaRenderer/SimpleElements.d.ts +25 -0
  161. package/dist/react/schemaRenderer/SimpleElements.d.ts.map +1 -0
  162. package/dist/react/schemaRenderer/SimpleElements.js +147 -0
  163. package/dist/react/schemaRenderer/SimpleElements.js.map +1 -0
  164. package/dist/react/schemaRenderer/TabsRenderer.d.ts +17 -0
  165. package/dist/react/schemaRenderer/TabsRenderer.d.ts.map +1 -0
  166. package/dist/react/schemaRenderer/TabsRenderer.js +31 -0
  167. package/dist/react/schemaRenderer/TabsRenderer.js.map +1 -0
  168. package/dist/react/schemaRenderer/WizardRenderer.d.ts +34 -0
  169. package/dist/react/schemaRenderer/WizardRenderer.d.ts.map +1 -0
  170. package/dist/react/schemaRenderer/WizardRenderer.js +208 -0
  171. package/dist/react/schemaRenderer/WizardRenderer.js.map +1 -0
  172. package/dist/react/schemaRenderer/action/ActionGroupTrigger.d.ts +21 -0
  173. package/dist/react/schemaRenderer/action/ActionGroupTrigger.d.ts.map +1 -0
  174. package/dist/react/schemaRenderer/action/ActionGroupTrigger.js +82 -0
  175. package/dist/react/schemaRenderer/action/ActionGroupTrigger.js.map +1 -0
  176. package/dist/react/schemaRenderer/action/ActionModalDialog.d.ts +30 -0
  177. package/dist/react/schemaRenderer/action/ActionModalDialog.d.ts.map +1 -0
  178. package/dist/react/schemaRenderer/action/ActionModalDialog.js +182 -0
  179. package/dist/react/schemaRenderer/action/ActionModalDialog.js.map +1 -0
  180. package/dist/react/schemaRenderer/action/ConfirmActionDialog.d.ts +17 -0
  181. package/dist/react/schemaRenderer/action/ConfirmActionDialog.d.ts.map +1 -0
  182. package/dist/react/schemaRenderer/action/ConfirmActionDialog.js +19 -0
  183. package/dist/react/schemaRenderer/action/ConfirmActionDialog.js.map +1 -0
  184. package/dist/react/schemaRenderer/action/HandlerActionButton.d.ts +16 -0
  185. package/dist/react/schemaRenderer/action/HandlerActionButton.d.ts.map +1 -0
  186. package/dist/react/schemaRenderer/action/HandlerActionButton.js +16 -0
  187. package/dist/react/schemaRenderer/action/HandlerActionButton.js.map +1 -0
  188. package/dist/react/schemaRenderer/action/MethodActionButton.d.ts +22 -0
  189. package/dist/react/schemaRenderer/action/MethodActionButton.d.ts.map +1 -0
  190. package/dist/react/schemaRenderer/action/MethodActionButton.js +26 -0
  191. package/dist/react/schemaRenderer/action/MethodActionButton.js.map +1 -0
  192. package/dist/react/schemaRenderer/action/buttons.d.ts +18 -0
  193. package/dist/react/schemaRenderer/action/buttons.d.ts.map +1 -0
  194. package/dist/react/schemaRenderer/action/buttons.js +74 -0
  195. package/dist/react/schemaRenderer/action/buttons.js.map +1 -0
  196. package/dist/react/schemaRenderer/action/helpers.d.ts +26 -0
  197. package/dist/react/schemaRenderer/action/helpers.d.ts.map +1 -0
  198. package/dist/react/schemaRenderer/action/helpers.js +126 -0
  199. package/dist/react/schemaRenderer/action/helpers.js.map +1 -0
  200. package/dist/react/schemaRenderer/action/renderAction.d.ts +21 -0
  201. package/dist/react/schemaRenderer/action/renderAction.d.ts.map +1 -0
  202. package/dist/react/schemaRenderer/action/renderAction.js +102 -0
  203. package/dist/react/schemaRenderer/action/renderAction.js.map +1 -0
  204. package/dist/react/schemaRenderer/columnFormat.d.ts +10 -0
  205. package/dist/react/schemaRenderer/columnFormat.d.ts.map +1 -0
  206. package/dist/react/schemaRenderer/columnFormat.js +76 -0
  207. package/dist/react/schemaRenderer/columnFormat.js.map +1 -0
  208. package/dist/react/schemaRenderer/constants.d.ts +8 -0
  209. package/dist/react/schemaRenderer/constants.d.ts.map +1 -0
  210. package/dist/react/schemaRenderer/constants.js +45 -0
  211. package/dist/react/schemaRenderer/constants.js.map +1 -0
  212. package/dist/react/schemaRenderer/form/FormRenderer.d.ts +29 -0
  213. package/dist/react/schemaRenderer/form/FormRenderer.d.ts.map +1 -0
  214. package/dist/react/schemaRenderer/form/FormRenderer.js +163 -0
  215. package/dist/react/schemaRenderer/form/FormRenderer.js.map +1 -0
  216. package/dist/react/schemaRenderer/form/renderField.d.ts +6 -0
  217. package/dist/react/schemaRenderer/form/renderField.d.ts.map +1 -0
  218. package/dist/react/schemaRenderer/form/renderField.js +239 -0
  219. package/dist/react/schemaRenderer/form/renderField.js.map +1 -0
  220. package/dist/react/schemaRenderer/helpers.d.ts +32 -0
  221. package/dist/react/schemaRenderer/helpers.d.ts.map +1 -0
  222. package/dist/react/schemaRenderer/helpers.js +52 -0
  223. package/dist/react/schemaRenderer/helpers.js.map +1 -0
  224. package/dist/react/schemaRenderer/table/CardsLayoutBody.d.ts +60 -0
  225. package/dist/react/schemaRenderer/table/CardsLayoutBody.d.ts.map +1 -0
  226. package/dist/react/schemaRenderer/table/CardsLayoutBody.js +189 -0
  227. package/dist/react/schemaRenderer/table/CardsLayoutBody.js.map +1 -0
  228. package/dist/react/schemaRenderer/table/TableRenderer.d.ts +29 -0
  229. package/dist/react/schemaRenderer/table/TableRenderer.d.ts.map +1 -0
  230. package/dist/react/schemaRenderer/table/TableRenderer.js +85 -0
  231. package/dist/react/schemaRenderer/table/TableRenderer.js.map +1 -0
  232. package/dist/react/schemaRenderer/table/TableRendererBody.d.ts +18 -0
  233. package/dist/react/schemaRenderer/table/TableRendererBody.d.ts.map +1 -0
  234. package/dist/react/schemaRenderer/table/TableRendererBody.js +555 -0
  235. package/dist/react/schemaRenderer/table/TableRendererBody.js.map +1 -0
  236. package/dist/react/schemaRenderer/table/filters.d.ts +263 -0
  237. package/dist/react/schemaRenderer/table/filters.d.ts.map +1 -0
  238. package/dist/react/schemaRenderer/table/filters.js +497 -0
  239. package/dist/react/schemaRenderer/table/filters.js.map +1 -0
  240. package/dist/react/schemaRenderer/table/formatCell.d.ts +11 -0
  241. package/dist/react/schemaRenderer/table/formatCell.d.ts.map +1 -0
  242. package/dist/react/schemaRenderer/table/formatCell.js +172 -0
  243. package/dist/react/schemaRenderer/table/formatCell.js.map +1 -0
  244. package/dist/react/schemaRenderer/table/links.d.ts +42 -0
  245. package/dist/react/schemaRenderer/table/links.d.ts.map +1 -0
  246. package/dist/react/schemaRenderer/table/links.js +55 -0
  247. package/dist/react/schemaRenderer/table/links.js.map +1 -0
  248. package/dist/react/schemaRenderer/table/renderRowActions.d.ts +13 -0
  249. package/dist/react/schemaRenderer/table/renderRowActions.d.ts.map +1 -0
  250. package/dist/react/schemaRenderer/table/renderRowActions.js +25 -0
  251. package/dist/react/schemaRenderer/table/renderRowActions.js.map +1 -0
  252. package/dist/react/schemaRenderer/table/url.d.ts +41 -0
  253. package/dist/react/schemaRenderer/table/url.d.ts.map +1 -0
  254. package/dist/react/schemaRenderer/table/url.js +114 -0
  255. package/dist/react/schemaRenderer/table/url.js.map +1 -0
  256. package/dist/routes/globals.d.ts +13 -0
  257. package/dist/routes/globals.d.ts.map +1 -0
  258. package/dist/routes/globals.js +131 -0
  259. package/dist/routes/globals.js.map +1 -0
  260. package/dist/routes/helpers.d.ts +217 -0
  261. package/dist/routes/helpers.d.ts.map +1 -0
  262. package/dist/routes/helpers.js +498 -0
  263. package/dist/routes/helpers.js.map +1 -0
  264. package/dist/routes/pages.d.ts +15 -0
  265. package/dist/routes/pages.d.ts.map +1 -0
  266. package/dist/routes/pages.js +145 -0
  267. package/dist/routes/pages.js.map +1 -0
  268. package/dist/routes/panel.d.ts +19 -0
  269. package/dist/routes/panel.d.ts.map +1 -0
  270. package/dist/routes/panel.js +191 -0
  271. package/dist/routes/panel.js.map +1 -0
  272. package/dist/routes/relations.d.ts +21 -0
  273. package/dist/routes/relations.d.ts.map +1 -0
  274. package/dist/routes/relations.js +1239 -0
  275. package/dist/routes/relations.js.map +1 -0
  276. package/dist/routes/resources.d.ts +28 -0
  277. package/dist/routes/resources.d.ts.map +1 -0
  278. package/dist/routes/resources.js +741 -0
  279. package/dist/routes/resources.js.map +1 -0
  280. package/dist/routes/theme.d.ts +12 -0
  281. package/dist/routes/theme.d.ts.map +1 -0
  282. package/dist/routes/theme.js +82 -0
  283. package/dist/routes/theme.js.map +1 -0
  284. package/dist/routes.d.ts.map +1 -1
  285. package/dist/routes.js +64 -3078
  286. package/dist/routes.js.map +1 -1
  287. package/dist/vite.d.ts +1 -0
  288. package/dist/vite.d.ts.map +1 -1
  289. package/dist/vite.js +26 -5
  290. package/dist/vite.js.map +1 -1
  291. package/package.json +2 -1
  292. package/src/Pilotiq.ts +95 -0
  293. package/src/actions/Action.ts +79 -723
  294. package/src/actions/bulkFactories.ts +168 -0
  295. package/src/actions/crudFactories.ts +220 -0
  296. package/src/actions/factoryHelpers.ts +177 -0
  297. package/src/actions/m2mFactories.ts +193 -0
  298. package/src/actions/relationFactories.ts +372 -0
  299. package/src/elements/dispatchForm.ts +1 -1
  300. package/src/elements/dispatchTable.ts +1 -1
  301. package/src/fields/Field.ts +39 -0
  302. package/src/pageData/breadcrumbs.ts +288 -0
  303. package/src/pageData/forms.ts +578 -0
  304. package/src/pageData/helpers.ts +764 -0
  305. package/src/pageData/misc.ts +347 -0
  306. package/src/pageData/navigation.ts +779 -0
  307. package/src/pageData/relationPages.ts +1246 -0
  308. package/src/pageData/relationTabs.ts +286 -0
  309. package/src/pageData/resourcePages.ts +593 -0
  310. package/src/pageData.ts +122 -4731
  311. package/src/react/AppShell.tsx +27 -1
  312. package/src/react/CollabExtensionFactoryRegistry.ts +55 -0
  313. package/src/react/CollabRoomContext.ts +42 -0
  314. package/src/react/FormCollabBindingRegistry.ts +72 -0
  315. package/src/react/FormStateContext.tsx +91 -0
  316. package/src/react/RecordWrapperGate.tsx +40 -0
  317. package/src/react/RecordWrapperRegistry.ts +39 -0
  318. package/src/react/SchemaRenderer.tsx +230 -6479
  319. package/src/react/component-slots.test.ts +103 -0
  320. package/src/react/component-slots.ts +116 -0
  321. package/src/react/fields/BuilderInput.tsx +29 -117
  322. package/src/react/fields/MarkdownInput.tsx +0 -1
  323. package/src/react/fields/RepeaterInput.tsx +29 -130
  324. package/src/react/fields/rowState.ts +106 -0
  325. package/src/react/fields/useRowReorderDnd.ts +78 -0
  326. package/src/react/index.ts +38 -0
  327. package/src/react/layouts/SidebarLayout.tsx +39 -28
  328. package/src/react/layouts/TopbarLayout.tsx +70 -57
  329. package/src/react/parseRecordEditUrl.test.ts +75 -0
  330. package/src/react/parseRecordEditUrl.ts +55 -0
  331. package/src/react/persistedState.ts +40 -0
  332. package/src/react/schemaRenderer/AlertRenderer.tsx +112 -0
  333. package/src/react/schemaRenderer/EntryRenderer.tsx +501 -0
  334. package/src/react/schemaRenderer/SectionRenderer.tsx +120 -0
  335. package/src/react/schemaRenderer/SimpleElements.tsx +306 -0
  336. package/src/react/schemaRenderer/TabsRenderer.tsx +62 -0
  337. package/src/react/schemaRenderer/WizardRenderer.tsx +338 -0
  338. package/src/react/schemaRenderer/action/ActionGroupTrigger.tsx +177 -0
  339. package/src/react/schemaRenderer/action/ActionModalDialog.tsx +273 -0
  340. package/src/react/schemaRenderer/action/ConfirmActionDialog.tsx +61 -0
  341. package/src/react/schemaRenderer/action/HandlerActionButton.tsx +43 -0
  342. package/src/react/schemaRenderer/action/MethodActionButton.tsx +64 -0
  343. package/src/react/schemaRenderer/action/buttons.tsx +99 -0
  344. package/src/react/schemaRenderer/action/helpers.ts +140 -0
  345. package/src/react/schemaRenderer/action/renderAction.tsx +245 -0
  346. package/src/react/schemaRenderer/columnFormat.ts +65 -0
  347. package/src/react/schemaRenderer/constants.ts +50 -0
  348. package/src/react/schemaRenderer/form/FormRenderer.tsx +245 -0
  349. package/src/react/schemaRenderer/form/renderField.tsx +511 -0
  350. package/src/react/schemaRenderer/helpers.tsx +81 -0
  351. package/src/react/schemaRenderer/table/CardsLayoutBody.tsx +308 -0
  352. package/src/react/schemaRenderer/table/TableRenderer.tsx +123 -0
  353. package/src/react/schemaRenderer/table/TableRendererBody.tsx +974 -0
  354. package/src/react/schemaRenderer/table/filters.tsx +1233 -0
  355. package/src/react/schemaRenderer/table/formatCell.tsx +264 -0
  356. package/src/react/schemaRenderer/table/links.tsx +112 -0
  357. package/src/react/schemaRenderer/table/renderRowActions.tsx +52 -0
  358. package/src/react/schemaRenderer/table/url.tsx +143 -0
  359. package/src/routes/globals.ts +154 -0
  360. package/src/routes/helpers.ts +668 -0
  361. package/src/routes/pages.ts +173 -0
  362. package/src/routes/panel.ts +204 -0
  363. package/src/routes/relations.ts +1219 -0
  364. package/src/routes/resources.ts +786 -0
  365. package/src/routes/theme.ts +109 -0
  366. package/src/routes.test.ts +1 -1
  367. package/src/routes.ts +64 -3176
  368. package/src/schema/TableWidget.test.ts +2 -2
  369. package/src/theme/migrate.test.ts +178 -0
  370. package/src/vite.test.ts +184 -0
  371. package/src/vite.ts +26 -4
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Shared row-state helpers consumed by both `RepeaterInput` and
3
+ * `BuilderInput`. The two fields keep parallel storage namespaces
4
+ * (`pilotiq.repeater.…` vs `pilotiq.builder.…`) so users with both on
5
+ * the same page can collapse them independently — the namespace is the
6
+ * only thing that varies between the two callers.
7
+ */
8
+
9
+ import {
10
+ readStoredString, removeStoredString, writeStoredString,
11
+ } from '../persistedState.js'
12
+
13
+ let _rowSeqFallback = 0
14
+
15
+ export function generateRowId(): string {
16
+ type CryptoLike = { randomUUID?: () => string }
17
+ const c = (globalThis as { crypto?: CryptoLike }).crypto
18
+ if (c?.randomUUID) return c.randomUUID()
19
+ return `row-${Date.now()}-${++_rowSeqFallback}`
20
+ }
21
+
22
+ export type RowStateNamespace = 'repeater' | 'builder'
23
+
24
+ export interface CollapsedStorage {
25
+ key: (formId: string, name: string, rowId: string) => string
26
+ read: (formId: string, name: string, rowId: string, defaultValue: boolean) => boolean
27
+ write: (formId: string, name: string, rowId: string, value: boolean) => void
28
+ remove: (formId: string, name: string, rowId: string) => void
29
+ seed: (
30
+ rows: { id: string }[],
31
+ formId: string,
32
+ name: string,
33
+ defaultValue: boolean,
34
+ collapsible: boolean,
35
+ ) => Record<string, boolean>
36
+ }
37
+
38
+ /**
39
+ * Build a namespaced per-row collapse-state store. Uses `'true'` /
40
+ * `'false'` encoding (predates the `'1'` / `'0'` flag helper — kept for
41
+ * back-compat with already-persisted state).
42
+ */
43
+ export function makeCollapsedStorage(namespace: RowStateNamespace): CollapsedStorage {
44
+ const key = (formId: string, name: string, rowId: string): string =>
45
+ `pilotiq.${namespace}.${formId}.${name}.${rowId}`
46
+
47
+ const read = (formId: string, name: string, rowId: string, defaultValue: boolean): boolean => {
48
+ const raw = readStoredString(key(formId, name, rowId))
49
+ if (raw === null) return defaultValue
50
+ return raw === 'true'
51
+ }
52
+
53
+ const write = (formId: string, name: string, rowId: string, value: boolean): void => {
54
+ writeStoredString(key(formId, name, rowId), String(value))
55
+ }
56
+
57
+ const remove = (formId: string, name: string, rowId: string): void => {
58
+ removeStoredString(key(formId, name, rowId))
59
+ }
60
+
61
+ const seed = (
62
+ rows: { id: string }[],
63
+ formId: string,
64
+ name: string,
65
+ defaultValue: boolean,
66
+ collapsible: boolean,
67
+ ): Record<string, boolean> => {
68
+ if (!collapsible) return {}
69
+ const out: Record<string, boolean> = {}
70
+ for (const row of rows) out[row.id] = read(formId, name, row.id, defaultValue)
71
+ return out
72
+ }
73
+
74
+ return { key, read, write, remove, seed }
75
+ }
76
+
77
+ export interface AccordionStorage {
78
+ key: (formId: string, name: string) => string
79
+ /**
80
+ * `undefined` = no value stored (caller falls back to default-open
81
+ * heuristic). Empty string = user explicitly closed every row (caller
82
+ * maps to `null` openId). Any other string = the open row id.
83
+ */
84
+ read: (formId: string, name: string) => string | undefined
85
+ write: (formId: string, name: string, openId: string | null) => void
86
+ }
87
+
88
+ /**
89
+ * Build a namespaced accordion-open-row store. Always one slot per
90
+ * (formId, name) pair regardless of row count.
91
+ */
92
+ export function makeAccordionStorage(namespace: RowStateNamespace): AccordionStorage {
93
+ const key = (formId: string, name: string): string =>
94
+ `pilotiq.${namespace}.${formId}.${name}.accordion`
95
+
96
+ const read = (formId: string, name: string): string | undefined => {
97
+ const raw = readStoredString(key(formId, name))
98
+ return raw === null ? undefined : raw
99
+ }
100
+
101
+ const write = (formId: string, name: string, openId: string | null): void => {
102
+ writeStoredString(key(formId, name), openId ?? '')
103
+ }
104
+
105
+ return { key, read, write }
106
+ }
@@ -0,0 +1,78 @@
1
+ import React, { useState } from 'react'
2
+
3
+ /**
4
+ * Shared HTML5 drag-and-drop wiring for any list of rows that the user
5
+ * can reorder. Consumed by `TableRendererBody` (POSTs the new order
6
+ * to the server with rollback), `RepeaterInput`, and `BuilderInput`
7
+ * (both mutate local row state in place).
8
+ *
9
+ * Generic on `HTMLElement` so the same handlers wire onto a `<div>`
10
+ * row (card / grid layouts) AND a `<tr>` row (table layout) — the
11
+ * consumer cast happens at the JSX boundary.
12
+ */
13
+
14
+ export interface RowReorderDnd {
15
+ dragId: string | null
16
+ /** The boundary slot the cursor is over (0..rows.length); null when no drag is active. */
17
+ dropAt: number | null
18
+ onDragStart: (id: string) => (e: React.DragEvent<HTMLElement>) => void
19
+ onDragOver: (idx: number) => (e: React.DragEvent<HTMLElement>) => void
20
+ onDrop: (e: React.DragEvent<HTMLElement>) => void
21
+ onDragEnd: () => void
22
+ }
23
+
24
+ export interface UseRowReorderDndOptions {
25
+ /** When false, every handler short-circuits without firing `onDrop`. */
26
+ enabled: boolean
27
+ /** Fires once per successful drop, after the hook has cleared its drag state. */
28
+ onDrop: (fromId: string, dropAt: number) => void | Promise<void>
29
+ }
30
+
31
+ export function useRowReorderDnd({
32
+ enabled,
33
+ onDrop,
34
+ }: UseRowReorderDndOptions): RowReorderDnd {
35
+ const [dragId, setDragId] = useState<string | null>(null)
36
+ const [dropAt, setDropAt] = useState<number | null>(null)
37
+
38
+ const handleDragStart = (id: string) => (e: React.DragEvent<HTMLElement>): void => {
39
+ if (!enabled) return
40
+ setDragId(id)
41
+ // dataTransfer needs *something* to register the drag in Firefox.
42
+ e.dataTransfer.effectAllowed = 'move'
43
+ try { e.dataTransfer.setData('text/plain', id) } catch { /* IE quirk; ignore */ }
44
+ }
45
+
46
+ const handleDragOver = (idx: number) => (e: React.DragEvent<HTMLElement>): void => {
47
+ if (!enabled || dragId === null) return
48
+ e.preventDefault()
49
+ e.dataTransfer.dropEffect = 'move'
50
+ // Drop above this row when cursor is in its top half, below when in its bottom half.
51
+ const rect = e.currentTarget.getBoundingClientRect()
52
+ const aboveHalf = e.clientY < rect.top + rect.height / 2
53
+ setDropAt(aboveHalf ? idx : idx + 1)
54
+ }
55
+
56
+ const handleDrop = (e: React.DragEvent<HTMLElement>): void => {
57
+ if (!enabled || dragId === null || dropAt === null) {
58
+ setDragId(null); setDropAt(null); return
59
+ }
60
+ e.preventDefault()
61
+ const fromId = dragId
62
+ const at = dropAt
63
+ setDragId(null); setDropAt(null)
64
+ void onDrop(fromId, at)
65
+ }
66
+
67
+ const handleDragEnd = (): void => {
68
+ setDragId(null); setDropAt(null)
69
+ }
70
+
71
+ return {
72
+ dragId, dropAt,
73
+ onDragStart: handleDragStart,
74
+ onDragOver: handleDragOver,
75
+ onDrop: handleDrop,
76
+ onDragEnd: handleDragEnd,
77
+ }
78
+ }
@@ -32,6 +32,34 @@ export {
32
32
  getPendingSuggestionApplier,
33
33
  type PendingSuggestionApplier,
34
34
  } from './PendingSuggestionApplierRegistry.js'
35
+ export {
36
+ CollabRoomContext,
37
+ useCollabRoom,
38
+ type CollabRoom,
39
+ } from './CollabRoomContext.js'
40
+ export {
41
+ registerCollabExtensions,
42
+ getCollabExtensions,
43
+ type CollabExtensionFactory,
44
+ type CollabExtensionFactoryArgs,
45
+ } from './CollabExtensionFactoryRegistry.js'
46
+ export {
47
+ registerFormCollabBinding,
48
+ getFormCollabBinding,
49
+ type FormCollabBinding,
50
+ type FormCollabBindingFactory,
51
+ type FormCollabBindingFactoryArgs,
52
+ } from './FormCollabBindingRegistry.js'
53
+ export {
54
+ registerRecordWrapper,
55
+ getRecordWrapper,
56
+ type RecordWrapperProps,
57
+ } from './RecordWrapperRegistry.js'
58
+ export {
59
+ RecordWrapperGate,
60
+ type RecordWrapperGateProps,
61
+ } from './RecordWrapperGate.js'
62
+ export { parseRecordEditUrl, type RecordEditIdentity } from './parseRecordEditUrl.js'
35
63
  export {
36
64
  registerWidgetRenderer,
37
65
  getWidgetRenderer,
@@ -81,6 +109,7 @@ export {
81
109
  type RightSidebarProps,
82
110
  } from './RightSidebar.js'
83
111
  export { RightSidebarTrigger } from './RightSidebarTrigger.js'
112
+ export { SearchTrigger } from './SearchTrigger.js'
84
113
  export {
85
114
  useResizableWidth,
86
115
  clampPanelWidth,
@@ -96,6 +125,15 @@ export { NotificationBell } from './NotificationBell.js'
96
125
  export { RenderHookSlot } from './RenderHookSlot.js'
97
126
  export { HeadHooks } from './HeadHooks.js'
98
127
 
128
+ export {
129
+ isNavItemActive,
130
+ type NavComponentProps,
131
+ type HeaderComponentProps,
132
+ type FooterComponentProps,
133
+ type ComponentSlotRegistry,
134
+ } from './component-slots.js'
135
+ export type { NavItem } from '../pageData.js'
136
+
99
137
  // Re-export pure theme functions for client-safe usage (avoids importing main barrel which has server-only code)
100
138
  export { generateThemeCSS } from '../theme/generate-css.js'
101
139
  export { resolveTheme } from '../theme/resolve.js'
@@ -140,13 +140,17 @@ function NavTree({
140
140
  )
141
141
  }
142
142
 
143
- export function SidebarLayout({ panel, basePath, currentPath, children }: AppShellProps) {
143
+ export function SidebarLayout({ panel, basePath, currentPath, children, componentSlotRegistry }: AppShellProps) {
144
144
  const title = panel.branding?.title ?? panel.name
145
145
  const groups = groupItems(panel.navigation ?? [])
146
146
  const hooks = panel.renderHooks
147
147
  const dn = panel.databaseNotifications
148
148
  const bellInTopbar = dn && dn.position === 'topbar'
149
149
  const bellInSidebar = dn && dn.position === 'sidebar'
150
+ const NavSlot = componentSlotRegistry?.nav
151
+ const HeaderSlot = componentSlotRegistry?.header
152
+ const FooterSlot = componentSlotRegistry?.footer
153
+ const slotProps = currentPath !== undefined ? { currentPath } : {}
150
154
 
151
155
  return (
152
156
  <SidebarProvider>
@@ -172,14 +176,17 @@ export function SidebarLayout({ panel, basePath, currentPath, children }: AppShe
172
176
 
173
177
  <SidebarContent>
174
178
  <RenderHookSlot name="panels::sidebar.nav.start" hooks={hooks} />
175
- {groups.map((g, idx) => (
176
- <SidebarGroup key={g.group ?? `__top__${idx}`}>
177
- {g.group !== undefined && <SidebarGroupLabel>{g.group}</SidebarGroupLabel>}
178
- <SidebarGroupContent>
179
- <NavTree items={g.items} pathname={currentPath} basePath={basePath} />
180
- </SidebarGroupContent>
181
- </SidebarGroup>
182
- ))}
179
+ {NavSlot
180
+ ? <NavSlot navigation={panel.navigation ?? []} basePath={basePath} {...slotProps} />
181
+ : groups.map((g, idx) => (
182
+ <SidebarGroup key={g.group ?? `__top__${idx}`}>
183
+ {g.group !== undefined && <SidebarGroupLabel>{g.group}</SidebarGroupLabel>}
184
+ <SidebarGroupContent>
185
+ <NavTree items={g.items} pathname={currentPath} basePath={basePath} />
186
+ </SidebarGroupContent>
187
+ </SidebarGroup>
188
+ ))
189
+ }
183
190
  <RenderHookSlot name="panels::sidebar.nav.end" hooks={hooks} />
184
191
  </SidebarContent>
185
192
 
@@ -210,29 +217,33 @@ export function SidebarLayout({ panel, basePath, currentPath, children }: AppShe
210
217
  </Sidebar>
211
218
 
212
219
  <SidebarInset>
213
- <header className="sticky top-0 z-10 flex h-14 shrink-0 items-center gap-2">
214
- <div className="flex flex-1 items-center gap-2 px-3">
215
- <SidebarTrigger />
216
- <Separator orientation="vertical" className="me-2 data-[orientation=vertical]:h-4" />
217
- <RenderHookSlot name="panels::topbar.start" hooks={hooks} />
218
- <SearchTrigger />
219
- </div>
220
- <div className="flex items-center gap-1 px-3">
221
- <ThemeToggle />
222
- {bellInTopbar && <NotificationBell meta={dn} />}
223
- <RightSidebarTrigger />
224
- <UserMenu
225
- userMenu={panel.userMenu}
226
- before={<RenderHookSlot name="panels::user-menu.before" hooks={hooks} />}
227
- after={<RenderHookSlot name="panels::user-menu.after" hooks={hooks} />}
228
- />
229
- <RenderHookSlot name="panels::topbar.end" hooks={hooks} />
230
- </div>
231
- </header>
220
+ {HeaderSlot
221
+ ? <HeaderSlot navigation={panel.navigation ?? []} basePath={basePath} {...slotProps} />
222
+ : <header className="sticky top-0 z-10 flex h-14 shrink-0 items-center gap-2">
223
+ <div className="flex flex-1 items-center gap-2 px-3">
224
+ <SidebarTrigger />
225
+ <Separator orientation="vertical" className="me-2 data-[orientation=vertical]:h-4" />
226
+ <RenderHookSlot name="panels::topbar.start" hooks={hooks} />
227
+ <SearchTrigger />
228
+ </div>
229
+ <div className="flex items-center gap-1 px-3">
230
+ <ThemeToggle />
231
+ {bellInTopbar && <NotificationBell meta={dn} />}
232
+ <RightSidebarTrigger />
233
+ <UserMenu
234
+ userMenu={panel.userMenu}
235
+ before={<RenderHookSlot name="panels::user-menu.before" hooks={hooks} />}
236
+ after={<RenderHookSlot name="panels::user-menu.after" hooks={hooks} />}
237
+ />
238
+ <RenderHookSlot name="panels::topbar.end" hooks={hooks} />
239
+ </div>
240
+ </header>
241
+ }
232
242
  <div className="flex flex-1 flex-col px-4 pb-4">
233
243
  {children}
234
244
  <RenderHookSlot name="panels::footer" hooks={hooks} />
235
245
  </div>
246
+ {FooterSlot && <FooterSlot basePath={basePath} {...slotProps} />}
236
247
  </SidebarInset>
237
248
  </SidebarProvider>
238
249
  )
@@ -166,7 +166,7 @@ function groupItems(items: NavItem[]): Array<{ group: string | undefined; items:
166
166
  return order.map(g => ({ group: g, items: buckets.get(g)! }))
167
167
  }
168
168
 
169
- export function TopbarLayout({ panel, basePath, currentPath, children }: AppShellProps) {
169
+ export function TopbarLayout({ panel, basePath, currentPath, children, componentSlotRegistry }: AppShellProps) {
170
170
  const title = panel.branding?.title ?? panel.name
171
171
  const groups = groupItems(panel.navigation ?? [])
172
172
  const hooks = panel.renderHooks
@@ -174,72 +174,85 @@ export function TopbarLayout({ panel, basePath, currentPath, children }: AppShel
174
174
  // no sidebar exists in this layout, so the bell rides in the topbar
175
175
  // chrome regardless of the configured position.
176
176
  const dn = panel.databaseNotifications
177
+ const NavSlot = componentSlotRegistry?.nav
178
+ const HeaderSlot = componentSlotRegistry?.header
179
+ const FooterSlot = componentSlotRegistry?.footer
180
+ const slotProps = currentPath !== undefined ? { currentPath } : {}
177
181
 
178
182
  return (
179
183
  <div className="flex flex-col h-screen bg-background text-foreground overflow-hidden">
180
- <header className="h-14 shrink-0 border-b bg-card flex items-center gap-4 px-6">
181
- <div className="flex items-center gap-2 me-2">
182
- {panel.branding?.logo
183
- ? <>
184
- <img src={panel.branding.logo} alt={title} className="h-6 w-6" />
185
- <span className="text-sm font-semibold">{title}</span>
186
- </>
187
- : <span className="text-sm font-semibold">{title}</span>
188
- }
189
- </div>
190
- <Separator orientation="vertical" className="h-4" />
191
- <RenderHookSlot name="panels::topbar.start" hooks={hooks} />
192
- <nav className="flex items-center gap-1 flex-1 overflow-x-auto">
193
- <a
194
- href={basePath}
195
- className={cn(linkBase, currentPath === basePath ? linkActive : linkIdle)}
196
- >
197
- Dashboard
198
- </a>
199
- {groups.map((g, idx) => {
200
- if (g.group === undefined) {
201
- return g.items.map(it => (
202
- it.children && it.children.length > 0
203
- ? <ParentDropdown key={it.name} item={it} pathname={currentPath} basePath={basePath} />
204
- : <FlatLink key={it.name} item={it} pathname={currentPath} basePath={basePath} />
205
- ))
184
+ {HeaderSlot
185
+ ? <HeaderSlot navigation={panel.navigation ?? []} basePath={basePath} {...slotProps} />
186
+ : <header className="h-14 shrink-0 border-b bg-card flex items-center gap-4 px-6">
187
+ <div className="flex items-center gap-2 me-2">
188
+ {panel.branding?.logo
189
+ ? <>
190
+ <img src={panel.branding.logo} alt={title} className="h-6 w-6" />
191
+ <span className="text-sm font-semibold">{title}</span>
192
+ </>
193
+ : <span className="text-sm font-semibold">{title}</span>
194
+ }
195
+ </div>
196
+ <Separator orientation="vertical" className="h-4" />
197
+ <RenderHookSlot name="panels::topbar.start" hooks={hooks} />
198
+ {NavSlot
199
+ ? <div className="flex items-center gap-1 flex-1 overflow-x-auto">
200
+ <NavSlot navigation={panel.navigation ?? []} basePath={basePath} {...slotProps} />
201
+ </div>
202
+ : <nav className="flex items-center gap-1 flex-1 overflow-x-auto">
203
+ <a
204
+ href={basePath}
205
+ className={cn(linkBase, currentPath === basePath ? linkActive : linkIdle)}
206
+ >
207
+ Dashboard
208
+ </a>
209
+ {groups.map((g, idx) => {
210
+ if (g.group === undefined) {
211
+ return g.items.map(it => (
212
+ it.children && it.children.length > 0
213
+ ? <ParentDropdown key={it.name} item={it} pathname={currentPath} basePath={basePath} />
214
+ : <FlatLink key={it.name} item={it} pathname={currentPath} basePath={basePath} />
215
+ ))
216
+ }
217
+ return (
218
+ <GroupDropdown
219
+ key={`${g.group}__${idx}`}
220
+ label={g.group}
221
+ items={g.items}
222
+ pathname={currentPath}
223
+ basePath={basePath}
224
+ />
225
+ )
226
+ })}
227
+ {panel.themeEditor && (
228
+ <a
229
+ href={`${basePath}/theme`}
230
+ className={cn(linkBase, currentPath === `${basePath}/theme` ? linkActive : linkIdle)}
231
+ >
232
+ Theme
233
+ </a>
234
+ )}
235
+ </nav>
206
236
  }
207
- return (
208
- <GroupDropdown
209
- key={`${g.group}__${idx}`}
210
- label={g.group}
211
- items={g.items}
212
- pathname={currentPath}
213
- basePath={basePath}
214
- />
215
- )
216
- })}
217
- {panel.themeEditor && (
218
- <a
219
- href={`${basePath}/theme`}
220
- className={cn(linkBase, currentPath === `${basePath}/theme` ? linkActive : linkIdle)}
221
- >
222
- Theme
223
- </a>
224
- )}
225
- </nav>
226
- <SearchTrigger />
227
- <ThemeToggle />
228
- {dn && <NotificationBell meta={dn} />}
229
- <RightSidebarTrigger />
230
- <UserMenu
231
- userMenu={panel.userMenu}
232
- before={<RenderHookSlot name="panels::user-menu.before" hooks={hooks} />}
233
- after={<RenderHookSlot name="panels::user-menu.after" hooks={hooks} />}
234
- />
235
- <RenderHookSlot name="panels::topbar.end" hooks={hooks} />
236
- </header>
237
+ <SearchTrigger />
238
+ <ThemeToggle />
239
+ {dn && <NotificationBell meta={dn} />}
240
+ <RightSidebarTrigger />
241
+ <UserMenu
242
+ userMenu={panel.userMenu}
243
+ before={<RenderHookSlot name="panels::user-menu.before" hooks={hooks} />}
244
+ after={<RenderHookSlot name="panels::user-menu.after" hooks={hooks} />}
245
+ />
246
+ <RenderHookSlot name="panels::topbar.end" hooks={hooks} />
247
+ </header>
248
+ }
237
249
  <div className="flex flex-1 overflow-hidden">
238
250
  <main className="flex-1 overflow-y-auto p-6">
239
251
  {children}
240
252
  <RenderHookSlot name="panels::footer" hooks={hooks} />
241
253
  </main>
242
254
  </div>
255
+ {FooterSlot && <FooterSlot basePath={basePath} {...slotProps} />}
243
256
  </div>
244
257
  )
245
258
  }
@@ -0,0 +1,75 @@
1
+ import { test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { parseRecordEditUrl } from './parseRecordEditUrl.js'
4
+
5
+ test('parseRecordEditUrl: bare resource edit', () => {
6
+ assert.deepEqual(
7
+ parseRecordEditUrl('/admin/articles/123/edit', '/admin'),
8
+ { resourceSlug: 'articles', recordId: '123' },
9
+ )
10
+ })
11
+
12
+ test('parseRecordEditUrl: cluster-prefixed resource edit', () => {
13
+ assert.deepEqual(
14
+ parseRecordEditUrl('/admin/blog/articles/123/edit', '/admin'),
15
+ { resourceSlug: 'blog/articles', recordId: '123' },
16
+ )
17
+ })
18
+
19
+ test('parseRecordEditUrl: nested-relation edit picks child id', () => {
20
+ assert.deepEqual(
21
+ parseRecordEditUrl('/admin/articles/123/comments/456/edit', '/admin'),
22
+ { resourceSlug: 'articles/123/comments', recordId: '456' },
23
+ )
24
+ })
25
+
26
+ test('parseRecordEditUrl: list page returns null', () => {
27
+ assert.equal(parseRecordEditUrl('/admin/articles', '/admin'), null)
28
+ assert.equal(parseRecordEditUrl('/admin/articles/123', '/admin'), null)
29
+ })
30
+
31
+ test('parseRecordEditUrl: create page returns null', () => {
32
+ assert.equal(parseRecordEditUrl('/admin/articles/create', '/admin'), null)
33
+ assert.equal(parseRecordEditUrl('/admin/articles/123/comments/create', '/admin'), null)
34
+ })
35
+
36
+ test('parseRecordEditUrl: basePath mismatch returns null', () => {
37
+ assert.equal(parseRecordEditUrl('/site/articles/123/edit', '/admin'), null)
38
+ })
39
+
40
+ test('parseRecordEditUrl: trailing slashes tolerated on URL', () => {
41
+ assert.deepEqual(
42
+ parseRecordEditUrl('/admin/articles/123/edit/', '/admin'),
43
+ { resourceSlug: 'articles', recordId: '123' },
44
+ )
45
+ })
46
+
47
+ test('parseRecordEditUrl: trailing slashes tolerated on basePath', () => {
48
+ assert.deepEqual(
49
+ parseRecordEditUrl('/admin/articles/123/edit', '/admin/'),
50
+ { resourceSlug: 'articles', recordId: '123' },
51
+ )
52
+ })
53
+
54
+ test('parseRecordEditUrl: root basePath', () => {
55
+ assert.deepEqual(
56
+ parseRecordEditUrl('/articles/123/edit', ''),
57
+ { resourceSlug: 'articles', recordId: '123' },
58
+ )
59
+ })
60
+
61
+ test('parseRecordEditUrl: empty path returns null', () => {
62
+ assert.equal(parseRecordEditUrl('', '/admin'), null)
63
+ })
64
+
65
+ test('parseRecordEditUrl: too-short path returns null', () => {
66
+ assert.equal(parseRecordEditUrl('/admin/edit', '/admin'), null)
67
+ // 'edit' alone after basePath isn't enough — needs slug + id + 'edit'.
68
+ })
69
+
70
+ test('parseRecordEditUrl: slug-only edit (no record id) returns null', () => {
71
+ // '/admin/123/edit' would technically match parts.length===3 with
72
+ // 'edit' last and '123' as recordId — but with empty slug after the
73
+ // slice. Defensive: reject when slugParts is empty.
74
+ assert.equal(parseRecordEditUrl('/admin/edit', '/admin'), null)
75
+ })
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Parses a pilotiq URL into a record-edit identity, or returns `null`
3
+ * for any URL that isn't a record-bound edit page.
4
+ *
5
+ * A URL matches when:
6
+ * 1. it starts with the panel's `basePath`
7
+ * 2. after stripping the prefix it ends with `/edit`
8
+ * 3. there are at least three remaining segments (resource slug,
9
+ * record id, `edit`)
10
+ *
11
+ * The `resourceSlug` is the slash-joined chain of every segment up to
12
+ * the record id — this gives clustered resources (`${base}/blog/articles/123/edit`)
13
+ * and nested-relation edits (`${base}/articles/123/comments/456/edit`)
14
+ * distinct slugs so two URLs that target different records always
15
+ * produce different room names downstream.
16
+ *
17
+ * `/admin/articles/123/edit` → { resourceSlug: 'articles', recordId: '123' }
18
+ * `/admin/blog/articles/123/edit` → { resourceSlug: 'blog/articles', recordId: '123' }
19
+ * `/admin/articles/123/comments/456/edit` → { resourceSlug: 'articles/123/comments', recordId: '456' }
20
+ * `/admin/articles/123/comments` → null (no trailing /edit)
21
+ * `/admin/articles/123/comments/create` → null (no record id)
22
+ * `/site/articles/123/edit` → null (basePath mismatch when basePath='/admin')
23
+ */
24
+ export interface RecordEditIdentity {
25
+ resourceSlug: string
26
+ recordId: string
27
+ }
28
+
29
+ export function parseRecordEditUrl(
30
+ currentPath: string,
31
+ basePath: string,
32
+ ): RecordEditIdentity | null {
33
+ if (!currentPath) return null
34
+ // Normalise — trailing slashes on the URL or trailing slashes on
35
+ // basePath would otherwise reject perfectly valid matches.
36
+ const trimmedPath = currentPath.replace(/\/+$/, '')
37
+ const trimmedBase = basePath.replace(/\/+$/, '')
38
+
39
+ if (trimmedBase !== '' && !trimmedPath.startsWith(trimmedBase)) return null
40
+
41
+ const tail = trimmedPath.slice(trimmedBase.length).replace(/^\/+/, '')
42
+ const parts = tail.split('/').filter(Boolean)
43
+
44
+ if (parts.length < 3) return null
45
+ if (parts[parts.length - 1] !== 'edit') return null
46
+
47
+ const recordId = parts[parts.length - 2]!
48
+ const slugParts = parts.slice(0, parts.length - 2)
49
+ if (slugParts.length === 0) return null
50
+
51
+ return {
52
+ resourceSlug: slugParts.join('/'),
53
+ recordId,
54
+ }
55
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Thin localStorage wrappers shared by every persistable surface
3
+ * (collapsible sections, dismissible alerts, wizard step, hidden table
4
+ * columns, group fold state, filter strip toggle, Repeater / Builder
5
+ * row collapse + accordion).
6
+ *
7
+ * Every helper silently no-ops on SSR / private mode / quota — callers
8
+ * already treat all three the same way.
9
+ */
10
+
11
+ export function readStoredString(key: string): string | null {
12
+ if (typeof window === 'undefined') return null
13
+ try { return window.localStorage.getItem(key) }
14
+ catch { return null }
15
+ }
16
+
17
+ export function writeStoredString(key: string, value: string): void {
18
+ if (typeof window === 'undefined') return
19
+ try { window.localStorage.setItem(key, value) } catch { /* quota / blocked */ }
20
+ }
21
+
22
+ export function removeStoredString(key: string): void {
23
+ if (typeof window === 'undefined') return
24
+ try { window.localStorage.removeItem(key) } catch { /* */ }
25
+ }
26
+
27
+ /**
28
+ * Boolean flag using `'1'` / `'0'` encoding. Returns `defaultValue`
29
+ * when the key is unset, SSR, or storage is blocked.
30
+ */
31
+ export function readStoredFlag(key: string, defaultValue: boolean): boolean {
32
+ const raw = readStoredString(key)
33
+ if (raw === '1') return true
34
+ if (raw === '0') return false
35
+ return defaultValue
36
+ }
37
+
38
+ export function writeStoredFlag(key: string, value: boolean): void {
39
+ writeStoredString(key, value ? '1' : '0')
40
+ }