@pilotiq/pilotiq 0.23.1 → 0.24.2

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 (500) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/boost/guidelines.md +566 -0
  3. package/boost/skills/pilotiq-fields/SKILL.md +47 -0
  4. package/boost/skills/pilotiq-fields/rules/field-catalog.md +288 -0
  5. package/boost/skills/pilotiq-fields/rules/reactive-fields.md +199 -0
  6. package/boost/skills/pilotiq-fields/rules/validation.md +198 -0
  7. package/boost/skills/pilotiq-relations/SKILL.md +47 -0
  8. package/boost/skills/pilotiq-relations/rules/relation-managers.md +256 -0
  9. package/boost/skills/pilotiq-relations/rules/repeater-relationship.md +177 -0
  10. package/boost/skills/pilotiq-resource/SKILL.md +61 -0
  11. package/boost/skills/pilotiq-resource/rules/authorization.md +242 -0
  12. package/boost/skills/pilotiq-resource/rules/defining-resources.md +228 -0
  13. package/boost/skills/pilotiq-resource/rules/page-overrides.md +296 -0
  14. package/dist/actions/exportFactory.d.ts +10 -0
  15. package/dist/actions/exportFactory.d.ts.map +1 -1
  16. package/dist/actions/exportFactory.js +10 -0
  17. package/dist/actions/exportFactory.js.map +1 -1
  18. package/dist/react/CollabRoomContext.d.ts +5 -5
  19. package/dist/react/index.d.ts +0 -1
  20. package/dist/react/index.d.ts.map +1 -1
  21. package/dist/react/index.js +0 -1
  22. package/dist/react/index.js.map +1 -1
  23. package/dist/routes/helpers.d.ts.map +1 -1
  24. package/dist/routes/helpers.js +6 -2
  25. package/dist/routes/helpers.js.map +1 -1
  26. package/dist/routes/relations.d.ts.map +1 -1
  27. package/dist/routes/relations.js +12 -0
  28. package/dist/routes/relations.js.map +1 -1
  29. package/package.json +6 -1
  30. package/.turbo/turbo-build.log +0 -8
  31. package/CLAUDE.md +0 -265
  32. package/dist/react/useCollabSeed.d.ts +0 -23
  33. package/dist/react/useCollabSeed.d.ts.map +0 -1
  34. package/dist/react/useCollabSeed.js +0 -82
  35. package/dist/react/useCollabSeed.js.map +0 -1
  36. package/src/Cluster.test.ts +0 -283
  37. package/src/Cluster.ts +0 -83
  38. package/src/Column.test.ts +0 -199
  39. package/src/Column.ts +0 -710
  40. package/src/Global.test.ts +0 -367
  41. package/src/Global.ts +0 -169
  42. package/src/Page.test.ts +0 -114
  43. package/src/Page.ts +0 -208
  44. package/src/Pilotiq.perf.test.ts +0 -252
  45. package/src/Pilotiq.test.ts +0 -129
  46. package/src/Pilotiq.ts +0 -1158
  47. package/src/PilotiqRegistry.ts +0 -36
  48. package/src/PilotiqServiceProvider.ts +0 -121
  49. package/src/RelationManager.test.ts +0 -400
  50. package/src/RelationManager.ts +0 -527
  51. package/src/RenderHook.test.ts +0 -252
  52. package/src/RenderHook.ts +0 -242
  53. package/src/Resource.test.ts +0 -284
  54. package/src/Resource.ts +0 -526
  55. package/src/RightPanel.test.ts +0 -202
  56. package/src/RightPanel.ts +0 -132
  57. package/src/Tab.test.ts +0 -91
  58. package/src/Tab.ts +0 -156
  59. package/src/UserMenuItem.ts +0 -145
  60. package/src/actions/Action.test.ts +0 -2526
  61. package/src/actions/Action.ts +0 -1515
  62. package/src/actions/ActionGroup.test.ts +0 -112
  63. package/src/actions/ActionGroup.ts +0 -173
  64. package/src/actions/attachFactory.ts +0 -172
  65. package/src/actions/bulkFactories.ts +0 -168
  66. package/src/actions/crudFactories.ts +0 -220
  67. package/src/actions/exportFactory.ts +0 -215
  68. package/src/actions/factoryHelpers.ts +0 -177
  69. package/src/actions/importFactory.ts +0 -243
  70. package/src/actions/index.ts +0 -17
  71. package/src/actions/m2mFactories.ts +0 -193
  72. package/src/actions/relationFactories.ts +0 -372
  73. package/src/applyPageHooks.test.ts +0 -463
  74. package/src/applyPageHooks.ts +0 -330
  75. package/src/authorization.test.ts +0 -483
  76. package/src/breadcrumbs.test.ts +0 -238
  77. package/src/cells/coerce.test.ts +0 -85
  78. package/src/cells/coerce.ts +0 -84
  79. package/src/clusterPaths.ts +0 -35
  80. package/src/columns/BadgeColumn.test.ts +0 -54
  81. package/src/columns/BadgeColumn.ts +0 -32
  82. package/src/columns/BooleanColumn.test.ts +0 -41
  83. package/src/columns/BooleanColumn.ts +0 -18
  84. package/src/columns/ColorColumn.test.ts +0 -37
  85. package/src/columns/ColorColumn.ts +0 -38
  86. package/src/columns/IconColumn.test.ts +0 -54
  87. package/src/columns/IconColumn.ts +0 -37
  88. package/src/columns/ImageColumn.test.ts +0 -41
  89. package/src/columns/ImageColumn.ts +0 -28
  90. package/src/columns/SelectColumn.ts +0 -98
  91. package/src/columns/TextColumn.test.ts +0 -190
  92. package/src/columns/TextColumn.ts +0 -20
  93. package/src/columns/TextInputColumn.ts +0 -68
  94. package/src/columns/ToggleColumn.ts +0 -46
  95. package/src/columns/editableColumns.test.ts +0 -238
  96. package/src/columns/index.ts +0 -9
  97. package/src/defaultGlobalPages.ts +0 -95
  98. package/src/defaultPages.test.ts +0 -634
  99. package/src/defaultPages.ts +0 -617
  100. package/src/defaultViewPage.test.ts +0 -147
  101. package/src/elements/Form.test.ts +0 -223
  102. package/src/elements/Form.ts +0 -416
  103. package/src/elements/ListTabs.ts +0 -28
  104. package/src/elements/Table.test.ts +0 -422
  105. package/src/elements/Table.ts +0 -850
  106. package/src/elements/TableGroup.test.ts +0 -260
  107. package/src/elements/TableGroup.ts +0 -334
  108. package/src/elements/dispatchAction.test.ts +0 -463
  109. package/src/elements/dispatchAction.ts +0 -355
  110. package/src/elements/dispatchForm.test.ts +0 -477
  111. package/src/elements/dispatchForm.ts +0 -1993
  112. package/src/elements/dispatchTable.test.ts +0 -1514
  113. package/src/elements/dispatchTable.ts +0 -745
  114. package/src/elements/index.ts +0 -21
  115. package/src/entries/BadgeEntry.ts +0 -39
  116. package/src/entries/CodeEntry.test.ts +0 -40
  117. package/src/entries/CodeEntry.ts +0 -52
  118. package/src/entries/ColorEntry.ts +0 -63
  119. package/src/entries/ComponentEntry.test.ts +0 -173
  120. package/src/entries/ComponentEntry.ts +0 -95
  121. package/src/entries/Entry.ts +0 -304
  122. package/src/entries/IconEntry.ts +0 -49
  123. package/src/entries/ImageEntry.ts +0 -61
  124. package/src/entries/KeyValueEntry.ts +0 -47
  125. package/src/entries/RepeatableEntry.test.ts +0 -239
  126. package/src/entries/RepeatableEntry.ts +0 -173
  127. package/src/entries/TextEntry.test.ts +0 -394
  128. package/src/entries/TextEntry.ts +0 -60
  129. package/src/entries/index.ts +0 -12
  130. package/src/entries/leaves.test.ts +0 -306
  131. package/src/entries/registry.ts +0 -54
  132. package/src/fields/BuilderField.test.ts +0 -1188
  133. package/src/fields/BuilderField.ts +0 -605
  134. package/src/fields/BuilderRelationship.test.ts +0 -811
  135. package/src/fields/CheckboxField.test.ts +0 -44
  136. package/src/fields/CheckboxField.ts +0 -27
  137. package/src/fields/CheckboxListField.test.ts +0 -99
  138. package/src/fields/CheckboxListField.ts +0 -66
  139. package/src/fields/ColorPickerField.test.ts +0 -33
  140. package/src/fields/ColorPickerField.ts +0 -25
  141. package/src/fields/DateField.ts +0 -54
  142. package/src/fields/DateTimeField.test.ts +0 -55
  143. package/src/fields/EmailField.ts +0 -16
  144. package/src/fields/Field.test.ts +0 -654
  145. package/src/fields/Field.ts +0 -817
  146. package/src/fields/FileUploadField.test.ts +0 -143
  147. package/src/fields/FileUploadField.ts +0 -159
  148. package/src/fields/HiddenField.test.ts +0 -27
  149. package/src/fields/HiddenField.ts +0 -28
  150. package/src/fields/KeyValueField.test.ts +0 -105
  151. package/src/fields/KeyValueField.ts +0 -55
  152. package/src/fields/MarkdownField.test.ts +0 -167
  153. package/src/fields/MarkdownField.ts +0 -162
  154. package/src/fields/NumberField.ts +0 -33
  155. package/src/fields/RadioField.test.ts +0 -94
  156. package/src/fields/RadioField.ts +0 -67
  157. package/src/fields/RepeaterField.test.ts +0 -1806
  158. package/src/fields/RepeaterField.ts +0 -939
  159. package/src/fields/RepeaterRelationship.test.ts +0 -1923
  160. package/src/fields/RepeaterSimple.test.ts +0 -248
  161. package/src/fields/RowButton.test.ts +0 -219
  162. package/src/fields/RowButton.ts +0 -135
  163. package/src/fields/SelectField.test.ts +0 -192
  164. package/src/fields/SelectField.ts +0 -235
  165. package/src/fields/SliderField.test.ts +0 -50
  166. package/src/fields/SliderField.ts +0 -53
  167. package/src/fields/SlugField.ts +0 -24
  168. package/src/fields/TagsInputField.test.ts +0 -154
  169. package/src/fields/TagsInputField.ts +0 -133
  170. package/src/fields/TextField.test.ts +0 -213
  171. package/src/fields/TextField.ts +0 -177
  172. package/src/fields/TextareaField.test.ts +0 -58
  173. package/src/fields/TextareaField.ts +0 -59
  174. package/src/fields/ToggleButtonsField.test.ts +0 -106
  175. package/src/fields/ToggleButtonsField.ts +0 -59
  176. package/src/fields/ToggleField.ts +0 -16
  177. package/src/fields/disableOptionsWhenSelectedInSiblingRepeaterItems.test.ts +0 -319
  178. package/src/fields/optionsResolver.ts +0 -95
  179. package/src/fields/resolveField.ts +0 -28
  180. package/src/filters/BooleanFilter.ts +0 -35
  181. package/src/filters/DateRangeFilter.test.ts +0 -194
  182. package/src/filters/DateRangeFilter.ts +0 -148
  183. package/src/filters/Filter.test.ts +0 -268
  184. package/src/filters/Filter.ts +0 -184
  185. package/src/filters/FormFilter.test.ts +0 -238
  186. package/src/filters/FormFilter.ts +0 -215
  187. package/src/filters/MultiSelectFilter.test.ts +0 -119
  188. package/src/filters/MultiSelectFilter.ts +0 -78
  189. package/src/filters/QueryBuilderFilter.test.ts +0 -662
  190. package/src/filters/QueryBuilderFilter.ts +0 -398
  191. package/src/filters/SelectFilter.ts +0 -46
  192. package/src/filters/TernaryFilter.test.ts +0 -160
  193. package/src/filters/TernaryFilter.ts +0 -72
  194. package/src/filters/TrashedFilter.test.ts +0 -149
  195. package/src/filters/TrashedFilter.ts +0 -55
  196. package/src/filters/queryBuilder/BooleanConstraint.ts +0 -31
  197. package/src/filters/queryBuilder/Constraint.ts +0 -115
  198. package/src/filters/queryBuilder/DateConstraint.ts +0 -69
  199. package/src/filters/queryBuilder/NumberConstraint.ts +0 -66
  200. package/src/filters/queryBuilder/SelectConstraint.ts +0 -72
  201. package/src/filters/queryBuilder/TextConstraint.ts +0 -64
  202. package/src/filters/queryBuilder/index.ts +0 -12
  203. package/src/icons/index.ts +0 -2
  204. package/src/icons/lucide.ts +0 -204
  205. package/src/icons/registry.test.ts +0 -56
  206. package/src/icons/registry.ts +0 -41
  207. package/src/icons/types.ts +0 -47
  208. package/src/index.ts +0 -525
  209. package/src/io/csv.test.ts +0 -142
  210. package/src/io/csv.ts +0 -170
  211. package/src/nestedRelationManagerData.test.ts +0 -547
  212. package/src/notifications/Notification.test.ts +0 -210
  213. package/src/notifications/Notification.ts +0 -354
  214. package/src/notifications/broadcast.test.ts +0 -110
  215. package/src/notifications/broadcast.ts +0 -95
  216. package/src/notifications/database.test.ts +0 -383
  217. package/src/notifications/database.ts +0 -398
  218. package/src/notifications/databaseNotifications.test.ts +0 -187
  219. package/src/notifications/dispatchNotificationAction.test.ts +0 -341
  220. package/src/notifications/dispatchNotificationAction.ts +0 -142
  221. package/src/notifications/flash.test.ts +0 -89
  222. package/src/notifications/flash.ts +0 -71
  223. package/src/notifications/index.ts +0 -45
  224. package/src/notifications/registerBroadcastAuth.test.ts +0 -134
  225. package/src/notifications/registerBroadcastAuth.ts +0 -100
  226. package/src/notifications/resolveSavedNotification.test.ts +0 -82
  227. package/src/notifications/resolveSavedNotification.ts +0 -59
  228. package/src/notifications/types.ts +0 -93
  229. package/src/orm/m2mAccessor.ts +0 -66
  230. package/src/orm/modelDefaults.test.ts +0 -633
  231. package/src/orm/modelDefaults.ts +0 -666
  232. package/src/pageData/breadcrumbs.ts +0 -288
  233. package/src/pageData/forms.ts +0 -578
  234. package/src/pageData/helpers.ts +0 -857
  235. package/src/pageData/misc.ts +0 -347
  236. package/src/pageData/navigation.ts +0 -842
  237. package/src/pageData/relationPages.ts +0 -1248
  238. package/src/pageData/relationTabs.ts +0 -286
  239. package/src/pageData/resourcePages.ts +0 -609
  240. package/src/pageData.test.ts +0 -1545
  241. package/src/pageData.ts +0 -341
  242. package/src/plugins/index.ts +0 -8
  243. package/src/plugins/themeEditor.test.ts +0 -36
  244. package/src/plugins/themeEditor.ts +0 -45
  245. package/src/react/AppShell.tsx +0 -251
  246. package/src/react/CollabExtensionFactoryRegistry.ts +0 -55
  247. package/src/react/CollabRoomContext.ts +0 -98
  248. package/src/react/CollabTextRendererRegistry.ts +0 -102
  249. package/src/react/CommandPalette.tsx +0 -375
  250. package/src/react/CurrentUserContext.tsx +0 -50
  251. package/src/react/CustomPageWrapperGate.tsx +0 -69
  252. package/src/react/CustomPageWrapperRegistry.ts +0 -45
  253. package/src/react/FieldFocusReporterRegistry.ts +0 -37
  254. package/src/react/FieldLabelSlotRegistry.ts +0 -30
  255. package/src/react/FieldPresenceRegistry.ts +0 -46
  256. package/src/react/FormCollabBindingRegistry.ts +0 -242
  257. package/src/react/FormStateContext.tsx +0 -591
  258. package/src/react/HeadHooks.tsx +0 -126
  259. package/src/react/MarkdownEditorRegistry.test.ts +0 -38
  260. package/src/react/MarkdownEditorRegistry.ts +0 -107
  261. package/src/react/NotificationActionStrip.tsx +0 -263
  262. package/src/react/NotificationBell.tsx +0 -426
  263. package/src/react/PendingSuggestionApplierRegistry.test.ts +0 -97
  264. package/src/react/PendingSuggestionApplierRegistry.ts +0 -98
  265. package/src/react/PendingSuggestionOverlayRegistry.ts +0 -54
  266. package/src/react/PendingSuggestionsContext.tsx +0 -172
  267. package/src/react/RecordWrapperGate.tsx +0 -58
  268. package/src/react/RecordWrapperRegistry.ts +0 -39
  269. package/src/react/RenderHookSlot.tsx +0 -32
  270. package/src/react/RightSidebar.tsx +0 -257
  271. package/src/react/RightSidebarContext.tsx +0 -234
  272. package/src/react/RightSidebarTrigger.tsx +0 -53
  273. package/src/react/RowCoordsContext.tsx +0 -23
  274. package/src/react/SchemaRenderer.tsx +0 -549
  275. package/src/react/SearchTrigger.tsx +0 -46
  276. package/src/react/ThemeProvider.tsx +0 -93
  277. package/src/react/ThemeSettingsPage.tsx +0 -579
  278. package/src/react/ThemeToggle.tsx +0 -20
  279. package/src/react/Toaster.tsx +0 -158
  280. package/src/react/UserMenu.tsx +0 -196
  281. package/src/react/WidgetDataContext.tsx +0 -157
  282. package/src/react/cells/EditableCell.tsx +0 -389
  283. package/src/react/component-slots.test.ts +0 -103
  284. package/src/react/component-slots.ts +0 -116
  285. package/src/react/fieldJsHandler.test.ts +0 -166
  286. package/src/react/fieldJsHandler.ts +0 -79
  287. package/src/react/fields/BuilderInput.tsx +0 -1078
  288. package/src/react/fields/CheckboxInput.tsx +0 -39
  289. package/src/react/fields/CheckboxListInput.tsx +0 -102
  290. package/src/react/fields/ColorInput.tsx +0 -71
  291. package/src/react/fields/DateFieldInput.tsx +0 -70
  292. package/src/react/fields/DateTimeInput.tsx +0 -62
  293. package/src/react/fields/FieldShell.tsx +0 -348
  294. package/src/react/fields/FileUploadInput.tsx +0 -639
  295. package/src/react/fields/HiddenInput.tsx +0 -17
  296. package/src/react/fields/KeyValueInput.tsx +0 -230
  297. package/src/react/fields/MarkdownInput.tsx +0 -560
  298. package/src/react/fields/RadioInput.tsx +0 -81
  299. package/src/react/fields/RepeaterInput.test.ts +0 -116
  300. package/src/react/fields/RepeaterInput.tsx +0 -1420
  301. package/src/react/fields/SelectFieldInput.tsx +0 -280
  302. package/src/react/fields/SliderInput.tsx +0 -81
  303. package/src/react/fields/TagsInput.tsx +0 -283
  304. package/src/react/fields/TextLikeInput.tsx +0 -256
  305. package/src/react/fields/ToggleButtonsInput.tsx +0 -60
  306. package/src/react/fields/ToggleFieldInput.tsx +0 -56
  307. package/src/react/fields/relationshipRenameDispatch.test.ts +0 -106
  308. package/src/react/fields/relationshipRenameDispatch.ts +0 -97
  309. package/src/react/fields/repeaterReconcile.test.ts +0 -114
  310. package/src/react/fields/repeaterReconcile.ts +0 -104
  311. package/src/react/fields/rowChromeButton.tsx +0 -336
  312. package/src/react/fields/rowState.ts +0 -106
  313. package/src/react/fields/syncRowGates.test.ts +0 -202
  314. package/src/react/fields/syncRowGates.ts +0 -66
  315. package/src/react/fields/textInputControls.tsx +0 -238
  316. package/src/react/fields/useRowReorderDnd.ts +0 -78
  317. package/src/react/formStateHelpers.test.ts +0 -508
  318. package/src/react/formStateHelpers.ts +0 -381
  319. package/src/react/hooks/use-mobile.ts +0 -19
  320. package/src/react/icon-context.tsx +0 -60
  321. package/src/react/index.ts +0 -195
  322. package/src/react/layouts/SidebarLayout.tsx +0 -250
  323. package/src/react/layouts/TopbarLayout.tsx +0 -258
  324. package/src/react/navigate.tsx +0 -37
  325. package/src/react/onProviderSynced.test.ts +0 -90
  326. package/src/react/parseRecordEditUrl.test.ts +0 -122
  327. package/src/react/parseRecordEditUrl.ts +0 -94
  328. package/src/react/persistedState.ts +0 -40
  329. package/src/react/registry.ts +0 -48
  330. package/src/react/right-panel-registry.tsx +0 -47
  331. package/src/react/schemaRenderer/AlertRenderer.tsx +0 -112
  332. package/src/react/schemaRenderer/EntryRenderer.tsx +0 -501
  333. package/src/react/schemaRenderer/SectionRenderer.tsx +0 -120
  334. package/src/react/schemaRenderer/SimpleElements.tsx +0 -306
  335. package/src/react/schemaRenderer/TabsRenderer.tsx +0 -62
  336. package/src/react/schemaRenderer/WizardRenderer.tsx +0 -338
  337. package/src/react/schemaRenderer/action/ActionGroupTrigger.tsx +0 -177
  338. package/src/react/schemaRenderer/action/ActionModalDialog.tsx +0 -273
  339. package/src/react/schemaRenderer/action/ConfirmActionDialog.tsx +0 -61
  340. package/src/react/schemaRenderer/action/HandlerActionButton.tsx +0 -43
  341. package/src/react/schemaRenderer/action/MethodActionButton.tsx +0 -64
  342. package/src/react/schemaRenderer/action/buttons.tsx +0 -99
  343. package/src/react/schemaRenderer/action/helpers.ts +0 -140
  344. package/src/react/schemaRenderer/action/renderAction.tsx +0 -245
  345. package/src/react/schemaRenderer/columnFormat.ts +0 -65
  346. package/src/react/schemaRenderer/constants.ts +0 -50
  347. package/src/react/schemaRenderer/form/FormRenderer.tsx +0 -274
  348. package/src/react/schemaRenderer/form/renderField.tsx +0 -511
  349. package/src/react/schemaRenderer/helpers.tsx +0 -81
  350. package/src/react/schemaRenderer/table/CardsLayoutBody.tsx +0 -308
  351. package/src/react/schemaRenderer/table/TableRenderer.tsx +0 -123
  352. package/src/react/schemaRenderer/table/TableRendererBody.tsx +0 -974
  353. package/src/react/schemaRenderer/table/filters.tsx +0 -1233
  354. package/src/react/schemaRenderer/table/formatCell.tsx +0 -264
  355. package/src/react/schemaRenderer/table/links.tsx +0 -112
  356. package/src/react/schemaRenderer/table/renderRowActions.tsx +0 -52
  357. package/src/react/schemaRenderer/table/url.tsx +0 -143
  358. package/src/react/theme-preview/apply.ts +0 -99
  359. package/src/react/theme-preview/build-html.ts +0 -436
  360. package/src/react/ui/button.tsx +0 -51
  361. package/src/react/ui/calendar.tsx +0 -67
  362. package/src/react/ui/checkbox.tsx +0 -29
  363. package/src/react/ui/dialog.tsx +0 -108
  364. package/src/react/ui/dropdown-menu.tsx +0 -97
  365. package/src/react/ui/input.tsx +0 -20
  366. package/src/react/ui/label.tsx +0 -21
  367. package/src/react/ui/popover.tsx +0 -50
  368. package/src/react/ui/select.tsx +0 -169
  369. package/src/react/ui/separator.tsx +0 -25
  370. package/src/react/ui/sheet.tsx +0 -136
  371. package/src/react/ui/sidebar.tsx +0 -723
  372. package/src/react/ui/skeleton.tsx +0 -13
  373. package/src/react/ui/slider.tsx +0 -34
  374. package/src/react/ui/switch.tsx +0 -28
  375. package/src/react/ui/table.tsx +0 -105
  376. package/src/react/ui/tabs.tsx +0 -63
  377. package/src/react/ui/textarea.tsx +0 -18
  378. package/src/react/ui/tooltip.tsx +0 -64
  379. package/src/react/useCollabSeed.ts +0 -86
  380. package/src/react/useResizableWidth.ts +0 -139
  381. package/src/react/utils.ts +0 -6
  382. package/src/react/widgetRegistry.test.ts +0 -43
  383. package/src/react/widgetRegistry.ts +0 -50
  384. package/src/react/widgets/StatsOverviewRenderer.tsx +0 -232
  385. package/src/react/widgets/TableWidgetRenderer.tsx +0 -231
  386. package/src/react/widgets/ViewRenderer.tsx +0 -71
  387. package/src/relationManagerData.test.ts +0 -1595
  388. package/src/richtext/index.ts +0 -8
  389. package/src/richtext/registry.ts +0 -89
  390. package/src/routes/globals.ts +0 -148
  391. package/src/routes/guard.test.ts +0 -325
  392. package/src/routes/helpers.ts +0 -700
  393. package/src/routes/pages.ts +0 -175
  394. package/src/routes/panel.ts +0 -204
  395. package/src/routes/relations.ts +0 -1227
  396. package/src/routes/resources.ts +0 -781
  397. package/src/routes/theme.ts +0 -91
  398. package/src/routes-nested-relations.test.ts +0 -676
  399. package/src/routes-relations.test.ts +0 -972
  400. package/src/routes.test.ts +0 -2027
  401. package/src/routes.ts +0 -303
  402. package/src/schema/Alert.test.ts +0 -109
  403. package/src/schema/Alert.ts +0 -131
  404. package/src/schema/Block.ts +0 -169
  405. package/src/schema/Breadcrumbs.ts +0 -40
  406. package/src/schema/Card.ts +0 -35
  407. package/src/schema/Divider.ts +0 -20
  408. package/src/schema/Element.ts +0 -219
  409. package/src/schema/EmptyState.test.ts +0 -37
  410. package/src/schema/EmptyState.ts +0 -63
  411. package/src/schema/Fieldset.ts +0 -43
  412. package/src/schema/Grid.ts +0 -43
  413. package/src/schema/Group.ts +0 -30
  414. package/src/schema/Heading.ts +0 -39
  415. package/src/schema/Html.ts +0 -67
  416. package/src/schema/Icon.ts +0 -54
  417. package/src/schema/Image.ts +0 -57
  418. package/src/schema/LinkTag.ts +0 -41
  419. package/src/schema/Markdown.ts +0 -85
  420. package/src/schema/MetaTag.ts +0 -41
  421. package/src/schema/RelationTabs.ts +0 -71
  422. package/src/schema/ScriptTag.ts +0 -55
  423. package/src/schema/Section.ts +0 -160
  424. package/src/schema/ServerDataElement.test.ts +0 -140
  425. package/src/schema/ServerDataElement.ts +0 -156
  426. package/src/schema/SlotComponent.test.ts +0 -77
  427. package/src/schema/SlotComponent.ts +0 -71
  428. package/src/schema/Split.ts +0 -50
  429. package/src/schema/Stat.test.ts +0 -118
  430. package/src/schema/Stat.ts +0 -154
  431. package/src/schema/StatsOverview.test.ts +0 -141
  432. package/src/schema/StatsOverview.ts +0 -119
  433. package/src/schema/StyleTag.ts +0 -35
  434. package/src/schema/TableWidget.test.ts +0 -297
  435. package/src/schema/TableWidget.ts +0 -289
  436. package/src/schema/Tabs.ts +0 -79
  437. package/src/schema/Text.ts +0 -58
  438. package/src/schema/UnorderedList.ts +0 -49
  439. package/src/schema/View.test.ts +0 -111
  440. package/src/schema/View.ts +0 -127
  441. package/src/schema/Wizard.ts +0 -220
  442. package/src/schema/containers.test.ts +0 -564
  443. package/src/schema/headTags.test.ts +0 -134
  444. package/src/schema/index.ts +0 -40
  445. package/src/schema/primes.test.ts +0 -269
  446. package/src/schema/resolveSchema.test.ts +0 -379
  447. package/src/schema/resolveSchema.ts +0 -917
  448. package/src/schema/sanitize.ts +0 -58
  449. package/src/search.test.ts +0 -446
  450. package/src/search.ts +0 -178
  451. package/src/sessionFilters.test.ts +0 -375
  452. package/src/sessionFilters.ts +0 -143
  453. package/src/slot-components/index.ts +0 -10
  454. package/src/slot-components/registry.ts +0 -56
  455. package/src/styles/file-upload.css +0 -13
  456. package/src/summarizers/Summarizer.test.ts +0 -84
  457. package/src/summarizers/Summarizer.ts +0 -123
  458. package/src/summarizers/index.ts +0 -11
  459. package/src/theme/base-colors.ts +0 -68
  460. package/src/theme/chart-colors.ts +0 -50
  461. package/src/theme/colors.ts +0 -447
  462. package/src/theme/generate-css.test.ts +0 -139
  463. package/src/theme/generate-css.ts +0 -44
  464. package/src/theme/generate-scale.test.ts +0 -106
  465. package/src/theme/generate-scale.ts +0 -97
  466. package/src/theme/icon-map.ts +0 -42
  467. package/src/theme/index.ts +0 -34
  468. package/src/theme/migrate.test.ts +0 -178
  469. package/src/theme/migrate.ts +0 -81
  470. package/src/theme/presets.ts +0 -135
  471. package/src/theme/radius.ts +0 -18
  472. package/src/theme/resolve.test.ts +0 -238
  473. package/src/theme/resolve.ts +0 -96
  474. package/src/theme/spacing.ts +0 -18
  475. package/src/theme/storage.test.ts +0 -126
  476. package/src/theme/storage.ts +0 -106
  477. package/src/theme/theme-colors.ts +0 -88
  478. package/src/theme/types.ts +0 -125
  479. package/src/uploads/UploadAdapter.ts +0 -35
  480. package/src/uploads/index.ts +0 -2
  481. package/src/uploads/localUpload.test.ts +0 -70
  482. package/src/uploads/localUpload.ts +0 -84
  483. package/src/validation/Validator.ts +0 -49
  484. package/src/validation/index.ts +0 -28
  485. package/src/validation/rules.ts +0 -78
  486. package/src/validation/runValidators.ts +0 -435
  487. package/src/validation/uniqueValidator.test.ts +0 -196
  488. package/src/validation/uniqueValidator.ts +0 -133
  489. package/src/validation/validators.test.ts +0 -268
  490. package/src/vite.test.ts +0 -184
  491. package/src/vite.ts +0 -787
  492. package/src/widgets/index.ts +0 -10
  493. package/src/widgets/registry.ts +0 -45
  494. package/src/widgets.test.ts +0 -592
  495. package/tsconfig.build.json +0 -11
  496. package/tsconfig.json +0 -4
  497. package/tsconfig.test.json +0 -10
  498. package/views/react/Dashboard.tsx +0 -27
  499. package/views/react/Resources/Form.tsx +0 -102
  500. 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
-