torch-glare 2.4.2 → 2.4.4
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/apps/lib/components/FormBuilder/context.ts +2 -5
- package/apps/lib/components/FormBuilder/fields/ChoiceFields.tsx +2 -7
- package/apps/lib/components/FormBuilder/fields/ColorField.tsx +1 -18
- package/apps/lib/components/FormBuilder/fields/CustomField.tsx +1 -6
- package/apps/lib/components/FormBuilder/fields/DateField.tsx +1 -3
- package/apps/lib/components/FormBuilder/fields/FieldArray.tsx +10 -15
- package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +6 -32
- package/apps/lib/components/FormBuilder/fields/FileField.tsx +1 -2
- package/apps/lib/components/FormBuilder/fields/OptionListFields.tsx +2 -9
- package/apps/lib/components/FormBuilder/fields/OtpField.tsx +1 -2
- package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +24 -23
- package/apps/lib/components/FormBuilder/fields/RichTextEditorField.tsx +1 -7
- package/apps/lib/components/FormBuilder/fields/SelectField.tsx +3 -13
- package/apps/lib/components/FormBuilder/fields/SignatureField.tsx +1 -19
- package/apps/lib/components/FormBuilder/fields/SliderField.tsx +1 -6
- package/apps/lib/components/FormBuilder/fields/SwitchBoxField.tsx +1 -2
- package/apps/lib/components/FormBuilder/fields/TableField.tsx +22 -26
- package/apps/lib/components/FormBuilder/fields/TextField.tsx +5 -14
- package/apps/lib/components/FormBuilder/fields/TreeSelectField.tsx +1 -7
- package/apps/lib/components/FormBuilder/form-builder.tsx +22 -36
- package/apps/lib/components/FormBuilder/header.tsx +2 -6
- package/apps/lib/components/FormBuilder/index.ts +1 -5
- package/apps/lib/components/FormBuilder/stepper.tsx +88 -15
- package/apps/lib/components/FormBuilder/submit.tsx +1 -4
- package/apps/lib/components/FormBuilder/types.ts +2 -5
- package/apps/lib/components/FormRenderer/FormDrawer.tsx +9 -2
- package/apps/lib/components/FormRenderer/detail.tsx +207 -0
- package/apps/lib/components/FormRenderer/form-renderer.tsx +55 -14
- package/apps/lib/components/FormRenderer/index.ts +7 -5
- package/apps/lib/components/FormRenderer/types.ts +2 -3
- package/apps/lib/components/Popover.tsx +6 -2
- package/apps/lib/components/SearchableSelect.tsx +7 -2
- package/apps/lib/layouts/FieldSection.tsx +25 -22
- package/apps/lib/registry.json +1 -1
- package/apps/lib/tsconfig.tsbuildinfo +1 -1
- package/dist/src/shared/tailwindInit.d.ts.map +1 -1
- package/dist/src/shared/tailwindInit.js +3 -0
- package/dist/src/shared/tailwindInit.js.map +1 -1
- package/docs/components/form-builder.md +22 -29
- package/docs/components/form-renderer.md +72 -11
- package/docs/components/searchable-select.md +50 -46
- package/docs/how-to/forms-with-form-builder.md +68 -11
- package/docs/reference/tailwind-plugins.md +11 -1
- package/docs/tutorials/getting-started.md +9 -0
- package/package.json +1 -1
- package/apps/lib/components/FormBuilder/DisplayField.tsx +0 -40
- package/apps/lib/components/FormBuilder/viewFormat.tsx +0 -137
|
@@ -7,7 +7,6 @@ import { InputField } from "../../InputField";
|
|
|
7
7
|
import { Textarea } from "../../Textarea";
|
|
8
8
|
import { PasswordLevel } from "../../PasswordLevel";
|
|
9
9
|
import { useLoading, useCell } from "../context";
|
|
10
|
-
import { formatFieldView } from "../viewFormat";
|
|
11
10
|
import { formatNumber } from "../numberFormat";
|
|
12
11
|
import type { BaseFieldProps, CurrencyFieldProps, PasswordFieldProps } from "../types";
|
|
13
12
|
import { FieldShell } from "./FieldShell";
|
|
@@ -110,7 +109,7 @@ function TextLike({ props, type }: { props: BaseFieldProps; type: "text" | "emai
|
|
|
110
109
|
const loading = useLoading();
|
|
111
110
|
const cell = useCell();
|
|
112
111
|
return (
|
|
113
|
-
<FieldShell {...props}
|
|
112
|
+
<FieldShell {...props}>
|
|
114
113
|
{(field) => (
|
|
115
114
|
<InputField
|
|
116
115
|
{...field}
|
|
@@ -136,10 +135,7 @@ export function PasswordField(props: PasswordFieldProps) {
|
|
|
136
135
|
const loading = useLoading();
|
|
137
136
|
const cell = useCell();
|
|
138
137
|
return (
|
|
139
|
-
<FieldShell
|
|
140
|
-
{...props}
|
|
141
|
-
view={(v) => formatFieldView({ kind: "text", value: v ? "••••••••" : "" })}
|
|
142
|
-
>
|
|
138
|
+
<FieldShell {...props}>
|
|
143
139
|
{(field) => (
|
|
144
140
|
<div className="flex w-full flex-col gap-2">
|
|
145
141
|
<InputField
|
|
@@ -162,7 +158,7 @@ export function NumberField(props: BaseFieldProps) {
|
|
|
162
158
|
const loading = useLoading();
|
|
163
159
|
const cell = useCell();
|
|
164
160
|
return (
|
|
165
|
-
<FieldShell {...props}
|
|
161
|
+
<FieldShell {...props}>
|
|
166
162
|
{(field) => (
|
|
167
163
|
<NumberInput
|
|
168
164
|
field={field}
|
|
@@ -179,12 +175,7 @@ export function CurrencyField(props: CurrencyFieldProps) {
|
|
|
179
175
|
const loading = useLoading();
|
|
180
176
|
const cell = useCell();
|
|
181
177
|
return (
|
|
182
|
-
<FieldShell
|
|
183
|
-
{...props}
|
|
184
|
-
view={(v) =>
|
|
185
|
-
formatFieldView({ kind: "currency", value: v, currencySymbol: props.currencySymbol })
|
|
186
|
-
}
|
|
187
|
-
>
|
|
178
|
+
<FieldShell {...props}>
|
|
188
179
|
{(field) => (
|
|
189
180
|
<NumberInput
|
|
190
181
|
field={field}
|
|
@@ -205,7 +196,7 @@ export function CurrencyField(props: CurrencyFieldProps) {
|
|
|
205
196
|
export function TextareaField(props: BaseFieldProps & { rows?: number }) {
|
|
206
197
|
const loading = useLoading();
|
|
207
198
|
return (
|
|
208
|
-
<FieldShell {...props}
|
|
199
|
+
<FieldShell {...props}>
|
|
209
200
|
{(field, fieldState) => (
|
|
210
201
|
<Textarea
|
|
211
202
|
{...field}
|
|
@@ -25,13 +25,7 @@ export function TreeSelectField<T>(props: TreeSelectFieldProps<T>) {
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
return (
|
|
28
|
-
<FieldShell
|
|
29
|
-
{...props}
|
|
30
|
-
view={(v) => {
|
|
31
|
-
const node = findById(v);
|
|
32
|
-
return { value: node ? String(props.getNodeLabel(node)) : "" };
|
|
33
|
-
}}
|
|
34
|
-
>
|
|
28
|
+
<FieldShell {...props}>
|
|
35
29
|
{(field) => (
|
|
36
30
|
<SearchableTree
|
|
37
31
|
nodes={props.nodes}
|
|
@@ -7,13 +7,7 @@ import { useForm, type FieldValues } from "react-hook-form";
|
|
|
7
7
|
import { cn } from "../../utils/cn";
|
|
8
8
|
import { Form } from "../Form";
|
|
9
9
|
import { SectionBlock, type SectionColor } from "../SectionBlock";
|
|
10
|
-
import {
|
|
11
|
-
LoadingContext,
|
|
12
|
-
ModeContext,
|
|
13
|
-
DirectionContext,
|
|
14
|
-
StepperContext,
|
|
15
|
-
FormIdContext,
|
|
16
|
-
} from "./context";
|
|
10
|
+
import { LoadingContext, DirectionContext, StepperContext, FormIdContext } from "./context";
|
|
17
11
|
import { Header } from "./header";
|
|
18
12
|
import type { FormBuilderRootProps } from "./types";
|
|
19
13
|
import {
|
|
@@ -94,7 +88,6 @@ function FormBuilderRoot<T extends FieldValues = FieldValues>({
|
|
|
94
88
|
defaultValues,
|
|
95
89
|
values,
|
|
96
90
|
loading = false,
|
|
97
|
-
mode = "edit",
|
|
98
91
|
fieldDirection,
|
|
99
92
|
resetOnSuccess,
|
|
100
93
|
conclusion,
|
|
@@ -119,22 +112,21 @@ function FormBuilderRoot<T extends FieldValues = FieldValues>({
|
|
|
119
112
|
const header = childArray.find(isHeaderElement);
|
|
120
113
|
const rest = childArray.filter((n) => !isHeaderElement(n));
|
|
121
114
|
|
|
122
|
-
const isView = mode === "view";
|
|
123
115
|
const stepperEl = rest.find(isStepperElement);
|
|
124
116
|
const stepChildren = stepperEl ? React.Children.toArray(stepperEl.props.children) : [];
|
|
125
117
|
const steps = stepChildren.filter(isStepElement);
|
|
126
118
|
const stepExtras = stepChildren.filter((n) => !isStepElement(n));
|
|
127
|
-
const isStepper = !!stepperEl
|
|
119
|
+
const isStepper = !!stepperEl;
|
|
128
120
|
|
|
129
121
|
// Stepper state is lifted HERE so the nav can live in its own grid column, outside the
|
|
130
122
|
// `<form>`. Called unconditionally (inert when there are no steps) to keep hooks order stable.
|
|
131
123
|
const stepper = useStepperState(steps, form.trigger as Parameters<typeof useStepperState>[1]);
|
|
132
124
|
|
|
133
|
-
// The stepper nav is its own column beside the fields, inside the
|
|
125
|
+
// The stepper nav is its own grid column beside the fields, inside the scrolling body.
|
|
134
126
|
const nav = isStepper ? <StepperNav /> : null;
|
|
135
127
|
|
|
136
|
-
// The fields the `<form>` wraps: the stepper's steps (+ any custom footer extras like
|
|
137
|
-
//
|
|
128
|
+
// The fields the `<form>` wraps: the stepper's steps (+ any custom footer extras like Back/Next),
|
|
129
|
+
// or the plain children. The Submit itself lives outside the form (see FormRenderer).
|
|
138
130
|
const fields = isStepper ? (
|
|
139
131
|
<>
|
|
140
132
|
{steps.map((step, i) => (
|
|
@@ -151,12 +143,10 @@ function FormBuilderRoot<T extends FieldValues = FieldValues>({
|
|
|
151
143
|
// The fields column caps at 1200px and centers — as the middle column of the grid, and
|
|
152
144
|
// standalone.
|
|
153
145
|
const fieldsInner = (
|
|
154
|
-
<div className="mx-auto flex w-full max-w-[1100px] flex-col gap-4">{fields}</div>
|
|
146
|
+
<div className="mx-auto flex w-full max-w-[1100px] flex-col gap-4 px-[48px]">{fields}</div>
|
|
155
147
|
);
|
|
156
148
|
|
|
157
|
-
const formEl =
|
|
158
|
-
<div className="w-full min-w-0">{fieldsInner}</div>
|
|
159
|
-
) : (
|
|
149
|
+
const formEl = (
|
|
160
150
|
<form id={id} className="w-full min-w-0" onSubmit={form.handleSubmit(handleValid, onInvalid)}>
|
|
161
151
|
{fieldsInner}
|
|
162
152
|
</form>
|
|
@@ -189,13 +179,12 @@ function FormBuilderRoot<T extends FieldValues = FieldValues>({
|
|
|
189
179
|
bodyInner
|
|
190
180
|
);
|
|
191
181
|
|
|
192
|
-
// The conclusion lives OUTSIDE the
|
|
193
|
-
// drawer's tray
|
|
194
|
-
// It never wraps below the form: the two stay side-by-side at every screen size.
|
|
182
|
+
// The conclusion (right) lives OUTSIDE the scroll surface — its own panel beside it (mirroring the
|
|
183
|
+
// drawer's tray, a 6px gutter). Only the surface's body scrolls; the conclusion stays put.
|
|
195
184
|
const body = conclusion ? (
|
|
196
|
-
<div className="flex h-full flex-row items-stretch
|
|
185
|
+
<div className="flex h-full flex-row items-stretch">
|
|
197
186
|
<div className="min-h-0 min-w-0 flex-1">{surface}</div>
|
|
198
|
-
<div className="flex min-h-0">{conclusion}</div>
|
|
187
|
+
<div className="ml-[6px] flex min-h-0">{conclusion}</div>
|
|
199
188
|
</div>
|
|
200
189
|
) : (
|
|
201
190
|
surface
|
|
@@ -203,7 +192,7 @@ function FormBuilderRoot<T extends FieldValues = FieldValues>({
|
|
|
203
192
|
|
|
204
193
|
// `className` lands on the OUTERMOST element — the one a parent lays out (e.g. `flex-1 min-h-0`
|
|
205
194
|
// to fill a flex column). `h-full` fills a parent that has a definite height.
|
|
206
|
-
const outerClassName = cn("h-full w-full", className);
|
|
195
|
+
const outerClassName = cn("h-full w-full @container", className);
|
|
207
196
|
|
|
208
197
|
const tree = (
|
|
209
198
|
<Form {...form}>
|
|
@@ -214,15 +203,13 @@ function FormBuilderRoot<T extends FieldValues = FieldValues>({
|
|
|
214
203
|
return (
|
|
215
204
|
<FormIdContext.Provider value={id}>
|
|
216
205
|
<LoadingContext.Provider value={loading}>
|
|
217
|
-
<
|
|
218
|
-
|
|
219
|
-
{
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
</DirectionContext.Provider>
|
|
225
|
-
</ModeContext.Provider>
|
|
206
|
+
<DirectionContext.Provider value={direction}>
|
|
207
|
+
{isStepper ? (
|
|
208
|
+
<StepperContext.Provider value={stepper}>{tree}</StepperContext.Provider>
|
|
209
|
+
) : (
|
|
210
|
+
tree
|
|
211
|
+
)}
|
|
212
|
+
</DirectionContext.Provider>
|
|
226
213
|
</LoadingContext.Provider>
|
|
227
214
|
</FormIdContext.Provider>
|
|
228
215
|
);
|
|
@@ -240,10 +227,9 @@ function FormBuilderRoot<T extends FieldValues = FieldValues>({
|
|
|
240
227
|
* </FormBuilder>
|
|
241
228
|
* ```
|
|
242
229
|
*
|
|
243
|
-
* Steps are components (`FormBuilder.Stepper` + `FormBuilder.Step`)
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* with `display: "drawer"`).
|
|
230
|
+
* Steps are components (`FormBuilder.Stepper` + `FormBuilder.Step`). FormBuilder is
|
|
231
|
+
* drawer-unaware — to show a form in a drawer, wrap it in `FormRenderer`'s `FormDrawer`
|
|
232
|
+
* (or use `FormRenderer` with `display: "drawer"`).
|
|
247
233
|
*/
|
|
248
234
|
export const FormBuilder = Object.assign(FormBuilderRoot, {
|
|
249
235
|
// fields
|
|
@@ -4,7 +4,6 @@ import { ReactNode } from "react";
|
|
|
4
4
|
|
|
5
5
|
import { cn } from "../../utils/cn";
|
|
6
6
|
import { HeaderBar } from "../HeaderBar";
|
|
7
|
-
import { useMode } from "./context";
|
|
8
7
|
|
|
9
8
|
export type HeaderVariant = "new" | "edit" | "detail";
|
|
10
9
|
|
|
@@ -90,12 +89,9 @@ export interface HeaderProps {
|
|
|
90
89
|
* Place it as a direct child of `<FormBuilder>`; the root then switches to the
|
|
91
90
|
* scroll-shell layout that reserves space beneath the floating header.
|
|
92
91
|
*/
|
|
93
|
-
export function Header({ title, label, variant, children }: HeaderProps) {
|
|
94
|
-
const mode = useMode();
|
|
95
|
-
const v: HeaderVariant = variant ?? (mode === "view" ? "detail" : "new");
|
|
96
|
-
|
|
92
|
+
export function Header({ title, label, variant = "new", children }: HeaderProps) {
|
|
97
93
|
return (
|
|
98
|
-
<FormHeaderBar title={title} label={label} variant={
|
|
94
|
+
<FormHeaderBar title={title} label={label} variant={variant}>
|
|
99
95
|
{children}
|
|
100
96
|
</FormHeaderBar>
|
|
101
97
|
);
|
|
@@ -2,13 +2,9 @@ export { FormBuilder } from "./form-builder";
|
|
|
2
2
|
export type { SectionProps } from "./form-builder";
|
|
3
3
|
export { FormHeaderBar } from "./header";
|
|
4
4
|
export type { HeaderProps, HeaderVariant, FormHeaderBarProps } from "./header";
|
|
5
|
-
export { DisplayField } from "./DisplayField";
|
|
6
|
-
export type { DisplayFieldProps } from "./DisplayField";
|
|
7
5
|
export { RichTextField } from "../TextEditor/RichTextField";
|
|
8
|
-
export { formatFieldView } from "./viewFormat";
|
|
9
|
-
export type { FieldView, ViewKind, ViewFormatOptions } from "./viewFormat";
|
|
10
6
|
export type { StepProps } from "./stepper";
|
|
11
|
-
export type {
|
|
7
|
+
export type { FieldDirection } from "./context";
|
|
12
8
|
export type {
|
|
13
9
|
BaseFieldProps,
|
|
14
10
|
OptionItem,
|
|
@@ -6,7 +6,13 @@ import { useFormState, type FieldPath, type FieldValues } from "react-hook-form"
|
|
|
6
6
|
import { cn } from "../../utils/cn";
|
|
7
7
|
import { Button } from "../Button";
|
|
8
8
|
import { FormStepper, FormStep, FormStepIndicator, FormStepLabel } from "../FormStepper";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
StepContext,
|
|
11
|
+
StepperContext,
|
|
12
|
+
useStepper,
|
|
13
|
+
type StepperContextValue,
|
|
14
|
+
type StepRegistry,
|
|
15
|
+
} from "./context";
|
|
10
16
|
|
|
11
17
|
// ─── Step (declaration only — the Stepper reads its props) ───────────────────
|
|
12
18
|
|
|
@@ -72,7 +78,7 @@ function StepSlot({
|
|
|
72
78
|
* to bottom, joined by a short vertical connector between consecutive steps.
|
|
73
79
|
*/
|
|
74
80
|
function StepperNav() {
|
|
75
|
-
const { titles, currentStep, goToStep, stepFields } = useStepper();
|
|
81
|
+
const { titles, currentStep, goToStep, stepFields, completedSteps } = useStepper();
|
|
76
82
|
const { errors } = useFormState();
|
|
77
83
|
|
|
78
84
|
const stepHasError = (index: number) =>
|
|
@@ -83,8 +89,13 @@ function StepperNav() {
|
|
|
83
89
|
{titles.map((title, index) => {
|
|
84
90
|
// The step buttons ARE the navigation: click to move. Backward is free;
|
|
85
91
|
// clicking forward validates the steps in between (goToStep) and stops at
|
|
86
|
-
// the first one with errors.
|
|
87
|
-
|
|
92
|
+
// the first one with errors. A live error shows red; a step that has passed
|
|
93
|
+
// validation stays checked (success) even after navigating back to it.
|
|
94
|
+
const type = stepHasError(index)
|
|
95
|
+
? "negative"
|
|
96
|
+
: completedSteps.has(index)
|
|
97
|
+
? "success"
|
|
98
|
+
: "default";
|
|
88
99
|
return (
|
|
89
100
|
<React.Fragment key={title}>
|
|
90
101
|
<FormStep index={index} type={type} onClick={() => void goToStep(index)}>
|
|
@@ -105,24 +116,71 @@ function StepperNav() {
|
|
|
105
116
|
);
|
|
106
117
|
}
|
|
107
118
|
|
|
108
|
-
// ─── Back / Next
|
|
119
|
+
// ─── Back / Next chevron nav (the Figma header action bar) ───────────────────
|
|
109
120
|
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Shared chevron nav button, matching the Figma header action bar (Body-HeaderBar-1.0) — the
|
|
123
|
+
* Glare `Button` icon variant, the same control Select/SearchableSelect use for their chevrons.
|
|
124
|
+
*/
|
|
125
|
+
function StepNavButton({
|
|
126
|
+
dir,
|
|
127
|
+
onClick,
|
|
128
|
+
disabled,
|
|
129
|
+
}: {
|
|
130
|
+
dir: "left" | "right";
|
|
131
|
+
onClick: () => void;
|
|
132
|
+
disabled: boolean;
|
|
133
|
+
}) {
|
|
112
134
|
return (
|
|
113
|
-
<Button
|
|
114
|
-
|
|
135
|
+
<Button
|
|
136
|
+
type="button"
|
|
137
|
+
buttonType="icon"
|
|
138
|
+
size="M"
|
|
139
|
+
onClick={onClick}
|
|
140
|
+
disabled={disabled}
|
|
141
|
+
aria-label={dir === "left" ? "Previous step" : "Next step"}
|
|
142
|
+
>
|
|
143
|
+
<i
|
|
144
|
+
className={cn(
|
|
145
|
+
"text-[18px]",
|
|
146
|
+
dir === "left" ? "ri-arrow-left-s-line" : "ri-arrow-right-s-line",
|
|
147
|
+
)}
|
|
148
|
+
/>
|
|
115
149
|
</Button>
|
|
116
150
|
);
|
|
117
151
|
}
|
|
118
152
|
|
|
119
|
-
|
|
153
|
+
/** `FormBuilder.Back` — chevron to the previous step; disabled on the first. */
|
|
154
|
+
export function Back() {
|
|
155
|
+
const { goToPrevious, isFirstStep } = useStepper();
|
|
156
|
+
return <StepNavButton dir="left" onClick={goToPrevious} disabled={isFirstStep} />;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** `FormBuilder.Next` — chevron to the next step (validates first); disabled on the last. */
|
|
160
|
+
export function Next() {
|
|
120
161
|
const { goToNext, isLastStep } = useStepper();
|
|
121
|
-
|
|
162
|
+
return <StepNavButton dir="right" onClick={() => void goToNext()} disabled={isLastStep} />;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── Stepper action bar (Back/Next + divider, then the Submit) ────────────────
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* `FormRenderer` wraps its `actions` in this. When the form is a stepper it prepends the
|
|
169
|
+
* chevron `Back`/`Next` controls + a divider before the (user-provided) Submit — the Figma
|
|
170
|
+
* `Body-HeaderBar-1.0` layout. Outside a stepper there's no `StepperContext`, so it renders
|
|
171
|
+
* the actions untouched.
|
|
172
|
+
*/
|
|
173
|
+
export function StepperActions({ children }: { children?: React.ReactNode }) {
|
|
174
|
+
const stepper = React.useContext(StepperContext);
|
|
175
|
+
if (!stepper) return <>{children}</>;
|
|
122
176
|
return (
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
177
|
+
<div className="flex items-center gap-2">
|
|
178
|
+
<Back />
|
|
179
|
+
<Next />
|
|
180
|
+
{/* Divider — white-alpha hairline between the nav and the Submit. */}
|
|
181
|
+
<span aria-hidden className="mx-1 h-5 w-px rounded-[2px] bg-white-alpha-20" />
|
|
182
|
+
{children}
|
|
183
|
+
</div>
|
|
126
184
|
);
|
|
127
185
|
}
|
|
128
186
|
|
|
@@ -170,13 +228,27 @@ export function useStepperState(
|
|
|
170
228
|
trigger: TriggerFn,
|
|
171
229
|
): StepperContextValue {
|
|
172
230
|
const [currentStep, setCurrentStep] = React.useState(0);
|
|
231
|
+
// Steps that have passed their last validation — kept so their checkmark persists when the
|
|
232
|
+
// user navigates back to an earlier step.
|
|
233
|
+
const [completedSteps, setCompletedSteps] = React.useState<Set<number>>(new Set());
|
|
173
234
|
const stepFieldsRef = React.useRef<Record<number, Set<string>>>({});
|
|
174
235
|
const titles = steps.map((s) => s.props.title);
|
|
175
236
|
const lastIndex = steps.length - 1;
|
|
176
237
|
|
|
238
|
+
const markStep = (step: number, passed: boolean) =>
|
|
239
|
+
setCompletedSteps((prev) => {
|
|
240
|
+
if (passed === prev.has(step)) return prev; // no change
|
|
241
|
+
const next = new Set(prev);
|
|
242
|
+
if (passed) next.add(step);
|
|
243
|
+
else next.delete(step);
|
|
244
|
+
return next;
|
|
245
|
+
});
|
|
246
|
+
|
|
177
247
|
const validateStep = async (step: number) => {
|
|
178
248
|
const names = [...(stepFieldsRef.current[step] ?? [])] as FieldPath<FieldValues>[];
|
|
179
|
-
|
|
249
|
+
const passed = names.length === 0 ? true : await trigger(names);
|
|
250
|
+
markStep(step, passed);
|
|
251
|
+
return passed;
|
|
180
252
|
};
|
|
181
253
|
|
|
182
254
|
// Navigation runs through the step buttons. Backward is free; going forward
|
|
@@ -211,6 +283,7 @@ export function useStepperState(
|
|
|
211
283
|
goToPrevious: () => setCurrentStep((s) => Math.max(s - 1, 0)),
|
|
212
284
|
goToStep,
|
|
213
285
|
stepFields: stepFieldsRef.current,
|
|
286
|
+
completedSteps,
|
|
214
287
|
};
|
|
215
288
|
}
|
|
216
289
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { ReactNode } from "react";
|
|
4
4
|
|
|
5
5
|
import { Button } from "../Button";
|
|
6
|
-
import { useFormId, useLoading
|
|
6
|
+
import { useFormId, useLoading } from "./context";
|
|
7
7
|
|
|
8
8
|
export interface SubmitButtonProps {
|
|
9
9
|
children?: ReactNode;
|
|
@@ -21,9 +21,6 @@ export interface SubmitButtonProps {
|
|
|
21
21
|
export function SubmitButton({ children, className, loadingText, form }: SubmitButtonProps) {
|
|
22
22
|
const loading = useLoading();
|
|
23
23
|
const ctxFormId = useFormId();
|
|
24
|
-
const mode = useMode();
|
|
25
|
-
|
|
26
|
-
if (mode === "view") return null;
|
|
27
24
|
|
|
28
25
|
return (
|
|
29
26
|
<Button
|
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
Resolver,
|
|
9
9
|
UseFormReturn,
|
|
10
10
|
} from "react-hook-form";
|
|
11
|
-
import type { FieldDirection
|
|
11
|
+
import type { FieldDirection } from "./context";
|
|
12
12
|
import type { SectionColor } from "../SectionBlock";
|
|
13
13
|
|
|
14
14
|
/** Props shared by every `FormBuilder.*` field. `name` is the RHF path. */
|
|
@@ -69,13 +69,12 @@ export interface FileFieldProps extends BaseFieldProps {
|
|
|
69
69
|
multiple?: boolean;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/** `FormBuilder.Custom` — bring your own control +
|
|
72
|
+
/** `FormBuilder.Custom` — bring your own control, keeping the FieldSection + validation wiring. */
|
|
73
73
|
export interface CustomFieldProps extends BaseFieldProps {
|
|
74
74
|
render: (args: {
|
|
75
75
|
field: ControllerRenderProps<FieldValues, string>;
|
|
76
76
|
fieldState: ControllerFieldState;
|
|
77
77
|
}) => ReactNode;
|
|
78
|
-
formatView?: (value: unknown) => ReactNode;
|
|
79
78
|
}
|
|
80
79
|
|
|
81
80
|
/** `FormBuilder.Date` / `.DateRange` / `.DateMultiple` / `.DateTime`. */
|
|
@@ -248,8 +247,6 @@ export interface FormBuilderRootProps<T extends FieldValues = FieldValues> {
|
|
|
248
247
|
values?: T;
|
|
249
248
|
/** Loading flag — Submit shows a spinner and inputs disable. */
|
|
250
249
|
loading?: boolean;
|
|
251
|
-
/** `"edit"` (default) or read-only `"view"`. */
|
|
252
|
-
mode?: FormBuilderMode;
|
|
253
250
|
/** Field row direction. Defaults to horizontal (vertical inside a drawer). */
|
|
254
251
|
fieldDirection?: FieldDirection;
|
|
255
252
|
/** Reset to defaults after a successful submit. */
|
|
@@ -106,8 +106,15 @@ export function FormDrawer({
|
|
|
106
106
|
{actions}
|
|
107
107
|
</FormHeaderBar>
|
|
108
108
|
|
|
109
|
-
{/* pt-[72px] clears the 44px header pill (inset 4px) — same as the page shell.
|
|
110
|
-
|
|
109
|
+
{/* pt-[72px] clears the 44px header pill (inset 4px) — same as the page shell. The
|
|
110
|
+
48px bottom breathing-room goes on an inner wrapper, not the scroll container:
|
|
111
|
+
FormBuilder's outer element is `h-full`, which would pin it to the content box and
|
|
112
|
+
swallow the container's own `pb`. On a plain wrapper that `h-full` resolves to the
|
|
113
|
+
content height, so the padding actually lengthens the scroll. The conclusion panel
|
|
114
|
+
is a separate sibling, so it keeps its own spacing. */}
|
|
115
|
+
<div className="h-full overflow-y-auto px-3 pt-[72px]">
|
|
116
|
+
<div className="pb-[48px]">{children}</div>
|
|
117
|
+
</div>
|
|
111
118
|
</div>
|
|
112
119
|
</DrawerPanel>
|
|
113
120
|
|