@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
@@ -0,0 +1,288 @@
1
+ # Field Catalog
2
+
3
+ 24 built-in field types. Every field is a static `make(name)` builder; all share the `Field` base setters (see "Common setters" at the bottom).
4
+
5
+ ## Text-like
6
+
7
+ ```ts
8
+ TextField.make('title')
9
+ .placeholder('e.g. My first article')
10
+ .prefix('https://')
11
+ .suffix('.com')
12
+ .copyable() // value-side copy icon
13
+ .password() // <input type="password">
14
+ .revealable() // password reveal toggle
15
+ .mask('+1 (999) 999-9999') // input mask
16
+ .datalist(['draft', 'review', 'final'])// browser-native datalist
17
+ .stripCharacters(/[^\w-]/g) // sanitize on input
18
+ .trim() // trim whitespace
19
+ .inputMode('email') // mobile keyboard hint
20
+ .autocapitalize('words')
21
+
22
+ EmailField.make('email') // auto-attaches email() validator
23
+
24
+ NumberField.make('price')
25
+ .min(0).max(10_000).step(0.01)
26
+
27
+ Slider.make('volume')
28
+ .range(0, 100).step(5)
29
+ ```
30
+
31
+ ## Long text
32
+
33
+ ```ts
34
+ Textarea.make('bio')
35
+ .rows(5)
36
+ .cols(80)
37
+ .autosize() // grow with content
38
+ .disableGrammarly()
39
+
40
+ MarkdownField.make('body')
41
+ .toolbarButtons(['bold', 'italic', 'link', 'codeBlock'])
42
+ .minHeight('20rem')
43
+
44
+ RichTextField.make('description') // requires @pilotiq/tiptap
45
+ .toolbarButtons([...])
46
+
47
+ CodeEditorField.make('snippet') // requires @pilotiq/codemirror
48
+ .language('javascript')
49
+ .lineNumbers()
50
+ ```
51
+
52
+ `RichTextField` and `CodeEditorField` ship in separate adapter packages — install `@pilotiq/tiptap` / `@pilotiq/codemirror` and register via `.plugins([tiptap(), codeEditor()])` on the panel.
53
+
54
+ ## Choice
55
+
56
+ ```ts
57
+ SelectField.make('status')
58
+ .options({ draft: 'Draft', review: 'In review', published: 'Published' })
59
+ .searchable()
60
+ .nullable() // adds a clear option
61
+ .preload() // fetch all options on mount (default)
62
+
63
+ // Dynamic options
64
+ SelectField.make('region')
65
+ .options(async ({ $get }) => {
66
+ const country = $get('country')
67
+ return country ? regionsFor(country) : {}
68
+ })
69
+ .live()
70
+
71
+ // Inline create
72
+ SelectField.make('tagId')
73
+ .options(async () => Object.fromEntries((await Tag.all()).map(t => [t.id, t.name])))
74
+ .createOptionForm([TextField.make('name').required()])
75
+ .createOptionUsing(async ({ name }) => {
76
+ const tag = await Tag.create({ name })
77
+ return tag.id // return the new value
78
+ })
79
+
80
+ RadioField.make('priority') // single-select radio stack
81
+ .options({ low: 'Low', med: 'Medium', high: 'High' })
82
+
83
+ ToggleButtons.make('size') // chip-style segmented (sugar over Radio)
84
+ .options({ s: 'S', m: 'M', l: 'L', xl: 'XL' })
85
+ ```
86
+
87
+ ## Boolean
88
+
89
+ ```ts
90
+ CheckboxField.make('agreeToTerms') // single bool, renders as checkbox
91
+ .required('You must agree to continue')
92
+
93
+ ToggleField.make('isPublic') // single bool, renders as switch
94
+
95
+ CheckboxList.make('topics') // string[] value, checkbox stack
96
+ .options({ js: 'JavaScript', ts: 'TypeScript', rust: 'Rust' })
97
+ ```
98
+
99
+ ## Tags / collections
100
+
101
+ ```ts
102
+ TagsInput.make('keywords') // string[] value, JSON-encoded
103
+ .suggestions(['design', 'code', 'process'])
104
+ .reorderable() // HTML5 drag-and-drop
105
+ .maxTags(8)
106
+ .separator(',')
107
+
108
+ KeyValueField.make('metadata') // Record<string, string>
109
+ .keyLabel('Field')
110
+ .valueLabel('Value')
111
+ ```
112
+
113
+ ## Date / time / color / file
114
+
115
+ ```ts
116
+ DateField.make('publishedAt')
117
+ .minDate(new Date()) // future-only
118
+
119
+ DateTimePicker.make('eventAt')
120
+ .seconds(false)
121
+
122
+ ColorPicker.make('brandColor')
123
+ .palette(['#ef4444', '#10b981', '#3b82f6'])
124
+
125
+ FileUpload.make('cover')
126
+ .accept('image/*')
127
+ .maxSize(5 * 1024 * 1024) // 5 MB
128
+ .multiple() // string[] when on
129
+ .imageEditor() // crop / rotate before upload
130
+ ```
131
+
132
+ `FileUpload` requires an upload adapter wired at the panel level:
133
+
134
+ ```ts
135
+ import { localUpload } from '@pilotiq/pilotiq/uploads'
136
+
137
+ adminPanel.uploads({
138
+ adapter: localUpload({ root: 'public/uploads', urlPrefix: '/uploads' }),
139
+ })
140
+ ```
141
+
142
+ S3 / R2 / custom adapters implement the same `UploadAdapter` interface.
143
+
144
+ ## Array-of-rows (Repeater / Builder)
145
+
146
+ ```ts
147
+ Repeater.make('items') // uniform rows
148
+ .schema([
149
+ TextField.make('label').required(),
150
+ NumberField.make('qty').required(),
151
+ ])
152
+ .min(1)
153
+ .maxItems(10)
154
+ .reorderable()
155
+ .cloneable()
156
+ .collapsible()
157
+ .itemLabel(row => row.label || 'New item')
158
+
159
+ Builder.make('content') // heterogeneous rows
160
+ .blocks([
161
+ Block.make('heading').icon('heading').schema([
162
+ TextField.make('text').required(),
163
+ SelectField.make('level').options({ h1: 'H1', h2: 'H2', h3: 'H3' }),
164
+ ]),
165
+ Block.make('paragraph').icon('text').schema([
166
+ MarkdownField.make('body'),
167
+ ]),
168
+ Block.make('image').icon('image').schema([
169
+ FileUpload.make('src').accept('image/*').required(),
170
+ TextField.make('alt'),
171
+ ]),
172
+ ])
173
+ .reorderable()
174
+ .blockPickerColumns(2)
175
+ ```
176
+
177
+ For relation-backed rows (real `hasMany` / `morph*` / M2M children instead of JSON-blob storage), see the `pilotiq-relations` skill.
178
+
179
+ ## Hidden
180
+
181
+ ```ts
182
+ HiddenField.make('authorId') // always submitted, never rendered
183
+ .default(({ user }) => user.id)
184
+ ```
185
+
186
+ ## Common setters
187
+
188
+ Every field inherits these from `Field`:
189
+
190
+ ```ts
191
+ Field.make('name')
192
+ .label('Display label') // sr-only if empty
193
+ .helperText('Shown below the input')
194
+ .placeholder('e.g. Hello world')
195
+ .default('initial value') // or () => value, or (ctx) => value
196
+ .prefix('$') // or .prefix({ icon: 'dollar' })
197
+ .suffix('USD')
198
+ .required() // implicit required validator
199
+ .validate([Field.email(), Field.unique({ model: User })])
200
+ .visible(({ user }) => user.role === 'admin')
201
+ .hidden(rule)
202
+ .disabled(rule)
203
+ .columnSpan(2) // inside a Grid / Section.columns(n)
204
+ .live() // re-resolve schema on change
205
+ .afterStateUpdated((value, ctx) => ctx.$set('slug', slugify(value)))
206
+ .dehydrated(false) // exclude from POST body
207
+ .formatStateUsing(v => `${v} px`) // display transform (read paths)
208
+ .autofocus()
209
+ .hiddenLabel() // visually hidden, sr-only kept
210
+ .validationAttribute('email address') // tunes the implicit-required text
211
+ .extraAttributes({ 'data-cy': 'name' }) // outer wrapper attrs
212
+ .extraInputAttributes({ autocomplete: 'off' }) // <input> attrs
213
+ .disabledOn(['edit']) // page-mode sugar
214
+ .hiddenOn(['view'])
215
+ .visibleOn(['create', 'edit'])
216
+ .readonly() // disabled + non-submittable
217
+ ```
218
+
219
+ ## Operation-aware shortcuts
220
+
221
+ `disabledOn` / `hiddenOn` / `visibleOn` are sugar over `disabled(ctx => ctx.mode === 'edit')` / `hidden(ctx => ctx.mode === 'view')` / `visible(ctx => ['create', 'edit'].includes(ctx.mode))`.
222
+
223
+ They resolve against page mode (`'table' | 'create' | 'edit' | 'view'`) and no-op on custom Pages (mode is unset). `readonly()` wins over `disabledOn`.
224
+
225
+ ```ts
226
+ TextField.make('email')
227
+ .disabledOn(['edit']) // can set on create, locked on edit
228
+ .visibleOn(['create', 'edit']) // never on view
229
+ ```
230
+
231
+ ## Conditional visibility
232
+
233
+ `.visible(rule)` / `.hidden(rule)` / `.disabled(rule)` accept `boolean | (ctx: ConditionContext) => bool | Promise<bool>`:
234
+
235
+ ```ts
236
+ ConditionContext = {
237
+ record?: unknown // current record on edit/view
238
+ values?: Record<string, unknown> // form values (only when reactive)
239
+ user?: unknown // from Pilotiq.user()
240
+ mode?: 'create' | 'edit' | 'view'
241
+ }
242
+ ```
243
+
244
+ ```ts
245
+ TextField.make('publishUrl')
246
+ .visible(({ values }) => values?.status === 'published')
247
+
248
+ TextField.make('adminNotes')
249
+ .visible(({ user }) => user.role === 'admin')
250
+
251
+ TextField.make('signature')
252
+ .disabled(({ record }) => record?.locked === true)
253
+ ```
254
+
255
+ For visibility that depends on form values to change in real-time, ALSO add `.live()` to the source field — otherwise the dependent field only re-evaluates on submit.
256
+
257
+ ## Display-only transforms
258
+
259
+ `formatStateUsing(v => …)` runs on the read path (loadRecord → fill) to transform the value for display. It does NOT affect the submitted value:
260
+
261
+ ```ts
262
+ NumberField.make('priceInCents')
263
+ .formatStateUsing(v => (v / 100).toFixed(2))
264
+ // user sees "9.99"; column stores 999
265
+ ```
266
+
267
+ For two-way conversion, pair with an accessor / mutator on the underlying model (e.g. `Attribute.make({ get: c => c / 100, set: d => d * 100 })`).
268
+
269
+ ## Mass-assignment + `dehydrated`
270
+
271
+ Fields submit by default. `dehydrated(false)` excludes the field from the POST body:
272
+
273
+ ```ts
274
+ TextField.make('computedSlug')
275
+ .dehydrated(false)
276
+ .formatStateUsing(({ values }) => slugify(values?.title ?? ''))
277
+ ```
278
+
279
+ Useful for derived display, server-computed values, or admin-only debug toggles. The model never receives the field; coerce + validate skip it.
280
+
281
+ ## Common pitfalls
282
+
283
+ - **`SelectField.options(fn)` without `.live()` upstream** — when options depend on another field via `$get`, the source field must be `.live()` for the dependent options to re-fetch. Otherwise the options resolve once at form load.
284
+ - **`TagsInput` stores `string[]` via JSON-encoded hidden input** — if you read it directly with `parseFormBody`, decode the JSON. The framework's `coerceFormValues` already handles it.
285
+ - **`FileUpload` without an upload adapter** silently hides the drop zone via `RenderContext.hasUploadAdapter`. Wire `panel.uploads({ adapter })` to expose it.
286
+ - **`Repeater.simple(field)` is a different storage shape.** `Repeater.make().schema([TextField.make('value')])` stores `[{ value: 'a' }, { value: 'b' }]`. `Repeater.simple(TextField.make('value'))` stores `['a', 'b']` — flat array. The framework wraps/unwraps internally; the inner schema must be a single field.
287
+ - **`HiddenField` is still in the submitted body** — `dehydrated(false)` exists for the case where you want a value rendered but not submitted (typically for `formatStateUsing` display).
288
+ - **`columnSpan(n)` only works inside a layout that defines a column grid** (`Section.columns(n)` / `Grid.columns(n)`). Bare schema arrays don't grid.
@@ -0,0 +1,199 @@
1
+ # Reactive Fields
2
+
3
+ Forms are static by default — the schema resolves once on page load, the form renders, the user fills + submits. Reactive fields make schema resolution dynamic: fields can re-resolve on every keystroke (`live()`), and a field's value can imperatively update sibling fields via `afterStateUpdated`.
4
+
5
+ ## `Field.live()`
6
+
7
+ `live()` marks a field as a re-resolve trigger:
8
+
9
+ ```ts
10
+ TextField.make('title')
11
+ .live() // re-resolve on every change
12
+ .afterStateUpdated((title, ctx) => ctx.$set('slug', slugify(title)))
13
+
14
+ SelectField.make('country')
15
+ .options(countries)
16
+ .live()
17
+
18
+ SelectField.make('region') // dependent on country
19
+ .options(({ $get }) => {
20
+ const country = $get('country')
21
+ return country ? regionsFor(country) : {}
22
+ })
23
+ ```
24
+
25
+ When a `live()` field changes:
26
+
27
+ 1. Client POSTs `{ changed: 'title', values: {...} }` to `Form.stateUrl`.
28
+ 2. Server runs `applyStateUpdate()` — finds the changed field, runs its `afterStateUpdated` hook (which may `$set` siblings), then re-resolves the form with the updated values.
29
+ 3. Server returns the new `FormMeta` (with refreshed conditional visibility, options, helper text).
30
+ 4. Client diffs and re-renders.
31
+
32
+ Tune the trigger:
33
+
34
+ ```ts
35
+ TextField.make('search')
36
+ .live({ debounce: 300 }) // wait 300ms after last keystroke
37
+ .afterStateUpdated((q, ctx) => ctx.$set('results', searchFor(q)))
38
+
39
+ TextField.make('rawJson')
40
+ .live({ onBlur: true }) // only fire on blur, not per keystroke
41
+ ```
42
+
43
+ Bare `.live()` fires immediately on every change.
44
+
45
+ ## `afterStateUpdated`
46
+
47
+ The server-side hook that fires when a `live()` field changes:
48
+
49
+ ```ts
50
+ TextField.make('title')
51
+ .live()
52
+ .afterStateUpdated(async (value, ctx) => {
53
+ // value: the new value of THIS field
54
+ // ctx: { $get, $set, values, user, record?, basePath?, row? }
55
+
56
+ if (!ctx.$get('slug')) { // only auto-fill if blank
57
+ ctx.$set('slug', slugify(value))
58
+ }
59
+ })
60
+ ```
61
+
62
+ The `ctx` API:
63
+
64
+ - **`$get(name)`** — read another field's current value. Works for nested paths (`$get('contacts.0.email')`).
65
+ - **`$set(name, value)`** — write another field's value. The new value lands in the next `FormMeta` response.
66
+ - **`values`** — snapshot of all form values at this moment.
67
+ - **`row`** — when the hook fires inside a `Repeater` / `Builder` row, this is the row context `{ index, id, values, fieldName, blockType? }`. The plain `$get` / `$set` are scoped to the row — `$set('label', 'X')` writes `items.<row>.label`.
68
+
69
+ Resolve-time `$set` is a no-op closure during schema re-resolution; only the `afterStateUpdated` write survives. This prevents infinite re-resolve loops.
70
+
71
+ Throws fail-loudly — the server returns 500 and the client falls back to the previous form state.
72
+
73
+ ## Client-only reactivity: `afterStateUpdatedJs`
74
+
75
+ For trivial transformations (title → slug, sum of two fields), the server round-trip is overkill. `afterStateUpdatedJs` compiles a string body via `new Function` and runs it in the browser:
76
+
77
+ ```ts
78
+ TextField.make('title')
79
+ .afterStateUpdatedJs(`$set('slug', $state.title.toLowerCase().replace(/\\s+/g, '-'))`)
80
+
81
+ NumberField.make('subtotal')
82
+ .afterStateUpdatedJs(`$set('total', $state.subtotal * 1.0875)`)
83
+ ```
84
+
85
+ The body has these bindings:
86
+
87
+ - `$state` — the form's current values
88
+ - `$get(name)` / `$set(name, value)` — same shape as the server hook
89
+ - `$value` — the changed field's new value (sugar over `$state[$name]`)
90
+ - `$name` — the changed field's name
91
+
92
+ Compiled once per source-string (cached via identity in `react/fieldJsHandler.ts`). Runs synchronously on every change. No `live()` required — the JS fires regardless.
93
+
94
+ Compose with the server hook:
95
+
96
+ ```ts
97
+ TextField.make('title')
98
+ .live()
99
+ .afterStateUpdatedJs(`$set('slug', $value.toLowerCase().replace(/\\s+/g, '-'))`)
100
+ .afterStateUpdated(async (value, ctx) => {
101
+ // Server-side: validate the auto-slug is unique
102
+ const existing = await Article.where('slug', ctx.$get('slug')).count()
103
+ if (existing > 0) ctx.$set('slug', `${ctx.$get('slug')}-${Date.now()}`)
104
+ })
105
+ ```
106
+
107
+ JS runs first, server response overlays sibling values when it comes back.
108
+
109
+ Note: `afterStateUpdatedJs` requires CSP `unsafe-eval`. If your CSP is locked down, stick with the server hook.
110
+
111
+ ## Multi-form pages: pin `formId`
112
+
113
+ The auto-fall-back covers single-form pages. For pages with multiple forms (a record-page with both a "Settings" form and a "Notifications" form), you MUST pin `formId` explicitly:
114
+
115
+ ```ts
116
+ Form.make()
117
+ .formId('settings') // stable across re-renders
118
+ .schema([...])
119
+
120
+ Form.make()
121
+ .formId('notifications')
122
+ .schema([...])
123
+ ```
124
+
125
+ Without `formId`, the framework can't tell which form's state-update endpoint to POST to — live() silently fails. The auto-fallback uses the page slug; multi-form pages need distinct names.
126
+
127
+ The same applies to Repeaters / Builders that live inside live() forms — the form's `formId` is what disambiguates them.
128
+
129
+ ## `$get` inside `SelectField.options(fn)`
130
+
131
+ Dependent options are the most common reactive pattern:
132
+
133
+ ```ts
134
+ SelectField.make('country').options(countries).live()
135
+
136
+ SelectField.make('region')
137
+ .options(async ({ $get, user }) => {
138
+ const country = $get('country')
139
+ if (!country) return {}
140
+ return await regionsFor(country)
141
+ })
142
+ ```
143
+
144
+ The options resolver receives the same `ctx` as `afterStateUpdated`. It runs every re-resolve cycle (so always sees fresh `$get`). Without `live()` on the source, the dependent options resolve ONCE on form load and never update.
145
+
146
+ ## Conditional visibility based on live values
147
+
148
+ ```ts
149
+ SelectField.make('billingType')
150
+ .options({ none: 'No billing', card: 'Credit card', invoice: 'Net 30' })
151
+ .live()
152
+
153
+ TextField.make('cardNumber')
154
+ .visible(({ values }) => values?.billingType === 'card')
155
+
156
+ TextField.make('purchaseOrder')
157
+ .visible(({ values }) => values?.billingType === 'invoice')
158
+ ```
159
+
160
+ Both `visible(({ values }) => …)` rules see fresh `values` on every re-resolve. Without `.live()` on `billingType`, the dependent fields re-evaluate only on submit.
161
+
162
+ ## Reactive fields inside Repeater / Builder
163
+
164
+ Inside an array-row container, `$get` / `$set` accept dotted paths AND scope to the row by default:
165
+
166
+ ```ts
167
+ Repeater.make('items')
168
+ .schema([
169
+ SelectField.make('product').options(products).live(),
170
+ NumberField.make('quantity').default(1).live(),
171
+ NumberField.make('lineTotal')
172
+ .disabled()
173
+ .afterStateUpdatedJs(`
174
+ const product = $get('product')
175
+ const qty = Number($get('quantity'))
176
+ const price = product ? PRICES[product] : 0
177
+ $set('lineTotal', price * qty)
178
+ `),
179
+ ])
180
+
181
+ // Or cross-row read with dotted path:
182
+ NumberField.make('discount')
183
+ .afterStateUpdated((v, ctx) => {
184
+ const subtotal = (ctx.values.items ?? []).reduce((s, r) => s + (r.lineTotal ?? 0), 0)
185
+ ctx.$set('total', subtotal - v)
186
+ })
187
+ ```
188
+
189
+ Inside the row, `$get('product')` reads the row's `product`. Outside the Repeater, `$get('items.0.product')` reads the same value via dotted path.
190
+
191
+ ## Common pitfalls
192
+
193
+ - **`afterStateUpdated` without `.live()`** only fires on submit. The hook still exists but the partial-resolve endpoint never gets called.
194
+ - **Multi-form pages without `.formId('id')`** — live() silently no-ops because the framework can't route the partial-resolve POST. See `feedback_pilotiq_live_forms_pin_formid.md`.
195
+ - **Infinite loops via cross-`$set`** — if A's `afterStateUpdated` sets B, and B's `afterStateUpdated` sets A, the resolve-time `$set` no-op prevents the loop. But synchronous JS loops in `afterStateUpdatedJs` will hang the browser — write idempotent JS.
196
+ - **`$get` returning `undefined`** — fields not yet rendered or `dehydrated(false)` aren't in `values`. Guard with `?? defaultValue`.
197
+ - **`debounce` on dependent SelectFields** — the source's debounce delays the partial-resolve POST, which delays the dependent's options refresh. For "type to search" patterns, debounce the source 200-400ms.
198
+ - **`afterStateUpdatedJs` CSP** — if your app has a strict CSP without `unsafe-eval`, JS handlers throw at registration. Use the server hook instead.
199
+ - **Reading `values.items` outside a row context** — when reading the full Repeater array from a sibling field (e.g. computing `total` from all rows), use `ctx.values` (the form-wide snapshot), not `$get`. `$get('items')` returns the array; `$get('items.0.qty')` returns one row's field; both work.
@@ -0,0 +1,198 @@
1
+ # Validation
2
+
3
+ Validators are functions: `(value, ctx?) => string | null | Promise<string | null>`. Return a message string to fail; return `null` to pass. The framework runs them in declaration order, awaits async ones, and aggregates per-field errors before calling the form's `save` handler.
4
+
5
+ ## Built-in validators
6
+
7
+ ```ts
8
+ import { Field } from '@pilotiq/pilotiq'
9
+
10
+ TextField.make('email')
11
+ .validate([
12
+ Field.required(), // or just .required()
13
+ Field.email(),
14
+ Field.maxLength(255),
15
+ ])
16
+
17
+ TextField.make('password')
18
+ .password()
19
+ .validate([
20
+ Field.required(),
21
+ Field.minLength(8, 'At least 8 characters'),
22
+ Field.pattern(/[A-Z]/, 'Must contain an uppercase letter'),
23
+ Field.pattern(/[0-9]/, 'Must contain a digit'),
24
+ ])
25
+
26
+ NumberField.make('age')
27
+ .validate([
28
+ Field.min(18, 'Must be 18 or older'),
29
+ Field.max(120),
30
+ ])
31
+
32
+ TextField.make('slug')
33
+ .validate([
34
+ Field.required(),
35
+ Field.pattern(/^[a-z0-9-]+$/, 'Lowercase letters, numbers, hyphens only'),
36
+ ])
37
+ ```
38
+
39
+ The full set: `required(message?) / email(message?) / minLength(n, message?) / maxLength(n, message?) / min(n, message?) / max(n, message?) / pattern(regex, message)`.
40
+
41
+ `Field.required()` is auto-contributed when you call `.required()` directly — no need to add it twice.
42
+
43
+ ## Custom validators
44
+
45
+ A validator is just a function. Inline:
46
+
47
+ ```ts
48
+ TextField.make('username')
49
+ .validate(async (value, ctx) => {
50
+ if (typeof value !== 'string') return null
51
+ if (value.length < 3) return 'At least 3 characters'
52
+ if (value.startsWith('admin')) return 'Reserved prefix'
53
+ return null // pass
54
+ })
55
+ ```
56
+
57
+ The `ctx` shape:
58
+
59
+ ```ts
60
+ {
61
+ record?: unknown // current record on edit/view
62
+ values?: Record<string, unknown> // all form values
63
+ user?: unknown
64
+ mode?: 'create' | 'edit' | 'view'
65
+ basePath?: string
66
+ }
67
+ ```
68
+
69
+ Async is fine — the framework awaits each one.
70
+
71
+ ## `Field.unique()` async DB probe
72
+
73
+ The standard "this field must be unique across all records" check:
74
+
75
+ ```ts
76
+ import { Field } from '@pilotiq/pilotiq'
77
+
78
+ TextField.make('slug')
79
+ .validate(Field.unique({
80
+ model: Article,
81
+ column: 'slug', // optional, defaults to field name
82
+ ignoreRecord: true, // skip the row matching ctx.record[pk]
83
+ where: { status: 'published' }, // optional scope
84
+ caseInsensitive: true,
85
+ message: 'Slug is already in use', // optional custom message
86
+ }))
87
+ ```
88
+
89
+ How it works:
90
+
91
+ - Issues `M.query().where(column, value).paginate(1, 2)` — limit 2 to detect uniqueness without scanning the table.
92
+ - `ignoreRecord: true` (default `true`) skips the row matching `ctx.record[primaryKey]` so edit-no-change saves don't conflict.
93
+ - `caseInsensitive: true` switches to SQL `LIKE` with `%` / `_` / `\` escaped (SQLite + MySQL friendly; Postgres collation-dependent).
94
+ - `where: { status: 'published' }` adds AND-clauses to the lookup query (useful for soft scopes — "unique among published rows").
95
+ - Inside a `Repeater`, `unique()` probes the database but does NOT see unsaved sibling rows. Pair with `distinct()` for cross-row uniqueness within the form.
96
+
97
+ `Field.unique` accepts a `Model`-like object (anything with `.query()`). The Resource doesn't need to be using the same Model.
98
+
99
+ ## `Field.distinct()` cross-row uniqueness inside Repeater / Builder
100
+
101
+ Inside a `Repeater` or `Builder`, `unique()` only checks the database. To enforce that values are unique ACROSS rows in the form itself (before submit), use `distinct()`:
102
+
103
+ ```ts
104
+ Repeater.make('contacts')
105
+ .schema([
106
+ TextField.make('email')
107
+ .validate(Field.email())
108
+ .distinct(), // unique within this Repeater
109
+ TextField.make('label'),
110
+ ])
111
+ ```
112
+
113
+ Options:
114
+
115
+ ```ts
116
+ TextField.make('email')
117
+ .distinct({
118
+ caseInsensitive: true, // default false
119
+ ignoreNulls: true, // default true — skip empty rows
120
+ message: 'Email already used in another row',
121
+ })
122
+ ```
123
+
124
+ For `Builder`, distinctness is per-block-type — `heading.text="X"` never conflicts with `paragraph.text="X"`.
125
+
126
+ Pair with `unique({ model })` for in-form + cross-record uniqueness:
127
+
128
+ ```ts
129
+ TextField.make('slug')
130
+ .validate(Field.unique({ model: Article })) // unique in DB
131
+ .distinct() // unique in this form too
132
+ ```
133
+
134
+ ## Form-level validators
135
+
136
+ `Form.validate(fn)` runs after every field's validators have passed:
137
+
138
+ ```ts
139
+ form
140
+ .schema([
141
+ DateField.make('startsAt').required(),
142
+ DateField.make('endsAt').required(),
143
+ ])
144
+ .validate(({ values }) => {
145
+ if (values.endsAt < values.startsAt) {
146
+ return { endsAt: 'Must be after start date' }
147
+ }
148
+ return null
149
+ })
150
+ ```
151
+
152
+ Return shape:
153
+ - `null` (or `{}`) — pass
154
+ - `{ [fieldName]: 'message' }` — per-field errors (replaces any field-level errors)
155
+ - `{ _form: 'Top-level message' }` — form-wide error, rendered at the top of the form
156
+
157
+ Form-level validators run AFTER coercion (so values are typed) but BEFORE save.
158
+
159
+ ## Validation order
160
+
161
+ For each form submit, the framework runs:
162
+
163
+ 1. **Coerce raw FormData values** to typed values (string → number for NumberField, JSON-string → object for KeyValueField, etc.). Coerce errors aren't reported per-field; raw values fall through.
164
+ 2. **Field-level validators** — for every Field with `validate([...])` or `.required()`. Aggregated by field name.
165
+ 3. **Form-level `validate(fn)`** — if present, runs only when no field errors fired (early exit).
166
+ 4. **`save(ctx)` / model.create / model.update** — only reached when validation passes.
167
+
168
+ Important: **validate runs BEFORE coerce in `dispatchFormSubmit`**, then field-types do their own coerce-fold during their `runValidators`. The order is "validate the raw value, then coerce the validated value" — useful to remember when writing custom validators.
169
+
170
+ ## Errors on the wire
171
+
172
+ Field errors land on `FormMeta.errors` as `Record<fieldName, string[]>`. The renderer auto-stamps them under each field's input. Repeater/Builder errors key as `items.<i>.<name>` / `name.<i>.data.<child>` respectively; `min/maxItems` lands under the bare field name.
173
+
174
+ For form-level errors, the `_form` key is special — the renderer surfaces it as an Alert at the top of the form.
175
+
176
+ ## Throwing validators
177
+
178
+ A validator that throws is treated as a failure. The message comes from `err.message`:
179
+
180
+ ```ts
181
+ TextField.make('email')
182
+ .validate(async (value) => {
183
+ if (typeof value !== 'string') throw new Error('Email is required')
184
+ if (!value.includes('@')) return 'Invalid email'
185
+ return null
186
+ })
187
+ ```
188
+
189
+ For unique-across-DB or other async lookups, prefer returning `null` for "skip the check" rather than throwing — throws log + fail with the error message verbatim.
190
+
191
+ ## Common pitfalls
192
+
193
+ - **`required()` doesn't see `null` as empty** — by default `required()` checks for `value !== null && value !== undefined && value !== ''`. For domain-specific empty (`[]`, `{}`), write a custom validator.
194
+ - **`Field.unique()` without `ignoreRecord: true`** trips on edit-no-change saves — the row's own value conflicts with itself. Default is `true`; only set to `false` for "even my own row counts."
195
+ - **`distinct()` outside a Repeater/Builder** is a no-op. The framework only evaluates it inside an array-row container.
196
+ - **Async validators in parallel** — validators within ONE field run serially in declaration order; validators across DIFFERENT fields run in parallel. Don't rely on cross-field ordering.
197
+ - **`pattern(/regex/)` without anchors** matches anywhere in the string. Add `^…$` if you mean "the whole value."
198
+ - **Validation result `{}`** (empty object) means "pass" — the framework treats no-keys as no-errors. Return `null` for clarity.