@pilotiq/pilotiq 0.7.1 → 0.8.0

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 (367) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/CHANGELOG.md +154 -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/RecordWrapperGate.d.ts +25 -0
  93. package/dist/react/RecordWrapperGate.d.ts.map +1 -0
  94. package/dist/react/RecordWrapperGate.js +30 -0
  95. package/dist/react/RecordWrapperGate.js.map +1 -0
  96. package/dist/react/RecordWrapperRegistry.d.ts +31 -0
  97. package/dist/react/RecordWrapperRegistry.d.ts.map +1 -0
  98. package/dist/react/RecordWrapperRegistry.js +15 -0
  99. package/dist/react/RecordWrapperRegistry.js.map +1 -0
  100. package/dist/react/SchemaRenderer.d.ts +17 -23
  101. package/dist/react/SchemaRenderer.d.ts.map +1 -1
  102. package/dist/react/SchemaRenderer.js +71 -3647
  103. package/dist/react/SchemaRenderer.js.map +1 -1
  104. package/dist/react/component-slots.d.ts +103 -0
  105. package/dist/react/component-slots.d.ts.map +1 -0
  106. package/dist/react/component-slots.js +18 -0
  107. package/dist/react/component-slots.js.map +1 -0
  108. package/dist/react/fields/BuilderInput.d.ts.map +1 -1
  109. package/dist/react/fields/BuilderInput.js +21 -117
  110. package/dist/react/fields/BuilderInput.js.map +1 -1
  111. package/dist/react/fields/MarkdownInput.d.ts.map +1 -1
  112. package/dist/react/fields/MarkdownInput.js +1 -3
  113. package/dist/react/fields/MarkdownInput.js.map +1 -1
  114. package/dist/react/fields/RepeaterInput.d.ts.map +1 -1
  115. package/dist/react/fields/RepeaterInput.js +22 -127
  116. package/dist/react/fields/RepeaterInput.js.map +1 -1
  117. package/dist/react/fields/rowState.d.ts +40 -0
  118. package/dist/react/fields/rowState.d.ts.map +1 -0
  119. package/dist/react/fields/rowState.js +60 -0
  120. package/dist/react/fields/rowState.js.map +1 -0
  121. package/dist/react/fields/useRowReorderDnd.d.ts +28 -0
  122. package/dist/react/fields/useRowReorderDnd.d.ts.map +1 -0
  123. package/dist/react/fields/useRowReorderDnd.js +51 -0
  124. package/dist/react/fields/useRowReorderDnd.js.map +1 -0
  125. package/dist/react/index.d.ts +9 -0
  126. package/dist/react/index.d.ts.map +1 -1
  127. package/dist/react/index.js +8 -0
  128. package/dist/react/index.js.map +1 -1
  129. package/dist/react/layouts/SidebarLayout.d.ts +1 -1
  130. package/dist/react/layouts/SidebarLayout.d.ts.map +1 -1
  131. package/dist/react/layouts/SidebarLayout.js +10 -2
  132. package/dist/react/layouts/SidebarLayout.js.map +1 -1
  133. package/dist/react/layouts/TopbarLayout.d.ts +1 -1
  134. package/dist/react/layouts/TopbarLayout.d.ts.map +1 -1
  135. package/dist/react/layouts/TopbarLayout.js +19 -11
  136. package/dist/react/layouts/TopbarLayout.js.map +1 -1
  137. package/dist/react/parseRecordEditUrl.d.ts +29 -0
  138. package/dist/react/parseRecordEditUrl.d.ts.map +1 -0
  139. package/dist/react/parseRecordEditUrl.js +25 -0
  140. package/dist/react/parseRecordEditUrl.js.map +1 -0
  141. package/dist/react/persistedState.d.ts +19 -0
  142. package/dist/react/persistedState.d.ts.map +1 -0
  143. package/dist/react/persistedState.js +51 -0
  144. package/dist/react/persistedState.js.map +1 -0
  145. package/dist/react/schemaRenderer/AlertRenderer.d.ts +12 -0
  146. package/dist/react/schemaRenderer/AlertRenderer.d.ts.map +1 -0
  147. package/dist/react/schemaRenderer/AlertRenderer.js +61 -0
  148. package/dist/react/schemaRenderer/AlertRenderer.js.map +1 -0
  149. package/dist/react/schemaRenderer/EntryRenderer.d.ts +13 -0
  150. package/dist/react/schemaRenderer/EntryRenderer.d.ts.map +1 -0
  151. package/dist/react/schemaRenderer/EntryRenderer.js +277 -0
  152. package/dist/react/schemaRenderer/EntryRenderer.js.map +1 -0
  153. package/dist/react/schemaRenderer/SectionRenderer.d.ts +16 -0
  154. package/dist/react/schemaRenderer/SectionRenderer.d.ts.map +1 -0
  155. package/dist/react/schemaRenderer/SectionRenderer.js +62 -0
  156. package/dist/react/schemaRenderer/SectionRenderer.js.map +1 -0
  157. package/dist/react/schemaRenderer/SimpleElements.d.ts +25 -0
  158. package/dist/react/schemaRenderer/SimpleElements.d.ts.map +1 -0
  159. package/dist/react/schemaRenderer/SimpleElements.js +147 -0
  160. package/dist/react/schemaRenderer/SimpleElements.js.map +1 -0
  161. package/dist/react/schemaRenderer/TabsRenderer.d.ts +17 -0
  162. package/dist/react/schemaRenderer/TabsRenderer.d.ts.map +1 -0
  163. package/dist/react/schemaRenderer/TabsRenderer.js +31 -0
  164. package/dist/react/schemaRenderer/TabsRenderer.js.map +1 -0
  165. package/dist/react/schemaRenderer/WizardRenderer.d.ts +34 -0
  166. package/dist/react/schemaRenderer/WizardRenderer.d.ts.map +1 -0
  167. package/dist/react/schemaRenderer/WizardRenderer.js +208 -0
  168. package/dist/react/schemaRenderer/WizardRenderer.js.map +1 -0
  169. package/dist/react/schemaRenderer/action/ActionGroupTrigger.d.ts +21 -0
  170. package/dist/react/schemaRenderer/action/ActionGroupTrigger.d.ts.map +1 -0
  171. package/dist/react/schemaRenderer/action/ActionGroupTrigger.js +82 -0
  172. package/dist/react/schemaRenderer/action/ActionGroupTrigger.js.map +1 -0
  173. package/dist/react/schemaRenderer/action/ActionModalDialog.d.ts +30 -0
  174. package/dist/react/schemaRenderer/action/ActionModalDialog.d.ts.map +1 -0
  175. package/dist/react/schemaRenderer/action/ActionModalDialog.js +182 -0
  176. package/dist/react/schemaRenderer/action/ActionModalDialog.js.map +1 -0
  177. package/dist/react/schemaRenderer/action/ConfirmActionDialog.d.ts +17 -0
  178. package/dist/react/schemaRenderer/action/ConfirmActionDialog.d.ts.map +1 -0
  179. package/dist/react/schemaRenderer/action/ConfirmActionDialog.js +19 -0
  180. package/dist/react/schemaRenderer/action/ConfirmActionDialog.js.map +1 -0
  181. package/dist/react/schemaRenderer/action/HandlerActionButton.d.ts +16 -0
  182. package/dist/react/schemaRenderer/action/HandlerActionButton.d.ts.map +1 -0
  183. package/dist/react/schemaRenderer/action/HandlerActionButton.js +16 -0
  184. package/dist/react/schemaRenderer/action/HandlerActionButton.js.map +1 -0
  185. package/dist/react/schemaRenderer/action/MethodActionButton.d.ts +22 -0
  186. package/dist/react/schemaRenderer/action/MethodActionButton.d.ts.map +1 -0
  187. package/dist/react/schemaRenderer/action/MethodActionButton.js +26 -0
  188. package/dist/react/schemaRenderer/action/MethodActionButton.js.map +1 -0
  189. package/dist/react/schemaRenderer/action/buttons.d.ts +18 -0
  190. package/dist/react/schemaRenderer/action/buttons.d.ts.map +1 -0
  191. package/dist/react/schemaRenderer/action/buttons.js +74 -0
  192. package/dist/react/schemaRenderer/action/buttons.js.map +1 -0
  193. package/dist/react/schemaRenderer/action/helpers.d.ts +26 -0
  194. package/dist/react/schemaRenderer/action/helpers.d.ts.map +1 -0
  195. package/dist/react/schemaRenderer/action/helpers.js +126 -0
  196. package/dist/react/schemaRenderer/action/helpers.js.map +1 -0
  197. package/dist/react/schemaRenderer/action/renderAction.d.ts +21 -0
  198. package/dist/react/schemaRenderer/action/renderAction.d.ts.map +1 -0
  199. package/dist/react/schemaRenderer/action/renderAction.js +102 -0
  200. package/dist/react/schemaRenderer/action/renderAction.js.map +1 -0
  201. package/dist/react/schemaRenderer/columnFormat.d.ts +10 -0
  202. package/dist/react/schemaRenderer/columnFormat.d.ts.map +1 -0
  203. package/dist/react/schemaRenderer/columnFormat.js +76 -0
  204. package/dist/react/schemaRenderer/columnFormat.js.map +1 -0
  205. package/dist/react/schemaRenderer/constants.d.ts +8 -0
  206. package/dist/react/schemaRenderer/constants.d.ts.map +1 -0
  207. package/dist/react/schemaRenderer/constants.js +45 -0
  208. package/dist/react/schemaRenderer/constants.js.map +1 -0
  209. package/dist/react/schemaRenderer/form/FormRenderer.d.ts +29 -0
  210. package/dist/react/schemaRenderer/form/FormRenderer.d.ts.map +1 -0
  211. package/dist/react/schemaRenderer/form/FormRenderer.js +152 -0
  212. package/dist/react/schemaRenderer/form/FormRenderer.js.map +1 -0
  213. package/dist/react/schemaRenderer/form/renderField.d.ts +6 -0
  214. package/dist/react/schemaRenderer/form/renderField.d.ts.map +1 -0
  215. package/dist/react/schemaRenderer/form/renderField.js +239 -0
  216. package/dist/react/schemaRenderer/form/renderField.js.map +1 -0
  217. package/dist/react/schemaRenderer/helpers.d.ts +32 -0
  218. package/dist/react/schemaRenderer/helpers.d.ts.map +1 -0
  219. package/dist/react/schemaRenderer/helpers.js +52 -0
  220. package/dist/react/schemaRenderer/helpers.js.map +1 -0
  221. package/dist/react/schemaRenderer/table/CardsLayoutBody.d.ts +60 -0
  222. package/dist/react/schemaRenderer/table/CardsLayoutBody.d.ts.map +1 -0
  223. package/dist/react/schemaRenderer/table/CardsLayoutBody.js +189 -0
  224. package/dist/react/schemaRenderer/table/CardsLayoutBody.js.map +1 -0
  225. package/dist/react/schemaRenderer/table/TableRenderer.d.ts +29 -0
  226. package/dist/react/schemaRenderer/table/TableRenderer.d.ts.map +1 -0
  227. package/dist/react/schemaRenderer/table/TableRenderer.js +85 -0
  228. package/dist/react/schemaRenderer/table/TableRenderer.js.map +1 -0
  229. package/dist/react/schemaRenderer/table/TableRendererBody.d.ts +18 -0
  230. package/dist/react/schemaRenderer/table/TableRendererBody.d.ts.map +1 -0
  231. package/dist/react/schemaRenderer/table/TableRendererBody.js +555 -0
  232. package/dist/react/schemaRenderer/table/TableRendererBody.js.map +1 -0
  233. package/dist/react/schemaRenderer/table/filters.d.ts +263 -0
  234. package/dist/react/schemaRenderer/table/filters.d.ts.map +1 -0
  235. package/dist/react/schemaRenderer/table/filters.js +497 -0
  236. package/dist/react/schemaRenderer/table/filters.js.map +1 -0
  237. package/dist/react/schemaRenderer/table/formatCell.d.ts +11 -0
  238. package/dist/react/schemaRenderer/table/formatCell.d.ts.map +1 -0
  239. package/dist/react/schemaRenderer/table/formatCell.js +172 -0
  240. package/dist/react/schemaRenderer/table/formatCell.js.map +1 -0
  241. package/dist/react/schemaRenderer/table/links.d.ts +42 -0
  242. package/dist/react/schemaRenderer/table/links.d.ts.map +1 -0
  243. package/dist/react/schemaRenderer/table/links.js +55 -0
  244. package/dist/react/schemaRenderer/table/links.js.map +1 -0
  245. package/dist/react/schemaRenderer/table/renderRowActions.d.ts +13 -0
  246. package/dist/react/schemaRenderer/table/renderRowActions.d.ts.map +1 -0
  247. package/dist/react/schemaRenderer/table/renderRowActions.js +25 -0
  248. package/dist/react/schemaRenderer/table/renderRowActions.js.map +1 -0
  249. package/dist/react/schemaRenderer/table/url.d.ts +41 -0
  250. package/dist/react/schemaRenderer/table/url.d.ts.map +1 -0
  251. package/dist/react/schemaRenderer/table/url.js +114 -0
  252. package/dist/react/schemaRenderer/table/url.js.map +1 -0
  253. package/dist/routes/globals.d.ts +13 -0
  254. package/dist/routes/globals.d.ts.map +1 -0
  255. package/dist/routes/globals.js +131 -0
  256. package/dist/routes/globals.js.map +1 -0
  257. package/dist/routes/helpers.d.ts +217 -0
  258. package/dist/routes/helpers.d.ts.map +1 -0
  259. package/dist/routes/helpers.js +498 -0
  260. package/dist/routes/helpers.js.map +1 -0
  261. package/dist/routes/pages.d.ts +15 -0
  262. package/dist/routes/pages.d.ts.map +1 -0
  263. package/dist/routes/pages.js +145 -0
  264. package/dist/routes/pages.js.map +1 -0
  265. package/dist/routes/panel.d.ts +19 -0
  266. package/dist/routes/panel.d.ts.map +1 -0
  267. package/dist/routes/panel.js +191 -0
  268. package/dist/routes/panel.js.map +1 -0
  269. package/dist/routes/relations.d.ts +21 -0
  270. package/dist/routes/relations.d.ts.map +1 -0
  271. package/dist/routes/relations.js +1239 -0
  272. package/dist/routes/relations.js.map +1 -0
  273. package/dist/routes/resources.d.ts +28 -0
  274. package/dist/routes/resources.d.ts.map +1 -0
  275. package/dist/routes/resources.js +741 -0
  276. package/dist/routes/resources.js.map +1 -0
  277. package/dist/routes/theme.d.ts +12 -0
  278. package/dist/routes/theme.d.ts.map +1 -0
  279. package/dist/routes/theme.js +82 -0
  280. package/dist/routes/theme.js.map +1 -0
  281. package/dist/routes.d.ts.map +1 -1
  282. package/dist/routes.js +64 -3078
  283. package/dist/routes.js.map +1 -1
  284. package/dist/vite.d.ts +1 -0
  285. package/dist/vite.d.ts.map +1 -1
  286. package/dist/vite.js +31 -10
  287. package/dist/vite.js.map +1 -1
  288. package/package.json +2 -1
  289. package/src/Pilotiq.ts +95 -0
  290. package/src/actions/Action.ts +79 -723
  291. package/src/actions/bulkFactories.ts +168 -0
  292. package/src/actions/crudFactories.ts +220 -0
  293. package/src/actions/factoryHelpers.ts +177 -0
  294. package/src/actions/m2mFactories.ts +193 -0
  295. package/src/actions/relationFactories.ts +372 -0
  296. package/src/elements/dispatchForm.ts +1 -1
  297. package/src/elements/dispatchTable.ts +1 -1
  298. package/src/fields/Field.ts +39 -0
  299. package/src/pageData/breadcrumbs.ts +288 -0
  300. package/src/pageData/forms.ts +578 -0
  301. package/src/pageData/helpers.ts +764 -0
  302. package/src/pageData/misc.ts +347 -0
  303. package/src/pageData/navigation.ts +779 -0
  304. package/src/pageData/relationPages.ts +1246 -0
  305. package/src/pageData/relationTabs.ts +286 -0
  306. package/src/pageData/resourcePages.ts +593 -0
  307. package/src/pageData.ts +122 -4731
  308. package/src/react/AppShell.tsx +27 -1
  309. package/src/react/CollabExtensionFactoryRegistry.ts +55 -0
  310. package/src/react/CollabRoomContext.ts +42 -0
  311. package/src/react/FormCollabBindingRegistry.ts +72 -0
  312. package/src/react/RecordWrapperGate.tsx +40 -0
  313. package/src/react/RecordWrapperRegistry.ts +39 -0
  314. package/src/react/SchemaRenderer.tsx +230 -6479
  315. package/src/react/component-slots.test.ts +103 -0
  316. package/src/react/component-slots.ts +116 -0
  317. package/src/react/fields/BuilderInput.tsx +29 -117
  318. package/src/react/fields/MarkdownInput.tsx +0 -1
  319. package/src/react/fields/RepeaterInput.tsx +29 -130
  320. package/src/react/fields/rowState.ts +106 -0
  321. package/src/react/fields/useRowReorderDnd.ts +78 -0
  322. package/src/react/index.ts +38 -0
  323. package/src/react/layouts/SidebarLayout.tsx +39 -28
  324. package/src/react/layouts/TopbarLayout.tsx +70 -57
  325. package/src/react/parseRecordEditUrl.test.ts +75 -0
  326. package/src/react/parseRecordEditUrl.ts +55 -0
  327. package/src/react/persistedState.ts +40 -0
  328. package/src/react/schemaRenderer/AlertRenderer.tsx +112 -0
  329. package/src/react/schemaRenderer/EntryRenderer.tsx +501 -0
  330. package/src/react/schemaRenderer/SectionRenderer.tsx +120 -0
  331. package/src/react/schemaRenderer/SimpleElements.tsx +306 -0
  332. package/src/react/schemaRenderer/TabsRenderer.tsx +62 -0
  333. package/src/react/schemaRenderer/WizardRenderer.tsx +338 -0
  334. package/src/react/schemaRenderer/action/ActionGroupTrigger.tsx +177 -0
  335. package/src/react/schemaRenderer/action/ActionModalDialog.tsx +273 -0
  336. package/src/react/schemaRenderer/action/ConfirmActionDialog.tsx +61 -0
  337. package/src/react/schemaRenderer/action/HandlerActionButton.tsx +43 -0
  338. package/src/react/schemaRenderer/action/MethodActionButton.tsx +64 -0
  339. package/src/react/schemaRenderer/action/buttons.tsx +99 -0
  340. package/src/react/schemaRenderer/action/helpers.ts +140 -0
  341. package/src/react/schemaRenderer/action/renderAction.tsx +245 -0
  342. package/src/react/schemaRenderer/columnFormat.ts +65 -0
  343. package/src/react/schemaRenderer/constants.ts +50 -0
  344. package/src/react/schemaRenderer/form/FormRenderer.tsx +233 -0
  345. package/src/react/schemaRenderer/form/renderField.tsx +511 -0
  346. package/src/react/schemaRenderer/helpers.tsx +81 -0
  347. package/src/react/schemaRenderer/table/CardsLayoutBody.tsx +308 -0
  348. package/src/react/schemaRenderer/table/TableRenderer.tsx +123 -0
  349. package/src/react/schemaRenderer/table/TableRendererBody.tsx +974 -0
  350. package/src/react/schemaRenderer/table/filters.tsx +1233 -0
  351. package/src/react/schemaRenderer/table/formatCell.tsx +264 -0
  352. package/src/react/schemaRenderer/table/links.tsx +112 -0
  353. package/src/react/schemaRenderer/table/renderRowActions.tsx +52 -0
  354. package/src/react/schemaRenderer/table/url.tsx +143 -0
  355. package/src/routes/globals.ts +154 -0
  356. package/src/routes/helpers.ts +668 -0
  357. package/src/routes/pages.ts +173 -0
  358. package/src/routes/panel.ts +204 -0
  359. package/src/routes/relations.ts +1219 -0
  360. package/src/routes/resources.ts +786 -0
  361. package/src/routes/theme.ts +109 -0
  362. package/src/routes.test.ts +1 -1
  363. package/src/routes.ts +64 -3176
  364. package/src/schema/TableWidget.test.ts +2 -2
  365. package/src/theme/migrate.test.ts +178 -0
  366. package/src/vite.test.ts +184 -0
  367. package/src/vite.ts +31 -9
@@ -0,0 +1,264 @@
1
+ import React, { useState } from 'react'
2
+ import { CheckIcon, CircleIcon, CopyIcon } from 'lucide-react'
3
+ import type { ElementMeta } from '../../../schema/Element.js'
4
+ import {
5
+ BADGE_COLOR_CLASSES,
6
+ COLUMN_COLOR_CLASSES,
7
+ COLUMN_WEIGHT_CLASSES,
8
+ } from '../constants.js'
9
+ import { resolveIcon } from '../helpers.js'
10
+ import { applyColumnFormat } from '../columnFormat.js'
11
+
12
+ // ─── Table cell rendering ───────────────────────────────────
13
+ //
14
+ // `formatCell` is the column-type dispatch (text / badge / icon /
15
+ // image / color + array variants). `wrapCell` and `wrapCellList`
16
+ // apply the shared text chrome (color / weight / tooltip / line-clamp /
17
+ // wrap / copy-to-clipboard) so every column-type path stays consistent.
18
+
19
+ /** Render a cell. Honors the column's `columnType` (badge/icon/boolean/
20
+ * image), built-in `format` spec, and per-row `_formatted[name]`
21
+ * overrides from server-side `formatStateUsing` callbacks. */
22
+ export function formatCell(
23
+ value: unknown,
24
+ col?: ElementMeta,
25
+ row?: Record<string, unknown>,
26
+ ): React.ReactNode {
27
+ if (col === undefined) {
28
+ // Legacy raw-value fallback for non-column callsites.
29
+ if (value === null || value === undefined) return <span className="text-muted-foreground">—</span>
30
+ if (value instanceof Date) return value.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })
31
+ if (typeof value === 'boolean') return value ? 'Yes' : 'No'
32
+ if (typeof value === 'object') return JSON.stringify(value)
33
+ return String(value)
34
+ }
35
+
36
+ const columnType = String(col['columnType'] ?? 'text')
37
+ const fallback = (col['default'] as string | undefined)
38
+
39
+ // Per-row server-eval result wins over everything.
40
+ const colName = String(col['name'] ?? '')
41
+ const formatted = (row?.['_formatted'] as Record<string, string> | undefined)?.[colName]
42
+ const richtext = (row?.['_richtextCells'] as Record<string, true> | undefined)?.[colName] === true
43
+ const isBlank = value === null || value === undefined || value === ''
44
+
45
+ if (formatted !== undefined && formatted !== '') {
46
+ return wrapCell(formatted, col, richtext)
47
+ }
48
+ if (isBlank) {
49
+ return <span className="text-muted-foreground">{fallback ?? '—'}</span>
50
+ }
51
+
52
+ switch (columnType) {
53
+ case 'badge': {
54
+ const map = (col['badgeColors'] as Record<string, string> | undefined) ?? {}
55
+ const color = map[String(value)] ?? 'gray'
56
+ const cls = BADGE_COLOR_CLASSES[color] ?? BADGE_COLOR_CLASSES['gray']
57
+ return (
58
+ <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${cls}`}>
59
+ {String(value)}
60
+ </span>
61
+ )
62
+ }
63
+ case 'icon':
64
+ case 'boolean': {
65
+ const map = (col['iconOptions'] as Record<string, { icon: string; color?: string }> | undefined) ?? {}
66
+ const opt = map[String(value)]
67
+ if (!opt) return <span className="text-muted-foreground">—</span>
68
+ const Icon = resolveIcon(opt.icon) ?? CircleIcon
69
+ const colorClass = opt.color ? (COLUMN_COLOR_CLASSES[opt.color] ?? '') : ''
70
+ return <Icon className={`size-4 inline ${colorClass}`} aria-label={String(value)} />
71
+ }
72
+ case 'image': {
73
+ const url = String(value)
74
+ const size = (col['imageSize'] as number | undefined) ?? 32
75
+ const shape = col['imageShape'] === 'circle' ? 'rounded-full' : 'rounded-md'
76
+ return (
77
+ <img
78
+ src={url}
79
+ alt=""
80
+ width={size}
81
+ height={size}
82
+ className={`${shape} object-cover`}
83
+ />
84
+ )
85
+ }
86
+ case 'color': {
87
+ const css = String(value)
88
+ const shape = col['colorShape'] as 'rounded' | 'square' | 'circle' | undefined
89
+ const shapeClass =
90
+ shape === 'circle' ? 'rounded-full' :
91
+ shape === 'square' ? 'rounded-none' : 'rounded'
92
+ const hideValue = col['colorHideValue'] === true
93
+ return (
94
+ <span className="inline-flex items-center gap-2">
95
+ <span
96
+ className={`size-4 border border-border ${shapeClass}`}
97
+ style={{ backgroundColor: css }}
98
+ aria-hidden="true"
99
+ />
100
+ {!hideValue && <span className="text-sm">{css}</span>}
101
+ </span>
102
+ )
103
+ }
104
+ default: {
105
+ // Array-valued cells — `bulleted()` wins over `listWithLineBreaks()`
106
+ // when both are set. Falls through to the standard string path for
107
+ // non-array values so the per-cell formatters keep working.
108
+ if (Array.isArray(value)) {
109
+ const items = value.map(v => String(v))
110
+ if (col['bulleted'] === true) {
111
+ return wrapCellList(items, col, 'bulleted')
112
+ }
113
+ if (col['listWithLineBreaks'] === true) {
114
+ return wrapCellList(items, col, 'lines')
115
+ }
116
+ // Bare array — comma-join (matches the existing legacy fallback).
117
+ return wrapCell(items.join(', '), col)
118
+ }
119
+ // Text column — apply built-in format, then wrapper.
120
+ const fmt = col['format'] as { kind: string; [k: string]: unknown } | undefined
121
+ const display = fmt ? applyColumnFormat(value, fmt) : String(value)
122
+ return wrapCell(display, col)
123
+ }
124
+ }
125
+ }
126
+
127
+ /** Apply text-rendering chrome (color, weight, line-clamp, wrap, tooltip)
128
+ * to a stringified cell value. Used by the text and per-row formatter
129
+ * paths so styling stays consistent. When `asHtml` is true the content
130
+ * is server-rendered HTML (e.g. from the registered richtext renderer)
131
+ * and gets injected via `dangerouslySetInnerHTML`. */
132
+ function wrapCell(content: string, col: ElementMeta, asHtml = false): React.ReactNode {
133
+ const color = col['color'] as string | undefined
134
+ const weight = col['weight'] as string | undefined
135
+ const tooltip = col['tooltip'] as string | undefined
136
+ const wrapping = Boolean(col['wrap'])
137
+ const clamp = col['lineClamp'] as number | undefined
138
+ const copyMsg = col['copyMessage'] as string | undefined
139
+
140
+ const colorCls = color ? (COLUMN_COLOR_CLASSES[color] ?? '') : ''
141
+ const weightCls = weight ? (COLUMN_WEIGHT_CLASSES[weight] ?? '') : ''
142
+ const wrapCls = wrapping ? 'whitespace-normal' : ''
143
+ const clampStyle = clamp !== undefined
144
+ ? { display: '-webkit-box', WebkitLineClamp: String(clamp), WebkitBoxOrient: 'vertical' as const, overflow: 'hidden' }
145
+ : undefined
146
+
147
+ const valueNode = asHtml
148
+ ? (
149
+ <span
150
+ className={`prose prose-sm max-w-none dark:prose-invert ${colorCls} ${weightCls} ${wrapCls}`.trim()}
151
+ title={tooltip}
152
+ style={clampStyle}
153
+ dangerouslySetInnerHTML={{ __html: content }}
154
+ />
155
+ )
156
+ : (
157
+ <span
158
+ className={`${colorCls} ${weightCls} ${wrapCls}`.trim()}
159
+ title={tooltip}
160
+ style={clampStyle}
161
+ >
162
+ {content}
163
+ </span>
164
+ )
165
+
166
+ if (copyMsg === undefined) return valueNode
167
+
168
+ // Copy-to-clipboard trigger — copies the rendered text. For richtext
169
+ // cells the underlying source isn't separately stamped on the wire
170
+ // (would double the row payload), so the rendered HTML is what gets
171
+ // copied; admins comfortable with HTML still get something usable.
172
+ return (
173
+ <span className="inline-flex items-center gap-1.5">
174
+ {valueNode}
175
+ <CellCopyButton text={content} label={copyMsg} />
176
+ </span>
177
+ )
178
+ }
179
+
180
+ /** Tabular-list rendering used by `Column.bulleted()` /
181
+ * `Column.listWithLineBreaks()`. `mode='bulleted'` mounts a `<ul>` with
182
+ * bullet markers; `mode='lines'` separates entries with `<br>`. Both
183
+ * inherit the same color / weight / wrap / tooltip / clamp chrome as
184
+ * the text path. Empty arrays fall through to the muted dash. */
185
+ function wrapCellList(
186
+ items: string[],
187
+ col: ElementMeta,
188
+ mode: 'bulleted' | 'lines',
189
+ ): React.ReactNode {
190
+ if (items.length === 0) {
191
+ const fallback = (col['default'] as string | undefined) ?? '—'
192
+ return <span className="text-muted-foreground">{fallback}</span>
193
+ }
194
+ const color = col['color'] as string | undefined
195
+ const weight = col['weight'] as string | undefined
196
+ const tooltip = col['tooltip'] as string | undefined
197
+
198
+ const colorCls = color ? (COLUMN_COLOR_CLASSES[color] ?? '') : ''
199
+ const weightCls = weight ? (COLUMN_WEIGHT_CLASSES[weight] ?? '') : ''
200
+
201
+ if (mode === 'bulleted') {
202
+ return (
203
+ <ul
204
+ className={`list-disc pl-4 space-y-0.5 ${colorCls} ${weightCls}`.trim()}
205
+ title={tooltip}
206
+ >
207
+ {items.map((s, i) => <li key={i}>{s}</li>)}
208
+ </ul>
209
+ )
210
+ }
211
+ return (
212
+ <span
213
+ className={`${colorCls} ${weightCls}`.trim()}
214
+ title={tooltip}
215
+ >
216
+ {items.map((s, i) => (
217
+ <React.Fragment key={i}>
218
+ {i > 0 && <br />}
219
+ {s}
220
+ </React.Fragment>
221
+ ))}
222
+ </span>
223
+ )
224
+ }
225
+
226
+ /** Slim copy-to-clipboard button used by `Column.copyMessage()`. The
227
+ * label doubles as the toast text. Mirrors `EntryCopyButton`'s shape
228
+ * but compact enough to live inline next to a cell value. */
229
+ function CellCopyButton({ text, label }: { text: string; label: string }): React.ReactNode {
230
+ const [copied, setCopied] = useState(false)
231
+ const handleClick = (e: React.MouseEvent) => {
232
+ e.stopPropagation()
233
+ e.preventDefault()
234
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
235
+ navigator.clipboard.writeText(text).then(() => {
236
+ setCopied(true)
237
+ setTimeout(() => setCopied(false), 1500)
238
+ }).catch(() => { /* ignore — older browser / permission denied */ })
239
+ }
240
+ }
241
+ return (
242
+ <button
243
+ type="button"
244
+ onClick={handleClick}
245
+ aria-label={copied ? label : 'Copy'}
246
+ title={copied ? label : 'Copy'}
247
+ data-no-row-nav
248
+ className="inline-flex h-5 w-5 items-center justify-center rounded text-muted-foreground hover:text-foreground hover:bg-muted"
249
+ >
250
+ {copied ? <CheckIcon className="size-3" /> : <CopyIcon className="size-3" />}
251
+ </button>
252
+ )
253
+ }
254
+
255
+ /** Resolve a stable row identifier — prefers the row's `id` field, falls
256
+ * back to the row index for rows missing one. Used as the React key
257
+ * in table bodies and as the `:id` substitution for row actions. */
258
+ export function rowId(row: unknown, index: number): string {
259
+ if (row && typeof row === 'object' && 'id' in row) {
260
+ const id = (row as { id?: unknown }).id
261
+ if (id !== undefined && id !== null) return String(id)
262
+ }
263
+ return String(index)
264
+ }
@@ -0,0 +1,112 @@
1
+ import React from 'react'
2
+ import type { NavigateFn } from '../../navigate.js'
3
+
4
+ // ─── Inline link chrome ─────────────────────────────────────
5
+ //
6
+ // Three small chrome components used by the table body:
7
+ // - RecordCellLink wraps each row-data cell in a real <a href> so
8
+ // cmd-click / right-click "open in new tab" works; plain clicks
9
+ // SPA-nav via useNavigate.
10
+ // - ActiveGroupKeyChip surfaces the current `?groupKey=` drill-in
11
+ // state above the table with an × that clears it.
12
+ // - GroupHeadingLink turns a banded group's heading into a clickable
13
+ // drill-in trigger when the group is scopable.
14
+
15
+ /**
16
+ * Modifier-aware SPA-nav click handler. Plain left-click intercepts to
17
+ * `navigate(href)`; cmd / ctrl / shift / alt-click and middle-click
18
+ * fall through to the browser so "open in new tab" / "save link as"
19
+ * semantics keep working on every real `<a href>` in the table body.
20
+ */
21
+ export function useSpaNavClick(
22
+ href: string,
23
+ navigate: NavigateFn,
24
+ ): (e: React.MouseEvent<HTMLAnchorElement>) => void {
25
+ return (e) => {
26
+ if (e.button !== 0) return
27
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return
28
+ e.preventDefault()
29
+ void navigate(href)
30
+ }
31
+ }
32
+
33
+ export function RecordCellLink({
34
+ href, navigate, children,
35
+ }: {
36
+ href: string
37
+ navigate: NavigateFn
38
+ children: React.ReactNode
39
+ }) {
40
+ const onClick = useSpaNavClick(href, navigate)
41
+ return (
42
+ <a
43
+ href={href}
44
+ onClick={onClick}
45
+ className="block px-2 py-2 text-inherit no-underline hover:text-inherit focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded"
46
+ >
47
+ {children}
48
+ </a>
49
+ )
50
+ }
51
+
52
+ /**
53
+ * "Drilled into <Label>: <Value>" chip above the table when a group
54
+ * heading has been clicked. The × clears `?<prefix>groupKey=`, returning
55
+ * the table to its banded view. Real `<a href>` with `useNavigate()`
56
+ * intercept on plain left-click so cmd-click / middle-click open a
57
+ * fresh tab (rare but valid for sharing the banded view URL).
58
+ */
59
+ export function ActiveGroupKeyChip({
60
+ label, value, displayValue, clearHref, navigate,
61
+ }: {
62
+ label: string
63
+ value: string
64
+ displayValue: string
65
+ clearHref: string
66
+ navigate: NavigateFn
67
+ }) {
68
+ const onClick = useSpaNavClick(clearHref, navigate)
69
+ return (
70
+ <div className="flex items-center gap-2 rounded-md border bg-muted/40 px-3 py-2 text-sm">
71
+ <span className="text-muted-foreground">Drilled into</span>
72
+ <span className="font-medium text-foreground">
73
+ {label ? `${label}: ` : ''}{displayValue || value}
74
+ </span>
75
+ <a
76
+ href={clearHref}
77
+ onClick={onClick}
78
+ aria-label="Clear drill-in"
79
+ className="ms-auto text-muted-foreground hover:text-foreground"
80
+ >
81
+ ×
82
+ </a>
83
+ </div>
84
+ )
85
+ }
86
+
87
+ /**
88
+ * Group-heading text wrapped in a real `<a href>` that SPA-navs into the
89
+ * drilled-in URL. Plain left-click intercepts for `useNavigate()`;
90
+ * cmd/ctrl/shift-click + middle-click fall through to the browser so
91
+ * "open in new tab" semantics work. Visually inherits the heading
92
+ * styling — the link adds underline-on-hover affordance without
93
+ * disturbing the surrounding text-transform / size.
94
+ */
95
+ export function GroupHeadingLink({
96
+ href, navigate, children,
97
+ }: {
98
+ href: string
99
+ navigate: NavigateFn
100
+ children: React.ReactNode
101
+ }) {
102
+ const onClick = useSpaNavClick(href, navigate)
103
+ return (
104
+ <a
105
+ href={href}
106
+ onClick={onClick}
107
+ className="inline-flex items-center gap-1 text-inherit no-underline hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded"
108
+ >
109
+ {children}
110
+ </a>
111
+ )
112
+ }
@@ -0,0 +1,52 @@
1
+ import React from 'react'
2
+ import type { ElementMeta } from '../../../schema/Element.js'
3
+ import type { RenderActionOptions } from '../action/buttons.js'
4
+
5
+ // ─── Row-action chrome ──────────────────────────────────────
6
+ //
7
+ // Inline action strip mounted in the trailing cell of each table row.
8
+ // Per-row visibility and disabled state come from the server-side eval
9
+ // inside `dispatchTable` (`_visibleActions` / `_disabledActions` keys on
10
+ // the row); we just consume the stamped allow/disallow sets here.
11
+ //
12
+ // The dispatch (link / fetch+JSON / modal / confirm) lives on the
13
+ // action layer — `renderActionLike` is injected to keep the cycle
14
+ // between this module and `SchemaRenderer.tsx`'s top-level dispatch
15
+ // clean.
16
+
17
+ type RenderActionLike = (el: ElementMeta, index: number, opts?: RenderActionOptions) => React.ReactNode
18
+
19
+ /**
20
+ * Render row actions inline. Each Action becomes a small button next to
21
+ * the others; an `ActionGroup` placed in row position keeps its dropdown
22
+ * via `ActionGroupTrigger` (the dropdown UX is opt-in via grouping, not
23
+ * a default). The `:id` substitution comes from `opts.ids = [rowId]`.
24
+ */
25
+ export function renderRowActions(
26
+ rowId: string,
27
+ rowRecord: Record<string, unknown> | undefined,
28
+ actions: ElementMeta[],
29
+ renderActionLike: RenderActionLike,
30
+ ): React.ReactNode {
31
+ const rowVisibleSet = new Set((rowRecord?.['_visibleActions'] as string[] | undefined) ?? [])
32
+ const rowDisabledSet = new Set((rowRecord?.['_disabledActions'] as string[] | undefined) ?? [])
33
+
34
+ const visible = actions.filter(a => {
35
+ if (!a['conditional']) return true
36
+ return rowVisibleSet.has(String(a['name'] ?? ''))
37
+ })
38
+
39
+ const decorate = (a: ElementMeta): ElementMeta => {
40
+ const name = String(a['name'] ?? '')
41
+ if (rowDisabledSet.has(name)) {
42
+ return { ...a, disabled: true }
43
+ }
44
+ return a
45
+ }
46
+
47
+ return (
48
+ <div className="flex items-center justify-end gap-1">
49
+ {visible.map((a, i) => renderActionLike(decorate(a), i, { ids: [rowId], size: 'sm' }))}
50
+ </div>
51
+ )
52
+ }
@@ -0,0 +1,143 @@
1
+ import React from 'react'
2
+ import type { NavigateFn } from '../../navigate.js'
3
+
4
+ // ─── Table URL helpers ──────────────────────────────────────
5
+ //
6
+ // The table renderer mirrors its current sort / search / page / group
7
+ // state to the URL query string. These helpers build / parse / dedupe
8
+ // that slice without dragging the server-side dispatcher into the
9
+ // client bundle.
10
+
11
+ export interface TableUrlState {
12
+ search?: string
13
+ sort?: { column: string; direction: 'asc' | 'desc' }
14
+ page?: number
15
+ /** Active group column for `?group=`. Empty string means an explicit
16
+ * "no grouping" override (set on the URL when the user picks "None"
17
+ * in the dropdown to override `defaultGroup`); `undefined` omits the
18
+ * key entirely so the configured default takes over. */
19
+ group?: string
20
+ /** Drilled-in group key for `?groupKey=`. `undefined` omits — the
21
+ * heading is banded (or no group at all); empty string explicitly
22
+ * clears (used by the chip's × so a stale URL value doesn't return
23
+ * via foreign-param round-trip). */
24
+ groupKey?: string
25
+ }
26
+
27
+ // Mirror of `prefixedKey` in `elements/dispatchTable.ts`. Kept inline so
28
+ // SchemaRenderer doesn't drag the server-side dispatcher into the client
29
+ // bundle.
30
+ export function prefixK(prefix: string | undefined, key: string): string {
31
+ return prefix === undefined || prefix === '' ? key : `${prefix}_${key}`
32
+ }
33
+
34
+ let cachedSearchString: string | null = null
35
+ let cachedSearchParams: URLSearchParams | null = null
36
+
37
+ export function getCurrentSearchParams(): URLSearchParams | null {
38
+ if (typeof window === 'undefined') return null
39
+ const s = window.location.search
40
+ if (s === cachedSearchString && cachedSearchParams) return cachedSearchParams
41
+ cachedSearchString = s
42
+ cachedSearchParams = new URLSearchParams(s)
43
+ return cachedSearchParams
44
+ }
45
+
46
+ export function SearchFormHiddenInputs({ prefix }: { prefix: string | undefined }): React.ReactElement {
47
+ const sp = getCurrentSearchParams()
48
+ if (!sp) return <></>
49
+ const searchKey = prefixK(prefix, 'search')
50
+ const pageKey = prefixK(prefix, 'page')
51
+ const inputs: React.ReactElement[] = []
52
+ let i = 0
53
+ for (const [k, v] of sp) {
54
+ if (k === searchKey || k === pageKey) continue
55
+ inputs.push(<input key={i++} type="hidden" name={k} value={v} />)
56
+ }
57
+ return <>{inputs}</>
58
+ }
59
+
60
+ export function buildTableQuery(
61
+ state: TableUrlState,
62
+ override: TableUrlState,
63
+ pathname: string,
64
+ filterValues: Record<string, string> = {},
65
+ prefix?: string,
66
+ ): string {
67
+ const merged: TableUrlState = { ...state, ...override }
68
+ const params = new URLSearchParams()
69
+ // Foreign URL params (other tables' state, app-level params) round-trip
70
+ // verbatim so this builder only ever rewrites its own slice.
71
+ const currentParams = getCurrentSearchParams()
72
+ if (currentParams) {
73
+ const ours = new Set([
74
+ prefixK(prefix, 'search'),
75
+ prefixK(prefix, 'sort'),
76
+ prefixK(prefix, 'page'),
77
+ prefixK(prefix, 'perPage'),
78
+ prefixK(prefix, 'group'),
79
+ prefixK(prefix, 'groupKey'),
80
+ ...Object.keys(filterValues).map(n => prefixK(prefix, n)),
81
+ ])
82
+ for (const [k, v] of currentParams) {
83
+ if (ours.has(k)) continue
84
+ params.set(k, v)
85
+ }
86
+ }
87
+ // Carry forward active filter values so sort/pagination links don't
88
+ // accidentally clear them. Filter names can't collide with reserved
89
+ // keys (search/sort/page/perPage/group) — that's enforced upstream.
90
+ for (const [name, val] of Object.entries(filterValues)) {
91
+ if (val) params.set(prefixK(prefix, name), val)
92
+ }
93
+ if (merged.search) params.set(prefixK(prefix, 'search'), merged.search)
94
+ if (merged.sort) params.set(prefixK(prefix, 'sort'), `${merged.sort.column}:${merged.sort.direction}`)
95
+ if (merged.page && merged.page > 1) params.set(prefixK(prefix, 'page'), String(merged.page))
96
+ if (merged.group !== undefined) params.set(prefixK(prefix, 'group'), merged.group)
97
+ // groupKey is sparse — only writes when the override sets a non-empty
98
+ // value. Drill-out (chip ×) passes `''` to clear; the foreign-param
99
+ // dedupe set above already filtered the stale value out, so an empty
100
+ // override produces a URL without the key.
101
+ if (merged.groupKey) params.set(prefixK(prefix, 'groupKey'), merged.groupKey)
102
+ const qs = params.toString()
103
+ // Always anchor to a real pathname — Vike's client-side router treats
104
+ // a bare `?qs` href as a fresh URL with empty pathname, which routes
105
+ // to the dashboard and blanks the page during SPA navigation.
106
+ const base = pathname || (typeof window !== 'undefined' ? window.location.pathname : '')
107
+ return qs ? `${base}?${qs}` : (base || '#')
108
+ }
109
+
110
+ /**
111
+ * SPA-navigate to the current URL with the filter slice patched in
112
+ * place. `null` or empty-string values delete the key; non-empty values
113
+ * set it. The accompanying `?page` is always cleared so users land on
114
+ * the first page of the relaxed / tightened set. No-op on SSR.
115
+ *
116
+ * Used by every filter widget's "apply" / "clear" path (FilterSelect /
117
+ * MultiSelect / DateRange / Form / QueryBuilder + ActiveFiltersBar).
118
+ */
119
+ export function patchFilterUrl(
120
+ navigate: NavigateFn,
121
+ prefix: string | undefined,
122
+ patches: Record<string, string | null>,
123
+ ): void {
124
+ if (typeof window === 'undefined') return
125
+ const url = new URL(window.location.href)
126
+ for (const [name, value] of Object.entries(patches)) {
127
+ const k = prefixK(prefix, name)
128
+ if (value === null || value === '') url.searchParams.delete(k)
129
+ else url.searchParams.set(k, value)
130
+ }
131
+ url.searchParams.delete(prefixK(prefix, 'page'))
132
+ void navigate(url.pathname + url.search)
133
+ }
134
+
135
+ export function nextSortDir(
136
+ current: TableUrlState['sort'],
137
+ column: string,
138
+ ): { column: string; direction: 'asc' | 'desc' } {
139
+ if (current?.column === column) {
140
+ return { column, direction: current.direction === 'asc' ? 'desc' : 'asc' }
141
+ }
142
+ return { column, direction: 'asc' }
143
+ }