@pilotiq/pilotiq 0.24.1 → 0.24.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (518) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/boost/guidelines.md +571 -0
  3. package/boost/skills/pilotiq-actions/SKILL.md +49 -0
  4. package/boost/skills/pilotiq-actions/rules/dispatch-modes.md +177 -0
  5. package/boost/skills/pilotiq-actions/rules/factories.md +130 -0
  6. package/boost/skills/pilotiq-actions/rules/visibility-and-authorization.md +125 -0
  7. package/boost/skills/pilotiq-fields/SKILL.md +47 -0
  8. package/boost/skills/pilotiq-fields/rules/field-catalog.md +288 -0
  9. package/boost/skills/pilotiq-fields/rules/reactive-fields.md +199 -0
  10. package/boost/skills/pilotiq-fields/rules/validation.md +198 -0
  11. package/boost/skills/pilotiq-relations/SKILL.md +47 -0
  12. package/boost/skills/pilotiq-relations/rules/relation-managers.md +256 -0
  13. package/boost/skills/pilotiq-relations/rules/repeater-relationship.md +177 -0
  14. package/boost/skills/pilotiq-resource/SKILL.md +61 -0
  15. package/boost/skills/pilotiq-resource/rules/authorization.md +242 -0
  16. package/boost/skills/pilotiq-resource/rules/defining-resources.md +228 -0
  17. package/boost/skills/pilotiq-resource/rules/page-overrides.md +296 -0
  18. package/dist/Pilotiq.d.ts +31 -0
  19. package/dist/Pilotiq.d.ts.map +1 -1
  20. package/dist/Pilotiq.js +3 -1
  21. package/dist/Pilotiq.js.map +1 -1
  22. package/dist/PilotiqRegistry.d.ts +13 -0
  23. package/dist/PilotiqRegistry.d.ts.map +1 -1
  24. package/dist/PilotiqRegistry.js +15 -0
  25. package/dist/PilotiqRegistry.js.map +1 -1
  26. package/dist/pageData/misc.d.ts.map +1 -1
  27. package/dist/pageData/misc.js +6 -0
  28. package/dist/pageData/misc.js.map +1 -1
  29. package/dist/pageData/navigation.d.ts +1 -0
  30. package/dist/pageData/navigation.d.ts.map +1 -1
  31. package/dist/pageData/navigation.js +3 -0
  32. package/dist/pageData/navigation.js.map +1 -1
  33. package/dist/pageData/relationPages.d.ts.map +1 -1
  34. package/dist/pageData/relationPages.js +3 -0
  35. package/dist/pageData/relationPages.js.map +1 -1
  36. package/dist/pageData/resourcePages.d.ts.map +1 -1
  37. package/dist/pageData/resourcePages.js +8 -0
  38. package/dist/pageData/resourcePages.js.map +1 -1
  39. package/dist/react/AppShell.d.ts +8 -0
  40. package/dist/react/AppShell.d.ts.map +1 -1
  41. package/dist/react/AppShell.js.map +1 -1
  42. package/dist/react/layouts/SidebarLayout.d.ts.map +1 -1
  43. package/dist/react/layouts/SidebarLayout.js +10 -2
  44. package/dist/react/layouts/SidebarLayout.js.map +1 -1
  45. package/dist/react/widgets/StatsOverviewRenderer.d.ts.map +1 -1
  46. package/dist/react/widgets/StatsOverviewRenderer.js +32 -18
  47. package/dist/react/widgets/StatsOverviewRenderer.js.map +1 -1
  48. package/dist/routes/relations.d.ts.map +1 -1
  49. package/dist/routes/relations.js +25 -18
  50. package/dist/routes/relations.js.map +1 -1
  51. package/dist/routes/resources.js.map +1 -1
  52. package/package.json +10 -5
  53. package/.turbo/turbo-build.log +0 -8
  54. package/CLAUDE.md +0 -265
  55. package/src/Cluster.test.ts +0 -283
  56. package/src/Cluster.ts +0 -83
  57. package/src/Column.test.ts +0 -199
  58. package/src/Column.ts +0 -710
  59. package/src/Global.test.ts +0 -367
  60. package/src/Global.ts +0 -169
  61. package/src/Page.test.ts +0 -114
  62. package/src/Page.ts +0 -208
  63. package/src/Pilotiq.perf.test.ts +0 -252
  64. package/src/Pilotiq.test.ts +0 -129
  65. package/src/Pilotiq.ts +0 -1158
  66. package/src/PilotiqRegistry.ts +0 -36
  67. package/src/PilotiqServiceProvider.ts +0 -121
  68. package/src/RelationManager.test.ts +0 -400
  69. package/src/RelationManager.ts +0 -527
  70. package/src/RenderHook.test.ts +0 -252
  71. package/src/RenderHook.ts +0 -242
  72. package/src/Resource.test.ts +0 -284
  73. package/src/Resource.ts +0 -526
  74. package/src/RightPanel.test.ts +0 -202
  75. package/src/RightPanel.ts +0 -132
  76. package/src/Tab.test.ts +0 -91
  77. package/src/Tab.ts +0 -156
  78. package/src/UserMenuItem.ts +0 -145
  79. package/src/actions/Action.test.ts +0 -2526
  80. package/src/actions/Action.ts +0 -1515
  81. package/src/actions/ActionGroup.test.ts +0 -112
  82. package/src/actions/ActionGroup.ts +0 -173
  83. package/src/actions/attachFactory.ts +0 -172
  84. package/src/actions/bulkFactories.ts +0 -168
  85. package/src/actions/crudFactories.ts +0 -220
  86. package/src/actions/exportFactory.ts +0 -225
  87. package/src/actions/factoryHelpers.ts +0 -177
  88. package/src/actions/importFactory.ts +0 -243
  89. package/src/actions/index.ts +0 -17
  90. package/src/actions/m2mFactories.ts +0 -193
  91. package/src/actions/relationFactories.ts +0 -372
  92. package/src/applyPageHooks.test.ts +0 -463
  93. package/src/applyPageHooks.ts +0 -330
  94. package/src/authorization.test.ts +0 -483
  95. package/src/breadcrumbs.test.ts +0 -238
  96. package/src/cells/coerce.test.ts +0 -85
  97. package/src/cells/coerce.ts +0 -84
  98. package/src/clusterPaths.ts +0 -35
  99. package/src/columns/BadgeColumn.test.ts +0 -54
  100. package/src/columns/BadgeColumn.ts +0 -32
  101. package/src/columns/BooleanColumn.test.ts +0 -41
  102. package/src/columns/BooleanColumn.ts +0 -18
  103. package/src/columns/ColorColumn.test.ts +0 -37
  104. package/src/columns/ColorColumn.ts +0 -38
  105. package/src/columns/IconColumn.test.ts +0 -54
  106. package/src/columns/IconColumn.ts +0 -37
  107. package/src/columns/ImageColumn.test.ts +0 -41
  108. package/src/columns/ImageColumn.ts +0 -28
  109. package/src/columns/SelectColumn.ts +0 -98
  110. package/src/columns/TextColumn.test.ts +0 -190
  111. package/src/columns/TextColumn.ts +0 -20
  112. package/src/columns/TextInputColumn.ts +0 -68
  113. package/src/columns/ToggleColumn.ts +0 -46
  114. package/src/columns/editableColumns.test.ts +0 -238
  115. package/src/columns/index.ts +0 -9
  116. package/src/defaultGlobalPages.ts +0 -95
  117. package/src/defaultPages.test.ts +0 -634
  118. package/src/defaultPages.ts +0 -617
  119. package/src/defaultViewPage.test.ts +0 -147
  120. package/src/elements/Form.test.ts +0 -223
  121. package/src/elements/Form.ts +0 -416
  122. package/src/elements/ListTabs.ts +0 -28
  123. package/src/elements/Table.test.ts +0 -422
  124. package/src/elements/Table.ts +0 -850
  125. package/src/elements/TableGroup.test.ts +0 -260
  126. package/src/elements/TableGroup.ts +0 -334
  127. package/src/elements/dispatchAction.test.ts +0 -463
  128. package/src/elements/dispatchAction.ts +0 -355
  129. package/src/elements/dispatchForm.test.ts +0 -477
  130. package/src/elements/dispatchForm.ts +0 -1993
  131. package/src/elements/dispatchTable.test.ts +0 -1514
  132. package/src/elements/dispatchTable.ts +0 -745
  133. package/src/elements/index.ts +0 -21
  134. package/src/entries/BadgeEntry.ts +0 -39
  135. package/src/entries/CodeEntry.test.ts +0 -40
  136. package/src/entries/CodeEntry.ts +0 -52
  137. package/src/entries/ColorEntry.ts +0 -63
  138. package/src/entries/ComponentEntry.test.ts +0 -173
  139. package/src/entries/ComponentEntry.ts +0 -95
  140. package/src/entries/Entry.ts +0 -304
  141. package/src/entries/IconEntry.ts +0 -49
  142. package/src/entries/ImageEntry.ts +0 -61
  143. package/src/entries/KeyValueEntry.ts +0 -47
  144. package/src/entries/RepeatableEntry.test.ts +0 -239
  145. package/src/entries/RepeatableEntry.ts +0 -173
  146. package/src/entries/TextEntry.test.ts +0 -394
  147. package/src/entries/TextEntry.ts +0 -60
  148. package/src/entries/index.ts +0 -12
  149. package/src/entries/leaves.test.ts +0 -306
  150. package/src/entries/registry.ts +0 -54
  151. package/src/fields/BuilderField.test.ts +0 -1188
  152. package/src/fields/BuilderField.ts +0 -605
  153. package/src/fields/BuilderRelationship.test.ts +0 -811
  154. package/src/fields/CheckboxField.test.ts +0 -44
  155. package/src/fields/CheckboxField.ts +0 -27
  156. package/src/fields/CheckboxListField.test.ts +0 -99
  157. package/src/fields/CheckboxListField.ts +0 -66
  158. package/src/fields/ColorPickerField.test.ts +0 -33
  159. package/src/fields/ColorPickerField.ts +0 -25
  160. package/src/fields/DateField.ts +0 -54
  161. package/src/fields/DateTimeField.test.ts +0 -55
  162. package/src/fields/EmailField.ts +0 -16
  163. package/src/fields/Field.test.ts +0 -654
  164. package/src/fields/Field.ts +0 -817
  165. package/src/fields/FileUploadField.test.ts +0 -143
  166. package/src/fields/FileUploadField.ts +0 -159
  167. package/src/fields/HiddenField.test.ts +0 -27
  168. package/src/fields/HiddenField.ts +0 -28
  169. package/src/fields/KeyValueField.test.ts +0 -105
  170. package/src/fields/KeyValueField.ts +0 -55
  171. package/src/fields/MarkdownField.test.ts +0 -167
  172. package/src/fields/MarkdownField.ts +0 -162
  173. package/src/fields/NumberField.ts +0 -33
  174. package/src/fields/RadioField.test.ts +0 -94
  175. package/src/fields/RadioField.ts +0 -67
  176. package/src/fields/RepeaterField.test.ts +0 -1806
  177. package/src/fields/RepeaterField.ts +0 -939
  178. package/src/fields/RepeaterRelationship.test.ts +0 -1923
  179. package/src/fields/RepeaterSimple.test.ts +0 -248
  180. package/src/fields/RowButton.test.ts +0 -219
  181. package/src/fields/RowButton.ts +0 -135
  182. package/src/fields/SelectField.test.ts +0 -192
  183. package/src/fields/SelectField.ts +0 -235
  184. package/src/fields/SliderField.test.ts +0 -50
  185. package/src/fields/SliderField.ts +0 -53
  186. package/src/fields/SlugField.ts +0 -24
  187. package/src/fields/TagsInputField.test.ts +0 -154
  188. package/src/fields/TagsInputField.ts +0 -133
  189. package/src/fields/TextField.test.ts +0 -213
  190. package/src/fields/TextField.ts +0 -177
  191. package/src/fields/TextareaField.test.ts +0 -58
  192. package/src/fields/TextareaField.ts +0 -59
  193. package/src/fields/ToggleButtonsField.test.ts +0 -106
  194. package/src/fields/ToggleButtonsField.ts +0 -59
  195. package/src/fields/ToggleField.ts +0 -16
  196. package/src/fields/disableOptionsWhenSelectedInSiblingRepeaterItems.test.ts +0 -319
  197. package/src/fields/optionsResolver.ts +0 -95
  198. package/src/fields/resolveField.ts +0 -28
  199. package/src/filters/BooleanFilter.ts +0 -35
  200. package/src/filters/DateRangeFilter.test.ts +0 -194
  201. package/src/filters/DateRangeFilter.ts +0 -148
  202. package/src/filters/Filter.test.ts +0 -268
  203. package/src/filters/Filter.ts +0 -184
  204. package/src/filters/FormFilter.test.ts +0 -238
  205. package/src/filters/FormFilter.ts +0 -215
  206. package/src/filters/MultiSelectFilter.test.ts +0 -119
  207. package/src/filters/MultiSelectFilter.ts +0 -78
  208. package/src/filters/QueryBuilderFilter.test.ts +0 -662
  209. package/src/filters/QueryBuilderFilter.ts +0 -398
  210. package/src/filters/SelectFilter.ts +0 -46
  211. package/src/filters/TernaryFilter.test.ts +0 -160
  212. package/src/filters/TernaryFilter.ts +0 -72
  213. package/src/filters/TrashedFilter.test.ts +0 -149
  214. package/src/filters/TrashedFilter.ts +0 -55
  215. package/src/filters/queryBuilder/BooleanConstraint.ts +0 -31
  216. package/src/filters/queryBuilder/Constraint.ts +0 -115
  217. package/src/filters/queryBuilder/DateConstraint.ts +0 -69
  218. package/src/filters/queryBuilder/NumberConstraint.ts +0 -66
  219. package/src/filters/queryBuilder/SelectConstraint.ts +0 -72
  220. package/src/filters/queryBuilder/TextConstraint.ts +0 -64
  221. package/src/filters/queryBuilder/index.ts +0 -12
  222. package/src/icons/index.ts +0 -2
  223. package/src/icons/lucide.ts +0 -204
  224. package/src/icons/registry.test.ts +0 -56
  225. package/src/icons/registry.ts +0 -41
  226. package/src/icons/types.ts +0 -47
  227. package/src/index.ts +0 -525
  228. package/src/io/csv.test.ts +0 -142
  229. package/src/io/csv.ts +0 -170
  230. package/src/nestedRelationManagerData.test.ts +0 -547
  231. package/src/notifications/Notification.test.ts +0 -210
  232. package/src/notifications/Notification.ts +0 -354
  233. package/src/notifications/broadcast.test.ts +0 -110
  234. package/src/notifications/broadcast.ts +0 -95
  235. package/src/notifications/database.test.ts +0 -383
  236. package/src/notifications/database.ts +0 -398
  237. package/src/notifications/databaseNotifications.test.ts +0 -187
  238. package/src/notifications/dispatchNotificationAction.test.ts +0 -341
  239. package/src/notifications/dispatchNotificationAction.ts +0 -142
  240. package/src/notifications/flash.test.ts +0 -89
  241. package/src/notifications/flash.ts +0 -71
  242. package/src/notifications/index.ts +0 -45
  243. package/src/notifications/registerBroadcastAuth.test.ts +0 -134
  244. package/src/notifications/registerBroadcastAuth.ts +0 -100
  245. package/src/notifications/resolveSavedNotification.test.ts +0 -82
  246. package/src/notifications/resolveSavedNotification.ts +0 -59
  247. package/src/notifications/types.ts +0 -93
  248. package/src/orm/m2mAccessor.ts +0 -66
  249. package/src/orm/modelDefaults.test.ts +0 -633
  250. package/src/orm/modelDefaults.ts +0 -666
  251. package/src/pageData/breadcrumbs.ts +0 -288
  252. package/src/pageData/forms.ts +0 -578
  253. package/src/pageData/helpers.ts +0 -857
  254. package/src/pageData/misc.ts +0 -347
  255. package/src/pageData/navigation.ts +0 -842
  256. package/src/pageData/relationPages.ts +0 -1248
  257. package/src/pageData/relationTabs.ts +0 -286
  258. package/src/pageData/resourcePages.ts +0 -609
  259. package/src/pageData.test.ts +0 -1545
  260. package/src/pageData.ts +0 -341
  261. package/src/plugins/index.ts +0 -8
  262. package/src/plugins/themeEditor.test.ts +0 -36
  263. package/src/plugins/themeEditor.ts +0 -45
  264. package/src/react/AppShell.tsx +0 -251
  265. package/src/react/CollabExtensionFactoryRegistry.ts +0 -55
  266. package/src/react/CollabRoomContext.ts +0 -98
  267. package/src/react/CollabTextRendererRegistry.ts +0 -102
  268. package/src/react/CommandPalette.tsx +0 -375
  269. package/src/react/CurrentUserContext.tsx +0 -50
  270. package/src/react/CustomPageWrapperGate.tsx +0 -69
  271. package/src/react/CustomPageWrapperRegistry.ts +0 -45
  272. package/src/react/FieldFocusReporterRegistry.ts +0 -37
  273. package/src/react/FieldLabelSlotRegistry.ts +0 -30
  274. package/src/react/FieldPresenceRegistry.ts +0 -46
  275. package/src/react/FormCollabBindingRegistry.ts +0 -242
  276. package/src/react/FormStateContext.tsx +0 -591
  277. package/src/react/HeadHooks.tsx +0 -126
  278. package/src/react/MarkdownEditorRegistry.test.ts +0 -38
  279. package/src/react/MarkdownEditorRegistry.ts +0 -107
  280. package/src/react/NotificationActionStrip.tsx +0 -263
  281. package/src/react/NotificationBell.tsx +0 -426
  282. package/src/react/PendingSuggestionApplierRegistry.test.ts +0 -97
  283. package/src/react/PendingSuggestionApplierRegistry.ts +0 -98
  284. package/src/react/PendingSuggestionOverlayRegistry.ts +0 -54
  285. package/src/react/PendingSuggestionsContext.tsx +0 -172
  286. package/src/react/RecordWrapperGate.tsx +0 -58
  287. package/src/react/RecordWrapperRegistry.ts +0 -39
  288. package/src/react/RenderHookSlot.tsx +0 -32
  289. package/src/react/RightSidebar.tsx +0 -257
  290. package/src/react/RightSidebarContext.tsx +0 -234
  291. package/src/react/RightSidebarTrigger.tsx +0 -53
  292. package/src/react/RowCoordsContext.tsx +0 -23
  293. package/src/react/SchemaRenderer.tsx +0 -549
  294. package/src/react/SearchTrigger.tsx +0 -46
  295. package/src/react/ThemeProvider.tsx +0 -93
  296. package/src/react/ThemeSettingsPage.tsx +0 -579
  297. package/src/react/ThemeToggle.tsx +0 -20
  298. package/src/react/Toaster.tsx +0 -158
  299. package/src/react/UserMenu.tsx +0 -196
  300. package/src/react/WidgetDataContext.tsx +0 -157
  301. package/src/react/cells/EditableCell.tsx +0 -389
  302. package/src/react/component-slots.test.ts +0 -103
  303. package/src/react/component-slots.ts +0 -116
  304. package/src/react/fieldJsHandler.test.ts +0 -166
  305. package/src/react/fieldJsHandler.ts +0 -79
  306. package/src/react/fields/BuilderInput.tsx +0 -1078
  307. package/src/react/fields/CheckboxInput.tsx +0 -39
  308. package/src/react/fields/CheckboxListInput.tsx +0 -102
  309. package/src/react/fields/ColorInput.tsx +0 -71
  310. package/src/react/fields/DateFieldInput.tsx +0 -70
  311. package/src/react/fields/DateTimeInput.tsx +0 -62
  312. package/src/react/fields/FieldShell.tsx +0 -348
  313. package/src/react/fields/FileUploadInput.tsx +0 -639
  314. package/src/react/fields/HiddenInput.tsx +0 -17
  315. package/src/react/fields/KeyValueInput.tsx +0 -230
  316. package/src/react/fields/MarkdownInput.tsx +0 -560
  317. package/src/react/fields/RadioInput.tsx +0 -81
  318. package/src/react/fields/RepeaterInput.test.ts +0 -116
  319. package/src/react/fields/RepeaterInput.tsx +0 -1420
  320. package/src/react/fields/SelectFieldInput.tsx +0 -280
  321. package/src/react/fields/SliderInput.tsx +0 -81
  322. package/src/react/fields/TagsInput.tsx +0 -283
  323. package/src/react/fields/TextLikeInput.tsx +0 -256
  324. package/src/react/fields/ToggleButtonsInput.tsx +0 -60
  325. package/src/react/fields/ToggleFieldInput.tsx +0 -56
  326. package/src/react/fields/relationshipRenameDispatch.test.ts +0 -106
  327. package/src/react/fields/relationshipRenameDispatch.ts +0 -97
  328. package/src/react/fields/repeaterReconcile.test.ts +0 -114
  329. package/src/react/fields/repeaterReconcile.ts +0 -104
  330. package/src/react/fields/rowChromeButton.tsx +0 -336
  331. package/src/react/fields/rowState.ts +0 -106
  332. package/src/react/fields/syncRowGates.test.ts +0 -202
  333. package/src/react/fields/syncRowGates.ts +0 -66
  334. package/src/react/fields/textInputControls.tsx +0 -238
  335. package/src/react/fields/useRowReorderDnd.ts +0 -78
  336. package/src/react/formStateHelpers.test.ts +0 -508
  337. package/src/react/formStateHelpers.ts +0 -381
  338. package/src/react/hooks/use-mobile.ts +0 -19
  339. package/src/react/icon-context.tsx +0 -60
  340. package/src/react/index.ts +0 -194
  341. package/src/react/layouts/SidebarLayout.tsx +0 -250
  342. package/src/react/layouts/TopbarLayout.tsx +0 -258
  343. package/src/react/navigate.tsx +0 -37
  344. package/src/react/onProviderSynced.test.ts +0 -90
  345. package/src/react/parseRecordEditUrl.test.ts +0 -122
  346. package/src/react/parseRecordEditUrl.ts +0 -94
  347. package/src/react/persistedState.ts +0 -40
  348. package/src/react/registry.ts +0 -48
  349. package/src/react/right-panel-registry.tsx +0 -47
  350. package/src/react/schemaRenderer/AlertRenderer.tsx +0 -112
  351. package/src/react/schemaRenderer/EntryRenderer.tsx +0 -501
  352. package/src/react/schemaRenderer/SectionRenderer.tsx +0 -120
  353. package/src/react/schemaRenderer/SimpleElements.tsx +0 -306
  354. package/src/react/schemaRenderer/TabsRenderer.tsx +0 -62
  355. package/src/react/schemaRenderer/WizardRenderer.tsx +0 -338
  356. package/src/react/schemaRenderer/action/ActionGroupTrigger.tsx +0 -177
  357. package/src/react/schemaRenderer/action/ActionModalDialog.tsx +0 -273
  358. package/src/react/schemaRenderer/action/ConfirmActionDialog.tsx +0 -61
  359. package/src/react/schemaRenderer/action/HandlerActionButton.tsx +0 -43
  360. package/src/react/schemaRenderer/action/MethodActionButton.tsx +0 -64
  361. package/src/react/schemaRenderer/action/buttons.tsx +0 -99
  362. package/src/react/schemaRenderer/action/helpers.ts +0 -140
  363. package/src/react/schemaRenderer/action/renderAction.tsx +0 -245
  364. package/src/react/schemaRenderer/columnFormat.ts +0 -65
  365. package/src/react/schemaRenderer/constants.ts +0 -50
  366. package/src/react/schemaRenderer/form/FormRenderer.tsx +0 -274
  367. package/src/react/schemaRenderer/form/renderField.tsx +0 -511
  368. package/src/react/schemaRenderer/helpers.tsx +0 -81
  369. package/src/react/schemaRenderer/table/CardsLayoutBody.tsx +0 -308
  370. package/src/react/schemaRenderer/table/TableRenderer.tsx +0 -123
  371. package/src/react/schemaRenderer/table/TableRendererBody.tsx +0 -974
  372. package/src/react/schemaRenderer/table/filters.tsx +0 -1233
  373. package/src/react/schemaRenderer/table/formatCell.tsx +0 -264
  374. package/src/react/schemaRenderer/table/links.tsx +0 -112
  375. package/src/react/schemaRenderer/table/renderRowActions.tsx +0 -52
  376. package/src/react/schemaRenderer/table/url.tsx +0 -143
  377. package/src/react/theme-preview/apply.ts +0 -99
  378. package/src/react/theme-preview/build-html.ts +0 -436
  379. package/src/react/ui/button.tsx +0 -51
  380. package/src/react/ui/calendar.tsx +0 -67
  381. package/src/react/ui/checkbox.tsx +0 -29
  382. package/src/react/ui/dialog.tsx +0 -108
  383. package/src/react/ui/dropdown-menu.tsx +0 -97
  384. package/src/react/ui/input.tsx +0 -20
  385. package/src/react/ui/label.tsx +0 -21
  386. package/src/react/ui/popover.tsx +0 -50
  387. package/src/react/ui/select.tsx +0 -169
  388. package/src/react/ui/separator.tsx +0 -25
  389. package/src/react/ui/sheet.tsx +0 -136
  390. package/src/react/ui/sidebar.tsx +0 -723
  391. package/src/react/ui/skeleton.tsx +0 -13
  392. package/src/react/ui/slider.tsx +0 -34
  393. package/src/react/ui/switch.tsx +0 -28
  394. package/src/react/ui/table.tsx +0 -105
  395. package/src/react/ui/tabs.tsx +0 -63
  396. package/src/react/ui/textarea.tsx +0 -18
  397. package/src/react/ui/tooltip.tsx +0 -64
  398. package/src/react/useResizableWidth.ts +0 -139
  399. package/src/react/utils.ts +0 -6
  400. package/src/react/widgetRegistry.test.ts +0 -43
  401. package/src/react/widgetRegistry.ts +0 -50
  402. package/src/react/widgets/StatsOverviewRenderer.tsx +0 -232
  403. package/src/react/widgets/TableWidgetRenderer.tsx +0 -231
  404. package/src/react/widgets/ViewRenderer.tsx +0 -71
  405. package/src/relationManagerData.test.ts +0 -1595
  406. package/src/richtext/index.ts +0 -8
  407. package/src/richtext/registry.ts +0 -89
  408. package/src/routes/globals.ts +0 -148
  409. package/src/routes/guard.test.ts +0 -325
  410. package/src/routes/helpers.ts +0 -704
  411. package/src/routes/pages.ts +0 -175
  412. package/src/routes/panel.ts +0 -204
  413. package/src/routes/relations.ts +0 -1243
  414. package/src/routes/resources.ts +0 -781
  415. package/src/routes/theme.ts +0 -91
  416. package/src/routes-nested-relations.test.ts +0 -676
  417. package/src/routes-relations.test.ts +0 -972
  418. package/src/routes.test.ts +0 -2027
  419. package/src/routes.ts +0 -303
  420. package/src/schema/Alert.test.ts +0 -109
  421. package/src/schema/Alert.ts +0 -131
  422. package/src/schema/Block.ts +0 -169
  423. package/src/schema/Breadcrumbs.ts +0 -40
  424. package/src/schema/Card.ts +0 -35
  425. package/src/schema/Divider.ts +0 -20
  426. package/src/schema/Element.ts +0 -219
  427. package/src/schema/EmptyState.test.ts +0 -37
  428. package/src/schema/EmptyState.ts +0 -63
  429. package/src/schema/Fieldset.ts +0 -43
  430. package/src/schema/Grid.ts +0 -43
  431. package/src/schema/Group.ts +0 -30
  432. package/src/schema/Heading.ts +0 -39
  433. package/src/schema/Html.ts +0 -67
  434. package/src/schema/Icon.ts +0 -54
  435. package/src/schema/Image.ts +0 -57
  436. package/src/schema/LinkTag.ts +0 -41
  437. package/src/schema/Markdown.ts +0 -85
  438. package/src/schema/MetaTag.ts +0 -41
  439. package/src/schema/RelationTabs.ts +0 -71
  440. package/src/schema/ScriptTag.ts +0 -55
  441. package/src/schema/Section.ts +0 -160
  442. package/src/schema/ServerDataElement.test.ts +0 -140
  443. package/src/schema/ServerDataElement.ts +0 -156
  444. package/src/schema/SlotComponent.test.ts +0 -77
  445. package/src/schema/SlotComponent.ts +0 -71
  446. package/src/schema/Split.ts +0 -50
  447. package/src/schema/Stat.test.ts +0 -118
  448. package/src/schema/Stat.ts +0 -154
  449. package/src/schema/StatsOverview.test.ts +0 -141
  450. package/src/schema/StatsOverview.ts +0 -119
  451. package/src/schema/StyleTag.ts +0 -35
  452. package/src/schema/TableWidget.test.ts +0 -297
  453. package/src/schema/TableWidget.ts +0 -289
  454. package/src/schema/Tabs.ts +0 -79
  455. package/src/schema/Text.ts +0 -58
  456. package/src/schema/UnorderedList.ts +0 -49
  457. package/src/schema/View.test.ts +0 -111
  458. package/src/schema/View.ts +0 -127
  459. package/src/schema/Wizard.ts +0 -220
  460. package/src/schema/containers.test.ts +0 -564
  461. package/src/schema/headTags.test.ts +0 -134
  462. package/src/schema/index.ts +0 -40
  463. package/src/schema/primes.test.ts +0 -269
  464. package/src/schema/resolveSchema.test.ts +0 -379
  465. package/src/schema/resolveSchema.ts +0 -917
  466. package/src/schema/sanitize.ts +0 -58
  467. package/src/search.test.ts +0 -446
  468. package/src/search.ts +0 -178
  469. package/src/sessionFilters.test.ts +0 -375
  470. package/src/sessionFilters.ts +0 -143
  471. package/src/slot-components/index.ts +0 -10
  472. package/src/slot-components/registry.ts +0 -56
  473. package/src/styles/file-upload.css +0 -13
  474. package/src/summarizers/Summarizer.test.ts +0 -84
  475. package/src/summarizers/Summarizer.ts +0 -123
  476. package/src/summarizers/index.ts +0 -11
  477. package/src/theme/base-colors.ts +0 -68
  478. package/src/theme/chart-colors.ts +0 -50
  479. package/src/theme/colors.ts +0 -447
  480. package/src/theme/generate-css.test.ts +0 -139
  481. package/src/theme/generate-css.ts +0 -44
  482. package/src/theme/generate-scale.test.ts +0 -106
  483. package/src/theme/generate-scale.ts +0 -97
  484. package/src/theme/icon-map.ts +0 -42
  485. package/src/theme/index.ts +0 -34
  486. package/src/theme/migrate.test.ts +0 -178
  487. package/src/theme/migrate.ts +0 -81
  488. package/src/theme/presets.ts +0 -135
  489. package/src/theme/radius.ts +0 -18
  490. package/src/theme/resolve.test.ts +0 -238
  491. package/src/theme/resolve.ts +0 -96
  492. package/src/theme/spacing.ts +0 -18
  493. package/src/theme/storage.test.ts +0 -126
  494. package/src/theme/storage.ts +0 -106
  495. package/src/theme/theme-colors.ts +0 -88
  496. package/src/theme/types.ts +0 -125
  497. package/src/uploads/UploadAdapter.ts +0 -35
  498. package/src/uploads/index.ts +0 -2
  499. package/src/uploads/localUpload.test.ts +0 -70
  500. package/src/uploads/localUpload.ts +0 -84
  501. package/src/validation/Validator.ts +0 -49
  502. package/src/validation/index.ts +0 -28
  503. package/src/validation/rules.ts +0 -78
  504. package/src/validation/runValidators.ts +0 -435
  505. package/src/validation/uniqueValidator.test.ts +0 -196
  506. package/src/validation/uniqueValidator.ts +0 -133
  507. package/src/validation/validators.test.ts +0 -268
  508. package/src/vite.test.ts +0 -184
  509. package/src/vite.ts +0 -787
  510. package/src/widgets/index.ts +0 -10
  511. package/src/widgets/registry.ts +0 -45
  512. package/src/widgets.test.ts +0 -592
  513. package/tsconfig.build.json +0 -11
  514. package/tsconfig.json +0 -4
  515. package/tsconfig.test.json +0 -10
  516. package/views/react/Dashboard.tsx +0 -27
  517. package/views/react/Resources/Form.tsx +0 -102
  518. package/views/react/Resources/Index.tsx +0 -49
@@ -1,939 +0,0 @@
1
- import { Element, type ElementMeta, type LayoutContext } from '../schema/Element.js'
2
- import { Field, type FieldMeta } from './Field.js'
3
- import type { RenderContext } from '../schema/resolveSchema.js'
4
- import type { Action, ActionMeta } from '../actions/Action.js'
5
- import type { ModelLike } from '../orm/modelDefaults.js'
6
- import {
7
- RowButton,
8
- type RowButtonKind,
9
- type RowButtonsMeta,
10
- } from './RowButton.js'
11
-
12
- /**
13
- * Configuration for `Repeater.relationship(...)`. Stores rows in a real
14
- * `HasMany` relation on the parent record instead of serializing them
15
- * to a JSON column.
16
- *
17
- * `name` is the only required field — it must match a key on the
18
- * parent's `static relations` map (rudder ORM convention). The
19
- * `model` + `foreignKey` overrides exist for ORMs that don't follow
20
- * that convention; both default to discovery via the parent's
21
- * relations map at submit time.
22
- *
23
- * `orderColumn` is the integer column on the child model that
24
- * receives the row's 0-based index after each save. Omit when the
25
- * order doesn't need to round-trip through the database.
26
- */
27
- export interface RepeaterRelationshipConfig {
28
- /** Relationship key on the parent (e.g. `'attachments'`). */
29
- name: string
30
- /** Override the child model. Defaults to `parent.relations[name].model()`. */
31
- model?: ModelLike
32
- /** Override the FK column on the child. Defaults to `parent.relations[name].foreignKey`. */
33
- foreignKey?: string
34
- /** Optional integer column on the child to receive the row index. */
35
- orderColumn?: string
36
- /**
37
- * M2M only — pivot-table extra columns to surface as editable per-row
38
- * fields. Each name must match an inner-schema field's `name`; the
39
- * row's values for these names round-trip through the pivot row
40
- * (read via `accessor.withPivot(...)`, written via
41
- * `accessor.attach({ id: { … } })` for new rows and
42
- * `accessor.updatePivot(id, { … })` for existing rows). No-op (and
43
- * a clear error at submit time) on `hasMany` / `morphMany` modes.
44
- */
45
- pivotColumns?: string[]
46
- }
47
-
48
- /** Public meta — `model` + `foreignKey` are server-only and stay private. */
49
- export interface RepeaterRelationshipMeta {
50
- name: string
51
- orderColumn?: string
52
- }
53
-
54
- /**
55
- * Mode of the underlying relation, derived from the parent's `static
56
- * relations` map at submit time. Surfaced on `RepeaterRowContext.mode`
57
- * so per-row hooks can branch when needed (e.g. an `afterDelete` that
58
- * runs cleanup only when the child record was actually removed, not
59
- * when a pivot row was detached).
60
- */
61
- export type RepeaterRelationMode =
62
- | 'hasMany'
63
- | 'morphMany'
64
- | 'belongsToMany'
65
- | 'morphToMany'
66
- | 'morphedByMany'
67
-
68
- /**
69
- * Context passed to `Repeater.afterCreate / afterUpdate / afterDelete`
70
- * hooks. One invocation per row; `parent` is the post-save parent record
71
- * (PK already set), `record` carries the persisted child.
72
- *
73
- * For M2M modes (`belongsToMany / morphToMany / morphedByMany`)
74
- * `afterDelete` fires after `accessor.detach(...)` — the child record
75
- * may still exist (other parents may link to it). Branch on `ctx.mode`
76
- * if your cleanup depends on physical deletion vs detach.
77
- */
78
- export interface RepeaterRowContext<P = unknown> {
79
- /** Post-save parent record (the same `record` the surrounding form's
80
- * `afterSave` would see). */
81
- parent: P
82
- /** Convenience — `parent[primaryKey]`. */
83
- parentId: string | number
84
- /** The Repeater field's `name`. */
85
- field: string
86
- /** 0-based index of the row in the submitted set; `-1` for `afterDelete`
87
- * since deleted rows aren't in the submitted set. */
88
- index: number
89
- /** Underlying relation mode — see above for `afterDelete` semantics
90
- * on M2M. */
91
- mode: RepeaterRelationMode
92
- }
93
-
94
- /** Handler signature shared by the three after-hooks. */
95
- export type RepeaterRowAfterHandler<C = unknown, P = unknown> = (
96
- record: C,
97
- ctx: RepeaterRowContext<P>,
98
- ) => void | Promise<void>
99
-
100
- /**
101
- * Function evaluated once per row at meta-build to derive a human-readable
102
- * label for the collapsed-row header. Called with the row's submitted values
103
- * (or `{}` on a fresh blank row); should return a short string. Errors are
104
- * swallowed and the renderer falls back to the row index.
105
- */
106
- export type RepeaterItemLabel = (row: Record<string, unknown>) => string
107
-
108
- /**
109
- * Responsive column-count config for `Repeater.grid()` and `Builder.grid()`.
110
- * Keys map to the standard Tailwind breakpoints (sm/md/lg/xl/2xl). `default`
111
- * sets the column count below the smallest declared breakpoint and falls back
112
- * to 1 when omitted. Pass values ≥ 2 — anything lower is dropped at the field
113
- * level so the renderer sees only meaningful entries.
114
- *
115
- * Example: `.grid({ default: 1, md: 2, xl: 3 })` renders one column on mobile,
116
- * two from `md` up, three from `xl` up.
117
- */
118
- export interface ResponsiveGridConfig {
119
- default?: number
120
- sm?: number
121
- md?: number
122
- lg?: number
123
- xl?: number
124
- '2xl'?: number
125
- }
126
-
127
- /** Single-number form for `grid(n)` (current API) OR responsive object form. */
128
- export type RepeaterGridConfig = number | ResponsiveGridConfig
129
-
130
- /**
131
- * Header descriptor for `Repeater.table([...])` mode. One entry per inner
132
- * schema field, in declaration order — column[i] is the header for
133
- * `schema[i]`. Object literal (not a class) to keep the surface lean;
134
- * promote to a builder class if we ever need chaining or async resolvers.
135
- *
136
- * `alignment` aligns header text + cell contents (cells use `text-*`).
137
- * `width` is a raw CSS width string passed to `<col style="width: …">`.
138
- * `required` adds a red asterisk after the header label — purely visual,
139
- * doesn't affect validation (the inner field's own `required()` does).
140
- */
141
- export interface RepeaterTableColumn {
142
- label: string
143
- alignment?: 'left' | 'center' | 'right'
144
- width?: string
145
- required?: boolean
146
- }
147
-
148
- /**
149
- * Per-row visibility rule. Either a literal `boolean` or a callback receiving
150
- * a row-scoped `LayoutContext`. The context's `values / $get / $set / row` are
151
- * row-local; `record / user` mirror the parent form's render context.
152
- *
153
- * Returning truthy hides the row. The renderer keeps hidden rows mounted with
154
- * `display: none` so their inputs (and `__id`) round-trip through FormData
155
- * unchanged — visibility is purely UX, never a data filter.
156
- *
157
- * Throwing predicates fail-closed-as-visible (i.e. row stays visible) and log
158
- * a warning. We choose the inverse posture from `Element.evaluateVisibility`
159
- * because a misbehaving `itemHidden` should not silently hide rows the user
160
- * thinks they're editing.
161
- */
162
- export type RepeaterItemHiddenRule =
163
- | boolean
164
- | ((ctx: LayoutContext) => boolean | Promise<boolean>)
165
-
166
- /**
167
- * Per-row capability gate. Evaluated against a row-scoped `LayoutContext`
168
- * (same shape as `itemHidden`'s rule). Resolving truthy keeps the
169
- * capability enabled (the matching row button stays mounted); resolving
170
- * falsy removes it. Throwing predicates fail-open — the capability stays
171
- * enabled and we log a warning, mirroring `itemHidden`'s posture so a
172
- * misbehaving rule doesn't silently lock the user out of editing data.
173
- *
174
- * Used by `itemCanDelete / itemCanClone / itemCanReorder`.
175
- */
176
- export type RepeaterItemCanRule =
177
- | boolean
178
- | ((ctx: LayoutContext) => boolean | Promise<boolean>)
179
-
180
- /**
181
- * Resolved metadata for a single Repeater row. `id` is a stable identifier
182
- * scoped to the form render — survives reorder + clone client-side and is
183
- * round-tripped through a hidden `__id` value on submit so the renderer
184
- * keeps stable React keys across SSR / SPA / partial-resolve cycles.
185
- *
186
- * `children` is the resolved inner schema (resolved with row-scoped values).
187
- * Renderers iterate `rows` and feed each `children` array to `SchemaRenderer`.
188
- *
189
- * `hidden` is set when `itemHidden(rule)` resolved truthy for this row; the
190
- * renderer keeps the row mounted but hides its chrome + body so values still
191
- * round-trip on submit.
192
- */
193
- export interface RepeaterRowMeta {
194
- id: string
195
- children: ElementMeta[]
196
- itemLabel?: string
197
- hidden?: boolean
198
- /**
199
- * Resolved per-row action metas for `extraItemActions(...)`. Empty or
200
- * absent when the field has no extra actions, OR when every action's
201
- * visibility rule resolved false for this row. The renderer mounts these
202
- * in the row header alongside clone/delete; clicking dispatches the
203
- * action with `_rowPath = "<fieldName>.<index>"` so the server can
204
- * reconstruct the row-scoped handler context.
205
- */
206
- extraActions?: ActionMeta[]
207
- /**
208
- * Per-row capability flags. Stamped only when the corresponding
209
- * `itemCan*(rule)` resolved falsy for this row — non-customized rows
210
- * pay zero serialization cost. The renderer reads these and skips the
211
- * matching button: `canDelete: false` removes the trash, `canClone: false`
212
- * removes the clone (no-op when `cloneable()` is off field-wide),
213
- * `canReorder: false` removes the drag grip and Up/Down arrows on this
214
- * row only (no-op when `reorderable()` is off).
215
- *
216
- * Capability gates are presentation, not authorization — they hide the
217
- * button but don't reject tampered POST bodies. For real authorization,
218
- * gate the parent form's lifecycle hooks.
219
- */
220
- canDelete?: false
221
- canClone?: false
222
- canReorder?: false
223
- }
224
-
225
- export interface RepeaterFieldMeta extends FieldMeta {
226
- fieldType: 'repeater'
227
- rows: RepeaterRowMeta[]
228
- /** Zero-row blueprint for the client's Add button. */
229
- template: ElementMeta[]
230
- columns?: number
231
- minItems?: number
232
- maxItems?: number
233
- defaultItems?: number
234
- reorderable?: boolean
235
- collapsible?: boolean
236
- defaultCollapsed?: boolean
237
- /**
238
- * Set when `Repeater.accordion()` is configured. The renderer replaces
239
- * the per-row collapsed map with a single "open row id" slot — picking
240
- * row N collapses every other row. Implies `collapsible: true` (the
241
- * accordion ergonomic only makes sense over a collapsible repeater).
242
- */
243
- accordion?: boolean
244
- cloneable?: boolean
245
- addActionLabel?: string
246
- /**
247
- * Set when `Repeater.simple(field)` is configured. Tells the renderer
248
- * to drop the per-row chrome (header, clone, collapse) and lay the
249
- * single inner field out flush with a trash button on each row. The
250
- * wire format is unchanged — `<name>.<i>.<innerName>` — only the
251
- * stored shape differs (`[v]` instead of `[{name: v}]`).
252
- */
253
- simple?: boolean
254
- /**
255
- * Set when `Repeater.grid(...)` is configured. Lays the ROWS themselves
256
- * in an n-column grid (different from `columns(n)` which grids the inner
257
- * schema *inside* a row). Useful for tile-style pickers / member cards /
258
- * icon palettes.
259
- *
260
- * Two shapes:
261
- * - `number` (≥ 2) — fixed grid at every viewport.
262
- * - `ResponsiveGridConfig` — `{ default?, sm?, md?, lg?, xl?, '2xl'? }`
263
- * keyed by Tailwind breakpoint. The renderer emits a scoped
264
- * `<style>` block with media queries per declared breakpoint.
265
- *
266
- * Renderer swaps the outer `flex flex-col` container for a CSS grid and
267
- * skips the drag-drop indicator in grid mode (the horizontal bar reads
268
- * wrong across grid cells); button reorder still works.
269
- */
270
- grid?: RepeaterGridConfig
271
- /**
272
- * Set when `Repeater.table([...])` is configured. Renders rows as
273
- * `<tr>` and inner fields as `<td>`, with the supplied column
274
- * headers in a `<thead>`. Useful for compact uniform-row repeaters.
275
- * Mutually exclusive with `simple` (single-field shape conflicts)
276
- * and with `grid` (different layout); collapsible/accordion are
277
- * meaningless on `<tr>` rows so the renderer ignores them. The
278
- * inner schema's field labels are suppressed via `[&_label]:sr-only`
279
- * so headers carry the labelling. `clone / delete / extraActions`
280
- * land in a final actions cell when configured.
281
- */
282
- table?: {
283
- columns: RepeaterTableColumn[]
284
- }
285
- /**
286
- * Set when `Repeater.relationship(...)` is configured. Tells diagnostics
287
- * (and any future client UI) that the row diff is persisted via a
288
- * `HasMany` relation rather than a JSON column on the parent. The wire
289
- * shape carries only `{ name, orderColumn? }` — `model` and
290
- * `foreignKey` stay server-only.
291
- */
292
- relationship?: RepeaterRelationshipMeta
293
- /**
294
- * Per-slot chrome overrides for the seven built-in row buttons (add /
295
- * clone / delete / moveUp / moveDown / reorder / collapse). Authors set
296
- * these via `.addAction(RowButton.make()…)` etc.; the renderer merges
297
- * each override on top of its hardcoded default (icon + tooltip + color
298
- * + aria-label). Absent slots fall through to defaults — non-customized
299
- * Repeaters pay zero serialization cost.
300
- */
301
- buttons?: RowButtonsMeta
302
- }
303
-
304
- /**
305
- * Array-of-subschema field. The author composes an inner schema once via
306
- * `.schema([...])`; the rendered form lets the end user add / remove /
307
- * reorder rows of that schema.
308
- *
309
- * Storage on the parent record is a plain array of objects:
310
- * `[{ field1, field2 }, …]`. No special wrapper, no `position` column,
311
- * no per-row identity persistence.
312
- *
313
- * `toMeta` resolves the inner schema once per submitted row, plus a
314
- * zero-row template for the client's Add button. `coerceFormValues` and
315
- * `validateSchema` recurse into the rows using `<name>.<i>.<childName>`
316
- * dotted keys for both flat form-encoded bodies and JSON bodies.
317
- *
318
- * Plan #14.
319
- */
320
- export class RepeaterField extends Field {
321
- protected override _children: Element[] = []
322
-
323
- private _columns?: number
324
- private _minItems?: number
325
- private _maxItems?: number
326
- private _defaultItems = 1
327
- private _reorderable = false
328
- private _collapsible = false
329
- private _defaultCollapsed = false
330
- private _accordion = false
331
- private _cloneable = false
332
- private _addActionLabel?: string
333
- private _itemLabel?: RepeaterItemLabel
334
- private _itemHidden?: RepeaterItemHiddenRule
335
- private _itemCanDelete?: RepeaterItemCanRule
336
- private _itemCanClone?: RepeaterItemCanRule
337
- private _itemCanReorder?: RepeaterItemCanRule
338
- private _extraItemActions: Action[] = []
339
- private _simple = false
340
- private _grid?: RepeaterGridConfig
341
- private _tableColumns?: RepeaterTableColumn[]
342
- private _buttons: { [K in RowButtonKind]?: RowButton } = {}
343
- private _relationship?: RepeaterRelationshipConfig
344
- private _afterCreate?: RepeaterRowAfterHandler
345
- private _afterUpdate?: RepeaterRowAfterHandler
346
- private _afterDelete?: RepeaterRowAfterHandler
347
-
348
- private constructor(name: string) {
349
- super(name, 'repeater')
350
- }
351
-
352
- static make(name: string): RepeaterField {
353
- return new RepeaterField(name)
354
- }
355
-
356
- /** Inner schema rendered per row. Each row resolves these elements. */
357
- schema(elements: Element[]): this {
358
- this._children = elements
359
- return this
360
- }
361
-
362
- /**
363
- * Single-field "flat array" Repeater. Storage shape changes from
364
- * `[{ <innerName>: value }]` to `[value, value, …]` — handy for
365
- * keyword/alias/alt-domain lists where the row is just one input.
366
- *
367
- * Wire format on the form stays the same `<name>.<i>.<innerName>` shape
368
- * (the inner field's name is opaque to the consumer); the flat shape
369
- * shows up only in the saved record (after coerce → unwrap) and in the
370
- * loaded record (re-wrapped on the way into `resolveRepeaterRows`).
371
- *
372
- * Validators run against the wrapped shape so per-field rules
373
- * (`required`, `unique`, custom validators) work the same as in a
374
- * regular Repeater. The chrome strips down: no per-row header, no
375
- * collapse, no clone — just an inline trash button on each row plus
376
- * the bottom Add button. Reorder still works when `reorderable()`
377
- * is set.
378
- *
379
- * Calling `simple()` replaces the inner schema with the single field —
380
- * pass any prior `schema(...)` you'd called as wasted; `simple()` is
381
- * the schema for these rows.
382
- */
383
- simple(field: Field): this {
384
- if (this._relationship) {
385
- throw new Error(
386
- `[Pilotiq] Repeater "${this.name}": simple() is incompatible with relationship() — flat-array storage can't round-trip through child records.`,
387
- )
388
- }
389
- this._simple = true
390
- this._children = [field]
391
- return this
392
- }
393
-
394
- /** Grid column count for the inner schema (passed through to client). */
395
- columns(n: number): this { this._columns = n; return this }
396
-
397
- /** Number of empty rows to render initially when no values exist. */
398
- defaultItems(n: number): this { this._defaultItems = n; return this }
399
-
400
- /** Validator + client gate: at least `n` rows on submit. */
401
- minItems(n: number): this { this._minItems = n; return this }
402
-
403
- /** Validator + client gate: at most `n` rows on submit. */
404
- maxItems(n: number): this { this._maxItems = n; return this }
405
-
406
- /** Show drag handle + keyboard reorder controls per row. */
407
- reorderable(value: boolean = true): this { this._reorderable = value; return this }
408
-
409
- /** Show collapse chevron per row. */
410
- collapsible(value: boolean = true): this { this._collapsible = value; return this }
411
-
412
- /** Render rows collapsed by default (requires `collapsible()`). */
413
- collapsed(value: boolean = true): this { this._defaultCollapsed = value; return this }
414
-
415
- /**
416
- * Accordion mode: only one row open at a time. Picking a different row
417
- * collapses the currently-open one. Pair with `collapsed()` to start
418
- * with every row collapsed (default is "first row open"). Auto-arms
419
- * `collapsible()` since the accordion ergonomic is meaningless on a
420
- * non-collapsible repeater.
421
- */
422
- accordion(value: boolean = true): this {
423
- this._accordion = value
424
- if (value) this._collapsible = true
425
- return this
426
- }
427
-
428
- /** Show duplicate-row button per row. */
429
- cloneable(value: boolean = true): this { this._cloneable = value; return this }
430
-
431
- /**
432
- * Function evaluated per row for the collapsed-row header. The result
433
- * lands on `RepeaterRowMeta.itemLabel`; renderers fall back to the row
434
- * index when missing or when the function throws.
435
- */
436
- itemLabel(fn: RepeaterItemLabel): this { this._itemLabel = fn; return this }
437
-
438
- /**
439
- * Per-row visibility predicate. Evaluated against a row-scoped
440
- * `LayoutContext` (`values / $get / $set / row` all row-local). Returning
441
- * truthy hides the row from the user; the renderer keeps inputs mounted
442
- * via `display: none` so values round-trip through FormData on submit.
443
- *
444
- * Throwing → row stays visible + warn (inverse of layout `visible()` —
445
- * a misbehaving rule shouldn't silently hide data the user is editing).
446
- */
447
- itemHidden(rule: RepeaterItemHiddenRule): this { this._itemHidden = rule; return this }
448
-
449
- /**
450
- * Per-row gate for the trash button. Resolving truthy keeps the trash
451
- * button visible (default); resolving falsy hides it on that row only.
452
- * Useful for "this row is finalized — only the others can be removed".
453
- *
454
- * Predicate sees the same row-scoped `LayoutContext` as `itemHidden`
455
- * (`values / $get / $set / row` are row-local, `record / user` mirror
456
- * the parent form). Throwing predicates fail-open — the button stays
457
- * visible and we log a warning, mirroring `itemHidden`'s posture.
458
- *
459
- * Presentation only. Tampered POST bodies that delete a gated row will
460
- * still go through; gate the parent form's lifecycle hooks for real
461
- * authorization.
462
- */
463
- itemCanDelete(rule: RepeaterItemCanRule): this { this._itemCanDelete = rule; return this }
464
-
465
- /**
466
- * Per-row gate for the clone (`Duplicate row`) button. Resolving truthy
467
- * keeps it visible (default); resolving falsy hides it on that row only.
468
- * No-op when `cloneable()` is off field-wide.
469
- *
470
- * Same predicate shape, same fail-open posture as `itemCanDelete`.
471
- */
472
- itemCanClone(rule: RepeaterItemCanRule): this { this._itemCanClone = rule; return this }
473
-
474
- /**
475
- * Per-row gate for the reorder controls (drag grip + Up / Down arrows).
476
- * Resolving truthy keeps them visible (default); resolving falsy hides
477
- * them on that row only — pinning the row to its current position while
478
- * its neighbours stay reorderable. No-op when `reorderable()` is off
479
- * field-wide.
480
- *
481
- * Same predicate shape, same fail-open posture as `itemCanDelete`. Drag
482
- * targeting is unaffected — a non-pinned row can still drop at the
483
- * pinned row's slot, which is the right semantics (the OTHER row is the
484
- * one being moved). Pin both sides if you need a pair to stay adjacent.
485
- */
486
- itemCanReorder(rule: RepeaterItemCanRule): this { this._itemCanReorder = rule; return this }
487
-
488
- /** Custom label for the "Add row" button. Default `'Add'`. */
489
- addActionLabel(label: string): this { this._addActionLabel = label; return this }
490
-
491
- /**
492
- * Lay the ROWS themselves in an n-column grid — different from
493
- * `columns(n)` which grids the inner schema *inside* a row.
494
- *
495
- * Two forms:
496
- * - `grid(2)` — fixed 2-column grid at every viewport.
497
- * - `grid({ default: 1, md: 2, xl: 3 })` — responsive: column count
498
- * changes at the named Tailwind breakpoint (sm 640px / md 768px /
499
- * lg 1024px / xl 1280px / 2xl 1536px). `default` (or `1` when
500
- * omitted) is the count below the smallest declared breakpoint.
501
- *
502
- * Pass `n >= 2` or an object with at least one breakpoint having a value
503
- * `>= 2`; otherwise the grid mode resets (vertical stack, the default).
504
- *
505
- * In grid mode the renderer keeps reorder buttons working but
506
- * suppresses the horizontal drop indicator (which doesn't read
507
- * across grid cells). Drag-and-drop itself still moves rows; the
508
- * cursor is the only feedback.
509
- */
510
- grid(config: RepeaterGridConfig): this {
511
- const normalized = normalizeGridConfig(config)
512
- if (normalized === undefined) delete this._grid
513
- else this._grid = normalized
514
- return this
515
- }
516
-
517
- /**
518
- * Render rows as a compact HTML table — one `<tr>` per row, one
519
- * `<td>` per inner field, with the supplied column headers above.
520
- * Columns map 1:1 to `schema()` fields in declaration order.
521
- *
522
- * Pass an empty array to turn table mode off (handy for toggling via
523
- * a config value). Mutually exclusive with `simple()` (single-field
524
- * shape conflicts) and `grid()` (different layout) — the field
525
- * applies whichever was set last; renderer ignores collapsible /
526
- * accordion in table mode (`<tr>` rows can't collapse). The inner
527
- * schema's field labels render `sr-only` so headers carry the
528
- * labelling; clone / delete / `extraItemActions` land in a final
529
- * actions cell when configured.
530
- */
531
- table(columns: RepeaterTableColumn[]): this {
532
- if (columns.length === 0) delete this._tableColumns
533
- else this._tableColumns = columns
534
- return this
535
- }
536
-
537
- /**
538
- * Persist rows to a `HasMany` (or `MorphMany` / `MorphOne`) relation on
539
- * the parent record instead of serializing them to a JSON column. Each
540
- * row maps to a real child record; submit creates / updates / deletes
541
- * children against the relation transparently.
542
- *
543
- * For `morphMany`, the child also carries `<morphName>Id` +
544
- * `<morphName>Type` columns; the framework stamps both on every create
545
- * (and refuses to overwrite them on update) so a tampered POST body
546
- * can't reassign a row to a different polymorphic parent. The morph
547
- * shape is read off the parent's `static relations[name]` descriptor —
548
- * no `foreignKey` override applies.
549
- *
550
- * Pass either the relationship name as a string (the common case —
551
- * `model` + `foreignKey` are auto-discovered from the parent's
552
- * `static relations` map) or an object form for explicit overrides.
553
- *
554
- * Mutually exclusive with `simple()` (the flat `[v, v, …]` storage
555
- * shape can't round-trip through child records that need named
556
- * columns) and with `dehydrated(false)` (the field's whole purpose
557
- * is to write data — silently dropping it would be confusing).
558
- * Validators run unchanged.
559
- */
560
- relationship(arg: string | RepeaterRelationshipConfig): this {
561
- if (this._simple) {
562
- throw new Error(
563
- `[Pilotiq] Repeater "${this.name}": relationship() is incompatible with simple() — relationship-backed rows need named columns on the child model.`,
564
- )
565
- }
566
- if (this.isDehydrated() === false) {
567
- throw new Error(
568
- `[Pilotiq] Repeater "${this.name}": relationship() is incompatible with dehydrated(false) — the field's purpose is to persist data.`,
569
- )
570
- }
571
- this._relationship = typeof arg === 'string' ? { name: arg } : { ...arg }
572
- return this
573
- }
574
-
575
- /**
576
- * Sugar over `.relationship({ ..., orderColumn: col })`. Sets the
577
- * order column on an already-configured relationship; throws when
578
- * `relationship()` hasn't been called first so misconfiguration
579
- * surfaces eagerly.
580
- */
581
- orderColumn(col: string): this {
582
- if (!this._relationship) {
583
- throw new Error(
584
- `[Pilotiq] Repeater "${this.name}": orderColumn() requires relationship() to be configured first.`,
585
- )
586
- }
587
- this._relationship.orderColumn = col
588
- return this
589
- }
590
-
591
- /**
592
- * M2M only — declare pivot-table extra columns to surface as editable
593
- * per-row fields. Each entry must match an inner-schema field's `name`;
594
- * row values for those names load via `accessor.withPivot(...)`,
595
- * persist via `accessor.attach({ id: pivotData })` for new rows and
596
- * `accessor.updatePivot(id, pivotData)` for existing rows.
597
- *
598
- * Sugar over `.relationship({ ..., pivotColumns: cols })`. Like
599
- * `orderColumn()`, throws when `relationship()` hasn't been called
600
- * first. Empty array clears the projection.
601
- */
602
- pivotColumns(cols: string[]): this {
603
- if (!this._relationship) {
604
- throw new Error(
605
- `[Pilotiq] Repeater "${this.name}": pivotColumns() requires relationship() to be configured first.`,
606
- )
607
- }
608
- this._relationship.pivotColumns = [...cols]
609
- return this
610
- }
611
-
612
- /**
613
- * Per-row hook that fires after each newly created child record is
614
- * persisted in `relationship()` mode. Receives the created record
615
- * (with its primary key set) and a `RepeaterRowContext` carrying
616
- * the parent record + row index + relation mode. Errors propagate
617
- * — a throwing handler aborts the rest of the persist diff (the
618
- * parent + any earlier rows have already saved; v1 is non-
619
- * transactional).
620
- *
621
- * No-op outside `relationship()` mode. Use `Form.afterCreate(...)`
622
- * for hooks that fire on the parent record's create.
623
- */
624
- afterCreate(fn: RepeaterRowAfterHandler): this {
625
- if (!this._relationship) {
626
- throw new Error(
627
- `[Pilotiq] Repeater "${this.name}": afterCreate() requires relationship() to be configured first.`,
628
- )
629
- }
630
- this._afterCreate = fn
631
- return this
632
- }
633
-
634
- /** Per-row hook that fires after each existing child record is
635
- * updated. Receives the updated record from the child model's
636
- * `update()` return (or the post-update reload when `update`
637
- * returns void). See `afterCreate` notes for error semantics. */
638
- afterUpdate(fn: RepeaterRowAfterHandler): this {
639
- if (!this._relationship) {
640
- throw new Error(
641
- `[Pilotiq] Repeater "${this.name}": afterUpdate() requires relationship() to be configured first.`,
642
- )
643
- }
644
- this._afterUpdate = fn
645
- return this
646
- }
647
-
648
- /** Per-row hook that fires after each removed row is processed.
649
- * For `hasMany / morphMany` modes the child record was physically
650
- * deleted via `model.delete()`; for M2M modes only the pivot row
651
- * was detached and the child may still exist. Branch on
652
- * `ctx.mode` when that distinction matters. */
653
- afterDelete(fn: RepeaterRowAfterHandler): this {
654
- if (!this._relationship) {
655
- throw new Error(
656
- `[Pilotiq] Repeater "${this.name}": afterDelete() requires relationship() to be configured first.`,
657
- )
658
- }
659
- this._afterDelete = fn
660
- return this
661
- }
662
-
663
- /**
664
- * Customize the bottom Add button's chrome (label / icon / color /
665
- * tooltip). Equivalent to `addActionLabel()` plus icon + color
666
- * overrides — when both are set, this customizer wins (it ships under
667
- * `meta.buttons.add` which the renderer reads after `addActionLabel`).
668
- */
669
- addAction(b: RowButton): this { this._buttons.add = b; return this }
670
-
671
- /** Customize the per-row clone (`Duplicate row`) button. */
672
- cloneAction(b: RowButton): this { this._buttons.clone = b; return this }
673
-
674
- /** Customize the per-row trash (`Remove row`) button. */
675
- deleteAction(b: RowButton): this { this._buttons.delete = b; return this }
676
-
677
- /** Customize the per-row Up arrow. */
678
- moveUpAction(b: RowButton): this { this._buttons.moveUp = b; return this }
679
-
680
- /** Customize the per-row Down arrow. */
681
- moveDownAction(b: RowButton): this { this._buttons.moveDown = b; return this }
682
-
683
- /**
684
- * Customize the drag-grip handle. `label` becomes the `aria-label`,
685
- * `tooltip` becomes the `title` attribute, `icon` swaps the grip glyph,
686
- * `color` re-tones the handle. The grip isn't a real `<button>` (it's a
687
- * draggable `<span>`) so click semantics don't apply.
688
- */
689
- reorderAction(b: RowButton): this { this._buttons.reorder = b; return this }
690
-
691
- /**
692
- * Customize the per-row collapse chevron. Applies to BOTH states by
693
- * default — the open chevron and the collapsed chevron share the
694
- * override unless `expandAction(...)` is also set, in which case
695
- * `collapseAction` covers only the open state and `expandAction`
696
- * covers the collapsed state.
697
- */
698
- collapseAction(b: RowButton): this { this._buttons.collapse = b; return this }
699
-
700
- /**
701
- * Customize the per-row chevron when the row is currently *collapsed*
702
- * (i.e. the "click me to expand" state). Sibling of `collapseAction`
703
- * for the closed-state glyph; without this, both states fall through
704
- * to `collapseAction` (back-compat) and ultimately to the default
705
- * chevron pair (right when collapsed, down when open).
706
- */
707
- expandAction(b: RowButton): this { this._buttons.expand = b; return this }
708
-
709
- /**
710
- * Mount an "Expand all" button in the field header — clicking it opens
711
- * every collapsed row. Opt-in: calling without args shows the button
712
- * with the default icon (chevron-down) + label ("Expand all"); pass a
713
- * `RowButton` to override icon / label / tooltip / color.
714
- *
715
- * Auto-arms `collapsible()` since the affordance is meaningless without
716
- * collapsible rows. In `accordion()` mode the button opens the first
717
- * visible row (accordion's "only one open" invariant survives).
718
- */
719
- expandAllAction(button?: RowButton): this {
720
- this._buttons.expandAll = button ?? RowButton.make()
721
- this._collapsible = true
722
- return this
723
- }
724
-
725
- /**
726
- * Mount a "Collapse all" button in the field header — clicking it
727
- * collapses every open row. Opt-in (calling enables; pass a
728
- * `RowButton` to customize). Auto-arms `collapsible()`. In `accordion()`
729
- * mode the button closes the currently-open row, leaving everything
730
- * collapsed.
731
- */
732
- collapseAllAction(button?: RowButton): this {
733
- this._buttons.collapseAll = button ?? RowButton.make()
734
- this._collapsible = true
735
- return this
736
- }
737
-
738
- /**
739
- * Per-row action buttons rendered in each row's header alongside the
740
- * built-in clone/delete strip. Useful for "Mark featured", "Send test",
741
- * "Run preview", etc. — handler-style only in v1 (no `.href(…)` or
742
- * `.method(…)`-style row actions; no modal-form actions either).
743
- *
744
- * Each action's handler receives a row-scoped `ActionContext` with
745
- * `ctx.row = { index, id, values }` (the row's submitted values, not
746
- * the parent record). Visibility rules (`visible / hidden / disabled`)
747
- * are evaluated per row at meta-build with `{ values, record, user }`
748
- * — `values` is the row's data, `record` mirrors the parent form's
749
- * record (same as inner-field condition callbacks).
750
- *
751
- * Actions register one per row; the dispatcher uses `_rowPath` from the
752
- * submit body to know which row was triggered. Naming collisions
753
- * between row actions and page-level actions are NOT allowed (the
754
- * server's `findActions` walker treats row-scoped actions separately
755
- * via `findRowExtraActions`, but listing the same name in both spots
756
- * is undefined behavior).
757
- */
758
- extraItemActions(actions: Action[]): this {
759
- this._extraItemActions = actions
760
- return this
761
- }
762
-
763
- // ─── Read-only access for resolver / coercion / validation ──
764
-
765
- override getChildren(): Element[] | undefined {
766
- return this._children.length > 0 ? this._children : undefined
767
- }
768
-
769
- /** Direct access to the inner schema — used by coercion + validation. */
770
- getInnerSchema(): Element[] { return this._children }
771
-
772
- getColumns(): number | undefined { return this._columns }
773
- getDefaultItems(): number { return this._defaultItems }
774
- getMinItems(): number | undefined { return this._minItems }
775
- getMaxItems(): number | undefined { return this._maxItems }
776
- isReorderable(): boolean { return this._reorderable }
777
- isCollapsible(): boolean { return this._collapsible }
778
- isDefaultCollapsed(): boolean { return this._defaultCollapsed }
779
- isAccordion(): boolean { return this._accordion }
780
- isCloneable(): boolean { return this._cloneable }
781
- getItemLabel(): RepeaterItemLabel | undefined { return this._itemLabel }
782
- getItemHidden(): RepeaterItemHiddenRule | undefined { return this._itemHidden }
783
- getItemCanDelete(): RepeaterItemCanRule | undefined { return this._itemCanDelete }
784
- getItemCanClone(): RepeaterItemCanRule | undefined { return this._itemCanClone }
785
- getItemCanReorder(): RepeaterItemCanRule | undefined { return this._itemCanReorder }
786
- getAddActionLabel(): string | undefined { return this._addActionLabel }
787
- getExtraItemActions(): Action[] { return this._extraItemActions }
788
- isSimple(): boolean { return this._simple }
789
- getGrid(): RepeaterGridConfig | undefined { return this._grid }
790
- getTableColumns(): RepeaterTableColumn[] | undefined { return this._tableColumns }
791
- isTable(): boolean { return this._tableColumns !== undefined }
792
- /** Resolved relationship config (`undefined` when not configured). */
793
- getRelationship(): RepeaterRelationshipConfig | undefined { return this._relationship }
794
- isRelationship(): boolean { return this._relationship !== undefined }
795
-
796
- getAfterCreate(): RepeaterRowAfterHandler | undefined { return this._afterCreate }
797
- getAfterUpdate(): RepeaterRowAfterHandler | undefined { return this._afterUpdate }
798
- getAfterDelete(): RepeaterRowAfterHandler | undefined { return this._afterDelete }
799
- /** The configured customizer for a given slot, or `undefined`. */
800
- getButton(kind: RowButtonKind): RowButton | undefined { return this._buttons[kind] }
801
- /**
802
- * The single inner field of a `simple()` repeater. Returns `undefined`
803
- * outside simple mode (or when the inner schema hasn't been set yet).
804
- * Used by the wrap/unwrap helpers in `dispatchForm` and `resolveSchema`
805
- * — internal contract is "the simple inner field's name is the wrapping
806
- * key for `[v]` ↔ `[{name: v}]` transforms".
807
- */
808
- getSimpleInnerField(): Field | undefined {
809
- if (!this._simple) return undefined
810
- const first = this._children[0]
811
- if (first instanceof Field) return first
812
- return undefined
813
- }
814
-
815
- // ─── Meta ──────────────────────────────────────────────
816
-
817
- /**
818
- * Build the bare Repeater meta — base FieldMeta + per-Repeater config
819
- * keys. Rows + template are populated by the resolver in step 2 (an
820
- * `ElementResolver` registered for `'field'` is the wrong shape since
821
- * Repeater needs ctx.values per-row; the resolver special-cases
822
- * Repeater inline). Until then this returns an empty rows/template
823
- * pair so the type stays stable.
824
- */
825
- override toMeta(ctx?: RenderContext): RepeaterFieldMeta {
826
- const base = this.buildMeta(ctx)
827
- const meta: RepeaterFieldMeta = {
828
- ...base,
829
- fieldType: 'repeater',
830
- rows: [],
831
- template: [],
832
- defaultItems: this._defaultItems,
833
- }
834
- if (this._columns !== undefined) meta.columns = this._columns
835
- if (this._minItems !== undefined) meta.minItems = this._minItems
836
- if (this._maxItems !== undefined) meta.maxItems = this._maxItems
837
- if (this._reorderable) meta.reorderable = true
838
- if (this._collapsible) meta.collapsible = true
839
- if (this._defaultCollapsed) meta.defaultCollapsed = true
840
- if (this._accordion) meta.accordion = true
841
- if (this._cloneable) meta.cloneable = true
842
- if (this._addActionLabel !== undefined) meta.addActionLabel = this._addActionLabel
843
- if (this._simple) meta.simple = true
844
- if (this._grid !== undefined) meta.grid = this._grid
845
- if (this._tableColumns !== undefined) meta.table = { columns: this._tableColumns }
846
- if (this._relationship !== undefined) {
847
- const r: RepeaterRelationshipMeta = { name: this._relationship.name }
848
- if (this._relationship.orderColumn !== undefined) r.orderColumn = this._relationship.orderColumn
849
- meta.relationship = r
850
- }
851
- const buttons = serializeRowButtons(this._buttons)
852
- if (buttons !== undefined) meta.buttons = buttons
853
- return meta
854
- }
855
- }
856
-
857
- export const Repeater = RepeaterField
858
-
859
- /**
860
- * Structural Repeater check — Vite's SSR module cache can load this
861
- * package via two paths during a single dev session, so `instanceof
862
- * RepeaterField` can silently return false across module copies. The
863
- * structural check uses the serialized type discriminator + the
864
- * `fieldType` property name, both of which are stable strings.
865
- *
866
- * Mirrors the pattern documented in
867
- * `feedback_vite_ssr_module_dup_instanceof.md`.
868
- */
869
- export function isRepeaterField(el: { getType(): string; fieldType?: string }): boolean {
870
- return el.getType() === 'field' && el['fieldType'] === 'repeater'
871
- }
872
-
873
- /**
874
- * Walk a sparse `{ [kind]: RowButton }` map and serialize only the slots
875
- * the user actually set. Returns `undefined` when nothing is configured so
876
- * `meta.buttons` stays absent for non-customized fields. Each slot's inner
877
- * meta is the result of `RowButton.toMeta()` — already JSON-safe and
878
- * already drops unset keys.
879
- *
880
- * Shared between Repeater + Builder so the wire shape stays identical
881
- * across both renderers.
882
- */
883
- export function serializeRowButtons(
884
- buttons: { [K in RowButtonKind]?: RowButton },
885
- ): RowButtonsMeta | undefined {
886
- const out: RowButtonsMeta = {}
887
- let any = false
888
- for (const k of Object.keys(buttons) as RowButtonKind[]) {
889
- const b = buttons[k]
890
- if (b === undefined) continue
891
- out[k] = b.toMeta()
892
- any = true
893
- }
894
- return any ? out : undefined
895
- }
896
-
897
- const RESPONSIVE_BREAKPOINTS = ['default', 'sm', 'md', 'lg', 'xl', '2xl'] as const
898
-
899
- /**
900
- * Coerce a user-supplied grid config into the shape stored on `_grid`.
901
- *
902
- * - `number` ≥ 2 → returned unchanged.
903
- * - `number` < 2 → `undefined` (resets the grid mode — mirrors the existing
904
- * "passing 1 turns it off" sentinel).
905
- * - `ResponsiveGridConfig` → object with each declared breakpoint floored
906
- * to an integer ≥ 2; entries below 2 are dropped. When no breakpoint
907
- * survives the filter, returns `undefined` (no grid mode at any size).
908
- * Single-entry results collapse to the bare number when the only
909
- * surviving entry is `default` — that's exactly the scalar form.
910
- *
911
- * Shared with `Builder.grid()` so the two fields validate identically.
912
- */
913
- export function normalizeGridConfig(
914
- config: RepeaterGridConfig,
915
- ): RepeaterGridConfig | undefined {
916
- if (typeof config === 'number') {
917
- return config >= 2 ? Math.floor(config) : undefined
918
- }
919
- const out: ResponsiveGridConfig = {}
920
- let breakpointCount = 0 // declared breakpoints other than `default`
921
- for (const k of RESPONSIVE_BREAKPOINTS) {
922
- const raw = config[k]
923
- if (typeof raw !== 'number') continue
924
- const n = Math.floor(raw)
925
- // `default` allows n=1 (explicit "vertical stack below the smallest
926
- // declared breakpoint"). Other breakpoints require n >= 2 — anything
927
- // smaller would just fall through to the previous rule.
928
- if (k === 'default' ? n < 1 : n < 2) continue
929
- out[k] = n
930
- if (k !== 'default') breakpointCount++
931
- }
932
- // No actual breakpoint overrides → no responsive behavior. Either
933
- // collapse to the scalar form (when default exists) or reset.
934
- if (breakpointCount === 0) {
935
- if (typeof out.default === 'number' && out.default >= 2) return out.default
936
- return undefined
937
- }
938
- return out
939
- }