@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.
- package/CHANGELOG.md +33 -0
- package/boost/guidelines.md +566 -0
- package/boost/skills/pilotiq-fields/SKILL.md +47 -0
- package/boost/skills/pilotiq-fields/rules/field-catalog.md +288 -0
- package/boost/skills/pilotiq-fields/rules/reactive-fields.md +199 -0
- package/boost/skills/pilotiq-fields/rules/validation.md +198 -0
- package/boost/skills/pilotiq-relations/SKILL.md +47 -0
- package/boost/skills/pilotiq-relations/rules/relation-managers.md +256 -0
- package/boost/skills/pilotiq-relations/rules/repeater-relationship.md +177 -0
- package/boost/skills/pilotiq-resource/SKILL.md +61 -0
- package/boost/skills/pilotiq-resource/rules/authorization.md +242 -0
- package/boost/skills/pilotiq-resource/rules/defining-resources.md +228 -0
- package/boost/skills/pilotiq-resource/rules/page-overrides.md +296 -0
- package/package.json +6 -1
- package/.turbo/turbo-build.log +0 -8
- package/CLAUDE.md +0 -265
- package/src/Cluster.test.ts +0 -283
- package/src/Cluster.ts +0 -83
- package/src/Column.test.ts +0 -199
- package/src/Column.ts +0 -710
- package/src/Global.test.ts +0 -367
- package/src/Global.ts +0 -169
- package/src/Page.test.ts +0 -114
- package/src/Page.ts +0 -208
- package/src/Pilotiq.perf.test.ts +0 -252
- package/src/Pilotiq.test.ts +0 -129
- package/src/Pilotiq.ts +0 -1158
- package/src/PilotiqRegistry.ts +0 -36
- package/src/PilotiqServiceProvider.ts +0 -121
- package/src/RelationManager.test.ts +0 -400
- package/src/RelationManager.ts +0 -527
- package/src/RenderHook.test.ts +0 -252
- package/src/RenderHook.ts +0 -242
- package/src/Resource.test.ts +0 -284
- package/src/Resource.ts +0 -526
- package/src/RightPanel.test.ts +0 -202
- package/src/RightPanel.ts +0 -132
- package/src/Tab.test.ts +0 -91
- package/src/Tab.ts +0 -156
- package/src/UserMenuItem.ts +0 -145
- package/src/actions/Action.test.ts +0 -2526
- package/src/actions/Action.ts +0 -1515
- package/src/actions/ActionGroup.test.ts +0 -112
- package/src/actions/ActionGroup.ts +0 -173
- package/src/actions/attachFactory.ts +0 -172
- package/src/actions/bulkFactories.ts +0 -168
- package/src/actions/crudFactories.ts +0 -220
- package/src/actions/exportFactory.ts +0 -225
- package/src/actions/factoryHelpers.ts +0 -177
- package/src/actions/importFactory.ts +0 -243
- package/src/actions/index.ts +0 -17
- package/src/actions/m2mFactories.ts +0 -193
- package/src/actions/relationFactories.ts +0 -372
- package/src/applyPageHooks.test.ts +0 -463
- package/src/applyPageHooks.ts +0 -330
- package/src/authorization.test.ts +0 -483
- package/src/breadcrumbs.test.ts +0 -238
- package/src/cells/coerce.test.ts +0 -85
- package/src/cells/coerce.ts +0 -84
- package/src/clusterPaths.ts +0 -35
- package/src/columns/BadgeColumn.test.ts +0 -54
- package/src/columns/BadgeColumn.ts +0 -32
- package/src/columns/BooleanColumn.test.ts +0 -41
- package/src/columns/BooleanColumn.ts +0 -18
- package/src/columns/ColorColumn.test.ts +0 -37
- package/src/columns/ColorColumn.ts +0 -38
- package/src/columns/IconColumn.test.ts +0 -54
- package/src/columns/IconColumn.ts +0 -37
- package/src/columns/ImageColumn.test.ts +0 -41
- package/src/columns/ImageColumn.ts +0 -28
- package/src/columns/SelectColumn.ts +0 -98
- package/src/columns/TextColumn.test.ts +0 -190
- package/src/columns/TextColumn.ts +0 -20
- package/src/columns/TextInputColumn.ts +0 -68
- package/src/columns/ToggleColumn.ts +0 -46
- package/src/columns/editableColumns.test.ts +0 -238
- package/src/columns/index.ts +0 -9
- package/src/defaultGlobalPages.ts +0 -95
- package/src/defaultPages.test.ts +0 -634
- package/src/defaultPages.ts +0 -617
- package/src/defaultViewPage.test.ts +0 -147
- package/src/elements/Form.test.ts +0 -223
- package/src/elements/Form.ts +0 -416
- package/src/elements/ListTabs.ts +0 -28
- package/src/elements/Table.test.ts +0 -422
- package/src/elements/Table.ts +0 -850
- package/src/elements/TableGroup.test.ts +0 -260
- package/src/elements/TableGroup.ts +0 -334
- package/src/elements/dispatchAction.test.ts +0 -463
- package/src/elements/dispatchAction.ts +0 -355
- package/src/elements/dispatchForm.test.ts +0 -477
- package/src/elements/dispatchForm.ts +0 -1993
- package/src/elements/dispatchTable.test.ts +0 -1514
- package/src/elements/dispatchTable.ts +0 -745
- package/src/elements/index.ts +0 -21
- package/src/entries/BadgeEntry.ts +0 -39
- package/src/entries/CodeEntry.test.ts +0 -40
- package/src/entries/CodeEntry.ts +0 -52
- package/src/entries/ColorEntry.ts +0 -63
- package/src/entries/ComponentEntry.test.ts +0 -173
- package/src/entries/ComponentEntry.ts +0 -95
- package/src/entries/Entry.ts +0 -304
- package/src/entries/IconEntry.ts +0 -49
- package/src/entries/ImageEntry.ts +0 -61
- package/src/entries/KeyValueEntry.ts +0 -47
- package/src/entries/RepeatableEntry.test.ts +0 -239
- package/src/entries/RepeatableEntry.ts +0 -173
- package/src/entries/TextEntry.test.ts +0 -394
- package/src/entries/TextEntry.ts +0 -60
- package/src/entries/index.ts +0 -12
- package/src/entries/leaves.test.ts +0 -306
- package/src/entries/registry.ts +0 -54
- package/src/fields/BuilderField.test.ts +0 -1188
- package/src/fields/BuilderField.ts +0 -605
- package/src/fields/BuilderRelationship.test.ts +0 -811
- package/src/fields/CheckboxField.test.ts +0 -44
- package/src/fields/CheckboxField.ts +0 -27
- package/src/fields/CheckboxListField.test.ts +0 -99
- package/src/fields/CheckboxListField.ts +0 -66
- package/src/fields/ColorPickerField.test.ts +0 -33
- package/src/fields/ColorPickerField.ts +0 -25
- package/src/fields/DateField.ts +0 -54
- package/src/fields/DateTimeField.test.ts +0 -55
- package/src/fields/EmailField.ts +0 -16
- package/src/fields/Field.test.ts +0 -654
- package/src/fields/Field.ts +0 -817
- package/src/fields/FileUploadField.test.ts +0 -143
- package/src/fields/FileUploadField.ts +0 -159
- package/src/fields/HiddenField.test.ts +0 -27
- package/src/fields/HiddenField.ts +0 -28
- package/src/fields/KeyValueField.test.ts +0 -105
- package/src/fields/KeyValueField.ts +0 -55
- package/src/fields/MarkdownField.test.ts +0 -167
- package/src/fields/MarkdownField.ts +0 -162
- package/src/fields/NumberField.ts +0 -33
- package/src/fields/RadioField.test.ts +0 -94
- package/src/fields/RadioField.ts +0 -67
- package/src/fields/RepeaterField.test.ts +0 -1806
- package/src/fields/RepeaterField.ts +0 -939
- package/src/fields/RepeaterRelationship.test.ts +0 -1923
- package/src/fields/RepeaterSimple.test.ts +0 -248
- package/src/fields/RowButton.test.ts +0 -219
- package/src/fields/RowButton.ts +0 -135
- package/src/fields/SelectField.test.ts +0 -192
- package/src/fields/SelectField.ts +0 -235
- package/src/fields/SliderField.test.ts +0 -50
- package/src/fields/SliderField.ts +0 -53
- package/src/fields/SlugField.ts +0 -24
- package/src/fields/TagsInputField.test.ts +0 -154
- package/src/fields/TagsInputField.ts +0 -133
- package/src/fields/TextField.test.ts +0 -213
- package/src/fields/TextField.ts +0 -177
- package/src/fields/TextareaField.test.ts +0 -58
- package/src/fields/TextareaField.ts +0 -59
- package/src/fields/ToggleButtonsField.test.ts +0 -106
- package/src/fields/ToggleButtonsField.ts +0 -59
- package/src/fields/ToggleField.ts +0 -16
- package/src/fields/disableOptionsWhenSelectedInSiblingRepeaterItems.test.ts +0 -319
- package/src/fields/optionsResolver.ts +0 -95
- package/src/fields/resolveField.ts +0 -28
- package/src/filters/BooleanFilter.ts +0 -35
- package/src/filters/DateRangeFilter.test.ts +0 -194
- package/src/filters/DateRangeFilter.ts +0 -148
- package/src/filters/Filter.test.ts +0 -268
- package/src/filters/Filter.ts +0 -184
- package/src/filters/FormFilter.test.ts +0 -238
- package/src/filters/FormFilter.ts +0 -215
- package/src/filters/MultiSelectFilter.test.ts +0 -119
- package/src/filters/MultiSelectFilter.ts +0 -78
- package/src/filters/QueryBuilderFilter.test.ts +0 -662
- package/src/filters/QueryBuilderFilter.ts +0 -398
- package/src/filters/SelectFilter.ts +0 -46
- package/src/filters/TernaryFilter.test.ts +0 -160
- package/src/filters/TernaryFilter.ts +0 -72
- package/src/filters/TrashedFilter.test.ts +0 -149
- package/src/filters/TrashedFilter.ts +0 -55
- package/src/filters/queryBuilder/BooleanConstraint.ts +0 -31
- package/src/filters/queryBuilder/Constraint.ts +0 -115
- package/src/filters/queryBuilder/DateConstraint.ts +0 -69
- package/src/filters/queryBuilder/NumberConstraint.ts +0 -66
- package/src/filters/queryBuilder/SelectConstraint.ts +0 -72
- package/src/filters/queryBuilder/TextConstraint.ts +0 -64
- package/src/filters/queryBuilder/index.ts +0 -12
- package/src/icons/index.ts +0 -2
- package/src/icons/lucide.ts +0 -204
- package/src/icons/registry.test.ts +0 -56
- package/src/icons/registry.ts +0 -41
- package/src/icons/types.ts +0 -47
- package/src/index.ts +0 -525
- package/src/io/csv.test.ts +0 -142
- package/src/io/csv.ts +0 -170
- package/src/nestedRelationManagerData.test.ts +0 -547
- package/src/notifications/Notification.test.ts +0 -210
- package/src/notifications/Notification.ts +0 -354
- package/src/notifications/broadcast.test.ts +0 -110
- package/src/notifications/broadcast.ts +0 -95
- package/src/notifications/database.test.ts +0 -383
- package/src/notifications/database.ts +0 -398
- package/src/notifications/databaseNotifications.test.ts +0 -187
- package/src/notifications/dispatchNotificationAction.test.ts +0 -341
- package/src/notifications/dispatchNotificationAction.ts +0 -142
- package/src/notifications/flash.test.ts +0 -89
- package/src/notifications/flash.ts +0 -71
- package/src/notifications/index.ts +0 -45
- package/src/notifications/registerBroadcastAuth.test.ts +0 -134
- package/src/notifications/registerBroadcastAuth.ts +0 -100
- package/src/notifications/resolveSavedNotification.test.ts +0 -82
- package/src/notifications/resolveSavedNotification.ts +0 -59
- package/src/notifications/types.ts +0 -93
- package/src/orm/m2mAccessor.ts +0 -66
- package/src/orm/modelDefaults.test.ts +0 -633
- package/src/orm/modelDefaults.ts +0 -666
- package/src/pageData/breadcrumbs.ts +0 -288
- package/src/pageData/forms.ts +0 -578
- package/src/pageData/helpers.ts +0 -857
- package/src/pageData/misc.ts +0 -347
- package/src/pageData/navigation.ts +0 -842
- package/src/pageData/relationPages.ts +0 -1248
- package/src/pageData/relationTabs.ts +0 -286
- package/src/pageData/resourcePages.ts +0 -609
- package/src/pageData.test.ts +0 -1545
- package/src/pageData.ts +0 -341
- package/src/plugins/index.ts +0 -8
- package/src/plugins/themeEditor.test.ts +0 -36
- package/src/plugins/themeEditor.ts +0 -45
- package/src/react/AppShell.tsx +0 -251
- package/src/react/CollabExtensionFactoryRegistry.ts +0 -55
- package/src/react/CollabRoomContext.ts +0 -98
- package/src/react/CollabTextRendererRegistry.ts +0 -102
- package/src/react/CommandPalette.tsx +0 -375
- package/src/react/CurrentUserContext.tsx +0 -50
- package/src/react/CustomPageWrapperGate.tsx +0 -69
- package/src/react/CustomPageWrapperRegistry.ts +0 -45
- package/src/react/FieldFocusReporterRegistry.ts +0 -37
- package/src/react/FieldLabelSlotRegistry.ts +0 -30
- package/src/react/FieldPresenceRegistry.ts +0 -46
- package/src/react/FormCollabBindingRegistry.ts +0 -242
- package/src/react/FormStateContext.tsx +0 -591
- package/src/react/HeadHooks.tsx +0 -126
- package/src/react/MarkdownEditorRegistry.test.ts +0 -38
- package/src/react/MarkdownEditorRegistry.ts +0 -107
- package/src/react/NotificationActionStrip.tsx +0 -263
- package/src/react/NotificationBell.tsx +0 -426
- package/src/react/PendingSuggestionApplierRegistry.test.ts +0 -97
- package/src/react/PendingSuggestionApplierRegistry.ts +0 -98
- package/src/react/PendingSuggestionOverlayRegistry.ts +0 -54
- package/src/react/PendingSuggestionsContext.tsx +0 -172
- package/src/react/RecordWrapperGate.tsx +0 -58
- package/src/react/RecordWrapperRegistry.ts +0 -39
- package/src/react/RenderHookSlot.tsx +0 -32
- package/src/react/RightSidebar.tsx +0 -257
- package/src/react/RightSidebarContext.tsx +0 -234
- package/src/react/RightSidebarTrigger.tsx +0 -53
- package/src/react/RowCoordsContext.tsx +0 -23
- package/src/react/SchemaRenderer.tsx +0 -549
- package/src/react/SearchTrigger.tsx +0 -46
- package/src/react/ThemeProvider.tsx +0 -93
- package/src/react/ThemeSettingsPage.tsx +0 -579
- package/src/react/ThemeToggle.tsx +0 -20
- package/src/react/Toaster.tsx +0 -158
- package/src/react/UserMenu.tsx +0 -196
- package/src/react/WidgetDataContext.tsx +0 -157
- package/src/react/cells/EditableCell.tsx +0 -389
- package/src/react/component-slots.test.ts +0 -103
- package/src/react/component-slots.ts +0 -116
- package/src/react/fieldJsHandler.test.ts +0 -166
- package/src/react/fieldJsHandler.ts +0 -79
- package/src/react/fields/BuilderInput.tsx +0 -1078
- package/src/react/fields/CheckboxInput.tsx +0 -39
- package/src/react/fields/CheckboxListInput.tsx +0 -102
- package/src/react/fields/ColorInput.tsx +0 -71
- package/src/react/fields/DateFieldInput.tsx +0 -70
- package/src/react/fields/DateTimeInput.tsx +0 -62
- package/src/react/fields/FieldShell.tsx +0 -348
- package/src/react/fields/FileUploadInput.tsx +0 -639
- package/src/react/fields/HiddenInput.tsx +0 -17
- package/src/react/fields/KeyValueInput.tsx +0 -230
- package/src/react/fields/MarkdownInput.tsx +0 -560
- package/src/react/fields/RadioInput.tsx +0 -81
- package/src/react/fields/RepeaterInput.test.ts +0 -116
- package/src/react/fields/RepeaterInput.tsx +0 -1420
- package/src/react/fields/SelectFieldInput.tsx +0 -280
- package/src/react/fields/SliderInput.tsx +0 -81
- package/src/react/fields/TagsInput.tsx +0 -283
- package/src/react/fields/TextLikeInput.tsx +0 -256
- package/src/react/fields/ToggleButtonsInput.tsx +0 -60
- package/src/react/fields/ToggleFieldInput.tsx +0 -56
- package/src/react/fields/relationshipRenameDispatch.test.ts +0 -106
- package/src/react/fields/relationshipRenameDispatch.ts +0 -97
- package/src/react/fields/repeaterReconcile.test.ts +0 -114
- package/src/react/fields/repeaterReconcile.ts +0 -104
- package/src/react/fields/rowChromeButton.tsx +0 -336
- package/src/react/fields/rowState.ts +0 -106
- package/src/react/fields/syncRowGates.test.ts +0 -202
- package/src/react/fields/syncRowGates.ts +0 -66
- package/src/react/fields/textInputControls.tsx +0 -238
- package/src/react/fields/useRowReorderDnd.ts +0 -78
- package/src/react/formStateHelpers.test.ts +0 -508
- package/src/react/formStateHelpers.ts +0 -381
- package/src/react/hooks/use-mobile.ts +0 -19
- package/src/react/icon-context.tsx +0 -60
- package/src/react/index.ts +0 -194
- package/src/react/layouts/SidebarLayout.tsx +0 -250
- package/src/react/layouts/TopbarLayout.tsx +0 -258
- package/src/react/navigate.tsx +0 -37
- package/src/react/onProviderSynced.test.ts +0 -90
- package/src/react/parseRecordEditUrl.test.ts +0 -122
- package/src/react/parseRecordEditUrl.ts +0 -94
- package/src/react/persistedState.ts +0 -40
- package/src/react/registry.ts +0 -48
- package/src/react/right-panel-registry.tsx +0 -47
- package/src/react/schemaRenderer/AlertRenderer.tsx +0 -112
- package/src/react/schemaRenderer/EntryRenderer.tsx +0 -501
- package/src/react/schemaRenderer/SectionRenderer.tsx +0 -120
- package/src/react/schemaRenderer/SimpleElements.tsx +0 -306
- package/src/react/schemaRenderer/TabsRenderer.tsx +0 -62
- package/src/react/schemaRenderer/WizardRenderer.tsx +0 -338
- package/src/react/schemaRenderer/action/ActionGroupTrigger.tsx +0 -177
- package/src/react/schemaRenderer/action/ActionModalDialog.tsx +0 -273
- package/src/react/schemaRenderer/action/ConfirmActionDialog.tsx +0 -61
- package/src/react/schemaRenderer/action/HandlerActionButton.tsx +0 -43
- package/src/react/schemaRenderer/action/MethodActionButton.tsx +0 -64
- package/src/react/schemaRenderer/action/buttons.tsx +0 -99
- package/src/react/schemaRenderer/action/helpers.ts +0 -140
- package/src/react/schemaRenderer/action/renderAction.tsx +0 -245
- package/src/react/schemaRenderer/columnFormat.ts +0 -65
- package/src/react/schemaRenderer/constants.ts +0 -50
- package/src/react/schemaRenderer/form/FormRenderer.tsx +0 -274
- package/src/react/schemaRenderer/form/renderField.tsx +0 -511
- package/src/react/schemaRenderer/helpers.tsx +0 -81
- package/src/react/schemaRenderer/table/CardsLayoutBody.tsx +0 -308
- package/src/react/schemaRenderer/table/TableRenderer.tsx +0 -123
- package/src/react/schemaRenderer/table/TableRendererBody.tsx +0 -974
- package/src/react/schemaRenderer/table/filters.tsx +0 -1233
- package/src/react/schemaRenderer/table/formatCell.tsx +0 -264
- package/src/react/schemaRenderer/table/links.tsx +0 -112
- package/src/react/schemaRenderer/table/renderRowActions.tsx +0 -52
- package/src/react/schemaRenderer/table/url.tsx +0 -143
- package/src/react/theme-preview/apply.ts +0 -99
- package/src/react/theme-preview/build-html.ts +0 -436
- package/src/react/ui/button.tsx +0 -51
- package/src/react/ui/calendar.tsx +0 -67
- package/src/react/ui/checkbox.tsx +0 -29
- package/src/react/ui/dialog.tsx +0 -108
- package/src/react/ui/dropdown-menu.tsx +0 -97
- package/src/react/ui/input.tsx +0 -20
- package/src/react/ui/label.tsx +0 -21
- package/src/react/ui/popover.tsx +0 -50
- package/src/react/ui/select.tsx +0 -169
- package/src/react/ui/separator.tsx +0 -25
- package/src/react/ui/sheet.tsx +0 -136
- package/src/react/ui/sidebar.tsx +0 -723
- package/src/react/ui/skeleton.tsx +0 -13
- package/src/react/ui/slider.tsx +0 -34
- package/src/react/ui/switch.tsx +0 -28
- package/src/react/ui/table.tsx +0 -105
- package/src/react/ui/tabs.tsx +0 -63
- package/src/react/ui/textarea.tsx +0 -18
- package/src/react/ui/tooltip.tsx +0 -64
- package/src/react/useResizableWidth.ts +0 -139
- package/src/react/utils.ts +0 -6
- package/src/react/widgetRegistry.test.ts +0 -43
- package/src/react/widgetRegistry.ts +0 -50
- package/src/react/widgets/StatsOverviewRenderer.tsx +0 -232
- package/src/react/widgets/TableWidgetRenderer.tsx +0 -231
- package/src/react/widgets/ViewRenderer.tsx +0 -71
- package/src/relationManagerData.test.ts +0 -1595
- package/src/richtext/index.ts +0 -8
- package/src/richtext/registry.ts +0 -89
- package/src/routes/globals.ts +0 -148
- package/src/routes/guard.test.ts +0 -325
- package/src/routes/helpers.ts +0 -704
- package/src/routes/pages.ts +0 -175
- package/src/routes/panel.ts +0 -204
- package/src/routes/relations.ts +0 -1243
- package/src/routes/resources.ts +0 -781
- package/src/routes/theme.ts +0 -91
- package/src/routes-nested-relations.test.ts +0 -676
- package/src/routes-relations.test.ts +0 -972
- package/src/routes.test.ts +0 -2027
- package/src/routes.ts +0 -303
- package/src/schema/Alert.test.ts +0 -109
- package/src/schema/Alert.ts +0 -131
- package/src/schema/Block.ts +0 -169
- package/src/schema/Breadcrumbs.ts +0 -40
- package/src/schema/Card.ts +0 -35
- package/src/schema/Divider.ts +0 -20
- package/src/schema/Element.ts +0 -219
- package/src/schema/EmptyState.test.ts +0 -37
- package/src/schema/EmptyState.ts +0 -63
- package/src/schema/Fieldset.ts +0 -43
- package/src/schema/Grid.ts +0 -43
- package/src/schema/Group.ts +0 -30
- package/src/schema/Heading.ts +0 -39
- package/src/schema/Html.ts +0 -67
- package/src/schema/Icon.ts +0 -54
- package/src/schema/Image.ts +0 -57
- package/src/schema/LinkTag.ts +0 -41
- package/src/schema/Markdown.ts +0 -85
- package/src/schema/MetaTag.ts +0 -41
- package/src/schema/RelationTabs.ts +0 -71
- package/src/schema/ScriptTag.ts +0 -55
- package/src/schema/Section.ts +0 -160
- package/src/schema/ServerDataElement.test.ts +0 -140
- package/src/schema/ServerDataElement.ts +0 -156
- package/src/schema/SlotComponent.test.ts +0 -77
- package/src/schema/SlotComponent.ts +0 -71
- package/src/schema/Split.ts +0 -50
- package/src/schema/Stat.test.ts +0 -118
- package/src/schema/Stat.ts +0 -154
- package/src/schema/StatsOverview.test.ts +0 -141
- package/src/schema/StatsOverview.ts +0 -119
- package/src/schema/StyleTag.ts +0 -35
- package/src/schema/TableWidget.test.ts +0 -297
- package/src/schema/TableWidget.ts +0 -289
- package/src/schema/Tabs.ts +0 -79
- package/src/schema/Text.ts +0 -58
- package/src/schema/UnorderedList.ts +0 -49
- package/src/schema/View.test.ts +0 -111
- package/src/schema/View.ts +0 -127
- package/src/schema/Wizard.ts +0 -220
- package/src/schema/containers.test.ts +0 -564
- package/src/schema/headTags.test.ts +0 -134
- package/src/schema/index.ts +0 -40
- package/src/schema/primes.test.ts +0 -269
- package/src/schema/resolveSchema.test.ts +0 -379
- package/src/schema/resolveSchema.ts +0 -917
- package/src/schema/sanitize.ts +0 -58
- package/src/search.test.ts +0 -446
- package/src/search.ts +0 -178
- package/src/sessionFilters.test.ts +0 -375
- package/src/sessionFilters.ts +0 -143
- package/src/slot-components/index.ts +0 -10
- package/src/slot-components/registry.ts +0 -56
- package/src/styles/file-upload.css +0 -13
- package/src/summarizers/Summarizer.test.ts +0 -84
- package/src/summarizers/Summarizer.ts +0 -123
- package/src/summarizers/index.ts +0 -11
- package/src/theme/base-colors.ts +0 -68
- package/src/theme/chart-colors.ts +0 -50
- package/src/theme/colors.ts +0 -447
- package/src/theme/generate-css.test.ts +0 -139
- package/src/theme/generate-css.ts +0 -44
- package/src/theme/generate-scale.test.ts +0 -106
- package/src/theme/generate-scale.ts +0 -97
- package/src/theme/icon-map.ts +0 -42
- package/src/theme/index.ts +0 -34
- package/src/theme/migrate.test.ts +0 -178
- package/src/theme/migrate.ts +0 -81
- package/src/theme/presets.ts +0 -135
- package/src/theme/radius.ts +0 -18
- package/src/theme/resolve.test.ts +0 -238
- package/src/theme/resolve.ts +0 -96
- package/src/theme/spacing.ts +0 -18
- package/src/theme/storage.test.ts +0 -126
- package/src/theme/storage.ts +0 -106
- package/src/theme/theme-colors.ts +0 -88
- package/src/theme/types.ts +0 -125
- package/src/uploads/UploadAdapter.ts +0 -35
- package/src/uploads/index.ts +0 -2
- package/src/uploads/localUpload.test.ts +0 -70
- package/src/uploads/localUpload.ts +0 -84
- package/src/validation/Validator.ts +0 -49
- package/src/validation/index.ts +0 -28
- package/src/validation/rules.ts +0 -78
- package/src/validation/runValidators.ts +0 -435
- package/src/validation/uniqueValidator.test.ts +0 -196
- package/src/validation/uniqueValidator.ts +0 -133
- package/src/validation/validators.test.ts +0 -268
- package/src/vite.test.ts +0 -184
- package/src/vite.ts +0 -787
- package/src/widgets/index.ts +0 -10
- package/src/widgets/registry.ts +0 -45
- package/src/widgets.test.ts +0 -592
- package/tsconfig.build.json +0 -11
- package/tsconfig.json +0 -4
- package/tsconfig.test.json +0 -10
- package/views/react/Dashboard.tsx +0 -27
- package/views/react/Resources/Form.tsx +0 -102
- package/views/react/Resources/Index.tsx +0 -49
|
@@ -1,1078 +0,0 @@
|
|
|
1
|
-
import React, { useContext, useEffect, useId, useMemo, useRef, useState } from 'react'
|
|
2
|
-
import { ChevronDownIcon, PlusIcon } from 'lucide-react'
|
|
3
|
-
import type { ElementMeta } from '../../schema/Element.js'
|
|
4
|
-
import { Button } from '../ui/button.js'
|
|
5
|
-
import { SchemaRenderer } from '../SchemaRenderer.js'
|
|
6
|
-
import { FormIdContext, useFormState, useRowBinding } from '../FormStateContext.js'
|
|
7
|
-
import { findFieldMeta } from '../formStateHelpers.js'
|
|
8
|
-
import { RowCoordsContext } from '../RowCoordsContext.js'
|
|
9
|
-
import { useIconFor } from '../icon-context.js'
|
|
10
|
-
import { reorderRows, ExtraActionStrip, buildGridContainer } from './RepeaterInput.js'
|
|
11
|
-
import { syncRowGates } from './syncRowGates.js'
|
|
12
|
-
import { consumeReconcileFlag, computeReconcilePlan } from './repeaterReconcile.js'
|
|
13
|
-
import type { RowButtonsMeta } from '../../fields/RowButton.js'
|
|
14
|
-
import {
|
|
15
|
-
RowChromeIconButton,
|
|
16
|
-
ReorderGrip,
|
|
17
|
-
CollapseChevron,
|
|
18
|
-
BulkCollapseHeader,
|
|
19
|
-
resolveRowChrome,
|
|
20
|
-
DEFAULT_MOVE_UP,
|
|
21
|
-
DEFAULT_MOVE_DOWN,
|
|
22
|
-
DEFAULT_CLONE,
|
|
23
|
-
DEFAULT_DELETE,
|
|
24
|
-
} from './rowChromeButton.js'
|
|
25
|
-
import {
|
|
26
|
-
generateRowId, makeAccordionStorage, makeCollapsedStorage,
|
|
27
|
-
} from './rowState.js'
|
|
28
|
-
import { useRowReorderDnd } from './useRowReorderDnd.js'
|
|
29
|
-
|
|
30
|
-
const collapsedStorage = makeCollapsedStorage('builder')
|
|
31
|
-
const accordionStorage = makeAccordionStorage('builder')
|
|
32
|
-
const initSeedCollapsed = collapsedStorage.seed
|
|
33
|
-
const writeCollapsedToStorage = collapsedStorage.write
|
|
34
|
-
const deleteCollapsedFromStorage = collapsedStorage.remove
|
|
35
|
-
const readAccordionFromStorage = accordionStorage.read
|
|
36
|
-
const writeAccordionToStorage = accordionStorage.write
|
|
37
|
-
|
|
38
|
-
interface BlockShape {
|
|
39
|
-
name: string
|
|
40
|
-
label: string
|
|
41
|
-
icon?: string
|
|
42
|
-
columns?: number
|
|
43
|
-
maxItems?: number
|
|
44
|
-
template: ElementMeta[]
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface BuilderRowShape {
|
|
48
|
-
id: string
|
|
49
|
-
type: string
|
|
50
|
-
children: ElementMeta[]
|
|
51
|
-
itemLabel?: string
|
|
52
|
-
hidden?: boolean
|
|
53
|
-
unknownType?: boolean
|
|
54
|
-
extraActions?: ElementMeta[]
|
|
55
|
-
canDelete?: false
|
|
56
|
-
canClone?: false
|
|
57
|
-
canReorder?: false
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
interface BuilderMetaShape {
|
|
61
|
-
rows?: BuilderRowShape[]
|
|
62
|
-
blocks?: BlockShape[]
|
|
63
|
-
minItems?: number
|
|
64
|
-
maxItems?: number
|
|
65
|
-
reorderable?: boolean
|
|
66
|
-
reorderableWithButtons?: boolean
|
|
67
|
-
collapsible?: boolean
|
|
68
|
-
defaultCollapsed?: boolean
|
|
69
|
-
accordion?: boolean
|
|
70
|
-
cloneable?: boolean
|
|
71
|
-
addable?: boolean
|
|
72
|
-
deletable?: boolean
|
|
73
|
-
addBetween?: boolean
|
|
74
|
-
blockNumbers?: boolean
|
|
75
|
-
itemNumbers?: boolean
|
|
76
|
-
blockIcons?: boolean
|
|
77
|
-
blockPickerColumns?: number
|
|
78
|
-
addActionLabel?: string
|
|
79
|
-
addActionAlignment?: 'start' | 'center' | 'end'
|
|
80
|
-
defaultBlock?: string
|
|
81
|
-
grid?: number
|
|
82
|
-
buttons?: RowButtonsMeta
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
interface RowState {
|
|
86
|
-
id: string
|
|
87
|
-
type: string
|
|
88
|
-
children: ElementMeta[]
|
|
89
|
-
itemLabel?: string
|
|
90
|
-
hidden?: boolean
|
|
91
|
-
unknownType?: boolean
|
|
92
|
-
extraActions?: ElementMeta[]
|
|
93
|
-
// Per-row capability flags from `itemCan*(rule)`. Stamped only when the
|
|
94
|
-
// rule resolved falsy server-side. See `RepeaterField`'s RowState for
|
|
95
|
-
// the full contract — semantics are identical.
|
|
96
|
-
canDelete?: false
|
|
97
|
-
canClone?: false
|
|
98
|
-
canReorder?: false
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Builder renderer (Plan #14 follow-up).
|
|
103
|
-
*
|
|
104
|
-
* Heterogeneous rows: each row's children come from one of the
|
|
105
|
-
* registered blocks (picked at add-time). Per-row name prefixing emits
|
|
106
|
-
* `{name}.{i}.data.{innerField}` for inner inputs, plus
|
|
107
|
-
* `{name}.{i}.__id` and `{name}.{i}.type` hidden inputs so the row's
|
|
108
|
-
* envelope round-trips through FormData.
|
|
109
|
-
*
|
|
110
|
-
* Mirrors `RepeaterInput`'s reorder / collapse / clone / inner-field
|
|
111
|
-
* live-resolve plumbing — the array-row primitives (`reorderRows`)
|
|
112
|
-
* are imported directly to keep the two fields' behavior aligned.
|
|
113
|
-
*/
|
|
114
|
-
export function BuilderInput({
|
|
115
|
-
el,
|
|
116
|
-
name,
|
|
117
|
-
disabled,
|
|
118
|
-
}: {
|
|
119
|
-
el: ElementMeta
|
|
120
|
-
name: string
|
|
121
|
-
disabled: boolean
|
|
122
|
-
}): React.ReactElement {
|
|
123
|
-
const formIdFromCtx = useContext(FormIdContext)
|
|
124
|
-
const formId = formIdFromCtx || `builder-${name}`
|
|
125
|
-
const meta = el as BuilderMetaShape
|
|
126
|
-
const blocks = meta.blocks ?? []
|
|
127
|
-
const blocksByName = useMemo(
|
|
128
|
-
() => new Map(blocks.map(b => [b.name, b])),
|
|
129
|
-
[blocks],
|
|
130
|
-
)
|
|
131
|
-
const minItems = typeof meta.minItems === 'number' ? meta.minItems : undefined
|
|
132
|
-
const maxItems = typeof meta.maxItems === 'number' ? meta.maxItems : undefined
|
|
133
|
-
const collapsible = Boolean(meta.collapsible)
|
|
134
|
-
const defaultCollapsed = Boolean(meta.defaultCollapsed)
|
|
135
|
-
const accordion = Boolean(meta.accordion)
|
|
136
|
-
const reorderable = Boolean(meta.reorderable)
|
|
137
|
-
const cloneable = Boolean(meta.cloneable)
|
|
138
|
-
const addable = meta.addable !== false
|
|
139
|
-
const deletable = meta.deletable !== false
|
|
140
|
-
const addBetween = Boolean(meta.addBetween)
|
|
141
|
-
const buttonsOnly = Boolean(meta.reorderableWithButtons)
|
|
142
|
-
const showNumbers = Boolean(meta.blockNumbers || meta.itemNumbers)
|
|
143
|
-
const showIcons = meta.blockIcons !== false
|
|
144
|
-
const pickerColumns = typeof meta.blockPickerColumns === 'number' && meta.blockPickerColumns > 1
|
|
145
|
-
? meta.blockPickerColumns
|
|
146
|
-
: 1
|
|
147
|
-
const buttons = meta.buttons
|
|
148
|
-
// Customizer wins over the legacy `addActionLabel`. Default 'Add block'
|
|
149
|
-
// is the final fallback (mirrors `BuilderField.addActionLabel`).
|
|
150
|
-
const addLabel = buttons?.add?.label
|
|
151
|
-
?? (typeof meta.addActionLabel === 'string' ? meta.addActionLabel : 'Add block')
|
|
152
|
-
const addAlignment = meta.addActionAlignment ?? 'start'
|
|
153
|
-
// Row-grid mode mirrors RepeaterField.grid() — n-column grid for the
|
|
154
|
-
// ROWS themselves (distinct from per-block `Block.columns(n)` which
|
|
155
|
-
// grids fields *inside* a block body). DnD drop indicator is
|
|
156
|
-
// suppressed in grid mode (see RepeaterInput for the same caveat).
|
|
157
|
-
// Accepts the scalar form (`grid(2)`) or a responsive object — see
|
|
158
|
-
// `buildGridContainer` for the breakpoint mapping.
|
|
159
|
-
const gridScopeId = useId()
|
|
160
|
-
const gridContainer = useMemo(
|
|
161
|
-
() => buildGridContainer(
|
|
162
|
-
meta.grid as number | Record<string, number | undefined> | undefined,
|
|
163
|
-
gridScopeId,
|
|
164
|
-
),
|
|
165
|
-
[meta.grid, gridScopeId],
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
const initialRows: RowState[] = useMemo(
|
|
169
|
-
() => (meta.rows ?? []).map(r => ({
|
|
170
|
-
id: r.id,
|
|
171
|
-
type: r.type,
|
|
172
|
-
children: r.children,
|
|
173
|
-
...(r.itemLabel !== undefined ? { itemLabel: r.itemLabel } : {}),
|
|
174
|
-
...(r.hidden ? { hidden: true } : {}),
|
|
175
|
-
...(r.unknownType ? { unknownType: true } : {}),
|
|
176
|
-
...(r.extraActions && r.extraActions.length > 0 ? { extraActions: r.extraActions } : {}),
|
|
177
|
-
...(r.canDelete === false ? { canDelete: false as const } : {}),
|
|
178
|
-
...(r.canClone === false ? { canClone: false as const } : {}),
|
|
179
|
-
...(r.canReorder === false ? { canReorder: false as const } : {}),
|
|
180
|
-
})),
|
|
181
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
182
|
-
[],
|
|
183
|
-
)
|
|
184
|
-
const [rows, setRows] = useState<RowState[]>(initialRows)
|
|
185
|
-
const metaRows = meta.rows
|
|
186
|
-
useEffect(() => {
|
|
187
|
-
if (!metaRows) return
|
|
188
|
-
setRows(prev => syncRowGates(prev, metaRows))
|
|
189
|
-
}, [metaRows])
|
|
190
|
-
// Phase F.5 — row-array CRDT binding (mirrors `RepeaterInput`). The
|
|
191
|
-
// initial-row payload that lands on `add()` carries the block's `type`
|
|
192
|
-
// alongside the empty field map so peers see the discriminator from
|
|
193
|
-
// the first event — without it, the picker dropdown choice doesn't
|
|
194
|
-
// propagate until the user makes their first inner-field edit.
|
|
195
|
-
const rowBinding = useRowBinding(name)
|
|
196
|
-
// Mirror row identities into the form's values map so dotted row-leaf
|
|
197
|
-
// consumers can resolve the row's `__id` via `rowIdAtIndex(ctx.values,
|
|
198
|
-
// name, i)`. Mirrors the same plumbing in RepeaterInput.
|
|
199
|
-
const formStateForIds = useFormState()
|
|
200
|
-
const ctxSetValue = formStateForIds?.setValue
|
|
201
|
-
useEffect(() => {
|
|
202
|
-
if (!ctxSetValue) return
|
|
203
|
-
for (let i = 0; i < rows.length; i++) {
|
|
204
|
-
const row = rows[i]
|
|
205
|
-
if (!row) continue
|
|
206
|
-
ctxSetValue(`${name}.${i}.__id`, row.id)
|
|
207
|
-
}
|
|
208
|
-
}, [rows, name, ctxSetValue])
|
|
209
|
-
// Phase F.5 — reconcile remote row events. Builder mirrors
|
|
210
|
-
// RepeaterInput but reads `event.values.type` to pick the block whose
|
|
211
|
-
// template seeds the new row's children. Falls back to the first
|
|
212
|
-
// registered block when the remote `type` doesn't match a known one;
|
|
213
|
-
// the row still mounts so the user sees the change rather than a
|
|
214
|
-
// silent drop (matches the server-side `unknownType` fallback).
|
|
215
|
-
useEffect(() => {
|
|
216
|
-
if (!rowBinding) return
|
|
217
|
-
return rowBinding.subscribe((event) => {
|
|
218
|
-
if (event.kind === 'add') {
|
|
219
|
-
setRows((prev) => {
|
|
220
|
-
if (prev.some(r => r.id === event.rowId)) return prev
|
|
221
|
-
const blockType = typeof event.values['type'] === 'string'
|
|
222
|
-
? event.values['type'] as string
|
|
223
|
-
: (meta.blocks?.[0]?.name ?? '')
|
|
224
|
-
const block = blocksByName.get(blockType)
|
|
225
|
-
const incoming: RowState = {
|
|
226
|
-
id: event.rowId,
|
|
227
|
-
type: blockType,
|
|
228
|
-
children: block?.template ?? [],
|
|
229
|
-
}
|
|
230
|
-
const next = prev.slice()
|
|
231
|
-
const at = Math.max(0, Math.min(event.index, next.length))
|
|
232
|
-
next.splice(at, 0, incoming)
|
|
233
|
-
return next
|
|
234
|
-
})
|
|
235
|
-
return
|
|
236
|
-
}
|
|
237
|
-
if (event.kind === 'remove') {
|
|
238
|
-
setRows((prev) => {
|
|
239
|
-
if (!prev.some(r => r.id === event.rowId)) return prev
|
|
240
|
-
return prev.filter(r => r.id !== event.rowId)
|
|
241
|
-
})
|
|
242
|
-
return
|
|
243
|
-
}
|
|
244
|
-
setRows((prev) => {
|
|
245
|
-
const fromIdx = prev.findIndex(r => r.id === event.rowId)
|
|
246
|
-
if (fromIdx < 0) return prev
|
|
247
|
-
if (fromIdx === event.to) return prev
|
|
248
|
-
const next = prev.slice()
|
|
249
|
-
const [moved] = next.splice(fromIdx, 1)
|
|
250
|
-
if (!moved) return prev
|
|
251
|
-
next.splice(event.to, 0, moved)
|
|
252
|
-
return next
|
|
253
|
-
})
|
|
254
|
-
})
|
|
255
|
-
}, [rowBinding, blocksByName, meta.blocks])
|
|
256
|
-
|
|
257
|
-
// Phase A reconciliation for `Builder.relationship` PK-switch — mirrors
|
|
258
|
-
// the effect in `RepeaterInput`. See its comment + the plan doc:
|
|
259
|
-
// `pilotiq-pro/docs/plans/repeater-relationship-pk-switch.md`.
|
|
260
|
-
useEffect(() => {
|
|
261
|
-
if (!rowBinding) return
|
|
262
|
-
if (!consumeReconcileFlag(formId)) return
|
|
263
|
-
const timer = setTimeout(() => {
|
|
264
|
-
const plan = computeReconcilePlan({
|
|
265
|
-
current: rowBinding.current(),
|
|
266
|
-
authoritative: initialRows.map(r => r.id),
|
|
267
|
-
})
|
|
268
|
-
for (const id of plan.toRemove) rowBinding.remove(id)
|
|
269
|
-
// For Builder, the row carries a block `type` discriminator; seed
|
|
270
|
-
// it on the add path so peers' picker dropdowns see the right
|
|
271
|
-
// block (matches the existing add path in `addBlock`). The block
|
|
272
|
-
// type comes from initialRows when the row is server-rendered.
|
|
273
|
-
for (const id of plan.toAdd) {
|
|
274
|
-
const row = initialRows.find(r => r.id === id)
|
|
275
|
-
rowBinding.add(id, row?.type ? { type: row.type } : {})
|
|
276
|
-
}
|
|
277
|
-
}, 1500)
|
|
278
|
-
return () => clearTimeout(timer)
|
|
279
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
280
|
-
}, [rowBinding, formId])
|
|
281
|
-
const [collapsed, setCollapsed] = useState<Record<string, boolean>>(() =>
|
|
282
|
-
accordion ? {} : initSeedCollapsed(initialRows, formId, name, defaultCollapsed, collapsible),
|
|
283
|
-
)
|
|
284
|
-
// Accordion mode: single open row id (null = all collapsed). See
|
|
285
|
-
// RepeaterInput for the seeding semantics — Builder mirrors it exactly.
|
|
286
|
-
const [accordionOpenId, setAccordionOpenId] = useState<string | null>(() => {
|
|
287
|
-
if (!accordion) return null
|
|
288
|
-
const stored = readAccordionFromStorage(formId, name)
|
|
289
|
-
if (stored !== undefined) {
|
|
290
|
-
if (stored === '' || initialRows.some(r => r.id === stored)) return stored === '' ? null : stored
|
|
291
|
-
}
|
|
292
|
-
if (defaultCollapsed) return null
|
|
293
|
-
const firstVisible = initialRows.find(r => !r.hidden)
|
|
294
|
-
return firstVisible?.id ?? null
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
const atMin = minItems !== undefined && rows.length <= minItems
|
|
298
|
-
const atMax = maxItems !== undefined && rows.length >= maxItems
|
|
299
|
-
|
|
300
|
-
// Per-block-type cap: greys out the picker option once the cap is
|
|
301
|
-
// hit. Counted across ALL rows (including hidden ones — `itemHidden`
|
|
302
|
-
// is purely UX, not a data filter).
|
|
303
|
-
const typeCounts = useMemo(() => {
|
|
304
|
-
const m = new Map<string, number>()
|
|
305
|
-
for (const r of rows) m.set(r.type, (m.get(r.type) ?? 0) + 1)
|
|
306
|
-
return m
|
|
307
|
-
}, [rows])
|
|
308
|
-
|
|
309
|
-
// `atIndex` (when defined) splices the new row at that position,
|
|
310
|
-
// shifting existing rows down. Used by the inline `addBetween` zones;
|
|
311
|
-
// bottom Add button leaves it undefined → append.
|
|
312
|
-
const addRowOfType = (blockName: string, atIndex?: number): void => {
|
|
313
|
-
if (atMax) return
|
|
314
|
-
const block = blocksByName.get(blockName)
|
|
315
|
-
if (!block) return
|
|
316
|
-
const cap = block.maxItems
|
|
317
|
-
if (cap !== undefined && (typeCounts.get(blockName) ?? 0) >= cap) return
|
|
318
|
-
const newRow: RowState = {
|
|
319
|
-
id: generateRowId(),
|
|
320
|
-
type: block.name,
|
|
321
|
-
children: block.template,
|
|
322
|
-
}
|
|
323
|
-
setRows(prev => {
|
|
324
|
-
if (atIndex === undefined) return [...prev, newRow]
|
|
325
|
-
const i = Math.max(0, Math.min(atIndex, prev.length))
|
|
326
|
-
return [...prev.slice(0, i), newRow, ...prev.slice(i)]
|
|
327
|
-
})
|
|
328
|
-
// F.5 — seed the new block's discriminator on the CRDT side so peers
|
|
329
|
-
// pick the right inner schema without waiting for the user's first edit.
|
|
330
|
-
rowBinding?.add(newRow.id, { type: block.name })
|
|
331
|
-
if (accordion) {
|
|
332
|
-
setAccordionOpenId(newRow.id)
|
|
333
|
-
writeAccordionToStorage(formId, name, newRow.id)
|
|
334
|
-
return
|
|
335
|
-
}
|
|
336
|
-
if (collapsible && defaultCollapsed) {
|
|
337
|
-
setCollapsed(prev => ({ ...prev, [newRow.id]: true }))
|
|
338
|
-
writeCollapsedToStorage(formId, name, newRow.id, true)
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const removeRow = (id: string): void => {
|
|
343
|
-
if (atMin) return
|
|
344
|
-
setRows(prev => prev.filter(r => r.id !== id))
|
|
345
|
-
rowBinding?.remove(id)
|
|
346
|
-
if (accordion) {
|
|
347
|
-
if (accordionOpenId === id) {
|
|
348
|
-
setAccordionOpenId(null)
|
|
349
|
-
writeAccordionToStorage(formId, name, null)
|
|
350
|
-
}
|
|
351
|
-
return
|
|
352
|
-
}
|
|
353
|
-
setCollapsed(prev => {
|
|
354
|
-
const { [id]: _drop, ...rest } = prev
|
|
355
|
-
return rest
|
|
356
|
-
})
|
|
357
|
-
deleteCollapsedFromStorage(formId, name, id)
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const cloneRow = (id: string): void => {
|
|
361
|
-
if (atMax) return
|
|
362
|
-
let cloneId: string | null = null
|
|
363
|
-
let cloneType: string | null = null
|
|
364
|
-
setRows(prev => {
|
|
365
|
-
const idx = prev.findIndex(r => r.id === id)
|
|
366
|
-
if (idx < 0) return prev
|
|
367
|
-
const source = prev[idx]!
|
|
368
|
-
// Block.maxItems applies to clones too.
|
|
369
|
-
const block = blocksByName.get(source.type)
|
|
370
|
-
const cap = block?.maxItems
|
|
371
|
-
if (cap !== undefined && (typeCounts.get(source.type) ?? 0) >= cap) return prev
|
|
372
|
-
cloneId = generateRowId()
|
|
373
|
-
cloneType = source.type
|
|
374
|
-
const clone: RowState = {
|
|
375
|
-
id: cloneId,
|
|
376
|
-
type: source.type,
|
|
377
|
-
children: source.children,
|
|
378
|
-
...(source.itemLabel !== undefined ? { itemLabel: source.itemLabel } : {}),
|
|
379
|
-
}
|
|
380
|
-
const next = prev.slice()
|
|
381
|
-
next.splice(idx + 1, 0, clone)
|
|
382
|
-
return next
|
|
383
|
-
})
|
|
384
|
-
if (cloneId !== null && cloneType !== null) {
|
|
385
|
-
rowBinding?.add(cloneId, { type: cloneType })
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const moveRow = (id: string, dir: -1 | 1): void => {
|
|
390
|
-
const idx = rows.findIndex(r => r.id === id)
|
|
391
|
-
if (idx < 0) return
|
|
392
|
-
let next: RowState[]
|
|
393
|
-
if (dir === -1) {
|
|
394
|
-
let target = idx - 1
|
|
395
|
-
while (target >= 0 && rows[target]?.hidden) target--
|
|
396
|
-
if (target < 0) return
|
|
397
|
-
next = reorderRows(rows, idx, target)
|
|
398
|
-
} else {
|
|
399
|
-
let target = idx + 1
|
|
400
|
-
while (target < rows.length && rows[target]?.hidden) target++
|
|
401
|
-
if (target >= rows.length) return
|
|
402
|
-
next = reorderRows(rows, idx, target + 1)
|
|
403
|
-
}
|
|
404
|
-
if (next === rows) return
|
|
405
|
-
setRows(next)
|
|
406
|
-
rowBinding?.reorder(next.map(r => r.id))
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// ── DnD state (skipped when buttonsOnly) ────────────────
|
|
410
|
-
const dndEnabled = reorderable && !buttonsOnly && !disabled
|
|
411
|
-
const {
|
|
412
|
-
dragId, dropAt,
|
|
413
|
-
onDragStart: onRowDragStart,
|
|
414
|
-
onDragOver: onRowDragOver,
|
|
415
|
-
onDrop: onRowDrop,
|
|
416
|
-
onDragEnd: onRowDragEnd,
|
|
417
|
-
} = useRowReorderDnd({
|
|
418
|
-
enabled: dndEnabled,
|
|
419
|
-
onDrop: (fromId, at) => {
|
|
420
|
-
// See RepeaterInput's matching onDrop comment — closure-mutation
|
|
421
|
-
// through setRows's updater is unreliable when other state updates
|
|
422
|
-
// are batched (useRowReorderDnd nulls dragId/dropAt right before
|
|
423
|
-
// calling this).
|
|
424
|
-
const fromIdx = rows.findIndex(r => r.id === fromId)
|
|
425
|
-
if (fromIdx < 0) return
|
|
426
|
-
const next = reorderRows(rows, fromIdx, at)
|
|
427
|
-
if (next === rows) return
|
|
428
|
-
setRows(next)
|
|
429
|
-
rowBinding?.reorder(next.map(r => r.id))
|
|
430
|
-
},
|
|
431
|
-
})
|
|
432
|
-
|
|
433
|
-
// ── Inner-field live re-resolve (mirrors RepeaterInput) ─
|
|
434
|
-
const formState = useFormState()
|
|
435
|
-
const fireLive = (n: string, value: string, eventKind: 'change' | 'blur'): void => {
|
|
436
|
-
if (!formState) return
|
|
437
|
-
if (!n.includes('.')) return
|
|
438
|
-
const fieldMeta = findFieldMeta(formState.formMeta, n)
|
|
439
|
-
const liveCfg = fieldMeta?.['live']
|
|
440
|
-
const hasJs = (fieldMeta as { afterStateUpdatedJs?: string } | undefined)?.afterStateUpdatedJs !== undefined
|
|
441
|
-
if (!liveCfg && !hasJs) return
|
|
442
|
-
if (liveCfg) {
|
|
443
|
-
const onBlurMode = typeof liveCfg === 'object' && liveCfg !== null
|
|
444
|
-
&& (liveCfg as { onBlur?: boolean }).onBlur === true
|
|
445
|
-
if (eventKind === 'change' && onBlurMode) return
|
|
446
|
-
if (eventKind === 'blur' && !onBlurMode) return
|
|
447
|
-
} else {
|
|
448
|
-
if (eventKind === 'blur') return
|
|
449
|
-
}
|
|
450
|
-
formState.triggerLive(n, value)
|
|
451
|
-
}
|
|
452
|
-
const onContainerChange = (e: React.ChangeEvent<HTMLDivElement>): void => {
|
|
453
|
-
const t = e.target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
|
454
|
-
if (!t.name) return
|
|
455
|
-
fireLive(t.name, t.value, 'change')
|
|
456
|
-
}
|
|
457
|
-
const onContainerBlur = (e: React.FocusEvent<HTMLDivElement>): void => {
|
|
458
|
-
const t = e.target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
|
459
|
-
if (!t.name) return
|
|
460
|
-
fireLive(t.name, t.value, 'blur')
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const toggleCollapsed = (id: string): void => {
|
|
464
|
-
if (accordion) {
|
|
465
|
-
const next = accordionOpenId === id ? null : id
|
|
466
|
-
setAccordionOpenId(next)
|
|
467
|
-
writeAccordionToStorage(formId, name, next)
|
|
468
|
-
return
|
|
469
|
-
}
|
|
470
|
-
setCollapsed(prev => {
|
|
471
|
-
const nextValue = !prev[id]
|
|
472
|
-
writeCollapsedToStorage(formId, name, id, nextValue)
|
|
473
|
-
return { ...prev, [id]: nextValue }
|
|
474
|
-
})
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Bulk expand / collapse — accordion preserves its "only one open"
|
|
478
|
-
// invariant by opening the first visible row on expandAll and clearing
|
|
479
|
-
// openId on collapseAll. Per-row mode iterates every row and writes
|
|
480
|
-
// the storage slot so reload restores the bulk state.
|
|
481
|
-
const expandAll = (): void => {
|
|
482
|
-
if (accordion) {
|
|
483
|
-
const firstVisible = rows.find(r => !r.hidden)
|
|
484
|
-
const next = firstVisible?.id ?? null
|
|
485
|
-
setAccordionOpenId(next)
|
|
486
|
-
writeAccordionToStorage(formId, name, next)
|
|
487
|
-
return
|
|
488
|
-
}
|
|
489
|
-
setCollapsed({})
|
|
490
|
-
for (const r of rows) writeCollapsedToStorage(formId, name, r.id, false)
|
|
491
|
-
}
|
|
492
|
-
const collapseAll = (): void => {
|
|
493
|
-
if (accordion) {
|
|
494
|
-
setAccordionOpenId(null)
|
|
495
|
-
writeAccordionToStorage(formId, name, null)
|
|
496
|
-
return
|
|
497
|
-
}
|
|
498
|
-
const next: Record<string, boolean> = {}
|
|
499
|
-
for (const r of rows) {
|
|
500
|
-
next[r.id] = true
|
|
501
|
-
writeCollapsedToStorage(formId, name, r.id, true)
|
|
502
|
-
}
|
|
503
|
-
setCollapsed(next)
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
const hasVisibleRow = rows.some(r => !r.hidden)
|
|
507
|
-
const firstVisibleIdx = rows.findIndex(r => !r.hidden)
|
|
508
|
-
const lastVisibleIdx = (() => {
|
|
509
|
-
for (let i = rows.length - 1; i >= 0; i--) if (!rows[i]?.hidden) return i
|
|
510
|
-
return -1
|
|
511
|
-
})()
|
|
512
|
-
|
|
513
|
-
const addAlignClass = addAlignment === 'center'
|
|
514
|
-
? 'self-center'
|
|
515
|
-
: addAlignment === 'end'
|
|
516
|
-
? 'self-end'
|
|
517
|
-
: 'self-start'
|
|
518
|
-
|
|
519
|
-
return (
|
|
520
|
-
<div
|
|
521
|
-
className="flex flex-col gap-3"
|
|
522
|
-
style={gridContainer.wrapperStyle}
|
|
523
|
-
onChange={onContainerChange}
|
|
524
|
-
onBlur={onContainerBlur}
|
|
525
|
-
>
|
|
526
|
-
<BulkCollapseHeader
|
|
527
|
-
buttons={buttons}
|
|
528
|
-
disabled={disabled || !hasVisibleRow}
|
|
529
|
-
onExpandAll={expandAll}
|
|
530
|
-
onCollapseAll={collapseAll}
|
|
531
|
-
/>
|
|
532
|
-
|
|
533
|
-
{!hasVisibleRow && (
|
|
534
|
-
<div className="rounded-md border border-dashed px-4 py-6 text-center text-sm text-muted-foreground">
|
|
535
|
-
No items yet. Click {addLabel} to start.
|
|
536
|
-
</div>
|
|
537
|
-
)}
|
|
538
|
-
|
|
539
|
-
{gridContainer.styleBlock}
|
|
540
|
-
<div
|
|
541
|
-
className={gridContainer.className}
|
|
542
|
-
style={gridContainer.style}
|
|
543
|
-
>
|
|
544
|
-
{rows.map((row, i) => (
|
|
545
|
-
<React.Fragment key={row.id}>
|
|
546
|
-
{!row.hidden && dropAt === i && !gridContainer.hasGrid && <DropIndicator />}
|
|
547
|
-
{!row.hidden && addBetween && addable && blocks.length > 0 && !gridContainer.hasGrid && (
|
|
548
|
-
<BetweenInserter
|
|
549
|
-
blocks={blocks}
|
|
550
|
-
typeCounts={typeCounts}
|
|
551
|
-
atMax={atMax}
|
|
552
|
-
disabled={disabled}
|
|
553
|
-
columns={pickerColumns}
|
|
554
|
-
onPick={(blockName) => addRowOfType(blockName, i)}
|
|
555
|
-
/>
|
|
556
|
-
)}
|
|
557
|
-
<BuilderRow
|
|
558
|
-
row={row}
|
|
559
|
-
block={blocksByName.get(row.type)}
|
|
560
|
-
index={i}
|
|
561
|
-
isFirstVisible={i === firstVisibleIdx}
|
|
562
|
-
isLastVisible={i === lastVisibleIdx}
|
|
563
|
-
name={name}
|
|
564
|
-
disabled={disabled}
|
|
565
|
-
collapsible={collapsible}
|
|
566
|
-
isCollapsed={collapsible && (
|
|
567
|
-
accordion
|
|
568
|
-
? accordionOpenId !== row.id
|
|
569
|
-
: (collapsed[row.id] ?? false)
|
|
570
|
-
)}
|
|
571
|
-
reorderable={reorderable}
|
|
572
|
-
buttonsOnly={buttonsOnly}
|
|
573
|
-
cloneable={cloneable}
|
|
574
|
-
deletable={deletable}
|
|
575
|
-
atMin={atMin}
|
|
576
|
-
atMax={atMax}
|
|
577
|
-
showNumbers={showNumbers}
|
|
578
|
-
showIcons={showIcons}
|
|
579
|
-
buttons={buttons}
|
|
580
|
-
isDragging={dragId === row.id}
|
|
581
|
-
rowPath={`${name}.${i}`}
|
|
582
|
-
onMoveUp={() => moveRow(row.id, -1)}
|
|
583
|
-
onMoveDown={() => moveRow(row.id, 1)}
|
|
584
|
-
onClone={() => cloneRow(row.id)}
|
|
585
|
-
onRemove={() => removeRow(row.id)}
|
|
586
|
-
onToggleCollapse={() => toggleCollapsed(row.id)}
|
|
587
|
-
onDragStart={onRowDragStart(row.id)}
|
|
588
|
-
onDragOver={onRowDragOver(i)}
|
|
589
|
-
onDrop={onRowDrop}
|
|
590
|
-
onDragEnd={onRowDragEnd}
|
|
591
|
-
/>
|
|
592
|
-
</React.Fragment>
|
|
593
|
-
))}
|
|
594
|
-
{dropAt === rows.length && !gridContainer.hasGrid && <DropIndicator />}
|
|
595
|
-
</div>
|
|
596
|
-
|
|
597
|
-
{addable && blocks.length > 0 && (
|
|
598
|
-
<BlockPicker
|
|
599
|
-
blocks={blocks}
|
|
600
|
-
typeCounts={typeCounts}
|
|
601
|
-
atMax={atMax}
|
|
602
|
-
disabled={disabled}
|
|
603
|
-
label={addLabel}
|
|
604
|
-
buttons={buttons}
|
|
605
|
-
alignClass={addAlignClass}
|
|
606
|
-
columns={pickerColumns}
|
|
607
|
-
onPick={addRowOfType}
|
|
608
|
-
/>
|
|
609
|
-
)}
|
|
610
|
-
</div>
|
|
611
|
-
)
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// ─── Block picker dropdown ──────────────────────────────────
|
|
615
|
-
|
|
616
|
-
function BlockPicker({
|
|
617
|
-
blocks, typeCounts, atMax, disabled, label, buttons, alignClass, columns, onPick,
|
|
618
|
-
}: {
|
|
619
|
-
blocks: BlockShape[]
|
|
620
|
-
typeCounts: Map<string, number>
|
|
621
|
-
atMax: boolean
|
|
622
|
-
disabled: boolean
|
|
623
|
-
label: string
|
|
624
|
-
buttons: RowButtonsMeta | undefined
|
|
625
|
-
alignClass: string
|
|
626
|
-
columns: number
|
|
627
|
-
onPick: (blockName: string) => void
|
|
628
|
-
}): React.ReactElement {
|
|
629
|
-
// Resolve customizer overrides (icon + tooltip) for the bottom Add
|
|
630
|
-
// button. Color is intentionally ignored to preserve the outline-button
|
|
631
|
-
// visual identity (use a header `Action.color()` if you need a tinted
|
|
632
|
-
// chrome elsewhere). Label was already pre-resolved upstream.
|
|
633
|
-
const { Icon: AddIcon, tooltip: addTooltip } = resolveRowChrome(
|
|
634
|
-
{ Icon: PlusIcon, label, tooltip: '', colorClass: '' },
|
|
635
|
-
buttons?.add,
|
|
636
|
-
)
|
|
637
|
-
const [open, setOpen] = useState(false)
|
|
638
|
-
const containerRef = useRef<HTMLDivElement>(null)
|
|
639
|
-
|
|
640
|
-
// Close on outside click / Escape — keeps the picker UX out of the
|
|
641
|
-
// way without pulling in a Popover dependency.
|
|
642
|
-
useEffect(() => {
|
|
643
|
-
if (!open) return
|
|
644
|
-
const onDocPointerDown = (e: PointerEvent): void => {
|
|
645
|
-
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
646
|
-
setOpen(false)
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
const onDocKey = (e: KeyboardEvent): void => {
|
|
650
|
-
if (e.key === 'Escape') setOpen(false)
|
|
651
|
-
}
|
|
652
|
-
document.addEventListener('pointerdown', onDocPointerDown)
|
|
653
|
-
document.addEventListener('keydown', onDocKey)
|
|
654
|
-
return () => {
|
|
655
|
-
document.removeEventListener('pointerdown', onDocPointerDown)
|
|
656
|
-
document.removeEventListener('keydown', onDocKey)
|
|
657
|
-
}
|
|
658
|
-
}, [open])
|
|
659
|
-
|
|
660
|
-
// Single-block shortcut — skip the dropdown entirely.
|
|
661
|
-
if (blocks.length === 1) {
|
|
662
|
-
const only = blocks[0]!
|
|
663
|
-
const onlyAtCap = only.maxItems !== undefined && (typeCounts.get(only.name) ?? 0) >= only.maxItems
|
|
664
|
-
return (
|
|
665
|
-
<Button
|
|
666
|
-
type="button"
|
|
667
|
-
variant="outline"
|
|
668
|
-
size="sm"
|
|
669
|
-
onClick={() => onPick(only.name)}
|
|
670
|
-
disabled={disabled || atMax || onlyAtCap}
|
|
671
|
-
title={addTooltip || undefined}
|
|
672
|
-
className={alignClass}
|
|
673
|
-
>
|
|
674
|
-
<AddIcon className="size-4" />
|
|
675
|
-
{label}
|
|
676
|
-
</Button>
|
|
677
|
-
)
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
return (
|
|
681
|
-
<div ref={containerRef} className={`relative ${alignClass}`}>
|
|
682
|
-
<Button
|
|
683
|
-
type="button"
|
|
684
|
-
variant="outline"
|
|
685
|
-
size="sm"
|
|
686
|
-
onClick={() => setOpen(o => !o)}
|
|
687
|
-
disabled={disabled || atMax}
|
|
688
|
-
title={addTooltip || undefined}
|
|
689
|
-
aria-haspopup="menu"
|
|
690
|
-
aria-expanded={open}
|
|
691
|
-
>
|
|
692
|
-
<AddIcon className="size-4" />
|
|
693
|
-
{label}
|
|
694
|
-
<ChevronDownIcon className="size-3 opacity-50" />
|
|
695
|
-
</Button>
|
|
696
|
-
{open && (
|
|
697
|
-
<div
|
|
698
|
-
role="menu"
|
|
699
|
-
className="absolute z-20 mt-2 min-w-[12rem] rounded-md border bg-popover p-1 shadow-md"
|
|
700
|
-
>
|
|
701
|
-
<div
|
|
702
|
-
className={columns > 1 ? 'grid gap-1' : 'flex flex-col gap-0.5'}
|
|
703
|
-
style={columns > 1 ? { gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` } : undefined}
|
|
704
|
-
>
|
|
705
|
-
{blocks.map(b => {
|
|
706
|
-
const atTypeCap = b.maxItems !== undefined && (typeCounts.get(b.name) ?? 0) >= b.maxItems
|
|
707
|
-
return (
|
|
708
|
-
<BlockPickerItem
|
|
709
|
-
key={b.name}
|
|
710
|
-
block={b}
|
|
711
|
-
disabled={atTypeCap}
|
|
712
|
-
onPick={() => { onPick(b.name); setOpen(false) }}
|
|
713
|
-
/>
|
|
714
|
-
)
|
|
715
|
-
})}
|
|
716
|
-
</div>
|
|
717
|
-
</div>
|
|
718
|
-
)}
|
|
719
|
-
</div>
|
|
720
|
-
)
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
function BlockPickerItem({
|
|
724
|
-
block, disabled, onPick,
|
|
725
|
-
}: {
|
|
726
|
-
block: BlockShape
|
|
727
|
-
disabled: boolean
|
|
728
|
-
onPick: () => void
|
|
729
|
-
}): React.ReactElement {
|
|
730
|
-
const Icon = useIconFor(block.icon)
|
|
731
|
-
return (
|
|
732
|
-
<button
|
|
733
|
-
type="button"
|
|
734
|
-
role="menuitem"
|
|
735
|
-
onClick={onPick}
|
|
736
|
-
disabled={disabled}
|
|
737
|
-
className="flex items-center gap-2 rounded px-2 py-1.5 text-left text-sm hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
|
|
738
|
-
>
|
|
739
|
-
{Icon && <Icon className="size-4 shrink-0 text-muted-foreground" />}
|
|
740
|
-
<span className="truncate">{block.label}</span>
|
|
741
|
-
</button>
|
|
742
|
-
)
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// ─── Inline insert-between zone (Builder.addBetween) ────────
|
|
746
|
-
//
|
|
747
|
-
// Hairline horizontal "+" button that lives between rows. Hidden
|
|
748
|
-
// (opacity-0) until hovered or focused so the row stack stays calm,
|
|
749
|
-
// then surfaces on hover. Clicking opens a compact picker rooted at
|
|
750
|
-
// the inserter — the same `BlockPickerItem` shape as the bottom Add
|
|
751
|
-
// button. When only one block is registered, clicking the line
|
|
752
|
-
// inserts directly without a dropdown.
|
|
753
|
-
//
|
|
754
|
-
// Insertion index is owned by the parent — the inserter just calls
|
|
755
|
-
// `onPick(blockName)` and the caller splices at the right place.
|
|
756
|
-
|
|
757
|
-
function BetweenInserter({
|
|
758
|
-
blocks, typeCounts, atMax, disabled, columns, onPick,
|
|
759
|
-
}: {
|
|
760
|
-
blocks: BlockShape[]
|
|
761
|
-
typeCounts: Map<string, number>
|
|
762
|
-
atMax: boolean
|
|
763
|
-
disabled: boolean
|
|
764
|
-
columns: number
|
|
765
|
-
onPick: (blockName: string) => void
|
|
766
|
-
}): React.ReactElement | null {
|
|
767
|
-
const [open, setOpen] = useState(false)
|
|
768
|
-
const containerRef = useRef<HTMLDivElement>(null)
|
|
769
|
-
|
|
770
|
-
useEffect(() => {
|
|
771
|
-
if (!open) return
|
|
772
|
-
const onDocPointerDown = (e: PointerEvent): void => {
|
|
773
|
-
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
774
|
-
setOpen(false)
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
const onDocKey = (e: KeyboardEvent): void => {
|
|
778
|
-
if (e.key === 'Escape') setOpen(false)
|
|
779
|
-
}
|
|
780
|
-
document.addEventListener('pointerdown', onDocPointerDown)
|
|
781
|
-
document.addEventListener('keydown', onDocKey)
|
|
782
|
-
return () => {
|
|
783
|
-
document.removeEventListener('pointerdown', onDocPointerDown)
|
|
784
|
-
document.removeEventListener('keydown', onDocKey)
|
|
785
|
-
}
|
|
786
|
-
}, [open])
|
|
787
|
-
|
|
788
|
-
if (blocks.length === 0) return null
|
|
789
|
-
|
|
790
|
-
const isDisabled = disabled || atMax
|
|
791
|
-
|
|
792
|
-
return (
|
|
793
|
-
<div ref={containerRef} className="relative -my-1 flex justify-center">
|
|
794
|
-
<button
|
|
795
|
-
type="button"
|
|
796
|
-
onClick={() => {
|
|
797
|
-
if (isDisabled) return
|
|
798
|
-
// Single-block shortcut — skip the dropdown.
|
|
799
|
-
if (blocks.length === 1) {
|
|
800
|
-
const only = blocks[0]!
|
|
801
|
-
const onlyAtCap = only.maxItems !== undefined && (typeCounts.get(only.name) ?? 0) >= only.maxItems
|
|
802
|
-
if (onlyAtCap) return
|
|
803
|
-
onPick(only.name)
|
|
804
|
-
return
|
|
805
|
-
}
|
|
806
|
-
setOpen(o => !o)
|
|
807
|
-
}}
|
|
808
|
-
disabled={isDisabled}
|
|
809
|
-
aria-label="Insert block here"
|
|
810
|
-
aria-haspopup={blocks.length > 1 ? 'menu' : undefined}
|
|
811
|
-
aria-expanded={blocks.length > 1 ? open : undefined}
|
|
812
|
-
className="group/inserter flex h-4 w-full items-center justify-center opacity-0 hover:opacity-100 focus-visible:opacity-100 transition-opacity disabled:pointer-events-none"
|
|
813
|
-
>
|
|
814
|
-
<span className="flex h-px w-full items-center bg-border group-hover/inserter:bg-primary group-focus-visible/inserter:bg-primary transition-colors">
|
|
815
|
-
<span className="mx-auto flex size-5 items-center justify-center rounded-full border border-primary bg-background text-primary">
|
|
816
|
-
<PlusIcon className="size-3" />
|
|
817
|
-
</span>
|
|
818
|
-
</span>
|
|
819
|
-
</button>
|
|
820
|
-
{open && blocks.length > 1 && (
|
|
821
|
-
<div
|
|
822
|
-
role="menu"
|
|
823
|
-
className="absolute left-1/2 top-full z-20 mt-1 min-w-[12rem] -translate-x-1/2 rounded-md border bg-popover p-1 shadow-md"
|
|
824
|
-
>
|
|
825
|
-
<div
|
|
826
|
-
className={columns > 1 ? 'grid gap-1' : 'flex flex-col gap-0.5'}
|
|
827
|
-
style={columns > 1 ? { gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` } : undefined}
|
|
828
|
-
>
|
|
829
|
-
{blocks.map(b => {
|
|
830
|
-
const atTypeCap = b.maxItems !== undefined && (typeCounts.get(b.name) ?? 0) >= b.maxItems
|
|
831
|
-
return (
|
|
832
|
-
<BlockPickerItem
|
|
833
|
-
key={b.name}
|
|
834
|
-
block={b}
|
|
835
|
-
disabled={atTypeCap}
|
|
836
|
-
onPick={() => { onPick(b.name); setOpen(false) }}
|
|
837
|
-
/>
|
|
838
|
-
)
|
|
839
|
-
})}
|
|
840
|
-
</div>
|
|
841
|
-
</div>
|
|
842
|
-
)}
|
|
843
|
-
</div>
|
|
844
|
-
)
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// ─── Row ────────────────────────────────────────────────────
|
|
848
|
-
|
|
849
|
-
function BuilderRow({
|
|
850
|
-
row, block, index, isFirstVisible, isLastVisible, name, disabled,
|
|
851
|
-
collapsible, isCollapsed, reorderable, buttonsOnly, cloneable, deletable,
|
|
852
|
-
atMin, atMax, showNumbers, showIcons, buttons, isDragging,
|
|
853
|
-
rowPath,
|
|
854
|
-
onMoveUp, onMoveDown, onClone, onRemove, onToggleCollapse,
|
|
855
|
-
onDragStart, onDragOver, onDrop, onDragEnd,
|
|
856
|
-
}: {
|
|
857
|
-
row: RowState
|
|
858
|
-
block: BlockShape | undefined
|
|
859
|
-
index: number
|
|
860
|
-
isFirstVisible: boolean
|
|
861
|
-
isLastVisible: boolean
|
|
862
|
-
name: string
|
|
863
|
-
disabled: boolean
|
|
864
|
-
collapsible: boolean
|
|
865
|
-
isCollapsed: boolean
|
|
866
|
-
reorderable: boolean
|
|
867
|
-
buttonsOnly: boolean
|
|
868
|
-
cloneable: boolean
|
|
869
|
-
deletable: boolean
|
|
870
|
-
atMin: boolean
|
|
871
|
-
atMax: boolean
|
|
872
|
-
showNumbers: boolean
|
|
873
|
-
showIcons: boolean
|
|
874
|
-
buttons: RowButtonsMeta | undefined
|
|
875
|
-
isDragging: boolean
|
|
876
|
-
rowPath: string
|
|
877
|
-
onMoveUp: () => void
|
|
878
|
-
onMoveDown: () => void
|
|
879
|
-
onClone: () => void
|
|
880
|
-
onRemove: () => void
|
|
881
|
-
onToggleCollapse: () => void
|
|
882
|
-
onDragStart: (e: React.DragEvent<HTMLElement>) => void
|
|
883
|
-
onDragOver: (e: React.DragEvent<HTMLElement>) => void
|
|
884
|
-
onDrop: (e: React.DragEvent<HTMLElement>) => void
|
|
885
|
-
onDragEnd: (e: React.DragEvent<HTMLElement>) => void
|
|
886
|
-
}): React.ReactElement {
|
|
887
|
-
// Inner inputs sit under `name.<i>.data.*` so the {type, data}
|
|
888
|
-
// envelope round-trips through FormData. Hidden envelope inputs
|
|
889
|
-
// (`__id` and `type`) get their own siblings at `name.<i>.__id` and
|
|
890
|
-
// `name.<i>.type`.
|
|
891
|
-
const dataPrefix = `${name}.${index}.data`
|
|
892
|
-
const namespaced = useMemo(
|
|
893
|
-
() => row.children.map(c => prefixFieldNames(c, dataPrefix)),
|
|
894
|
-
[row.children, dataPrefix],
|
|
895
|
-
)
|
|
896
|
-
// Row coords for dotted-path text leaves under this row — composes
|
|
897
|
-
// fragment-key `${arrayName}.${rowId}.${fieldName}` (Phase 1 of
|
|
898
|
-
// collab-row-text-tiptap-backed.md). `parseRowFieldPath` strips the
|
|
899
|
-
// Builder-specific `data` segment, so the coords use the array name +
|
|
900
|
-
// the row's stable id without referencing the dialect.
|
|
901
|
-
const rowCoords = useMemo(
|
|
902
|
-
() => ({ arrayName: name, rowIndex: index, rowId: row.id }),
|
|
903
|
-
[name, index, row.id],
|
|
904
|
-
)
|
|
905
|
-
|
|
906
|
-
const RowIcon = useIconFor(showIcons ? block?.icon : undefined)
|
|
907
|
-
const blockLabel = block?.label ?? row.type ?? 'Block'
|
|
908
|
-
const numberPrefix = showNumbers ? `${index + 1}. ` : ''
|
|
909
|
-
const headerLabel = row.itemLabel ?? `${numberPrefix}${blockLabel}`
|
|
910
|
-
|
|
911
|
-
if (row.hidden) {
|
|
912
|
-
return (
|
|
913
|
-
<RowCoordsContext.Provider value={rowCoords}>
|
|
914
|
-
<div style={{ display: 'none' }} data-pilotiq-builder-row="hidden">
|
|
915
|
-
<input type="hidden" name={`${name}.${index}.__id`} value={row.id} readOnly />
|
|
916
|
-
<input type="hidden" name={`${name}.${index}.type`} value={row.type} readOnly />
|
|
917
|
-
<SchemaRenderer elements={namespaced} />
|
|
918
|
-
</div>
|
|
919
|
-
</RowCoordsContext.Provider>
|
|
920
|
-
)
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
if (row.unknownType || !block) {
|
|
924
|
-
// Stale block type — keep envelope round-tripping but show a clear
|
|
925
|
-
// placeholder. Values inside `data` aren't editable here (the
|
|
926
|
-
// schema is gone), but they survive submit because the server
|
|
927
|
-
// passes them through verbatim.
|
|
928
|
-
return (
|
|
929
|
-
<div className="rounded-md border border-dashed bg-muted/30 px-3 py-2 text-sm text-muted-foreground">
|
|
930
|
-
<input type="hidden" name={`${name}.${index}.__id`} value={row.id} readOnly />
|
|
931
|
-
<input type="hidden" name={`${name}.${index}.type`} value={row.type} readOnly />
|
|
932
|
-
Unknown block type "{row.type}". Block was removed from the schema —
|
|
933
|
-
save will preserve the row's data unchanged.
|
|
934
|
-
</div>
|
|
935
|
-
)
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// Per-row capability gates — `itemCan*(rule)` server-resolved.
|
|
939
|
-
// Composes with the global `deletable / cloneable / reorderable` flags:
|
|
940
|
-
// a per-row gate can only narrow what the global flag allows.
|
|
941
|
-
const canDelete = row.canDelete !== false
|
|
942
|
-
const canClone = row.canClone !== false
|
|
943
|
-
const canReorder = row.canReorder !== false
|
|
944
|
-
|
|
945
|
-
// Drag source on the grip `<span>`, drop target on the row container.
|
|
946
|
-
// See RepeaterInput's RepeaterRow for the rationale (lets the row body
|
|
947
|
-
// host a Tiptap contenteditable without losing reorder).
|
|
948
|
-
const rowRef = useRef<HTMLDivElement>(null)
|
|
949
|
-
const dragEnabled = reorderable && !buttonsOnly && !disabled && canReorder
|
|
950
|
-
const containerDropTargetProps = dragEnabled
|
|
951
|
-
? { onDragOver, onDrop, onDragEnd }
|
|
952
|
-
: {}
|
|
953
|
-
const gripDragHandleProps = dragEnabled
|
|
954
|
-
? {
|
|
955
|
-
draggable: true as const,
|
|
956
|
-
onDragStart: (e: React.DragEvent<HTMLElement>): void => {
|
|
957
|
-
if (rowRef.current) e.dataTransfer.setDragImage(rowRef.current, 0, 0)
|
|
958
|
-
onDragStart(e)
|
|
959
|
-
},
|
|
960
|
-
}
|
|
961
|
-
: undefined
|
|
962
|
-
|
|
963
|
-
const innerColumns = block.columns && block.columns > 1 ? block.columns : 1
|
|
964
|
-
|
|
965
|
-
return (
|
|
966
|
-
<RowCoordsContext.Provider value={rowCoords}>
|
|
967
|
-
<div
|
|
968
|
-
ref={rowRef}
|
|
969
|
-
className={`rounded-md border bg-card transition-opacity ${isDragging ? 'opacity-50' : ''}`}
|
|
970
|
-
data-pilotiq-builder-row=""
|
|
971
|
-
{...containerDropTargetProps}
|
|
972
|
-
>
|
|
973
|
-
<div className="flex items-center gap-2 border-b px-3 py-2">
|
|
974
|
-
{reorderable && !buttonsOnly && canReorder && (
|
|
975
|
-
<ReorderGrip disabled={disabled} buttons={buttons} dragHandleProps={gripDragHandleProps} />
|
|
976
|
-
)}
|
|
977
|
-
{collapsible && (
|
|
978
|
-
<CollapseChevron
|
|
979
|
-
isCollapsed={isCollapsed}
|
|
980
|
-
disabled={disabled}
|
|
981
|
-
buttons={buttons}
|
|
982
|
-
onToggle={onToggleCollapse}
|
|
983
|
-
/>
|
|
984
|
-
)}
|
|
985
|
-
{RowIcon && <RowIcon className="size-4 shrink-0 text-muted-foreground" />}
|
|
986
|
-
<span className="flex-1 truncate text-sm font-medium">{headerLabel}</span>
|
|
987
|
-
<input type="hidden" name={`${name}.${index}.__id`} value={row.id} readOnly />
|
|
988
|
-
<input type="hidden" name={`${name}.${index}.type`} value={row.type} readOnly />
|
|
989
|
-
{reorderable && canReorder && (
|
|
990
|
-
<>
|
|
991
|
-
<RowChromeIconButton
|
|
992
|
-
defaults={DEFAULT_MOVE_UP}
|
|
993
|
-
override={buttons?.moveUp}
|
|
994
|
-
disabled={disabled || isFirstVisible}
|
|
995
|
-
onClick={onMoveUp}
|
|
996
|
-
/>
|
|
997
|
-
<RowChromeIconButton
|
|
998
|
-
defaults={DEFAULT_MOVE_DOWN}
|
|
999
|
-
override={buttons?.moveDown}
|
|
1000
|
-
disabled={disabled || isLastVisible}
|
|
1001
|
-
onClick={onMoveDown}
|
|
1002
|
-
/>
|
|
1003
|
-
</>
|
|
1004
|
-
)}
|
|
1005
|
-
{row.extraActions && row.extraActions.length > 0 && (
|
|
1006
|
-
<ExtraActionStrip
|
|
1007
|
-
actions={row.extraActions}
|
|
1008
|
-
rowPath={rowPath}
|
|
1009
|
-
disabled={disabled}
|
|
1010
|
-
/>
|
|
1011
|
-
)}
|
|
1012
|
-
{cloneable && canClone && (
|
|
1013
|
-
<RowChromeIconButton
|
|
1014
|
-
defaults={DEFAULT_CLONE}
|
|
1015
|
-
override={buttons?.clone}
|
|
1016
|
-
disabled={disabled || atMax}
|
|
1017
|
-
onClick={onClone}
|
|
1018
|
-
/>
|
|
1019
|
-
)}
|
|
1020
|
-
{deletable && canDelete && (
|
|
1021
|
-
<RowChromeIconButton
|
|
1022
|
-
defaults={DEFAULT_DELETE}
|
|
1023
|
-
override={buttons?.delete}
|
|
1024
|
-
disabled={disabled || atMin}
|
|
1025
|
-
onClick={onRemove}
|
|
1026
|
-
/>
|
|
1027
|
-
)}
|
|
1028
|
-
</div>
|
|
1029
|
-
|
|
1030
|
-
<div
|
|
1031
|
-
className="p-3"
|
|
1032
|
-
style={isCollapsed ? { display: 'none' } : undefined}
|
|
1033
|
-
>
|
|
1034
|
-
{innerColumns > 1
|
|
1035
|
-
? (
|
|
1036
|
-
<div
|
|
1037
|
-
className="grid gap-3"
|
|
1038
|
-
style={{ gridTemplateColumns: `repeat(${innerColumns}, minmax(0, 1fr))` }}
|
|
1039
|
-
>
|
|
1040
|
-
<SchemaRenderer elements={namespaced} />
|
|
1041
|
-
</div>
|
|
1042
|
-
)
|
|
1043
|
-
: <SchemaRenderer elements={namespaced} />}
|
|
1044
|
-
</div>
|
|
1045
|
-
</div>
|
|
1046
|
-
</RowCoordsContext.Provider>
|
|
1047
|
-
)
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
function DropIndicator(): React.ReactElement {
|
|
1051
|
-
return (
|
|
1052
|
-
<div
|
|
1053
|
-
aria-hidden="true"
|
|
1054
|
-
className="pointer-events-none h-0.5 rounded-full bg-primary"
|
|
1055
|
-
/>
|
|
1056
|
-
)
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
/**
|
|
1060
|
-
* Recursively prefix every Field meta's `name` with a row-scoped path.
|
|
1061
|
-
* Mirrors `RepeaterInput.prefixFieldNames` but doesn't recurse into
|
|
1062
|
-
* inner Repeater/Builder rows (nested array-row inside a Block isn't
|
|
1063
|
-
* reactive in v1 — see plan).
|
|
1064
|
-
*/
|
|
1065
|
-
function prefixFieldNames(el: ElementMeta, prefix: string): ElementMeta {
|
|
1066
|
-
if (el.type === 'field' && typeof el['name'] === 'string') {
|
|
1067
|
-
const innerName = el['name']
|
|
1068
|
-
return { ...el, name: `${prefix}.${innerName}` }
|
|
1069
|
-
}
|
|
1070
|
-
if (Array.isArray(el.children)) {
|
|
1071
|
-
return {
|
|
1072
|
-
...el,
|
|
1073
|
-
children: (el.children as ElementMeta[]).map(c => prefixFieldNames(c, prefix)),
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
return el
|
|
1077
|
-
}
|
|
1078
|
-
|