@datum-cloud/datum-ui 0.6.0-alpha.b817c77 → 0.6.0
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 +4 -0
- package/dist/autocomplete/index.mjs +1 -1
- package/dist/{autocomplete-V5-qslzS.mjs → autocomplete-CkYJueBL.mjs} +2 -2
- package/dist/autosearch/index.mjs +199 -0
- package/dist/{calendar-date-picker-DWK94_DC.mjs → calendar-date-picker-CDT-8Ha8.mjs} +2 -1
- package/dist/combobox/index.mjs +2 -0
- package/dist/combobox-B-C9lJeD.mjs +97 -0
- package/dist/components/features/autocomplete/autocomplete.d.ts +1 -1
- package/dist/components/features/autocomplete/autocomplete.d.ts.map +1 -1
- package/dist/components/features/autocomplete/autocomplete.types.d.ts +2 -0
- package/dist/components/features/autocomplete/autocomplete.types.d.ts.map +1 -1
- package/dist/components/features/autosearch/autosearch.d.ts +35 -0
- package/dist/components/features/autosearch/autosearch.d.ts.map +1 -0
- package/dist/components/features/autosearch/autosearch.types.d.ts +51 -0
- package/dist/components/features/autosearch/autosearch.types.d.ts.map +1 -0
- package/dist/components/features/autosearch/index.d.ts +3 -0
- package/dist/components/features/autosearch/index.d.ts.map +1 -0
- package/dist/components/features/calendar-date-picker/calendar-date-picker.d.ts +2 -1
- package/dist/components/features/calendar-date-picker/calendar-date-picker.d.ts.map +1 -1
- package/dist/components/features/combobox/combobox.d.ts +27 -0
- package/dist/components/features/combobox/combobox.d.ts.map +1 -0
- package/dist/components/features/combobox/index.d.ts +3 -0
- package/dist/components/features/combobox/index.d.ts.map +1 -0
- package/dist/components/features/combobox/types.d.ts +84 -0
- package/dist/components/features/combobox/types.d.ts.map +1 -0
- package/dist/components/features/date-time-picker/date-time-picker.d.ts +9 -0
- package/dist/components/features/date-time-picker/date-time-picker.d.ts.map +1 -0
- package/dist/components/features/date-time-picker/index.d.ts +3 -0
- package/dist/components/features/date-time-picker/index.d.ts.map +1 -0
- package/dist/components/features/date-time-picker/types.d.ts +59 -0
- package/dist/components/features/date-time-picker/types.d.ts.map +1 -0
- package/dist/components/features/date-time-picker/utils/format.d.ts +13 -0
- package/dist/components/features/date-time-picker/utils/format.d.ts.map +1 -0
- package/dist/components/features/date-time-picker/utils/index.d.ts +3 -0
- package/dist/components/features/date-time-picker/utils/index.d.ts.map +1 -0
- package/dist/components/features/date-time-picker/utils/timezone.d.ts +23 -0
- package/dist/components/features/date-time-picker/utils/timezone.d.ts.map +1 -0
- package/dist/components/features/form/adapter-types.d.ts +26 -0
- package/dist/components/features/form/adapter-types.d.ts.map +1 -1
- package/dist/components/features/form/adapters/conform/conform-adapter.d.ts.map +1 -1
- package/dist/components/features/form/adapters/rhf/rhf-adapter.d.ts.map +1 -1
- package/dist/components/features/form/components/form-autocomplete.d.ts.map +1 -1
- package/dist/components/features/form/components/form-autosearch.d.ts +37 -0
- package/dist/components/features/form/components/form-autosearch.d.ts.map +1 -0
- package/dist/components/features/form/components/form-checkbox.d.ts.map +1 -1
- package/dist/components/features/form/components/form-combobox.d.ts +80 -0
- package/dist/components/features/form/components/form-combobox.d.ts.map +1 -0
- package/dist/components/features/form/components/form-copy-box.d.ts +3 -0
- 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-date-picker.d.ts +40 -0
- package/dist/components/features/form/components/form-date-picker.d.ts.map +1 -0
- package/dist/components/features/form/components/form-date-time-picker.d.ts +39 -0
- package/dist/components/features/form/components/form-date-time-picker.d.ts.map +1 -0
- package/dist/components/features/form/components/form-dialog.d.ts.map +1 -1
- package/dist/components/features/form/components/form-field.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.map +1 -1
- package/dist/components/features/form/components/form-switch.d.ts.map +1 -1
- package/dist/components/features/form/components/form-time-picker.d.ts +21 -0
- package/dist/components/features/form/components/form-time-picker.d.ts.map +1 -0
- package/dist/components/features/form/components/form-transfer.d.ts +37 -0
- package/dist/components/features/form/components/form-transfer.d.ts.map +1 -0
- package/dist/components/features/form/components/index.d.ts +7 -1
- package/dist/components/features/form/components/index.d.ts.map +1 -1
- package/dist/components/features/form/components/stepper/form-stepper.d.ts +3 -1
- package/dist/components/features/form/components/stepper/form-stepper.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-display-touched.d.ts +20 -0
- package/dist/components/features/form/hooks/use-display-touched.d.ts.map +1 -0
- package/dist/components/features/form/hooks/use-field.d.ts +4 -0
- 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/index.d.ts +39 -21
- 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 +46 -0
- package/dist/components/features/form/types/index.d.ts.map +1 -1
- package/dist/components/features/form/utils/get-field-constraints.d.ts +23 -1
- package/dist/components/features/form/utils/get-field-constraints.d.ts.map +1 -1
- 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/components/features/form/utils/zod-helpers.d.ts +12 -0
- package/dist/components/features/form/utils/zod-helpers.d.ts.map +1 -0
- package/dist/components/features/time-picker/index.d.ts +3 -0
- package/dist/components/features/time-picker/index.d.ts.map +1 -0
- package/dist/components/features/time-picker/time-picker.d.ts +22 -0
- package/dist/components/features/time-picker/time-picker.d.ts.map +1 -0
- package/dist/components/features/time-picker/types.d.ts +31 -0
- package/dist/components/features/time-picker/types.d.ts.map +1 -0
- package/dist/components/features/transfer/components/index.d.ts +9 -0
- package/dist/components/features/transfer/components/index.d.ts.map +1 -0
- package/dist/components/features/transfer/components/transfer-group.d.ts +7 -0
- package/dist/components/features/transfer/components/transfer-group.d.ts.map +1 -0
- package/dist/components/features/transfer/components/transfer-item.d.ts +10 -0
- package/dist/components/features/transfer/components/transfer-item.d.ts.map +1 -0
- package/dist/components/features/transfer/components/transfer-panel.d.ts +18 -0
- package/dist/components/features/transfer/components/transfer-panel.d.ts.map +1 -0
- package/dist/components/features/transfer/components/transfer-search.d.ts +9 -0
- package/dist/components/features/transfer/components/transfer-search.d.ts.map +1 -0
- package/dist/components/features/transfer/hooks/use-transfer-dnd.d.ts +26 -0
- package/dist/components/features/transfer/hooks/use-transfer-dnd.d.ts.map +1 -0
- package/dist/components/features/transfer/hooks/use-transfer-state.d.ts +20 -0
- package/dist/components/features/transfer/hooks/use-transfer-state.d.ts.map +1 -0
- package/dist/components/features/transfer/index.d.ts +3 -0
- package/dist/components/features/transfer/index.d.ts.map +1 -0
- package/dist/components/features/transfer/transfer.d.ts +6 -0
- package/dist/components/features/transfer/transfer.d.ts.map +1 -0
- package/dist/components/features/transfer/types.d.ts +69 -0
- package/dist/components/features/transfer/types.d.ts.map +1 -0
- package/dist/data-table/index.mjs +1 -1
- package/dist/date-picker/index.mjs +2 -2
- package/dist/date-time-picker/index.mjs +2 -0
- package/dist/date-time-picker-BomrW07W.mjs +178 -0
- package/dist/form/adapters/conform/index.mjs +110 -13
- package/dist/form/adapters/rhf/index.mjs +116 -27
- package/dist/form/index.mjs +3 -3
- package/dist/form/stepper/index.mjs +519 -0
- package/dist/{form-BE1xBne4.mjs → form-B3rQ4CH9.mjs} +447 -605
- package/dist/form-context-Ccxm-wqL.mjs +17 -0
- package/dist/grid/index.mjs +1 -1
- package/dist/hooks/index.mjs +2 -2
- package/dist/index.mjs +16 -16
- package/dist/input-number/index.mjs +1 -1
- package/dist/map/index.mjs +1 -1
- package/dist/{map-Cw7u8r6E.mjs → map-CWIQ-eql.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-DvIOp0hh.mjs +321 -0
- package/dist/tag-input/index.mjs +1 -1
- package/dist/task-queue/index.mjs +1 -1
- package/dist/time-picker/index.mjs +2 -0
- package/dist/time-picker-BoF7pZZ2.mjs +43 -0
- package/dist/transfer/index.mjs +2 -0
- package/dist/transfer-46C-rFFW.mjs +264 -0
- package/dist/{get-field-constraints-BPMW8VvY.mjs → use-display-touched-I39aXEBD.mjs} +51 -16
- package/package.json +42 -1
- /package/dist/{adapter-context-B7L2ucTr.mjs → adapter-context-rWveHhDd.mjs} +0 -0
- /package/dist/{col-YBbQ5wlb.mjs → col-1T0Q3SlH.mjs} +0 -0
- /package/dist/{hooks-DYjN7lvC.mjs → hooks-D8r2M2U6.mjs} +0 -0
- /package/dist/{input-number-DEjXG2I6.mjs → input-number-a7uydAsw.mjs} +0 -0
- /package/dist/{map-leaflet-imports-D6nTEOIh.mjs → map-leaflet-imports-CRSKA79m.mjs} +0 -0
- /package/dist/{more-actions-BNQ2yfWZ.mjs → more-actions-ILnEZq_E.mjs} +0 -0
- /package/dist/{page-title-CNiRNZ7p.mjs → page-title-ChsnpBiH.mjs} +0 -0
- /package/dist/{tag-input-BKed-cul.mjs → tag-input-T9cUX9-G.mjs} +0 -0
- /package/dist/{task-queue-dropdown-Di_Wjspz.mjs → task-queue-dropdown-Wcbj-f0V.mjs} +0 -0
- /package/dist/{to-api-format-Cq4prffn.mjs → to-api-format-Bh3c01gr.mjs} +0 -0
- /package/dist/{use-copy-to-clipboard-BGdTmkFV.mjs → use-copy-to-clipboard-uNeeVHC4.mjs} +0 -0
|
@@ -10,16 +10,21 @@ import { i as SelectItem, l as SelectTrigger, n as SelectContent, t as Select, u
|
|
|
10
10
|
import { t as Tooltip } from "./tooltip-Cruvl5F6.mjs";
|
|
11
11
|
import { t as Switch } from "./switch-DQJQhPIQ.mjs";
|
|
12
12
|
import { t as Textarea } from "./textarea-BwD-MmTV.mjs";
|
|
13
|
-
import { t as Autocomplete } from "./autocomplete-
|
|
13
|
+
import { t as Autocomplete } from "./autocomplete-CkYJueBL.mjs";
|
|
14
|
+
import { t as CalendarDatePicker } from "./calendar-date-picker-CDT-8Ha8.mjs";
|
|
14
15
|
import { t as toast } from "./toast-BWnN5fax.mjs";
|
|
15
|
-
import {
|
|
16
|
-
import { n as
|
|
17
|
-
import {
|
|
16
|
+
import { Autosearch } from "./autosearch/index.mjs";
|
|
17
|
+
import { n as useFormContext$1, t as FormProvider } from "./form-context-Ccxm-wqL.mjs";
|
|
18
|
+
import { t as Combobox } from "./combobox-B-C9lJeD.mjs";
|
|
19
|
+
import { t as useCopyToClipboard } from "./use-copy-to-clipboard-uNeeVHC4.mjs";
|
|
20
|
+
import { t as DateTimePicker } from "./date-time-picker-BomrW07W.mjs";
|
|
21
|
+
import { n as useAdapter } from "./adapter-context-rWveHhDd.mjs";
|
|
18
22
|
import { InputWithAddons } from "./input-with-addons/index.mjs";
|
|
23
|
+
import { t as TimePicker } from "./time-picker-BoF7pZZ2.mjs";
|
|
24
|
+
import { t as Transfer } from "./transfer-46C-rFFW.mjs";
|
|
19
25
|
import { CheckIcon, CircleHelp, CopyIcon } from "lucide-react";
|
|
20
26
|
import * as React$1 from "react";
|
|
21
27
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
22
|
-
import { z } from "zod";
|
|
23
28
|
//#region src/components/features/form/context/field-context.tsx
|
|
24
29
|
const FieldContext = React$1.createContext(null);
|
|
25
30
|
function FieldProvider({ children, value }) {
|
|
@@ -90,7 +95,10 @@ function FormAutocomplete({ disabled, className, ...props }) {
|
|
|
90
95
|
name: fieldState?.name,
|
|
91
96
|
id,
|
|
92
97
|
value,
|
|
93
|
-
onValueChange: (val) =>
|
|
98
|
+
onValueChange: (val) => {
|
|
99
|
+
fieldState?.change(val);
|
|
100
|
+
fieldState?.blur();
|
|
101
|
+
},
|
|
94
102
|
disabled: isDisabled,
|
|
95
103
|
triggerClassName: cn(hasErrors && "border-destructive", props.triggerClassName),
|
|
96
104
|
className
|
|
@@ -98,19 +106,58 @@ function FormAutocomplete({ disabled, className, ...props }) {
|
|
|
98
106
|
}
|
|
99
107
|
FormAutocomplete.displayName = "Form.Autocomplete";
|
|
100
108
|
//#endregion
|
|
101
|
-
//#region src/components/features/form/
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
//#region src/components/features/form/components/form-autosearch.tsx
|
|
110
|
+
/**
|
|
111
|
+
* Form.Autosearch - Search-first input with dropdown results
|
|
112
|
+
*
|
|
113
|
+
* Automatically wired to the parent Form.Field context.
|
|
114
|
+
* Shows a text input that triggers search and displays results in a popover.
|
|
115
|
+
* Different from Form.Autocomplete which shows all options upfront.
|
|
116
|
+
*
|
|
117
|
+
* @example Basic usage
|
|
118
|
+
* ```tsx
|
|
119
|
+
* <Form.Field name="userId" label="User" required>
|
|
120
|
+
* <Form.Autosearch
|
|
121
|
+
* options={users}
|
|
122
|
+
* onSearch={handleSearch}
|
|
123
|
+
* loading={isSearching}
|
|
124
|
+
* placeholder="Search users..."
|
|
125
|
+
* />
|
|
126
|
+
* </Form.Field>
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* @example With debounce control
|
|
130
|
+
* ```tsx
|
|
131
|
+
* <Form.Field name="email" label="Email">
|
|
132
|
+
* <Form.Autosearch
|
|
133
|
+
* options={searchResults}
|
|
134
|
+
* onSearch={debouncedSearch}
|
|
135
|
+
* searchDebounceMs={500}
|
|
136
|
+
* placeholder="Type email to search..."
|
|
137
|
+
* />
|
|
138
|
+
* </Form.Field>
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
function FormAutosearch({ disabled, className, inputClassName, ...props }) {
|
|
142
|
+
const { id, errors, disabled: fieldDisabled, fieldState } = useFieldContext$1();
|
|
143
|
+
const isDisabled = disabled ?? fieldDisabled;
|
|
144
|
+
const hasErrors = errors && errors.length > 0;
|
|
145
|
+
const value = fieldState?.value != null ? String(fieldState.value) : "";
|
|
146
|
+
return /* @__PURE__ */ jsx(Autosearch, {
|
|
147
|
+
...props,
|
|
148
|
+
name: fieldState?.name,
|
|
149
|
+
id,
|
|
105
150
|
value,
|
|
106
|
-
|
|
151
|
+
onValueChange: (val) => {
|
|
152
|
+
fieldState?.change(val);
|
|
153
|
+
fieldState?.blur();
|
|
154
|
+
},
|
|
155
|
+
disabled: isDisabled,
|
|
156
|
+
inputClassName: cn(hasErrors && "border-destructive", inputClassName),
|
|
157
|
+
className
|
|
107
158
|
});
|
|
108
159
|
}
|
|
109
|
-
|
|
110
|
-
const context = React$1.use(FormContext);
|
|
111
|
-
if (!context) throw new Error("useFormContext must be used within a Form.Root component");
|
|
112
|
-
return context;
|
|
113
|
-
}
|
|
160
|
+
FormAutosearch.displayName = "Form.Autosearch";
|
|
114
161
|
//#endregion
|
|
115
162
|
//#region src/components/features/form/components/form-button.tsx
|
|
116
163
|
/**
|
|
@@ -168,7 +215,10 @@ function FormCheckbox({ label, disabled, className }) {
|
|
|
168
215
|
children: [/* @__PURE__ */ jsx(Checkbox, {
|
|
169
216
|
id,
|
|
170
217
|
checked,
|
|
171
|
-
onCheckedChange: (value) =>
|
|
218
|
+
onCheckedChange: (value) => {
|
|
219
|
+
fieldState?.change(Boolean(value));
|
|
220
|
+
fieldState?.blur();
|
|
221
|
+
},
|
|
172
222
|
disabled: isDisabled,
|
|
173
223
|
"aria-invalid": hasErrors || void 0,
|
|
174
224
|
"aria-describedby": hasErrors ? `${id}-error` : void 0
|
|
@@ -181,6 +231,36 @@ function FormCheckbox({ label, disabled, className }) {
|
|
|
181
231
|
}
|
|
182
232
|
FormCheckbox.displayName = "Form.Checkbox";
|
|
183
233
|
//#endregion
|
|
234
|
+
//#region src/components/features/form/components/form-combobox.tsx
|
|
235
|
+
function FormCombobox({ options, placeholder, searchPlaceholder, emptyMessage, disabled, className, triggerClassName, contentClassName, searchable = true, showDropdownArrow = true, clearable = false, "data-testid": testId, modal }) {
|
|
236
|
+
const { id, errors, disabled: fieldDisabled, fieldState } = useFieldContext$1();
|
|
237
|
+
const isDisabled = disabled ?? fieldDisabled;
|
|
238
|
+
const hasErrors = errors && errors.length > 0;
|
|
239
|
+
const handleChange = React$1.useCallback((value) => {
|
|
240
|
+
fieldState?.change(value ?? "");
|
|
241
|
+
fieldState?.blur();
|
|
242
|
+
}, [fieldState]);
|
|
243
|
+
return /* @__PURE__ */ jsx(Combobox, {
|
|
244
|
+
id,
|
|
245
|
+
options,
|
|
246
|
+
value: fieldState?.value ?? "",
|
|
247
|
+
onChange: handleChange,
|
|
248
|
+
placeholder,
|
|
249
|
+
searchPlaceholder,
|
|
250
|
+
emptyMessage,
|
|
251
|
+
disabled: isDisabled,
|
|
252
|
+
searchable,
|
|
253
|
+
showDropdownArrow,
|
|
254
|
+
clearable,
|
|
255
|
+
modal,
|
|
256
|
+
className,
|
|
257
|
+
triggerClassName: cn(hasErrors && "border-destructive", triggerClassName),
|
|
258
|
+
contentClassName,
|
|
259
|
+
"data-testid": testId
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
FormCombobox.displayName = "Form.Combobox";
|
|
263
|
+
//#endregion
|
|
184
264
|
//#region src/components/features/form/components/form-copy-box.tsx
|
|
185
265
|
/**
|
|
186
266
|
* Form.CopyBox - Read-only field with copy-to-clipboard functionality
|
|
@@ -246,6 +326,7 @@ function FormCopyBox({ variant = "default", className, contentClassName, buttonC
|
|
|
246
326
|
})]
|
|
247
327
|
});
|
|
248
328
|
}
|
|
329
|
+
FormCopyBox.displayName = "Form.CopyBox";
|
|
249
330
|
//#endregion
|
|
250
331
|
//#region src/components/features/form/components/form-custom.tsx
|
|
251
332
|
/**
|
|
@@ -270,17 +351,119 @@ function FormCopyBox({ variant = "default", className, contentClassName, buttonC
|
|
|
270
351
|
* ```
|
|
271
352
|
*/
|
|
272
353
|
function FormCustom({ children }) {
|
|
273
|
-
const
|
|
354
|
+
const ctx = useFormContext$1();
|
|
274
355
|
return /* @__PURE__ */ jsx(Fragment$1, { children: children({
|
|
275
|
-
form,
|
|
276
|
-
fields,
|
|
277
|
-
isSubmitting,
|
|
278
|
-
|
|
279
|
-
|
|
356
|
+
form: ctx.form,
|
|
357
|
+
fields: ctx.fields,
|
|
358
|
+
isSubmitting: ctx.isSubmitting,
|
|
359
|
+
isDirty: ctx.isDirty,
|
|
360
|
+
isValid: ctx.isValid,
|
|
361
|
+
isSubmitted: ctx.isSubmitted,
|
|
362
|
+
submitCount: ctx.submitCount,
|
|
363
|
+
dirtyFields: ctx.dirtyFields,
|
|
364
|
+
touchedFields: ctx.touchedFields,
|
|
365
|
+
submit: ctx.submit,
|
|
366
|
+
reset: ctx.reset
|
|
280
367
|
}) });
|
|
281
368
|
}
|
|
282
369
|
FormCustom.displayName = "Form.Custom";
|
|
283
370
|
//#endregion
|
|
371
|
+
//#region src/components/features/form/components/form-date-picker.tsx
|
|
372
|
+
function FormDatePicker({ placeholder, disabled, className, triggerClassName, numberOfMonths = 1, minDate: minDateProp, maxDate: maxDateProp, disableFuture, disablePast, modal }) {
|
|
373
|
+
const { id, errors, disabled: fieldDisabled, fieldState } = useFieldContext$1();
|
|
374
|
+
const isDisabled = disabled ?? fieldDisabled;
|
|
375
|
+
const hasErrors = errors && errors.length > 0;
|
|
376
|
+
const currentValue = React$1.useMemo(() => {
|
|
377
|
+
const val = fieldState?.value;
|
|
378
|
+
if (!val) return {
|
|
379
|
+
from: void 0,
|
|
380
|
+
to: void 0
|
|
381
|
+
};
|
|
382
|
+
if (val instanceof Date) return {
|
|
383
|
+
from: val,
|
|
384
|
+
to: val
|
|
385
|
+
};
|
|
386
|
+
if (typeof val === "string") {
|
|
387
|
+
const date = new Date(val);
|
|
388
|
+
return {
|
|
389
|
+
from: date,
|
|
390
|
+
to: date
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
if (typeof val === "object" && "from" in val) return val;
|
|
394
|
+
return {
|
|
395
|
+
from: void 0,
|
|
396
|
+
to: void 0
|
|
397
|
+
};
|
|
398
|
+
}, [fieldState?.value]);
|
|
399
|
+
const minDate = minDateProp;
|
|
400
|
+
const maxDate = maxDateProp;
|
|
401
|
+
return /* @__PURE__ */ jsx(CalendarDatePicker, {
|
|
402
|
+
id,
|
|
403
|
+
date: currentValue,
|
|
404
|
+
onDateSelect: React$1.useCallback((range) => {
|
|
405
|
+
if (!range) {
|
|
406
|
+
fieldState?.change(void 0);
|
|
407
|
+
fieldState?.blur();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (numberOfMonths === 1) fieldState?.change(range.from.toISOString());
|
|
411
|
+
else fieldState?.change({
|
|
412
|
+
from: range.from.toISOString(),
|
|
413
|
+
to: range.to?.toISOString()
|
|
414
|
+
});
|
|
415
|
+
fieldState?.blur();
|
|
416
|
+
}, [fieldState, numberOfMonths]),
|
|
417
|
+
numberOfMonths,
|
|
418
|
+
placeholder,
|
|
419
|
+
disabled: isDisabled,
|
|
420
|
+
minDate,
|
|
421
|
+
maxDate,
|
|
422
|
+
disableFuture,
|
|
423
|
+
disablePast,
|
|
424
|
+
variant: "outline",
|
|
425
|
+
modal,
|
|
426
|
+
className: cn(className),
|
|
427
|
+
triggerClassName: cn(triggerClassName),
|
|
428
|
+
"aria-invalid": hasErrors || void 0,
|
|
429
|
+
"aria-describedby": hasErrors ? `${id}-error` : void 0
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
FormDatePicker.displayName = "Form.DatePicker";
|
|
433
|
+
//#endregion
|
|
434
|
+
//#region src/components/features/form/components/form-date-time-picker.tsx
|
|
435
|
+
function FormDateTimePicker({ minDate: minDateProp, maxDate: maxDateProp, disabledDates, timezone, showTimezoneIndicator, placeholder, disabled, className, modal }) {
|
|
436
|
+
const { id, errors, disabled: fieldDisabled, fieldState } = useFieldContext$1();
|
|
437
|
+
const isDisabled = disabled ?? fieldDisabled;
|
|
438
|
+
const hasErrors = errors && errors.length > 0;
|
|
439
|
+
const currentValue = React$1.useMemo(() => {
|
|
440
|
+
const val = fieldState?.value;
|
|
441
|
+
if (!val) return void 0;
|
|
442
|
+
if (typeof val === "string") return val;
|
|
443
|
+
}, [fieldState?.value]);
|
|
444
|
+
const minDate = minDateProp;
|
|
445
|
+
const maxDate = maxDateProp;
|
|
446
|
+
return /* @__PURE__ */ jsx(DateTimePicker, {
|
|
447
|
+
value: currentValue,
|
|
448
|
+
onChange: React$1.useCallback((value) => {
|
|
449
|
+
fieldState?.change(value);
|
|
450
|
+
fieldState?.blur();
|
|
451
|
+
}, [fieldState]),
|
|
452
|
+
minDate,
|
|
453
|
+
maxDate,
|
|
454
|
+
disabledDates,
|
|
455
|
+
timezone,
|
|
456
|
+
showTimezoneIndicator,
|
|
457
|
+
placeholder,
|
|
458
|
+
disabled: isDisabled,
|
|
459
|
+
modal,
|
|
460
|
+
className: cn(className),
|
|
461
|
+
"aria-invalid": hasErrors || void 0,
|
|
462
|
+
"aria-describedby": hasErrors ? `${id}-error` : void 0
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
FormDateTimePicker.displayName = "Form.DateTimePicker";
|
|
466
|
+
//#endregion
|
|
284
467
|
//#region src/components/features/form/components/form-description.tsx
|
|
285
468
|
/**
|
|
286
469
|
* Form.Description - Display field description/helper text
|
|
@@ -377,9 +560,6 @@ function FormDialog({ open, onOpenChange, defaultOpen, title, description, trigg
|
|
|
377
560
|
try {
|
|
378
561
|
await onSubmit?.(data);
|
|
379
562
|
onSuccess?.(data);
|
|
380
|
-
} catch (error) {
|
|
381
|
-
console.error("Form submission error:", error);
|
|
382
|
-
throw error;
|
|
383
563
|
} finally {
|
|
384
564
|
if (loading === void 0) setInternalIsSubmitting(false);
|
|
385
565
|
}
|
|
@@ -526,10 +706,33 @@ function FieldLabel({ htmlFor, label, hasErrors, required, tooltip, className })
|
|
|
526
706
|
*/
|
|
527
707
|
function FormField({ name, children, label, description, tooltip, required = false, disabled = false, className, labelClassName }) {
|
|
528
708
|
const adapter = useAdapter();
|
|
529
|
-
const { fields, isSubmitting, form } = useFormContext$1();
|
|
709
|
+
const { fields, isSubmitting, form, mode, displayTouchedFields, markFieldTouched } = useFormContext$1();
|
|
530
710
|
const fieldState = adapter.useField(name);
|
|
531
|
-
const
|
|
711
|
+
const isDisplayTouched = displayTouchedFields.includes(name);
|
|
712
|
+
const rawErrors = fieldState.errors;
|
|
713
|
+
const errors = isDisplayTouched ? rawErrors : [];
|
|
532
714
|
const hasErrors = errors.length > 0;
|
|
715
|
+
const hasFocusedRef = React$1.useRef(false);
|
|
716
|
+
React$1.useEffect(() => {
|
|
717
|
+
hasFocusedRef.current = false;
|
|
718
|
+
}, [name]);
|
|
719
|
+
const handleFocus = React$1.useCallback((e) => {
|
|
720
|
+
if (e.target.type !== "hidden") hasFocusedRef.current = true;
|
|
721
|
+
}, []);
|
|
722
|
+
const handleChange = React$1.useCallback(() => {
|
|
723
|
+
if (mode === "onChange" && hasFocusedRef.current) markFieldTouched(name);
|
|
724
|
+
}, [
|
|
725
|
+
mode,
|
|
726
|
+
name,
|
|
727
|
+
markFieldTouched
|
|
728
|
+
]);
|
|
729
|
+
const handleBlur = React$1.useCallback(() => {
|
|
730
|
+
if ((mode === "onChange" || mode === "onBlur") && hasFocusedRef.current) markFieldTouched(name);
|
|
731
|
+
}, [
|
|
732
|
+
mode,
|
|
733
|
+
name,
|
|
734
|
+
markFieldTouched
|
|
735
|
+
]);
|
|
533
736
|
const fieldId = fieldState.id;
|
|
534
737
|
const descriptionId = description ? `${fieldId}-description` : void 0;
|
|
535
738
|
const errorId = hasErrors ? `${fieldId}-error` : void 0;
|
|
@@ -576,6 +779,9 @@ function FormField({ name, children, label, description, tooltip, required = fal
|
|
|
576
779
|
value: contextValue,
|
|
577
780
|
children: /* @__PURE__ */ jsxs("div", {
|
|
578
781
|
className: cn("flex flex-col space-y-2", className),
|
|
782
|
+
onFocusCapture: handleFocus,
|
|
783
|
+
onChange: handleChange,
|
|
784
|
+
onBlur: handleBlur,
|
|
579
785
|
children: [
|
|
580
786
|
label && /* @__PURE__ */ jsx(FieldLabel, {
|
|
581
787
|
htmlFor: fieldId,
|
|
@@ -675,6 +881,39 @@ function FormInput({ ref, type = "text", className, disabled, ...props }) {
|
|
|
675
881
|
}
|
|
676
882
|
FormInput.displayName = "Form.Input";
|
|
677
883
|
//#endregion
|
|
884
|
+
//#region src/components/features/form/components/form-input-group.tsx
|
|
885
|
+
/**
|
|
886
|
+
* Form.InputGroup - Input with leading/trailing addons
|
|
887
|
+
*
|
|
888
|
+
* Automatically wired to the parent Form.Field context.
|
|
889
|
+
*
|
|
890
|
+
* @example
|
|
891
|
+
* ```tsx
|
|
892
|
+
* <Form.Field name="website" label="Website" required>
|
|
893
|
+
* <Form.InputGroup leading="https://" placeholder="example.com" />
|
|
894
|
+
* </Form.Field>
|
|
895
|
+
* ```
|
|
896
|
+
*/
|
|
897
|
+
function FormInputGroup({ ref, className, disabled, ...props }) {
|
|
898
|
+
const { id, errors, disabled: fieldDisabled, fieldState } = useFieldContext$1();
|
|
899
|
+
const isDisabled = disabled ?? fieldDisabled;
|
|
900
|
+
const hasErrors = errors && errors.length > 0;
|
|
901
|
+
return /* @__PURE__ */ jsx(InputWithAddons, {
|
|
902
|
+
...props,
|
|
903
|
+
ref,
|
|
904
|
+
id,
|
|
905
|
+
name: fieldState?.name,
|
|
906
|
+
value: fieldState?.value ?? "",
|
|
907
|
+
onChange: (e) => fieldState?.change(e.target.value),
|
|
908
|
+
onBlur: () => fieldState?.blur(),
|
|
909
|
+
className: cn("text-xs!", className),
|
|
910
|
+
disabled: isDisabled,
|
|
911
|
+
"aria-invalid": hasErrors || void 0,
|
|
912
|
+
"aria-describedby": hasErrors ? `${id}-error` : void 0
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
FormInputGroup.displayName = "Form.InputGroup";
|
|
916
|
+
//#endregion
|
|
678
917
|
//#region src/components/features/form/components/form-radio-group.tsx
|
|
679
918
|
/**
|
|
680
919
|
* Form.RadioGroup - Radio button group component
|
|
@@ -698,7 +937,10 @@ function FormRadioGroup({ orientation = "vertical", disabled, className, childre
|
|
|
698
937
|
const hasErrors = errors && errors.length > 0;
|
|
699
938
|
return /* @__PURE__ */ jsx(RadioGroup, {
|
|
700
939
|
value: fieldState?.value != null ? String(fieldState.value) : void 0,
|
|
701
|
-
onValueChange: (val) =>
|
|
940
|
+
onValueChange: (val) => {
|
|
941
|
+
fieldState?.change(val);
|
|
942
|
+
fieldState?.blur();
|
|
943
|
+
},
|
|
702
944
|
disabled: isDisabled,
|
|
703
945
|
"aria-invalid": hasErrors || void 0,
|
|
704
946
|
"aria-describedby": hasErrors ? `${id}-error` : void 0,
|
|
@@ -716,7 +958,8 @@ FormRadioGroup.displayName = "Form.RadioGroup";
|
|
|
716
958
|
* ```
|
|
717
959
|
*/
|
|
718
960
|
function FormRadioItem({ value, label, description, disabled }) {
|
|
719
|
-
const
|
|
961
|
+
const { id: fieldId } = useFieldContext$1();
|
|
962
|
+
const radioId = `${fieldId}-radio-${value}`;
|
|
720
963
|
return /* @__PURE__ */ jsxs("div", {
|
|
721
964
|
className: "flex items-start space-x-2",
|
|
722
965
|
children: [/* @__PURE__ */ jsx(RadioGroupItem, {
|
|
@@ -762,13 +1005,17 @@ FormRadioItem.displayName = "Form.RadioItem";
|
|
|
762
1005
|
* </Form.Root>
|
|
763
1006
|
* ```
|
|
764
1007
|
*/
|
|
765
|
-
function FormRoot({ schema, children, onSubmit, action, method = "POST", formComponent: FormComp = "form", id, name, defaultValues, mode = "
|
|
1008
|
+
function FormRoot({ schema, children, onSubmit, action, method = "POST", formComponent: FormComp = "form", id, name, defaultValues, mode = "onChange", isSubmitting: externalIsSubmitting, onError, onSuccess, telemetry, className }) {
|
|
766
1009
|
const adapter = useAdapter();
|
|
767
1010
|
const [internalIsSubmitting, setInternalIsSubmitting] = React$1.useState(false);
|
|
768
1011
|
const isSubmitting = externalIsSubmitting ?? internalIsSubmitting;
|
|
1012
|
+
const [isSubmitted, setIsSubmitted] = React$1.useState(false);
|
|
1013
|
+
const [submitCount, setSubmitCount] = React$1.useState(0);
|
|
769
1014
|
const formRef = React$1.useRef(null);
|
|
770
1015
|
const wrappedOnSubmit = React$1.useCallback(async (data) => {
|
|
771
1016
|
setInternalIsSubmitting(true);
|
|
1017
|
+
setIsSubmitted(true);
|
|
1018
|
+
setSubmitCount((prev) => prev + 1);
|
|
772
1019
|
try {
|
|
773
1020
|
await onSubmit?.(data);
|
|
774
1021
|
telemetry?.onSuccess?.({
|
|
@@ -807,25 +1054,49 @@ function FormRoot({ schema, children, onSubmit, action, method = "POST", formCom
|
|
|
807
1054
|
onSubmit: onSubmit ? wrappedOnSubmit : void 0,
|
|
808
1055
|
formRef
|
|
809
1056
|
});
|
|
1057
|
+
const { formState } = instance;
|
|
810
1058
|
const contextValue = React$1.useMemo(() => ({
|
|
811
1059
|
form: instance,
|
|
812
1060
|
fields: instance.fields,
|
|
813
1061
|
isSubmitting,
|
|
1062
|
+
isDirty: formState.isDirty,
|
|
1063
|
+
isValid: formState.isValid,
|
|
1064
|
+
isSubmitted,
|
|
1065
|
+
submitCount,
|
|
1066
|
+
dirtyFields: formState.dirtyFields,
|
|
1067
|
+
touchedFields: formState.touchedFields,
|
|
1068
|
+
mode,
|
|
1069
|
+
displayTouchedFields: instance.touchedFields,
|
|
1070
|
+
markFieldTouched: instance.markFieldTouched,
|
|
1071
|
+
markAllFieldsTouched: instance.markAllFieldsTouched,
|
|
814
1072
|
submit: () => formRef.current?.requestSubmit(),
|
|
815
1073
|
reset: () => instance.reset(),
|
|
816
1074
|
formId: instance.id
|
|
817
|
-
}), [
|
|
818
|
-
|
|
819
|
-
const renderProps = {
|
|
820
|
-
form: instance,
|
|
821
|
-
fields: instance.fields,
|
|
1075
|
+
}), [
|
|
1076
|
+
instance,
|
|
822
1077
|
isSubmitting,
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1078
|
+
formState,
|
|
1079
|
+
isSubmitted,
|
|
1080
|
+
submitCount,
|
|
1081
|
+
mode
|
|
1082
|
+
]);
|
|
1083
|
+
const isRenderFunction = typeof children === "function";
|
|
826
1084
|
const renderChildren = () => {
|
|
827
|
-
if (isRenderFunction) return children
|
|
828
|
-
return children
|
|
1085
|
+
if (!isRenderFunction) return children;
|
|
1086
|
+
return children({
|
|
1087
|
+
form: instance,
|
|
1088
|
+
fields: instance.fields,
|
|
1089
|
+
isSubmitting,
|
|
1090
|
+
isDirty: formState.isDirty,
|
|
1091
|
+
isValid: formState.isValid,
|
|
1092
|
+
isSubmitted,
|
|
1093
|
+
submitCount,
|
|
1094
|
+
dirtyFields: formState.dirtyFields,
|
|
1095
|
+
touchedFields: formState.touchedFields,
|
|
1096
|
+
mode,
|
|
1097
|
+
submit: () => formRef.current?.requestSubmit(),
|
|
1098
|
+
reset: () => instance.reset()
|
|
1099
|
+
});
|
|
829
1100
|
};
|
|
830
1101
|
return /* @__PURE__ */ jsx(FormProvider, {
|
|
831
1102
|
value: contextValue,
|
|
@@ -840,7 +1111,9 @@ function FormRoot({ schema, children, onSubmit, action, method = "POST", formCom
|
|
|
840
1111
|
autoComplete: "off",
|
|
841
1112
|
noValidate: true,
|
|
842
1113
|
onSubmit: (e) => {
|
|
1114
|
+
const submitter = e.nativeEvent.submitter;
|
|
843
1115
|
e.stopPropagation();
|
|
1116
|
+
if (submitter && !submitter.hidden) instance.markAllFieldsTouched();
|
|
844
1117
|
telemetry?.onSubmit?.({
|
|
845
1118
|
formName: name ?? "",
|
|
846
1119
|
formId: id
|
|
@@ -955,7 +1228,10 @@ function FormSwitch({ label, disabled, className }) {
|
|
|
955
1228
|
children: [/* @__PURE__ */ jsx(Switch, {
|
|
956
1229
|
id,
|
|
957
1230
|
checked,
|
|
958
|
-
onCheckedChange: (value) =>
|
|
1231
|
+
onCheckedChange: (value) => {
|
|
1232
|
+
fieldState?.change(Boolean(value));
|
|
1233
|
+
fieldState?.blur();
|
|
1234
|
+
},
|
|
959
1235
|
disabled: isDisabled,
|
|
960
1236
|
"aria-invalid": hasErrors || void 0,
|
|
961
1237
|
"aria-describedby": hasErrors ? `${id}-error` : void 0
|
|
@@ -1002,6 +1278,75 @@ function FormTextarea({ ref, className, disabled, rows = 3, ...props }) {
|
|
|
1002
1278
|
}
|
|
1003
1279
|
FormTextarea.displayName = "Form.Textarea";
|
|
1004
1280
|
//#endregion
|
|
1281
|
+
//#region src/components/features/form/components/form-time-picker.tsx
|
|
1282
|
+
function FormTimePicker({ min, max, step, placeholder, disabled, className }) {
|
|
1283
|
+
const { id, errors, disabled: fieldDisabled, fieldState } = useFieldContext$1();
|
|
1284
|
+
const isDisabled = disabled ?? fieldDisabled;
|
|
1285
|
+
const hasErrors = errors && errors.length > 0;
|
|
1286
|
+
return /* @__PURE__ */ jsx(TimePicker, {
|
|
1287
|
+
id,
|
|
1288
|
+
value: fieldState?.value ?? "",
|
|
1289
|
+
onChange: React$1.useCallback((value) => {
|
|
1290
|
+
fieldState?.change(value || void 0);
|
|
1291
|
+
fieldState?.blur();
|
|
1292
|
+
}, [fieldState]),
|
|
1293
|
+
min,
|
|
1294
|
+
max,
|
|
1295
|
+
step,
|
|
1296
|
+
placeholder,
|
|
1297
|
+
disabled: isDisabled,
|
|
1298
|
+
className: cn(className),
|
|
1299
|
+
"aria-invalid": hasErrors || void 0,
|
|
1300
|
+
"aria-describedby": hasErrors ? `${id}-error` : void 0
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
FormTimePicker.displayName = "Form.TimePicker";
|
|
1304
|
+
//#endregion
|
|
1305
|
+
//#region src/components/features/form/components/form-transfer.tsx
|
|
1306
|
+
/**
|
|
1307
|
+
* Form.Transfer - Transfer list component for selecting multiple items
|
|
1308
|
+
*
|
|
1309
|
+
* Automatically wired to the parent Form.Field context.
|
|
1310
|
+
* Displays minItems/maxItems constraints when provided.
|
|
1311
|
+
*
|
|
1312
|
+
* @example
|
|
1313
|
+
* ```tsx
|
|
1314
|
+
* <Form.Field name="teams" label="Select Teams">
|
|
1315
|
+
* <Form.Transfer
|
|
1316
|
+
* items={teams}
|
|
1317
|
+
* itemKey="id"
|
|
1318
|
+
* itemLabel="name"
|
|
1319
|
+
* minItems={2}
|
|
1320
|
+
* maxItems={5}
|
|
1321
|
+
* />
|
|
1322
|
+
* </Form.Field>
|
|
1323
|
+
* ```
|
|
1324
|
+
*/
|
|
1325
|
+
function FormTransfer({ disabled, minItems, maxItems, ...props }) {
|
|
1326
|
+
const { id, errors, disabled: fieldDisabled, fieldState } = useFieldContext$1();
|
|
1327
|
+
const isDisabled = disabled ?? fieldDisabled;
|
|
1328
|
+
const hasErrors = errors && errors.length > 0;
|
|
1329
|
+
const value = Array.isArray(fieldState?.value) ? fieldState.value : [];
|
|
1330
|
+
const handleChange = React$1.useCallback((newValue) => {
|
|
1331
|
+
fieldState?.change(newValue);
|
|
1332
|
+
fieldState?.blur();
|
|
1333
|
+
}, [fieldState]);
|
|
1334
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
|
|
1335
|
+
"aria-invalid": hasErrors || void 0,
|
|
1336
|
+
"aria-describedby": hasErrors ? `${id}-error` : void 0,
|
|
1337
|
+
children: /* @__PURE__ */ jsx(Transfer, {
|
|
1338
|
+
...props,
|
|
1339
|
+
value,
|
|
1340
|
+
onChange: handleChange,
|
|
1341
|
+
disabled: isDisabled
|
|
1342
|
+
})
|
|
1343
|
+
}), (minItems != null || maxItems != null) && /* @__PURE__ */ jsx("p", {
|
|
1344
|
+
className: "text-ring text-xs text-wrap mt-2",
|
|
1345
|
+
children: minItems != null && maxItems != null ? `Select between ${minItems} and ${maxItems} items` : minItems != null ? `Select at least ${minItems} items` : `Select up to ${maxItems} items`
|
|
1346
|
+
})] });
|
|
1347
|
+
}
|
|
1348
|
+
FormTransfer.displayName = "Form.Transfer";
|
|
1349
|
+
//#endregion
|
|
1005
1350
|
//#region src/components/features/form/hooks/use-watch.ts
|
|
1006
1351
|
/**
|
|
1007
1352
|
* Hook to watch a field's value reactively.
|
|
@@ -1078,525 +1423,15 @@ function FormWhen({ field, is, isNot, in: inArray, notIn, children }) {
|
|
|
1078
1423
|
}
|
|
1079
1424
|
FormWhen.displayName = "Form.When";
|
|
1080
1425
|
//#endregion
|
|
1081
|
-
//#region src/components/features/form/components/stepper/form-stepper.tsx
|
|
1082
|
-
const FormStepperContext = React$1.createContext(null);
|
|
1083
|
-
function useFormStepperContext() {
|
|
1084
|
-
const context = React$1.use(FormStepperContext);
|
|
1085
|
-
if (!context) throw new Error("useFormStepperContext must be used within a Form.Stepper component");
|
|
1086
|
-
return context;
|
|
1087
|
-
}
|
|
1088
|
-
/**
|
|
1089
|
-
* Recursively unwrap ZodIntersection (from .and()) to extract the base ZodObject.
|
|
1090
|
-
*
|
|
1091
|
-
* Zod v4 schema types use `def.type` as a string discriminant:
|
|
1092
|
-
* - "intersection" (from .and()): merge left + right base objects
|
|
1093
|
-
* - "object": return directly
|
|
1094
|
-
*
|
|
1095
|
-
* Note: In Zod v4, .superRefine() and .refine() return `this` (no wrapper),
|
|
1096
|
-
* so only ZodIntersection needs unwrapping.
|
|
1097
|
-
*/
|
|
1098
|
-
function getBaseObject(schema) {
|
|
1099
|
-
if (schema.def.type === "intersection") {
|
|
1100
|
-
const intersectionDef = schema.def;
|
|
1101
|
-
const left = getBaseObject(intersectionDef.left);
|
|
1102
|
-
const right = getBaseObject(intersectionDef.right);
|
|
1103
|
-
return left.merge(right);
|
|
1104
|
-
}
|
|
1105
|
-
if (schema.def.type !== "object") {
|
|
1106
|
-
console.warn(`mergeSchemas: expected ZodObject or ZodIntersection but got "${schema.def.type}". Falling back to empty object.`);
|
|
1107
|
-
return z.object({});
|
|
1108
|
-
}
|
|
1109
|
-
return schema;
|
|
1110
|
-
}
|
|
1111
|
-
/**
|
|
1112
|
-
* Merge multiple zod schemas into one ZodObject for HTML constraint generation.
|
|
1113
|
-
* Handles ZodIntersection (.and()) by unwrapping to base ZodObject shapes.
|
|
1114
|
-
* Per-step validation still uses the original schemas with all refinements intact.
|
|
1115
|
-
*/
|
|
1116
|
-
function mergeSchemas(steps) {
|
|
1117
|
-
if (steps.length === 0) throw new Error("Form.Stepper requires at least one step");
|
|
1118
|
-
return steps.reduce((acc, step, index) => {
|
|
1119
|
-
const base = getBaseObject(step.schema);
|
|
1120
|
-
if (index === 0) return base;
|
|
1121
|
-
return acc.merge(base);
|
|
1122
|
-
}, {});
|
|
1123
|
-
}
|
|
1124
|
-
/**
|
|
1125
|
-
* Convert StepConfig[] to Stepperize step format
|
|
1126
|
-
*/
|
|
1127
|
-
function toStepperizeSteps(steps) {
|
|
1128
|
-
return steps.map((step) => ({
|
|
1129
|
-
id: step.id,
|
|
1130
|
-
label: step.label,
|
|
1131
|
-
description: step.description
|
|
1132
|
-
}));
|
|
1133
|
-
}
|
|
1134
|
-
/**
|
|
1135
|
-
* Form.Stepper - Multi-step form container
|
|
1136
|
-
*
|
|
1137
|
-
* Uses Stepperize internally for step navigation and a single Conform form
|
|
1138
|
-
* instance for all steps. Schemas are auto-merged for unified validation.
|
|
1139
|
-
*
|
|
1140
|
-
* @example
|
|
1141
|
-
* ```tsx
|
|
1142
|
-
* const steps = [
|
|
1143
|
-
* { id: 'account', label: 'Account', schema: accountSchema },
|
|
1144
|
-
* { id: 'profile', label: 'Profile', schema: profileSchema },
|
|
1145
|
-
* ];
|
|
1146
|
-
*
|
|
1147
|
-
* <Form.Stepper steps={steps} onComplete={handleComplete}>
|
|
1148
|
-
* <Form.StepperNavigation />
|
|
1149
|
-
*
|
|
1150
|
-
* <Form.Step id="account">
|
|
1151
|
-
* <Form.Field name="email" label="Email" required>
|
|
1152
|
-
* <Form.Input type="email" />
|
|
1153
|
-
* </Form.Field>
|
|
1154
|
-
* </Form.Step>
|
|
1155
|
-
*
|
|
1156
|
-
* <Form.Step id="profile">
|
|
1157
|
-
* <Form.Field name="name" label="Full Name" required>
|
|
1158
|
-
* <Form.Input />
|
|
1159
|
-
* </Form.Field>
|
|
1160
|
-
* </Form.Step>
|
|
1161
|
-
*
|
|
1162
|
-
* <Form.StepperControls />
|
|
1163
|
-
* </Form.Stepper>
|
|
1164
|
-
* ```
|
|
1165
|
-
*/
|
|
1166
|
-
function FormStepper({ steps, children, onComplete, onStepChange, initialStep, className, defaultValues, id, formComponent }) {
|
|
1167
|
-
const stepperDef = React$1.useMemo(() => {
|
|
1168
|
-
return defineStepper(...toStepperizeSteps(steps));
|
|
1169
|
-
}, [steps]);
|
|
1170
|
-
const initialStepIndex = React$1.useMemo(() => {
|
|
1171
|
-
if (!initialStep) return void 0;
|
|
1172
|
-
const index = steps.findIndex((s) => s.id === initialStep);
|
|
1173
|
-
return index >= 0 ? steps[index].id : void 0;
|
|
1174
|
-
}, [initialStep, steps]);
|
|
1175
|
-
const { Stepper } = stepperDef;
|
|
1176
|
-
const providerProps = initialStepIndex ? { initialStep: initialStepIndex } : {};
|
|
1177
|
-
return /* @__PURE__ */ jsx(Stepper.Provider, {
|
|
1178
|
-
...providerProps,
|
|
1179
|
-
children: /* @__PURE__ */ jsx(FormStepperContent, {
|
|
1180
|
-
steps,
|
|
1181
|
-
stepperDef,
|
|
1182
|
-
onComplete,
|
|
1183
|
-
onStepChange,
|
|
1184
|
-
className,
|
|
1185
|
-
defaultValues,
|
|
1186
|
-
id,
|
|
1187
|
-
formComponent,
|
|
1188
|
-
children
|
|
1189
|
-
})
|
|
1190
|
-
});
|
|
1191
|
-
}
|
|
1192
|
-
FormStepper.displayName = "Form.Stepper";
|
|
1193
|
-
function FormStepperContent({ steps, stepperDef, children, onComplete, onStepChange, className, defaultValues, id, formComponent }) {
|
|
1194
|
-
const { useStepper } = stepperDef;
|
|
1195
|
-
const stepper = useStepper();
|
|
1196
|
-
return /* @__PURE__ */ jsx(StepForm, {
|
|
1197
|
-
steps,
|
|
1198
|
-
stepper,
|
|
1199
|
-
currentStepConfig: React$1.useMemo(() => steps.find((s) => s.id === stepper.state.current.data.id) ?? steps[0], [steps, stepper.state.current.data.id]),
|
|
1200
|
-
combinedSchema: React$1.useMemo(() => mergeSchemas(steps), [steps]),
|
|
1201
|
-
storedValues: React$1.useMemo(() => {
|
|
1202
|
-
const allMetadata = steps.reduce((acc, step) => ({
|
|
1203
|
-
...acc,
|
|
1204
|
-
...stepper.metadata.get(step.id) || {}
|
|
1205
|
-
}), {});
|
|
1206
|
-
return {
|
|
1207
|
-
...defaultValues,
|
|
1208
|
-
...allMetadata
|
|
1209
|
-
};
|
|
1210
|
-
}, [
|
|
1211
|
-
steps,
|
|
1212
|
-
stepper,
|
|
1213
|
-
defaultValues,
|
|
1214
|
-
stepper.state.current.data.id
|
|
1215
|
-
]),
|
|
1216
|
-
onComplete,
|
|
1217
|
-
onStepChange,
|
|
1218
|
-
className,
|
|
1219
|
-
id,
|
|
1220
|
-
formComponent,
|
|
1221
|
-
children
|
|
1222
|
-
}, stepper.state.current.data.id);
|
|
1223
|
-
}
|
|
1224
|
-
function StepForm({ steps, stepper, currentStepConfig, combinedSchema: _combinedSchema, storedValues, children, onComplete, onStepChange, className, id, formComponent: FormComp = "form" }) {
|
|
1225
|
-
const adapter = useAdapter();
|
|
1226
|
-
const [isSubmitting, setIsSubmitting] = React$1.useState(false);
|
|
1227
|
-
const formRef = React$1.useRef(null);
|
|
1228
|
-
const currentIndex = stepper.lookup.getIndex(stepper.state.current.data.id);
|
|
1229
|
-
const handleStepSubmit = React$1.useCallback(async (data) => {
|
|
1230
|
-
stepper.metadata.set(stepper.state.current.data.id, data);
|
|
1231
|
-
if (stepper.state.isLast) {
|
|
1232
|
-
setIsSubmitting(true);
|
|
1233
|
-
try {
|
|
1234
|
-
await onComplete({
|
|
1235
|
-
...steps.reduce((acc, step) => ({
|
|
1236
|
-
...acc,
|
|
1237
|
-
...stepper.metadata.get(step.id) || {}
|
|
1238
|
-
}), {}),
|
|
1239
|
-
...data
|
|
1240
|
-
});
|
|
1241
|
-
} catch (error) {
|
|
1242
|
-
console.error("Stepper form completion error:", error);
|
|
1243
|
-
} finally {
|
|
1244
|
-
setIsSubmitting(false);
|
|
1245
|
-
}
|
|
1246
|
-
} else {
|
|
1247
|
-
const nextStepId = stepper.lookup.getNext(stepper.state.current.data.id)?.id;
|
|
1248
|
-
if (nextStepId) {
|
|
1249
|
-
stepper.navigation.goTo(nextStepId);
|
|
1250
|
-
onStepChange?.(nextStepId, "next");
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
}, [
|
|
1254
|
-
stepper,
|
|
1255
|
-
steps,
|
|
1256
|
-
onComplete,
|
|
1257
|
-
onStepChange
|
|
1258
|
-
]);
|
|
1259
|
-
const instance = adapter.useCreateForm({
|
|
1260
|
-
schema: currentStepConfig.schema,
|
|
1261
|
-
defaultValues: storedValues,
|
|
1262
|
-
mode: "onSubmit",
|
|
1263
|
-
id: `${id ?? "stepper"}-${currentStepConfig.id}`,
|
|
1264
|
-
onSubmit: handleStepSubmit,
|
|
1265
|
-
formRef
|
|
1266
|
-
});
|
|
1267
|
-
const next = React$1.useCallback(() => {
|
|
1268
|
-
formRef.current?.requestSubmit();
|
|
1269
|
-
}, []);
|
|
1270
|
-
const prev = React$1.useCallback(() => {
|
|
1271
|
-
const currentValues = instance.getValues();
|
|
1272
|
-
if (Object.keys(currentValues).length > 0) stepper.metadata.set(stepper.state.current.data.id, currentValues);
|
|
1273
|
-
const prevStepId = stepper.lookup.getPrev(stepper.state.current.data.id)?.id;
|
|
1274
|
-
if (prevStepId) {
|
|
1275
|
-
stepper.navigation.goTo(prevStepId);
|
|
1276
|
-
onStepChange?.(prevStepId, "prev");
|
|
1277
|
-
}
|
|
1278
|
-
}, [
|
|
1279
|
-
instance,
|
|
1280
|
-
stepper,
|
|
1281
|
-
onStepChange
|
|
1282
|
-
]);
|
|
1283
|
-
const goTo = React$1.useCallback((stepId) => {
|
|
1284
|
-
if (stepper.lookup.getIndex(stepId) < currentIndex) {
|
|
1285
|
-
const currentValues = instance.getValues();
|
|
1286
|
-
if (Object.keys(currentValues).length > 0) stepper.metadata.set(stepper.state.current.data.id, currentValues);
|
|
1287
|
-
stepper.navigation.goTo(stepId);
|
|
1288
|
-
onStepChange?.(stepId, "prev");
|
|
1289
|
-
}
|
|
1290
|
-
}, [
|
|
1291
|
-
instance,
|
|
1292
|
-
stepper,
|
|
1293
|
-
currentIndex,
|
|
1294
|
-
onStepChange
|
|
1295
|
-
]);
|
|
1296
|
-
const getStepData = React$1.useCallback((stepId) => stepper.metadata.get(stepId), [stepper]);
|
|
1297
|
-
const getAllStepData = React$1.useCallback(() => {
|
|
1298
|
-
return steps.reduce((acc, step) => ({
|
|
1299
|
-
...acc,
|
|
1300
|
-
...stepper.metadata.get(step.id) || {}
|
|
1301
|
-
}), {});
|
|
1302
|
-
}, [steps, stepper]);
|
|
1303
|
-
const stepperContextValue = React$1.useMemo(() => ({
|
|
1304
|
-
steps,
|
|
1305
|
-
current: currentStepConfig,
|
|
1306
|
-
currentIndex,
|
|
1307
|
-
next,
|
|
1308
|
-
prev,
|
|
1309
|
-
goTo,
|
|
1310
|
-
isFirst: stepper.state.isFirst,
|
|
1311
|
-
isLast: stepper.state.isLast,
|
|
1312
|
-
getStepData,
|
|
1313
|
-
getAllStepData,
|
|
1314
|
-
utils: { getIndex: (stepId) => stepper.lookup.getIndex(stepId) }
|
|
1315
|
-
}), [
|
|
1316
|
-
steps,
|
|
1317
|
-
currentStepConfig,
|
|
1318
|
-
currentIndex,
|
|
1319
|
-
stepper,
|
|
1320
|
-
next,
|
|
1321
|
-
prev,
|
|
1322
|
-
goTo,
|
|
1323
|
-
getStepData,
|
|
1324
|
-
getAllStepData
|
|
1325
|
-
]);
|
|
1326
|
-
const contextValue = React$1.useMemo(() => ({
|
|
1327
|
-
form: instance,
|
|
1328
|
-
fields: instance.fields,
|
|
1329
|
-
isSubmitting,
|
|
1330
|
-
submit: () => formRef.current?.requestSubmit(),
|
|
1331
|
-
reset: () => instance.reset(),
|
|
1332
|
-
formId: instance.id
|
|
1333
|
-
}), [instance, isSubmitting]);
|
|
1334
|
-
const renderProps = {
|
|
1335
|
-
steps,
|
|
1336
|
-
current: currentStepConfig,
|
|
1337
|
-
currentIndex,
|
|
1338
|
-
next,
|
|
1339
|
-
prev,
|
|
1340
|
-
goTo,
|
|
1341
|
-
isFirst: stepper.state.isFirst,
|
|
1342
|
-
isLast: stepper.state.isLast,
|
|
1343
|
-
getStepData,
|
|
1344
|
-
getAllStepData
|
|
1345
|
-
};
|
|
1346
|
-
const resolvedChildren = typeof children === "function" ? children(renderProps) : children;
|
|
1347
|
-
return /* @__PURE__ */ jsx(FormStepperContext, {
|
|
1348
|
-
value: stepperContextValue,
|
|
1349
|
-
children: /* @__PURE__ */ jsx(FormProvider, {
|
|
1350
|
-
value: contextValue,
|
|
1351
|
-
children: /* @__PURE__ */ jsx(adapter.FormProvider, {
|
|
1352
|
-
instance,
|
|
1353
|
-
children: /* @__PURE__ */ jsx(FormComp, {
|
|
1354
|
-
ref: formRef,
|
|
1355
|
-
...instance.formProps,
|
|
1356
|
-
className: cn("space-y-6", className),
|
|
1357
|
-
autoComplete: "off",
|
|
1358
|
-
noValidate: true,
|
|
1359
|
-
onSubmit: (e) => {
|
|
1360
|
-
e.stopPropagation();
|
|
1361
|
-
const adapterSubmit = instance.formProps.onSubmit;
|
|
1362
|
-
adapterSubmit?.(e);
|
|
1363
|
-
},
|
|
1364
|
-
children: resolvedChildren
|
|
1365
|
-
})
|
|
1366
|
-
})
|
|
1367
|
-
})
|
|
1368
|
-
});
|
|
1369
|
-
}
|
|
1370
|
-
//#endregion
|
|
1371
|
-
//#region src/components/features/form/components/stepper/form-step.tsx
|
|
1372
|
-
/**
|
|
1373
|
-
* Form.Step - Individual step content container
|
|
1374
|
-
*
|
|
1375
|
-
* Only renders its children when the step is active.
|
|
1376
|
-
* Works with the single-form architecture - fields remain registered
|
|
1377
|
-
* even when unmounted, preserving their values.
|
|
1378
|
-
*
|
|
1379
|
-
* @example
|
|
1380
|
-
* ```tsx
|
|
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
|
-
*/
|
|
1388
|
-
function FormStep({ id, children }) {
|
|
1389
|
-
const { current } = useFormStepperContext();
|
|
1390
|
-
if (current.id !== id) return null;
|
|
1391
|
-
return /* @__PURE__ */ jsx(Fragment$1, { children });
|
|
1392
|
-
}
|
|
1393
|
-
FormStep.displayName = "Form.Step";
|
|
1394
|
-
//#endregion
|
|
1395
|
-
//#region src/components/features/form/components/stepper/stepper-controls.tsx
|
|
1396
|
-
/**
|
|
1397
|
-
* Form.StepperControls - Navigation buttons (Previous/Next/Submit)
|
|
1398
|
-
*
|
|
1399
|
-
* Provides Previous and Next/Submit buttons for navigating between steps.
|
|
1400
|
-
* The Next button triggers form validation before advancing.
|
|
1401
|
-
* The Previous button navigates back without validation.
|
|
1402
|
-
*
|
|
1403
|
-
* @example
|
|
1404
|
-
* ```tsx
|
|
1405
|
-
* <Form.StepperControls
|
|
1406
|
-
* prevLabel={(isFirst) => isFirst ? 'Cancel' : 'Previous'}
|
|
1407
|
-
* nextLabel={(isLast) => isLast ? 'Submit' : 'Next'}
|
|
1408
|
-
* loadingText="Creating..."
|
|
1409
|
-
* onCancel={() => setOpen(false)}
|
|
1410
|
-
* />
|
|
1411
|
-
* ```
|
|
1412
|
-
*
|
|
1413
|
-
* @example With external loading state
|
|
1414
|
-
* ```tsx
|
|
1415
|
-
* <Form.StepperControls
|
|
1416
|
-
* loading={fetcher.state === 'submitting'}
|
|
1417
|
-
* disabled={!isValid}
|
|
1418
|
-
* loadingText="Saving..."
|
|
1419
|
-
* />
|
|
1420
|
-
* ```
|
|
1421
|
-
*/
|
|
1422
|
-
function StepperControls({ prevLabel = "Previous", nextLabel = (isLast) => isLast ? "Submit" : "Next", loadingText = "Submitting...", showPrev = true, loading, disabled, onPrev, onCancel, className }) {
|
|
1423
|
-
const { prev, isFirst, isLast } = useFormStepperContext();
|
|
1424
|
-
const { isSubmitting: formIsSubmitting } = useFormContext$1();
|
|
1425
|
-
const isLoading = loading ?? formIsSubmitting;
|
|
1426
|
-
const isDisabled = disabled ?? false;
|
|
1427
|
-
const getPrevLabel = () => {
|
|
1428
|
-
if (typeof prevLabel === "function") return prevLabel(isFirst);
|
|
1429
|
-
return prevLabel;
|
|
1430
|
-
};
|
|
1431
|
-
const getNextLabel = () => {
|
|
1432
|
-
if (typeof nextLabel === "function") return nextLabel(isLast);
|
|
1433
|
-
return nextLabel;
|
|
1434
|
-
};
|
|
1435
|
-
const handlePrev = () => {
|
|
1436
|
-
if (isFirst && onCancel) onCancel();
|
|
1437
|
-
else {
|
|
1438
|
-
onPrev?.();
|
|
1439
|
-
prev();
|
|
1440
|
-
}
|
|
1441
|
-
};
|
|
1442
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1443
|
-
className: cn("flex items-center justify-between gap-3", className),
|
|
1444
|
-
children: [/* @__PURE__ */ jsx("div", { children: showPrev && /* @__PURE__ */ jsx(Button, {
|
|
1445
|
-
htmlType: "button",
|
|
1446
|
-
type: "quaternary",
|
|
1447
|
-
theme: "outline",
|
|
1448
|
-
size: "small",
|
|
1449
|
-
onClick: handlePrev,
|
|
1450
|
-
disabled: isLoading || isDisabled,
|
|
1451
|
-
children: getPrevLabel()
|
|
1452
|
-
}) }), /* @__PURE__ */ jsx(Button, {
|
|
1453
|
-
htmlType: "submit",
|
|
1454
|
-
type: "primary",
|
|
1455
|
-
size: "small",
|
|
1456
|
-
loading: isLoading,
|
|
1457
|
-
disabled: isLoading || isDisabled,
|
|
1458
|
-
children: isLoading && isLast ? loadingText : getNextLabel()
|
|
1459
|
-
})]
|
|
1460
|
-
});
|
|
1461
|
-
}
|
|
1462
|
-
StepperControls.displayName = "Form.StepperControls";
|
|
1463
|
-
//#endregion
|
|
1464
|
-
//#region src/components/features/form/components/stepper/stepper-navigation.tsx
|
|
1465
|
-
/**
|
|
1466
|
-
* Form.StepperNavigation - Step indicators/progress
|
|
1467
|
-
*
|
|
1468
|
-
* Displays visual step indicators showing current progress through the form.
|
|
1469
|
-
* Supports horizontal and vertical variants with optional label orientation.
|
|
1470
|
-
*
|
|
1471
|
-
* @example
|
|
1472
|
-
* ```tsx
|
|
1473
|
-
* <Form.StepperNavigation variant="horizontal" labelOrientation="vertical" />
|
|
1474
|
-
* ```
|
|
1475
|
-
*/
|
|
1476
|
-
function StepperNavigation({ variant = "horizontal", labelOrientation = "vertical", className }) {
|
|
1477
|
-
const { steps, currentIndex } = useFormStepperContext();
|
|
1478
|
-
if (variant === "horizontal" && labelOrientation === "vertical") return /* @__PURE__ */ jsx("nav", {
|
|
1479
|
-
"aria-label": "Form steps",
|
|
1480
|
-
className: cn("flex flex-row items-start justify-between", className),
|
|
1481
|
-
children: steps.map((step, index) => {
|
|
1482
|
-
const isActive = index === currentIndex;
|
|
1483
|
-
const isCompleted = index < currentIndex;
|
|
1484
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1485
|
-
className: "relative flex flex-1 flex-col items-center",
|
|
1486
|
-
children: [
|
|
1487
|
-
!(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" }),
|
|
1488
|
-
/* @__PURE__ */ jsx("div", {
|
|
1489
|
-
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"),
|
|
1490
|
-
"aria-current": isActive ? "step" : void 0,
|
|
1491
|
-
children: isCompleted ? /* @__PURE__ */ jsx(CheckIcon, { className: "text-tertiary h-4 w-4" }) : index + 1
|
|
1492
|
-
}),
|
|
1493
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1494
|
-
className: "mt-1",
|
|
1495
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
1496
|
-
className: cn("text-xs font-medium", isActive && "text-foreground", isCompleted && "text-stepper-label", !isActive && !isCompleted && "text-stepper-label"),
|
|
1497
|
-
children: step.label
|
|
1498
|
-
}), step.description && /* @__PURE__ */ jsx("p", {
|
|
1499
|
-
className: "text-muted-foreground mt-0.5 text-xs",
|
|
1500
|
-
children: step.description
|
|
1501
|
-
})]
|
|
1502
|
-
})
|
|
1503
|
-
]
|
|
1504
|
-
}, step.id);
|
|
1505
|
-
})
|
|
1506
|
-
});
|
|
1507
|
-
if (variant === "horizontal") return /* @__PURE__ */ jsx("nav", {
|
|
1508
|
-
"aria-label": "Form steps",
|
|
1509
|
-
className: cn("flex flex-row items-center", className),
|
|
1510
|
-
children: steps.map((step, index) => {
|
|
1511
|
-
const isActive = index === currentIndex;
|
|
1512
|
-
const isCompleted = index < currentIndex;
|
|
1513
|
-
const isLast = index === steps.length - 1;
|
|
1514
|
-
return /* @__PURE__ */ jsxs(React$1.Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
1515
|
-
className: "flex items-center",
|
|
1516
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
1517
|
-
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"),
|
|
1518
|
-
"aria-current": isActive ? "step" : void 0,
|
|
1519
|
-
children: isCompleted ? /* @__PURE__ */ jsx(CheckIcon, { className: "text-tertiary size-4" }) : index + 1
|
|
1520
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
1521
|
-
className: "ml-2",
|
|
1522
|
-
children: /* @__PURE__ */ jsx("span", {
|
|
1523
|
-
className: cn("text-sm font-medium", isActive && "text-foreground", isCompleted && "text-stepper-label", !isActive && !isCompleted && "text-stepper-label"),
|
|
1524
|
-
children: step.label
|
|
1525
|
-
})
|
|
1526
|
-
})]
|
|
1527
|
-
}), !isLast && /* @__PURE__ */ jsx("div", { className: "bg-stepper-line mx-4 h-0.5 min-w-8 flex-1" })] }, step.id);
|
|
1528
|
-
})
|
|
1529
|
-
});
|
|
1530
|
-
return /* @__PURE__ */ jsx("nav", {
|
|
1531
|
-
"aria-label": "Form steps",
|
|
1532
|
-
className: cn("flex flex-col", className),
|
|
1533
|
-
children: steps.map((step, index) => {
|
|
1534
|
-
const isActive = index === currentIndex;
|
|
1535
|
-
const isCompleted = index < currentIndex;
|
|
1536
|
-
const isLast = index === steps.length - 1;
|
|
1537
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1538
|
-
className: "flex flex-row",
|
|
1539
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
1540
|
-
className: "flex flex-col items-center",
|
|
1541
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
1542
|
-
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"),
|
|
1543
|
-
"aria-current": isActive ? "step" : void 0,
|
|
1544
|
-
children: isCompleted ? /* @__PURE__ */ jsx(CheckIcon, { className: "text-tertiary size-4" }) : index + 1
|
|
1545
|
-
}), !isLast && /* @__PURE__ */ jsx("div", { className: "bg-stepper-line my-1 min-h-8 w-0.5 flex-1" })]
|
|
1546
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
1547
|
-
className: "ml-3 pb-8",
|
|
1548
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
1549
|
-
className: cn("text-sm font-medium", isActive && "text-foreground", isCompleted && "text-stepper-label", !isActive && !isCompleted && "text-stepper-label"),
|
|
1550
|
-
children: step.label
|
|
1551
|
-
}), step.description && /* @__PURE__ */ jsx("p", {
|
|
1552
|
-
className: "text-muted-foreground mt-0.5 text-xs",
|
|
1553
|
-
children: step.description
|
|
1554
|
-
})]
|
|
1555
|
-
})]
|
|
1556
|
-
}, step.id);
|
|
1557
|
-
})
|
|
1558
|
-
});
|
|
1559
|
-
}
|
|
1560
|
-
StepperNavigation.displayName = "Form.StepperNavigation";
|
|
1561
|
-
//#endregion
|
|
1562
|
-
//#region src/components/features/form/components/form-input-group.tsx
|
|
1563
|
-
/**
|
|
1564
|
-
* Form.InputGroup - Input with leading/trailing addons
|
|
1565
|
-
*
|
|
1566
|
-
* Automatically wired to the parent Form.Field context.
|
|
1567
|
-
*
|
|
1568
|
-
* @example
|
|
1569
|
-
* ```tsx
|
|
1570
|
-
* <Form.Field name="website" label="Website" required>
|
|
1571
|
-
* <Form.InputGroup leading="https://" placeholder="example.com" />
|
|
1572
|
-
* </Form.Field>
|
|
1573
|
-
* ```
|
|
1574
|
-
*/
|
|
1575
|
-
function FormInputGroup({ ref, className, disabled, ...props }) {
|
|
1576
|
-
const { id, errors, disabled: fieldDisabled, fieldState } = useFieldContext$1();
|
|
1577
|
-
const isDisabled = disabled ?? fieldDisabled;
|
|
1578
|
-
const hasErrors = errors && errors.length > 0;
|
|
1579
|
-
return /* @__PURE__ */ jsx(InputWithAddons, {
|
|
1580
|
-
...props,
|
|
1581
|
-
ref,
|
|
1582
|
-
id,
|
|
1583
|
-
name: fieldState?.name,
|
|
1584
|
-
value: fieldState?.value ?? "",
|
|
1585
|
-
onChange: (e) => fieldState?.change(e.target.value),
|
|
1586
|
-
onBlur: () => fieldState?.blur(),
|
|
1587
|
-
className: cn("text-xs!", className),
|
|
1588
|
-
disabled: isDisabled,
|
|
1589
|
-
"aria-invalid": hasErrors || void 0,
|
|
1590
|
-
"aria-describedby": hasErrors ? `${id}-error` : void 0
|
|
1591
|
-
});
|
|
1592
|
-
}
|
|
1593
|
-
FormInputGroup.displayName = "Form.InputGroup";
|
|
1594
|
-
//#endregion
|
|
1595
1426
|
//#region src/components/features/form/hooks/use-field.ts
|
|
1596
1427
|
/**
|
|
1597
1428
|
* Hook to access and control a specific field.
|
|
1598
1429
|
* Delegates to the active adapter's useField implementation.
|
|
1599
1430
|
*
|
|
1431
|
+
* Note: `meta.disabled` is always `false` because this hook operates
|
|
1432
|
+
* independently of Form.Field. If you need disabled state, read it
|
|
1433
|
+
* from your component props or from the Form.Field context via `useFieldContext()`.
|
|
1434
|
+
*
|
|
1600
1435
|
* @example
|
|
1601
1436
|
* ```tsx
|
|
1602
1437
|
* function MyCustomInput({ name }: { name: string }) {
|
|
@@ -1682,48 +1517,36 @@ function useFormContext() {
|
|
|
1682
1517
|
return useFormContext$1();
|
|
1683
1518
|
}
|
|
1684
1519
|
//#endregion
|
|
1685
|
-
//#region src/components/features/form/hooks/use-
|
|
1520
|
+
//#region src/components/features/form/hooks/use-form-state.ts
|
|
1686
1521
|
/**
|
|
1687
|
-
* Hook to access
|
|
1688
|
-
* Must be used within a Form.Stepper component
|
|
1522
|
+
* Hook to access form-level state (dirty, valid, submitted, etc.)
|
|
1689
1523
|
*
|
|
1690
1524
|
* @example
|
|
1691
1525
|
* ```tsx
|
|
1692
|
-
* function
|
|
1693
|
-
* const {
|
|
1694
|
-
* current,
|
|
1695
|
-
* currentIndex,
|
|
1696
|
-
* steps,
|
|
1697
|
-
* next,
|
|
1698
|
-
* prev,
|
|
1699
|
-
* goTo,
|
|
1700
|
-
* isFirst,
|
|
1701
|
-
* isLast,
|
|
1702
|
-
* } = useStepper();
|
|
1526
|
+
* function SaveButton() {
|
|
1527
|
+
* const { isDirty, isSubmitting, isValid } = useFormState()
|
|
1703
1528
|
*
|
|
1704
1529
|
* return (
|
|
1705
|
-
* <
|
|
1706
|
-
*
|
|
1707
|
-
*
|
|
1708
|
-
*
|
|
1709
|
-
*
|
|
1710
|
-
*
|
|
1530
|
+
* <button
|
|
1531
|
+
* type="submit"
|
|
1532
|
+
* disabled={!isDirty || isSubmitting || !isValid}
|
|
1533
|
+
* >
|
|
1534
|
+
* Save Changes
|
|
1535
|
+
* </button>
|
|
1536
|
+
* )
|
|
1711
1537
|
* }
|
|
1712
1538
|
* ```
|
|
1713
1539
|
*/
|
|
1714
|
-
function
|
|
1715
|
-
const
|
|
1540
|
+
function useFormState() {
|
|
1541
|
+
const ctx = useFormContext$1();
|
|
1716
1542
|
return {
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
isLast: context.isLast,
|
|
1725
|
-
getStepData: context.getStepData,
|
|
1726
|
-
getAllStepData: context.getAllStepData
|
|
1543
|
+
isDirty: ctx.isDirty,
|
|
1544
|
+
isValid: ctx.isValid,
|
|
1545
|
+
isSubmitting: ctx.isSubmitting,
|
|
1546
|
+
isSubmitted: ctx.isSubmitted,
|
|
1547
|
+
submitCount: ctx.submitCount,
|
|
1548
|
+
dirtyFields: ctx.dirtyFields,
|
|
1549
|
+
touchedFields: ctx.touchedFields
|
|
1727
1550
|
};
|
|
1728
1551
|
}
|
|
1729
1552
|
//#endregion
|
|
@@ -1774,19 +1597,34 @@ function useStepper() {
|
|
|
1774
1597
|
* }
|
|
1775
1598
|
* ```
|
|
1776
1599
|
*
|
|
1777
|
-
* @example Multi-Step Form
|
|
1600
|
+
* @example Multi-Step Form (separate import)
|
|
1778
1601
|
* ```tsx
|
|
1602
|
+
* import { FormStepper, FormStep, StepperNavigation, StepperControls } from '@datum-cloud/datum-ui/form/stepper';
|
|
1603
|
+
*
|
|
1779
1604
|
* const steps = [
|
|
1780
1605
|
* { id: 'account', label: 'Account', schema: accountSchema },
|
|
1781
1606
|
* { id: 'profile', label: 'Profile', schema: profileSchema },
|
|
1782
1607
|
* ];
|
|
1783
1608
|
*
|
|
1784
|
-
* <
|
|
1785
|
-
* <
|
|
1786
|
-
* <
|
|
1787
|
-
* <
|
|
1788
|
-
* <
|
|
1789
|
-
* </
|
|
1609
|
+
* <FormStepper steps={steps} onComplete={handleComplete}>
|
|
1610
|
+
* <StepperNavigation />
|
|
1611
|
+
* <FormStep id="account">...</FormStep>
|
|
1612
|
+
* <FormStep id="profile">...</FormStep>
|
|
1613
|
+
* <StepperControls />
|
|
1614
|
+
* </FormStepper>
|
|
1615
|
+
* ```
|
|
1616
|
+
*
|
|
1617
|
+
* @example Form State
|
|
1618
|
+
* ```tsx
|
|
1619
|
+
* <Form.Root schema={schema} onSubmit={handleSubmit}>
|
|
1620
|
+
* {({ isDirty, isValid, isSubmitted, submitCount }) => (
|
|
1621
|
+
* <>
|
|
1622
|
+
* <Form.Field name="name"><Form.Input /></Form.Field>
|
|
1623
|
+
* <Form.Submit disabled={!isDirty || !isValid}>Save</Form.Submit>
|
|
1624
|
+
* {isSubmitted && <p>Submitted {submitCount} time(s)</p>}
|
|
1625
|
+
* </>
|
|
1626
|
+
* )}
|
|
1627
|
+
* </Form.Root>
|
|
1790
1628
|
* ```
|
|
1791
1629
|
*
|
|
1792
1630
|
* @example Conditional Fields
|
|
@@ -1818,13 +1656,15 @@ function useStepper() {
|
|
|
1818
1656
|
* - Form.When - Conditional rendering
|
|
1819
1657
|
* - Form.FieldArray - Dynamic array of fields
|
|
1820
1658
|
* - Form.Custom - Escape hatch for custom implementations
|
|
1821
|
-
* - Form.Stepper, Form.Step, Form.StepperNavigation, Form.StepperControls
|
|
1822
1659
|
* - Form.Dialog - Form rendered inside a Dialog
|
|
1823
1660
|
* - Form.Submit, Form.Button, Form.Error, Form.Description
|
|
1824
1661
|
*
|
|
1662
|
+
* Stepper (separate import):
|
|
1663
|
+
* - `@datum-cloud/datum-ui/form/stepper` provides FormStepper, FormStep, StepperNavigation, StepperControls, useStepper
|
|
1664
|
+
*
|
|
1825
1665
|
* Hooks:
|
|
1826
|
-
* - Form.useFormContext, Form.useFieldContext, Form.useField
|
|
1827
|
-
* - Form.useWatch, Form.useWatchAll
|
|
1666
|
+
* - Form.useFormContext, Form.useFormState, Form.useFieldContext, Form.useField
|
|
1667
|
+
* - Form.useWatch, Form.useWatchAll
|
|
1828
1668
|
*/
|
|
1829
1669
|
const Form = {
|
|
1830
1670
|
Root: FormRoot,
|
|
@@ -1843,21 +1683,23 @@ const Form = {
|
|
|
1843
1683
|
RadioItem: FormRadioItem,
|
|
1844
1684
|
CopyBox: FormCopyBox,
|
|
1845
1685
|
Autocomplete: FormAutocomplete,
|
|
1686
|
+
Autosearch: FormAutosearch,
|
|
1687
|
+
Combobox: FormCombobox,
|
|
1846
1688
|
InputGroup: FormInputGroup,
|
|
1689
|
+
DatePicker: FormDatePicker,
|
|
1690
|
+
DateTimePicker: FormDateTimePicker,
|
|
1691
|
+
TimePicker: FormTimePicker,
|
|
1692
|
+
Transfer: FormTransfer,
|
|
1847
1693
|
When: FormWhen,
|
|
1848
1694
|
FieldArray: FormFieldArray,
|
|
1849
1695
|
Custom: FormCustom,
|
|
1850
|
-
Stepper: FormStepper,
|
|
1851
|
-
Step: FormStep,
|
|
1852
|
-
StepperNavigation,
|
|
1853
|
-
StepperControls,
|
|
1854
1696
|
Dialog: FormDialog,
|
|
1855
1697
|
useFormContext,
|
|
1698
|
+
useFormState,
|
|
1856
1699
|
useFieldContext,
|
|
1857
1700
|
useField,
|
|
1858
1701
|
useWatch,
|
|
1859
|
-
useWatchAll
|
|
1860
|
-
useStepper
|
|
1702
|
+
useWatchAll
|
|
1861
1703
|
};
|
|
1862
1704
|
//#endregion
|
|
1863
|
-
export {
|
|
1705
|
+
export { FormCheckbox as A, FormDialog as C, FormCustom as D, FormDatePicker as E, FormAutosearch as M, FormAutocomplete as N, FormCopyBox as O, FormError as S, FormDateTimePicker as T, FormRadioGroup as _, useField as a, FormFieldArray as b, useWatchAll as c, FormTextarea as d, FormSwitch as f, FormRoot as g, FormSelectItem as h, useFieldContext as i, FormButton as j, FormCombobox as k, FormTransfer as l, FormSelect as m, useFormState as n, FormWhen as o, FormSubmit as p, useFormContext as r, useWatch as s, Form as t, FormTimePicker as u, FormRadioItem as v, FormDescription as w, FormField as x, FormInput as y };
|