torch-glare 2.4.2 → 2.4.3
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 +15 -28
- 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/form-renderer.tsx +9 -11
- package/apps/lib/components/FormRenderer/index.ts +1 -6
- package/apps/lib/components/FormRenderer/types.ts +0 -2
- 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 +19 -29
- package/docs/components/form-renderer.md +16 -10
- package/docs/components/searchable-select.md +50 -46
- package/docs/how-to/forms-with-form-builder.md +14 -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,12 +112,11 @@ 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.
|
|
@@ -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>
|
|
@@ -203,7 +193,7 @@ function FormBuilderRoot<T extends FieldValues = FieldValues>({
|
|
|
203
193
|
|
|
204
194
|
// `className` lands on the OUTERMOST element — the one a parent lays out (e.g. `flex-1 min-h-0`
|
|
205
195
|
// to fill a flex column). `h-full` fills a parent that has a definite height.
|
|
206
|
-
const outerClassName = cn("h-full w-full", className);
|
|
196
|
+
const outerClassName = cn("h-full w-full @container", className);
|
|
207
197
|
|
|
208
198
|
const tree = (
|
|
209
199
|
<Form {...form}>
|
|
@@ -214,15 +204,13 @@ function FormBuilderRoot<T extends FieldValues = FieldValues>({
|
|
|
214
204
|
return (
|
|
215
205
|
<FormIdContext.Provider value={id}>
|
|
216
206
|
<LoadingContext.Provider value={loading}>
|
|
217
|
-
<
|
|
218
|
-
|
|
219
|
-
{
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
</DirectionContext.Provider>
|
|
225
|
-
</ModeContext.Provider>
|
|
207
|
+
<DirectionContext.Provider value={direction}>
|
|
208
|
+
{isStepper ? (
|
|
209
|
+
<StepperContext.Provider value={stepper}>{tree}</StepperContext.Provider>
|
|
210
|
+
) : (
|
|
211
|
+
tree
|
|
212
|
+
)}
|
|
213
|
+
</DirectionContext.Provider>
|
|
226
214
|
</LoadingContext.Provider>
|
|
227
215
|
</FormIdContext.Provider>
|
|
228
216
|
);
|
|
@@ -240,10 +228,9 @@ function FormBuilderRoot<T extends FieldValues = FieldValues>({
|
|
|
240
228
|
* </FormBuilder>
|
|
241
229
|
* ```
|
|
242
230
|
*
|
|
243
|
-
* Steps are components (`FormBuilder.Stepper` + `FormBuilder.Step`)
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* with `display: "drawer"`).
|
|
231
|
+
* Steps are components (`FormBuilder.Stepper` + `FormBuilder.Step`). FormBuilder is
|
|
232
|
+
* drawer-unaware — to show a form in a drawer, wrap it in `FormRenderer`'s `FormDrawer`
|
|
233
|
+
* (or use `FormRenderer` with `display: "drawer"`).
|
|
247
234
|
*/
|
|
248
235
|
export const FormBuilder = Object.assign(FormBuilderRoot, {
|
|
249
236
|
// 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
|
|
|
@@ -4,7 +4,8 @@ import { useId } from "react";
|
|
|
4
4
|
import { FieldValues } from "react-hook-form";
|
|
5
5
|
|
|
6
6
|
import { FormBuilder } from "../FormBuilder";
|
|
7
|
-
import { FormIdContext, LoadingContext
|
|
7
|
+
import { FormIdContext, LoadingContext } from "../FormBuilder/context";
|
|
8
|
+
import { StepperActions } from "../FormBuilder/stepper";
|
|
8
9
|
import { FormDrawer } from "./FormDrawer";
|
|
9
10
|
import type { FormRendererProps } from "./types";
|
|
10
11
|
|
|
@@ -25,7 +26,6 @@ export function FormRenderer<T extends FieldValues = FieldValues>({
|
|
|
25
26
|
resolver,
|
|
26
27
|
defaultValues,
|
|
27
28
|
values,
|
|
28
|
-
mode = "edit",
|
|
29
29
|
loading,
|
|
30
30
|
resetOnSuccess,
|
|
31
31
|
fieldDirection,
|
|
@@ -61,7 +61,6 @@ export function FormRenderer<T extends FieldValues = FieldValues>({
|
|
|
61
61
|
resolver={resolver}
|
|
62
62
|
defaultValues={defaultValues}
|
|
63
63
|
values={values}
|
|
64
|
-
mode={mode}
|
|
65
64
|
loading={loading}
|
|
66
65
|
fieldDirection={effectiveDirection}
|
|
67
66
|
resetOnSuccess={resetOnSuccess}
|
|
@@ -73,7 +72,8 @@ export function FormRenderer<T extends FieldValues = FieldValues>({
|
|
|
73
72
|
>
|
|
74
73
|
{useHeader && (
|
|
75
74
|
<FormBuilder.Header title={header!.title} label={header!.label} variant={header!.variant}>
|
|
76
|
-
{
|
|
75
|
+
{/* A stepper form prepends chevron Back/Next + a divider before the Submit. */}
|
|
76
|
+
{actions && <StepperActions>{actions}</StepperActions>}
|
|
77
77
|
</FormBuilder.Header>
|
|
78
78
|
)}
|
|
79
79
|
|
|
@@ -90,15 +90,13 @@ export function FormRenderer<T extends FieldValues = FieldValues>({
|
|
|
90
90
|
badge={badge ?? header?.label}
|
|
91
91
|
variant={header?.variant}
|
|
92
92
|
summary={summary}
|
|
93
|
-
// The drawer header sits outside the `<form>`, so re-supply the form
|
|
94
|
-
// a bare `FormBuilder.Submit` reads (on the page it gets these from the
|
|
93
|
+
// The drawer header sits outside the `<form>`, so re-supply the form-id and loading
|
|
94
|
+
// context a bare `FormBuilder.Submit` reads (on the page it gets these from the tree).
|
|
95
95
|
actions={
|
|
96
96
|
actions ? (
|
|
97
|
-
<
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
</LoadingContext.Provider>
|
|
101
|
-
</ModeContext.Provider>
|
|
97
|
+
<LoadingContext.Provider value={!!loading}>
|
|
98
|
+
<FormIdContext.Provider value={formId}>{actions}</FormIdContext.Provider>
|
|
99
|
+
</LoadingContext.Provider>
|
|
102
100
|
) : undefined
|
|
103
101
|
}
|
|
104
102
|
onOpenInNewTab={onOpenInNewTab}
|
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
export { FormRenderer } from "./form-renderer";
|
|
2
2
|
export { FormDrawer } from "./FormDrawer";
|
|
3
3
|
export type { FormDrawerProps } from "./FormDrawer";
|
|
4
|
-
export type {
|
|
5
|
-
FormRendererProps,
|
|
6
|
-
FormRendererMode,
|
|
7
|
-
FormRendererDisplay,
|
|
8
|
-
FieldDirection,
|
|
9
|
-
} from "./types";
|
|
4
|
+
export type { FormRendererProps, FormRendererDisplay, FieldDirection } from "./types";
|
|
@@ -15,7 +15,6 @@ import type {
|
|
|
15
15
|
* form's header / drawer action bar) where you place the Save.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
export type FormRendererMode = "edit" | "view";
|
|
19
18
|
export type FormRendererDisplay = "page" | "drawer";
|
|
20
19
|
export type FieldDirection = "horizontal" | "vertical";
|
|
21
20
|
|
|
@@ -29,7 +28,6 @@ export interface FormRendererProps<T extends FieldValues = FieldValues> {
|
|
|
29
28
|
resolver?: Resolver<T>;
|
|
30
29
|
defaultValues?: DefaultValues<T>;
|
|
31
30
|
values?: T;
|
|
32
|
-
mode?: FormRendererMode;
|
|
33
31
|
loading?: boolean;
|
|
34
32
|
resetOnSuccess?: boolean;
|
|
35
33
|
/** Row layout; defaults to vertical inside a drawer. */
|
|
@@ -239,10 +239,14 @@ const popoverStyles = cva(
|
|
|
239
239
|
"bg-background-system-body-primary",
|
|
240
240
|
"shadow-[0px_0px_18px_0px_rgba(0,0,0,0.75)]",
|
|
241
241
|
],
|
|
242
|
+
// Adopts the DropdownMenu surface (`menuContentStyles`): backdrop-blurred, borderless
|
|
243
|
+
// rounded-14 panel with a soft ambient shadow — keeping the original background color.
|
|
242
244
|
PresentationStyle: [
|
|
243
|
-
"border-
|
|
245
|
+
"border-transparent",
|
|
246
|
+
"rounded-[14px]",
|
|
247
|
+
"backdrop-blur-[21px]",
|
|
244
248
|
"bg-background-presentation-form-base",
|
|
245
|
-
"shadow-[
|
|
249
|
+
"shadow-[0_0_32px_2px_rgba(0,0,0,0.20),0_0_48px_2px_rgba(0,0,0,0.05)]",
|
|
246
250
|
],
|
|
247
251
|
},
|
|
248
252
|
overlayBlur: {
|
|
@@ -24,6 +24,9 @@ export interface SearchableSelectOption {
|
|
|
24
24
|
value: string;
|
|
25
25
|
label: string;
|
|
26
26
|
icon?: ReactNode;
|
|
27
|
+
/** Text used for client-side filtering. Defaults to `label` — set it to match on a subset
|
|
28
|
+
* (e.g. a country name only, when the label also shows a dial code). */
|
|
29
|
+
searchText?: string;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
interface Props {
|
|
@@ -38,6 +41,7 @@ interface Props {
|
|
|
38
41
|
theme?: Themes;
|
|
39
42
|
dir?: string;
|
|
40
43
|
className?: string;
|
|
44
|
+
inputClassName?: string;
|
|
41
45
|
/** Transparent border/background so the trigger blends into a table cell. */
|
|
42
46
|
onTable?: boolean;
|
|
43
47
|
|
|
@@ -107,6 +111,7 @@ export function SearchableSelect({
|
|
|
107
111
|
theme,
|
|
108
112
|
dir,
|
|
109
113
|
className,
|
|
114
|
+
inputClassName,
|
|
110
115
|
onTable,
|
|
111
116
|
filterClientSide = true,
|
|
112
117
|
onSearchChange,
|
|
@@ -155,7 +160,7 @@ export function SearchableSelect({
|
|
|
155
160
|
if (!filterClientSide) return options;
|
|
156
161
|
const q = search.trim().toLowerCase();
|
|
157
162
|
if (!q) return options;
|
|
158
|
-
return options.filter((o) => o.label.toLowerCase().includes(q));
|
|
163
|
+
return options.filter((o) => (o.searchText ?? o.label).toLowerCase().includes(q));
|
|
159
164
|
}, [options, search, filterClientSide]);
|
|
160
165
|
|
|
161
166
|
const selectedOption = options.find((o) => o.value === value) ?? null;
|
|
@@ -201,7 +206,7 @@ export function SearchableSelect({
|
|
|
201
206
|
setOpen(true);
|
|
202
207
|
}}
|
|
203
208
|
onBlur={() => setSearching(false)}
|
|
204
|
-
className={cn("min-w-[100px] flex-1", {
|
|
209
|
+
className={cn("min-w-[100px] flex-1", inputClassName, {
|
|
205
210
|
"!h-[18px]": size === "XS",
|
|
206
211
|
"!h-[22px]": size === "S",
|
|
207
212
|
"!h-[24px]": size === "M",
|
|
@@ -31,32 +31,35 @@ export function FieldSection({
|
|
|
31
31
|
<section
|
|
32
32
|
{...props}
|
|
33
33
|
data-theme={theme}
|
|
34
|
-
className={cn(
|
|
35
|
-
"grid py-[16px] px-[12px] w-full max-w-[1200px] min-w-[0px] ",
|
|
36
|
-
direction === "vertical" && "grid-rows-[auto_1fr] gap-[12px]",
|
|
37
|
-
direction === "horizontal" && "grid-cols-[350px_1fr] gap-[24px]",
|
|
38
|
-
direction === "flexible" &&
|
|
39
|
-
"grid-rows-[auto_1fr] gap-[12px] lg:grid-cols-[350px_1fr] lg:grid-rows-[1fr] lg:gap-[24px]",
|
|
40
|
-
className,
|
|
41
|
-
)}
|
|
34
|
+
className={cn("w-full max-w-[1200px] min-w-[0px] @container", className)}
|
|
42
35
|
>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
labelDirections={"horizontal"}
|
|
51
|
-
/>
|
|
36
|
+
<div
|
|
37
|
+
className={cn(
|
|
38
|
+
"grid py-[16px] px-[12px] w-full min-w-[0px]",
|
|
39
|
+
direction === "vertical" && "grid-rows-[auto_1fr] gap-[12px]",
|
|
40
|
+
direction === "horizontal" && "grid-cols-[350px_1fr] gap-[24px]",
|
|
41
|
+
direction === "flexible" &&
|
|
42
|
+
"grid-rows-[auto_1fr] gap-[12px] @md:grid-cols-[350px_1fr] @md:grid-rows-[1fr] @md:gap-[24px]",
|
|
52
43
|
)}
|
|
44
|
+
>
|
|
45
|
+
{/* Fixed width section for labels */}
|
|
46
|
+
<div className="flex flex-col gap-[12px]">
|
|
47
|
+
{label && (
|
|
48
|
+
<Label
|
|
49
|
+
size={size}
|
|
50
|
+
label={label}
|
|
51
|
+
requiredLabel={requiredLabel}
|
|
52
|
+
labelDirections={"horizontal"}
|
|
53
|
+
/>
|
|
54
|
+
)}
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
{secondaryLabel && <Label size={size} secondaryLabel={secondaryLabel} />}
|
|
57
|
+
{childrenUnderLabel}
|
|
58
|
+
</div>
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
{/* Flexible section that takes up the remaining space */}
|
|
61
|
+
<div className="grid grid-cols-1 place-items-end gap-[12px]">{children}</div>
|
|
62
|
+
</div>
|
|
60
63
|
</section>
|
|
61
64
|
);
|
|
62
65
|
}
|
package/apps/lib/registry.json
CHANGED