@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,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,245 @@
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 { useCollabRoom } from '../../CollabRoomContext.js'
6
+ import { getFormCollabBinding } from '../../FormCollabBindingRegistry.js'
7
+ import { useNavigate } from '../../navigate.js'
8
+ import { useToast } from '../../Toaster.js'
9
+ import { renderField } from './renderField.js'
10
+
11
+ // ─── Form ───────────────────────────────────────────────────
12
+
13
+ type RenderElement = (el: ElementMeta, index: number) => React.ReactNode
14
+
15
+ /**
16
+ * Top-level `<form>` element. Owns:
17
+ * - HTML form chrome (action, method, _method spoof, hidden formId)
18
+ * - Fetch + JSON submission with `Accept: application/json` (so the
19
+ * server can return 422 with field errors instead of re-rendering)
20
+ * - Inline error stamping (`_form` banner + per-field error strings)
21
+ * - `FormStateProvider` mount when the form has any reactive field
22
+ * (`live()` or `afterStateUpdatedJs`) and a `stateUrl` was stamped.
23
+ *
24
+ * `renderElement` is injected for non-field children inside the form
25
+ * body (cards / dividers / fieldsets / etc).
26
+ */
27
+ export function FormRenderer({
28
+ el,
29
+ renderElement,
30
+ }: {
31
+ el: ElementMeta
32
+ renderElement: RenderElement
33
+ }) {
34
+ const formId = String(el['formId'] ?? '')
35
+ const method = String(el['method'] ?? 'post').toLowerCase()
36
+ const action = el['action'] ? String(el['action']) : undefined
37
+ const stateUrl = el['stateUrl'] ? String(el['stateUrl']) : undefined
38
+ const serverValues = (el['values'] as Record<string, unknown> | undefined) ?? {}
39
+ const serverErrors = (el['errors'] as Record<string, string[]> | undefined) ?? {}
40
+
41
+ // Phase F2 — the controlled-form path also activates when we're
42
+ // inside a `<RecordCollabRoom>` AND a plugin (e.g.
43
+ // `@pilotiq-pro/collab`) registered a `FormCollabBinding` factory.
44
+ // Without this, forms with no `live()` fields stayed uncontrolled
45
+ // and the collab binding never wired up.
46
+ const collabRoom = useCollabRoom()
47
+ const collabFactory = getFormCollabBinding()
48
+ const collabActive = !!(collabRoom && collabFactory && formId)
49
+ const useControlled = !!stateUrl || collabActive
50
+
51
+ // Methods other than GET/POST are spoofed via _method, mirroring Laravel.
52
+ const httpMethod = method === 'get' ? 'get' : 'post'
53
+ const spoofedMethod = method !== 'get' && method !== 'post' ? method : undefined
54
+
55
+ const navigate = useNavigate()
56
+ const { notify } = useToast()
57
+
58
+ // Client-side errors override server-rendered ones after a fetch-mode
59
+ // 422 response. Field values stay uncontrolled — the inputs in the DOM
60
+ // still hold whatever the user typed, so we don't need to mirror them.
61
+ const [clientErrors, setClientErrors] = useState<Record<string, string[]> | null>(null)
62
+ const [submitting, setSubmitting] = useState(false)
63
+ const errors = clientErrors ?? serverErrors
64
+
65
+ // Plan #14 — formRef is threaded into FormStateProvider so live triggers
66
+ // can snapshot the form's full DOM state via FormData (captures
67
+ // uncontrolled inner-Repeater inputs that don't participate in the
68
+ // controlled values map).
69
+ const formRef = useRef<HTMLFormElement | null>(null)
70
+
71
+ const formErrors = errors['_form'] ?? []
72
+ const hasFieldErrors = Object.keys(errors).some(k => k !== '_form')
73
+
74
+ const onSubmit = async (e: React.FormEvent<HTMLFormElement>): Promise<void> => {
75
+ if (!action) return // no action URL → fall through to native submit
76
+ e.preventDefault()
77
+ if (submitting) return
78
+ setSubmitting(true)
79
+ setClientErrors(null)
80
+
81
+ try {
82
+ // Thread `event.submitter` so the clicked submit button's
83
+ // name/value pair lands in the FormData. Without this, secondary
84
+ // submits like "Create & create another" can't signal which
85
+ // button fired through the body. Supported in all evergreen
86
+ // browsers since 2022; cast through `as any` because TS lib.dom
87
+ // hasn't picked up the optional submitter argument on every
88
+ // version.
89
+ const submitter = (e.nativeEvent as SubmitEvent).submitter as HTMLElement | null
90
+ const fd = new (FormData as any)(e.currentTarget, submitter ?? undefined) as FormData
91
+ const res = await fetch(action, {
92
+ method: 'POST',
93
+ headers: { 'Accept': 'application/json' },
94
+ body: fd,
95
+ })
96
+ const data = await res.json().catch(() => ({}))
97
+
98
+ if (res.status === 422) {
99
+ const next = (data as { errors?: Record<string, string[]> }).errors ?? {}
100
+ setClientErrors(next)
101
+ // Surface a banner-level message if no field errors were returned
102
+ // — the form-level _form key lights up the existing banner.
103
+ setSubmitting(false)
104
+ return
105
+ }
106
+ if (!res.ok) {
107
+ const message = String((data as { error?: string }).error ?? `Request failed (${res.status})`)
108
+ notify({ type: 'error', title: 'Save failed', body: message })
109
+ setSubmitting(false)
110
+ return
111
+ }
112
+
113
+ // Success — drain notifications and SPA-navigate to the redirect.
114
+ const notifs = (data as { notifications?: NotificationMeta[] }).notifications
115
+ if (notifs && notifs.length > 0) for (const n of notifs) notify(n)
116
+ const redirect = String((data as { redirect?: string }).redirect ?? '')
117
+ // The server may force a navigate even when the redirect equals
118
+ // the current URL — used by "Create & create another" so the
119
+ // form remounts with empty defaults instead of preserving the
120
+ // just-submitted values. Otherwise: skip navigate when the
121
+ // redirect matches the current URL, since re-fetching the same
122
+ // page would force a form remount and reset scroll.
123
+ const force = Boolean((data as { force?: boolean }).force)
124
+ const currentUrl = typeof window !== 'undefined'
125
+ ? window.location.pathname + window.location.search
126
+ : ''
127
+ if (redirect && (force || redirect !== currentUrl)) {
128
+ navigate(redirect)
129
+ // Don't reset submitting on success — the navigation will unmount us.
130
+ } else {
131
+ setSubmitting(false)
132
+ }
133
+ } catch (err) {
134
+ notify({ type: 'error', title: 'Save failed', body: err instanceof Error ? err.message : String(err) })
135
+ setSubmitting(false)
136
+ }
137
+ }
138
+
139
+ return (
140
+ <form
141
+ ref={formRef}
142
+ id={formId || undefined}
143
+ data-form-id={formId || undefined}
144
+ method={httpMethod}
145
+ action={action}
146
+ onSubmit={onSubmit}
147
+ className="flex flex-col gap-6"
148
+ >
149
+ {formId && <input type="hidden" name="_formId" value={formId} />}
150
+ {spoofedMethod && <input type="hidden" name="_method" value={spoofedMethod} />}
151
+ {(formErrors.length > 0 || hasFieldErrors) && (
152
+ <div className="rounded-lg border border-destructive/40 bg-destructive/5 text-destructive p-3 text-sm">
153
+ {formErrors.length > 0 ? (
154
+ <ul className="list-disc pl-4">
155
+ {formErrors.map((msg, i) => <li key={i}>{msg}</li>)}
156
+ </ul>
157
+ ) : (
158
+ 'Please correct the errors below.'
159
+ )}
160
+ </div>
161
+ )}
162
+ <FormIdContext.Provider value={formId}>
163
+ {useControlled ? (
164
+ <FormStateProvider initialMeta={el} initialErrors={errors} formRef={formRef}>
165
+ <FormBody
166
+ fallbackChildren={el.children ?? []}
167
+ fallbackValues={serverValues}
168
+ fallbackErrors={errors}
169
+ renderElement={renderElement}
170
+ />
171
+ </FormStateProvider>
172
+ ) : (
173
+ (el.children ?? []).map((child, i) => renderFormChild(child, i, serverValues, errors, renderElement))
174
+ )}
175
+ </FormIdContext.Provider>
176
+ </form>
177
+ )
178
+ }
179
+
180
+ /**
181
+ * Renders the controlled-form's children, sourcing them from the
182
+ * `FormStateProvider`'s current `formMeta` (which gets replaced after
183
+ * each live POST). Falls back to the props if (somehow) used outside a
184
+ * provider — the shell only mounts this when `stateUrl` is set so the
185
+ * fallback path is dead code in practice, but keeping it defensive.
186
+ */
187
+ function FormBody({
188
+ fallbackChildren, fallbackValues, fallbackErrors, renderElement,
189
+ }: {
190
+ fallbackChildren: ElementMeta[]
191
+ fallbackValues: Record<string, unknown>
192
+ fallbackErrors: Record<string, string[]>
193
+ renderElement: RenderElement
194
+ }): React.ReactElement {
195
+ const ctx = useFormState()
196
+ if (!ctx) {
197
+ return <>{fallbackChildren.map((child, i) => renderFormChild(child, i, fallbackValues, fallbackErrors, renderElement))}</>
198
+ }
199
+ const children = (ctx.formMeta.children ?? []) as ElementMeta[]
200
+ return <>{children.map((child, i) => renderFormChild(child, i, ctx.values, ctx.errors, renderElement))}</>
201
+ }
202
+
203
+ /**
204
+ * Render one child of a form's resolved schema with per-field values + errors.
205
+ *
206
+ * Field elements wrap in error-stamp chrome; non-field children fall
207
+ * through to `renderElement` so the form body can host cards / dividers /
208
+ * fieldsets / etc.
209
+ */
210
+ export function renderFormChild(
211
+ child: ElementMeta,
212
+ index: number,
213
+ values: Record<string, unknown>,
214
+ errors: Record<string, string[]>,
215
+ renderElement: RenderElement,
216
+ ): React.ReactNode {
217
+ if (child.type === 'field') {
218
+ const name = String(child['name'] ?? '')
219
+ const fieldErrors = errors[name] ?? []
220
+ const value = values[name]
221
+ return (
222
+ <div key={index} className="flex flex-col gap-1">
223
+ {renderFieldWithValue(child, index, value, renderElement)}
224
+ {fieldErrors.map((msg, i) => (
225
+ <p key={i} className="text-xs text-destructive">{msg}</p>
226
+ ))}
227
+ </div>
228
+ )
229
+ }
230
+ return renderElement(child, index)
231
+ }
232
+
233
+ function renderFieldWithValue(
234
+ el: ElementMeta,
235
+ index: number,
236
+ value: unknown,
237
+ renderElement: RenderElement,
238
+ ): React.ReactNode {
239
+ // The form-state value (from `withValues` / record-fill) wins when present;
240
+ // otherwise the meta's own `defaultValue` (Plan #6 `Field.default()`) survives.
241
+ const enriched: ElementMeta = value !== undefined
242
+ ? { ...el, defaultValue: value }
243
+ : el
244
+ return renderField(enriched, index, renderElement)
245
+ }