@pilotiq/pilotiq 0.24.1 → 0.24.3

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 (518) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/boost/guidelines.md +571 -0
  3. package/boost/skills/pilotiq-actions/SKILL.md +49 -0
  4. package/boost/skills/pilotiq-actions/rules/dispatch-modes.md +177 -0
  5. package/boost/skills/pilotiq-actions/rules/factories.md +130 -0
  6. package/boost/skills/pilotiq-actions/rules/visibility-and-authorization.md +125 -0
  7. package/boost/skills/pilotiq-fields/SKILL.md +47 -0
  8. package/boost/skills/pilotiq-fields/rules/field-catalog.md +288 -0
  9. package/boost/skills/pilotiq-fields/rules/reactive-fields.md +199 -0
  10. package/boost/skills/pilotiq-fields/rules/validation.md +198 -0
  11. package/boost/skills/pilotiq-relations/SKILL.md +47 -0
  12. package/boost/skills/pilotiq-relations/rules/relation-managers.md +256 -0
  13. package/boost/skills/pilotiq-relations/rules/repeater-relationship.md +177 -0
  14. package/boost/skills/pilotiq-resource/SKILL.md +61 -0
  15. package/boost/skills/pilotiq-resource/rules/authorization.md +242 -0
  16. package/boost/skills/pilotiq-resource/rules/defining-resources.md +228 -0
  17. package/boost/skills/pilotiq-resource/rules/page-overrides.md +296 -0
  18. package/dist/Pilotiq.d.ts +31 -0
  19. package/dist/Pilotiq.d.ts.map +1 -1
  20. package/dist/Pilotiq.js +3 -1
  21. package/dist/Pilotiq.js.map +1 -1
  22. package/dist/PilotiqRegistry.d.ts +13 -0
  23. package/dist/PilotiqRegistry.d.ts.map +1 -1
  24. package/dist/PilotiqRegistry.js +15 -0
  25. package/dist/PilotiqRegistry.js.map +1 -1
  26. package/dist/pageData/misc.d.ts.map +1 -1
  27. package/dist/pageData/misc.js +6 -0
  28. package/dist/pageData/misc.js.map +1 -1
  29. package/dist/pageData/navigation.d.ts +1 -0
  30. package/dist/pageData/navigation.d.ts.map +1 -1
  31. package/dist/pageData/navigation.js +3 -0
  32. package/dist/pageData/navigation.js.map +1 -1
  33. package/dist/pageData/relationPages.d.ts.map +1 -1
  34. package/dist/pageData/relationPages.js +3 -0
  35. package/dist/pageData/relationPages.js.map +1 -1
  36. package/dist/pageData/resourcePages.d.ts.map +1 -1
  37. package/dist/pageData/resourcePages.js +8 -0
  38. package/dist/pageData/resourcePages.js.map +1 -1
  39. package/dist/react/AppShell.d.ts +8 -0
  40. package/dist/react/AppShell.d.ts.map +1 -1
  41. package/dist/react/AppShell.js.map +1 -1
  42. package/dist/react/layouts/SidebarLayout.d.ts.map +1 -1
  43. package/dist/react/layouts/SidebarLayout.js +10 -2
  44. package/dist/react/layouts/SidebarLayout.js.map +1 -1
  45. package/dist/react/widgets/StatsOverviewRenderer.d.ts.map +1 -1
  46. package/dist/react/widgets/StatsOverviewRenderer.js +32 -18
  47. package/dist/react/widgets/StatsOverviewRenderer.js.map +1 -1
  48. package/dist/routes/relations.d.ts.map +1 -1
  49. package/dist/routes/relations.js +25 -18
  50. package/dist/routes/relations.js.map +1 -1
  51. package/dist/routes/resources.js.map +1 -1
  52. package/package.json +10 -5
  53. package/.turbo/turbo-build.log +0 -8
  54. package/CLAUDE.md +0 -265
  55. package/src/Cluster.test.ts +0 -283
  56. package/src/Cluster.ts +0 -83
  57. package/src/Column.test.ts +0 -199
  58. package/src/Column.ts +0 -710
  59. package/src/Global.test.ts +0 -367
  60. package/src/Global.ts +0 -169
  61. package/src/Page.test.ts +0 -114
  62. package/src/Page.ts +0 -208
  63. package/src/Pilotiq.perf.test.ts +0 -252
  64. package/src/Pilotiq.test.ts +0 -129
  65. package/src/Pilotiq.ts +0 -1158
  66. package/src/PilotiqRegistry.ts +0 -36
  67. package/src/PilotiqServiceProvider.ts +0 -121
  68. package/src/RelationManager.test.ts +0 -400
  69. package/src/RelationManager.ts +0 -527
  70. package/src/RenderHook.test.ts +0 -252
  71. package/src/RenderHook.ts +0 -242
  72. package/src/Resource.test.ts +0 -284
  73. package/src/Resource.ts +0 -526
  74. package/src/RightPanel.test.ts +0 -202
  75. package/src/RightPanel.ts +0 -132
  76. package/src/Tab.test.ts +0 -91
  77. package/src/Tab.ts +0 -156
  78. package/src/UserMenuItem.ts +0 -145
  79. package/src/actions/Action.test.ts +0 -2526
  80. package/src/actions/Action.ts +0 -1515
  81. package/src/actions/ActionGroup.test.ts +0 -112
  82. package/src/actions/ActionGroup.ts +0 -173
  83. package/src/actions/attachFactory.ts +0 -172
  84. package/src/actions/bulkFactories.ts +0 -168
  85. package/src/actions/crudFactories.ts +0 -220
  86. package/src/actions/exportFactory.ts +0 -225
  87. package/src/actions/factoryHelpers.ts +0 -177
  88. package/src/actions/importFactory.ts +0 -243
  89. package/src/actions/index.ts +0 -17
  90. package/src/actions/m2mFactories.ts +0 -193
  91. package/src/actions/relationFactories.ts +0 -372
  92. package/src/applyPageHooks.test.ts +0 -463
  93. package/src/applyPageHooks.ts +0 -330
  94. package/src/authorization.test.ts +0 -483
  95. package/src/breadcrumbs.test.ts +0 -238
  96. package/src/cells/coerce.test.ts +0 -85
  97. package/src/cells/coerce.ts +0 -84
  98. package/src/clusterPaths.ts +0 -35
  99. package/src/columns/BadgeColumn.test.ts +0 -54
  100. package/src/columns/BadgeColumn.ts +0 -32
  101. package/src/columns/BooleanColumn.test.ts +0 -41
  102. package/src/columns/BooleanColumn.ts +0 -18
  103. package/src/columns/ColorColumn.test.ts +0 -37
  104. package/src/columns/ColorColumn.ts +0 -38
  105. package/src/columns/IconColumn.test.ts +0 -54
  106. package/src/columns/IconColumn.ts +0 -37
  107. package/src/columns/ImageColumn.test.ts +0 -41
  108. package/src/columns/ImageColumn.ts +0 -28
  109. package/src/columns/SelectColumn.ts +0 -98
  110. package/src/columns/TextColumn.test.ts +0 -190
  111. package/src/columns/TextColumn.ts +0 -20
  112. package/src/columns/TextInputColumn.ts +0 -68
  113. package/src/columns/ToggleColumn.ts +0 -46
  114. package/src/columns/editableColumns.test.ts +0 -238
  115. package/src/columns/index.ts +0 -9
  116. package/src/defaultGlobalPages.ts +0 -95
  117. package/src/defaultPages.test.ts +0 -634
  118. package/src/defaultPages.ts +0 -617
  119. package/src/defaultViewPage.test.ts +0 -147
  120. package/src/elements/Form.test.ts +0 -223
  121. package/src/elements/Form.ts +0 -416
  122. package/src/elements/ListTabs.ts +0 -28
  123. package/src/elements/Table.test.ts +0 -422
  124. package/src/elements/Table.ts +0 -850
  125. package/src/elements/TableGroup.test.ts +0 -260
  126. package/src/elements/TableGroup.ts +0 -334
  127. package/src/elements/dispatchAction.test.ts +0 -463
  128. package/src/elements/dispatchAction.ts +0 -355
  129. package/src/elements/dispatchForm.test.ts +0 -477
  130. package/src/elements/dispatchForm.ts +0 -1993
  131. package/src/elements/dispatchTable.test.ts +0 -1514
  132. package/src/elements/dispatchTable.ts +0 -745
  133. package/src/elements/index.ts +0 -21
  134. package/src/entries/BadgeEntry.ts +0 -39
  135. package/src/entries/CodeEntry.test.ts +0 -40
  136. package/src/entries/CodeEntry.ts +0 -52
  137. package/src/entries/ColorEntry.ts +0 -63
  138. package/src/entries/ComponentEntry.test.ts +0 -173
  139. package/src/entries/ComponentEntry.ts +0 -95
  140. package/src/entries/Entry.ts +0 -304
  141. package/src/entries/IconEntry.ts +0 -49
  142. package/src/entries/ImageEntry.ts +0 -61
  143. package/src/entries/KeyValueEntry.ts +0 -47
  144. package/src/entries/RepeatableEntry.test.ts +0 -239
  145. package/src/entries/RepeatableEntry.ts +0 -173
  146. package/src/entries/TextEntry.test.ts +0 -394
  147. package/src/entries/TextEntry.ts +0 -60
  148. package/src/entries/index.ts +0 -12
  149. package/src/entries/leaves.test.ts +0 -306
  150. package/src/entries/registry.ts +0 -54
  151. package/src/fields/BuilderField.test.ts +0 -1188
  152. package/src/fields/BuilderField.ts +0 -605
  153. package/src/fields/BuilderRelationship.test.ts +0 -811
  154. package/src/fields/CheckboxField.test.ts +0 -44
  155. package/src/fields/CheckboxField.ts +0 -27
  156. package/src/fields/CheckboxListField.test.ts +0 -99
  157. package/src/fields/CheckboxListField.ts +0 -66
  158. package/src/fields/ColorPickerField.test.ts +0 -33
  159. package/src/fields/ColorPickerField.ts +0 -25
  160. package/src/fields/DateField.ts +0 -54
  161. package/src/fields/DateTimeField.test.ts +0 -55
  162. package/src/fields/EmailField.ts +0 -16
  163. package/src/fields/Field.test.ts +0 -654
  164. package/src/fields/Field.ts +0 -817
  165. package/src/fields/FileUploadField.test.ts +0 -143
  166. package/src/fields/FileUploadField.ts +0 -159
  167. package/src/fields/HiddenField.test.ts +0 -27
  168. package/src/fields/HiddenField.ts +0 -28
  169. package/src/fields/KeyValueField.test.ts +0 -105
  170. package/src/fields/KeyValueField.ts +0 -55
  171. package/src/fields/MarkdownField.test.ts +0 -167
  172. package/src/fields/MarkdownField.ts +0 -162
  173. package/src/fields/NumberField.ts +0 -33
  174. package/src/fields/RadioField.test.ts +0 -94
  175. package/src/fields/RadioField.ts +0 -67
  176. package/src/fields/RepeaterField.test.ts +0 -1806
  177. package/src/fields/RepeaterField.ts +0 -939
  178. package/src/fields/RepeaterRelationship.test.ts +0 -1923
  179. package/src/fields/RepeaterSimple.test.ts +0 -248
  180. package/src/fields/RowButton.test.ts +0 -219
  181. package/src/fields/RowButton.ts +0 -135
  182. package/src/fields/SelectField.test.ts +0 -192
  183. package/src/fields/SelectField.ts +0 -235
  184. package/src/fields/SliderField.test.ts +0 -50
  185. package/src/fields/SliderField.ts +0 -53
  186. package/src/fields/SlugField.ts +0 -24
  187. package/src/fields/TagsInputField.test.ts +0 -154
  188. package/src/fields/TagsInputField.ts +0 -133
  189. package/src/fields/TextField.test.ts +0 -213
  190. package/src/fields/TextField.ts +0 -177
  191. package/src/fields/TextareaField.test.ts +0 -58
  192. package/src/fields/TextareaField.ts +0 -59
  193. package/src/fields/ToggleButtonsField.test.ts +0 -106
  194. package/src/fields/ToggleButtonsField.ts +0 -59
  195. package/src/fields/ToggleField.ts +0 -16
  196. package/src/fields/disableOptionsWhenSelectedInSiblingRepeaterItems.test.ts +0 -319
  197. package/src/fields/optionsResolver.ts +0 -95
  198. package/src/fields/resolveField.ts +0 -28
  199. package/src/filters/BooleanFilter.ts +0 -35
  200. package/src/filters/DateRangeFilter.test.ts +0 -194
  201. package/src/filters/DateRangeFilter.ts +0 -148
  202. package/src/filters/Filter.test.ts +0 -268
  203. package/src/filters/Filter.ts +0 -184
  204. package/src/filters/FormFilter.test.ts +0 -238
  205. package/src/filters/FormFilter.ts +0 -215
  206. package/src/filters/MultiSelectFilter.test.ts +0 -119
  207. package/src/filters/MultiSelectFilter.ts +0 -78
  208. package/src/filters/QueryBuilderFilter.test.ts +0 -662
  209. package/src/filters/QueryBuilderFilter.ts +0 -398
  210. package/src/filters/SelectFilter.ts +0 -46
  211. package/src/filters/TernaryFilter.test.ts +0 -160
  212. package/src/filters/TernaryFilter.ts +0 -72
  213. package/src/filters/TrashedFilter.test.ts +0 -149
  214. package/src/filters/TrashedFilter.ts +0 -55
  215. package/src/filters/queryBuilder/BooleanConstraint.ts +0 -31
  216. package/src/filters/queryBuilder/Constraint.ts +0 -115
  217. package/src/filters/queryBuilder/DateConstraint.ts +0 -69
  218. package/src/filters/queryBuilder/NumberConstraint.ts +0 -66
  219. package/src/filters/queryBuilder/SelectConstraint.ts +0 -72
  220. package/src/filters/queryBuilder/TextConstraint.ts +0 -64
  221. package/src/filters/queryBuilder/index.ts +0 -12
  222. package/src/icons/index.ts +0 -2
  223. package/src/icons/lucide.ts +0 -204
  224. package/src/icons/registry.test.ts +0 -56
  225. package/src/icons/registry.ts +0 -41
  226. package/src/icons/types.ts +0 -47
  227. package/src/index.ts +0 -525
  228. package/src/io/csv.test.ts +0 -142
  229. package/src/io/csv.ts +0 -170
  230. package/src/nestedRelationManagerData.test.ts +0 -547
  231. package/src/notifications/Notification.test.ts +0 -210
  232. package/src/notifications/Notification.ts +0 -354
  233. package/src/notifications/broadcast.test.ts +0 -110
  234. package/src/notifications/broadcast.ts +0 -95
  235. package/src/notifications/database.test.ts +0 -383
  236. package/src/notifications/database.ts +0 -398
  237. package/src/notifications/databaseNotifications.test.ts +0 -187
  238. package/src/notifications/dispatchNotificationAction.test.ts +0 -341
  239. package/src/notifications/dispatchNotificationAction.ts +0 -142
  240. package/src/notifications/flash.test.ts +0 -89
  241. package/src/notifications/flash.ts +0 -71
  242. package/src/notifications/index.ts +0 -45
  243. package/src/notifications/registerBroadcastAuth.test.ts +0 -134
  244. package/src/notifications/registerBroadcastAuth.ts +0 -100
  245. package/src/notifications/resolveSavedNotification.test.ts +0 -82
  246. package/src/notifications/resolveSavedNotification.ts +0 -59
  247. package/src/notifications/types.ts +0 -93
  248. package/src/orm/m2mAccessor.ts +0 -66
  249. package/src/orm/modelDefaults.test.ts +0 -633
  250. package/src/orm/modelDefaults.ts +0 -666
  251. package/src/pageData/breadcrumbs.ts +0 -288
  252. package/src/pageData/forms.ts +0 -578
  253. package/src/pageData/helpers.ts +0 -857
  254. package/src/pageData/misc.ts +0 -347
  255. package/src/pageData/navigation.ts +0 -842
  256. package/src/pageData/relationPages.ts +0 -1248
  257. package/src/pageData/relationTabs.ts +0 -286
  258. package/src/pageData/resourcePages.ts +0 -609
  259. package/src/pageData.test.ts +0 -1545
  260. package/src/pageData.ts +0 -341
  261. package/src/plugins/index.ts +0 -8
  262. package/src/plugins/themeEditor.test.ts +0 -36
  263. package/src/plugins/themeEditor.ts +0 -45
  264. package/src/react/AppShell.tsx +0 -251
  265. package/src/react/CollabExtensionFactoryRegistry.ts +0 -55
  266. package/src/react/CollabRoomContext.ts +0 -98
  267. package/src/react/CollabTextRendererRegistry.ts +0 -102
  268. package/src/react/CommandPalette.tsx +0 -375
  269. package/src/react/CurrentUserContext.tsx +0 -50
  270. package/src/react/CustomPageWrapperGate.tsx +0 -69
  271. package/src/react/CustomPageWrapperRegistry.ts +0 -45
  272. package/src/react/FieldFocusReporterRegistry.ts +0 -37
  273. package/src/react/FieldLabelSlotRegistry.ts +0 -30
  274. package/src/react/FieldPresenceRegistry.ts +0 -46
  275. package/src/react/FormCollabBindingRegistry.ts +0 -242
  276. package/src/react/FormStateContext.tsx +0 -591
  277. package/src/react/HeadHooks.tsx +0 -126
  278. package/src/react/MarkdownEditorRegistry.test.ts +0 -38
  279. package/src/react/MarkdownEditorRegistry.ts +0 -107
  280. package/src/react/NotificationActionStrip.tsx +0 -263
  281. package/src/react/NotificationBell.tsx +0 -426
  282. package/src/react/PendingSuggestionApplierRegistry.test.ts +0 -97
  283. package/src/react/PendingSuggestionApplierRegistry.ts +0 -98
  284. package/src/react/PendingSuggestionOverlayRegistry.ts +0 -54
  285. package/src/react/PendingSuggestionsContext.tsx +0 -172
  286. package/src/react/RecordWrapperGate.tsx +0 -58
  287. package/src/react/RecordWrapperRegistry.ts +0 -39
  288. package/src/react/RenderHookSlot.tsx +0 -32
  289. package/src/react/RightSidebar.tsx +0 -257
  290. package/src/react/RightSidebarContext.tsx +0 -234
  291. package/src/react/RightSidebarTrigger.tsx +0 -53
  292. package/src/react/RowCoordsContext.tsx +0 -23
  293. package/src/react/SchemaRenderer.tsx +0 -549
  294. package/src/react/SearchTrigger.tsx +0 -46
  295. package/src/react/ThemeProvider.tsx +0 -93
  296. package/src/react/ThemeSettingsPage.tsx +0 -579
  297. package/src/react/ThemeToggle.tsx +0 -20
  298. package/src/react/Toaster.tsx +0 -158
  299. package/src/react/UserMenu.tsx +0 -196
  300. package/src/react/WidgetDataContext.tsx +0 -157
  301. package/src/react/cells/EditableCell.tsx +0 -389
  302. package/src/react/component-slots.test.ts +0 -103
  303. package/src/react/component-slots.ts +0 -116
  304. package/src/react/fieldJsHandler.test.ts +0 -166
  305. package/src/react/fieldJsHandler.ts +0 -79
  306. package/src/react/fields/BuilderInput.tsx +0 -1078
  307. package/src/react/fields/CheckboxInput.tsx +0 -39
  308. package/src/react/fields/CheckboxListInput.tsx +0 -102
  309. package/src/react/fields/ColorInput.tsx +0 -71
  310. package/src/react/fields/DateFieldInput.tsx +0 -70
  311. package/src/react/fields/DateTimeInput.tsx +0 -62
  312. package/src/react/fields/FieldShell.tsx +0 -348
  313. package/src/react/fields/FileUploadInput.tsx +0 -639
  314. package/src/react/fields/HiddenInput.tsx +0 -17
  315. package/src/react/fields/KeyValueInput.tsx +0 -230
  316. package/src/react/fields/MarkdownInput.tsx +0 -560
  317. package/src/react/fields/RadioInput.tsx +0 -81
  318. package/src/react/fields/RepeaterInput.test.ts +0 -116
  319. package/src/react/fields/RepeaterInput.tsx +0 -1420
  320. package/src/react/fields/SelectFieldInput.tsx +0 -280
  321. package/src/react/fields/SliderInput.tsx +0 -81
  322. package/src/react/fields/TagsInput.tsx +0 -283
  323. package/src/react/fields/TextLikeInput.tsx +0 -256
  324. package/src/react/fields/ToggleButtonsInput.tsx +0 -60
  325. package/src/react/fields/ToggleFieldInput.tsx +0 -56
  326. package/src/react/fields/relationshipRenameDispatch.test.ts +0 -106
  327. package/src/react/fields/relationshipRenameDispatch.ts +0 -97
  328. package/src/react/fields/repeaterReconcile.test.ts +0 -114
  329. package/src/react/fields/repeaterReconcile.ts +0 -104
  330. package/src/react/fields/rowChromeButton.tsx +0 -336
  331. package/src/react/fields/rowState.ts +0 -106
  332. package/src/react/fields/syncRowGates.test.ts +0 -202
  333. package/src/react/fields/syncRowGates.ts +0 -66
  334. package/src/react/fields/textInputControls.tsx +0 -238
  335. package/src/react/fields/useRowReorderDnd.ts +0 -78
  336. package/src/react/formStateHelpers.test.ts +0 -508
  337. package/src/react/formStateHelpers.ts +0 -381
  338. package/src/react/hooks/use-mobile.ts +0 -19
  339. package/src/react/icon-context.tsx +0 -60
  340. package/src/react/index.ts +0 -194
  341. package/src/react/layouts/SidebarLayout.tsx +0 -250
  342. package/src/react/layouts/TopbarLayout.tsx +0 -258
  343. package/src/react/navigate.tsx +0 -37
  344. package/src/react/onProviderSynced.test.ts +0 -90
  345. package/src/react/parseRecordEditUrl.test.ts +0 -122
  346. package/src/react/parseRecordEditUrl.ts +0 -94
  347. package/src/react/persistedState.ts +0 -40
  348. package/src/react/registry.ts +0 -48
  349. package/src/react/right-panel-registry.tsx +0 -47
  350. package/src/react/schemaRenderer/AlertRenderer.tsx +0 -112
  351. package/src/react/schemaRenderer/EntryRenderer.tsx +0 -501
  352. package/src/react/schemaRenderer/SectionRenderer.tsx +0 -120
  353. package/src/react/schemaRenderer/SimpleElements.tsx +0 -306
  354. package/src/react/schemaRenderer/TabsRenderer.tsx +0 -62
  355. package/src/react/schemaRenderer/WizardRenderer.tsx +0 -338
  356. package/src/react/schemaRenderer/action/ActionGroupTrigger.tsx +0 -177
  357. package/src/react/schemaRenderer/action/ActionModalDialog.tsx +0 -273
  358. package/src/react/schemaRenderer/action/ConfirmActionDialog.tsx +0 -61
  359. package/src/react/schemaRenderer/action/HandlerActionButton.tsx +0 -43
  360. package/src/react/schemaRenderer/action/MethodActionButton.tsx +0 -64
  361. package/src/react/schemaRenderer/action/buttons.tsx +0 -99
  362. package/src/react/schemaRenderer/action/helpers.ts +0 -140
  363. package/src/react/schemaRenderer/action/renderAction.tsx +0 -245
  364. package/src/react/schemaRenderer/columnFormat.ts +0 -65
  365. package/src/react/schemaRenderer/constants.ts +0 -50
  366. package/src/react/schemaRenderer/form/FormRenderer.tsx +0 -274
  367. package/src/react/schemaRenderer/form/renderField.tsx +0 -511
  368. package/src/react/schemaRenderer/helpers.tsx +0 -81
  369. package/src/react/schemaRenderer/table/CardsLayoutBody.tsx +0 -308
  370. package/src/react/schemaRenderer/table/TableRenderer.tsx +0 -123
  371. package/src/react/schemaRenderer/table/TableRendererBody.tsx +0 -974
  372. package/src/react/schemaRenderer/table/filters.tsx +0 -1233
  373. package/src/react/schemaRenderer/table/formatCell.tsx +0 -264
  374. package/src/react/schemaRenderer/table/links.tsx +0 -112
  375. package/src/react/schemaRenderer/table/renderRowActions.tsx +0 -52
  376. package/src/react/schemaRenderer/table/url.tsx +0 -143
  377. package/src/react/theme-preview/apply.ts +0 -99
  378. package/src/react/theme-preview/build-html.ts +0 -436
  379. package/src/react/ui/button.tsx +0 -51
  380. package/src/react/ui/calendar.tsx +0 -67
  381. package/src/react/ui/checkbox.tsx +0 -29
  382. package/src/react/ui/dialog.tsx +0 -108
  383. package/src/react/ui/dropdown-menu.tsx +0 -97
  384. package/src/react/ui/input.tsx +0 -20
  385. package/src/react/ui/label.tsx +0 -21
  386. package/src/react/ui/popover.tsx +0 -50
  387. package/src/react/ui/select.tsx +0 -169
  388. package/src/react/ui/separator.tsx +0 -25
  389. package/src/react/ui/sheet.tsx +0 -136
  390. package/src/react/ui/sidebar.tsx +0 -723
  391. package/src/react/ui/skeleton.tsx +0 -13
  392. package/src/react/ui/slider.tsx +0 -34
  393. package/src/react/ui/switch.tsx +0 -28
  394. package/src/react/ui/table.tsx +0 -105
  395. package/src/react/ui/tabs.tsx +0 -63
  396. package/src/react/ui/textarea.tsx +0 -18
  397. package/src/react/ui/tooltip.tsx +0 -64
  398. package/src/react/useResizableWidth.ts +0 -139
  399. package/src/react/utils.ts +0 -6
  400. package/src/react/widgetRegistry.test.ts +0 -43
  401. package/src/react/widgetRegistry.ts +0 -50
  402. package/src/react/widgets/StatsOverviewRenderer.tsx +0 -232
  403. package/src/react/widgets/TableWidgetRenderer.tsx +0 -231
  404. package/src/react/widgets/ViewRenderer.tsx +0 -71
  405. package/src/relationManagerData.test.ts +0 -1595
  406. package/src/richtext/index.ts +0 -8
  407. package/src/richtext/registry.ts +0 -89
  408. package/src/routes/globals.ts +0 -148
  409. package/src/routes/guard.test.ts +0 -325
  410. package/src/routes/helpers.ts +0 -704
  411. package/src/routes/pages.ts +0 -175
  412. package/src/routes/panel.ts +0 -204
  413. package/src/routes/relations.ts +0 -1243
  414. package/src/routes/resources.ts +0 -781
  415. package/src/routes/theme.ts +0 -91
  416. package/src/routes-nested-relations.test.ts +0 -676
  417. package/src/routes-relations.test.ts +0 -972
  418. package/src/routes.test.ts +0 -2027
  419. package/src/routes.ts +0 -303
  420. package/src/schema/Alert.test.ts +0 -109
  421. package/src/schema/Alert.ts +0 -131
  422. package/src/schema/Block.ts +0 -169
  423. package/src/schema/Breadcrumbs.ts +0 -40
  424. package/src/schema/Card.ts +0 -35
  425. package/src/schema/Divider.ts +0 -20
  426. package/src/schema/Element.ts +0 -219
  427. package/src/schema/EmptyState.test.ts +0 -37
  428. package/src/schema/EmptyState.ts +0 -63
  429. package/src/schema/Fieldset.ts +0 -43
  430. package/src/schema/Grid.ts +0 -43
  431. package/src/schema/Group.ts +0 -30
  432. package/src/schema/Heading.ts +0 -39
  433. package/src/schema/Html.ts +0 -67
  434. package/src/schema/Icon.ts +0 -54
  435. package/src/schema/Image.ts +0 -57
  436. package/src/schema/LinkTag.ts +0 -41
  437. package/src/schema/Markdown.ts +0 -85
  438. package/src/schema/MetaTag.ts +0 -41
  439. package/src/schema/RelationTabs.ts +0 -71
  440. package/src/schema/ScriptTag.ts +0 -55
  441. package/src/schema/Section.ts +0 -160
  442. package/src/schema/ServerDataElement.test.ts +0 -140
  443. package/src/schema/ServerDataElement.ts +0 -156
  444. package/src/schema/SlotComponent.test.ts +0 -77
  445. package/src/schema/SlotComponent.ts +0 -71
  446. package/src/schema/Split.ts +0 -50
  447. package/src/schema/Stat.test.ts +0 -118
  448. package/src/schema/Stat.ts +0 -154
  449. package/src/schema/StatsOverview.test.ts +0 -141
  450. package/src/schema/StatsOverview.ts +0 -119
  451. package/src/schema/StyleTag.ts +0 -35
  452. package/src/schema/TableWidget.test.ts +0 -297
  453. package/src/schema/TableWidget.ts +0 -289
  454. package/src/schema/Tabs.ts +0 -79
  455. package/src/schema/Text.ts +0 -58
  456. package/src/schema/UnorderedList.ts +0 -49
  457. package/src/schema/View.test.ts +0 -111
  458. package/src/schema/View.ts +0 -127
  459. package/src/schema/Wizard.ts +0 -220
  460. package/src/schema/containers.test.ts +0 -564
  461. package/src/schema/headTags.test.ts +0 -134
  462. package/src/schema/index.ts +0 -40
  463. package/src/schema/primes.test.ts +0 -269
  464. package/src/schema/resolveSchema.test.ts +0 -379
  465. package/src/schema/resolveSchema.ts +0 -917
  466. package/src/schema/sanitize.ts +0 -58
  467. package/src/search.test.ts +0 -446
  468. package/src/search.ts +0 -178
  469. package/src/sessionFilters.test.ts +0 -375
  470. package/src/sessionFilters.ts +0 -143
  471. package/src/slot-components/index.ts +0 -10
  472. package/src/slot-components/registry.ts +0 -56
  473. package/src/styles/file-upload.css +0 -13
  474. package/src/summarizers/Summarizer.test.ts +0 -84
  475. package/src/summarizers/Summarizer.ts +0 -123
  476. package/src/summarizers/index.ts +0 -11
  477. package/src/theme/base-colors.ts +0 -68
  478. package/src/theme/chart-colors.ts +0 -50
  479. package/src/theme/colors.ts +0 -447
  480. package/src/theme/generate-css.test.ts +0 -139
  481. package/src/theme/generate-css.ts +0 -44
  482. package/src/theme/generate-scale.test.ts +0 -106
  483. package/src/theme/generate-scale.ts +0 -97
  484. package/src/theme/icon-map.ts +0 -42
  485. package/src/theme/index.ts +0 -34
  486. package/src/theme/migrate.test.ts +0 -178
  487. package/src/theme/migrate.ts +0 -81
  488. package/src/theme/presets.ts +0 -135
  489. package/src/theme/radius.ts +0 -18
  490. package/src/theme/resolve.test.ts +0 -238
  491. package/src/theme/resolve.ts +0 -96
  492. package/src/theme/spacing.ts +0 -18
  493. package/src/theme/storage.test.ts +0 -126
  494. package/src/theme/storage.ts +0 -106
  495. package/src/theme/theme-colors.ts +0 -88
  496. package/src/theme/types.ts +0 -125
  497. package/src/uploads/UploadAdapter.ts +0 -35
  498. package/src/uploads/index.ts +0 -2
  499. package/src/uploads/localUpload.test.ts +0 -70
  500. package/src/uploads/localUpload.ts +0 -84
  501. package/src/validation/Validator.ts +0 -49
  502. package/src/validation/index.ts +0 -28
  503. package/src/validation/rules.ts +0 -78
  504. package/src/validation/runValidators.ts +0 -435
  505. package/src/validation/uniqueValidator.test.ts +0 -196
  506. package/src/validation/uniqueValidator.ts +0 -133
  507. package/src/validation/validators.test.ts +0 -268
  508. package/src/vite.test.ts +0 -184
  509. package/src/vite.ts +0 -787
  510. package/src/widgets/index.ts +0 -10
  511. package/src/widgets/registry.ts +0 -45
  512. package/src/widgets.test.ts +0 -592
  513. package/tsconfig.build.json +0 -11
  514. package/tsconfig.json +0 -4
  515. package/tsconfig.test.json +0 -10
  516. package/views/react/Dashboard.tsx +0 -27
  517. package/views/react/Resources/Form.tsx +0 -102
  518. package/views/react/Resources/Index.tsx +0 -49
@@ -1,1420 +0,0 @@
1
- import React, { useContext, useEffect, useId, useMemo, useRef, useState } from 'react'
2
- import { PlusIcon } from 'lucide-react'
3
- import type { ElementMeta } from '../../schema/Element.js'
4
- import { Button } from '../ui/button.js'
5
- import { SchemaRenderer, dispatchHandlerAction } from '../SchemaRenderer.js'
6
- import { FormIdContext, useFormState, useRowBinding } from '../FormStateContext.js'
7
- import { findFieldMeta } from '../formStateHelpers.js'
8
- import { RowCoordsContext } from '../RowCoordsContext.js'
9
- import { useNavigate } from '../navigate.js'
10
- import { useToast } from '../Toaster.js'
11
- import type { RowButtonsMeta } from '../../fields/RowButton.js'
12
- import {
13
- RowChromeIconButton,
14
- ReorderGrip,
15
- CollapseChevron,
16
- BulkCollapseHeader,
17
- resolveRowChrome,
18
- DEFAULT_MOVE_UP,
19
- DEFAULT_MOVE_DOWN,
20
- DEFAULT_CLONE,
21
- DEFAULT_DELETE,
22
- } from './rowChromeButton.js'
23
- import { syncRowGates } from './syncRowGates.js'
24
- import { consumeReconcileFlag, computeReconcilePlan } from './repeaterReconcile.js'
25
- import {
26
- generateRowId, makeAccordionStorage, makeCollapsedStorage,
27
- } from './rowState.js'
28
- import { useRowReorderDnd } from './useRowReorderDnd.js'
29
-
30
- const collapsedStorage = makeCollapsedStorage('repeater')
31
- const accordionStorage = makeAccordionStorage('repeater')
32
- const initSeedCollapsed = collapsedStorage.seed
33
- const writeCollapsedToStorage = collapsedStorage.write
34
- const deleteCollapsedFromStorage = collapsedStorage.remove
35
- const readAccordionFromStorage = accordionStorage.read
36
- const writeAccordionToStorage = accordionStorage.write
37
-
38
- /**
39
- * Pure reorder helper — used by both the HTML5 DnD path and the
40
- * Up/Down button path. `insertBeforeIdx` is the boundary the dragged
41
- * row should land at (range `0..rows.length`); after removing the
42
- * source we adjust by -1 when the source sat below the target so the
43
- * caller never has to think about post-removal index shifts.
44
- *
45
- * No-ops when the move would leave the array unchanged.
46
- */
47
- export function reorderRows<T>(rows: T[], fromIdx: number, insertBeforeIdx: number): T[] {
48
- if (fromIdx < 0 || fromIdx >= rows.length) return rows
49
- if (insertBeforeIdx < 0 || insertBeforeIdx > rows.length) return rows
50
- if (fromIdx === insertBeforeIdx || fromIdx + 1 === insertBeforeIdx) return rows
51
- const next = rows.slice()
52
- const moved = next.splice(fromIdx, 1)[0] as T
53
- const target = insertBeforeIdx > fromIdx ? insertBeforeIdx - 1 : insertBeforeIdx
54
- next.splice(target, 0, moved)
55
- return next
56
- }
57
-
58
- /**
59
- * Tailwind v4 default breakpoint widths. Reused as container-query
60
- * thresholds so authors can think in the same `sm/md/lg/xl/2xl` ladder
61
- * regardless of viewport vs. container framing — at viewport width these
62
- * fire identically to the corresponding `@media` queries.
63
- */
64
- const RESPONSIVE_GRID_BREAKPOINTS: Array<{ key: 'sm' | 'md' | 'lg' | 'xl' | '2xl'; min: string }> = [
65
- { key: 'sm', min: '40rem' },
66
- { key: 'md', min: '48rem' },
67
- { key: 'lg', min: '64rem' },
68
- { key: 'xl', min: '80rem' },
69
- { key: '2xl', min: '96rem' },
70
- ]
71
-
72
- /**
73
- * Compute the grid container's className / style / scoped-CSS block from
74
- * a `meta.grid` value. Shared between Repeater and Builder so both render
75
- * responsive grids identically.
76
- *
77
- * - `meta.grid` undefined → `{ hasGrid: false }` and the caller falls back
78
- * to a vertical flex stack.
79
- * - Number form (`grid(2)`) → inline `gridTemplateColumns: repeat(N, …)`.
80
- * - Object form (`grid({ default: 1, md: 2 })`) → a fresh `<style>` block
81
- * keyed off `scopeId` + a matching className on the container; default
82
- * columns drive the base rule, each declared breakpoint adds a
83
- * `@container` query override.
84
- *
85
- * Responsive mode requires a CQ context — the caller paints
86
- * `wrapperStyle` (`container-type: inline-size`) on the outer wrapper so
87
- * the grid's `@container` rules resolve against the Repeater's
88
- * parent-allocated width, not the viewport. A 2-column Repeater dropped
89
- * into a `Split` aside therefore folds back to 1 column once the aside
90
- * is narrower than `md`, even on a wide screen.
91
- *
92
- * `scopeId` should be a stable per-field identifier (we use `useId()` from
93
- * React — already isolated to this render's component instance).
94
- */
95
- export function buildGridContainer(
96
- grid: number | Record<string, number | undefined> | undefined,
97
- scopeId: string,
98
- ): {
99
- hasGrid: boolean
100
- className: string
101
- style: React.CSSProperties | undefined
102
- styleBlock: React.ReactElement | null
103
- wrapperStyle: React.CSSProperties | undefined
104
- } {
105
- if (grid === undefined) {
106
- return { hasGrid: false, className: 'flex flex-col gap-3', style: undefined, styleBlock: null, wrapperStyle: undefined }
107
- }
108
- if (typeof grid === 'number') {
109
- return {
110
- hasGrid: true,
111
- className: 'grid gap-3',
112
- style: { gridTemplateColumns: `repeat(${grid}, minmax(0, 1fr))` },
113
- styleBlock: null,
114
- wrapperStyle: undefined,
115
- }
116
- }
117
- const cls = `pq-grid-${scopeId.replace(/:/g, '')}`
118
- const baseCols = typeof grid['default'] === 'number' ? grid['default'] : 1
119
- const rules: string[] = [
120
- `.${cls} { display: grid; gap: 0.75rem; grid-template-columns: repeat(${baseCols}, minmax(0, 1fr)); }`,
121
- ]
122
- for (const bp of RESPONSIVE_GRID_BREAKPOINTS) {
123
- const cols = grid[bp.key]
124
- if (typeof cols !== 'number') continue
125
- rules.push(`@container (min-width: ${bp.min}) { .${cls} { grid-template-columns: repeat(${cols}, minmax(0, 1fr)); } }`)
126
- }
127
- return {
128
- hasGrid: true,
129
- className: cls,
130
- style: undefined,
131
- styleBlock: <style>{rules.join('\n')}</style>,
132
- wrapperStyle: { containerType: 'inline-size' },
133
- }
134
- }
135
-
136
- interface RowState {
137
- id: string
138
- children: ElementMeta[]
139
- itemLabel?: string
140
- hidden?: boolean
141
- extraActions?: ElementMeta[]
142
- // Per-row capability flags from `itemCan*(rule)`. Stamped only when the
143
- // rule resolved falsy server-side; default ("allowed") leaves them unset.
144
- // Cloned rows inherit nothing (a fresh row had no rule evaluation pass).
145
- canDelete?: false
146
- canClone?: false
147
- canReorder?: false
148
- }
149
-
150
- /**
151
- * Repeater renderer (Plan #14).
152
- *
153
- * Rows are managed as local React state with stable `id` keys so
154
- * uncontrolled inner inputs preserve their typed values across
155
- * add/remove/reorder operations. Each row's resolved children meta is
156
- * deep-cloned with a row-scoped prefix on every Field's `name` so
157
- * submitted form bodies are flat-keyed (`items.0.product`, etc.) — the
158
- * server's `coerceFormValues` re-groups them into an array.
159
- *
160
- * Reorder: native HTML5 drag-and-drop on each row, with a 2px drop
161
- * indicator showing where the row will land. Up/Down buttons are kept
162
- * as a keyboard fallback. Both paths route through `reorderRows()` so
163
- * behavior is identical. Collapsed state persists per-row to
164
- * `localStorage` under `pilotiq.repeater.<formId>.<fieldName>.<rowId>`
165
- * when collapsible.
166
- *
167
- * Inner-field reactivity: this component does NOT integrate with
168
- * `FormStateProvider` for nested-path live updates; that surgery is
169
- * tracked separately. Repeaters with `live()` inner fields render
170
- * today but the `live` trigger doesn't roundtrip.
171
- */
172
- export function RepeaterInput({
173
- el,
174
- name,
175
- disabled,
176
- }: {
177
- el: ElementMeta
178
- name: string
179
- disabled: boolean
180
- }): React.ReactElement {
181
- // The parent <form>'s id, scoped via context. Falls back to the field
182
- // name when no Form is in scope (defensive — Repeaters always render
183
- // inside a Form on real pages, but Storybook / unit tests can mount
184
- // them bare).
185
- const formIdFromCtx = useContext(FormIdContext)
186
- const formId = formIdFromCtx || `repeater-${name}`
187
- const meta = el as RepeaterMetaShape
188
- const minItems = typeof meta.minItems === 'number' ? meta.minItems : undefined
189
- const maxItems = typeof meta.maxItems === 'number' ? meta.maxItems : undefined
190
- const collapsible = Boolean(meta.collapsible)
191
- const defaultCollapsed = Boolean(meta.defaultCollapsed)
192
- const accordion = Boolean(meta.accordion)
193
- const reorderable = Boolean(meta.reorderable)
194
- const cloneable = Boolean(meta.cloneable)
195
- const simple = Boolean(meta.simple)
196
- const buttons = meta.buttons
197
- // Customizer wins over the legacy `addActionLabel`. Default 'Add' is the
198
- // final fallback; documented in `RepeaterField.addActionLabel`.
199
- const addLabel = buttons?.add?.label
200
- ?? (typeof meta.addActionLabel === 'string' ? meta.addActionLabel : 'Add')
201
- const columns = typeof meta.columns === 'number' && meta.columns > 1 ? meta.columns : 1
202
- // Row-grid mode: scalar `grid: N` or responsive object `grid: { default, md, … }`
203
- // lays rows in an n-column grid. Distinct from `columns` which grids the inner
204
- // schema *inside* a row. We suppress the drop indicator in grid mode (a horizontal
205
- // accent line reads wrong across grid cells); button reorder still works.
206
- const gridScopeId = useId()
207
- const gridContainer = useMemo(
208
- () => buildGridContainer(
209
- meta.grid as number | Record<string, number | undefined> | undefined,
210
- gridScopeId,
211
- ),
212
- [meta.grid, gridScopeId],
213
- )
214
- // Table mode renders rows as `<tr>` and inner fields as `<td>`. Mutually
215
- // exclusive with `simple` and `grid` (the field setters arbitrate).
216
- // Collapsible / accordion are meaningless on `<tr>` rows so we ignore
217
- // those flags in this path.
218
- const tableColumns = meta.table?.columns
219
- const tableMode = Array.isArray(tableColumns) && tableColumns.length > 0
220
-
221
- const initialRows: RowState[] = useMemo(
222
- () => (meta.rows ?? []).map(r => ({
223
- id: r.id,
224
- children: r.children,
225
- ...(r.itemLabel !== undefined ? { itemLabel: r.itemLabel } : {}),
226
- ...(r.hidden ? { hidden: true } : {}),
227
- ...(r.extraActions && r.extraActions.length > 0 ? { extraActions: r.extraActions } : {}),
228
- ...(r.canDelete === false ? { canDelete: false as const } : {}),
229
- ...(r.canClone === false ? { canClone: false as const } : {}),
230
- ...(r.canReorder === false ? { canReorder: false as const } : {}),
231
- })),
232
- // eslint-disable-next-line react-hooks/exhaustive-deps
233
- [],
234
- )
235
- const [rows, setRows] = useState<RowState[]>(initialRows)
236
- const metaRows = meta.rows
237
- useEffect(() => {
238
- if (!metaRows) return
239
- setRows(prev => syncRowGates(prev, metaRows))
240
- }, [metaRows])
241
- // Phase F.5 — row-array CRDT binding. `null` outside a collab room
242
- // OR when the active binding doesn't implement F.5 row methods OR when
243
- // this Repeater opted out via `.collab(false)`. The four row mutations
244
- // (`addRow / cloneRow / removeRow / moveRow + DnD drop`) below call into
245
- // it when present so peers see the same lifecycle events; absent =
246
- // today's local-only behaviour, unchanged.
247
- const rowBinding = useRowBinding(name)
248
- // Mirror row identities into the form's values map so dotted row-leaf
249
- // consumers can resolve the row's `__id` via `rowIdAtIndex(ctx.values,
250
- // name, i)`. Setting a `__id` key routes through `routeBindingWrite` →
251
- // `parseRowFieldPath` which filters `__id` → no-op on the binding side,
252
- // so the only effect is a row entry landing in `valuesState`.
253
- // `formStateForIds` mirrors `formState` below; we read via
254
- // `useFormState()` here too instead of forward-referencing the later
255
- // binding.
256
- const formStateForIds = useFormState()
257
- const ctxSetValue = formStateForIds?.setValue
258
- useEffect(() => {
259
- if (!ctxSetValue) return
260
- for (let i = 0; i < rows.length; i++) {
261
- const row = rows[i]
262
- if (!row) continue
263
- ctxSetValue(`${name}.${i}.__id`, row.id)
264
- }
265
- }, [rows, name, ctxSetValue])
266
- // Phase F.5 — reconcile remote row events into the local `rows` state
267
- // by `__id`. Local mutations also surface here (Yjs observers fire on
268
- // local transactions); we dedupe by checking whether the rowId is
269
- // already present in the current state. `template` seeds new rows so
270
- // remote-added rows render with the same inner schema as locally-added
271
- // ones.
272
- useEffect(() => {
273
- if (!rowBinding) return
274
- const tpl = meta.template ?? []
275
- return rowBinding.subscribe((event) => {
276
- if (event.kind === 'add') {
277
- setRows((prev) => {
278
- if (prev.some(r => r.id === event.rowId)) return prev
279
- const incoming: RowState = { id: event.rowId, children: tpl }
280
- const next = prev.slice()
281
- const at = Math.max(0, Math.min(event.index, next.length))
282
- next.splice(at, 0, incoming)
283
- return next
284
- })
285
- return
286
- }
287
- if (event.kind === 'remove') {
288
- setRows((prev) => {
289
- if (!prev.some(r => r.id === event.rowId)) return prev
290
- return prev.filter(r => r.id !== event.rowId)
291
- })
292
- return
293
- }
294
- // move — recompute the local row order by lifting the row at `from`
295
- // and re-inserting at `to`. No-op when local already matches.
296
- setRows((prev) => {
297
- const fromIdx = prev.findIndex(r => r.id === event.rowId)
298
- if (fromIdx < 0) return prev
299
- if (fromIdx === event.to) return prev
300
- const next = prev.slice()
301
- const [moved] = next.splice(fromIdx, 1)
302
- if (!moved) return prev
303
- next.splice(event.to, 0, moved)
304
- return next
305
- })
306
- })
307
- }, [rowBinding, meta.template])
308
-
309
- // Phase A reconciliation for `Repeater.relationship` PK-switch — when
310
- // the surrounding form just submitted in this tab AND we're inside a
311
- // collab room with a row binding, snapshot the CRDT order after a
312
- // short settle (long enough for WS sync to deliver any persisted
313
- // state) and reconcile against `initialRows`. Drops orphan UUIDs
314
- // whose rows just persisted under a fresh DB PK; idempotent + no-op
315
- // for non-relationship Repeaters where `__id` stays UUID across
316
- // save+reload. Plan:
317
- // `pilotiq-pro/docs/plans/repeater-relationship-pk-switch.md`.
318
- useEffect(() => {
319
- if (!rowBinding) return
320
- if (!consumeReconcileFlag(formId)) return
321
- // Give WS sync time to deliver any persisted rows before reading
322
- // current(). 1500ms is conservative; typical sync settles in <300ms.
323
- // The reconciler is one-shot per submit, so we accept the brief
324
- // visual flicker over a tighter timer that might fire pre-sync.
325
- const timer = setTimeout(() => {
326
- const plan = computeReconcilePlan({
327
- current: rowBinding.current(),
328
- authoritative: initialRows.map(r => r.id),
329
- })
330
- for (const id of plan.toRemove) rowBinding.remove(id)
331
- for (const id of plan.toAdd) rowBinding.add(id, {})
332
- }, 1500)
333
- return () => clearTimeout(timer)
334
- // initialRows is a stable useMemo([]) ref so it's safe to omit. We
335
- // intentionally key only on rowBinding + formId — the reconciler is
336
- // tied to the submit lifecycle, not to row-state changes.
337
- // eslint-disable-next-line react-hooks/exhaustive-deps
338
- }, [rowBinding, formId])
339
- const [collapsed, setCollapsed] = useState<Record<string, boolean>>(() =>
340
- accordion ? {} : initSeedCollapsed(initialRows, formId, name, defaultCollapsed, collapsible),
341
- )
342
- // Accordion mode replaces the per-row collapsed map with a single open
343
- // row id (or `null` for "all collapsed"). Persisted under a single
344
- // `…accordion` storage key so reload + form swap restore the open row.
345
- // Initial value: respect `defaultCollapsed` (start with everything
346
- // collapsed when the author opted in), otherwise open the first
347
- // visible row — Filament's posture, and matches the implicit user
348
- // mental model that an accordion always shows *something*.
349
- const [accordionOpenId, setAccordionOpenId] = useState<string | null>(() => {
350
- if (!accordion) return null
351
- const stored = readAccordionFromStorage(formId, name)
352
- if (stored !== undefined) {
353
- // Storage may hold a stale id from before a row was deleted; if so,
354
- // fall through to the default.
355
- if (stored === '' || initialRows.some(r => r.id === stored)) return stored === '' ? null : stored
356
- }
357
- if (defaultCollapsed) return null
358
- const firstVisible = initialRows.find(r => !r.hidden)
359
- return firstVisible?.id ?? null
360
- })
361
-
362
- const atMin = minItems !== undefined && rows.length <= minItems
363
- const atMax = maxItems !== undefined && rows.length >= maxItems
364
-
365
- const addRow = (): void => {
366
- if (atMax) return
367
- const newRow: RowState = {
368
- id: generateRowId(),
369
- children: meta.template ?? [],
370
- }
371
- setRows(prev => [...prev, newRow])
372
- rowBinding?.add(newRow.id, {})
373
- if (accordion) {
374
- // New row should be the only one open — the user just asked for it.
375
- setAccordionOpenId(newRow.id)
376
- writeAccordionToStorage(formId, name, newRow.id)
377
- return
378
- }
379
- if (collapsible && defaultCollapsed) {
380
- setCollapsed(prev => ({ ...prev, [newRow.id]: true }))
381
- writeCollapsedToStorage(formId, name, newRow.id, true)
382
- }
383
- }
384
-
385
- const removeRow = (id: string): void => {
386
- if (atMin) return
387
- setRows(prev => prev.filter(r => r.id !== id))
388
- rowBinding?.remove(id)
389
- if (accordion) {
390
- if (accordionOpenId === id) {
391
- setAccordionOpenId(null)
392
- writeAccordionToStorage(formId, name, null)
393
- }
394
- return
395
- }
396
- setCollapsed(prev => {
397
- const { [id]: _drop, ...rest } = prev
398
- return rest
399
- })
400
- deleteCollapsedFromStorage(formId, name, id)
401
- }
402
-
403
- const cloneRow = (id: string): void => {
404
- if (atMax) return
405
- let cloneId: string | null = null
406
- setRows(prev => {
407
- const idx = prev.findIndex(r => r.id === id)
408
- if (idx < 0) return prev
409
- const source = prev[idx]!
410
- cloneId = generateRowId()
411
- const clone: RowState = {
412
- id: cloneId,
413
- children: source.children,
414
- ...(source.itemLabel !== undefined ? { itemLabel: source.itemLabel } : {}),
415
- }
416
- const next = prev.slice()
417
- next.splice(idx + 1, 0, clone)
418
- return next
419
- })
420
- // F.5 — register the clone's stable id on the binding. Per-field
421
- // clone-of-source values flow through `setRow` on the user's next
422
- // edit; v1 doesn't lift the source row's values onto the clone (the
423
- // binding's empty seed combined with the DOM's defaultValue-copied
424
- // inputs gives the local user the right visual state).
425
- if (cloneId !== null) rowBinding?.add(cloneId, {})
426
- }
427
-
428
- const moveRow = (id: string, dir: -1 | 1): void => {
429
- const idx = rows.findIndex(r => r.id === id)
430
- if (idx < 0) return
431
- // Skip past hidden neighbours so reorder operates between visible
432
- // rows. Hidden rows hold their absolute slot — the visible row hops
433
- // over them.
434
- let next: RowState[]
435
- if (dir === -1) {
436
- let target = idx - 1
437
- while (target >= 0 && rows[target]?.hidden) target--
438
- if (target < 0) return
439
- next = reorderRows(rows, idx, target)
440
- } else {
441
- let target = idx + 1
442
- while (target < rows.length && rows[target]?.hidden) target++
443
- if (target >= rows.length) return
444
- next = reorderRows(rows, idx, target + 1)
445
- }
446
- if (next === rows) return
447
- setRows(next)
448
- rowBinding?.reorder(next.map(r => r.id))
449
- }
450
-
451
- // ── DnD state ───────────────────────────────────────────
452
- const {
453
- dragId, dropAt,
454
- onDragStart: onRowDragStart,
455
- onDragOver: onRowDragOver,
456
- onDrop: onRowDrop,
457
- onDragEnd: onRowDragEnd,
458
- } = useRowReorderDnd({
459
- enabled: reorderable && !disabled,
460
- onDrop: (fromId, at) => {
461
- // Compute next from the current `rows` directly. The previous
462
- // setRows(updater) + closure-mutation pattern relied on React
463
- // running the updater synchronously inside setState — which only
464
- // happens when no other update is queued. `useRowReorderDnd`'s
465
- // handleDrop sets dragId/dropAt to null right before calling
466
- // this callback, so the updater runs in commit phase and the
467
- // outer `newOrder` stayed null past the `if` check, silently
468
- // skipping the rowBinding.reorder broadcast.
469
- const fromIdx = rows.findIndex(r => r.id === fromId)
470
- if (fromIdx < 0) return
471
- const next = reorderRows(rows, fromIdx, at)
472
- if (next === rows) return
473
- setRows(next)
474
- rowBinding?.reorder(next.map(r => r.id))
475
- },
476
- })
477
-
478
- // ── Inner-field live re-resolve (Plan #14 v1.1) ─────────────
479
- // Inner Repeater inputs are uncontrolled (so reorder/clone preserves
480
- // typed values). To make `Field.live()` work on them, we delegate
481
- // change/blur events at the container level: the dotted-path field
482
- // name on `target.name` is enough to find the field meta and decide
483
- // whether to fire. `triggerLive` then snapshots the form's full
484
- // FormData and POSTs to the partial-resolve endpoint — see
485
- // FormStateContext.
486
- //
487
- // React-controlled primitives that update via callbacks (Switch /
488
- // Slider / Base UI Select / etc.) don't bubble native input events
489
- // here. Each of those renderers calls `fs.triggerLive(value)`
490
- // explicitly to compensate (Plan #14 v1.2). Native inputs
491
- // (text/number/email/textarea/range/date/checkbox/radio) keep
492
- // bubbling through this delegate as before.
493
- const formState = useFormState()
494
- const fireLive = (name: string, value: string, eventKind: 'change' | 'blur'): void => {
495
- if (!formState) return
496
- if (!name.includes('.')) return // top-level fields handle their own live trigger
497
- const fieldMeta = findFieldMeta(formState.formMeta, name)
498
- const liveCfg = fieldMeta?.['live']
499
- const hasJs = (fieldMeta as { afterStateUpdatedJs?: string } | undefined)?.afterStateUpdatedJs !== undefined
500
- if (!liveCfg && !hasJs) return
501
- if (liveCfg) {
502
- const onBlurMode = typeof liveCfg === 'object' && liveCfg !== null
503
- && (liveCfg as { onBlur?: boolean }).onBlur === true
504
- if (eventKind === 'change' && onBlurMode) return
505
- if (eventKind === 'blur' && !onBlurMode) return
506
- } else {
507
- // JS-only handlers always fire immediately on change.
508
- if (eventKind === 'blur') return
509
- }
510
- formState.triggerLive(name, value)
511
- }
512
- const onContainerChange = (e: React.ChangeEvent<HTMLDivElement>): void => {
513
- const t = e.target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
514
- if (!t.name) return
515
- fireLive(t.name, t.value, 'change')
516
- }
517
- const onContainerBlur = (e: React.FocusEvent<HTMLDivElement>): void => {
518
- const t = e.target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
519
- if (!t.name) return
520
- fireLive(t.name, t.value, 'blur')
521
- }
522
-
523
- const toggleCollapsed = (id: string): void => {
524
- if (accordion) {
525
- // Click the open row to collapse all; click any other row to swap.
526
- // No "two rows open" state is reachable.
527
- const next = accordionOpenId === id ? null : id
528
- setAccordionOpenId(next)
529
- writeAccordionToStorage(formId, name, next)
530
- return
531
- }
532
- setCollapsed(prev => {
533
- const nextValue = !prev[id]
534
- writeCollapsedToStorage(formId, name, id, nextValue)
535
- return { ...prev, [id]: nextValue }
536
- })
537
- }
538
-
539
- // ── Bulk expand / collapse ──────────────────────────────
540
- // Accordion's "only one open" invariant survives both bulk actions:
541
- // expandAll opens the first visible row (matches the implicit
542
- // "always show something" mental model); collapseAll sets null.
543
- // Per-row mode iterates every row id and writes the storage slot
544
- // alongside the in-memory map so reload restores the bulk action.
545
- const expandAll = (): void => {
546
- if (accordion) {
547
- const firstVisible = rows.find(r => !r.hidden)
548
- const next = firstVisible?.id ?? null
549
- setAccordionOpenId(next)
550
- writeAccordionToStorage(formId, name, next)
551
- return
552
- }
553
- setCollapsed({})
554
- for (const r of rows) writeCollapsedToStorage(formId, name, r.id, false)
555
- }
556
- const collapseAll = (): void => {
557
- if (accordion) {
558
- setAccordionOpenId(null)
559
- writeAccordionToStorage(formId, name, null)
560
- return
561
- }
562
- const next: Record<string, boolean> = {}
563
- for (const r of rows) {
564
- next[r.id] = true
565
- writeCollapsedToStorage(formId, name, r.id, true)
566
- }
567
- setCollapsed(next)
568
- }
569
-
570
- // Visibility computed each render — hidden rows still occupy slots in
571
- // `rows` (so values round-trip + reorder-around math stays simple), but
572
- // they don't count for the user-facing empty state, drop indicator, or
573
- // first/last-visible disable on Up/Down buttons.
574
- const hasVisibleRow = rows.some(r => !r.hidden)
575
- const firstVisibleIdx = rows.findIndex(r => !r.hidden)
576
- const lastVisibleIdx = (() => {
577
- for (let i = rows.length - 1; i >= 0; i--) if (!rows[i]?.hidden) return i
578
- return -1
579
- })()
580
-
581
- if (tableMode && tableColumns) {
582
- // Table mode renders rows as `<tr>` with the inner schema's fields
583
- // as `<td>` cells. The reorder grip + extraActions + clone + delete
584
- // strip lives in a final actions column (only mounted when any of
585
- // those are configured). Hidden rows render with `display:none` on
586
- // `<tr>` so values still round-trip on submit. Drop indicator is
587
- // suppressed — a horizontal accent across `<td>` cells looks broken;
588
- // button reorder + drag itself still move rows.
589
- // Actions cell is always present in v1 — delete is implicit on every
590
- // Repeater row, and reorder/clone/extraActions land here too. When
591
- // every action happens to be disabled (e.g. atMin && no reorderable
592
- // && no clone), the cell still renders for column-count consistency.
593
- return (
594
- <RepeaterTableLayout
595
- rows={rows}
596
- name={name}
597
- disabled={disabled}
598
- columns={tableColumns}
599
- addLabel={addLabel}
600
- buttons={buttons}
601
- atMin={atMin}
602
- atMax={atMax}
603
- reorderable={reorderable}
604
- cloneable={cloneable}
605
- firstVisibleIdx={firstVisibleIdx}
606
- lastVisibleIdx={lastVisibleIdx}
607
- hasVisibleRow={hasVisibleRow}
608
- dragId={dragId}
609
- onAdd={addRow}
610
- onMoveUp={(id) => moveRow(id, -1)}
611
- onMoveDown={(id) => moveRow(id, 1)}
612
- onClone={cloneRow}
613
- onRemove={removeRow}
614
- onContainerChange={onContainerChange}
615
- onContainerBlur={onContainerBlur}
616
- onRowDragStart={onRowDragStart}
617
- onRowDragOver={onRowDragOver}
618
- onRowDrop={onRowDrop}
619
- onRowDragEnd={onRowDragEnd}
620
- />
621
- )
622
- }
623
-
624
- // In grid mode the rows themselves are grid items — wrap them in a
625
- // CSS grid; otherwise stack vertically. The empty state and Add
626
- // button are rendered as siblings so they don't get pulled into the
627
- // grid (Add stays at the natural bottom; empty state spans full).
628
- return (
629
- <div
630
- className="flex flex-col gap-3"
631
- style={gridContainer.wrapperStyle}
632
- onChange={onContainerChange}
633
- onBlur={onContainerBlur}
634
- >
635
- <BulkCollapseHeader
636
- buttons={buttons}
637
- disabled={disabled || !hasVisibleRow}
638
- onExpandAll={expandAll}
639
- onCollapseAll={collapseAll}
640
- />
641
-
642
- {!hasVisibleRow && (
643
- <div className="rounded-md border border-dashed px-4 py-6 text-center text-sm text-muted-foreground">
644
- No items yet. Click {addLabel} to start.
645
- </div>
646
- )}
647
-
648
- {gridContainer.styleBlock}
649
- <div
650
- className={gridContainer.className}
651
- style={gridContainer.style}
652
- >
653
- {rows.map((row, i) => (
654
- <React.Fragment key={row.id}>
655
- {!row.hidden && dropAt === i && !gridContainer.hasGrid && <DropIndicator />}
656
- <RepeaterRow
657
- row={row}
658
- index={i}
659
- isFirstVisible={i === firstVisibleIdx}
660
- isLastVisible={i === lastVisibleIdx}
661
- name={name}
662
- disabled={disabled}
663
- collapsible={collapsible}
664
- isCollapsed={collapsible && (
665
- accordion
666
- ? accordionOpenId !== row.id
667
- : (collapsed[row.id] ?? false)
668
- )}
669
- reorderable={reorderable}
670
- cloneable={cloneable}
671
- simple={simple}
672
- atMin={atMin}
673
- atMax={atMax}
674
- columns={columns}
675
- buttons={buttons}
676
- isDragging={dragId === row.id}
677
- rowPath={`${name}.${i}`}
678
- onMoveUp={() => moveRow(row.id, -1)}
679
- onMoveDown={() => moveRow(row.id, 1)}
680
- onClone={() => cloneRow(row.id)}
681
- onRemove={() => removeRow(row.id)}
682
- onToggleCollapse={() => toggleCollapsed(row.id)}
683
- onDragStart={onRowDragStart(row.id)}
684
- onDragOver={onRowDragOver(i)}
685
- onDrop={onRowDrop}
686
- onDragEnd={onRowDragEnd}
687
- />
688
- </React.Fragment>
689
- ))}
690
- {dropAt === rows.length && !gridContainer.hasGrid && <DropIndicator />}
691
- </div>
692
-
693
- <AddRowButton
694
- label={addLabel}
695
- buttons={buttons}
696
- disabled={disabled || atMax}
697
- onClick={addRow}
698
- />
699
- </div>
700
- )
701
- }
702
-
703
- /**
704
- * Bottom Add button — outline shadcn `<Button>`. Reads the customizer
705
- * (`addAction(RowButton.make()…)`) for icon + tooltip overrides; label
706
- * is already pre-resolved upstream so the legacy `addActionLabel()` setter
707
- * keeps working. Color override is intentionally ignored on the Add
708
- * button to preserve the outline-button visual identity (icon-color
709
- * tweaks would clash with the shadcn variant); use `Action.color()` on
710
- * a custom header action if you need a different chrome there.
711
- */
712
- function AddRowButton({
713
- label,
714
- buttons,
715
- disabled,
716
- onClick,
717
- }: {
718
- label: string
719
- buttons: RowButtonsMeta | undefined
720
- disabled: boolean
721
- onClick: () => void
722
- }): React.ReactElement {
723
- const { Icon, tooltip } = resolveRowChrome(
724
- { Icon: PlusIcon, label, tooltip: '', colorClass: '' },
725
- buttons?.add,
726
- )
727
- return (
728
- <Button
729
- type="button"
730
- variant="outline"
731
- size="sm"
732
- onClick={onClick}
733
- disabled={disabled}
734
- title={tooltip || undefined}
735
- className="self-start"
736
- >
737
- <Icon className="size-4" />
738
- {label}
739
- </Button>
740
- )
741
- }
742
-
743
- function RepeaterRow({
744
- row, index, isFirstVisible, isLastVisible, name, disabled,
745
- collapsible, isCollapsed, reorderable, cloneable, simple, atMin, atMax, columns,
746
- buttons,
747
- isDragging,
748
- rowPath,
749
- onMoveUp, onMoveDown, onClone, onRemove, onToggleCollapse,
750
- onDragStart, onDragOver, onDrop, onDragEnd,
751
- }: {
752
- row: RowState
753
- index: number
754
- isFirstVisible: boolean
755
- isLastVisible: boolean
756
- name: string
757
- disabled: boolean
758
- collapsible: boolean
759
- isCollapsed: boolean
760
- reorderable: boolean
761
- cloneable: boolean
762
- simple: boolean
763
- atMin: boolean
764
- atMax: boolean
765
- columns: number
766
- buttons: RowButtonsMeta | undefined
767
- isDragging: boolean
768
- rowPath: string
769
- onMoveUp: () => void
770
- onMoveDown: () => void
771
- onClone: () => void
772
- onRemove: () => void
773
- onToggleCollapse: () => void
774
- onDragStart: (e: React.DragEvent<HTMLElement>) => void
775
- onDragOver: (e: React.DragEvent<HTMLElement>) => void
776
- onDrop: (e: React.DragEvent<HTMLElement>) => void
777
- onDragEnd: (e: React.DragEvent<HTMLElement>) => void
778
- }): React.ReactElement {
779
- const prefix = `${name}.${index}`
780
- const namespaced = useMemo(
781
- () => row.children.map(c => prefixFieldNames(c, prefix)),
782
- [row.children, prefix],
783
- )
784
- const headerLabel = row.itemLabel ?? `Item ${index + 1}`
785
- // Row coords for dotted-path text leaves — composes fragment-key
786
- // `${arrayName}.${rowId}.${fieldName}` for the Tiptap-backed collab
787
- // renderer (see collab-row-text-tiptap-backed.md Phase 1).
788
- const rowCoords = useMemo(
789
- () => ({ arrayName: name, rowIndex: index, rowId: row.id }),
790
- [name, index, row.id],
791
- )
792
-
793
- // Hidden rows: render only the inputs (and __id) inside a display:none
794
- // wrapper so their values round-trip through FormData on submit. No
795
- // chrome, no drag wiring, no labels — `itemHidden` is purely UX.
796
- if (row.hidden) {
797
- return (
798
- <RowCoordsContext.Provider value={rowCoords}>
799
- <div style={{ display: 'none' }} data-pilotiq-repeater-row="hidden">
800
- <input type="hidden" name={`${prefix}.__id`} value={row.id} readOnly />
801
- <SchemaRenderer elements={namespaced} />
802
- </div>
803
- </RowCoordsContext.Provider>
804
- )
805
- }
806
-
807
- // Per-row capability gates — `itemCan*(rule)` server-resolved.
808
- // `canDelete / canClone / canReorder === false` removes the matching
809
- // button on this row; absent flags fall through to the global defaults.
810
- const canDelete = row.canDelete !== false
811
- const canClone = row.canClone !== false
812
- const canReorder = row.canReorder !== false
813
-
814
- // Drag source lives on the grip `<span>` (see `ReorderGrip`). The
815
- // row container is only the drop target — `dragend` bubbles, so source
816
- // cleanup still reaches it. Splitting source from target this way lets
817
- // row contents host a Tiptap contenteditable without the editor's
818
- // text-selection handler swallowing the row's dragstart.
819
- // Pinned rows (`canReorder === false`) lose the grip; others can still
820
- // accept drops — see itemCanReorder docstring.
821
- const rowRef = useRef<HTMLDivElement>(null)
822
- const dragEnabled = reorderable && !disabled && canReorder
823
- const containerDropTargetProps = dragEnabled
824
- ? { onDragOver, onDrop, onDragEnd }
825
- : {}
826
- const gripDragHandleProps = dragEnabled
827
- ? {
828
- draggable: true as const,
829
- onDragStart: (e: React.DragEvent<HTMLElement>): void => {
830
- // Use the row element as the drag preview so the user still
831
- // sees the whole row floating, not just the grip icon.
832
- if (rowRef.current) e.dataTransfer.setDragImage(rowRef.current, 0, 0)
833
- onDragStart(e)
834
- },
835
- }
836
- : undefined
837
-
838
- // Simple-mode: flatten the row to one input + inline action strip — no
839
- // header, no border, no collapse (a single field has nothing to collapse).
840
- // Reorder + delete still work; clone + extraActions are intentionally
841
- // dropped since there's no "row identity" worth duplicating in the flat
842
- // shape, and per-row buttons read cluttered next to a one-input row.
843
- // FieldShell renders a label by default — for simple rows we want flush
844
- // inputs (Filament's posture too), so we suppress the inner label by
845
- // wrapping in a class that hides the FieldShell's label slot.
846
- if (simple) {
847
- return (
848
- <RowCoordsContext.Provider value={rowCoords}>
849
- <div
850
- ref={rowRef}
851
- className={`flex items-center gap-2 transition-opacity ${isDragging ? 'opacity-50' : ''}`}
852
- data-pilotiq-repeater-row="simple"
853
- {...containerDropTargetProps}
854
- >
855
- <input type="hidden" name={`${prefix}.__id`} value={row.id} readOnly />
856
- {reorderable && canReorder && (
857
- <ReorderGrip disabled={disabled} buttons={buttons} dragHandleProps={gripDragHandleProps} />
858
- )}
859
- <div className="flex-1 [&_label]:sr-only">
860
- <SchemaRenderer elements={namespaced} />
861
- </div>
862
- {reorderable && canReorder && (
863
- <>
864
- <RowChromeIconButton
865
- defaults={DEFAULT_MOVE_UP}
866
- override={buttons?.moveUp}
867
- disabled={disabled || isFirstVisible}
868
- onClick={onMoveUp}
869
- />
870
- <RowChromeIconButton
871
- defaults={DEFAULT_MOVE_DOWN}
872
- override={buttons?.moveDown}
873
- disabled={disabled || isLastVisible}
874
- onClick={onMoveDown}
875
- />
876
- </>
877
- )}
878
- {canDelete && (
879
- <RowChromeIconButton
880
- defaults={DEFAULT_DELETE}
881
- override={buttons?.delete}
882
- disabled={disabled || atMin}
883
- onClick={onRemove}
884
- />
885
- )}
886
- </div>
887
- </RowCoordsContext.Provider>
888
- )
889
- }
890
-
891
- return (
892
- <RowCoordsContext.Provider value={rowCoords}>
893
- <div
894
- ref={rowRef}
895
- className={`rounded-md border bg-card transition-opacity ${isDragging ? 'opacity-50' : ''}`}
896
- data-pilotiq-repeater-row=""
897
- {...containerDropTargetProps}
898
- >
899
- <div className="flex items-center gap-2 border-b px-3 py-2">
900
- {reorderable && canReorder && (
901
- <ReorderGrip disabled={disabled} buttons={buttons} dragHandleProps={gripDragHandleProps} />
902
- )}
903
- {collapsible && (
904
- <CollapseChevron
905
- isCollapsed={isCollapsed}
906
- disabled={disabled}
907
- buttons={buttons}
908
- onToggle={onToggleCollapse}
909
- />
910
- )}
911
- <span className="flex-1 truncate text-sm font-medium">{headerLabel}</span>
912
- <input type="hidden" name={`${prefix}.__id`} value={row.id} readOnly />
913
- {reorderable && canReorder && (
914
- <>
915
- <RowChromeIconButton
916
- defaults={DEFAULT_MOVE_UP}
917
- override={buttons?.moveUp}
918
- disabled={disabled || isFirstVisible}
919
- onClick={onMoveUp}
920
- />
921
- <RowChromeIconButton
922
- defaults={DEFAULT_MOVE_DOWN}
923
- override={buttons?.moveDown}
924
- disabled={disabled || isLastVisible}
925
- onClick={onMoveDown}
926
- />
927
- </>
928
- )}
929
- {row.extraActions && row.extraActions.length > 0 && (
930
- <ExtraActionStrip
931
- actions={row.extraActions}
932
- rowPath={rowPath}
933
- disabled={disabled}
934
- />
935
- )}
936
- {cloneable && canClone && (
937
- <RowChromeIconButton
938
- defaults={DEFAULT_CLONE}
939
- override={buttons?.clone}
940
- disabled={disabled || atMax}
941
- onClick={onClone}
942
- />
943
- )}
944
- {canDelete && (
945
- <RowChromeIconButton
946
- defaults={DEFAULT_DELETE}
947
- override={buttons?.delete}
948
- disabled={disabled || atMin}
949
- onClick={onRemove}
950
- />
951
- )}
952
- </div>
953
-
954
- {/* Body — kept mounted (display:none on collapse) so uncontrolled
955
- input values persist across collapse toggles. */}
956
- <div
957
- className="p-3"
958
- style={isCollapsed ? { display: 'none' } : undefined}
959
- >
960
- {columns > 1
961
- ? (
962
- <div
963
- className="grid gap-3"
964
- style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }}
965
- >
966
- <SchemaRenderer elements={namespaced} />
967
- </div>
968
- )
969
- : <SchemaRenderer elements={namespaced} />}
970
- </div>
971
- </div>
972
- </RowCoordsContext.Provider>
973
- )
974
- }
975
-
976
- /**
977
- * Per-row extraItemActions strip. Each button dispatches its handler
978
- * action by snapshotting the parent `<form>` (so the server's
979
- * `coerceFormValues` sees the row's submitted fields), then POSTs to the
980
- * action's `dispatchUrl` with `_rowPath="<fieldName>.<index>"` in the
981
- * body — the server uses that path to navigate into the field's row
982
- * array and stamp `ctx.row = { index, id, values, fieldName }` on the
983
- * handler context.
984
- *
985
- * v1 — handler-style only. `href` / `method` / modal-form actions inside
986
- * `extraItemActions` are accepted by the type system but render here as
987
- * no-op buttons (they have neither a `dispatchUrl` nor a row-aware fetch
988
- * branch). Filament parity for those modes can land in a follow-up.
989
- *
990
- * Disabled actions render greyed out + skip dispatch (matches the
991
- * `meta.disabled` stamp from `resolveExtraItemActions`).
992
- */
993
- export function ExtraActionStrip({
994
- actions, rowPath, disabled,
995
- }: {
996
- actions: ElementMeta[]
997
- rowPath: string
998
- disabled: boolean
999
- }): React.ReactElement {
1000
- const navigate = useNavigate()
1001
- const { notify } = useToast()
1002
-
1003
- const onClick = (action: ElementMeta) => async (e: React.MouseEvent<HTMLButtonElement>): Promise<void> => {
1004
- if (disabled || action['disabled']) return
1005
- const dispatchUrl = action['dispatchUrl'] as string | undefined
1006
- if (!dispatchUrl) return
1007
- const form = e.currentTarget.closest('form')
1008
- const snapshot = form ? new FormData(form) : new FormData()
1009
- await dispatchHandlerAction(
1010
- dispatchUrl,
1011
- [],
1012
- navigate,
1013
- notify,
1014
- { _rowPath: rowPath },
1015
- snapshot,
1016
- )
1017
- }
1018
-
1019
- return (
1020
- <>
1021
- {actions.map((a, i) => {
1022
- const label = String(a['label'] ?? a['name'] ?? '')
1023
- const tooltip = (a['tooltip'] as string | undefined) ?? label
1024
- const isDisabled = disabled || Boolean(a['disabled'])
1025
- const destructive = Boolean(a['destructive'])
1026
- return (
1027
- <button
1028
- key={i}
1029
- type="button"
1030
- onClick={onClick(a)}
1031
- disabled={isDisabled}
1032
- aria-label={label}
1033
- title={tooltip}
1034
- data-action-name={a['name']}
1035
- className={`text-muted-foreground hover:text-foreground disabled:opacity-30 ${destructive ? 'hover:text-destructive' : ''}`.trim()}
1036
- >
1037
- <span className="text-xs font-medium">{label}</span>
1038
- </button>
1039
- )
1040
- })}
1041
- </>
1042
- )
1043
- }
1044
-
1045
- /**
1046
- * 2px-tall horizontal accent line rendered between rows when the user
1047
- * drags a row over a valid drop boundary. Uses `pointer-events: none`
1048
- * so the underlying row's `dragover` keeps firing — without this, the
1049
- * indicator would steal events and the drop slot would flicker.
1050
- */
1051
- function DropIndicator(): React.ReactElement {
1052
- return (
1053
- <div
1054
- aria-hidden="true"
1055
- className="pointer-events-none h-0.5 rounded-full bg-primary"
1056
- />
1057
- )
1058
- }
1059
-
1060
- /**
1061
- * Table-mode layout. Renders rows as `<tr>` and inner schema fields as
1062
- * `<td>` cells, with the supplied column headers in `<thead>`.
1063
- *
1064
- * Cells call `prefixFieldNames` to emit row-scoped flat dotted names
1065
- * (`items.0.name`, etc.) — same wire shape as the card layout, so
1066
- * `coerceFormValues` re-groups identically. The first inner FieldShell
1067
- * label is suppressed via a parent `[&_label]:sr-only` since the
1068
- * column header carries the labelling.
1069
- *
1070
- * The actions column hosts grip / Up / Down / clone / extraActions /
1071
- * delete affordances. We render it unconditionally so column count
1072
- * stays stable across rows even when individual buttons disable.
1073
- */
1074
- function RepeaterTableLayout({
1075
- rows, name, disabled, columns, addLabel, buttons, atMin, atMax,
1076
- reorderable, cloneable,
1077
- firstVisibleIdx, lastVisibleIdx, hasVisibleRow,
1078
- dragId,
1079
- onAdd, onMoveUp, onMoveDown, onClone, onRemove,
1080
- onContainerChange, onContainerBlur,
1081
- onRowDragStart, onRowDragOver, onRowDrop, onRowDragEnd,
1082
- }: {
1083
- rows: RowState[]
1084
- name: string
1085
- disabled: boolean
1086
- columns: TableColumnShape[]
1087
- addLabel: string
1088
- buttons: RowButtonsMeta | undefined
1089
- atMin: boolean
1090
- atMax: boolean
1091
- reorderable: boolean
1092
- cloneable: boolean
1093
- firstVisibleIdx: number
1094
- lastVisibleIdx: number
1095
- hasVisibleRow: boolean
1096
- dragId: string | null
1097
- onAdd: () => void
1098
- onMoveUp: (id: string) => void
1099
- onMoveDown: (id: string) => void
1100
- onClone: (id: string) => void
1101
- onRemove: (id: string) => void
1102
- onContainerChange: (e: React.ChangeEvent<HTMLDivElement>) => void
1103
- onContainerBlur: (e: React.FocusEvent<HTMLDivElement>) => void
1104
- onRowDragStart: (id: string) => (e: React.DragEvent<HTMLElement>) => void
1105
- onRowDragOver: (idx: number) => (e: React.DragEvent<HTMLElement>) => void
1106
- onRowDrop: (e: React.DragEvent<HTMLElement>) => void
1107
- onRowDragEnd: (e: React.DragEvent<HTMLElement>) => void
1108
- }): React.ReactElement {
1109
- // The container div carries the change/blur delegates so live() events
1110
- // bubble identically to the card path. `[&_label]:sr-only` hides the
1111
- // FieldShell label across every cell (column header carries it).
1112
- return (
1113
- <div
1114
- className="flex flex-col gap-3"
1115
- onChange={onContainerChange}
1116
- onBlur={onContainerBlur}
1117
- >
1118
- {!hasVisibleRow && (
1119
- <div className="rounded-md border border-dashed px-4 py-6 text-center text-sm text-muted-foreground">
1120
- No items yet. Click {addLabel} to start.
1121
- </div>
1122
- )}
1123
-
1124
- {hasVisibleRow && (
1125
- <div className="overflow-x-auto rounded-md border [&_label]:sr-only">
1126
- <table className="w-full border-collapse text-sm">
1127
- <colgroup>
1128
- {columns.map((c, i) => (
1129
- <col key={i} style={c.width ? { width: c.width } : undefined} />
1130
- ))}
1131
- <col />
1132
- </colgroup>
1133
- <thead className="bg-muted/40">
1134
- <tr>
1135
- {columns.map((c, i) => (
1136
- <th
1137
- key={i}
1138
- scope="col"
1139
- className={`px-3 py-2 text-xs font-medium text-muted-foreground ${alignClass(c.alignment)}`}
1140
- >
1141
- {c.label}
1142
- {c.required && <span className="ml-0.5 text-destructive">*</span>}
1143
- </th>
1144
- ))}
1145
- <th scope="col" className="w-px" aria-label="Actions" />
1146
- </tr>
1147
- </thead>
1148
- <tbody>
1149
- {rows.map((row, i) => (
1150
- <RepeaterTableRow
1151
- key={row.id}
1152
- row={row}
1153
- index={i}
1154
- name={name}
1155
- disabled={disabled}
1156
- columns={columns}
1157
- reorderable={reorderable}
1158
- cloneable={cloneable}
1159
- buttons={buttons}
1160
- isFirstVisible={i === firstVisibleIdx}
1161
- isLastVisible={i === lastVisibleIdx}
1162
- atMin={atMin}
1163
- atMax={atMax}
1164
- isDragging={dragId === row.id}
1165
- rowPath={`${name}.${i}`}
1166
- onMoveUp={() => onMoveUp(row.id)}
1167
- onMoveDown={() => onMoveDown(row.id)}
1168
- onClone={() => onClone(row.id)}
1169
- onRemove={() => onRemove(row.id)}
1170
- onDragStart={onRowDragStart(row.id)}
1171
- onDragOver={onRowDragOver(i)}
1172
- onDrop={onRowDrop}
1173
- onDragEnd={onRowDragEnd}
1174
- />
1175
- ))}
1176
- </tbody>
1177
- </table>
1178
- </div>
1179
- )}
1180
-
1181
- <AddRowButton
1182
- label={addLabel}
1183
- buttons={buttons}
1184
- disabled={disabled || atMax}
1185
- onClick={onAdd}
1186
- />
1187
- </div>
1188
- )
1189
- }
1190
-
1191
- function alignClass(a: 'left' | 'center' | 'right' | undefined): string {
1192
- if (a === 'right') return 'text-right'
1193
- if (a === 'center') return 'text-center'
1194
- return 'text-left'
1195
- }
1196
-
1197
- function RepeaterTableRow({
1198
- row, index, name, disabled, columns, reorderable, cloneable, buttons,
1199
- isFirstVisible, isLastVisible, atMin, atMax, isDragging, rowPath,
1200
- onMoveUp, onMoveDown, onClone, onRemove,
1201
- onDragStart, onDragOver, onDrop, onDragEnd,
1202
- }: {
1203
- row: RowState
1204
- index: number
1205
- name: string
1206
- disabled: boolean
1207
- columns: TableColumnShape[]
1208
- reorderable: boolean
1209
- cloneable: boolean
1210
- buttons: RowButtonsMeta | undefined
1211
- isFirstVisible: boolean
1212
- isLastVisible: boolean
1213
- atMin: boolean
1214
- atMax: boolean
1215
- isDragging: boolean
1216
- rowPath: string
1217
- onMoveUp: () => void
1218
- onMoveDown: () => void
1219
- onClone: () => void
1220
- onRemove: () => void
1221
- onDragStart: (e: React.DragEvent<HTMLElement>) => void
1222
- onDragOver: (e: React.DragEvent<HTMLElement>) => void
1223
- onDrop: (e: React.DragEvent<HTMLElement>) => void
1224
- onDragEnd: (e: React.DragEvent<HTMLElement>) => void
1225
- }): React.ReactElement {
1226
- const prefix = `${name}.${index}`
1227
- const namespaced = useMemo(
1228
- () => row.children.map(c => prefixFieldNames(c, prefix)),
1229
- [row.children, prefix],
1230
- )
1231
- const rowCoords = useMemo(
1232
- () => ({ arrayName: name, rowIndex: index, rowId: row.id }),
1233
- [name, index, row.id],
1234
- )
1235
-
1236
- if (row.hidden) {
1237
- // Render the hidden envelope as a single full-span cell so column
1238
- // count stays valid; `display:none` ensures the row is invisible but
1239
- // still in the form's submit. Using `<tr style="display:none">`
1240
- // would warn under React strict-mode in Firefox; the wrapping cell
1241
- // keeps the markup HTML-valid.
1242
- //
1243
- // The provider wraps the cell rather than the `<tr>` because React's
1244
- // table-row whitelisting only accepts `<th>/<td>` children, not a
1245
- // context provider; the provider is a no-DOM wrapper so it sits
1246
- // inside the cell fine.
1247
- return (
1248
- <tr style={{ display: 'none' }} data-pilotiq-repeater-row="hidden">
1249
- <td colSpan={columns.length + 1}>
1250
- <RowCoordsContext.Provider value={rowCoords}>
1251
- <input type="hidden" name={`${prefix}.__id`} value={row.id} readOnly />
1252
- <SchemaRenderer elements={namespaced} />
1253
- </RowCoordsContext.Provider>
1254
- </td>
1255
- </tr>
1256
- )
1257
- }
1258
-
1259
- // Pair each column header (in order) with the corresponding inner
1260
- // field meta. Extra fields beyond the column count fall through the
1261
- // last cell as stacked items — a misconfiguration but better than
1262
- // dropping them silently.
1263
- const fieldsPerCell: ElementMeta[][] = columns.map((_c, i) =>
1264
- i === columns.length - 1 ? namespaced.slice(i) : namespaced.slice(i, i + 1),
1265
- )
1266
-
1267
- // Per-row capability gates — see RepeaterRow for the contract.
1268
- const canDelete = row.canDelete !== false
1269
- const canClone = row.canClone !== false
1270
- const canReorder = row.canReorder !== false
1271
-
1272
- // Drag source on the grip, drop target on the `<tr>` — see RepeaterRow.
1273
- const rowRef = useRef<HTMLTableRowElement>(null)
1274
- const dragEnabled = reorderable && !disabled && canReorder
1275
- const containerDropTargetProps = dragEnabled
1276
- ? { onDragOver, onDrop, onDragEnd }
1277
- : {}
1278
- const gripDragHandleProps = dragEnabled
1279
- ? {
1280
- draggable: true as const,
1281
- onDragStart: (e: React.DragEvent<HTMLElement>): void => {
1282
- if (rowRef.current) e.dataTransfer.setDragImage(rowRef.current, 0, 0)
1283
- onDragStart(e)
1284
- },
1285
- }
1286
- : undefined
1287
-
1288
- return (
1289
- <RowCoordsContext.Provider value={rowCoords}>
1290
- <tr
1291
- ref={rowRef}
1292
- className={`border-t align-top ${isDragging ? 'opacity-50' : ''}`}
1293
- data-pilotiq-repeater-row=""
1294
- {...containerDropTargetProps}
1295
- >
1296
- <input type="hidden" name={`${prefix}.__id`} value={row.id} readOnly />
1297
- {columns.map((c, i) => (
1298
- <td key={i} className={`px-3 py-2 ${alignClass(c.alignment)}`}>
1299
- <SchemaRenderer elements={fieldsPerCell[i] ?? []} />
1300
- </td>
1301
- ))}
1302
- <td className="whitespace-nowrap px-3 py-2 text-right">
1303
- <div className="inline-flex items-center gap-1">
1304
- {reorderable && canReorder && (
1305
- <>
1306
- <ReorderGrip disabled={disabled} buttons={buttons} dragHandleProps={gripDragHandleProps} />
1307
- <RowChromeIconButton
1308
- defaults={DEFAULT_MOVE_UP}
1309
- override={buttons?.moveUp}
1310
- disabled={disabled || isFirstVisible}
1311
- onClick={onMoveUp}
1312
- />
1313
- <RowChromeIconButton
1314
- defaults={DEFAULT_MOVE_DOWN}
1315
- override={buttons?.moveDown}
1316
- disabled={disabled || isLastVisible}
1317
- onClick={onMoveDown}
1318
- />
1319
- </>
1320
- )}
1321
- {row.extraActions && row.extraActions.length > 0 && (
1322
- <ExtraActionStrip
1323
- actions={row.extraActions}
1324
- rowPath={rowPath}
1325
- disabled={disabled}
1326
- />
1327
- )}
1328
- {cloneable && canClone && (
1329
- <RowChromeIconButton
1330
- defaults={DEFAULT_CLONE}
1331
- override={buttons?.clone}
1332
- disabled={disabled || atMax}
1333
- onClick={onClone}
1334
- />
1335
- )}
1336
- {canDelete && (
1337
- <RowChromeIconButton
1338
- defaults={DEFAULT_DELETE}
1339
- override={buttons?.delete}
1340
- disabled={disabled || atMin}
1341
- onClick={onRemove}
1342
- />
1343
- )}
1344
- </div>
1345
- </td>
1346
- </tr>
1347
- </RowCoordsContext.Provider>
1348
- )
1349
- }
1350
-
1351
- interface RepeaterMetaShape {
1352
- rows?: Array<{
1353
- id: string
1354
- children: ElementMeta[]
1355
- itemLabel?: string
1356
- hidden?: boolean
1357
- extraActions?: ElementMeta[]
1358
- canDelete?: false
1359
- canClone?: false
1360
- canReorder?: false
1361
- }>
1362
- template?: ElementMeta[]
1363
- columns?: number
1364
- minItems?: number
1365
- maxItems?: number
1366
- defaultItems?: number
1367
- reorderable?: boolean
1368
- collapsible?: boolean
1369
- defaultCollapsed?: boolean
1370
- accordion?: boolean
1371
- cloneable?: boolean
1372
- addActionLabel?: string
1373
- simple?: boolean
1374
- grid?: number
1375
- table?: { columns: TableColumnShape[] }
1376
- buttons?: RowButtonsMeta
1377
- }
1378
-
1379
- interface TableColumnShape {
1380
- label: string
1381
- alignment?: 'left' | 'center' | 'right'
1382
- width?: string
1383
- required?: boolean
1384
- }
1385
-
1386
- /**
1387
- * Recursively prefix every Field meta's `name` with a row-scoped path.
1388
- * Inner Repeaters get their own per-row prefixing so nested Repeater
1389
- * row inputs land at `items.0.modifiers.1.name`.
1390
- */
1391
- function prefixFieldNames(el: ElementMeta, prefix: string): ElementMeta {
1392
- if (el.type === 'field' && typeof el['name'] === 'string') {
1393
- const innerName = el['name']
1394
- const newName = `${prefix}.${innerName}`
1395
- if (el['fieldType'] === 'repeater') {
1396
- const m = el as ElementMeta & RepeaterMetaShape
1397
- const rows = m.rows ?? []
1398
- const tpl = m.template ?? []
1399
- return {
1400
- ...el,
1401
- name: newName,
1402
- rows: rows.map(r => ({
1403
- ...r,
1404
- children: r.children.map(c => prefixFieldNames(c, `${newName}.${rows.indexOf(r)}`)),
1405
- })),
1406
- template: tpl.map(c => prefixFieldNames(c, `${newName}.0`)),
1407
- }
1408
- }
1409
- return { ...el, name: newName }
1410
- }
1411
- if (Array.isArray(el.children)) {
1412
- return {
1413
- ...el,
1414
- children: (el.children as ElementMeta[]).map(c => prefixFieldNames(c, prefix)),
1415
- }
1416
- }
1417
- return el
1418
- }
1419
-
1420
-