torch-glare 2.4.0 → 2.4.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/README.md +8 -0
- package/apps/lib/components/ActionButton.tsx +7 -12
- package/apps/lib/components/ActionsGroup.tsx +4 -15
- package/apps/lib/components/AlertDialog.tsx +173 -191
- package/apps/lib/components/Avatar.tsx +13 -16
- package/apps/lib/components/Badge.tsx +1 -3
- package/apps/lib/components/BadgeField.tsx +76 -71
- package/apps/lib/components/Breadcrumb.tsx +49 -85
- package/apps/lib/components/Button.tsx +21 -20
- package/apps/lib/components/ButtonGroup.tsx +31 -41
- package/apps/lib/components/Calendar.tsx +51 -29
- package/apps/lib/components/Card.tsx +40 -24
- package/apps/lib/components/Checkbox.tsx +9 -16
- package/apps/lib/components/ColorPicker.tsx +441 -0
- package/apps/lib/components/ConclusionHeader.tsx +148 -0
- package/apps/lib/components/ContextMenu.tsx +97 -96
- package/apps/lib/components/CountBadge.tsx +4 -6
- package/apps/lib/components/DataTable.tsx +196 -216
- package/apps/lib/components/DataViews/DataViewRadio.tsx +1 -6
- package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +13 -47
- package/apps/lib/components/DataViews/DataViewsHeader.tsx +1 -3
- package/apps/lib/components/DataViews/DataViewsLayout.tsx +5 -12
- package/apps/lib/components/DataViews/FilterPanel.tsx +155 -149
- package/apps/lib/components/DataViews/HeaderSearch.tsx +1 -5
- package/apps/lib/components/DataViews/InboxView.tsx +22 -68
- package/apps/lib/components/DataViews/InboxViewCard.tsx +68 -77
- package/apps/lib/components/DataViews/KanbanView.tsx +8 -25
- package/apps/lib/components/DataViews/PanelControls.tsx +3 -13
- package/apps/lib/components/DataViews/SettingsPanel.tsx +83 -89
- package/apps/lib/components/DataViews/TableView.tsx +8 -28
- package/apps/lib/components/DataViews/TreeView.tsx +12 -40
- package/apps/lib/components/DataViews/badgeAdapter.ts +32 -28
- package/apps/lib/components/DataViews/fieldRenderers.tsx +106 -141
- package/apps/lib/components/DataViews/filters/DatePickerRangeFilter.tsx +30 -23
- package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +39 -32
- package/apps/lib/components/DataViews/filters/PresetChips.tsx +15 -15
- package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +69 -58
- package/apps/lib/components/DataViews/index.ts +26 -26
- package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +12 -16
- package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +28 -31
- package/apps/lib/components/DataViews/types.ts +104 -115
- package/apps/lib/components/DatePicker.tsx +200 -196
- package/apps/lib/components/Dialog.tsx +92 -105
- package/apps/lib/components/Divider.tsx +21 -24
- package/apps/lib/components/Drawer.tsx +124 -142
- package/apps/lib/components/DropdownMenu.tsx +131 -171
- package/apps/lib/components/FieldHint.tsx +4 -11
- package/apps/lib/components/Form.tsx +146 -155
- package/apps/lib/components/FormBuilder/DisplayField.tsx +40 -0
- package/apps/lib/components/FormBuilder/context.ts +71 -0
- package/apps/lib/components/FormBuilder/fields/ChoiceFields.tsx +65 -0
- package/apps/lib/components/FormBuilder/fields/ColorField.tsx +76 -0
- package/apps/lib/components/FormBuilder/fields/CustomField.tsx +16 -0
- package/apps/lib/components/FormBuilder/fields/DateField.tsx +34 -0
- package/apps/lib/components/FormBuilder/fields/FieldArray.tsx +79 -0
- package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +174 -0
- package/apps/lib/components/FormBuilder/fields/FileField.tsx +40 -0
- package/apps/lib/components/FormBuilder/fields/OptionListFields.tsx +139 -0
- package/apps/lib/components/FormBuilder/fields/OtpField.tsx +32 -0
- package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +82 -0
- package/apps/lib/components/FormBuilder/fields/RichTextEditorField.tsx +37 -0
- package/apps/lib/components/FormBuilder/fields/SelectField.tsx +101 -0
- package/apps/lib/components/FormBuilder/fields/SignatureField.tsx +175 -0
- package/apps/lib/components/FormBuilder/fields/SliderField.tsx +72 -0
- package/apps/lib/components/FormBuilder/fields/SwitchBoxField.tsx +42 -0
- package/apps/lib/components/FormBuilder/fields/TableField.tsx +312 -0
- package/apps/lib/components/FormBuilder/fields/TextField.tsx +222 -0
- package/apps/lib/components/FormBuilder/fields/TreeSelectField.tsx +49 -0
- package/apps/lib/components/FormBuilder/fields/countries.ts +303 -0
- package/apps/lib/components/FormBuilder/fields/index.ts +25 -0
- package/apps/lib/components/FormBuilder/form-builder.tsx +292 -0
- package/apps/lib/components/FormBuilder/header.tsx +105 -0
- package/apps/lib/components/FormBuilder/index.ts +37 -0
- package/apps/lib/components/FormBuilder/numberFormat.ts +16 -0
- package/apps/lib/components/FormBuilder/stepper.tsx +217 -0
- package/apps/lib/components/FormBuilder/submit.tsx +41 -0
- package/apps/lib/components/FormBuilder/types.ts +264 -0
- package/apps/lib/components/FormBuilder/viewFormat.tsx +137 -0
- package/apps/lib/components/FormRenderer/FormDrawer.tsx +121 -0
- package/apps/lib/components/FormRenderer/form-renderer.tsx +113 -0
- package/apps/lib/components/FormRenderer/index.ts +9 -0
- package/apps/lib/components/FormRenderer/types.ts +79 -0
- package/apps/lib/components/FormStepper.tsx +9 -39
- package/apps/lib/components/FormSummary.tsx +282 -0
- package/apps/lib/components/HeaderBar.tsx +1 -9
- package/apps/lib/components/ImageAttachment.tsx +106 -93
- package/apps/lib/components/InnerLabelField.tsx +20 -16
- package/apps/lib/components/Input.tsx +171 -172
- package/apps/lib/components/InputField.tsx +44 -45
- package/apps/lib/components/InputOTP.tsx +68 -69
- package/apps/lib/components/Label.tsx +61 -58
- package/apps/lib/components/LabelField.tsx +4 -5
- package/apps/lib/components/LabeledCheckBox.tsx +8 -12
- package/apps/lib/components/LabeledRadio.tsx +7 -5
- package/apps/lib/components/LinkButton.tsx +2 -9
- package/apps/lib/components/LoginButton.tsx +4 -9
- package/apps/lib/components/PasswordLevel.tsx +5 -5
- package/apps/lib/components/Popover.tsx +15 -33
- package/apps/lib/components/ProfileMenu.tsx +67 -72
- package/apps/lib/components/Radio.tsx +1 -5
- package/apps/lib/components/RadioCard.tsx +11 -28
- package/apps/lib/components/ScrollArea.tsx +45 -43
- package/apps/lib/components/SearchField.tsx +49 -39
- package/apps/lib/components/SearchableSelect.tsx +16 -16
- package/apps/lib/components/SearchableTable.tsx +108 -118
- package/apps/lib/components/SearchableTree.tsx +469 -0
- package/apps/lib/components/SearchableTreeDialog.tsx +493 -0
- package/apps/lib/components/SectionBlock.tsx +19 -23
- package/apps/lib/components/Select.tsx +80 -186
- package/apps/lib/components/SimpleSelect.tsx +196 -187
- package/apps/lib/components/Skeleton.tsx +9 -12
- package/apps/lib/components/SlideDatePicker.tsx +215 -166
- package/apps/lib/components/SpinLoading.tsx +33 -49
- package/apps/lib/components/Stepper.tsx +13 -56
- package/apps/lib/components/Switch.tsx +8 -10
- package/apps/lib/components/TabFormItem.tsx +3 -12
- package/apps/lib/components/TabSwitch.tsx +7 -34
- package/apps/lib/components/Table.tsx +60 -75
- package/apps/lib/components/{ChartBlockTool.ts → TextEditor/ChartBlockTool.ts} +17 -52
- package/apps/lib/components/TextEditor/RichTextField.tsx +46 -0
- package/apps/lib/components/{TableDnDWrapper.ts → TextEditor/TableDnDWrapper.ts} +15 -48
- package/apps/lib/components/{TextEditor.tsx → TextEditor/TextEditor.tsx} +156 -113
- package/apps/lib/components/TextEditor/TextEditorToolbar.tsx +429 -0
- package/apps/lib/components/TextEditor/editor-tools/AlignmentTune.ts +70 -0
- package/apps/lib/components/TextEditor/editor-tools/ColorInlineTool.ts +50 -0
- package/apps/lib/components/TextEditor/editor-tools/StrikethroughInlineTool.ts +48 -0
- package/apps/lib/components/TextEditor/editor-tools/inlineFormat.ts +98 -0
- package/apps/lib/{types → components/TextEditor}/editorjs.d.ts +19 -0
- package/apps/lib/components/TextEditor/index.ts +7 -0
- package/apps/lib/{utils → components/TextEditor}/markdownParser.ts +2 -7
- package/apps/lib/components/Textarea.tsx +9 -13
- package/apps/lib/components/Timeline.tsx +21 -41
- package/apps/lib/components/Toast.tsx +83 -83
- package/apps/lib/components/Toggle.tsx +112 -114
- package/apps/lib/components/ToggleButton.tsx +1 -1
- package/apps/lib/components/Tooltip.tsx +24 -19
- package/apps/lib/components/TransparentLabel.tsx +61 -62
- package/apps/lib/components/TreeDropDown.tsx +86 -52
- package/apps/lib/components/TreeFolder/TreeFolder.tsx +15 -48
- package/apps/lib/components/TreeFolder/TreeFolderBreadcrumb.tsx +16 -21
- package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +9 -20
- package/apps/lib/components/TreeFolder/TreeFolderStyles.tsx +3 -3
- package/apps/lib/components/TreeFolder/icons.tsx +14 -17
- package/apps/lib/components/TreeFolder/index.ts +15 -8
- package/apps/lib/components/TreeFolder/treeFolderUtils.ts +47 -52
- package/apps/lib/components/TreeFolder/types.ts +32 -32
- package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +117 -123
- package/apps/lib/hooks/useActiveTreeItem.ts +52 -55
- package/apps/lib/hooks/useClickOutside.ts +21 -20
- package/apps/lib/hooks/useDataViewsState.ts +86 -80
- package/apps/lib/hooks/useIsMobile.ts +12 -12
- package/apps/lib/hooks/useResize.ts +54 -65
- package/apps/lib/hooks/useTagSelection.ts +201 -201
- package/apps/lib/layouts/CNLayout.tsx +181 -140
- package/apps/lib/layouts/DataViewCard.tsx +4 -18
- package/apps/lib/layouts/FieldSection.tsx +2 -6
- package/apps/lib/layouts/TreeSubLayout.tsx +24 -38
- package/apps/lib/providers/ThemeProvider.tsx +8 -23
- package/apps/lib/registry.json +997 -0
- package/apps/lib/tsconfig.json +9 -0
- package/apps/lib/tsconfig.tsbuildinfo +1 -0
- package/apps/lib/utils/cn.ts +1 -1
- package/apps/lib/utils/color.ts +175 -0
- package/apps/lib/utils/dataViews/columnUtils.ts +46 -44
- package/apps/lib/utils/dataViews/fieldUtils.ts +70 -71
- package/apps/lib/utils/dataViews/nestedDataUtils.tsx +129 -122
- package/apps/lib/utils/dataViews/pathUtils.ts +74 -67
- package/apps/lib/utils/dataViews/rangeUtils.ts +121 -112
- package/apps/lib/utils/dataViews/treeUtils.ts +204 -211
- package/apps/lib/utils/dateFormat.ts +64 -54
- package/apps/lib/utils/resize.ts +21 -25
- package/apps/lib/utils/types.ts +13 -13
- package/dist/bin/index.js +8 -2
- package/dist/bin/index.js.map +1 -1
- package/dist/src/shared/getAvailableFiles.d.ts.map +1 -1
- package/dist/src/shared/getAvailableFiles.js +6 -1
- package/dist/src/shared/getAvailableFiles.js.map +1 -1
- package/dist/src/shared/loadRegistry.d.ts +7 -0
- package/dist/src/shared/loadRegistry.d.ts.map +1 -0
- package/dist/src/shared/loadRegistry.js +31 -0
- package/dist/src/shared/loadRegistry.js.map +1 -0
- package/dist/src/shared/resolveInstallPlan.d.ts +12 -0
- package/dist/src/shared/resolveInstallPlan.d.ts.map +1 -0
- package/dist/src/shared/resolveInstallPlan.js +39 -0
- package/dist/src/shared/resolveInstallPlan.js.map +1 -0
- package/dist/src/types/main.d.ts +24 -0
- package/dist/src/types/main.d.ts.map +1 -1
- package/docs/README.md +10 -2
- package/docs/components/action-button.md +30 -24
- package/docs/components/actions-group.md +26 -20
- package/docs/components/alert-dialog.md +45 -45
- package/docs/components/avatar.md +6 -2
- package/docs/components/badge-field.md +6 -2
- package/docs/components/badge.md +7 -1
- package/docs/components/breadcrumb.md +15 -40
- package/docs/components/button-group.md +13 -10
- package/docs/components/button.md +58 -33
- package/docs/components/calendar.md +26 -19
- package/docs/components/card.md +23 -18
- package/docs/components/chart-block-tool.md +129 -0
- package/docs/components/checkbox.md +16 -8
- package/docs/components/cn-layout.md +7 -1
- package/docs/components/color-picker.md +101 -0
- package/docs/components/conclusion-header.md +80 -0
- package/docs/components/context-menu.md +18 -25
- package/docs/components/count-badge.md +7 -1
- package/docs/components/data-table.md +45 -33
- package/docs/components/data-views-config-panel.md +15 -11
- package/docs/components/data-views-layout.md +13 -16
- package/docs/components/date-picker.md +30 -23
- package/docs/components/dialog.md +43 -42
- package/docs/components/divider.md +10 -4
- package/docs/components/drawer.md +161 -135
- package/docs/components/dropdown-menu.md +25 -34
- package/docs/components/field-hint.md +37 -25
- package/docs/components/field-section.md +9 -3
- package/docs/components/form-builder.md +270 -0
- package/docs/components/form-renderer.md +228 -0
- package/docs/components/form-stepper.md +6 -0
- package/docs/components/form-summary.md +123 -0
- package/docs/components/form.md +17 -20
- package/docs/components/header-bar.md +6 -0
- package/docs/components/image-attachment.md +43 -45
- package/docs/components/inbox-view.md +16 -5
- package/docs/components/inner-label-field.md +34 -24
- package/docs/components/input-field.md +18 -10
- package/docs/components/input-otp.md +14 -11
- package/docs/components/input.md +22 -16
- package/docs/components/kanban-view.md +15 -5
- package/docs/components/label-field.md +32 -22
- package/docs/components/label.md +43 -32
- package/docs/components/labeled-check-box.md +16 -8
- package/docs/components/labeled-radio.md +18 -11
- package/docs/components/link-button.md +7 -1
- package/docs/components/login-button.md +8 -2
- package/docs/components/password-level.md +37 -24
- package/docs/components/popover.md +26 -23
- package/docs/components/profile-menu.md +38 -27
- package/docs/components/radio-card.md +16 -9
- package/docs/components/radio.md +16 -8
- package/docs/components/scroll-area.md +6 -2
- package/docs/components/search-field.md +14 -6
- package/docs/components/searchable-select.md +19 -14
- package/docs/components/searchable-table.md +18 -13
- package/docs/components/searchable-tree-dialog.md +190 -0
- package/docs/components/searchable-tree.md +200 -0
- package/docs/components/section-block.md +12 -4
- package/docs/components/select.md +23 -18
- package/docs/components/simple-select.md +14 -10
- package/docs/components/skeleton.md +43 -33
- package/docs/components/slide-date-picker.md +37 -29
- package/docs/components/spin-loading.md +32 -23
- package/docs/components/stepper.md +6 -0
- package/docs/components/switch.md +14 -6
- package/docs/components/tab-form-item.md +7 -1
- package/docs/components/tab-switch.md +13 -6
- package/docs/components/table-dnd-wrapper.md +107 -0
- package/docs/components/table-view.md +15 -5
- package/docs/components/table.md +28 -28
- package/docs/components/text-editor.md +37 -13
- package/docs/components/textarea.md +15 -7
- package/docs/components/timeline.md +6 -0
- package/docs/components/toast.md +35 -26
- package/docs/components/toggle-button.md +24 -21
- package/docs/components/toggle.md +23 -15
- package/docs/components/tooltip.md +42 -30
- package/docs/components/transparent-label.md +29 -20
- package/docs/components/tree-drop-down.md +31 -23
- package/docs/components/tree-sub-layout.md +6 -2
- package/docs/components/tree-view.md +16 -5
- package/docs/explanation/architecture.md +54 -0
- package/docs/explanation/design-system.md +52 -0
- package/docs/how-to/data-views-from-backend-response.md +6 -4
- package/docs/how-to/form-and-list-recipes.md +26 -19
- package/docs/how-to/forms-with-form-builder.md +408 -0
- package/docs/how-to/guides.md +177 -197
- package/docs/migration/changelog.md +38 -0
- package/docs/reference/cli.md +87 -0
- package/docs/reference/components.md +11 -12
- package/docs/reference/hooks.md +8 -8
- package/docs/reference/providers.md +22 -22
- package/docs/reference/tailwind-plugins.md +2 -2
- package/docs/reference/theme.md +85 -0
- package/docs/reference/types.md +4 -4
- package/docs/reference/utilities.md +19 -26
- package/docs/tutorials/building-first-form.md +170 -172
- package/docs/tutorials/component-composition.md +34 -50
- package/docs/tutorials/getting-started.md +2 -2
- package/docs/tutorials/theming-basics.md +13 -10
- package/package.json +31 -3
- package/dist/src/commands/block.d.ts +0 -5
- package/dist/src/commands/block.d.ts.map +0 -1
- package/dist/src/commands/block.js +0 -255
- package/dist/src/commands/block.js.map +0 -1
- package/dist/src/shared/configureFonts.d.ts +0 -6
- package/dist/src/shared/configureFonts.d.ts.map +0 -1
- package/dist/src/shared/configureFonts.js +0 -106
- package/dist/src/shared/configureFonts.js.map +0 -1
- package/dist/src/shared/configureGlobalCss.d.ts +0 -6
- package/dist/src/shared/configureGlobalCss.d.ts.map +0 -1
- package/dist/src/shared/configureGlobalCss.js +0 -154
- package/dist/src/shared/configureGlobalCss.js.map +0 -1
- package/dist/src/shared/configureTailwind.d.ts +0 -7
- package/dist/src/shared/configureTailwind.d.ts.map +0 -1
- package/dist/src/shared/configureTailwind.js +0 -163
- package/dist/src/shared/configureTailwind.js.map +0 -1
- package/dist/src/shared/detectFramework.d.ts +0 -23
- package/dist/src/shared/detectFramework.d.ts.map +0 -1
- package/dist/src/shared/detectFramework.js +0 -119
- package/dist/src/shared/detectFramework.js.map +0 -1
- package/dist/src/shared/installBaseUtils.d.ts +0 -5
- package/dist/src/shared/installBaseUtils.d.ts.map +0 -1
- package/dist/src/shared/installBaseUtils.js +0 -79
- package/dist/src/shared/installBaseUtils.js.map +0 -1
- package/dist/src/shared/resolveAliases.d.ts +0 -24
- package/dist/src/shared/resolveAliases.d.ts.map +0 -1
- package/dist/src/shared/resolveAliases.js +0 -98
- package/dist/src/shared/resolveAliases.js.map +0 -1
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ReactNode } from "react";
|
|
5
|
+
import { useForm, type FieldValues } from "react-hook-form";
|
|
6
|
+
|
|
7
|
+
import { cn } from "../../utils/cn";
|
|
8
|
+
import { Form } from "../Form";
|
|
9
|
+
import { SectionBlock, type SectionColor } from "../SectionBlock";
|
|
10
|
+
import {
|
|
11
|
+
LoadingContext,
|
|
12
|
+
ModeContext,
|
|
13
|
+
DirectionContext,
|
|
14
|
+
StepperContext,
|
|
15
|
+
FormIdContext,
|
|
16
|
+
} from "./context";
|
|
17
|
+
import { Header } from "./header";
|
|
18
|
+
import type { FormBuilderRootProps } from "./types";
|
|
19
|
+
import {
|
|
20
|
+
TextField,
|
|
21
|
+
EmailField,
|
|
22
|
+
PasswordField,
|
|
23
|
+
NumberField,
|
|
24
|
+
CurrencyField,
|
|
25
|
+
TextareaField,
|
|
26
|
+
SelectField,
|
|
27
|
+
SearchableSelectField,
|
|
28
|
+
MultiSelectField,
|
|
29
|
+
CheckboxField,
|
|
30
|
+
RadioCardsField,
|
|
31
|
+
RadioListField,
|
|
32
|
+
CheckboxGroupField,
|
|
33
|
+
SwitchBoxField,
|
|
34
|
+
DateField,
|
|
35
|
+
OtpField,
|
|
36
|
+
TreeSelectField,
|
|
37
|
+
SliderField,
|
|
38
|
+
ColorField,
|
|
39
|
+
SignatureField,
|
|
40
|
+
PhoneField,
|
|
41
|
+
FieldArray,
|
|
42
|
+
TableField,
|
|
43
|
+
FileField,
|
|
44
|
+
RichTextField,
|
|
45
|
+
CustomField,
|
|
46
|
+
} from "./fields";
|
|
47
|
+
import {
|
|
48
|
+
Stepper,
|
|
49
|
+
Step,
|
|
50
|
+
Back,
|
|
51
|
+
Next,
|
|
52
|
+
StepperNav,
|
|
53
|
+
StepSlot,
|
|
54
|
+
useStepperState,
|
|
55
|
+
isStepElement,
|
|
56
|
+
isStepperElement,
|
|
57
|
+
} from "./stepper";
|
|
58
|
+
import { SubmitButton } from "./submit";
|
|
59
|
+
|
|
60
|
+
// ─── Section ─────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
export interface SectionProps {
|
|
63
|
+
title?: ReactNode;
|
|
64
|
+
color?: SectionColor;
|
|
65
|
+
icon?: ReactNode;
|
|
66
|
+
children: ReactNode;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** `FormBuilder.Section` — a titled Glare SectionBlock grouping fields. */
|
|
70
|
+
function Section({ title, color, icon, children }: SectionProps) {
|
|
71
|
+
return (
|
|
72
|
+
<SectionBlock title={title} color={color} icon={icon}>
|
|
73
|
+
{children}
|
|
74
|
+
</SectionBlock>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isHeaderElement(node: React.ReactNode): node is React.ReactElement {
|
|
79
|
+
return (
|
|
80
|
+
React.isValidElement(node) &&
|
|
81
|
+
(node.type as { __isFormHeader?: boolean })?.__isFormHeader === true
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Root ────────────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
function FormBuilderRoot<T extends FieldValues = FieldValues>({
|
|
88
|
+
children,
|
|
89
|
+
id,
|
|
90
|
+
form: formProp,
|
|
91
|
+
onSubmit,
|
|
92
|
+
onInvalid,
|
|
93
|
+
resolver,
|
|
94
|
+
defaultValues,
|
|
95
|
+
values,
|
|
96
|
+
loading = false,
|
|
97
|
+
mode = "edit",
|
|
98
|
+
fieldDirection,
|
|
99
|
+
resetOnSuccess,
|
|
100
|
+
conclusion,
|
|
101
|
+
className,
|
|
102
|
+
}: FormBuilderRootProps<T>) {
|
|
103
|
+
// Hooks can't be conditional, so always create one; `formProp` wins when given
|
|
104
|
+
// (the caller hoisted `useForm` to share values with something outside the form).
|
|
105
|
+
const ownForm = useForm<T>({ resolver, defaultValues, values });
|
|
106
|
+
const form = formProp ?? ownForm;
|
|
107
|
+
// Don't force a direction — left undefined, FieldSection uses its responsive `flexible`
|
|
108
|
+
// layout (stacked on small screens, two-column at `lg`). Only a caller-set `fieldDirection`
|
|
109
|
+
// (e.g. vertical inside a drawer) pins it.
|
|
110
|
+
const direction = fieldDirection;
|
|
111
|
+
|
|
112
|
+
const handleValid = async (v: T) => {
|
|
113
|
+
await onSubmit(v);
|
|
114
|
+
if (resetOnSuccess) form.reset();
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Split out a FormBuilder.Header (renders absolutely) and detect a Stepper child.
|
|
118
|
+
const childArray = React.Children.toArray(children);
|
|
119
|
+
const header = childArray.find(isHeaderElement);
|
|
120
|
+
const rest = childArray.filter((n) => !isHeaderElement(n));
|
|
121
|
+
|
|
122
|
+
const isView = mode === "view";
|
|
123
|
+
const stepperEl = rest.find(isStepperElement);
|
|
124
|
+
const stepChildren = stepperEl ? React.Children.toArray(stepperEl.props.children) : [];
|
|
125
|
+
const steps = stepChildren.filter(isStepElement);
|
|
126
|
+
const stepExtras = stepChildren.filter((n) => !isStepElement(n));
|
|
127
|
+
const isStepper = !!stepperEl && !isView;
|
|
128
|
+
|
|
129
|
+
// Stepper state is lifted HERE so the nav can live in its own grid column, outside the
|
|
130
|
+
// `<form>`. Called unconditionally (inert when there are no steps) to keep hooks order stable.
|
|
131
|
+
const stepper = useStepperState(steps, form.trigger as Parameters<typeof useStepperState>[1]);
|
|
132
|
+
|
|
133
|
+
// The stepper nav is its own column beside the fields, inside the form surface.
|
|
134
|
+
const nav = isStepper ? <StepperNav /> : null;
|
|
135
|
+
|
|
136
|
+
// The fields the `<form>` wraps: the stepper's steps (+ any custom footer extras like
|
|
137
|
+
// Back/Next), or the plain children. The Submit itself lives outside the form (see FormRenderer).
|
|
138
|
+
const fields = isStepper ? (
|
|
139
|
+
<>
|
|
140
|
+
{steps.map((step, i) => (
|
|
141
|
+
<StepSlot key={i} index={i} active={i === stepper.currentStep}>
|
|
142
|
+
{step.props.children}
|
|
143
|
+
</StepSlot>
|
|
144
|
+
))}
|
|
145
|
+
{stepExtras}
|
|
146
|
+
</>
|
|
147
|
+
) : (
|
|
148
|
+
rest
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// The fields column caps at 1200px and centers — as the middle column of the grid, and
|
|
152
|
+
// standalone.
|
|
153
|
+
const fieldsInner = (
|
|
154
|
+
<div className="mx-auto flex w-full max-w-[1100px] flex-col gap-4">{fields}</div>
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const formEl = isView ? (
|
|
158
|
+
<div className="w-full min-w-0">{fieldsInner}</div>
|
|
159
|
+
) : (
|
|
160
|
+
<form id={id} className="w-full min-w-0" onSubmit={form.handleSubmit(handleValid, onInvalid)}>
|
|
161
|
+
{fieldsInner}
|
|
162
|
+
</form>
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// Inside the form surface: the stepper rail beside the fields. Columns never wrap — the
|
|
166
|
+
// layout stays side-by-side at every screen size (the fields column shrinks instead).
|
|
167
|
+
const bodyInner = nav ? (
|
|
168
|
+
<div className="grid w-full grid-cols-[1fr_minmax(0,1100px)_1fr] gap-8">
|
|
169
|
+
{nav}
|
|
170
|
+
{formEl}
|
|
171
|
+
{/* Empty third column — balances the rail's gutter so the middle column is centred. */}
|
|
172
|
+
<div />
|
|
173
|
+
</div>
|
|
174
|
+
) : (
|
|
175
|
+
formEl
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const surface = header ? (
|
|
179
|
+
// Scroll shell: the absolute header floats over the scrollable body. No fixed height — it
|
|
180
|
+
// fills whatever the parent gives it (like the drawer's panel fills its tray) and the body
|
|
181
|
+
// scrolls internally.
|
|
182
|
+
<div className="relative isolate flex h-full w-full flex-col overflow-hidden rounded-2xl bg-background-presentation-body-primary">
|
|
183
|
+
{header}
|
|
184
|
+
<div className="relative z-[1] flex min-h-0 w-full flex-1 flex-col overflow-y-auto px-6 py-6 pt-[72px] scrollbar-hide">
|
|
185
|
+
{bodyInner}
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
) : (
|
|
189
|
+
bodyInner
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// The conclusion lives OUTSIDE the form surface — its own panel beside it, exactly like the
|
|
193
|
+
// drawer's tray (`FormDrawer` puts the conclusion next to the form panel with a 6px gutter).
|
|
194
|
+
// It never wraps below the form: the two stay side-by-side at every screen size.
|
|
195
|
+
const body = conclusion ? (
|
|
196
|
+
<div className="flex h-full flex-row items-stretch gap-[6px]">
|
|
197
|
+
<div className="min-h-0 min-w-0 flex-1">{surface}</div>
|
|
198
|
+
<div className="flex min-h-0">{conclusion}</div>
|
|
199
|
+
</div>
|
|
200
|
+
) : (
|
|
201
|
+
surface
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// `className` lands on the OUTERMOST element — the one a parent lays out (e.g. `flex-1 min-h-0`
|
|
205
|
+
// to fill a flex column). `h-full` fills a parent that has a definite height.
|
|
206
|
+
const outerClassName = cn("h-full w-full", className);
|
|
207
|
+
|
|
208
|
+
const tree = (
|
|
209
|
+
<Form {...form}>
|
|
210
|
+
<div className={outerClassName}>{body}</div>
|
|
211
|
+
</Form>
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<FormIdContext.Provider value={id}>
|
|
216
|
+
<LoadingContext.Provider value={loading}>
|
|
217
|
+
<ModeContext.Provider value={mode}>
|
|
218
|
+
<DirectionContext.Provider value={direction}>
|
|
219
|
+
{isStepper ? (
|
|
220
|
+
<StepperContext.Provider value={stepper}>{tree}</StepperContext.Provider>
|
|
221
|
+
) : (
|
|
222
|
+
tree
|
|
223
|
+
)}
|
|
224
|
+
</DirectionContext.Provider>
|
|
225
|
+
</ModeContext.Provider>
|
|
226
|
+
</LoadingContext.Provider>
|
|
227
|
+
</FormIdContext.Provider>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* FormBuilder — a compound, composition-based form. Author a form as JSX:
|
|
233
|
+
*
|
|
234
|
+
* ```tsx
|
|
235
|
+
* <FormBuilder onSubmit={fn} resolver={r} defaultValues={d}>
|
|
236
|
+
* <FormBuilder.Section title="Identity" color="Blue">
|
|
237
|
+
* <FormBuilder.Text name="name" label="Name" required />
|
|
238
|
+
* </FormBuilder.Section>
|
|
239
|
+
* <FormBuilder.Submit>Save</FormBuilder.Submit>
|
|
240
|
+
* </FormBuilder>
|
|
241
|
+
* ```
|
|
242
|
+
*
|
|
243
|
+
* Steps are components (`FormBuilder.Stepper` + `FormBuilder.Step`) and
|
|
244
|
+
* `mode="view"` renders read-only. FormBuilder is drawer-unaware — to show a form
|
|
245
|
+
* in a drawer, wrap it in `FormRenderer`'s `FormDrawer` (or use `FormRenderer`
|
|
246
|
+
* with `display: "drawer"`).
|
|
247
|
+
*/
|
|
248
|
+
export const FormBuilder = Object.assign(FormBuilderRoot, {
|
|
249
|
+
// fields
|
|
250
|
+
Text: TextField,
|
|
251
|
+
Email: EmailField,
|
|
252
|
+
Password: PasswordField,
|
|
253
|
+
Number: NumberField,
|
|
254
|
+
Currency: CurrencyField,
|
|
255
|
+
Textarea: TextareaField,
|
|
256
|
+
Select: SelectField,
|
|
257
|
+
SearchableSelect: SearchableSelectField,
|
|
258
|
+
MultiSelect: MultiSelectField,
|
|
259
|
+
Tags: MultiSelectField,
|
|
260
|
+
RadioList: RadioListField,
|
|
261
|
+
RadioCards: RadioCardsField,
|
|
262
|
+
Checkbox: CheckboxField,
|
|
263
|
+
CheckboxGroup: CheckboxGroupField,
|
|
264
|
+
SwitchBox: SwitchBoxField,
|
|
265
|
+
Otp: OtpField,
|
|
266
|
+
TreeSelect: TreeSelectField,
|
|
267
|
+
Slider: SliderField,
|
|
268
|
+
Color: ColorField,
|
|
269
|
+
Signature: SignatureField,
|
|
270
|
+
Phone: PhoneField,
|
|
271
|
+
FieldArray: FieldArray,
|
|
272
|
+
Table: TableField,
|
|
273
|
+
Date: DateField,
|
|
274
|
+
DateRange: (props: Parameters<typeof DateField>[0]) => <DateField {...props} mode="range" />,
|
|
275
|
+
DateMultiple: (props: Parameters<typeof DateField>[0]) => (
|
|
276
|
+
<DateField {...props} mode="multiple" />
|
|
277
|
+
),
|
|
278
|
+
DateTime: (props: Parameters<typeof DateField>[0]) => <DateField {...props} timePicker />,
|
|
279
|
+
File: FileField,
|
|
280
|
+
Image: (props: Parameters<typeof FileField>[0]) => <FileField {...props} image />,
|
|
281
|
+
RichText: RichTextField,
|
|
282
|
+
Custom: CustomField,
|
|
283
|
+
// layout
|
|
284
|
+
Section,
|
|
285
|
+
Header,
|
|
286
|
+
Submit: SubmitButton,
|
|
287
|
+
// stepper
|
|
288
|
+
Stepper,
|
|
289
|
+
Step,
|
|
290
|
+
Back,
|
|
291
|
+
Next,
|
|
292
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../utils/cn";
|
|
6
|
+
import { HeaderBar } from "../HeaderBar";
|
|
7
|
+
import { useMode } from "./context";
|
|
8
|
+
|
|
9
|
+
export type HeaderVariant = "new" | "edit" | "detail";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_LABEL: Record<HeaderVariant, string> = {
|
|
12
|
+
new: "New",
|
|
13
|
+
edit: "Edit",
|
|
14
|
+
detail: "View",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export interface FormHeaderBarProps {
|
|
18
|
+
/** Plain title text (uppercased), e.g. the entity label or SKU. */
|
|
19
|
+
title: string;
|
|
20
|
+
/** Badge text — defaults from `variant` (New / Edit / View). */
|
|
21
|
+
label?: string;
|
|
22
|
+
/** Colored badge variant. */
|
|
23
|
+
variant?: HeaderVariant;
|
|
24
|
+
/** Action buttons shown in the right-hand action pill. */
|
|
25
|
+
children?: ReactNode;
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The floating form header: a `HeaderBar` title pill on the left and a dark action
|
|
31
|
+
* pill on the right, absolutely positioned over the scrollable body.
|
|
32
|
+
*
|
|
33
|
+
* Shared by **both** form surfaces — `FormBuilder.Header` (page) and `FormDrawer`
|
|
34
|
+
* (drawer) — so a form's title looks identical wherever it is rendered.
|
|
35
|
+
*
|
|
36
|
+
* Per the design both pills are 44px tall, reached differently: the title pill is
|
|
37
|
+
* 6px padding + 32px content, the action pill 8px + 28px.
|
|
38
|
+
*/
|
|
39
|
+
export function FormHeaderBar({
|
|
40
|
+
title,
|
|
41
|
+
label,
|
|
42
|
+
variant = "new",
|
|
43
|
+
children,
|
|
44
|
+
className,
|
|
45
|
+
}: FormHeaderBarProps) {
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
className={cn(
|
|
49
|
+
"absolute inset-x-0 top-0 z-[2] flex w-full items-start justify-between gap-2 p-1",
|
|
50
|
+
className,
|
|
51
|
+
)}
|
|
52
|
+
>
|
|
53
|
+
<HeaderBar
|
|
54
|
+
variant={variant}
|
|
55
|
+
label={label ?? DEFAULT_LABEL[variant]}
|
|
56
|
+
title={title}
|
|
57
|
+
theme="dark"
|
|
58
|
+
/>
|
|
59
|
+
|
|
60
|
+
{children && (
|
|
61
|
+
<div
|
|
62
|
+
data-theme="dark"
|
|
63
|
+
className="flex flex-col items-start rounded-[14px] border border-border-presentation-global-primary bg-background-presentation-form-base shadow-[0_0_32px_0_rgba(0,0,0,0.15)]"
|
|
64
|
+
>
|
|
65
|
+
<div className="sticky top-0 flex w-full items-center justify-between p-2">
|
|
66
|
+
<div className="flex h-7 items-center justify-center gap-2">{children}</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface HeaderProps {
|
|
75
|
+
/** Plain title text (uppercased), e.g. the entity label or SKU. */
|
|
76
|
+
title: string;
|
|
77
|
+
/** Badge text — defaults from `variant` (New / Edit / View). */
|
|
78
|
+
label?: string;
|
|
79
|
+
/** Colored badge variant. Defaults from the form `mode` (view → detail). */
|
|
80
|
+
variant?: HeaderVariant;
|
|
81
|
+
/** Action buttons shown in the right-hand action pill (e.g. FormBuilder.Submit). */
|
|
82
|
+
children?: ReactNode;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* `FormBuilder.Header` — the page form's title + action bar. A thin, mode-aware
|
|
87
|
+
* wrapper over `FormHeaderBar`: it defaults the badge variant from the form `mode`
|
|
88
|
+
* (view → detail).
|
|
89
|
+
*
|
|
90
|
+
* Place it as a direct child of `<FormBuilder>`; the root then switches to the
|
|
91
|
+
* scroll-shell layout that reserves space beneath the floating header.
|
|
92
|
+
*/
|
|
93
|
+
export function Header({ title, label, variant, children }: HeaderProps) {
|
|
94
|
+
const mode = useMode();
|
|
95
|
+
const v: HeaderVariant = variant ?? (mode === "view" ? "detail" : "new");
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<FormHeaderBar title={title} label={label} variant={v}>
|
|
99
|
+
{children}
|
|
100
|
+
</FormHeaderBar>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Marker so the FormBuilder root can detect the header among its children.
|
|
105
|
+
(Header as unknown as { __isFormHeader: boolean }).__isFormHeader = true;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export { FormBuilder } from "./form-builder";
|
|
2
|
+
export type { SectionProps } from "./form-builder";
|
|
3
|
+
export { FormHeaderBar } from "./header";
|
|
4
|
+
export type { HeaderProps, HeaderVariant, FormHeaderBarProps } from "./header";
|
|
5
|
+
export { DisplayField } from "./DisplayField";
|
|
6
|
+
export type { DisplayFieldProps } from "./DisplayField";
|
|
7
|
+
export { RichTextField } from "../TextEditor/RichTextField";
|
|
8
|
+
export { formatFieldView } from "./viewFormat";
|
|
9
|
+
export type { FieldView, ViewKind, ViewFormatOptions } from "./viewFormat";
|
|
10
|
+
export type { StepProps } from "./stepper";
|
|
11
|
+
export type { FormBuilderMode, FieldDirection } from "./context";
|
|
12
|
+
export type {
|
|
13
|
+
BaseFieldProps,
|
|
14
|
+
OptionItem,
|
|
15
|
+
SelectFieldProps,
|
|
16
|
+
SearchableSelectFieldProps,
|
|
17
|
+
OptionsFieldProps,
|
|
18
|
+
CurrencyFieldProps,
|
|
19
|
+
FileFieldProps,
|
|
20
|
+
CustomFieldProps,
|
|
21
|
+
DateFieldProps,
|
|
22
|
+
OtpFieldProps,
|
|
23
|
+
RadioCardOption,
|
|
24
|
+
RadioCardsFieldProps,
|
|
25
|
+
SwitchBoxFieldProps,
|
|
26
|
+
CheckboxFieldProps,
|
|
27
|
+
TreeSelectFieldProps,
|
|
28
|
+
PasswordFieldProps,
|
|
29
|
+
SliderFieldProps,
|
|
30
|
+
ColorFieldProps,
|
|
31
|
+
SignatureFieldProps,
|
|
32
|
+
PhoneFieldProps,
|
|
33
|
+
FieldArrayProps,
|
|
34
|
+
TableColumn,
|
|
35
|
+
TableFieldProps,
|
|
36
|
+
FormBuilderRootProps,
|
|
37
|
+
} from "./types";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const nf = new Intl.NumberFormat("en-US", { maximumFractionDigits: 20 });
|
|
2
|
+
|
|
3
|
+
/** Format a number with thousands separators (e.g. 1299 → "1,299"). Blank for empty. */
|
|
4
|
+
export function formatNumber(value: unknown): string {
|
|
5
|
+
if (value == null || value === "") return "";
|
|
6
|
+
const n = typeof value === "number" ? value : Number(String(value).replace(/,/g, ""));
|
|
7
|
+
return Number.isNaN(n) ? "" : nf.format(n);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Parse a possibly comma-formatted string to a number (or undefined when blank). */
|
|
11
|
+
export function parseNumber(raw: string): number | undefined {
|
|
12
|
+
const cleaned = raw.replace(/,/g, "").trim();
|
|
13
|
+
if (cleaned === "" || cleaned === "-" || cleaned === ".") return undefined;
|
|
14
|
+
const n = Number(cleaned);
|
|
15
|
+
return Number.isNaN(n) ? undefined : n;
|
|
16
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useFormState, type FieldPath, type FieldValues } from "react-hook-form";
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../utils/cn";
|
|
7
|
+
import { Button } from "../Button";
|
|
8
|
+
import { FormStepper, FormStep, FormStepIndicator, FormStepLabel } from "../FormStepper";
|
|
9
|
+
import { StepContext, useStepper, type StepperContextValue, type StepRegistry } from "./context";
|
|
10
|
+
|
|
11
|
+
// ─── Step (declaration only — the Stepper reads its props) ───────────────────
|
|
12
|
+
|
|
13
|
+
export interface StepProps {
|
|
14
|
+
title: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Declares a wizard step. It renders nothing itself — `FormBuilder.Stepper`
|
|
21
|
+
* collects Step elements as an array and renders every step's fields (so the
|
|
22
|
+
* **whole form is registered**), toggling visibility per the active step.
|
|
23
|
+
*/
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- props are read by Stepper, not rendered here
|
|
25
|
+
export function Step(_props: StepProps) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
(Step as unknown as { __isFormStep: boolean }).__isFormStep = true;
|
|
29
|
+
|
|
30
|
+
export function isStepElement(node: React.ReactNode): node is React.ReactElement<StepProps> {
|
|
31
|
+
return (
|
|
32
|
+
React.isValidElement(node) && (node.type as { __isFormStep?: boolean })?.__isFormStep === true
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ─── StepSlot — mounts one step's fields; hidden when not active ─────────────
|
|
37
|
+
|
|
38
|
+
function StepSlot({
|
|
39
|
+
index,
|
|
40
|
+
active,
|
|
41
|
+
children,
|
|
42
|
+
}: {
|
|
43
|
+
index: number;
|
|
44
|
+
active: boolean;
|
|
45
|
+
children: React.ReactNode;
|
|
46
|
+
}) {
|
|
47
|
+
const stepper = useStepper();
|
|
48
|
+
const registry: StepRegistry = React.useMemo(
|
|
49
|
+
() => ({
|
|
50
|
+
register: (name) => {
|
|
51
|
+
(stepper.stepFields[index] ??= new Set()).add(name);
|
|
52
|
+
},
|
|
53
|
+
unregister: (name) => {
|
|
54
|
+
stepper.stepFields[index]?.delete(name);
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
[stepper, index],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Always mounted (fields stay registered); CSS hides inactive steps.
|
|
61
|
+
return (
|
|
62
|
+
<div className={cn("flex flex-col gap-4", !active && "hidden")}>
|
|
63
|
+
<StepContext.Provider value={registry}>{children}</StepContext.Provider>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Stepper navigation (style only) ─────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Vertical step rail (matches `Tabs.svg`): the numbered state badges stacked top
|
|
72
|
+
* to bottom, joined by a short vertical connector between consecutive steps.
|
|
73
|
+
*/
|
|
74
|
+
function StepperNav() {
|
|
75
|
+
const { titles, currentStep, goToStep, stepFields } = useStepper();
|
|
76
|
+
const { errors } = useFormState();
|
|
77
|
+
|
|
78
|
+
const stepHasError = (index: number) =>
|
|
79
|
+
[...(stepFields[index] ?? [])].some((name) => name in errors);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<FormStepper activeStep={currentStep} className="shrink-0 flex-col items-start gap-[4px]">
|
|
83
|
+
{titles.map((title, index) => {
|
|
84
|
+
// The step buttons ARE the navigation: click to move. Backward is free;
|
|
85
|
+
// clicking forward validates the steps in between (goToStep) and stops at
|
|
86
|
+
// the first one with errors. Errored steps show a red indicator.
|
|
87
|
+
const type = stepHasError(index) ? "negative" : index < currentStep ? "success" : "default";
|
|
88
|
+
return (
|
|
89
|
+
<React.Fragment key={title}>
|
|
90
|
+
<FormStep index={index} type={type} onClick={() => void goToStep(index)}>
|
|
91
|
+
<FormStepIndicator />
|
|
92
|
+
<FormStepLabel>{title}</FormStepLabel>
|
|
93
|
+
</FormStep>
|
|
94
|
+
{/* Connector between steps — a 3×16 rounded bar centred under the badge. */}
|
|
95
|
+
{index < titles.length - 1 && (
|
|
96
|
+
<div
|
|
97
|
+
aria-hidden
|
|
98
|
+
className="ms-[12.5px] mt-[2px] h-[16px] w-[3px] rounded-full bg-[#A0A0A0]"
|
|
99
|
+
/>
|
|
100
|
+
)}
|
|
101
|
+
</React.Fragment>
|
|
102
|
+
);
|
|
103
|
+
})}
|
|
104
|
+
</FormStepper>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Back / Next / default footer ────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
export function Back({ children }: { children?: React.ReactNode }) {
|
|
111
|
+
const { goToPrevious, isFirstStep } = useStepper();
|
|
112
|
+
return (
|
|
113
|
+
<Button type="button" variant="BorderStyle" disabled={isFirstStep} onClick={goToPrevious}>
|
|
114
|
+
{children ?? "Back"}
|
|
115
|
+
</Button>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function Next({ children }: { children?: React.ReactNode }) {
|
|
120
|
+
const { goToNext, isLastStep } = useStepper();
|
|
121
|
+
if (isLastStep) return null;
|
|
122
|
+
return (
|
|
123
|
+
<Button type="button" variant="PrimeStyle" onClick={() => void goToNext()}>
|
|
124
|
+
{children ?? "Next"}
|
|
125
|
+
</Button>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Stepper root (marker) + state hook ──────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
export interface StepperProps {
|
|
132
|
+
children: React.ReactNode;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* `FormBuilder.Stepper` — declares a wizard. It renders **nothing itself**: the
|
|
137
|
+
* FormBuilder root detects it, lifts its state (so the nav can live in its own
|
|
138
|
+
* grid column *outside* the `<form>`), and renders `[nav | active step's fields]`.
|
|
139
|
+
* Children are `FormBuilder.Step`s (plus an optional custom footer).
|
|
140
|
+
*
|
|
141
|
+
* Every step's fields stay registered with react-hook-form at all times — the
|
|
142
|
+
* stepper only controls which step is *visible*. **Navigation is the step buttons
|
|
143
|
+
* themselves**: click a step to move there. Backward is free; clicking forward
|
|
144
|
+
* validates every step in between and stops at the first with errors (which shows a
|
|
145
|
+
* red indicator). Only the last step shows the Submit button.
|
|
146
|
+
*/
|
|
147
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- children are read by the FormBuilder root
|
|
148
|
+
export function Stepper(_props: StepperProps) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
(Stepper as unknown as { __isFormStepper: boolean }).__isFormStepper = true;
|
|
152
|
+
|
|
153
|
+
export function isStepperElement(node: React.ReactNode): node is React.ReactElement<StepperProps> {
|
|
154
|
+
return (
|
|
155
|
+
React.isValidElement(node) &&
|
|
156
|
+
(node.type as { __isFormStepper?: boolean })?.__isFormStepper === true
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
type TriggerFn = (names?: FieldPath<FieldValues>[]) => Promise<boolean>;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Stepper state — lifted to the FormBuilder root so the nav (rail) and the step
|
|
164
|
+
* fields can render in separate grid columns while sharing one state. `trigger` is
|
|
165
|
+
* the form's `trigger` (passed in — no `useFormContext` needed). Inert when
|
|
166
|
+
* `steps` is empty (a form without a stepper still calls this, for hooks order).
|
|
167
|
+
*/
|
|
168
|
+
export function useStepperState(
|
|
169
|
+
steps: React.ReactElement<StepProps>[],
|
|
170
|
+
trigger: TriggerFn,
|
|
171
|
+
): StepperContextValue {
|
|
172
|
+
const [currentStep, setCurrentStep] = React.useState(0);
|
|
173
|
+
const stepFieldsRef = React.useRef<Record<number, Set<string>>>({});
|
|
174
|
+
const titles = steps.map((s) => s.props.title);
|
|
175
|
+
const lastIndex = steps.length - 1;
|
|
176
|
+
|
|
177
|
+
const validateStep = async (step: number) => {
|
|
178
|
+
const names = [...(stepFieldsRef.current[step] ?? [])] as FieldPath<FieldValues>[];
|
|
179
|
+
return names.length === 0 ? true : trigger(names);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Navigation runs through the step buttons. Backward is free; going forward
|
|
183
|
+
// validates every step between the current one and the target, and stops at
|
|
184
|
+
// the first step that has errors (so you can't skip past an invalid step).
|
|
185
|
+
const goToStep = async (index: number) => {
|
|
186
|
+
const target = Math.max(0, Math.min(index, Math.max(0, lastIndex)));
|
|
187
|
+
if (target <= currentStep) {
|
|
188
|
+
setCurrentStep(target);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
for (let s = currentStep; s < target; s++) {
|
|
192
|
+
if (!(await validateStep(s))) {
|
|
193
|
+
setCurrentStep(s);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
setCurrentStep(target);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const goToNext = async () => {
|
|
201
|
+
if (await validateStep(currentStep)) setCurrentStep((s) => Math.min(s + 1, lastIndex));
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
currentStep,
|
|
206
|
+
totalSteps: steps.length,
|
|
207
|
+
titles,
|
|
208
|
+
isFirstStep: currentStep === 0,
|
|
209
|
+
isLastStep: currentStep === lastIndex,
|
|
210
|
+
goToNext,
|
|
211
|
+
goToPrevious: () => setCurrentStep((s) => Math.max(s - 1, 0)),
|
|
212
|
+
goToStep,
|
|
213
|
+
stepFields: stepFieldsRef.current,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export { StepperNav, StepSlot };
|