@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,245 @@
1
+ import React from 'react'
2
+ import type { ElementMeta } from '../../../schema/Element.js'
3
+ import { withTooltip } from '../helpers.js'
4
+ import {
5
+ actionButtonClass,
6
+ renderActionBadge,
7
+ renderActionIcon,
8
+ type RenderActionOptions,
9
+ } from './buttons.js'
10
+ import { ActionGroupTrigger } from './ActionGroupTrigger.js'
11
+ import { ActionModalDialog } from './ActionModalDialog.js'
12
+ import { ConfirmActionDialog } from './ConfirmActionDialog.js'
13
+ import { HandlerActionButton } from './HandlerActionButton.js'
14
+ import { MethodActionButton } from './MethodActionButton.js'
15
+
16
+ /**
17
+ * Function-prop deps for action rendering — injected from
18
+ * `SchemaRenderer.tsx`'s top-level dispatch. `renderElement` is needed
19
+ * for slot-component pass-through and modal-content-footer; `renderFormChild`
20
+ * is needed by `ActionModalDialog` for its form-body fields.
21
+ */
22
+ export interface ActionRendererDeps {
23
+ renderElement: (el: ElementMeta, index: number) => React.ReactNode
24
+ renderFormChild: (child: ElementMeta, index: number, values: Record<string, unknown>, errors: Record<string, string[]>) => React.ReactNode
25
+ }
26
+
27
+ /** Render either a single Action or an ActionGroup based on `el.type`.
28
+ * Used by callsites that accept both (table header / bulk toolbars,
29
+ * heading actions, container schemas). */
30
+ export function renderActionLike(
31
+ el: ElementMeta,
32
+ index: number,
33
+ opts: RenderActionOptions,
34
+ deps: ActionRendererDeps,
35
+ ): React.ReactNode {
36
+ if (el.type === 'slotComponent') {
37
+ // Plugin-contributed React mount — render through the main element
38
+ // dispatcher, which looks up the registered component and forwards
39
+ // its serialised props bag. Keeps every action-row slot (heading
40
+ // children, alert footer, empty-state footer, table-toolbar bulk
41
+ // strip) usable as a plugin extension point.
42
+ return deps.renderElement(el, index)
43
+ }
44
+ if (el.type === 'actionGroup') {
45
+ return (
46
+ <ActionGroupTrigger
47
+ key={index}
48
+ el={el}
49
+ ids={opts.ids ?? []}
50
+ renderFormChild={deps.renderFormChild}
51
+ renderElement={deps.renderElement}
52
+ />
53
+ )
54
+ }
55
+ return renderAction(el, index, opts, deps)
56
+ }
57
+
58
+ /** Render a single `Action` element. Dispatches on the Action's mode
59
+ * (submit / href / method / handler) and chrome (confirm, modal). */
60
+ export function renderAction(
61
+ el: ElementMeta,
62
+ index: number,
63
+ opts: RenderActionOptions,
64
+ deps: ActionRendererDeps,
65
+ ): React.ReactNode {
66
+ const name = String(el['name'] ?? '')
67
+ const label = String(el['label'] ?? name)
68
+ const destructive = Boolean(el['destructive'])
69
+ const href = el['href'] as string | undefined
70
+ const method = el['method'] as 'post' | 'put' | 'patch' | 'delete' | undefined
71
+ const actionUrl = el['action'] as string | undefined
72
+ const dispatchUrl = el['dispatchUrl'] as string | undefined
73
+ const submit = Boolean(el['submit'])
74
+ const confirm = el['confirm'] as { title?: string; message: string } | undefined
75
+ const tooltip = el['tooltip'] as string | undefined
76
+ const iconOnly = Boolean(el['iconOnly'])
77
+ const isDisabled = Boolean(el['disabled'])
78
+
79
+ const className = actionButtonClass(el, opts) + (isDisabled ? ' opacity-50 cursor-not-allowed pointer-events-none' : '')
80
+ const icon = renderActionIcon(el)
81
+ const badge = renderActionBadge(el)
82
+ // Icon-only buttons hide the label visually but expose it via aria-label.
83
+ const ariaLabel = iconOnly ? label : undefined
84
+ const inner = iconOnly ? <>{icon}{badge}</> : <>{icon}<span>{label}</span>{badge}</>
85
+
86
+ // Submit-style action — renders as <button type="submit">. Optionally
87
+ // targets a specific form via the HTML `form="<id>"` attribute so the
88
+ // button can submit a form it lives outside of (e.g. a page-header
89
+ // Save button driving a form below). When `formField` is set, the
90
+ // button posts a sentinel name/value pair (e.g. `_continueCreate=1`)
91
+ // so the server can branch on which submit was clicked.
92
+ if (submit) {
93
+ const formTarget = el['form'] as string | undefined
94
+ const formField = el['formField'] as { name: string; value: string } | undefined
95
+ if (confirm) {
96
+ // Confirm-gated submit: render as type="button" so click opens the
97
+ // dialog instead of submitting; on confirm, programmatically submit
98
+ // the targeted form (or the closest enclosing form if no formTarget).
99
+ // `formField` is intentionally not threaded here — programmatic
100
+ // `requestSubmit()` has no submitter, so the name/value pair would
101
+ // be lost anyway. Pair `.confirm()` with a hidden input on the form
102
+ // if you need a sentinel under a confirm flow.
103
+ return (
104
+ <ConfirmActionDialog
105
+ key={index}
106
+ title={confirm.title}
107
+ message={confirm.message}
108
+ destructive={destructive}
109
+ onConfirm={() => {
110
+ if (typeof document === 'undefined') return
111
+ const form = formTarget
112
+ ? document.getElementById(formTarget) as HTMLFormElement | null
113
+ : document.querySelector<HTMLFormElement>('form')
114
+ form?.requestSubmit()
115
+ }}
116
+ trigger={(open) => withTooltip(
117
+ <button
118
+ type="button"
119
+ onClick={open}
120
+ className={className}
121
+ data-action-name={name}
122
+ aria-label={ariaLabel}
123
+ >
124
+ {inner}
125
+ </button>,
126
+ tooltip,
127
+ )}
128
+ />
129
+ )
130
+ }
131
+ return withTooltip(
132
+ <button
133
+ key={index}
134
+ type="submit"
135
+ form={formTarget}
136
+ className={className}
137
+ data-action-name={name}
138
+ aria-label={ariaLabel}
139
+ {...(formField ? { name: formField.name, value: formField.value } : {})}
140
+ >
141
+ {inner}
142
+ </button>,
143
+ tooltip,
144
+ )
145
+ }
146
+
147
+ // Substitute the `:id` placeholder with the current row id when this
148
+ // action is rendered in a row context. Lets row-level link/form actions
149
+ // ship a single template URL like `/admin/articles/:id/edit`.
150
+ const rowId = opts.ids?.length === 1 ? opts.ids[0]! : undefined
151
+ const resolveTemplate = (s: string | undefined): string | undefined =>
152
+ s && rowId ? s.replace(':id', rowId) : s
153
+
154
+ // Link-style action.
155
+ if (href) {
156
+ return withTooltip(
157
+ <a
158
+ key={index}
159
+ href={resolveTemplate(href)}
160
+ className={className}
161
+ data-action-name={name}
162
+ aria-label={ariaLabel}
163
+ >
164
+ {inner}
165
+ </a>,
166
+ tooltip,
167
+ )
168
+ }
169
+
170
+ // Form-style action (POST/PUT/PATCH/DELETE) — fetch + JSON, no full reload.
171
+ if (method) {
172
+ const resolvedUrl = resolveTemplate(actionUrl)
173
+ return (
174
+ <MethodActionButton
175
+ key={index}
176
+ url={resolvedUrl}
177
+ method={method}
178
+ confirm={confirm}
179
+ destructive={destructive}
180
+ className={className}
181
+ name={name}
182
+ ariaLabel={ariaLabel}
183
+ tooltip={tooltip}
184
+ inner={inner}
185
+ />
186
+ )
187
+ }
188
+
189
+ // Handler-style action — fetch + JSON dispatch with `ids[]` body.
190
+ if (dispatchUrl) {
191
+ const ids = opts.ids ?? []
192
+ const modal = el['modal']
193
+ if (confirm || modal) {
194
+ return (
195
+ <ActionModalDialog
196
+ key={index}
197
+ meta={el}
198
+ ids={ids}
199
+ trigger={(open) => withTooltip(
200
+ <button
201
+ type="button"
202
+ onClick={open}
203
+ className={className}
204
+ data-action-name={name}
205
+ aria-label={ariaLabel}
206
+ >
207
+ {inner}
208
+ </button>,
209
+ tooltip,
210
+ )}
211
+ renderFormChild={deps.renderFormChild}
212
+ renderElement={deps.renderElement}
213
+ />
214
+ )
215
+ }
216
+ return (
217
+ <HandlerActionButton
218
+ key={index}
219
+ url={dispatchUrl}
220
+ ids={ids}
221
+ className={className}
222
+ name={name}
223
+ ariaLabel={ariaLabel}
224
+ tooltip={tooltip}
225
+ inner={inner}
226
+ />
227
+ )
228
+ }
229
+
230
+ // No dispatch wired (no href / method / dispatchUrl). Render a disabled
231
+ // placeholder so the user sees the button, but it does nothing.
232
+ return withTooltip(
233
+ <button
234
+ key={index}
235
+ type="button"
236
+ disabled
237
+ className={className + ' opacity-50 cursor-not-allowed'}
238
+ data-action-name={name}
239
+ aria-label={ariaLabel}
240
+ >
241
+ {inner}
242
+ </button>,
243
+ tooltip,
244
+ )
245
+ }
@@ -0,0 +1,65 @@
1
+ /** Apply a built-in `ColumnFormat` to a raw value; returns a string.
2
+ *
3
+ * Used by both `formatCell` (table layer) and `renderEntry` (infolist
4
+ * Phase #16). Hoisted out of the table block so EntryRenderer doesn't
5
+ * need to import across phases. */
6
+ export function applyColumnFormat(value: unknown, format: { kind: string; [k: string]: unknown }): string {
7
+ if (value === null || value === undefined || value === '') return ''
8
+ switch (format['kind']) {
9
+ case 'dateTime': {
10
+ const d = value instanceof Date ? value : new Date(String(value))
11
+ if (isNaN(d.getTime())) return String(value)
12
+ // Default — locale-aware short date+time. Custom patterns aren't
13
+ // supported (no date-fns dep); pattern is kept on meta for future use.
14
+ return d.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })
15
+ }
16
+ case 'since': {
17
+ const d = value instanceof Date ? value : new Date(String(value))
18
+ if (isNaN(d.getTime())) return String(value)
19
+ const seconds = Math.round((Date.now() - d.getTime()) / 1000)
20
+ const abs = Math.abs(seconds)
21
+ const past = seconds >= 0
22
+ const fmt = (n: number, unit: string): string =>
23
+ past ? `${n} ${unit}${n === 1 ? '' : 's'} ago` : `in ${n} ${unit}${n === 1 ? '' : 's'}`
24
+ if (abs < 60) return past ? 'just now' : 'in a moment'
25
+ if (abs < 3600) return fmt(Math.floor(abs / 60), 'minute')
26
+ if (abs < 86400) return fmt(Math.floor(abs / 3600), 'hour')
27
+ if (abs < 2592000) return fmt(Math.floor(abs / 86400), 'day')
28
+ if (abs < 31536000) return fmt(Math.floor(abs / 2592000), 'month')
29
+ return fmt(Math.floor(abs / 31536000), 'year')
30
+ }
31
+ case 'money': {
32
+ const n = typeof value === 'number' ? value : Number(value)
33
+ if (isNaN(n)) return String(value)
34
+ const currency = String(format['currency'] ?? 'USD')
35
+ const locale = format['locale'] as string | undefined
36
+ return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(n)
37
+ }
38
+ case 'numeric': {
39
+ const n = typeof value === 'number' ? value : Number(value)
40
+ if (isNaN(n)) return String(value)
41
+ const decimals = format['decimals'] as number | undefined
42
+ const locale = format['locale'] as string | undefined
43
+ const opts: Intl.NumberFormatOptions = {}
44
+ if (decimals !== undefined) {
45
+ opts.minimumFractionDigits = decimals
46
+ opts.maximumFractionDigits = decimals
47
+ }
48
+ return new Intl.NumberFormat(locale, opts).format(n)
49
+ }
50
+ case 'limit': {
51
+ const s = String(value)
52
+ const n = format['chars'] as number
53
+ return s.length > n ? s.slice(0, n) + '…' : s
54
+ }
55
+ case 'words': {
56
+ const s = String(value).trim()
57
+ if (s.length === 0) return s
58
+ const tokens = s.split(/\s+/)
59
+ const n = format['words'] as number
60
+ return tokens.length > n ? tokens.slice(0, n).join(' ') + '…' : s
61
+ }
62
+ default:
63
+ return String(value)
64
+ }
65
+ }
@@ -0,0 +1,50 @@
1
+ // Tailwind-class lookup tables shared across SchemaRenderer's element
2
+ // dispatchers. Pure data — no React, no DOM.
3
+
4
+ export const TEXT_COLOR_CLASSES: Record<string, string> = {
5
+ default: '',
6
+ muted: 'text-muted-foreground',
7
+ primary: 'text-primary',
8
+ destructive: 'text-destructive',
9
+ success: 'text-emerald-600 dark:text-emerald-400',
10
+ warning: 'text-amber-600 dark:text-amber-400',
11
+ info: 'text-blue-600 dark:text-blue-400',
12
+ }
13
+
14
+ export const TEXT_SIZE_CLASSES: Record<string, string> = {
15
+ xs: 'text-xs',
16
+ sm: 'text-sm',
17
+ base: 'text-base',
18
+ lg: 'text-lg',
19
+ xl: 'text-xl',
20
+ }
21
+
22
+ export const TEXT_WEIGHT_CLASSES: Record<string, string> = {
23
+ normal: 'font-normal',
24
+ medium: 'font-medium',
25
+ semibold: 'font-semibold',
26
+ bold: 'font-bold',
27
+ }
28
+
29
+ // `ColumnColor` / `ColumnWeight` mirror the text-side scales 1:1. Kept as
30
+ // named aliases so column-cell call sites still read distinctly, but a
31
+ // single source-of-truth means adding a new color / weight only touches
32
+ // the text map.
33
+ export const COLUMN_COLOR_CLASSES = TEXT_COLOR_CLASSES
34
+ export const COLUMN_WEIGHT_CLASSES = TEXT_WEIGHT_CLASSES
35
+
36
+ export const BADGE_COLOR_CLASSES: Record<string, string> = {
37
+ gray: 'bg-muted text-muted-foreground',
38
+ primary: 'bg-primary/10 text-primary',
39
+ success: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-200',
40
+ warning: 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-200',
41
+ destructive: 'bg-destructive/10 text-destructive',
42
+ info: 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-200',
43
+ }
44
+
45
+ export const alertStyles: Record<string, string> = {
46
+ info: 'border-blue-200 bg-blue-50 text-blue-800 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-200',
47
+ warning: 'border-amber-200 bg-amber-50 text-amber-800 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-200',
48
+ success: 'border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-200',
49
+ danger: 'border-red-200 bg-red-50 text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-200',
50
+ }
@@ -0,0 +1,233 @@
1
+ import React, { useRef, useState } from 'react'
2
+ import type { ElementMeta } from '../../../schema/Element.js'
3
+ import type { NotificationMeta } from '../../../notifications/Notification.js'
4
+ import { FormIdContext, FormStateProvider, useFormState } from '../../FormStateContext.js'
5
+ import { useNavigate } from '../../navigate.js'
6
+ import { useToast } from '../../Toaster.js'
7
+ import { renderField } from './renderField.js'
8
+
9
+ // ─── Form ───────────────────────────────────────────────────
10
+
11
+ type RenderElement = (el: ElementMeta, index: number) => React.ReactNode
12
+
13
+ /**
14
+ * Top-level `<form>` element. Owns:
15
+ * - HTML form chrome (action, method, _method spoof, hidden formId)
16
+ * - Fetch + JSON submission with `Accept: application/json` (so the
17
+ * server can return 422 with field errors instead of re-rendering)
18
+ * - Inline error stamping (`_form` banner + per-field error strings)
19
+ * - `FormStateProvider` mount when the form has any reactive field
20
+ * (`live()` or `afterStateUpdatedJs`) and a `stateUrl` was stamped.
21
+ *
22
+ * `renderElement` is injected for non-field children inside the form
23
+ * body (cards / dividers / fieldsets / etc).
24
+ */
25
+ export function FormRenderer({
26
+ el,
27
+ renderElement,
28
+ }: {
29
+ el: ElementMeta
30
+ renderElement: RenderElement
31
+ }) {
32
+ const formId = String(el['formId'] ?? '')
33
+ const method = String(el['method'] ?? 'post').toLowerCase()
34
+ const action = el['action'] ? String(el['action']) : undefined
35
+ const stateUrl = el['stateUrl'] ? String(el['stateUrl']) : undefined
36
+ const serverValues = (el['values'] as Record<string, unknown> | undefined) ?? {}
37
+ const serverErrors = (el['errors'] as Record<string, string[]> | undefined) ?? {}
38
+
39
+ // Methods other than GET/POST are spoofed via _method, mirroring Laravel.
40
+ const httpMethod = method === 'get' ? 'get' : 'post'
41
+ const spoofedMethod = method !== 'get' && method !== 'post' ? method : undefined
42
+
43
+ const navigate = useNavigate()
44
+ const { notify } = useToast()
45
+
46
+ // Client-side errors override server-rendered ones after a fetch-mode
47
+ // 422 response. Field values stay uncontrolled — the inputs in the DOM
48
+ // still hold whatever the user typed, so we don't need to mirror them.
49
+ const [clientErrors, setClientErrors] = useState<Record<string, string[]> | null>(null)
50
+ const [submitting, setSubmitting] = useState(false)
51
+ const errors = clientErrors ?? serverErrors
52
+
53
+ // Plan #14 — formRef is threaded into FormStateProvider so live triggers
54
+ // can snapshot the form's full DOM state via FormData (captures
55
+ // uncontrolled inner-Repeater inputs that don't participate in the
56
+ // controlled values map).
57
+ const formRef = useRef<HTMLFormElement | null>(null)
58
+
59
+ const formErrors = errors['_form'] ?? []
60
+ const hasFieldErrors = Object.keys(errors).some(k => k !== '_form')
61
+
62
+ const onSubmit = async (e: React.FormEvent<HTMLFormElement>): Promise<void> => {
63
+ if (!action) return // no action URL → fall through to native submit
64
+ e.preventDefault()
65
+ if (submitting) return
66
+ setSubmitting(true)
67
+ setClientErrors(null)
68
+
69
+ try {
70
+ // Thread `event.submitter` so the clicked submit button's
71
+ // name/value pair lands in the FormData. Without this, secondary
72
+ // submits like "Create & create another" can't signal which
73
+ // button fired through the body. Supported in all evergreen
74
+ // browsers since 2022; cast through `as any` because TS lib.dom
75
+ // hasn't picked up the optional submitter argument on every
76
+ // version.
77
+ const submitter = (e.nativeEvent as SubmitEvent).submitter as HTMLElement | null
78
+ const fd = new (FormData as any)(e.currentTarget, submitter ?? undefined) as FormData
79
+ const res = await fetch(action, {
80
+ method: 'POST',
81
+ headers: { 'Accept': 'application/json' },
82
+ body: fd,
83
+ })
84
+ const data = await res.json().catch(() => ({}))
85
+
86
+ if (res.status === 422) {
87
+ const next = (data as { errors?: Record<string, string[]> }).errors ?? {}
88
+ setClientErrors(next)
89
+ // Surface a banner-level message if no field errors were returned
90
+ // — the form-level _form key lights up the existing banner.
91
+ setSubmitting(false)
92
+ return
93
+ }
94
+ if (!res.ok) {
95
+ const message = String((data as { error?: string }).error ?? `Request failed (${res.status})`)
96
+ notify({ type: 'error', title: 'Save failed', body: message })
97
+ setSubmitting(false)
98
+ return
99
+ }
100
+
101
+ // Success — drain notifications and SPA-navigate to the redirect.
102
+ const notifs = (data as { notifications?: NotificationMeta[] }).notifications
103
+ if (notifs && notifs.length > 0) for (const n of notifs) notify(n)
104
+ const redirect = String((data as { redirect?: string }).redirect ?? '')
105
+ // The server may force a navigate even when the redirect equals
106
+ // the current URL — used by "Create & create another" so the
107
+ // form remounts with empty defaults instead of preserving the
108
+ // just-submitted values. Otherwise: skip navigate when the
109
+ // redirect matches the current URL, since re-fetching the same
110
+ // page would force a form remount and reset scroll.
111
+ const force = Boolean((data as { force?: boolean }).force)
112
+ const currentUrl = typeof window !== 'undefined'
113
+ ? window.location.pathname + window.location.search
114
+ : ''
115
+ if (redirect && (force || redirect !== currentUrl)) {
116
+ navigate(redirect)
117
+ // Don't reset submitting on success — the navigation will unmount us.
118
+ } else {
119
+ setSubmitting(false)
120
+ }
121
+ } catch (err) {
122
+ notify({ type: 'error', title: 'Save failed', body: err instanceof Error ? err.message : String(err) })
123
+ setSubmitting(false)
124
+ }
125
+ }
126
+
127
+ return (
128
+ <form
129
+ ref={formRef}
130
+ id={formId || undefined}
131
+ data-form-id={formId || undefined}
132
+ method={httpMethod}
133
+ action={action}
134
+ onSubmit={onSubmit}
135
+ className="flex flex-col gap-6"
136
+ >
137
+ {formId && <input type="hidden" name="_formId" value={formId} />}
138
+ {spoofedMethod && <input type="hidden" name="_method" value={spoofedMethod} />}
139
+ {(formErrors.length > 0 || hasFieldErrors) && (
140
+ <div className="rounded-lg border border-destructive/40 bg-destructive/5 text-destructive p-3 text-sm">
141
+ {formErrors.length > 0 ? (
142
+ <ul className="list-disc pl-4">
143
+ {formErrors.map((msg, i) => <li key={i}>{msg}</li>)}
144
+ </ul>
145
+ ) : (
146
+ 'Please correct the errors below.'
147
+ )}
148
+ </div>
149
+ )}
150
+ <FormIdContext.Provider value={formId}>
151
+ {stateUrl ? (
152
+ <FormStateProvider initialMeta={el} initialErrors={errors} formRef={formRef}>
153
+ <FormBody
154
+ fallbackChildren={el.children ?? []}
155
+ fallbackValues={serverValues}
156
+ fallbackErrors={errors}
157
+ renderElement={renderElement}
158
+ />
159
+ </FormStateProvider>
160
+ ) : (
161
+ (el.children ?? []).map((child, i) => renderFormChild(child, i, serverValues, errors, renderElement))
162
+ )}
163
+ </FormIdContext.Provider>
164
+ </form>
165
+ )
166
+ }
167
+
168
+ /**
169
+ * Renders the controlled-form's children, sourcing them from the
170
+ * `FormStateProvider`'s current `formMeta` (which gets replaced after
171
+ * each live POST). Falls back to the props if (somehow) used outside a
172
+ * provider — the shell only mounts this when `stateUrl` is set so the
173
+ * fallback path is dead code in practice, but keeping it defensive.
174
+ */
175
+ function FormBody({
176
+ fallbackChildren, fallbackValues, fallbackErrors, renderElement,
177
+ }: {
178
+ fallbackChildren: ElementMeta[]
179
+ fallbackValues: Record<string, unknown>
180
+ fallbackErrors: Record<string, string[]>
181
+ renderElement: RenderElement
182
+ }): React.ReactElement {
183
+ const ctx = useFormState()
184
+ if (!ctx) {
185
+ return <>{fallbackChildren.map((child, i) => renderFormChild(child, i, fallbackValues, fallbackErrors, renderElement))}</>
186
+ }
187
+ const children = (ctx.formMeta.children ?? []) as ElementMeta[]
188
+ return <>{children.map((child, i) => renderFormChild(child, i, ctx.values, ctx.errors, renderElement))}</>
189
+ }
190
+
191
+ /**
192
+ * Render one child of a form's resolved schema with per-field values + errors.
193
+ *
194
+ * Field elements wrap in error-stamp chrome; non-field children fall
195
+ * through to `renderElement` so the form body can host cards / dividers /
196
+ * fieldsets / etc.
197
+ */
198
+ export function renderFormChild(
199
+ child: ElementMeta,
200
+ index: number,
201
+ values: Record<string, unknown>,
202
+ errors: Record<string, string[]>,
203
+ renderElement: RenderElement,
204
+ ): React.ReactNode {
205
+ if (child.type === 'field') {
206
+ const name = String(child['name'] ?? '')
207
+ const fieldErrors = errors[name] ?? []
208
+ const value = values[name]
209
+ return (
210
+ <div key={index} className="flex flex-col gap-1">
211
+ {renderFieldWithValue(child, index, value, renderElement)}
212
+ {fieldErrors.map((msg, i) => (
213
+ <p key={i} className="text-xs text-destructive">{msg}</p>
214
+ ))}
215
+ </div>
216
+ )
217
+ }
218
+ return renderElement(child, index)
219
+ }
220
+
221
+ function renderFieldWithValue(
222
+ el: ElementMeta,
223
+ index: number,
224
+ value: unknown,
225
+ renderElement: RenderElement,
226
+ ): React.ReactNode {
227
+ // The form-state value (from `withValues` / record-fill) wins when present;
228
+ // otherwise the meta's own `defaultValue` (Plan #6 `Field.default()`) survives.
229
+ const enriched: ElementMeta = value !== undefined
230
+ ? { ...el, defaultValue: value }
231
+ : el
232
+ return renderField(enriched, index, renderElement)
233
+ }