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