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.
Files changed (47) hide show
  1. package/apps/lib/components/FormBuilder/context.ts +2 -5
  2. package/apps/lib/components/FormBuilder/fields/ChoiceFields.tsx +2 -7
  3. package/apps/lib/components/FormBuilder/fields/ColorField.tsx +1 -18
  4. package/apps/lib/components/FormBuilder/fields/CustomField.tsx +1 -6
  5. package/apps/lib/components/FormBuilder/fields/DateField.tsx +1 -3
  6. package/apps/lib/components/FormBuilder/fields/FieldArray.tsx +10 -15
  7. package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +6 -32
  8. package/apps/lib/components/FormBuilder/fields/FileField.tsx +1 -2
  9. package/apps/lib/components/FormBuilder/fields/OptionListFields.tsx +2 -9
  10. package/apps/lib/components/FormBuilder/fields/OtpField.tsx +1 -2
  11. package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +24 -23
  12. package/apps/lib/components/FormBuilder/fields/RichTextEditorField.tsx +1 -7
  13. package/apps/lib/components/FormBuilder/fields/SelectField.tsx +3 -13
  14. package/apps/lib/components/FormBuilder/fields/SignatureField.tsx +1 -19
  15. package/apps/lib/components/FormBuilder/fields/SliderField.tsx +1 -6
  16. package/apps/lib/components/FormBuilder/fields/SwitchBoxField.tsx +1 -2
  17. package/apps/lib/components/FormBuilder/fields/TableField.tsx +22 -26
  18. package/apps/lib/components/FormBuilder/fields/TextField.tsx +5 -14
  19. package/apps/lib/components/FormBuilder/fields/TreeSelectField.tsx +1 -7
  20. package/apps/lib/components/FormBuilder/form-builder.tsx +22 -36
  21. package/apps/lib/components/FormBuilder/header.tsx +2 -6
  22. package/apps/lib/components/FormBuilder/index.ts +1 -5
  23. package/apps/lib/components/FormBuilder/stepper.tsx +88 -15
  24. package/apps/lib/components/FormBuilder/submit.tsx +1 -4
  25. package/apps/lib/components/FormBuilder/types.ts +2 -5
  26. package/apps/lib/components/FormRenderer/FormDrawer.tsx +9 -2
  27. package/apps/lib/components/FormRenderer/detail.tsx +207 -0
  28. package/apps/lib/components/FormRenderer/form-renderer.tsx +55 -14
  29. package/apps/lib/components/FormRenderer/index.ts +7 -5
  30. package/apps/lib/components/FormRenderer/types.ts +2 -3
  31. package/apps/lib/components/Popover.tsx +6 -2
  32. package/apps/lib/components/SearchableSelect.tsx +7 -2
  33. package/apps/lib/layouts/FieldSection.tsx +25 -22
  34. package/apps/lib/registry.json +1 -1
  35. package/apps/lib/tsconfig.tsbuildinfo +1 -1
  36. package/dist/src/shared/tailwindInit.d.ts.map +1 -1
  37. package/dist/src/shared/tailwindInit.js +3 -0
  38. package/dist/src/shared/tailwindInit.js.map +1 -1
  39. package/docs/components/form-builder.md +22 -29
  40. package/docs/components/form-renderer.md +72 -11
  41. package/docs/components/searchable-select.md +50 -46
  42. package/docs/how-to/forms-with-form-builder.md +68 -11
  43. package/docs/reference/tailwind-plugins.md +11 -1
  44. package/docs/tutorials/getting-started.md +9 -0
  45. package/package.json +1 -1
  46. package/apps/lib/components/FormBuilder/DisplayField.tsx +0 -40
  47. 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} view={(v) => formatFieldView({ kind: "text", value: v })}>
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} view={(v) => formatFieldView({ kind: "number", value: v })}>
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} view={(v) => formatFieldView({ kind: "text", value: v })}>
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 && !isView;
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 form surface.
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
- // Back/Next), or the plain children. The Submit itself lives outside the form (see FormRenderer).
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 = isView ? (
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 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.
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 gap-[6px]">
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
- <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>
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`) 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"`).
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={v}>
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 { FormBuilderMode, FieldDirection } from "./context";
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 { StepContext, useStepper, type StepperContextValue, type StepRegistry } from "./context";
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. Errored steps show a red indicator.
87
- const type = stepHasError(index) ? "negative" : index < currentStep ? "success" : "default";
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 / default footer ────────────────────────────────────────────
119
+ // ─── Back / Next chevron nav (the Figma header action bar) ───────────────────
109
120
 
110
- export function Back({ children }: { children?: React.ReactNode }) {
111
- const { goToPrevious, isFirstStep } = useStepper();
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 type="button" variant="BorderStyle" disabled={isFirstStep} onClick={goToPrevious}>
114
- {children ?? "Back"}
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
- export function Next({ children }: { children?: React.ReactNode }) {
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
- if (isLastStep) return null;
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
- <Button type="button" variant="PrimeStyle" onClick={() => void goToNext()}>
124
- {children ?? "Next"}
125
- </Button>
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
- return names.length === 0 ? true : trigger(names);
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, useMode } from "./context";
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, FormBuilderMode } from "./context";
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 + optional read-only render. */
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
- <div className="h-full overflow-y-auto px-3 pb-[20px] pt-[72px]">{children}</div>
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