@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.
Files changed (90) hide show
  1. package/README.md +75 -40
  2. package/dist/adapter-context-BFqfq4Io.mjs +25 -0
  3. package/dist/components/features/form/adapter-context.d.ts +17 -0
  4. package/dist/components/features/form/adapter-context.d.ts.map +1 -0
  5. package/dist/components/features/form/adapter-types.d.ts +120 -0
  6. package/dist/components/features/form/adapter-types.d.ts.map +1 -0
  7. package/dist/components/features/form/adapters/conform/conform-adapter.d.ts +9 -0
  8. package/dist/components/features/form/adapters/conform/conform-adapter.d.ts.map +1 -0
  9. package/dist/components/features/form/adapters/conform/conform-provider.d.ts +22 -0
  10. package/dist/components/features/form/adapters/conform/conform-provider.d.ts.map +1 -0
  11. package/dist/components/features/form/adapters/conform/index.d.ts +3 -0
  12. package/dist/components/features/form/adapters/conform/index.d.ts.map +1 -0
  13. package/dist/components/features/form/adapters/rhf/index.d.ts +3 -0
  14. package/dist/components/features/form/adapters/rhf/index.d.ts.map +1 -0
  15. package/dist/components/features/form/adapters/rhf/rhf-adapter.d.ts +10 -0
  16. package/dist/components/features/form/adapters/rhf/rhf-adapter.d.ts.map +1 -0
  17. package/dist/components/features/form/adapters/rhf/rhf-provider.d.ts +22 -0
  18. package/dist/components/features/form/adapters/rhf/rhf-provider.d.ts.map +1 -0
  19. package/dist/components/features/form/components/form-autocomplete.d.ts.map +1 -1
  20. package/dist/components/features/form/components/form-checkbox.d.ts.map +1 -1
  21. package/dist/components/features/form/components/form-copy-box.d.ts.map +1 -1
  22. package/dist/components/features/form/components/form-custom.d.ts.map +1 -1
  23. package/dist/components/features/form/components/form-field-array.d.ts +5 -17
  24. package/dist/components/features/form/components/form-field-array.d.ts.map +1 -1
  25. package/dist/components/features/form/components/form-field.d.ts +7 -21
  26. package/dist/components/features/form/components/form-field.d.ts.map +1 -1
  27. package/dist/components/features/form/components/form-input-group.d.ts +4 -4
  28. package/dist/components/features/form/components/form-input-group.d.ts.map +1 -1
  29. package/dist/components/features/form/components/form-input.d.ts.map +1 -1
  30. package/dist/components/features/form/components/form-radio-group.d.ts.map +1 -1
  31. package/dist/components/features/form/components/form-root.d.ts +5 -25
  32. package/dist/components/features/form/components/form-root.d.ts.map +1 -1
  33. package/dist/components/features/form/components/form-select.d.ts.map +1 -1
  34. package/dist/components/features/form/components/form-switch.d.ts.map +1 -1
  35. package/dist/components/features/form/components/form-textarea.d.ts.map +1 -1
  36. package/dist/components/features/form/components/index.d.ts +0 -1
  37. package/dist/components/features/form/components/index.d.ts.map +1 -1
  38. package/dist/components/features/form/components/stepper/form-stepper.d.ts.map +1 -1
  39. package/dist/components/features/form/context/form-context.d.ts +2 -2
  40. package/dist/components/features/form/context/form-context.d.ts.map +1 -1
  41. package/dist/components/features/form/hooks/index.d.ts +1 -1
  42. package/dist/components/features/form/hooks/index.d.ts.map +1 -1
  43. package/dist/components/features/form/hooks/use-field.d.ts +12 -18
  44. package/dist/components/features/form/hooks/use-field.d.ts.map +1 -1
  45. package/dist/components/features/form/hooks/use-form-state.d.ts +36 -0
  46. package/dist/components/features/form/hooks/use-form-state.d.ts.map +1 -0
  47. package/dist/components/features/form/hooks/use-watch.d.ts +9 -20
  48. package/dist/components/features/form/hooks/use-watch.d.ts.map +1 -1
  49. package/dist/components/features/form/index.d.ts +63 -44
  50. package/dist/components/features/form/index.d.ts.map +1 -1
  51. package/dist/components/features/form/stepper/index.d.ts +17 -0
  52. package/dist/components/features/form/stepper/index.d.ts.map +1 -0
  53. package/dist/components/features/form/types/index.d.ts +68 -32
  54. package/dist/components/features/form/types/index.d.ts.map +1 -1
  55. package/dist/components/features/form/utils/get-field-constraints.d.ts +11 -0
  56. package/dist/components/features/form/utils/get-field-constraints.d.ts.map +1 -0
  57. package/dist/components/features/form/utils/get-schema-defaults.d.ts +24 -0
  58. package/dist/components/features/form/utils/get-schema-defaults.d.ts.map +1 -0
  59. package/dist/date-picker/index.mjs +1 -1
  60. package/dist/form/adapters/conform/index.mjs +320 -0
  61. package/dist/form/adapters/rhf/index.mjs +275 -0
  62. package/dist/form/index.mjs +3 -2
  63. package/dist/form/stepper/index.mjs +542 -0
  64. package/dist/form-C6AOB2f4.mjs +1397 -0
  65. package/dist/form-context-Ccxm-wqL.mjs +17 -0
  66. package/dist/get-field-constraints-D4xnXJEg.mjs +48 -0
  67. package/dist/grid/index.mjs +1 -1
  68. package/dist/hooks/index.mjs +2 -2
  69. package/dist/index.mjs +14 -13
  70. package/dist/input-number/index.mjs +1 -1
  71. package/dist/map/index.mjs +1 -1
  72. package/dist/{map-ClxB41Hg.mjs → map-BqpteT_8.mjs} +1 -1
  73. package/dist/more-actions/index.mjs +1 -1
  74. package/dist/page-title/index.mjs +1 -1
  75. package/dist/stepper/index.mjs +1 -320
  76. package/dist/stepper-C92Ib8Iy.mjs +321 -0
  77. package/dist/tag-input/index.mjs +1 -1
  78. package/dist/task-queue/index.mjs +1 -1
  79. package/package.json +27 -2
  80. package/dist/form-Co3fM4B7.mjs +0 -2114
  81. /package/dist/{col-q-J99UHe.mjs → col-CiSpQPUT.mjs} +0 -0
  82. /package/dist/{hooks-Cb7YlxN4.mjs → hooks-DNjmSsJT.mjs} +0 -0
  83. /package/dist/{input-number-mDB-5M5C.mjs → input-number-BTQdHqVZ.mjs} +0 -0
  84. /package/dist/{map-leaflet-imports-CaMm_rdF.mjs → map-leaflet-imports-CT4SpoDi.mjs} +0 -0
  85. /package/dist/{more-actions-CGagbIDT.mjs → more-actions-CucrYUnA.mjs} +0 -0
  86. /package/dist/{page-title-R7QbfbWp.mjs → page-title-CmsIi_A3.mjs} +0 -0
  87. /package/dist/{tag-input-BVSwNcRd.mjs → tag-input-B91C2wdr.mjs} +0 -0
  88. /package/dist/{task-queue-dropdown-DyM5R8KF.mjs → task-queue-dropdown-OOFuJcHb.mjs} +0 -0
  89. /package/dist/{to-api-format-BnbRFYQI.mjs → to-api-format-P0gmlqe8.mjs} +0 -0
  90. /package/dist/{use-copy-to-clipboard-BGdTmkFV.mjs → use-copy-to-clipboard-C2IEmhDn.mjs} +0 -0
@@ -1,2114 +0,0 @@
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 { t as useCopyToClipboard } from "./use-copy-to-clipboard-BGdTmkFV.mjs";
16
- import { defineStepper } from "./stepper/index.mjs";
17
- import { InputWithAddons } from "./input-with-addons/index.mjs";
18
- import { CheckIcon, CircleHelp, CopyIcon } from "lucide-react";
19
- import * as React$1 from "react";
20
- import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
21
- import { z } from "zod";
22
- import { FormProvider, getFormProps, getInputProps, getTextareaProps, useForm, useFormMetadata, useInputControl } from "@conform-to/react";
23
- import { getZodConstraint, parseWithZod } from "@conform-to/zod/v4";
24
- //#region src/components/features/form/context/field-context.tsx
25
- const FieldContext = React$1.createContext(null);
26
- function FieldProvider({ children, value }) {
27
- return /* @__PURE__ */ jsx(FieldContext, {
28
- value,
29
- children
30
- });
31
- }
32
- function useFieldContext$1() {
33
- const context = React$1.use(FieldContext);
34
- if (!context) throw new Error("useFieldContext must be used within a Form.Field component. Make sure your input component is wrapped with Form.Field.");
35
- return context;
36
- }
37
- /**
38
- * Optional field context - returns null if not within a Form.Field
39
- * Useful for components that can work both inside and outside Form.Field
40
- */
41
- function useOptionalFieldContext() {
42
- return React$1.use(FieldContext);
43
- }
44
- //#endregion
45
- //#region src/components/features/form/components/form-autocomplete.tsx
46
- /**
47
- * Form.Autocomplete - Searchable select component
48
- *
49
- * Automatically wired to the parent Form.Field context.
50
- * Supports flat/grouped options, virtualization, custom rendering, and async search.
51
- *
52
- * @example Basic usage
53
- * ```tsx
54
- * <Form.Field name="timezone" label="Timezone" required>
55
- * <Form.Autocomplete
56
- * options={timezones}
57
- * placeholder="Select timezone..."
58
- * />
59
- * </Form.Field>
60
- * ```
61
- *
62
- * @example Async search
63
- * ```tsx
64
- * <Form.Field name="userId" label="User">
65
- * <Form.Autocomplete
66
- * options={users ?? []}
67
- * onSearchChange={setSearch}
68
- * loading={isLoading}
69
- * placeholder="Search users..."
70
- * />
71
- * </Form.Field>
72
- * ```
73
- *
74
- * @example Grouped options
75
- * ```tsx
76
- * <Form.Field name="role" label="Role" required>
77
- * <Form.Autocomplete
78
- * options={roleGroups}
79
- * placeholder="Select a role..."
80
- * />
81
- * </Form.Field>
82
- * ```
83
- */
84
- function FormAutocomplete({ disabled, className, ...props }) {
85
- const { fieldMeta, disabled: fieldDisabled, errors } = useFieldContext$1();
86
- const control = useInputControl(fieldMeta);
87
- const isDisabled = disabled ?? fieldDisabled;
88
- const hasErrors = errors && errors.length > 0;
89
- const selectValue = Array.isArray(control.value) ? control.value[0] : control.value;
90
- return /* @__PURE__ */ jsx(Autocomplete, {
91
- ...props,
92
- name: fieldMeta.name,
93
- id: fieldMeta.id,
94
- value: selectValue ?? "",
95
- onValueChange: control.change,
96
- disabled: isDisabled,
97
- triggerClassName: cn(hasErrors && "border-destructive", props.triggerClassName),
98
- className
99
- });
100
- }
101
- FormAutocomplete.displayName = "Form.Autocomplete";
102
- //#endregion
103
- //#region src/components/features/form/context/form-context.tsx
104
- const FormContext = React$1.createContext(null);
105
- function FormProvider$1({ children, value }) {
106
- return /* @__PURE__ */ jsx(FormContext, {
107
- value,
108
- children
109
- });
110
- }
111
- function useFormContext$1() {
112
- const context = React$1.use(FormContext);
113
- if (!context) throw new Error("useFormContext must be used within a Form.Root component");
114
- return context;
115
- }
116
- //#endregion
117
- //#region src/components/features/form/components/form-button.tsx
118
- /**
119
- * Form.Button - A button for non-submit actions within a form
120
- *
121
- * Automatically gets disabled when the form is submitting.
122
- * Use this for cancel buttons, reset buttons, or other actions.
123
- *
124
- * @example
125
- * ```tsx
126
- * <Form.Button onClick={() => navigate(-1)}>
127
- * Cancel
128
- * </Form.Button>
129
- *
130
- * <Form.Button onClick={() => form.reset()} type="secondary">
131
- * Reset
132
- * </Form.Button>
133
- * ```
134
- */
135
- function FormButton({ children, onClick, type = "quaternary", theme = "borderless", size, disabled, className, disableOnSubmit = true }) {
136
- const { isSubmitting } = useFormContext$1();
137
- return /* @__PURE__ */ jsx(Button, {
138
- htmlType: "button",
139
- type,
140
- theme,
141
- size,
142
- disabled: disabled || disableOnSubmit && isSubmitting,
143
- className,
144
- onClick,
145
- children
146
- });
147
- }
148
- FormButton.displayName = "Form.Button";
149
- //#endregion
150
- //#region src/components/features/form/components/form-checkbox.tsx
151
- /**
152
- * Form.Checkbox - Checkbox input component
153
- *
154
- * Automatically wired to the parent Form.Field context.
155
- *
156
- * @example
157
- * ```tsx
158
- * <Form.Field name="terms">
159
- * <Form.Checkbox label="I agree to the terms and conditions" />
160
- * </Form.Field>
161
- * ```
162
- */
163
- function FormCheckbox({ label, disabled, className }) {
164
- const { fieldMeta, disabled: fieldDisabled, errors } = useFieldContext$1();
165
- const control = useInputControl(fieldMeta);
166
- const isDisabled = disabled ?? fieldDisabled;
167
- const hasErrors = errors && errors.length > 0;
168
- const isChecked = control.value === "on" || control.value === "true";
169
- const handleCheckedChange = (checked) => {
170
- control.change(checked ? "on" : "");
171
- };
172
- const checkboxId = fieldMeta.id;
173
- return /* @__PURE__ */ jsxs("div", {
174
- className: cn("flex items-center space-x-2", className),
175
- children: [/* @__PURE__ */ jsx(Checkbox, {
176
- id: checkboxId,
177
- name: fieldMeta.name,
178
- checked: isChecked,
179
- onCheckedChange: handleCheckedChange,
180
- disabled: isDisabled,
181
- "aria-invalid": hasErrors || void 0,
182
- "aria-describedby": hasErrors ? `${fieldMeta.id}-error` : void 0
183
- }), label && /* @__PURE__ */ jsx(Label, {
184
- htmlFor: checkboxId,
185
- className: cn("cursor-pointer text-sm font-normal", isDisabled && "cursor-not-allowed opacity-70"),
186
- children: label
187
- })]
188
- });
189
- }
190
- FormCheckbox.displayName = "Form.Checkbox";
191
- //#endregion
192
- //#region src/components/features/form/components/form-copy-box.tsx
193
- /**
194
- * Form.CopyBox - Read-only field with copy-to-clipboard functionality
195
- *
196
- * Displays field value in a read-only box with a copy button.
197
- * Automatically gets value from Form.Field context.
198
- *
199
- * @example Basic usage
200
- * ```tsx
201
- * <Form.Field name="organizationId" label="Organization ID">
202
- * <Form.CopyBox />
203
- * </Form.Field>
204
- * ```
205
- *
206
- * @example With icon-only button
207
- * ```tsx
208
- * <Form.Field name="apiKey" label="API Key">
209
- * <Form.CopyBox variant="icon-only" />
210
- * </Form.Field>
211
- * ```
212
- *
213
- * @example With placeholder
214
- * ```tsx
215
- * <Form.Field name="webhookUrl" label="Webhook URL">
216
- * <Form.CopyBox placeholder="Not configured" />
217
- * </Form.Field>
218
- * ```
219
- */
220
- function FormCopyBox({ variant = "default", className, contentClassName, buttonClassName, placeholder = "" }) {
221
- const { fieldMeta } = useFieldContext$1();
222
- const control = useInputControl(fieldMeta);
223
- const [_, copy] = useCopyToClipboard();
224
- const [copied, setCopied] = React$1.useState(false);
225
- const value = control.value ?? placeholder;
226
- const copyToClipboard = () => {
227
- const stringValue = String(value);
228
- if (!stringValue) return;
229
- copy(stringValue).then(() => {
230
- toast.success("Copied to clipboard");
231
- setCopied(true);
232
- setTimeout(() => {
233
- setCopied(false);
234
- }, 2e3);
235
- });
236
- };
237
- return /* @__PURE__ */ jsxs("div", {
238
- className: cn("group border-input flex h-10 w-full overflow-hidden rounded-lg border bg-[#F6F6F580] text-xs focus-within:outline-hidden", className),
239
- children: [/* @__PURE__ */ jsx("div", {
240
- className: cn("flex w-full items-center overflow-hidden px-3 py-2 text-xs opacity-50", contentClassName),
241
- children: /* @__PURE__ */ jsx("span", {
242
- className: "truncate",
243
- children: String(value)
244
- })
245
- }), /* @__PURE__ */ jsx("div", {
246
- className: "flex items-center py-2 pr-3",
247
- children: variant === "icon-only" ? /* @__PURE__ */ jsx("button", {
248
- type: "button",
249
- className: cn("text-muted-foreground hover:text-foreground flex size-7 items-center justify-center rounded-sm transition-colors", buttonClassName),
250
- onClick: copyToClipboard,
251
- children: copied ? /* @__PURE__ */ jsx(CheckIcon, { className: "size-4" }) : /* @__PURE__ */ jsx(CopyIcon, { className: "size-4" })
252
- }) : /* @__PURE__ */ jsxs(Button, {
253
- type: "quaternary",
254
- theme: "outline",
255
- size: "small",
256
- className: cn("h-7 w-fit gap-1 px-2 text-xs", buttonClassName),
257
- onClick: copyToClipboard,
258
- children: [/* @__PURE__ */ jsx(CopyIcon, { className: "size-3!" }), copied ? "Copied" : "Copy"]
259
- })
260
- })]
261
- });
262
- }
263
- //#endregion
264
- //#region src/components/features/form/components/form-custom.tsx
265
- /**
266
- * Form.Custom - Escape hatch for custom implementations
267
- *
268
- * Provides access to the underlying form context for complex use cases
269
- * that don't fit the standard component patterns.
270
- *
271
- * @example
272
- * ```tsx
273
- * <Form.Custom>
274
- * {({ form, fields, submit, reset }) => (
275
- * <MyCustomComponent
276
- * fields={fields}
277
- * onCustomAction={() => {
278
- * // Do something custom
279
- * submit();
280
- * }}
281
- * />
282
- * )}
283
- * </Form.Custom>
284
- * ```
285
- */
286
- function FormCustom({ children }) {
287
- const { form, fields, isSubmitting, submit, reset } = useFormContext$1();
288
- return /* @__PURE__ */ jsx(Fragment$1, { children: children({
289
- form,
290
- fields,
291
- isSubmitting,
292
- submit,
293
- reset
294
- }) });
295
- }
296
- FormCustom.displayName = "Form.Custom";
297
- //#endregion
298
- //#region src/components/features/form/components/form-description.tsx
299
- /**
300
- * Form.Description - Display field description/helper text
301
- *
302
- * @example
303
- * ```tsx
304
- * <Form.Field name="password">
305
- * <Form.Input type="password" />
306
- * <Form.Description>
307
- * Must be at least 8 characters with one uppercase letter
308
- * </Form.Description>
309
- * </Form.Field>
310
- * ```
311
- */
312
- function FormDescription({ children, className }) {
313
- const fieldContext = useOptionalFieldContext();
314
- return /* @__PURE__ */ jsx("p", {
315
- id: fieldContext ? `${fieldContext.id}-description` : void 0,
316
- className: cn("text-muted-foreground text-xs text-wrap", className),
317
- children
318
- });
319
- }
320
- FormDescription.displayName = "Form.Description";
321
- //#endregion
322
- //#region src/components/features/form/components/form-dialog.tsx
323
- /**
324
- * Form.Dialog - A dialog with an integrated form
325
- *
326
- * Combines Dialog and Form.Root into a single component with:
327
- * - Automatic dialog state management (controlled or uncontrolled)
328
- * - Built-in header with title and description
329
- * - Built-in footer with submit and cancel buttons
330
- * - Auto-close on successful submission
331
- * - Prevents accidental close during submission
332
- * - Supports render function pattern for form state access
333
- *
334
- * @example Basic usage
335
- * ```tsx
336
- * <Form.Dialog
337
- * title="Add User"
338
- * description="Enter user details"
339
- * schema={userSchema}
340
- * onSubmit={handleSubmit}
341
- * trigger={<Button>Add User</Button>}
342
- * >
343
- * <Form.Field name="name" label="Name" required>
344
- * <Form.Input />
345
- * </Form.Field>
346
- * <Form.Field name="email" label="Email" required>
347
- * <Form.Input type="email" />
348
- * </Form.Field>
349
- * </Form.Dialog>
350
- * ```
351
- *
352
- * @example With render function for form state access
353
- * ```tsx
354
- * <Form.Dialog
355
- * title="Edit User"
356
- * schema={userSchema}
357
- * defaultValues={user}
358
- * onSubmit={handleSubmit}
359
- * trigger={<Button>Edit</Button>}
360
- * >
361
- * {({ form, fields, isSubmitting, reset }) => (
362
- * <>
363
- * <Form.Field name="name" label="Name">
364
- * <Form.Input />
365
- * </Form.Field>
366
- * <Button variant="ghost" onClick={reset} disabled={isSubmitting}>
367
- * Reset
368
- * </Button>
369
- * </>
370
- * )}
371
- * </Form.Dialog>
372
- * ```
373
- */
374
- 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 }) {
375
- const [internalOpen, setInternalOpen] = React$1.useState(defaultOpen ?? false);
376
- const [internalIsSubmitting, setInternalIsSubmitting] = React$1.useState(false);
377
- const isSubmitting = loading ?? internalIsSubmitting;
378
- const isControlled = open !== void 0;
379
- const isOpen = isControlled ? open : internalOpen;
380
- const handleOpenChange = React$1.useCallback((value) => {
381
- if (!value && isSubmitting) return;
382
- if (!isControlled) setInternalOpen(value);
383
- onOpenChange?.(value);
384
- }, [
385
- isControlled,
386
- isSubmitting,
387
- onOpenChange
388
- ]);
389
- const handleSubmit = React$1.useCallback(async (data) => {
390
- if (loading === void 0) setInternalIsSubmitting(true);
391
- try {
392
- await onSubmit?.(data);
393
- onSuccess?.(data);
394
- } catch (error) {
395
- console.error("Form submission error:", error);
396
- throw error;
397
- } finally {
398
- if (loading === void 0) setInternalIsSubmitting(false);
399
- }
400
- }, [
401
- onSubmit,
402
- onSuccess,
403
- loading
404
- ]);
405
- const handleCancel = React$1.useCallback(() => {
406
- handleOpenChange(false);
407
- }, [handleOpenChange]);
408
- return /* @__PURE__ */ jsxs(Dialog, {
409
- open: isOpen,
410
- onOpenChange: handleOpenChange,
411
- children: [trigger && /* @__PURE__ */ jsx(Dialog.Trigger, { children: trigger }), /* @__PURE__ */ jsx(Dialog.Content, {
412
- className,
413
- children: /* @__PURE__ */ jsx(Form.Root, {
414
- schema,
415
- defaultValues,
416
- onSubmit: handleSubmit,
417
- onError,
418
- isSubmitting,
419
- mode: "onSubmit",
420
- formComponent,
421
- telemetry,
422
- className: cn("space-y-0", formClassName),
423
- children: (renderProps) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
424
- /* @__PURE__ */ jsx(Dialog.Header, {
425
- title,
426
- description,
427
- onClose: handleCancel,
428
- className: "border-b",
429
- descriptionClassName: "text-foreground/80"
430
- }),
431
- /* @__PURE__ */ jsx(Dialog.Body, {
432
- className: "space-y-0",
433
- children: typeof children === "function" ? children(renderProps) : children
434
- }),
435
- /* @__PURE__ */ jsxs(Dialog.Footer, {
436
- className: "border-t",
437
- children: [showCancel && /* @__PURE__ */ jsx(Form.Button, {
438
- type: "quaternary",
439
- theme: "outline",
440
- onClick: handleCancel,
441
- disableOnSubmit: true,
442
- children: cancelText
443
- }), /* @__PURE__ */ jsx(Form.Submit, {
444
- type: submitType,
445
- children: isSubmitting ? submitTextLoading : submitText
446
- })]
447
- })
448
- ] })
449
- })
450
- })]
451
- });
452
- }
453
- FormDialog.displayName = "Form.Dialog";
454
- //#endregion
455
- //#region src/components/features/form/components/form-error.tsx
456
- /**
457
- * Form.Error - Display field errors
458
- *
459
- * Can be used inside Form.Field to display errors automatically,
460
- * or standalone with custom rendering.
461
- *
462
- * @example
463
- * ```tsx
464
- * // Inside Form.Field - displays field errors automatically
465
- * <Form.Field name="email">
466
- * <Form.Input />
467
- * <Form.Error />
468
- * </Form.Field>
469
- *
470
- * // Custom rendering
471
- * <Form.Field name="email">
472
- * <Form.Input />
473
- * <Form.Error>
474
- * {(errors) => errors.map(e => <span key={e}>{e}</span>)}
475
- * </Form.Error>
476
- * </Form.Field>
477
- * ```
478
- */
479
- function FormError({ children, className }) {
480
- const errors = useOptionalFieldContext()?.errors;
481
- if (!errors || errors.length === 0) return null;
482
- if (typeof children === "function") return /* @__PURE__ */ jsx(Fragment$1, { children: children(errors) });
483
- return /* @__PURE__ */ jsx("ul", {
484
- className: cn("text-destructive space-y-1 text-sm font-medium", errors.length > 1 && "list-disc pl-4", className),
485
- role: "alert",
486
- "aria-live": "polite",
487
- children: errors.map((error) => /* @__PURE__ */ jsx("li", {
488
- className: "text-wrap",
489
- children: error
490
- }, error))
491
- });
492
- }
493
- FormError.displayName = "Form.Error";
494
- //#endregion
495
- //#region src/components/features/form/components/form-field.tsx
496
- /**
497
- * Internal FieldLabel component with hover-reveal tooltip
498
- */
499
- function FieldLabel({ htmlFor, label, hasErrors, required, tooltip, className }) {
500
- const [isTooltipVisible, setIsTooltipVisible] = React$1.useState(false);
501
- return /* @__PURE__ */ jsxs("div", {
502
- className: "relative flex w-fit items-center space-x-2",
503
- children: [/* @__PURE__ */ jsxs(Label, {
504
- htmlFor,
505
- className: cn("text-foreground/80 gap-0 text-xs font-semibold", hasErrors && "text-destructive", className),
506
- children: [label, required && /* @__PURE__ */ jsx("span", {
507
- className: "text-destructive/80 align-super text-sm leading-0",
508
- "aria-hidden": "true",
509
- children: "*"
510
- })]
511
- }), tooltip && /* @__PURE__ */ jsx(Tooltip, {
512
- message: tooltip,
513
- open: isTooltipVisible,
514
- onOpenChange: setIsTooltipVisible,
515
- side: "bottom",
516
- contentClassName: "max-w-xs text-wrap",
517
- children: /* @__PURE__ */ jsx(Icon, {
518
- icon: CircleHelp,
519
- className: cn("text-ring absolute top-0.5 -right-3 size-3.5 cursor-pointer transition-opacity duration-400")
520
- })
521
- })]
522
- });
523
- }
524
- /**
525
- * Form.Field - Field wrapper component
526
- *
527
- * Provides field context to children with:
528
- * - Automatic label rendering
529
- * - Error display
530
- * - Description text
531
- * - Required indicator
532
- * - Accessibility attributes
533
- *
534
- * Supports two patterns:
535
- * 1. ReactNode children - for standard Form inputs
536
- * 2. Render function - for custom components needing field access
537
- *
538
- * @example Standard usage
539
- * ```tsx
540
- * <Form.Field name="email" label="Email Address" required>
541
- * <Form.Input type="email" />
542
- * </Form.Field>
543
- * ```
544
- *
545
- * @example Render function for custom components
546
- * ```tsx
547
- * <Form.Field name="role" label="Role" required>
548
- * {({ control, meta, fields }) => (
549
- * <CustomSelect
550
- * name={meta.name}
551
- * value={control.value}
552
- * onChange={control.change}
553
- * />
554
- * )}
555
- * </Form.Field>
556
- * ```
557
- */
558
- function FormField({ name, children, label, description, tooltip, required = false, disabled = false, className, labelClassName }) {
559
- const { fields, form, isSubmitting } = useFormContext$1();
560
- const fieldMeta = React$1.useMemo(() => {
561
- const parts = name.split(".");
562
- let current = fields;
563
- for (let i = 0; i < parts.length; i++) {
564
- const part = parts[i];
565
- if (!current) break;
566
- if (/^\d+$/.test(part)) {
567
- const fieldList = current.getFieldList?.();
568
- if (fieldList) {
569
- const item = fieldList[Number.parseInt(part, 10)];
570
- if (i < parts.length - 1 && item?.getFieldset) current = item.getFieldset();
571
- else current = item;
572
- } else current = current[part];
573
- } else if (current[part] !== void 0) current = current[part];
574
- else if (typeof current.getFieldset === "function") current = current.getFieldset()[part];
575
- else current = void 0;
576
- }
577
- return current;
578
- }, [fields, name]);
579
- const errors = fieldMeta?.errors;
580
- const hasErrors = errors && errors.length > 0;
581
- const fieldId = fieldMeta?.id ?? "";
582
- const descriptionId = description ? `${fieldId}-description` : void 0;
583
- const errorId = hasErrors ? `${fieldId}-error` : void 0;
584
- const contextValue = React$1.useMemo(() => ({
585
- name: fieldMeta?.name ?? "",
586
- id: fieldId,
587
- errors,
588
- required,
589
- disabled,
590
- fieldMeta
591
- }), [
592
- fieldMeta,
593
- fieldId,
594
- errors,
595
- required,
596
- disabled
597
- ]);
598
- if (!fieldMeta) {
599
- console.warn(`Form.Field: Field "${name}" not found in form schema`);
600
- return null;
601
- }
602
- const isRenderFunction = typeof children === "function";
603
- const renderContent = () => {
604
- if (isRenderFunction) return /* @__PURE__ */ jsx(FormFieldRenderContent, {
605
- fieldMeta,
606
- fields,
607
- form,
608
- isSubmitting,
609
- required,
610
- disabled,
611
- children
612
- });
613
- return children;
614
- };
615
- return /* @__PURE__ */ jsx(FieldProvider, {
616
- value: contextValue,
617
- children: /* @__PURE__ */ jsxs("div", {
618
- className: cn("flex flex-col space-y-2", className),
619
- children: [
620
- label && /* @__PURE__ */ jsx(FieldLabel, {
621
- htmlFor: fieldId,
622
- label,
623
- hasErrors,
624
- required,
625
- tooltip,
626
- className: labelClassName
627
- }),
628
- renderContent(),
629
- description && /* @__PURE__ */ jsx("p", {
630
- id: descriptionId,
631
- className: "text-ring text-xs text-wrap",
632
- children: description
633
- }),
634
- hasErrors && /* @__PURE__ */ jsx("ul", {
635
- id: errorId,
636
- className: cn("text-destructive space-y-1 text-xs font-medium", errors.length > 1 && "list-disc pl-4"),
637
- role: "alert",
638
- "aria-live": "polite",
639
- children: errors.map((error) => /* @__PURE__ */ jsx("li", {
640
- className: "text-wrap",
641
- children: error
642
- }, error))
643
- })
644
- ]
645
- })
646
- });
647
- }
648
- /**
649
- * Internal component to handle render function pattern
650
- * This is needed because hooks (useInputControl) must be called unconditionally
651
- */
652
- function FormFieldRenderContent({ fieldMeta, fields, form, isSubmitting, required, disabled, children }) {
653
- const control = useInputControl(fieldMeta);
654
- const meta = React$1.useMemo(() => ({
655
- name: fieldMeta.name,
656
- id: fieldMeta.id,
657
- errors: fieldMeta.errors,
658
- required,
659
- disabled
660
- }), [
661
- fieldMeta.name,
662
- fieldMeta.id,
663
- fieldMeta.errors,
664
- required,
665
- disabled
666
- ]);
667
- return /* @__PURE__ */ jsx(Fragment$1, { children: children({
668
- field: fieldMeta,
669
- control: {
670
- value: control.value,
671
- change: control.change,
672
- blur: control.blur,
673
- focus: control.focus
674
- },
675
- meta,
676
- fields,
677
- form,
678
- isSubmitting
679
- }) });
680
- }
681
- FormField.displayName = "Form.Field";
682
- //#endregion
683
- //#region src/components/features/form/components/form-field-array.tsx
684
- /**
685
- * Form.FieldArray - Dynamic array of fields
686
- *
687
- * Provides helpers for managing arrays of form fields.
688
- *
689
- * @example
690
- * ```tsx
691
- * <Form.FieldArray name="members">
692
- * {({ fields, append, remove }) => (
693
- * <>
694
- * {fields.map((field, index) => (
695
- * <div key={field.key} className="flex gap-2">
696
- * <Form.Field name={`members.${index}.email`} label="Email">
697
- * <Form.Input type="email" />
698
- * </Form.Field>
699
- * <Form.Field name={`members.${index}.role`} label="Role">
700
- * <Form.Select>
701
- * <Form.SelectItem value="admin">Admin</Form.SelectItem>
702
- * <Form.SelectItem value="user">User</Form.SelectItem>
703
- * </Form.Select>
704
- * </Form.Field>
705
- * <button type="button" onClick={() => remove(index)}>
706
- * Remove
707
- * </button>
708
- * </div>
709
- * ))}
710
- * <button type="button" onClick={() => append({ email: '', role: 'user' })}>
711
- * Add Member
712
- * </button>
713
- * </>
714
- * )}
715
- * </Form.FieldArray>
716
- * ```
717
- */
718
- function FormFieldArray({ name, children }) {
719
- const { fields, formId } = useFormContext$1();
720
- const form = useFormMetadata(formId);
721
- const arrayField = React$1.useMemo(() => {
722
- const parts = name.split(".");
723
- let current = fields;
724
- for (const part of parts) {
725
- if (!current) break;
726
- if (typeof current.getFieldset === "function") current = current.getFieldset()[part];
727
- else current = current[part];
728
- }
729
- return current;
730
- }, [fields, name]);
731
- const arrayFieldName = arrayField?.name ?? "";
732
- const append = React$1.useCallback((value = {}) => {
733
- if (!arrayFieldName) return;
734
- form.insert({
735
- name: arrayFieldName,
736
- defaultValue: value
737
- });
738
- }, [form, arrayFieldName]);
739
- const remove = React$1.useCallback((index) => {
740
- if (!arrayFieldName) return;
741
- form.remove({
742
- name: arrayFieldName,
743
- index
744
- });
745
- }, [form, arrayFieldName]);
746
- const move = React$1.useCallback((from, to) => {
747
- if (!arrayFieldName) return;
748
- form.reorder({
749
- name: arrayFieldName,
750
- from,
751
- to
752
- });
753
- }, [form, arrayFieldName]);
754
- if (!arrayField) {
755
- console.warn(`Form.FieldArray: Field "${name}" not found in form schema`);
756
- return null;
757
- }
758
- return /* @__PURE__ */ jsx(Fragment$1, { children: children({
759
- fields: (arrayField.getFieldList?.() ?? []).map((field, index) => ({
760
- id: field.id,
761
- key: field.key,
762
- name: `${name}.${index}`
763
- })),
764
- append,
765
- remove,
766
- move
767
- }) });
768
- }
769
- FormFieldArray.displayName = "Form.FieldArray";
770
- //#endregion
771
- //#region src/components/features/form/components/form-input.tsx
772
- /**
773
- * Form.Input - Text input component
774
- *
775
- * Automatically wired to the parent Form.Field context.
776
- *
777
- * @example
778
- * ```tsx
779
- * <Form.Field name="email" label="Email" required>
780
- * <Form.Input type="email" placeholder="john@example.com" />
781
- * </Form.Field>
782
- * ```
783
- */
784
- function FormInput({ ref, type = "text", className, disabled, ...props }) {
785
- const { fieldMeta, disabled: fieldDisabled, errors } = useFieldContext$1();
786
- const inputProps = getInputProps(fieldMeta, { type });
787
- const isDisabled = disabled ?? fieldDisabled;
788
- const hasErrors = errors && errors.length > 0;
789
- return /* @__PURE__ */ jsx(Input, {
790
- ref,
791
- ...inputProps,
792
- ...props,
793
- type,
794
- disabled: isDisabled,
795
- "aria-invalid": hasErrors || void 0,
796
- "aria-describedby": hasErrors ? `${fieldMeta.id}-error` : void 0,
797
- className: cn("!text-xs", className)
798
- });
799
- }
800
- FormInput.displayName = "Form.Input";
801
- //#endregion
802
- //#region src/components/features/form/components/form-radio-group.tsx
803
- /**
804
- * Form.RadioGroup - Radio button group component
805
- *
806
- * Automatically wired to the parent Form.Field context.
807
- *
808
- * @example
809
- * ```tsx
810
- * <Form.Field name="plan" label="Select Plan" required>
811
- * <Form.RadioGroup orientation="vertical">
812
- * <Form.RadioItem value="free" label="Free" description="Basic features" />
813
- * <Form.RadioItem value="pro" label="Pro" description="Advanced features" />
814
- * <Form.RadioItem value="enterprise" label="Enterprise" description="Custom solutions" />
815
- * </Form.RadioGroup>
816
- * </Form.Field>
817
- * ```
818
- */
819
- function FormRadioGroup({ orientation = "vertical", disabled, className, children }) {
820
- const { fieldMeta, disabled: fieldDisabled, errors } = useFieldContext$1();
821
- const control = useInputControl(fieldMeta);
822
- const isDisabled = disabled ?? fieldDisabled;
823
- const hasErrors = errors && errors.length > 0;
824
- const radioValue = Array.isArray(control.value) ? control.value[0] : control.value;
825
- return /* @__PURE__ */ jsx(RadioGroup, {
826
- name: fieldMeta.name,
827
- value: radioValue ?? "",
828
- onValueChange: control.change,
829
- disabled: isDisabled,
830
- "aria-invalid": hasErrors || void 0,
831
- "aria-describedby": hasErrors ? `${fieldMeta.id}-error` : void 0,
832
- className: cn(orientation === "horizontal" ? "flex flex-row space-x-4" : "flex flex-col space-y-2", className),
833
- children
834
- });
835
- }
836
- FormRadioGroup.displayName = "Form.RadioGroup";
837
- /**
838
- * Form.RadioItem - Individual radio button option
839
- *
840
- * @example
841
- * ```tsx
842
- * <Form.RadioItem value="option1" label="Option 1" />
843
- * ```
844
- */
845
- function FormRadioItem({ value, label, description, disabled }) {
846
- const radioId = `radio-${value}`;
847
- return /* @__PURE__ */ jsxs("div", {
848
- className: "flex items-start space-x-2",
849
- children: [/* @__PURE__ */ jsx(RadioGroupItem, {
850
- id: radioId,
851
- value,
852
- disabled,
853
- className: "mt-1"
854
- }), /* @__PURE__ */ jsxs("div", {
855
- className: "flex flex-col",
856
- children: [/* @__PURE__ */ jsx(Label, {
857
- htmlFor: radioId,
858
- className: cn("cursor-pointer text-sm font-normal", disabled && "cursor-not-allowed opacity-70"),
859
- children: label
860
- }), description && /* @__PURE__ */ jsx("span", {
861
- className: "text-muted-foreground text-xs",
862
- children: description
863
- })]
864
- })]
865
- });
866
- }
867
- FormRadioItem.displayName = "Form.RadioItem";
868
- //#endregion
869
- //#region src/components/features/form/components/form-root.tsx
870
- /**
871
- * Form.Root - The root form component
872
- *
873
- * Provides form context to all children with built-in:
874
- * - Zod schema validation
875
- * - Conform integration
876
- * - Optional telemetry callbacks
877
- *
878
- * Supports two patterns:
879
- * 1. ReactNode children - for standard forms
880
- * 2. Render function - for forms needing access to form state
881
- *
882
- * @example Standard usage
883
- * ```tsx
884
- * <Form.Root schema={userSchema} onSubmit={handleSubmit}>
885
- * <Form.Field name="email" label="Email" required>
886
- * <Form.Input type="email" />
887
- * </Form.Field>
888
- * <Form.Submit>Save</Form.Submit>
889
- * </Form.Root>
890
- * ```
891
- *
892
- * @example Render function for form state access
893
- * ```tsx
894
- * <Form.Root schema={userSchema} onSubmit={handleSubmit}>
895
- * {({ form, fields, isSubmitting }) => (
896
- * <>
897
- * <Form.Field name="email" label="Email" required>
898
- * <Form.Input type="email" />
899
- * </Form.Field>
900
- * <Button
901
- * disabled={isSubmitting}
902
- * onClick={() => form.update({ value: { email: '' } })}
903
- * >
904
- * Cancel
905
- * </Button>
906
- * <Form.Submit>Save</Form.Submit>
907
- * </>
908
- * )}
909
- * </Form.Root>
910
- * ```
911
- */
912
- function FormRoot({ schema, children, onSubmit, action, method = "POST", formComponent: FormComp = "form", id, name, defaultValues, mode = "onBlur", isSubmitting: externalIsSubmitting, onError, onSuccess, telemetry, className }) {
913
- const [internalIsSubmitting, setInternalIsSubmitting] = React$1.useState(false);
914
- const isSubmitting = externalIsSubmitting ?? internalIsSubmitting;
915
- const formRef = React$1.useRef(null);
916
- const shouldValidate = mode === "onChange" ? "onInput" : mode;
917
- const [form, fields] = useForm({
918
- id,
919
- constraint: getZodConstraint(schema),
920
- shouldValidate,
921
- shouldRevalidate: mode === "onSubmit" ? "onSubmit" : "onInput",
922
- defaultValue: defaultValues,
923
- onValidate({ formData }) {
924
- return parseWithZod(formData, { schema });
925
- },
926
- async onSubmit(event, { submission }) {
927
- const formName = name || id || "unnamed-form";
928
- telemetry?.onSubmit?.({
929
- formName,
930
- formId: id
931
- });
932
- if (!onSubmit) {
933
- setInternalIsSubmitting(true);
934
- return;
935
- }
936
- event.preventDefault();
937
- if (submission?.status === "success") {
938
- setInternalIsSubmitting(true);
939
- try {
940
- await onSubmit(submission.value);
941
- onSuccess?.(submission.value);
942
- telemetry?.onSuccess?.({
943
- formName,
944
- formId: id
945
- });
946
- } catch (error) {
947
- telemetry?.onError?.({
948
- formName,
949
- formId: id,
950
- error
951
- });
952
- telemetry?.captureError?.(error, {
953
- message: `Form submission error: ${formName}`,
954
- tags: {
955
- "form.name": formName,
956
- "form.id": id || "unknown"
957
- }
958
- });
959
- onError?.(error);
960
- } finally {
961
- setInternalIsSubmitting(false);
962
- }
963
- } else if (submission?.status === "error") {
964
- telemetry?.onValidationError?.({
965
- formName,
966
- formId: id,
967
- fieldErrors: submission.error ?? {}
968
- });
969
- if (onError) {
970
- const { ZodError } = await import("zod");
971
- onError(new ZodError(Object.entries(submission.error ?? {}).flatMap(([path, messages]) => (messages ?? []).map((message) => ({
972
- code: "custom",
973
- path: path.split("."),
974
- message
975
- })))));
976
- }
977
- }
978
- }
979
- });
980
- const submit = React$1.useCallback(() => {
981
- formRef.current?.requestSubmit();
982
- }, []);
983
- const reset = React$1.useCallback(() => {
984
- form.reset();
985
- }, [form]);
986
- const contextValue = React$1.useMemo(() => ({
987
- form,
988
- fields,
989
- isSubmitting,
990
- submit,
991
- reset,
992
- formId: form.id
993
- }), [
994
- form,
995
- fields,
996
- isSubmitting,
997
- submit,
998
- reset
999
- ]);
1000
- const isRenderFunction = typeof children === "function";
1001
- const renderProps = React$1.useMemo(() => ({
1002
- form,
1003
- fields,
1004
- isSubmitting,
1005
- submit,
1006
- reset
1007
- }), [
1008
- form,
1009
- fields,
1010
- isSubmitting,
1011
- submit,
1012
- reset
1013
- ]);
1014
- const renderChildren = () => {
1015
- if (isRenderFunction) return children(renderProps);
1016
- return children;
1017
- };
1018
- const { onSubmit: conformOnSubmit, ...conformFormProps } = getFormProps(form);
1019
- return /* @__PURE__ */ jsx(FormProvider$1, {
1020
- value: contextValue,
1021
- children: /* @__PURE__ */ jsx(FormProvider, {
1022
- context: form.context,
1023
- children: /* @__PURE__ */ jsx(FormComp, {
1024
- ref: formRef,
1025
- ...conformFormProps,
1026
- onSubmit: (e) => {
1027
- e.stopPropagation();
1028
- conformOnSubmit(e);
1029
- },
1030
- method,
1031
- action,
1032
- className: cn("space-y-6", className),
1033
- autoComplete: "off",
1034
- children: renderChildren()
1035
- })
1036
- })
1037
- });
1038
- }
1039
- FormRoot.displayName = "Form.Root";
1040
- //#endregion
1041
- //#region src/components/features/form/components/form-select.tsx
1042
- /**
1043
- * Form.Select - Select dropdown component
1044
- *
1045
- * Automatically wired to the parent Form.Field context.
1046
- *
1047
- * @example
1048
- * ```tsx
1049
- * <Form.Field name="country" label="Country" required>
1050
- * <Form.Select placeholder="Select a country">
1051
- * <Form.SelectItem value="us">United States</Form.SelectItem>
1052
- * <Form.SelectItem value="uk">United Kingdom</Form.SelectItem>
1053
- * <Form.SelectItem value="ca">Canada</Form.SelectItem>
1054
- * </Form.Select>
1055
- * </Form.Field>
1056
- * ```
1057
- */
1058
- function FormSelect({ placeholder, disabled, className, children }) {
1059
- const { fieldMeta, disabled: fieldDisabled, errors } = useFieldContext$1();
1060
- const control = useInputControl(fieldMeta);
1061
- const isDisabled = disabled ?? fieldDisabled;
1062
- const hasErrors = errors && errors.length > 0;
1063
- const selectValue = Array.isArray(control.value) ? control.value[0] : control.value;
1064
- return /* @__PURE__ */ jsxs(Select, {
1065
- name: fieldMeta.name,
1066
- value: selectValue ?? "",
1067
- onValueChange: control.change,
1068
- disabled: isDisabled,
1069
- children: [/* @__PURE__ */ jsx(SelectTrigger, {
1070
- id: fieldMeta.id,
1071
- "aria-invalid": hasErrors || void 0,
1072
- "aria-describedby": hasErrors ? `${fieldMeta.id}-error` : void 0,
1073
- className: cn(className),
1074
- children: /* @__PURE__ */ jsx(SelectValue, { placeholder })
1075
- }), /* @__PURE__ */ jsx(SelectContent, { children })]
1076
- });
1077
- }
1078
- FormSelect.displayName = "Form.Select";
1079
- /**
1080
- * Form.SelectItem - Individual select option
1081
- *
1082
- * @example
1083
- * ```tsx
1084
- * <Form.SelectItem value="option1">Option 1</Form.SelectItem>
1085
- * ```
1086
- */
1087
- function FormSelectItem({ value, children, disabled }) {
1088
- return /* @__PURE__ */ jsx(SelectItem, {
1089
- value,
1090
- disabled,
1091
- children
1092
- });
1093
- }
1094
- FormSelectItem.displayName = "Form.SelectItem";
1095
- //#endregion
1096
- //#region src/components/features/form/components/form-submit.tsx
1097
- /**
1098
- * Form.Submit - Submit button with automatic loading state
1099
- *
1100
- * @example
1101
- * ```tsx
1102
- * <Form.Submit loadingText="Saving...">
1103
- * Save Changes
1104
- * </Form.Submit>
1105
- * ```
1106
- */
1107
- function FormSubmit({ children, loadingText, loading = false, ...props }) {
1108
- const { isSubmitting } = useFormContext$1();
1109
- const isLoading = loading || isSubmitting;
1110
- return /* @__PURE__ */ jsx(Button, {
1111
- htmlType: "submit",
1112
- disabled: props.disabled || isLoading,
1113
- loading: isLoading,
1114
- ...props,
1115
- children: isLoading && loadingText ? loadingText : children
1116
- });
1117
- }
1118
- FormSubmit.displayName = "Form.Submit";
1119
- //#endregion
1120
- //#region src/components/features/form/components/form-switch.tsx
1121
- /**
1122
- * Form.Switch - Toggle switch component
1123
- *
1124
- * Automatically wired to the parent Form.Field context.
1125
- *
1126
- * @example
1127
- * ```tsx
1128
- * <Form.Field name="notifications">
1129
- * <Form.Switch label="Enable email notifications" />
1130
- * </Form.Field>
1131
- * ```
1132
- */
1133
- function FormSwitch({ label, disabled, className }) {
1134
- const { fieldMeta, disabled: fieldDisabled, errors } = useFieldContext$1();
1135
- const control = useInputControl(fieldMeta);
1136
- const isDisabled = disabled ?? fieldDisabled;
1137
- const hasErrors = errors && errors.length > 0;
1138
- const isChecked = control.value === "on" || control.value === "true";
1139
- const handleCheckedChange = (checked) => {
1140
- control.change(checked ? "on" : "");
1141
- };
1142
- const switchId = fieldMeta.id;
1143
- return /* @__PURE__ */ jsxs("div", {
1144
- className: cn("flex items-center space-x-2", className),
1145
- children: [/* @__PURE__ */ jsx(Switch, {
1146
- id: switchId,
1147
- name: fieldMeta.name,
1148
- checked: isChecked,
1149
- onCheckedChange: handleCheckedChange,
1150
- disabled: isDisabled,
1151
- "aria-invalid": hasErrors || void 0,
1152
- "aria-describedby": hasErrors ? `${fieldMeta.id}-error` : void 0
1153
- }), label && /* @__PURE__ */ jsx(Label, {
1154
- htmlFor: switchId,
1155
- className: cn("cursor-pointer text-sm font-normal", isDisabled && "cursor-not-allowed opacity-70"),
1156
- children: label
1157
- })]
1158
- });
1159
- }
1160
- FormSwitch.displayName = "Form.Switch";
1161
- //#endregion
1162
- //#region src/components/features/form/components/form-textarea.tsx
1163
- /**
1164
- * Form.Textarea - Multi-line text input component
1165
- *
1166
- * Automatically wired to the parent Form.Field context.
1167
- *
1168
- * @example
1169
- * ```tsx
1170
- * <Form.Field name="bio" label="Bio">
1171
- * <Form.Textarea rows={4} placeholder="Tell us about yourself..." />
1172
- * </Form.Field>
1173
- * ```
1174
- */
1175
- function FormTextarea({ ref, className, disabled, rows = 3, ...props }) {
1176
- const { fieldMeta, disabled: fieldDisabled, errors } = useFieldContext$1();
1177
- const textareaProps = getTextareaProps(fieldMeta);
1178
- const isDisabled = disabled ?? fieldDisabled;
1179
- const hasErrors = errors && errors.length > 0;
1180
- return /* @__PURE__ */ jsx(Textarea, {
1181
- ref,
1182
- ...textareaProps,
1183
- ...props,
1184
- rows,
1185
- disabled: isDisabled,
1186
- "aria-invalid": hasErrors || void 0,
1187
- "aria-describedby": hasErrors ? `${fieldMeta.id}-error` : void 0,
1188
- className: cn(className)
1189
- });
1190
- }
1191
- FormTextarea.displayName = "Form.Textarea";
1192
- //#endregion
1193
- //#region src/components/features/form/hooks/use-watch.ts
1194
- /**
1195
- * Hook to watch a field's value
1196
- * Triggers re-render when the watched field value changes
1197
- *
1198
- * @example
1199
- * ```tsx
1200
- * function ConditionalField() {
1201
- * const contactMethod = useWatch('contactMethod');
1202
- *
1203
- * if (contactMethod === 'email') {
1204
- * return <Form.Field name="email"><Form.Input type="email" /></Form.Field>;
1205
- * }
1206
- *
1207
- * if (contactMethod === 'phone') {
1208
- * return <Form.Field name="phone"><Form.Input type="tel" /></Form.Field>;
1209
- * }
1210
- *
1211
- * return null;
1212
- * }
1213
- * ```
1214
- */
1215
- function useWatch(name) {
1216
- const { fields } = useFormContext$1();
1217
- return useInputControl(React$1.useMemo(() => {
1218
- const parts = name.split(".");
1219
- let current = fields;
1220
- for (const part of parts) {
1221
- if (!current) break;
1222
- if (/^\d+$/.test(part)) {
1223
- const fieldList = current.getFieldList?.();
1224
- if (fieldList) current = fieldList[Number.parseInt(part, 10)]?.getFieldset?.();
1225
- else current = current[part];
1226
- } else if (typeof current.getFieldset === "function") current = current.getFieldset()[part];
1227
- else current = current[part];
1228
- }
1229
- return current;
1230
- }, [fields, name])).value;
1231
- }
1232
- /**
1233
- * Hook to watch multiple fields at once
1234
- *
1235
- * @example
1236
- * ```tsx
1237
- * function Summary() {
1238
- * const values = useWatchAll(['firstName', 'lastName', 'email']);
1239
- *
1240
- * return (
1241
- * <div>
1242
- * Name: {values.firstName} {values.lastName}
1243
- * Email: {values.email}
1244
- * </div>
1245
- * );
1246
- * }
1247
- * ```
1248
- */
1249
- function useWatchAll(names) {
1250
- const { fields } = useFormContext$1();
1251
- return React$1.useMemo(() => {
1252
- const result = {};
1253
- for (const name of names) {
1254
- const parts = name.split(".");
1255
- let current = fields;
1256
- for (const part of parts) {
1257
- if (!current) break;
1258
- if (/^\d+$/.test(part)) {
1259
- const fieldList = current.getFieldList?.();
1260
- if (fieldList) current = fieldList[Number.parseInt(part, 10)]?.getFieldset?.();
1261
- else current = current[part];
1262
- } else if (typeof current.getFieldset === "function") current = current.getFieldset()[part];
1263
- else current = current[part];
1264
- }
1265
- if (current) result[name] = current.value;
1266
- }
1267
- return result;
1268
- }, [fields, names]);
1269
- }
1270
- //#endregion
1271
- //#region src/components/features/form/components/form-when.tsx
1272
- /**
1273
- * Form.When - Conditional rendering based on field values
1274
- *
1275
- * Renders children only when the specified field matches the condition.
1276
- *
1277
- * @example
1278
- * ```tsx
1279
- * // Render when field equals value
1280
- * <Form.When field="contactMethod" is="email">
1281
- * <Form.Field name="email"><Form.Input type="email" /></Form.Field>
1282
- * </Form.When>
1283
- *
1284
- * // Render when field does not equal value
1285
- * <Form.When field="contactMethod" isNot="none">
1286
- * <Form.Field name="contact"><Form.Input /></Form.Field>
1287
- * </Form.When>
1288
- *
1289
- * // Render when field value is in array
1290
- * <Form.When field="role" in={['admin', 'moderator']}>
1291
- * <Form.Field name="permissions"><Form.Input /></Form.Field>
1292
- * </Form.When>
1293
- *
1294
- * // Render when field value is not in array
1295
- * <Form.When field="status" notIn={['archived', 'deleted']}>
1296
- * <Form.Field name="actions"><Form.Input /></Form.Field>
1297
- * </Form.When>
1298
- * ```
1299
- */
1300
- function FormWhen({ field, is, isNot, in: inArray, notIn, children }) {
1301
- const value = useWatch(field);
1302
- let shouldRender = true;
1303
- if (is !== void 0) shouldRender = value === is;
1304
- if (isNot !== void 0 && shouldRender) shouldRender = value !== isNot;
1305
- if (inArray !== void 0 && shouldRender) shouldRender = inArray.includes(value);
1306
- if (notIn !== void 0 && shouldRender) shouldRender = !notIn.includes(value);
1307
- if (!shouldRender) return null;
1308
- return /* @__PURE__ */ jsx(Fragment$1, { children });
1309
- }
1310
- FormWhen.displayName = "Form.When";
1311
- //#endregion
1312
- //#region src/components/features/form/components/stepper/form-stepper.tsx
1313
- const FormStepperContext = React$1.createContext(null);
1314
- function useFormStepperContext() {
1315
- const context = React$1.use(FormStepperContext);
1316
- if (!context) throw new Error("useFormStepperContext must be used within a Form.Stepper component");
1317
- return context;
1318
- }
1319
- /**
1320
- * Recursively unwrap ZodIntersection (from .and()) to extract the base ZodObject.
1321
- *
1322
- * Zod v4 schema types use `def.type` as a string discriminant:
1323
- * - "intersection" (from .and()): merge left + right base objects
1324
- * - "object": return directly
1325
- *
1326
- * Note: In Zod v4, .superRefine() and .refine() return `this` (no wrapper),
1327
- * so only ZodIntersection needs unwrapping.
1328
- */
1329
- function getBaseObject(schema) {
1330
- if (schema.def.type === "intersection") {
1331
- const intersectionDef = schema.def;
1332
- const left = getBaseObject(intersectionDef.left);
1333
- const right = getBaseObject(intersectionDef.right);
1334
- return left.merge(right);
1335
- }
1336
- if (schema.def.type !== "object") {
1337
- console.warn(`mergeSchemas: expected ZodObject or ZodIntersection but got "${schema.def.type}". Falling back to empty object.`);
1338
- return z.object({});
1339
- }
1340
- return schema;
1341
- }
1342
- /**
1343
- * Merge multiple zod schemas into one ZodObject for HTML constraint generation.
1344
- * Handles ZodIntersection (.and()) by unwrapping to base ZodObject shapes.
1345
- * Per-step validation still uses the original schemas with all refinements intact.
1346
- */
1347
- function mergeSchemas(steps) {
1348
- if (steps.length === 0) throw new Error("Form.Stepper requires at least one step");
1349
- return steps.reduce((acc, step, index) => {
1350
- const base = getBaseObject(step.schema);
1351
- if (index === 0) return base;
1352
- return acc.merge(base);
1353
- }, {});
1354
- }
1355
- /**
1356
- * Convert StepConfig[] to Stepperize step format
1357
- */
1358
- function toStepperizeSteps(steps) {
1359
- return steps.map((step) => ({
1360
- id: step.id,
1361
- label: step.label,
1362
- description: step.description
1363
- }));
1364
- }
1365
- /**
1366
- * Form.Stepper - Multi-step form container
1367
- *
1368
- * Uses Stepperize internally for step navigation and a single Conform form
1369
- * instance for all steps. Schemas are auto-merged for unified validation.
1370
- *
1371
- * @example
1372
- * ```tsx
1373
- * const steps = [
1374
- * { id: 'account', label: 'Account', schema: accountSchema },
1375
- * { id: 'profile', label: 'Profile', schema: profileSchema },
1376
- * ];
1377
- *
1378
- * <Form.Stepper steps={steps} onComplete={handleComplete}>
1379
- * <Form.StepperNavigation />
1380
- *
1381
- * <Form.Step id="account">
1382
- * <Form.Field name="email" label="Email" required>
1383
- * <Form.Input type="email" />
1384
- * </Form.Field>
1385
- * </Form.Step>
1386
- *
1387
- * <Form.Step id="profile">
1388
- * <Form.Field name="name" label="Full Name" required>
1389
- * <Form.Input />
1390
- * </Form.Field>
1391
- * </Form.Step>
1392
- *
1393
- * <Form.StepperControls />
1394
- * </Form.Stepper>
1395
- * ```
1396
- */
1397
- function FormStepper({ steps, children, onComplete, onStepChange, initialStep, className, defaultValues, id, formComponent }) {
1398
- const stepperDef = React$1.useMemo(() => {
1399
- return defineStepper(...toStepperizeSteps(steps));
1400
- }, [steps]);
1401
- const initialStepIndex = React$1.useMemo(() => {
1402
- if (!initialStep) return void 0;
1403
- const index = steps.findIndex((s) => s.id === initialStep);
1404
- return index >= 0 ? steps[index].id : void 0;
1405
- }, [initialStep, steps]);
1406
- const { Stepper } = stepperDef;
1407
- const providerProps = initialStepIndex ? { initialStep: initialStepIndex } : {};
1408
- return /* @__PURE__ */ jsx(Stepper.Provider, {
1409
- ...providerProps,
1410
- children: /* @__PURE__ */ jsx(FormStepperContent, {
1411
- steps,
1412
- stepperDef,
1413
- onComplete,
1414
- onStepChange,
1415
- className,
1416
- defaultValues,
1417
- id,
1418
- formComponent,
1419
- children
1420
- })
1421
- });
1422
- }
1423
- FormStepper.displayName = "Form.Stepper";
1424
- function FormStepperContent({ steps, stepperDef, children, onComplete, onStepChange, className, defaultValues, id, formComponent }) {
1425
- const { useStepper } = stepperDef;
1426
- const stepper = useStepper();
1427
- return /* @__PURE__ */ jsx(StepForm, {
1428
- steps,
1429
- stepper,
1430
- currentStepConfig: React$1.useMemo(() => steps.find((s) => s.id === stepper.state.current.data.id) ?? steps[0], [steps, stepper.state.current.data.id]),
1431
- combinedSchema: React$1.useMemo(() => mergeSchemas(steps), [steps]),
1432
- storedValues: React$1.useMemo(() => {
1433
- const allMetadata = steps.reduce((acc, step) => ({
1434
- ...acc,
1435
- ...stepper.metadata.get(step.id) || {}
1436
- }), {});
1437
- return {
1438
- ...defaultValues,
1439
- ...allMetadata
1440
- };
1441
- }, [
1442
- steps,
1443
- stepper,
1444
- defaultValues,
1445
- stepper.state.current.data.id
1446
- ]),
1447
- onComplete,
1448
- onStepChange,
1449
- className,
1450
- id,
1451
- formComponent,
1452
- children
1453
- }, stepper.state.current.data.id);
1454
- }
1455
- function StepForm({ steps, stepper, currentStepConfig, combinedSchema, storedValues, children, onComplete, onStepChange, className, id, formComponent: FormComp = "form" }) {
1456
- const [isSubmitting, setIsSubmitting] = React$1.useState(false);
1457
- const formRef = React$1.useRef(null);
1458
- const [form, fields] = useForm({
1459
- id: id ?? "stepper-form",
1460
- constraint: getZodConstraint(combinedSchema),
1461
- shouldValidate: "onBlur",
1462
- shouldRevalidate: "onInput",
1463
- defaultValue: storedValues,
1464
- onValidate({ formData }) {
1465
- return parseWithZod(formData, { schema: currentStepConfig.schema });
1466
- },
1467
- async onSubmit(event, { submission }) {
1468
- event.preventDefault();
1469
- if (submission?.status !== "success") return;
1470
- if (submission.value) stepper.metadata.set(stepper.state.current.data.id, submission.value);
1471
- if (stepper.state.isLast) {
1472
- setIsSubmitting(true);
1473
- try {
1474
- await onComplete({
1475
- ...steps.reduce((acc, step) => ({
1476
- ...acc,
1477
- ...stepper.metadata.get(step.id) || {}
1478
- }), {}),
1479
- ...submission.value
1480
- });
1481
- } catch (error) {
1482
- console.error("Stepper form completion error:", error);
1483
- } finally {
1484
- setIsSubmitting(false);
1485
- }
1486
- } else {
1487
- const nextStepId = stepper.lookup.getNext(stepper.state.current.data.id)?.id;
1488
- if (nextStepId) {
1489
- stepper.navigation.goTo(nextStepId);
1490
- onStepChange?.(nextStepId, "next");
1491
- }
1492
- }
1493
- }
1494
- });
1495
- const next = React$1.useCallback(() => {
1496
- formRef.current?.requestSubmit();
1497
- }, []);
1498
- const prev = React$1.useCallback(() => {
1499
- if (formRef.current) {
1500
- const formData = new FormData(formRef.current);
1501
- const currentData = {};
1502
- formData.forEach((value, key) => {
1503
- if (!key.startsWith("$")) currentData[key] = value;
1504
- });
1505
- if (Object.keys(currentData).length > 0) stepper.metadata.set(stepper.state.current.data.id, currentData);
1506
- }
1507
- const prevStepId = stepper.lookup.getPrev(stepper.state.current.data.id)?.id;
1508
- if (prevStepId) {
1509
- stepper.navigation.goTo(prevStepId);
1510
- onStepChange?.(prevStepId, "prev");
1511
- }
1512
- }, [stepper, onStepChange]);
1513
- const goTo = React$1.useCallback((stepId) => {
1514
- const currentIndex = stepper.lookup.getIndex(stepper.state.current.data.id);
1515
- if (stepper.lookup.getIndex(stepId) < currentIndex) {
1516
- stepper.navigation.goTo(stepId);
1517
- onStepChange?.(stepId, "prev");
1518
- }
1519
- }, [stepper, onStepChange]);
1520
- const getStepData = React$1.useCallback((stepId) => stepper.metadata.get(stepId), [stepper]);
1521
- const getAllStepData = React$1.useCallback(() => {
1522
- return steps.reduce((acc, step) => ({
1523
- ...acc,
1524
- ...stepper.metadata.get(step.id) || {}
1525
- }), {});
1526
- }, [steps, stepper]);
1527
- const stepperContextValue = React$1.useMemo(() => ({
1528
- steps,
1529
- current: currentStepConfig,
1530
- currentIndex: stepper.lookup.getIndex(stepper.state.current.data.id),
1531
- next,
1532
- prev,
1533
- goTo,
1534
- isFirst: stepper.state.isFirst,
1535
- isLast: stepper.state.isLast,
1536
- getStepData,
1537
- getAllStepData,
1538
- utils: { getIndex: (id) => stepper.lookup.getIndex(id) }
1539
- }), [
1540
- steps,
1541
- currentStepConfig,
1542
- stepper,
1543
- next,
1544
- prev,
1545
- goTo,
1546
- getStepData,
1547
- getAllStepData
1548
- ]);
1549
- const formContextValue = React$1.useMemo(() => ({
1550
- form,
1551
- fields,
1552
- isSubmitting,
1553
- submit: () => formRef.current?.requestSubmit(),
1554
- reset: () => form.reset(),
1555
- formId: form.id
1556
- }), [
1557
- form,
1558
- fields,
1559
- isSubmitting
1560
- ]);
1561
- const renderProps = {
1562
- steps,
1563
- current: currentStepConfig,
1564
- currentIndex: stepper.lookup.getIndex(stepper.state.current.data.id),
1565
- next,
1566
- prev,
1567
- goTo,
1568
- isFirst: stepper.state.isFirst,
1569
- isLast: stepper.state.isLast,
1570
- getStepData,
1571
- getAllStepData
1572
- };
1573
- const resolvedChildren = typeof children === "function" ? children(renderProps) : children;
1574
- return /* @__PURE__ */ jsx(FormStepperContext, {
1575
- value: stepperContextValue,
1576
- children: /* @__PURE__ */ jsx(FormProvider$1, {
1577
- value: formContextValue,
1578
- children: /* @__PURE__ */ jsx(FormProvider, {
1579
- context: form.context,
1580
- children: /* @__PURE__ */ jsx(FormComp, {
1581
- ref: formRef,
1582
- ...getFormProps(form),
1583
- method: "POST",
1584
- className: cn("space-y-6", className),
1585
- autoComplete: "off",
1586
- children: resolvedChildren
1587
- })
1588
- })
1589
- })
1590
- });
1591
- }
1592
- //#endregion
1593
- //#region src/components/features/form/components/stepper/form-step.tsx
1594
- /**
1595
- * Form.Step - Individual step content container
1596
- *
1597
- * Only renders its children when the step is active.
1598
- * Works with the single-form architecture - fields remain registered
1599
- * even when unmounted, preserving their values.
1600
- *
1601
- * @example
1602
- * ```tsx
1603
- * <Form.Step id="account">
1604
- * <Form.Field name="email" label="Email" required>
1605
- * <Form.Input type="email" />
1606
- * </Form.Field>
1607
- * </Form.Step>
1608
- * ```
1609
- */
1610
- function FormStep({ id, children }) {
1611
- const { current } = useFormStepperContext();
1612
- if (current.id !== id) return null;
1613
- return /* @__PURE__ */ jsx(Fragment$1, { children });
1614
- }
1615
- FormStep.displayName = "Form.Step";
1616
- //#endregion
1617
- //#region src/components/features/form/components/stepper/stepper-controls.tsx
1618
- /**
1619
- * Form.StepperControls - Navigation buttons (Previous/Next/Submit)
1620
- *
1621
- * Provides Previous and Next/Submit buttons for navigating between steps.
1622
- * The Next button triggers form validation before advancing.
1623
- * The Previous button navigates back without validation.
1624
- *
1625
- * @example
1626
- * ```tsx
1627
- * <Form.StepperControls
1628
- * prevLabel={(isFirst) => isFirst ? 'Cancel' : 'Previous'}
1629
- * nextLabel={(isLast) => isLast ? 'Submit' : 'Next'}
1630
- * loadingText="Creating..."
1631
- * onCancel={() => setOpen(false)}
1632
- * />
1633
- * ```
1634
- *
1635
- * @example With external loading state
1636
- * ```tsx
1637
- * <Form.StepperControls
1638
- * loading={fetcher.state === 'submitting'}
1639
- * disabled={!isValid}
1640
- * loadingText="Saving..."
1641
- * />
1642
- * ```
1643
- */
1644
- function StepperControls({ prevLabel = "Previous", nextLabel = (isLast) => isLast ? "Submit" : "Next", loadingText = "Submitting...", showPrev = true, loading, disabled, onPrev, onCancel, className }) {
1645
- const { prev, isFirst, isLast } = useFormStepperContext();
1646
- const { isSubmitting: formIsSubmitting } = useFormContext$1();
1647
- const isLoading = loading ?? formIsSubmitting;
1648
- const isDisabled = disabled ?? false;
1649
- const getPrevLabel = () => {
1650
- if (typeof prevLabel === "function") return prevLabel(isFirst);
1651
- return prevLabel;
1652
- };
1653
- const getNextLabel = () => {
1654
- if (typeof nextLabel === "function") return nextLabel(isLast);
1655
- return nextLabel;
1656
- };
1657
- const handlePrev = () => {
1658
- if (isFirst && onCancel) onCancel();
1659
- else {
1660
- onPrev?.();
1661
- prev();
1662
- }
1663
- };
1664
- return /* @__PURE__ */ jsxs("div", {
1665
- className: cn("flex items-center justify-between gap-3", className),
1666
- children: [/* @__PURE__ */ jsx("div", { children: showPrev && /* @__PURE__ */ jsx(Button, {
1667
- htmlType: "button",
1668
- type: "quaternary",
1669
- theme: "outline",
1670
- size: "small",
1671
- onClick: handlePrev,
1672
- disabled: isLoading || isDisabled,
1673
- children: getPrevLabel()
1674
- }) }), /* @__PURE__ */ jsx(Button, {
1675
- htmlType: "submit",
1676
- type: "primary",
1677
- size: "small",
1678
- loading: isLoading,
1679
- disabled: isLoading || isDisabled,
1680
- children: isLoading && isLast ? loadingText : getNextLabel()
1681
- })]
1682
- });
1683
- }
1684
- StepperControls.displayName = "Form.StepperControls";
1685
- //#endregion
1686
- //#region src/components/features/form/components/stepper/stepper-navigation.tsx
1687
- /**
1688
- * Form.StepperNavigation - Step indicators/progress
1689
- *
1690
- * Displays visual step indicators showing current progress through the form.
1691
- * Supports horizontal and vertical variants with optional label orientation.
1692
- *
1693
- * @example
1694
- * ```tsx
1695
- * <Form.StepperNavigation variant="horizontal" labelOrientation="vertical" />
1696
- * ```
1697
- */
1698
- function StepperNavigation({ variant = "horizontal", labelOrientation = "vertical", className }) {
1699
- const { steps, currentIndex } = useFormStepperContext();
1700
- if (variant === "horizontal" && labelOrientation === "vertical") return /* @__PURE__ */ jsx("nav", {
1701
- "aria-label": "Form steps",
1702
- className: cn("flex flex-row items-start justify-between", className),
1703
- children: steps.map((step, index) => {
1704
- const isActive = index === currentIndex;
1705
- const isCompleted = index < currentIndex;
1706
- return /* @__PURE__ */ jsxs("div", {
1707
- className: "relative flex flex-1 flex-col items-center",
1708
- children: [
1709
- !(index === steps.length - 1) && /* @__PURE__ */ jsx("div", { className: "bg-stepper-line absolute top-4 right-[calc(-50%+20px)] left-[calc(50%+20px)] h-0.5" }),
1710
- /* @__PURE__ */ jsx("div", {
1711
- className: cn("relative z-10 flex h-8 w-8 items-center justify-center rounded-full border bg-transparent text-sm font-medium transition-colors", isActive && "border-primary bg-primary text-primary-foreground", isCompleted && "border-tertiary-foreground bg-tertiary-foreground text-tertiary", !isActive && !isCompleted && "border-stepper-label text-stepper-label"),
1712
- "aria-current": isActive ? "step" : void 0,
1713
- children: isCompleted ? /* @__PURE__ */ jsx(CheckIcon, { className: "text-tertiary h-4 w-4" }) : index + 1
1714
- }),
1715
- /* @__PURE__ */ jsxs("div", {
1716
- className: "mt-1",
1717
- children: [/* @__PURE__ */ jsx("span", {
1718
- className: cn("text-xs font-medium", isActive && "text-foreground", isCompleted && "text-stepper-label", !isActive && !isCompleted && "text-stepper-label"),
1719
- children: step.label
1720
- }), step.description && /* @__PURE__ */ jsx("p", {
1721
- className: "text-muted-foreground mt-0.5 text-xs",
1722
- children: step.description
1723
- })]
1724
- })
1725
- ]
1726
- }, step.id);
1727
- })
1728
- });
1729
- if (variant === "horizontal") return /* @__PURE__ */ jsx("nav", {
1730
- "aria-label": "Form steps",
1731
- className: cn("flex flex-row items-center", className),
1732
- children: steps.map((step, index) => {
1733
- const isActive = index === currentIndex;
1734
- const isCompleted = index < currentIndex;
1735
- const isLast = index === steps.length - 1;
1736
- return /* @__PURE__ */ jsxs(React$1.Fragment, { children: [/* @__PURE__ */ jsxs("div", {
1737
- className: "flex items-center",
1738
- children: [/* @__PURE__ */ jsx("div", {
1739
- className: cn("flex h-8 w-8 items-center justify-center rounded-full border text-sm font-medium transition-colors", isActive && "border-primary bg-primary text-primary-foreground", isCompleted && "border-tertiary-foreground bg-tertiary-foreground text-tertiary", !isActive && !isCompleted && "border-stepper-label text-stepper-label"),
1740
- "aria-current": isActive ? "step" : void 0,
1741
- children: isCompleted ? /* @__PURE__ */ jsx(CheckIcon, { className: "text-tertiary size-4" }) : index + 1
1742
- }), /* @__PURE__ */ jsx("div", {
1743
- className: "ml-2",
1744
- children: /* @__PURE__ */ jsx("span", {
1745
- className: cn("text-sm font-medium", isActive && "text-foreground", isCompleted && "text-stepper-label", !isActive && !isCompleted && "text-stepper-label"),
1746
- children: step.label
1747
- })
1748
- })]
1749
- }), !isLast && /* @__PURE__ */ jsx("div", { className: "bg-stepper-line mx-4 h-0.5 min-w-8 flex-1" })] }, step.id);
1750
- })
1751
- });
1752
- return /* @__PURE__ */ jsx("nav", {
1753
- "aria-label": "Form steps",
1754
- className: cn("flex flex-col", className),
1755
- children: steps.map((step, index) => {
1756
- const isActive = index === currentIndex;
1757
- const isCompleted = index < currentIndex;
1758
- const isLast = index === steps.length - 1;
1759
- return /* @__PURE__ */ jsxs("div", {
1760
- className: "flex flex-row",
1761
- children: [/* @__PURE__ */ jsxs("div", {
1762
- className: "flex flex-col items-center",
1763
- children: [/* @__PURE__ */ jsx("div", {
1764
- className: cn("flex h-8 w-8 items-center justify-center rounded-full border text-sm font-medium transition-colors", isActive && "border-primary bg-primary text-primary-foreground", isCompleted && "border-tertiary-foreground bg-tertiary-foreground text-tertiary", !isActive && !isCompleted && "border-stepper-label text-stepper-label"),
1765
- "aria-current": isActive ? "step" : void 0,
1766
- children: isCompleted ? /* @__PURE__ */ jsx(CheckIcon, { className: "text-tertiary size-4" }) : index + 1
1767
- }), !isLast && /* @__PURE__ */ jsx("div", { className: "bg-stepper-line my-1 min-h-8 w-0.5 flex-1" })]
1768
- }), /* @__PURE__ */ jsxs("div", {
1769
- className: "ml-3 pb-8",
1770
- children: [/* @__PURE__ */ jsx("span", {
1771
- className: cn("text-sm font-medium", isActive && "text-foreground", isCompleted && "text-stepper-label", !isActive && !isCompleted && "text-stepper-label"),
1772
- children: step.label
1773
- }), step.description && /* @__PURE__ */ jsx("p", {
1774
- className: "text-muted-foreground mt-0.5 text-xs",
1775
- children: step.description
1776
- })]
1777
- })]
1778
- }, step.id);
1779
- })
1780
- });
1781
- }
1782
- StepperNavigation.displayName = "Form.StepperNavigation";
1783
- //#endregion
1784
- //#region src/components/features/form/components/form-input-group.tsx
1785
- /**
1786
- * Form.Input - Text input component
1787
- *
1788
- * Automatically wired to the parent Form.Field context.
1789
- *
1790
- * @example
1791
- * ```tsx
1792
- * <Form.Field name="email" label="Email" required>
1793
- * <Form.Input type="email" placeholder="john@example.com" />
1794
- * </Form.Field>
1795
- * ```
1796
- */
1797
- function FormInputGroup({ ref, type = "text", className, disabled, ...props }) {
1798
- const { fieldMeta, disabled: fieldDisabled, errors } = useFieldContext$1();
1799
- const inputProps = getInputProps(fieldMeta, { type });
1800
- const isDisabled = disabled ?? fieldDisabled;
1801
- const hasErrors = errors && errors.length > 0;
1802
- return /* @__PURE__ */ jsx(InputWithAddons, {
1803
- ref,
1804
- ...inputProps,
1805
- ...props,
1806
- type,
1807
- disabled: isDisabled,
1808
- "aria-invalid": hasErrors || void 0,
1809
- "aria-describedby": hasErrors ? `${fieldMeta.id}-error` : void 0,
1810
- className: cn("text-xs!", className)
1811
- });
1812
- }
1813
- FormInputGroup.displayName = "Form.InputGroup";
1814
- //#endregion
1815
- //#region src/components/features/form/hooks/use-field.ts
1816
- /**
1817
- * Hook to access and control a specific field
1818
- * Provides field metadata, control methods, and errors
1819
- *
1820
- * @example
1821
- * ```tsx
1822
- * function MyCustomInput({ name }: { name: string }) {
1823
- * const { field, control, meta, errors } = useField(name);
1824
- *
1825
- * return (
1826
- * <div>
1827
- * <input
1828
- * name={meta.name}
1829
- * id={meta.id}
1830
- * value={control.value ?? ''}
1831
- * onChange={(e) => control.change(e.target.value)}
1832
- * onBlur={control.blur}
1833
- * aria-invalid={!!errors?.length}
1834
- * />
1835
- * {errors?.map((error) => (
1836
- * <span key={error} className="text-red-500">{error}</span>
1837
- * ))}
1838
- * </div>
1839
- * );
1840
- * }
1841
- * ```
1842
- */
1843
- function useField(name) {
1844
- const { fields } = useFormContext$1();
1845
- const field = React$1.useMemo(() => {
1846
- const parts = name.split(".");
1847
- let current = fields;
1848
- for (let i = 0; i < parts.length; i++) {
1849
- const part = parts[i];
1850
- if (!current) break;
1851
- if (/^\d+$/.test(part)) {
1852
- const fieldList = current.getFieldList?.();
1853
- if (fieldList) {
1854
- const item = fieldList[Number.parseInt(part, 10)];
1855
- if (i < parts.length - 1 && item?.getFieldset) current = item.getFieldset();
1856
- else current = item;
1857
- } else current = current[part];
1858
- } else if (current[part] !== void 0) current = current[part];
1859
- else if (typeof current.getFieldset === "function") current = current.getFieldset()[part];
1860
- else current = void 0;
1861
- }
1862
- return current;
1863
- }, [fields, name]);
1864
- if (!field) throw new Error(`Field "${name}" not found in form. Make sure the field name matches your schema.`);
1865
- const control = useInputControl(field);
1866
- const controlValue = Array.isArray(control.value) ? control.value[0] : control.value;
1867
- const meta = React$1.useMemo(() => ({
1868
- name: field.name,
1869
- id: field.id,
1870
- errors: field.errors,
1871
- required: field.required ?? false,
1872
- disabled: field.disabled ?? false
1873
- }), [
1874
- field.name,
1875
- field.id,
1876
- field.errors,
1877
- field.required,
1878
- field.disabled
1879
- ]);
1880
- return {
1881
- field,
1882
- control: {
1883
- value: controlValue,
1884
- change: control.change,
1885
- blur: control.blur,
1886
- focus: control.focus
1887
- },
1888
- meta,
1889
- errors: field.errors
1890
- };
1891
- }
1892
- //#endregion
1893
- //#region src/components/features/form/hooks/use-field-context.ts
1894
- /**
1895
- * Hook to access the current field context
1896
- * Must be used within a Form.Field component
1897
- *
1898
- * @example
1899
- * ```tsx
1900
- * function MyInput() {
1901
- * const { name, id, errors, required, disabled, fieldMeta } = useFieldContext();
1902
- *
1903
- * return (
1904
- * <input
1905
- * name={name}
1906
- * id={id}
1907
- * required={required}
1908
- * disabled={disabled}
1909
- * aria-invalid={!!errors?.length}
1910
- * />
1911
- * );
1912
- * }
1913
- * ```
1914
- */
1915
- function useFieldContext() {
1916
- return useFieldContext$1();
1917
- }
1918
- //#endregion
1919
- //#region src/components/features/form/hooks/use-form-context.ts
1920
- /**
1921
- * Hook to access the form context
1922
- *
1923
- * @example
1924
- * ```tsx
1925
- * function MyComponent() {
1926
- * const { form, fields, isSubmitting, submit, reset } = useFormContext();
1927
- *
1928
- * return (
1929
- * <button onClick={submit} disabled={isSubmitting}>
1930
- * Submit
1931
- * </button>
1932
- * );
1933
- * }
1934
- * ```
1935
- */
1936
- function useFormContext() {
1937
- return useFormContext$1();
1938
- }
1939
- //#endregion
1940
- //#region src/components/features/form/hooks/use-stepper.ts
1941
- /**
1942
- * Hook to access the stepper context
1943
- * Must be used within a Form.Stepper component
1944
- *
1945
- * @example
1946
- * ```tsx
1947
- * function StepContent() {
1948
- * const {
1949
- * current,
1950
- * currentIndex,
1951
- * steps,
1952
- * next,
1953
- * prev,
1954
- * goTo,
1955
- * isFirst,
1956
- * isLast,
1957
- * } = useStepper();
1958
- *
1959
- * return (
1960
- * <div>
1961
- * <h2>Step {currentIndex + 1}: {current.label}</h2>
1962
- * <button onClick={prev} disabled={isFirst}>Previous</button>
1963
- * <button onClick={next} disabled={isLast}>Next</button>
1964
- * </div>
1965
- * );
1966
- * }
1967
- * ```
1968
- */
1969
- function useStepper() {
1970
- const context = useFormStepperContext();
1971
- return {
1972
- steps: context.steps,
1973
- current: context.current,
1974
- currentIndex: context.currentIndex,
1975
- next: context.next,
1976
- prev: context.prev,
1977
- goTo: context.goTo,
1978
- isFirst: context.isFirst,
1979
- isLast: context.isLast,
1980
- getStepData: context.getStepData,
1981
- getAllStepData: context.getAllStepData
1982
- };
1983
- }
1984
- //#endregion
1985
- //#region src/components/features/form/index.ts
1986
- /**
1987
- * Datum Form Library
1988
- *
1989
- * A compound component pattern form library built on top of Conform.js and Zod
1990
- * for easy form creation with built-in validation, error handling, and accessibility features.
1991
- *
1992
- * @example Basic Usage
1993
- * ```tsx
1994
- * import { Form } from './';
1995
- * import { z } from 'zod';
1996
- *
1997
- * const userSchema = z.object({
1998
- * name: z.string().min(2),
1999
- * email: z.string().email(),
2000
- * });
2001
- *
2002
- * function UserForm() {
2003
- * return (
2004
- * <Form.Root schema={userSchema} onSubmit={(data) => console.log(data)}>
2005
- * <Form.Field name="name" label="Name" required>
2006
- * <Form.Input />
2007
- * </Form.Field>
2008
- * <Form.Field name="email" label="Email" required>
2009
- * <Form.Input type="email" />
2010
- * </Form.Field>
2011
- * <Form.Submit>Save</Form.Submit>
2012
- * </Form.Root>
2013
- * );
2014
- * }
2015
- * ```
2016
- *
2017
- * @example Multi-Step Form
2018
- * ```tsx
2019
- * const steps = [
2020
- * { id: 'account', label: 'Account', schema: accountSchema },
2021
- * { id: 'profile', label: 'Profile', schema: profileSchema },
2022
- * ];
2023
- *
2024
- * <Form.Stepper steps={steps} onComplete={handleComplete}>
2025
- * <Form.StepperNavigation />
2026
- * <Form.Step id="account">...</Form.Step>
2027
- * <Form.Step id="profile">...</Form.Step>
2028
- * <Form.StepperControls />
2029
- * </Form.Stepper>
2030
- * ```
2031
- *
2032
- * @example Conditional Fields
2033
- * ```tsx
2034
- * <Form.Field name="contactMethod">
2035
- * <Form.Select>
2036
- * <Form.SelectItem value="email">Email</Form.SelectItem>
2037
- * <Form.SelectItem value="phone">Phone</Form.SelectItem>
2038
- * </Form.Select>
2039
- * </Form.Field>
2040
- *
2041
- * <Form.When field="contactMethod" is="email">
2042
- * <Form.Field name="email"><Form.Input type="email" /></Form.Field>
2043
- * </Form.When>
2044
- * ```
2045
- */
2046
- /**
2047
- * Form compound component
2048
- *
2049
- * Contains all form-related components as properties:
2050
- * - Form.Root - Main form container
2051
- * - Form.Field - Field wrapper with label and error handling
2052
- * - Form.Input - Text input
2053
- * - Form.Textarea - Multi-line text input
2054
- * - Form.Select - Dropdown select
2055
- * - Form.SelectItem - Select option
2056
- * - Form.Checkbox - Checkbox input
2057
- * - Form.Switch - Toggle switch
2058
- * - Form.RadioGroup - Radio button group
2059
- * - Form.RadioItem - Radio button option
2060
- * - Form.Submit - Submit button with loading state
2061
- * - Form.Error - Error display
2062
- * - Form.Description - Helper text
2063
- * - Form.Autocomplete - Searchable select with virtualization
2064
- * - Form.When - Conditional rendering
2065
- * - Form.FieldArray - Dynamic array of fields
2066
- * - Form.Custom - Escape hatch for custom implementations
2067
- * - Form.Stepper - Multi-step form container
2068
- * - Form.Step - Individual step content
2069
- * - Form.StepperNavigation - Step progress indicators
2070
- * - Form.StepperControls - Previous/Next/Submit buttons
2071
- *
2072
- * Hooks available:
2073
- * - Form.useFormContext - Access form context
2074
- * - Form.useFieldContext - Access field context
2075
- * - Form.useField - Access and control a specific field
2076
- * - Form.useWatch - Watch a field's value
2077
- * - Form.useWatchAll - Watch multiple fields
2078
- * - Form.useStepper - Access stepper context
2079
- */
2080
- const Form = {
2081
- Root: FormRoot,
2082
- Field: FormField,
2083
- Submit: FormSubmit,
2084
- Button: FormButton,
2085
- Error: FormError,
2086
- Description: FormDescription,
2087
- Input: FormInput,
2088
- Textarea: FormTextarea,
2089
- Select: FormSelect,
2090
- SelectItem: FormSelectItem,
2091
- Checkbox: FormCheckbox,
2092
- Switch: FormSwitch,
2093
- RadioGroup: FormRadioGroup,
2094
- RadioItem: FormRadioItem,
2095
- CopyBox: FormCopyBox,
2096
- Autocomplete: FormAutocomplete,
2097
- InputGroup: FormInputGroup,
2098
- When: FormWhen,
2099
- FieldArray: FormFieldArray,
2100
- Custom: FormCustom,
2101
- Stepper: FormStepper,
2102
- Step: FormStep,
2103
- StepperNavigation,
2104
- StepperControls,
2105
- Dialog: FormDialog,
2106
- useFormContext,
2107
- useFieldContext,
2108
- useField,
2109
- useWatch,
2110
- useWatchAll,
2111
- useStepper
2112
- };
2113
- //#endregion
2114
- export { FormButton as A, FormField as C, FormCustom as D, FormDescription as E, FormCopyBox as O, FormFieldArray as S, FormDialog as T, FormSelectItem as _, useField as a, FormRadioItem as b, FormStep as c, useWatch as d, useWatchAll as f, FormSelect as g, FormSubmit as h, useFieldContext as i, FormAutocomplete as j, FormCheckbox as k, FormStepper as l, FormSwitch as m, useStepper as n, StepperNavigation as o, FormTextarea as p, useFormContext as r, StepperControls as s, Form as t, FormWhen as u, FormRoot as v, FormError as w, FormInput as x, FormRadioGroup as y };