@datum-cloud/datum-ui 0.5.0 → 0.6.0-alpha.a49f238
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 +75 -40
- package/dist/adapter-context-BFqfq4Io.mjs +25 -0
- package/dist/components/features/form/adapter-context.d.ts +17 -0
- package/dist/components/features/form/adapter-context.d.ts.map +1 -0
- package/dist/components/features/form/adapter-types.d.ts +120 -0
- package/dist/components/features/form/adapter-types.d.ts.map +1 -0
- package/dist/components/features/form/adapters/conform/conform-adapter.d.ts +9 -0
- package/dist/components/features/form/adapters/conform/conform-adapter.d.ts.map +1 -0
- package/dist/components/features/form/adapters/conform/conform-provider.d.ts +22 -0
- package/dist/components/features/form/adapters/conform/conform-provider.d.ts.map +1 -0
- package/dist/components/features/form/adapters/conform/index.d.ts +3 -0
- package/dist/components/features/form/adapters/conform/index.d.ts.map +1 -0
- package/dist/components/features/form/adapters/rhf/index.d.ts +3 -0
- package/dist/components/features/form/adapters/rhf/index.d.ts.map +1 -0
- package/dist/components/features/form/adapters/rhf/rhf-adapter.d.ts +10 -0
- package/dist/components/features/form/adapters/rhf/rhf-adapter.d.ts.map +1 -0
- package/dist/components/features/form/adapters/rhf/rhf-provider.d.ts +22 -0
- package/dist/components/features/form/adapters/rhf/rhf-provider.d.ts.map +1 -0
- package/dist/components/features/form/components/form-autocomplete.d.ts.map +1 -1
- package/dist/components/features/form/components/form-checkbox.d.ts.map +1 -1
- package/dist/components/features/form/components/form-copy-box.d.ts.map +1 -1
- package/dist/components/features/form/components/form-custom.d.ts.map +1 -1
- package/dist/components/features/form/components/form-field-array.d.ts +5 -17
- package/dist/components/features/form/components/form-field-array.d.ts.map +1 -1
- package/dist/components/features/form/components/form-field.d.ts +7 -21
- package/dist/components/features/form/components/form-field.d.ts.map +1 -1
- package/dist/components/features/form/components/form-input-group.d.ts +4 -4
- package/dist/components/features/form/components/form-input-group.d.ts.map +1 -1
- package/dist/components/features/form/components/form-input.d.ts.map +1 -1
- package/dist/components/features/form/components/form-radio-group.d.ts.map +1 -1
- package/dist/components/features/form/components/form-root.d.ts +5 -25
- package/dist/components/features/form/components/form-root.d.ts.map +1 -1
- package/dist/components/features/form/components/form-select.d.ts.map +1 -1
- package/dist/components/features/form/components/form-switch.d.ts.map +1 -1
- package/dist/components/features/form/components/form-textarea.d.ts.map +1 -1
- package/dist/components/features/form/components/index.d.ts +0 -1
- package/dist/components/features/form/components/index.d.ts.map +1 -1
- package/dist/components/features/form/components/stepper/form-stepper.d.ts.map +1 -1
- package/dist/components/features/form/context/form-context.d.ts +2 -2
- package/dist/components/features/form/context/form-context.d.ts.map +1 -1
- package/dist/components/features/form/hooks/index.d.ts +1 -1
- package/dist/components/features/form/hooks/index.d.ts.map +1 -1
- package/dist/components/features/form/hooks/use-field.d.ts +12 -18
- package/dist/components/features/form/hooks/use-field.d.ts.map +1 -1
- package/dist/components/features/form/hooks/use-form-state.d.ts +36 -0
- package/dist/components/features/form/hooks/use-form-state.d.ts.map +1 -0
- package/dist/components/features/form/hooks/use-watch.d.ts +9 -20
- package/dist/components/features/form/hooks/use-watch.d.ts.map +1 -1
- package/dist/components/features/form/index.d.ts +63 -44
- package/dist/components/features/form/index.d.ts.map +1 -1
- package/dist/components/features/form/stepper/index.d.ts +17 -0
- package/dist/components/features/form/stepper/index.d.ts.map +1 -0
- package/dist/components/features/form/types/index.d.ts +68 -32
- package/dist/components/features/form/types/index.d.ts.map +1 -1
- package/dist/components/features/form/utils/get-field-constraints.d.ts +11 -0
- package/dist/components/features/form/utils/get-field-constraints.d.ts.map +1 -0
- package/dist/components/features/form/utils/get-schema-defaults.d.ts +24 -0
- package/dist/components/features/form/utils/get-schema-defaults.d.ts.map +1 -0
- package/dist/date-picker/index.mjs +1 -1
- package/dist/form/adapters/conform/index.mjs +320 -0
- package/dist/form/adapters/rhf/index.mjs +275 -0
- package/dist/form/index.mjs +3 -2
- package/dist/form/stepper/index.mjs +542 -0
- package/dist/form-C6AOB2f4.mjs +1397 -0
- package/dist/form-context-Ccxm-wqL.mjs +17 -0
- package/dist/get-field-constraints-D4xnXJEg.mjs +48 -0
- package/dist/grid/index.mjs +1 -1
- package/dist/hooks/index.mjs +2 -2
- package/dist/index.mjs +14 -13
- package/dist/input-number/index.mjs +1 -1
- package/dist/map/index.mjs +1 -1
- package/dist/{map-ClxB41Hg.mjs → map-BqpteT_8.mjs} +1 -1
- package/dist/more-actions/index.mjs +1 -1
- package/dist/page-title/index.mjs +1 -1
- package/dist/stepper/index.mjs +1 -320
- package/dist/stepper-C92Ib8Iy.mjs +321 -0
- package/dist/tag-input/index.mjs +1 -1
- package/dist/task-queue/index.mjs +1 -1
- package/package.json +27 -2
- package/dist/form-Co3fM4B7.mjs +0 -2114
- /package/dist/{col-q-J99UHe.mjs → col-CiSpQPUT.mjs} +0 -0
- /package/dist/{hooks-Cb7YlxN4.mjs → hooks-DNjmSsJT.mjs} +0 -0
- /package/dist/{input-number-mDB-5M5C.mjs → input-number-BTQdHqVZ.mjs} +0 -0
- /package/dist/{map-leaflet-imports-CaMm_rdF.mjs → map-leaflet-imports-CT4SpoDi.mjs} +0 -0
- /package/dist/{more-actions-CGagbIDT.mjs → more-actions-CucrYUnA.mjs} +0 -0
- /package/dist/{page-title-R7QbfbWp.mjs → page-title-CmsIi_A3.mjs} +0 -0
- /package/dist/{tag-input-BVSwNcRd.mjs → tag-input-B91C2wdr.mjs} +0 -0
- /package/dist/{task-queue-dropdown-DyM5R8KF.mjs → task-queue-dropdown-OOFuJcHb.mjs} +0 -0
- /package/dist/{to-api-format-BnbRFYQI.mjs → to-api-format-P0gmlqe8.mjs} +0 -0
- /package/dist/{use-copy-to-clipboard-BGdTmkFV.mjs → use-copy-to-clipboard-C2IEmhDn.mjs} +0 -0
|
@@ -0,0 +1,1397 @@
|
|
|
1
|
+
import { t as cn } from "./cn-D2KYQ917.mjs";
|
|
2
|
+
import { t as Button } from "./button-BllvE9Lm.mjs";
|
|
3
|
+
import { t as Icon } from "./icon-wrapper-DuLp3RM1.mjs";
|
|
4
|
+
import { t as Checkbox } from "./checkbox-I5BvrMPe.mjs";
|
|
5
|
+
import { t as Dialog } from "./dialog-Bm2H9lrx.mjs";
|
|
6
|
+
import { t as Input } from "./input-FKGqZypx.mjs";
|
|
7
|
+
import { t as Label } from "./label-cnAhY-ej.mjs";
|
|
8
|
+
import { n as RadioGroupItem, t as RadioGroup } from "./radio-group-CiITR0LO.mjs";
|
|
9
|
+
import { i as SelectItem, l as SelectTrigger, n as SelectContent, t as Select, u as SelectValue } from "./select-CiLR_DiQ.mjs";
|
|
10
|
+
import { t as Tooltip } from "./tooltip-Cruvl5F6.mjs";
|
|
11
|
+
import { t as Switch } from "./switch-DQJQhPIQ.mjs";
|
|
12
|
+
import { t as Textarea } from "./textarea-BwD-MmTV.mjs";
|
|
13
|
+
import { t as Autocomplete } from "./autocomplete-V5-qslzS.mjs";
|
|
14
|
+
import { t as toast } from "./toast-BWnN5fax.mjs";
|
|
15
|
+
import { n as useFormContext$1, t as FormProvider } from "./form-context-Ccxm-wqL.mjs";
|
|
16
|
+
import { t as useCopyToClipboard } from "./use-copy-to-clipboard-C2IEmhDn.mjs";
|
|
17
|
+
import { n as useAdapter } from "./adapter-context-BFqfq4Io.mjs";
|
|
18
|
+
import { InputWithAddons } from "./input-with-addons/index.mjs";
|
|
19
|
+
import { CheckIcon, CircleHelp, CopyIcon } from "lucide-react";
|
|
20
|
+
import * as React$1 from "react";
|
|
21
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
22
|
+
//#region src/components/features/form/context/field-context.tsx
|
|
23
|
+
const FieldContext = React$1.createContext(null);
|
|
24
|
+
function FieldProvider({ children, value }) {
|
|
25
|
+
return /* @__PURE__ */ jsx(FieldContext, {
|
|
26
|
+
value,
|
|
27
|
+
children
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function useFieldContext$1() {
|
|
31
|
+
const context = React$1.use(FieldContext);
|
|
32
|
+
if (!context) throw new Error("useFieldContext must be used within a Form.Field component. Make sure your input component is wrapped with Form.Field.");
|
|
33
|
+
return context;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Optional field context - returns null if not within a Form.Field
|
|
37
|
+
* Useful for components that can work both inside and outside Form.Field
|
|
38
|
+
*/
|
|
39
|
+
function useOptionalFieldContext() {
|
|
40
|
+
return React$1.use(FieldContext);
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/components/features/form/components/form-autocomplete.tsx
|
|
44
|
+
/**
|
|
45
|
+
* Form.Autocomplete - Searchable select component
|
|
46
|
+
*
|
|
47
|
+
* Automatically wired to the parent Form.Field context.
|
|
48
|
+
* Supports flat/grouped options, virtualization, custom rendering, and async search.
|
|
49
|
+
*
|
|
50
|
+
* @example Basic usage
|
|
51
|
+
* ```tsx
|
|
52
|
+
* <Form.Field name="timezone" label="Timezone" required>
|
|
53
|
+
* <Form.Autocomplete
|
|
54
|
+
* options={timezones}
|
|
55
|
+
* placeholder="Select timezone..."
|
|
56
|
+
* />
|
|
57
|
+
* </Form.Field>
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @example Async search
|
|
61
|
+
* ```tsx
|
|
62
|
+
* <Form.Field name="userId" label="User">
|
|
63
|
+
* <Form.Autocomplete
|
|
64
|
+
* options={users ?? []}
|
|
65
|
+
* onSearchChange={setSearch}
|
|
66
|
+
* loading={isLoading}
|
|
67
|
+
* placeholder="Search users..."
|
|
68
|
+
* />
|
|
69
|
+
* </Form.Field>
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @example Grouped options
|
|
73
|
+
* ```tsx
|
|
74
|
+
* <Form.Field name="role" label="Role" required>
|
|
75
|
+
* <Form.Autocomplete
|
|
76
|
+
* options={roleGroups}
|
|
77
|
+
* placeholder="Select a role..."
|
|
78
|
+
* />
|
|
79
|
+
* </Form.Field>
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
function FormAutocomplete({ disabled, className, ...props }) {
|
|
83
|
+
const { id, errors, disabled: fieldDisabled, fieldState } = useFieldContext$1();
|
|
84
|
+
const isDisabled = disabled ?? fieldDisabled;
|
|
85
|
+
const hasErrors = errors && errors.length > 0;
|
|
86
|
+
const value = fieldState?.value != null ? String(fieldState.value) : "";
|
|
87
|
+
return /* @__PURE__ */ jsx(Autocomplete, {
|
|
88
|
+
...props,
|
|
89
|
+
name: fieldState?.name,
|
|
90
|
+
id,
|
|
91
|
+
value,
|
|
92
|
+
onValueChange: (val) => fieldState?.change(val),
|
|
93
|
+
disabled: isDisabled,
|
|
94
|
+
triggerClassName: cn(hasErrors && "border-destructive", props.triggerClassName),
|
|
95
|
+
className
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
FormAutocomplete.displayName = "Form.Autocomplete";
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/components/features/form/components/form-button.tsx
|
|
101
|
+
/**
|
|
102
|
+
* Form.Button - A button for non-submit actions within a form
|
|
103
|
+
*
|
|
104
|
+
* Automatically gets disabled when the form is submitting.
|
|
105
|
+
* Use this for cancel buttons, reset buttons, or other actions.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```tsx
|
|
109
|
+
* <Form.Button onClick={() => navigate(-1)}>
|
|
110
|
+
* Cancel
|
|
111
|
+
* </Form.Button>
|
|
112
|
+
*
|
|
113
|
+
* <Form.Button onClick={() => form.reset()} type="secondary">
|
|
114
|
+
* Reset
|
|
115
|
+
* </Form.Button>
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
function FormButton({ children, onClick, type = "quaternary", theme = "borderless", size, disabled, className, disableOnSubmit = true }) {
|
|
119
|
+
const { isSubmitting } = useFormContext$1();
|
|
120
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
121
|
+
htmlType: "button",
|
|
122
|
+
type,
|
|
123
|
+
theme,
|
|
124
|
+
size,
|
|
125
|
+
disabled: disabled || disableOnSubmit && isSubmitting,
|
|
126
|
+
className,
|
|
127
|
+
onClick,
|
|
128
|
+
children
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
FormButton.displayName = "Form.Button";
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/components/features/form/components/form-checkbox.tsx
|
|
134
|
+
/**
|
|
135
|
+
* Form.Checkbox - Checkbox input component
|
|
136
|
+
*
|
|
137
|
+
* Automatically wired to the parent Form.Field context.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```tsx
|
|
141
|
+
* <Form.Field name="terms">
|
|
142
|
+
* <Form.Checkbox label="I agree to the terms and conditions" />
|
|
143
|
+
* </Form.Field>
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
function FormCheckbox({ label, disabled, className }) {
|
|
147
|
+
const { id, errors, disabled: fieldDisabled, fieldState } = useFieldContext$1();
|
|
148
|
+
const isDisabled = disabled ?? fieldDisabled;
|
|
149
|
+
const hasErrors = errors && errors.length > 0;
|
|
150
|
+
const checked = Boolean(fieldState?.value);
|
|
151
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
152
|
+
className: cn("flex items-center space-x-2", className),
|
|
153
|
+
children: [/* @__PURE__ */ jsx(Checkbox, {
|
|
154
|
+
id,
|
|
155
|
+
checked,
|
|
156
|
+
onCheckedChange: (value) => fieldState?.change(Boolean(value)),
|
|
157
|
+
disabled: isDisabled,
|
|
158
|
+
"aria-invalid": hasErrors || void 0,
|
|
159
|
+
"aria-describedby": hasErrors ? `${id}-error` : void 0
|
|
160
|
+
}), label && /* @__PURE__ */ jsx(Label, {
|
|
161
|
+
htmlFor: id,
|
|
162
|
+
className: cn("cursor-pointer text-sm font-normal", isDisabled && "cursor-not-allowed opacity-70"),
|
|
163
|
+
children: label
|
|
164
|
+
})]
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
FormCheckbox.displayName = "Form.Checkbox";
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/components/features/form/components/form-copy-box.tsx
|
|
170
|
+
/**
|
|
171
|
+
* Form.CopyBox - Read-only field with copy-to-clipboard functionality
|
|
172
|
+
*
|
|
173
|
+
* Displays field value in a read-only box with a copy button.
|
|
174
|
+
* Automatically gets value from Form.Field context.
|
|
175
|
+
*
|
|
176
|
+
* @example Basic usage
|
|
177
|
+
* ```tsx
|
|
178
|
+
* <Form.Field name="organizationId" label="Organization ID">
|
|
179
|
+
* <Form.CopyBox />
|
|
180
|
+
* </Form.Field>
|
|
181
|
+
* ```
|
|
182
|
+
*
|
|
183
|
+
* @example With icon-only button
|
|
184
|
+
* ```tsx
|
|
185
|
+
* <Form.Field name="apiKey" label="API Key">
|
|
186
|
+
* <Form.CopyBox variant="icon-only" />
|
|
187
|
+
* </Form.Field>
|
|
188
|
+
* ```
|
|
189
|
+
*
|
|
190
|
+
* @example With placeholder
|
|
191
|
+
* ```tsx
|
|
192
|
+
* <Form.Field name="webhookUrl" label="Webhook URL">
|
|
193
|
+
* <Form.CopyBox placeholder="Not configured" />
|
|
194
|
+
* </Form.Field>
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
function FormCopyBox({ variant = "default", className, contentClassName, buttonClassName, placeholder = "" }) {
|
|
198
|
+
const { fieldState } = useFieldContext$1();
|
|
199
|
+
const [copied, copy] = useCopyToClipboard();
|
|
200
|
+
const value = fieldState?.value != null ? String(fieldState.value) : "";
|
|
201
|
+
const displayValue = value || placeholder;
|
|
202
|
+
const copyToClipboard = () => {
|
|
203
|
+
if (!value) return;
|
|
204
|
+
copy(value).then(() => {
|
|
205
|
+
toast.success("Copied to clipboard");
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
209
|
+
className: cn("group border-input flex h-10 w-full overflow-hidden rounded-lg border bg-[#F6F6F580] text-xs focus-within:outline-hidden", className),
|
|
210
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
211
|
+
className: cn("flex w-full items-center overflow-hidden px-3 py-2 text-xs opacity-50", contentClassName),
|
|
212
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
213
|
+
className: "truncate",
|
|
214
|
+
children: displayValue
|
|
215
|
+
})
|
|
216
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
217
|
+
className: "flex items-center py-2 pr-3",
|
|
218
|
+
children: variant === "icon-only" ? /* @__PURE__ */ jsx("button", {
|
|
219
|
+
type: "button",
|
|
220
|
+
className: cn("text-muted-foreground hover:text-foreground flex size-7 items-center justify-center rounded-sm transition-colors", buttonClassName),
|
|
221
|
+
onClick: copyToClipboard,
|
|
222
|
+
children: copied ? /* @__PURE__ */ jsx(CheckIcon, { className: "size-4" }) : /* @__PURE__ */ jsx(CopyIcon, { className: "size-4" })
|
|
223
|
+
}) : /* @__PURE__ */ jsxs(Button, {
|
|
224
|
+
type: "quaternary",
|
|
225
|
+
theme: "outline",
|
|
226
|
+
size: "small",
|
|
227
|
+
className: cn("h-7 w-fit gap-1 px-2 text-xs", buttonClassName),
|
|
228
|
+
onClick: copyToClipboard,
|
|
229
|
+
children: [/* @__PURE__ */ jsx(CopyIcon, { className: "size-3!" }), copied ? "Copied" : "Copy"]
|
|
230
|
+
})
|
|
231
|
+
})]
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/components/features/form/components/form-custom.tsx
|
|
236
|
+
/**
|
|
237
|
+
* Form.Custom - Escape hatch for custom implementations
|
|
238
|
+
*
|
|
239
|
+
* Provides access to the underlying form context for complex use cases
|
|
240
|
+
* that don't fit the standard component patterns.
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```tsx
|
|
244
|
+
* <Form.Custom>
|
|
245
|
+
* {({ form, fields, submit, reset }) => (
|
|
246
|
+
* <MyCustomComponent
|
|
247
|
+
* fields={fields}
|
|
248
|
+
* onCustomAction={() => {
|
|
249
|
+
* // Do something custom
|
|
250
|
+
* submit();
|
|
251
|
+
* }}
|
|
252
|
+
* />
|
|
253
|
+
* )}
|
|
254
|
+
* </Form.Custom>
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
function FormCustom({ children }) {
|
|
258
|
+
const ctx = useFormContext$1();
|
|
259
|
+
return /* @__PURE__ */ jsx(Fragment$1, { children: children({
|
|
260
|
+
form: ctx.form,
|
|
261
|
+
fields: ctx.fields,
|
|
262
|
+
isSubmitting: ctx.isSubmitting,
|
|
263
|
+
isDirty: ctx.isDirty,
|
|
264
|
+
isValid: ctx.isValid,
|
|
265
|
+
isSubmitted: ctx.isSubmitted,
|
|
266
|
+
submitCount: ctx.submitCount,
|
|
267
|
+
dirtyFields: ctx.dirtyFields,
|
|
268
|
+
touchedFields: ctx.touchedFields,
|
|
269
|
+
submit: ctx.submit,
|
|
270
|
+
reset: ctx.reset
|
|
271
|
+
}) });
|
|
272
|
+
}
|
|
273
|
+
FormCustom.displayName = "Form.Custom";
|
|
274
|
+
//#endregion
|
|
275
|
+
//#region src/components/features/form/components/form-description.tsx
|
|
276
|
+
/**
|
|
277
|
+
* Form.Description - Display field description/helper text
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* ```tsx
|
|
281
|
+
* <Form.Field name="password">
|
|
282
|
+
* <Form.Input type="password" />
|
|
283
|
+
* <Form.Description>
|
|
284
|
+
* Must be at least 8 characters with one uppercase letter
|
|
285
|
+
* </Form.Description>
|
|
286
|
+
* </Form.Field>
|
|
287
|
+
* ```
|
|
288
|
+
*/
|
|
289
|
+
function FormDescription({ children, className }) {
|
|
290
|
+
const fieldContext = useOptionalFieldContext();
|
|
291
|
+
return /* @__PURE__ */ jsx("p", {
|
|
292
|
+
id: fieldContext ? `${fieldContext.id}-description` : void 0,
|
|
293
|
+
className: cn("text-muted-foreground text-xs text-wrap", className),
|
|
294
|
+
children
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
FormDescription.displayName = "Form.Description";
|
|
298
|
+
//#endregion
|
|
299
|
+
//#region src/components/features/form/components/form-dialog.tsx
|
|
300
|
+
/**
|
|
301
|
+
* Form.Dialog - A dialog with an integrated form
|
|
302
|
+
*
|
|
303
|
+
* Combines Dialog and Form.Root into a single component with:
|
|
304
|
+
* - Automatic dialog state management (controlled or uncontrolled)
|
|
305
|
+
* - Built-in header with title and description
|
|
306
|
+
* - Built-in footer with submit and cancel buttons
|
|
307
|
+
* - Auto-close on successful submission
|
|
308
|
+
* - Prevents accidental close during submission
|
|
309
|
+
* - Supports render function pattern for form state access
|
|
310
|
+
*
|
|
311
|
+
* @example Basic usage
|
|
312
|
+
* ```tsx
|
|
313
|
+
* <Form.Dialog
|
|
314
|
+
* title="Add User"
|
|
315
|
+
* description="Enter user details"
|
|
316
|
+
* schema={userSchema}
|
|
317
|
+
* onSubmit={handleSubmit}
|
|
318
|
+
* trigger={<Button>Add User</Button>}
|
|
319
|
+
* >
|
|
320
|
+
* <Form.Field name="name" label="Name" required>
|
|
321
|
+
* <Form.Input />
|
|
322
|
+
* </Form.Field>
|
|
323
|
+
* <Form.Field name="email" label="Email" required>
|
|
324
|
+
* <Form.Input type="email" />
|
|
325
|
+
* </Form.Field>
|
|
326
|
+
* </Form.Dialog>
|
|
327
|
+
* ```
|
|
328
|
+
*
|
|
329
|
+
* @example With render function for form state access
|
|
330
|
+
* ```tsx
|
|
331
|
+
* <Form.Dialog
|
|
332
|
+
* title="Edit User"
|
|
333
|
+
* schema={userSchema}
|
|
334
|
+
* defaultValues={user}
|
|
335
|
+
* onSubmit={handleSubmit}
|
|
336
|
+
* trigger={<Button>Edit</Button>}
|
|
337
|
+
* >
|
|
338
|
+
* {({ form, fields, isSubmitting, reset }) => (
|
|
339
|
+
* <>
|
|
340
|
+
* <Form.Field name="name" label="Name">
|
|
341
|
+
* <Form.Input />
|
|
342
|
+
* </Form.Field>
|
|
343
|
+
* <Button variant="ghost" onClick={reset} disabled={isSubmitting}>
|
|
344
|
+
* Reset
|
|
345
|
+
* </Button>
|
|
346
|
+
* </>
|
|
347
|
+
* )}
|
|
348
|
+
* </Form.Dialog>
|
|
349
|
+
* ```
|
|
350
|
+
*/
|
|
351
|
+
function FormDialog({ open, onOpenChange, defaultOpen, title, description, trigger, schema, defaultValues, onSubmit, onSuccess, onError, submitText = "Submit", submitTextLoading = "Submitting...", cancelText = "Cancel", showCancel = true, submitType = "primary", loading, formComponent, telemetry, className, formClassName, children }) {
|
|
352
|
+
const [internalOpen, setInternalOpen] = React$1.useState(defaultOpen ?? false);
|
|
353
|
+
const [internalIsSubmitting, setInternalIsSubmitting] = React$1.useState(false);
|
|
354
|
+
const isSubmitting = loading ?? internalIsSubmitting;
|
|
355
|
+
const isControlled = open !== void 0;
|
|
356
|
+
const isOpen = isControlled ? open : internalOpen;
|
|
357
|
+
const handleOpenChange = React$1.useCallback((value) => {
|
|
358
|
+
if (!value && isSubmitting) return;
|
|
359
|
+
if (!isControlled) setInternalOpen(value);
|
|
360
|
+
onOpenChange?.(value);
|
|
361
|
+
}, [
|
|
362
|
+
isControlled,
|
|
363
|
+
isSubmitting,
|
|
364
|
+
onOpenChange
|
|
365
|
+
]);
|
|
366
|
+
const handleSubmit = React$1.useCallback(async (data) => {
|
|
367
|
+
if (loading === void 0) setInternalIsSubmitting(true);
|
|
368
|
+
try {
|
|
369
|
+
await onSubmit?.(data);
|
|
370
|
+
onSuccess?.(data);
|
|
371
|
+
} catch (error) {
|
|
372
|
+
console.error("Form submission error:", error);
|
|
373
|
+
throw error;
|
|
374
|
+
} finally {
|
|
375
|
+
if (loading === void 0) setInternalIsSubmitting(false);
|
|
376
|
+
}
|
|
377
|
+
}, [
|
|
378
|
+
onSubmit,
|
|
379
|
+
onSuccess,
|
|
380
|
+
loading
|
|
381
|
+
]);
|
|
382
|
+
const handleCancel = React$1.useCallback(() => {
|
|
383
|
+
handleOpenChange(false);
|
|
384
|
+
}, [handleOpenChange]);
|
|
385
|
+
return /* @__PURE__ */ jsxs(Dialog, {
|
|
386
|
+
open: isOpen,
|
|
387
|
+
onOpenChange: handleOpenChange,
|
|
388
|
+
children: [trigger && /* @__PURE__ */ jsx(Dialog.Trigger, { children: trigger }), /* @__PURE__ */ jsx(Dialog.Content, {
|
|
389
|
+
className,
|
|
390
|
+
children: /* @__PURE__ */ jsx(Form.Root, {
|
|
391
|
+
schema,
|
|
392
|
+
defaultValues,
|
|
393
|
+
onSubmit: handleSubmit,
|
|
394
|
+
onError,
|
|
395
|
+
isSubmitting,
|
|
396
|
+
mode: "onSubmit",
|
|
397
|
+
formComponent,
|
|
398
|
+
telemetry,
|
|
399
|
+
className: cn("space-y-0", formClassName),
|
|
400
|
+
children: (renderProps) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
401
|
+
/* @__PURE__ */ jsx(Dialog.Header, {
|
|
402
|
+
title,
|
|
403
|
+
description,
|
|
404
|
+
onClose: handleCancel,
|
|
405
|
+
className: "border-b",
|
|
406
|
+
descriptionClassName: "text-foreground/80"
|
|
407
|
+
}),
|
|
408
|
+
/* @__PURE__ */ jsx(Dialog.Body, {
|
|
409
|
+
className: "space-y-0",
|
|
410
|
+
children: typeof children === "function" ? children(renderProps) : children
|
|
411
|
+
}),
|
|
412
|
+
/* @__PURE__ */ jsxs(Dialog.Footer, {
|
|
413
|
+
className: "border-t",
|
|
414
|
+
children: [showCancel && /* @__PURE__ */ jsx(Form.Button, {
|
|
415
|
+
type: "quaternary",
|
|
416
|
+
theme: "outline",
|
|
417
|
+
onClick: handleCancel,
|
|
418
|
+
disableOnSubmit: true,
|
|
419
|
+
children: cancelText
|
|
420
|
+
}), /* @__PURE__ */ jsx(Form.Submit, {
|
|
421
|
+
type: submitType,
|
|
422
|
+
children: isSubmitting ? submitTextLoading : submitText
|
|
423
|
+
})]
|
|
424
|
+
})
|
|
425
|
+
] })
|
|
426
|
+
})
|
|
427
|
+
})]
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
FormDialog.displayName = "Form.Dialog";
|
|
431
|
+
//#endregion
|
|
432
|
+
//#region src/components/features/form/components/form-error.tsx
|
|
433
|
+
/**
|
|
434
|
+
* Form.Error - Display field errors
|
|
435
|
+
*
|
|
436
|
+
* Can be used inside Form.Field to display errors automatically,
|
|
437
|
+
* or standalone with custom rendering.
|
|
438
|
+
*
|
|
439
|
+
* @example
|
|
440
|
+
* ```tsx
|
|
441
|
+
* // Inside Form.Field - displays field errors automatically
|
|
442
|
+
* <Form.Field name="email">
|
|
443
|
+
* <Form.Input />
|
|
444
|
+
* <Form.Error />
|
|
445
|
+
* </Form.Field>
|
|
446
|
+
*
|
|
447
|
+
* // Custom rendering
|
|
448
|
+
* <Form.Field name="email">
|
|
449
|
+
* <Form.Input />
|
|
450
|
+
* <Form.Error>
|
|
451
|
+
* {(errors) => errors.map(e => <span key={e}>{e}</span>)}
|
|
452
|
+
* </Form.Error>
|
|
453
|
+
* </Form.Field>
|
|
454
|
+
* ```
|
|
455
|
+
*/
|
|
456
|
+
function FormError({ children, className }) {
|
|
457
|
+
const errors = useOptionalFieldContext()?.errors;
|
|
458
|
+
if (!errors || errors.length === 0) return null;
|
|
459
|
+
if (typeof children === "function") return /* @__PURE__ */ jsx(Fragment$1, { children: children(errors) });
|
|
460
|
+
return /* @__PURE__ */ jsx("ul", {
|
|
461
|
+
className: cn("text-destructive space-y-1 text-sm font-medium", errors.length > 1 && "list-disc pl-4", className),
|
|
462
|
+
role: "alert",
|
|
463
|
+
"aria-live": "polite",
|
|
464
|
+
children: errors.map((error) => /* @__PURE__ */ jsx("li", {
|
|
465
|
+
className: "text-wrap",
|
|
466
|
+
children: error
|
|
467
|
+
}, error))
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
FormError.displayName = "Form.Error";
|
|
471
|
+
//#endregion
|
|
472
|
+
//#region src/components/features/form/components/form-field.tsx
|
|
473
|
+
function FieldLabel({ htmlFor, label, hasErrors, required, tooltip, className }) {
|
|
474
|
+
const [isTooltipVisible, setIsTooltipVisible] = React$1.useState(false);
|
|
475
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
476
|
+
className: "relative flex w-fit items-center space-x-2",
|
|
477
|
+
children: [/* @__PURE__ */ jsxs(Label, {
|
|
478
|
+
htmlFor,
|
|
479
|
+
className: cn("text-foreground/80 gap-0 text-xs font-semibold", hasErrors && "text-destructive", className),
|
|
480
|
+
children: [label, required && /* @__PURE__ */ jsx("span", {
|
|
481
|
+
className: "text-destructive/80 align-super text-sm leading-0",
|
|
482
|
+
"aria-hidden": "true",
|
|
483
|
+
children: "*"
|
|
484
|
+
})]
|
|
485
|
+
}), tooltip && /* @__PURE__ */ jsx(Tooltip, {
|
|
486
|
+
message: tooltip,
|
|
487
|
+
open: isTooltipVisible,
|
|
488
|
+
onOpenChange: setIsTooltipVisible,
|
|
489
|
+
side: "bottom",
|
|
490
|
+
contentClassName: "max-w-xs text-wrap",
|
|
491
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
492
|
+
icon: CircleHelp,
|
|
493
|
+
className: cn("text-ring absolute top-0.5 -right-3 size-3.5 cursor-pointer transition-opacity duration-400")
|
|
494
|
+
})
|
|
495
|
+
})]
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Form.Field - Field wrapper that provides label, errors, and description.
|
|
500
|
+
* Uses the active adapter to resolve field state by name.
|
|
501
|
+
*
|
|
502
|
+
* @example Standard usage
|
|
503
|
+
* ```tsx
|
|
504
|
+
* <Form.Field name="email" label="Email" required>
|
|
505
|
+
* <Form.Input type="email" />
|
|
506
|
+
* </Form.Field>
|
|
507
|
+
* ```
|
|
508
|
+
*
|
|
509
|
+
* @example Render function for custom components
|
|
510
|
+
* ```tsx
|
|
511
|
+
* <Form.Field name="role" label="Role">
|
|
512
|
+
* {({ control, meta }) => (
|
|
513
|
+
* <CustomSelect value={control.value} onChange={control.change} />
|
|
514
|
+
* )}
|
|
515
|
+
* </Form.Field>
|
|
516
|
+
* ```
|
|
517
|
+
*/
|
|
518
|
+
function FormField({ name, children, label, description, tooltip, required = false, disabled = false, className, labelClassName }) {
|
|
519
|
+
const adapter = useAdapter();
|
|
520
|
+
const { fields, isSubmitting, form } = useFormContext$1();
|
|
521
|
+
const fieldState = adapter.useField(name);
|
|
522
|
+
const errors = fieldState.errors;
|
|
523
|
+
const hasErrors = errors.length > 0;
|
|
524
|
+
const fieldId = fieldState.id;
|
|
525
|
+
const descriptionId = description ? `${fieldId}-description` : void 0;
|
|
526
|
+
const errorId = hasErrors ? `${fieldId}-error` : void 0;
|
|
527
|
+
const fieldRequired = required || fieldState.required;
|
|
528
|
+
const contextValue = React$1.useMemo(() => ({
|
|
529
|
+
name,
|
|
530
|
+
id: fieldId,
|
|
531
|
+
errors,
|
|
532
|
+
required: fieldRequired,
|
|
533
|
+
disabled,
|
|
534
|
+
fieldState
|
|
535
|
+
}), [
|
|
536
|
+
name,
|
|
537
|
+
fieldId,
|
|
538
|
+
errors,
|
|
539
|
+
fieldRequired,
|
|
540
|
+
disabled,
|
|
541
|
+
fieldState
|
|
542
|
+
]);
|
|
543
|
+
const isRenderFunction = typeof children === "function";
|
|
544
|
+
const renderContent = () => {
|
|
545
|
+
if (isRenderFunction) return children({
|
|
546
|
+
field: fieldState,
|
|
547
|
+
control: {
|
|
548
|
+
value: fieldState.value,
|
|
549
|
+
change: fieldState.change,
|
|
550
|
+
blur: fieldState.blur,
|
|
551
|
+
focus: fieldState.focus
|
|
552
|
+
},
|
|
553
|
+
meta: {
|
|
554
|
+
name,
|
|
555
|
+
id: fieldId,
|
|
556
|
+
errors,
|
|
557
|
+
required: fieldRequired,
|
|
558
|
+
disabled
|
|
559
|
+
},
|
|
560
|
+
fields,
|
|
561
|
+
form,
|
|
562
|
+
isSubmitting
|
|
563
|
+
});
|
|
564
|
+
return children;
|
|
565
|
+
};
|
|
566
|
+
return /* @__PURE__ */ jsx(FieldProvider, {
|
|
567
|
+
value: contextValue,
|
|
568
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
569
|
+
className: cn("flex flex-col space-y-2", className),
|
|
570
|
+
children: [
|
|
571
|
+
label && /* @__PURE__ */ jsx(FieldLabel, {
|
|
572
|
+
htmlFor: fieldId,
|
|
573
|
+
label,
|
|
574
|
+
hasErrors,
|
|
575
|
+
required: fieldRequired,
|
|
576
|
+
tooltip,
|
|
577
|
+
className: labelClassName
|
|
578
|
+
}),
|
|
579
|
+
renderContent(),
|
|
580
|
+
description && /* @__PURE__ */ jsx("p", {
|
|
581
|
+
id: descriptionId,
|
|
582
|
+
className: "text-ring text-xs text-wrap",
|
|
583
|
+
children: description
|
|
584
|
+
}),
|
|
585
|
+
hasErrors && /* @__PURE__ */ jsx("ul", {
|
|
586
|
+
id: errorId,
|
|
587
|
+
className: cn("text-destructive space-y-1 text-xs font-medium", errors.length > 1 && "list-disc pl-4"),
|
|
588
|
+
role: "alert",
|
|
589
|
+
"aria-live": "polite",
|
|
590
|
+
children: errors.map((error) => /* @__PURE__ */ jsx("li", {
|
|
591
|
+
className: "text-wrap",
|
|
592
|
+
children: error
|
|
593
|
+
}, error))
|
|
594
|
+
})
|
|
595
|
+
]
|
|
596
|
+
})
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
FormField.displayName = "Form.Field";
|
|
600
|
+
//#endregion
|
|
601
|
+
//#region src/components/features/form/components/form-field-array.tsx
|
|
602
|
+
/**
|
|
603
|
+
* Form.FieldArray - Dynamic array of fields with append/remove/move helpers.
|
|
604
|
+
*
|
|
605
|
+
* @example
|
|
606
|
+
* ```tsx
|
|
607
|
+
* <Form.FieldArray name="members">
|
|
608
|
+
* {({ fields, append, remove }) => (
|
|
609
|
+
* <>
|
|
610
|
+
* {fields.map((field, index) => (
|
|
611
|
+
* <div key={field.key}>
|
|
612
|
+
* <Form.Field name={`members.${index}.email`} label="Email">
|
|
613
|
+
* <Form.Input type="email" />
|
|
614
|
+
* </Form.Field>
|
|
615
|
+
* <button onClick={() => remove(index)}>Remove</button>
|
|
616
|
+
* </div>
|
|
617
|
+
* ))}
|
|
618
|
+
* <button onClick={() => append({})}>Add Member</button>
|
|
619
|
+
* </>
|
|
620
|
+
* )}
|
|
621
|
+
* </Form.FieldArray>
|
|
622
|
+
* ```
|
|
623
|
+
*/
|
|
624
|
+
function FormFieldArray({ name, children }) {
|
|
625
|
+
const fieldArray = useAdapter().useFieldArray(name);
|
|
626
|
+
return /* @__PURE__ */ jsx(Fragment$1, { children: children({
|
|
627
|
+
fields: fieldArray.items,
|
|
628
|
+
append: fieldArray.append,
|
|
629
|
+
remove: fieldArray.remove,
|
|
630
|
+
move: fieldArray.move
|
|
631
|
+
}) });
|
|
632
|
+
}
|
|
633
|
+
FormFieldArray.displayName = "Form.FieldArray";
|
|
634
|
+
//#endregion
|
|
635
|
+
//#region src/components/features/form/components/form-input.tsx
|
|
636
|
+
/**
|
|
637
|
+
* Form.Input - Text input component
|
|
638
|
+
*
|
|
639
|
+
* Automatically wired to the parent Form.Field context.
|
|
640
|
+
*
|
|
641
|
+
* @example
|
|
642
|
+
* ```tsx
|
|
643
|
+
* <Form.Field name="email" label="Email" required>
|
|
644
|
+
* <Form.Input type="email" placeholder="john@example.com" />
|
|
645
|
+
* </Form.Field>
|
|
646
|
+
* ```
|
|
647
|
+
*/
|
|
648
|
+
function FormInput({ ref, type = "text", className, disabled, ...props }) {
|
|
649
|
+
const { id, errors, disabled: fieldDisabled, fieldState } = useFieldContext$1();
|
|
650
|
+
const isDisabled = disabled ?? fieldDisabled;
|
|
651
|
+
const hasErrors = errors && errors.length > 0;
|
|
652
|
+
return /* @__PURE__ */ jsx(Input, {
|
|
653
|
+
...props,
|
|
654
|
+
ref,
|
|
655
|
+
id,
|
|
656
|
+
name: fieldState?.name,
|
|
657
|
+
type,
|
|
658
|
+
value: fieldState?.value ?? "",
|
|
659
|
+
onChange: (e) => fieldState?.change(e.target.value),
|
|
660
|
+
onBlur: () => fieldState?.blur(),
|
|
661
|
+
className: cn("!text-xs", className),
|
|
662
|
+
disabled: isDisabled,
|
|
663
|
+
"aria-invalid": hasErrors || void 0,
|
|
664
|
+
"aria-describedby": hasErrors ? `${id}-error` : void 0
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
FormInput.displayName = "Form.Input";
|
|
668
|
+
//#endregion
|
|
669
|
+
//#region src/components/features/form/components/form-radio-group.tsx
|
|
670
|
+
/**
|
|
671
|
+
* Form.RadioGroup - Radio button group component
|
|
672
|
+
*
|
|
673
|
+
* Automatically wired to the parent Form.Field context.
|
|
674
|
+
*
|
|
675
|
+
* @example
|
|
676
|
+
* ```tsx
|
|
677
|
+
* <Form.Field name="plan" label="Select Plan" required>
|
|
678
|
+
* <Form.RadioGroup orientation="vertical">
|
|
679
|
+
* <Form.RadioItem value="free" label="Free" description="Basic features" />
|
|
680
|
+
* <Form.RadioItem value="pro" label="Pro" description="Advanced features" />
|
|
681
|
+
* <Form.RadioItem value="enterprise" label="Enterprise" description="Custom solutions" />
|
|
682
|
+
* </Form.RadioGroup>
|
|
683
|
+
* </Form.Field>
|
|
684
|
+
* ```
|
|
685
|
+
*/
|
|
686
|
+
function FormRadioGroup({ orientation = "vertical", disabled, className, children }) {
|
|
687
|
+
const { id, errors, disabled: fieldDisabled, fieldState } = useFieldContext$1();
|
|
688
|
+
const isDisabled = disabled ?? fieldDisabled;
|
|
689
|
+
const hasErrors = errors && errors.length > 0;
|
|
690
|
+
return /* @__PURE__ */ jsx(RadioGroup, {
|
|
691
|
+
value: fieldState?.value != null ? String(fieldState.value) : void 0,
|
|
692
|
+
onValueChange: (val) => fieldState?.change(val),
|
|
693
|
+
disabled: isDisabled,
|
|
694
|
+
"aria-invalid": hasErrors || void 0,
|
|
695
|
+
"aria-describedby": hasErrors ? `${id}-error` : void 0,
|
|
696
|
+
className: cn(orientation === "horizontal" ? "flex flex-row space-x-4" : "flex flex-col space-y-2", className),
|
|
697
|
+
children
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
FormRadioGroup.displayName = "Form.RadioGroup";
|
|
701
|
+
/**
|
|
702
|
+
* Form.RadioItem - Individual radio button option
|
|
703
|
+
*
|
|
704
|
+
* @example
|
|
705
|
+
* ```tsx
|
|
706
|
+
* <Form.RadioItem value="option1" label="Option 1" />
|
|
707
|
+
* ```
|
|
708
|
+
*/
|
|
709
|
+
function FormRadioItem({ value, label, description, disabled }) {
|
|
710
|
+
const radioId = `radio-${value}`;
|
|
711
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
712
|
+
className: "flex items-start space-x-2",
|
|
713
|
+
children: [/* @__PURE__ */ jsx(RadioGroupItem, {
|
|
714
|
+
id: radioId,
|
|
715
|
+
value,
|
|
716
|
+
disabled,
|
|
717
|
+
className: "mt-1"
|
|
718
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
719
|
+
className: "flex flex-col",
|
|
720
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
721
|
+
htmlFor: radioId,
|
|
722
|
+
className: cn("cursor-pointer text-sm font-normal", disabled && "cursor-not-allowed opacity-70"),
|
|
723
|
+
children: label
|
|
724
|
+
}), description && /* @__PURE__ */ jsx("span", {
|
|
725
|
+
className: "text-muted-foreground text-xs",
|
|
726
|
+
children: description
|
|
727
|
+
})]
|
|
728
|
+
})]
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
FormRadioItem.displayName = "Form.RadioItem";
|
|
732
|
+
//#endregion
|
|
733
|
+
//#region src/components/features/form/components/form-root.tsx
|
|
734
|
+
/**
|
|
735
|
+
* Form.Root - Root form container that integrates with the active adapter.
|
|
736
|
+
*
|
|
737
|
+
* @example Standard usage
|
|
738
|
+
* ```tsx
|
|
739
|
+
* <Form.Root schema={schema} onSubmit={handleSubmit}>
|
|
740
|
+
* <Form.Field name="email" label="Email">
|
|
741
|
+
* <Form.Input type="email" />
|
|
742
|
+
* </Form.Field>
|
|
743
|
+
* <Form.Submit>Save</Form.Submit>
|
|
744
|
+
* </Form.Root>
|
|
745
|
+
* ```
|
|
746
|
+
*
|
|
747
|
+
* @example Render function for form state access
|
|
748
|
+
* ```tsx
|
|
749
|
+
* <Form.Root schema={schema}>
|
|
750
|
+
* {({ form, fields, isSubmitting }) => (
|
|
751
|
+
* <Form.Field name="email"><Form.Input /></Form.Field>
|
|
752
|
+
* )}
|
|
753
|
+
* </Form.Root>
|
|
754
|
+
* ```
|
|
755
|
+
*/
|
|
756
|
+
function FormRoot({ schema, children, onSubmit, action, method = "POST", formComponent: FormComp = "form", id, name, defaultValues, mode = "onBlur", isSubmitting: externalIsSubmitting, onError, onSuccess, telemetry, className }) {
|
|
757
|
+
const adapter = useAdapter();
|
|
758
|
+
const [internalIsSubmitting, setInternalIsSubmitting] = React$1.useState(false);
|
|
759
|
+
const isSubmitting = externalIsSubmitting ?? internalIsSubmitting;
|
|
760
|
+
const [isSubmitted, setIsSubmitted] = React$1.useState(false);
|
|
761
|
+
const [submitCount, setSubmitCount] = React$1.useState(0);
|
|
762
|
+
const formRef = React$1.useRef(null);
|
|
763
|
+
const wrappedOnSubmit = React$1.useCallback(async (data) => {
|
|
764
|
+
setInternalIsSubmitting(true);
|
|
765
|
+
setIsSubmitted(true);
|
|
766
|
+
setSubmitCount((prev) => prev + 1);
|
|
767
|
+
try {
|
|
768
|
+
await onSubmit?.(data);
|
|
769
|
+
telemetry?.onSuccess?.({
|
|
770
|
+
formName: name ?? "",
|
|
771
|
+
formId: id
|
|
772
|
+
});
|
|
773
|
+
onSuccess?.(data);
|
|
774
|
+
} catch (error) {
|
|
775
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
776
|
+
telemetry?.onError?.({
|
|
777
|
+
formName: name ?? "",
|
|
778
|
+
formId: id,
|
|
779
|
+
error: err
|
|
780
|
+
});
|
|
781
|
+
telemetry?.captureError?.(err, {
|
|
782
|
+
formName: name ?? "",
|
|
783
|
+
formId: id
|
|
784
|
+
});
|
|
785
|
+
onError?.(error);
|
|
786
|
+
} finally {
|
|
787
|
+
setInternalIsSubmitting(false);
|
|
788
|
+
}
|
|
789
|
+
}, [
|
|
790
|
+
onSubmit,
|
|
791
|
+
onSuccess,
|
|
792
|
+
onError,
|
|
793
|
+
telemetry,
|
|
794
|
+
name,
|
|
795
|
+
id
|
|
796
|
+
]);
|
|
797
|
+
const instance = adapter.useCreateForm({
|
|
798
|
+
schema,
|
|
799
|
+
defaultValues,
|
|
800
|
+
mode,
|
|
801
|
+
id,
|
|
802
|
+
onSubmit: onSubmit ? wrappedOnSubmit : void 0,
|
|
803
|
+
formRef
|
|
804
|
+
});
|
|
805
|
+
const { formState } = instance;
|
|
806
|
+
const contextValue = React$1.useMemo(() => ({
|
|
807
|
+
form: instance,
|
|
808
|
+
fields: instance.fields,
|
|
809
|
+
isSubmitting,
|
|
810
|
+
isDirty: formState.isDirty,
|
|
811
|
+
isValid: formState.isValid,
|
|
812
|
+
isSubmitted,
|
|
813
|
+
submitCount,
|
|
814
|
+
dirtyFields: formState.dirtyFields,
|
|
815
|
+
touchedFields: formState.touchedFields,
|
|
816
|
+
submit: () => formRef.current?.requestSubmit(),
|
|
817
|
+
reset: () => instance.reset(),
|
|
818
|
+
formId: instance.id
|
|
819
|
+
}), [
|
|
820
|
+
instance,
|
|
821
|
+
isSubmitting,
|
|
822
|
+
formState,
|
|
823
|
+
isSubmitted,
|
|
824
|
+
submitCount
|
|
825
|
+
]);
|
|
826
|
+
const isRenderFunction = typeof children === "function";
|
|
827
|
+
const renderProps = {
|
|
828
|
+
form: instance,
|
|
829
|
+
fields: instance.fields,
|
|
830
|
+
isSubmitting,
|
|
831
|
+
isDirty: formState.isDirty,
|
|
832
|
+
isValid: formState.isValid,
|
|
833
|
+
isSubmitted,
|
|
834
|
+
submitCount,
|
|
835
|
+
dirtyFields: formState.dirtyFields,
|
|
836
|
+
touchedFields: formState.touchedFields,
|
|
837
|
+
submit: () => formRef.current?.requestSubmit(),
|
|
838
|
+
reset: () => instance.reset()
|
|
839
|
+
};
|
|
840
|
+
const renderChildren = () => {
|
|
841
|
+
if (isRenderFunction) return children(renderProps);
|
|
842
|
+
return children;
|
|
843
|
+
};
|
|
844
|
+
return /* @__PURE__ */ jsx(FormProvider, {
|
|
845
|
+
value: contextValue,
|
|
846
|
+
children: /* @__PURE__ */ jsx(adapter.FormProvider, {
|
|
847
|
+
instance,
|
|
848
|
+
children: /* @__PURE__ */ jsx(FormComp, {
|
|
849
|
+
ref: formRef,
|
|
850
|
+
...instance.formProps,
|
|
851
|
+
method,
|
|
852
|
+
action,
|
|
853
|
+
className: cn("space-y-6", className),
|
|
854
|
+
autoComplete: "off",
|
|
855
|
+
noValidate: true,
|
|
856
|
+
onSubmit: (e) => {
|
|
857
|
+
e.stopPropagation();
|
|
858
|
+
telemetry?.onSubmit?.({
|
|
859
|
+
formName: name ?? "",
|
|
860
|
+
formId: id
|
|
861
|
+
});
|
|
862
|
+
const adapterSubmit = instance.formProps.onSubmit;
|
|
863
|
+
adapterSubmit?.(e);
|
|
864
|
+
},
|
|
865
|
+
children: renderChildren()
|
|
866
|
+
})
|
|
867
|
+
})
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
FormRoot.displayName = "Form.Root";
|
|
871
|
+
//#endregion
|
|
872
|
+
//#region src/components/features/form/components/form-select.tsx
|
|
873
|
+
/**
|
|
874
|
+
* Form.Select - Select dropdown component
|
|
875
|
+
*
|
|
876
|
+
* Automatically wired to the parent Form.Field context.
|
|
877
|
+
*
|
|
878
|
+
* @example
|
|
879
|
+
* ```tsx
|
|
880
|
+
* <Form.Field name="country" label="Country" required>
|
|
881
|
+
* <Form.Select placeholder="Select a country">
|
|
882
|
+
* <Form.SelectItem value="us">United States</Form.SelectItem>
|
|
883
|
+
* <Form.SelectItem value="uk">United Kingdom</Form.SelectItem>
|
|
884
|
+
* <Form.SelectItem value="ca">Canada</Form.SelectItem>
|
|
885
|
+
* </Form.Select>
|
|
886
|
+
* </Form.Field>
|
|
887
|
+
* ```
|
|
888
|
+
*/
|
|
889
|
+
function FormSelect({ placeholder, disabled, className, children }) {
|
|
890
|
+
const { id, errors, disabled: fieldDisabled, fieldState } = useFieldContext$1();
|
|
891
|
+
const isDisabled = disabled ?? fieldDisabled;
|
|
892
|
+
const hasErrors = errors && errors.length > 0;
|
|
893
|
+
return /* @__PURE__ */ jsxs(Select, {
|
|
894
|
+
value: fieldState?.value != null ? String(fieldState.value) : void 0,
|
|
895
|
+
onValueChange: (val) => fieldState?.change(val),
|
|
896
|
+
disabled: isDisabled,
|
|
897
|
+
children: [/* @__PURE__ */ jsx(SelectTrigger, {
|
|
898
|
+
id,
|
|
899
|
+
className: cn(className),
|
|
900
|
+
"aria-invalid": hasErrors || void 0,
|
|
901
|
+
"aria-describedby": hasErrors ? `${id}-error` : void 0,
|
|
902
|
+
onBlur: () => fieldState?.blur(),
|
|
903
|
+
children: /* @__PURE__ */ jsx(SelectValue, { placeholder })
|
|
904
|
+
}), /* @__PURE__ */ jsx(SelectContent, { children })]
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
FormSelect.displayName = "Form.Select";
|
|
908
|
+
/**
|
|
909
|
+
* Form.SelectItem - Individual select option
|
|
910
|
+
*
|
|
911
|
+
* @example
|
|
912
|
+
* ```tsx
|
|
913
|
+
* <Form.SelectItem value="option1">Option 1</Form.SelectItem>
|
|
914
|
+
* ```
|
|
915
|
+
*/
|
|
916
|
+
function FormSelectItem({ value, children, disabled }) {
|
|
917
|
+
return /* @__PURE__ */ jsx(SelectItem, {
|
|
918
|
+
value,
|
|
919
|
+
disabled,
|
|
920
|
+
children
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
FormSelectItem.displayName = "Form.SelectItem";
|
|
924
|
+
//#endregion
|
|
925
|
+
//#region src/components/features/form/components/form-submit.tsx
|
|
926
|
+
/**
|
|
927
|
+
* Form.Submit - Submit button with automatic loading state
|
|
928
|
+
*
|
|
929
|
+
* @example
|
|
930
|
+
* ```tsx
|
|
931
|
+
* <Form.Submit loadingText="Saving...">
|
|
932
|
+
* Save Changes
|
|
933
|
+
* </Form.Submit>
|
|
934
|
+
* ```
|
|
935
|
+
*/
|
|
936
|
+
function FormSubmit({ children, loadingText, loading = false, ...props }) {
|
|
937
|
+
const { isSubmitting } = useFormContext$1();
|
|
938
|
+
const isLoading = loading || isSubmitting;
|
|
939
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
940
|
+
htmlType: "submit",
|
|
941
|
+
disabled: props.disabled || isLoading,
|
|
942
|
+
loading: isLoading,
|
|
943
|
+
...props,
|
|
944
|
+
children: isLoading && loadingText ? loadingText : children
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
FormSubmit.displayName = "Form.Submit";
|
|
948
|
+
//#endregion
|
|
949
|
+
//#region src/components/features/form/components/form-switch.tsx
|
|
950
|
+
/**
|
|
951
|
+
* Form.Switch - Toggle switch component
|
|
952
|
+
*
|
|
953
|
+
* Automatically wired to the parent Form.Field context.
|
|
954
|
+
*
|
|
955
|
+
* @example
|
|
956
|
+
* ```tsx
|
|
957
|
+
* <Form.Field name="notifications">
|
|
958
|
+
* <Form.Switch label="Enable email notifications" />
|
|
959
|
+
* </Form.Field>
|
|
960
|
+
* ```
|
|
961
|
+
*/
|
|
962
|
+
function FormSwitch({ label, disabled, className }) {
|
|
963
|
+
const { id, errors, disabled: fieldDisabled, fieldState } = useFieldContext$1();
|
|
964
|
+
const isDisabled = disabled ?? fieldDisabled;
|
|
965
|
+
const hasErrors = errors && errors.length > 0;
|
|
966
|
+
const checked = Boolean(fieldState?.value);
|
|
967
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
968
|
+
className: cn("flex items-center space-x-2", className),
|
|
969
|
+
children: [/* @__PURE__ */ jsx(Switch, {
|
|
970
|
+
id,
|
|
971
|
+
checked,
|
|
972
|
+
onCheckedChange: (value) => fieldState?.change(Boolean(value)),
|
|
973
|
+
disabled: isDisabled,
|
|
974
|
+
"aria-invalid": hasErrors || void 0,
|
|
975
|
+
"aria-describedby": hasErrors ? `${id}-error` : void 0
|
|
976
|
+
}), label && /* @__PURE__ */ jsx(Label, {
|
|
977
|
+
htmlFor: id,
|
|
978
|
+
className: cn("cursor-pointer text-sm font-normal", isDisabled && "cursor-not-allowed opacity-70"),
|
|
979
|
+
children: label
|
|
980
|
+
})]
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
FormSwitch.displayName = "Form.Switch";
|
|
984
|
+
//#endregion
|
|
985
|
+
//#region src/components/features/form/components/form-textarea.tsx
|
|
986
|
+
/**
|
|
987
|
+
* Form.Textarea - Multi-line text input component
|
|
988
|
+
*
|
|
989
|
+
* Automatically wired to the parent Form.Field context.
|
|
990
|
+
*
|
|
991
|
+
* @example
|
|
992
|
+
* ```tsx
|
|
993
|
+
* <Form.Field name="bio" label="Bio">
|
|
994
|
+
* <Form.Textarea rows={4} placeholder="Tell us about yourself..." />
|
|
995
|
+
* </Form.Field>
|
|
996
|
+
* ```
|
|
997
|
+
*/
|
|
998
|
+
function FormTextarea({ ref, className, disabled, rows = 3, ...props }) {
|
|
999
|
+
const { id, errors, disabled: fieldDisabled, fieldState } = useFieldContext$1();
|
|
1000
|
+
const isDisabled = disabled ?? fieldDisabled;
|
|
1001
|
+
const hasErrors = errors && errors.length > 0;
|
|
1002
|
+
return /* @__PURE__ */ jsx(Textarea, {
|
|
1003
|
+
...props,
|
|
1004
|
+
ref,
|
|
1005
|
+
id,
|
|
1006
|
+
name: fieldState?.name,
|
|
1007
|
+
value: fieldState?.value ?? "",
|
|
1008
|
+
onChange: (e) => fieldState?.change(e.target.value),
|
|
1009
|
+
onBlur: () => fieldState?.blur(),
|
|
1010
|
+
rows,
|
|
1011
|
+
className: cn(className),
|
|
1012
|
+
disabled: isDisabled,
|
|
1013
|
+
"aria-invalid": hasErrors || void 0,
|
|
1014
|
+
"aria-describedby": hasErrors ? `${id}-error` : void 0
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
FormTextarea.displayName = "Form.Textarea";
|
|
1018
|
+
//#endregion
|
|
1019
|
+
//#region src/components/features/form/hooks/use-watch.ts
|
|
1020
|
+
/**
|
|
1021
|
+
* Hook to watch a field's value reactively.
|
|
1022
|
+
* Delegates to the active adapter's useWatch implementation.
|
|
1023
|
+
*
|
|
1024
|
+
* @example
|
|
1025
|
+
* ```tsx
|
|
1026
|
+
* function ConditionalField() {
|
|
1027
|
+
* const contactMethod = useWatch('contactMethod')
|
|
1028
|
+
* if (contactMethod === 'email') {
|
|
1029
|
+
* return <Form.Field name="email"><Form.Input type="email" /></Form.Field>
|
|
1030
|
+
* }
|
|
1031
|
+
* return null
|
|
1032
|
+
* }
|
|
1033
|
+
* ```
|
|
1034
|
+
*/
|
|
1035
|
+
function useWatch(name) {
|
|
1036
|
+
return useAdapter().useWatch(name);
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Hook to watch multiple fields at once.
|
|
1040
|
+
* Delegates to the active adapter's useWatchAll implementation.
|
|
1041
|
+
*
|
|
1042
|
+
* @example
|
|
1043
|
+
* ```tsx
|
|
1044
|
+
* function Summary() {
|
|
1045
|
+
* const values = useWatchAll(['firstName', 'lastName', 'email'])
|
|
1046
|
+
* return <div>Name: {values.firstName} {values.lastName}</div>
|
|
1047
|
+
* }
|
|
1048
|
+
* ```
|
|
1049
|
+
*/
|
|
1050
|
+
function useWatchAll(names) {
|
|
1051
|
+
return useAdapter().useWatchAll(names);
|
|
1052
|
+
}
|
|
1053
|
+
//#endregion
|
|
1054
|
+
//#region src/components/features/form/components/form-when.tsx
|
|
1055
|
+
/**
|
|
1056
|
+
* Form.When - Conditional rendering based on field values
|
|
1057
|
+
*
|
|
1058
|
+
* Renders children only when the specified field matches the condition.
|
|
1059
|
+
*
|
|
1060
|
+
* @example
|
|
1061
|
+
* ```tsx
|
|
1062
|
+
* // Render when field equals value
|
|
1063
|
+
* <Form.When field="contactMethod" is="email">
|
|
1064
|
+
* <Form.Field name="email"><Form.Input type="email" /></Form.Field>
|
|
1065
|
+
* </Form.When>
|
|
1066
|
+
*
|
|
1067
|
+
* // Render when field does not equal value
|
|
1068
|
+
* <Form.When field="contactMethod" isNot="none">
|
|
1069
|
+
* <Form.Field name="contact"><Form.Input /></Form.Field>
|
|
1070
|
+
* </Form.When>
|
|
1071
|
+
*
|
|
1072
|
+
* // Render when field value is in array
|
|
1073
|
+
* <Form.When field="role" in={['admin', 'moderator']}>
|
|
1074
|
+
* <Form.Field name="permissions"><Form.Input /></Form.Field>
|
|
1075
|
+
* </Form.When>
|
|
1076
|
+
*
|
|
1077
|
+
* // Render when field value is not in array
|
|
1078
|
+
* <Form.When field="status" notIn={['archived', 'deleted']}>
|
|
1079
|
+
* <Form.Field name="actions"><Form.Input /></Form.Field>
|
|
1080
|
+
* </Form.When>
|
|
1081
|
+
* ```
|
|
1082
|
+
*/
|
|
1083
|
+
function FormWhen({ field, is, isNot, in: inArray, notIn, children }) {
|
|
1084
|
+
const value = useWatch(field);
|
|
1085
|
+
let shouldRender = true;
|
|
1086
|
+
if (is !== void 0) shouldRender = value === is;
|
|
1087
|
+
if (isNot !== void 0 && shouldRender) shouldRender = value !== isNot;
|
|
1088
|
+
if (inArray !== void 0 && shouldRender) shouldRender = inArray.includes(value);
|
|
1089
|
+
if (notIn !== void 0 && shouldRender) shouldRender = !notIn.includes(value);
|
|
1090
|
+
if (!shouldRender) return null;
|
|
1091
|
+
return /* @__PURE__ */ jsx(Fragment$1, { children });
|
|
1092
|
+
}
|
|
1093
|
+
FormWhen.displayName = "Form.When";
|
|
1094
|
+
//#endregion
|
|
1095
|
+
//#region src/components/features/form/components/form-input-group.tsx
|
|
1096
|
+
/**
|
|
1097
|
+
* Form.InputGroup - Input with leading/trailing addons
|
|
1098
|
+
*
|
|
1099
|
+
* Automatically wired to the parent Form.Field context.
|
|
1100
|
+
*
|
|
1101
|
+
* @example
|
|
1102
|
+
* ```tsx
|
|
1103
|
+
* <Form.Field name="website" label="Website" required>
|
|
1104
|
+
* <Form.InputGroup leading="https://" placeholder="example.com" />
|
|
1105
|
+
* </Form.Field>
|
|
1106
|
+
* ```
|
|
1107
|
+
*/
|
|
1108
|
+
function FormInputGroup({ ref, className, disabled, ...props }) {
|
|
1109
|
+
const { id, errors, disabled: fieldDisabled, fieldState } = useFieldContext$1();
|
|
1110
|
+
const isDisabled = disabled ?? fieldDisabled;
|
|
1111
|
+
const hasErrors = errors && errors.length > 0;
|
|
1112
|
+
return /* @__PURE__ */ jsx(InputWithAddons, {
|
|
1113
|
+
...props,
|
|
1114
|
+
ref,
|
|
1115
|
+
id,
|
|
1116
|
+
name: fieldState?.name,
|
|
1117
|
+
value: fieldState?.value ?? "",
|
|
1118
|
+
onChange: (e) => fieldState?.change(e.target.value),
|
|
1119
|
+
onBlur: () => fieldState?.blur(),
|
|
1120
|
+
className: cn("text-xs!", className),
|
|
1121
|
+
disabled: isDisabled,
|
|
1122
|
+
"aria-invalid": hasErrors || void 0,
|
|
1123
|
+
"aria-describedby": hasErrors ? `${id}-error` : void 0
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
FormInputGroup.displayName = "Form.InputGroup";
|
|
1127
|
+
//#endregion
|
|
1128
|
+
//#region src/components/features/form/hooks/use-field.ts
|
|
1129
|
+
/**
|
|
1130
|
+
* Hook to access and control a specific field.
|
|
1131
|
+
* Delegates to the active adapter's useField implementation.
|
|
1132
|
+
*
|
|
1133
|
+
* @example
|
|
1134
|
+
* ```tsx
|
|
1135
|
+
* function MyCustomInput({ name }: { name: string }) {
|
|
1136
|
+
* const { field, control, meta, errors } = useField(name)
|
|
1137
|
+
* return (
|
|
1138
|
+
* <input
|
|
1139
|
+
* name={meta.name}
|
|
1140
|
+
* id={meta.id}
|
|
1141
|
+
* value={control.value ?? ''}
|
|
1142
|
+
* onChange={(e) => control.change(e.target.value)}
|
|
1143
|
+
* onBlur={control.blur}
|
|
1144
|
+
* aria-invalid={!!errors?.length}
|
|
1145
|
+
* />
|
|
1146
|
+
* )
|
|
1147
|
+
* }
|
|
1148
|
+
* ```
|
|
1149
|
+
*/
|
|
1150
|
+
function useField(name) {
|
|
1151
|
+
const fieldState = useAdapter().useField(name);
|
|
1152
|
+
return {
|
|
1153
|
+
field: fieldState,
|
|
1154
|
+
control: {
|
|
1155
|
+
value: fieldState.value,
|
|
1156
|
+
change: fieldState.change,
|
|
1157
|
+
blur: fieldState.blur,
|
|
1158
|
+
focus: fieldState.focus
|
|
1159
|
+
},
|
|
1160
|
+
meta: {
|
|
1161
|
+
name: fieldState.name,
|
|
1162
|
+
id: fieldState.id,
|
|
1163
|
+
errors: fieldState.errors,
|
|
1164
|
+
required: fieldState.required,
|
|
1165
|
+
disabled: false
|
|
1166
|
+
},
|
|
1167
|
+
errors: fieldState.errors
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
//#endregion
|
|
1171
|
+
//#region src/components/features/form/hooks/use-field-context.ts
|
|
1172
|
+
/**
|
|
1173
|
+
* Hook to access the current field context
|
|
1174
|
+
* Must be used within a Form.Field component
|
|
1175
|
+
*
|
|
1176
|
+
* @example
|
|
1177
|
+
* ```tsx
|
|
1178
|
+
* function MyInput() {
|
|
1179
|
+
* const { name, id, errors, required, disabled, fieldMeta } = useFieldContext();
|
|
1180
|
+
*
|
|
1181
|
+
* return (
|
|
1182
|
+
* <input
|
|
1183
|
+
* name={name}
|
|
1184
|
+
* id={id}
|
|
1185
|
+
* required={required}
|
|
1186
|
+
* disabled={disabled}
|
|
1187
|
+
* aria-invalid={!!errors?.length}
|
|
1188
|
+
* />
|
|
1189
|
+
* );
|
|
1190
|
+
* }
|
|
1191
|
+
* ```
|
|
1192
|
+
*/
|
|
1193
|
+
function useFieldContext() {
|
|
1194
|
+
return useFieldContext$1();
|
|
1195
|
+
}
|
|
1196
|
+
//#endregion
|
|
1197
|
+
//#region src/components/features/form/hooks/use-form-context.ts
|
|
1198
|
+
/**
|
|
1199
|
+
* Hook to access the form context
|
|
1200
|
+
*
|
|
1201
|
+
* @example
|
|
1202
|
+
* ```tsx
|
|
1203
|
+
* function MyComponent() {
|
|
1204
|
+
* const { form, fields, isSubmitting, submit, reset } = useFormContext();
|
|
1205
|
+
*
|
|
1206
|
+
* return (
|
|
1207
|
+
* <button onClick={submit} disabled={isSubmitting}>
|
|
1208
|
+
* Submit
|
|
1209
|
+
* </button>
|
|
1210
|
+
* );
|
|
1211
|
+
* }
|
|
1212
|
+
* ```
|
|
1213
|
+
*/
|
|
1214
|
+
function useFormContext() {
|
|
1215
|
+
return useFormContext$1();
|
|
1216
|
+
}
|
|
1217
|
+
//#endregion
|
|
1218
|
+
//#region src/components/features/form/hooks/use-form-state.ts
|
|
1219
|
+
/**
|
|
1220
|
+
* Hook to access form-level state (dirty, valid, submitted, etc.)
|
|
1221
|
+
*
|
|
1222
|
+
* @example
|
|
1223
|
+
* ```tsx
|
|
1224
|
+
* function SaveButton() {
|
|
1225
|
+
* const { isDirty, isSubmitting, isValid } = useFormState()
|
|
1226
|
+
*
|
|
1227
|
+
* return (
|
|
1228
|
+
* <button
|
|
1229
|
+
* type="submit"
|
|
1230
|
+
* disabled={!isDirty || isSubmitting || !isValid}
|
|
1231
|
+
* >
|
|
1232
|
+
* Save Changes
|
|
1233
|
+
* </button>
|
|
1234
|
+
* )
|
|
1235
|
+
* }
|
|
1236
|
+
* ```
|
|
1237
|
+
*/
|
|
1238
|
+
function useFormState() {
|
|
1239
|
+
const ctx = useFormContext$1();
|
|
1240
|
+
return {
|
|
1241
|
+
isDirty: ctx.isDirty,
|
|
1242
|
+
isValid: ctx.isValid,
|
|
1243
|
+
isSubmitting: ctx.isSubmitting,
|
|
1244
|
+
isSubmitted: ctx.isSubmitted,
|
|
1245
|
+
submitCount: ctx.submitCount,
|
|
1246
|
+
dirtyFields: ctx.dirtyFields,
|
|
1247
|
+
touchedFields: ctx.touchedFields
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
//#endregion
|
|
1251
|
+
//#region src/components/features/form/index.ts
|
|
1252
|
+
/**
|
|
1253
|
+
* Datum Form Library
|
|
1254
|
+
*
|
|
1255
|
+
* A compound component pattern form library with pluggable adapter support.
|
|
1256
|
+
* Choose between Conform.js or React Hook Form as your form backend,
|
|
1257
|
+
* with Zod for schema validation.
|
|
1258
|
+
*
|
|
1259
|
+
* ## Adapter Setup
|
|
1260
|
+
*
|
|
1261
|
+
* Wrap your app with an adapter provider:
|
|
1262
|
+
*
|
|
1263
|
+
* ```tsx
|
|
1264
|
+
* // Conform adapter
|
|
1265
|
+
* import { ConformAdapter } from '@datum-cloud/datum-ui/form/adapters/conform'
|
|
1266
|
+
* <ConformAdapter><App /></ConformAdapter>
|
|
1267
|
+
*
|
|
1268
|
+
* // React Hook Form adapter
|
|
1269
|
+
* import { RHFAdapter } from '@datum-cloud/datum-ui/form/adapters/rhf'
|
|
1270
|
+
* <RHFAdapter><App /></RHFAdapter>
|
|
1271
|
+
* ```
|
|
1272
|
+
*
|
|
1273
|
+
* @example Basic Usage
|
|
1274
|
+
* ```tsx
|
|
1275
|
+
* import { Form } from '@datum-cloud/datum-ui/form';
|
|
1276
|
+
* import { z } from 'zod';
|
|
1277
|
+
*
|
|
1278
|
+
* const userSchema = z.object({
|
|
1279
|
+
* name: z.string().min(2),
|
|
1280
|
+
* email: z.string().email(),
|
|
1281
|
+
* });
|
|
1282
|
+
*
|
|
1283
|
+
* function UserForm() {
|
|
1284
|
+
* return (
|
|
1285
|
+
* <Form.Root schema={userSchema} onSubmit={(data) => console.log(data)}>
|
|
1286
|
+
* <Form.Field name="name" label="Name" required>
|
|
1287
|
+
* <Form.Input />
|
|
1288
|
+
* </Form.Field>
|
|
1289
|
+
* <Form.Field name="email" label="Email" required>
|
|
1290
|
+
* <Form.Input type="email" />
|
|
1291
|
+
* </Form.Field>
|
|
1292
|
+
* <Form.Submit>Save</Form.Submit>
|
|
1293
|
+
* </Form.Root>
|
|
1294
|
+
* );
|
|
1295
|
+
* }
|
|
1296
|
+
* ```
|
|
1297
|
+
*
|
|
1298
|
+
* @example Multi-Step Form (separate import)
|
|
1299
|
+
* ```tsx
|
|
1300
|
+
* import { FormStepper, FormStep, StepperNavigation, StepperControls } from '@datum-cloud/datum-ui/form/stepper';
|
|
1301
|
+
*
|
|
1302
|
+
* const steps = [
|
|
1303
|
+
* { id: 'account', label: 'Account', schema: accountSchema },
|
|
1304
|
+
* { id: 'profile', label: 'Profile', schema: profileSchema },
|
|
1305
|
+
* ];
|
|
1306
|
+
*
|
|
1307
|
+
* <FormStepper steps={steps} onComplete={handleComplete}>
|
|
1308
|
+
* <StepperNavigation />
|
|
1309
|
+
* <FormStep id="account">...</FormStep>
|
|
1310
|
+
* <FormStep id="profile">...</FormStep>
|
|
1311
|
+
* <StepperControls />
|
|
1312
|
+
* </FormStepper>
|
|
1313
|
+
* ```
|
|
1314
|
+
*
|
|
1315
|
+
* @example Form State
|
|
1316
|
+
* ```tsx
|
|
1317
|
+
* <Form.Root schema={schema} onSubmit={handleSubmit}>
|
|
1318
|
+
* {({ isDirty, isValid, isSubmitted, submitCount }) => (
|
|
1319
|
+
* <>
|
|
1320
|
+
* <Form.Field name="name"><Form.Input /></Form.Field>
|
|
1321
|
+
* <Form.Submit disabled={!isDirty || !isValid}>Save</Form.Submit>
|
|
1322
|
+
* {isSubmitted && <p>Submitted {submitCount} time(s)</p>}
|
|
1323
|
+
* </>
|
|
1324
|
+
* )}
|
|
1325
|
+
* </Form.Root>
|
|
1326
|
+
* ```
|
|
1327
|
+
*
|
|
1328
|
+
* @example Conditional Fields
|
|
1329
|
+
* ```tsx
|
|
1330
|
+
* <Form.Field name="contactMethod">
|
|
1331
|
+
* <Form.Select>
|
|
1332
|
+
* <Form.SelectItem value="email">Email</Form.SelectItem>
|
|
1333
|
+
* <Form.SelectItem value="phone">Phone</Form.SelectItem>
|
|
1334
|
+
* </Form.Select>
|
|
1335
|
+
* </Form.Field>
|
|
1336
|
+
*
|
|
1337
|
+
* <Form.When field="contactMethod" is="email">
|
|
1338
|
+
* <Form.Field name="email"><Form.Input type="email" /></Form.Field>
|
|
1339
|
+
* </Form.When>
|
|
1340
|
+
* ```
|
|
1341
|
+
*/
|
|
1342
|
+
/**
|
|
1343
|
+
* Form compound component
|
|
1344
|
+
*
|
|
1345
|
+
* Requires an adapter provider at the application root:
|
|
1346
|
+
* - `<ConformAdapter>` from `@datum-cloud/datum-ui/form/adapters/conform`
|
|
1347
|
+
* - `<RHFAdapter>` from `@datum-cloud/datum-ui/form/adapters/rhf`
|
|
1348
|
+
*
|
|
1349
|
+
* Components:
|
|
1350
|
+
* - Form.Root - Main form container
|
|
1351
|
+
* - Form.Field - Field wrapper with label and error handling
|
|
1352
|
+
* - Form.Input, Form.Textarea, Form.Select, Form.Checkbox, Form.Switch, Form.RadioGroup
|
|
1353
|
+
* - Form.Autocomplete, Form.CopyBox, Form.InputGroup
|
|
1354
|
+
* - Form.When - Conditional rendering
|
|
1355
|
+
* - Form.FieldArray - Dynamic array of fields
|
|
1356
|
+
* - Form.Custom - Escape hatch for custom implementations
|
|
1357
|
+
* - Form.Dialog - Form rendered inside a Dialog
|
|
1358
|
+
* - Form.Submit, Form.Button, Form.Error, Form.Description
|
|
1359
|
+
*
|
|
1360
|
+
* Stepper (separate import):
|
|
1361
|
+
* - `@datum-cloud/datum-ui/form/stepper` provides FormStepper, FormStep, StepperNavigation, StepperControls, useStepper
|
|
1362
|
+
*
|
|
1363
|
+
* Hooks:
|
|
1364
|
+
* - Form.useFormContext, Form.useFormState, Form.useFieldContext, Form.useField
|
|
1365
|
+
* - Form.useWatch, Form.useWatchAll
|
|
1366
|
+
*/
|
|
1367
|
+
const Form = {
|
|
1368
|
+
Root: FormRoot,
|
|
1369
|
+
Field: FormField,
|
|
1370
|
+
Submit: FormSubmit,
|
|
1371
|
+
Button: FormButton,
|
|
1372
|
+
Error: FormError,
|
|
1373
|
+
Description: FormDescription,
|
|
1374
|
+
Input: FormInput,
|
|
1375
|
+
Textarea: FormTextarea,
|
|
1376
|
+
Select: FormSelect,
|
|
1377
|
+
SelectItem: FormSelectItem,
|
|
1378
|
+
Checkbox: FormCheckbox,
|
|
1379
|
+
Switch: FormSwitch,
|
|
1380
|
+
RadioGroup: FormRadioGroup,
|
|
1381
|
+
RadioItem: FormRadioItem,
|
|
1382
|
+
CopyBox: FormCopyBox,
|
|
1383
|
+
Autocomplete: FormAutocomplete,
|
|
1384
|
+
InputGroup: FormInputGroup,
|
|
1385
|
+
When: FormWhen,
|
|
1386
|
+
FieldArray: FormFieldArray,
|
|
1387
|
+
Custom: FormCustom,
|
|
1388
|
+
Dialog: FormDialog,
|
|
1389
|
+
useFormContext,
|
|
1390
|
+
useFormState,
|
|
1391
|
+
useFieldContext,
|
|
1392
|
+
useField,
|
|
1393
|
+
useWatch,
|
|
1394
|
+
useWatchAll
|
|
1395
|
+
};
|
|
1396
|
+
//#endregion
|
|
1397
|
+
export { FormCustom as C, FormAutocomplete as D, FormButton as E, FormDescription as S, FormCheckbox as T, FormInput as _, useField as a, FormError as b, useWatchAll as c, FormSubmit as d, FormSelect as f, FormRadioItem as g, FormRadioGroup as h, useFieldContext as i, FormTextarea as l, FormRoot as m, useFormState as n, FormWhen as o, FormSelectItem as p, useFormContext as r, useWatch as s, Form as t, FormSwitch as u, FormFieldArray as v, FormCopyBox as w, FormDialog as x, FormField as y };
|