@datum-cloud/datum-ui 0.6.0-alpha.b8a44ac → 0.6.1

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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/dist/autocomplete/index.mjs +1 -1
  4. package/dist/{autocomplete-V5-qslzS.mjs → autocomplete-CkYJueBL.mjs} +2 -2
  5. package/dist/autosearch/index.mjs +199 -0
  6. package/dist/{calendar-date-picker-DWK94_DC.mjs → calendar-date-picker-CDT-8Ha8.mjs} +2 -1
  7. package/dist/combobox/index.mjs +1 -1
  8. package/dist/{combobox-cKTFK4uN.mjs → combobox-B-C9lJeD.mjs} +3 -2
  9. package/dist/components/features/autocomplete/autocomplete.d.ts +1 -1
  10. package/dist/components/features/autocomplete/autocomplete.d.ts.map +1 -1
  11. package/dist/components/features/autocomplete/autocomplete.types.d.ts +2 -0
  12. package/dist/components/features/autocomplete/autocomplete.types.d.ts.map +1 -1
  13. package/dist/components/features/autosearch/autosearch.d.ts +35 -0
  14. package/dist/components/features/autosearch/autosearch.d.ts.map +1 -0
  15. package/dist/components/features/autosearch/autosearch.types.d.ts +51 -0
  16. package/dist/components/features/autosearch/autosearch.types.d.ts.map +1 -0
  17. package/dist/components/features/autosearch/index.d.ts +3 -0
  18. package/dist/components/features/autosearch/index.d.ts.map +1 -0
  19. package/dist/components/features/calendar-date-picker/calendar-date-picker.d.ts +2 -1
  20. package/dist/components/features/calendar-date-picker/calendar-date-picker.d.ts.map +1 -1
  21. package/dist/components/features/combobox/combobox.d.ts +1 -1
  22. package/dist/components/features/combobox/combobox.d.ts.map +1 -1
  23. package/dist/components/features/combobox/types.d.ts +6 -0
  24. package/dist/components/features/combobox/types.d.ts.map +1 -1
  25. package/dist/components/features/date-time-picker/date-time-picker.d.ts +1 -1
  26. package/dist/components/features/date-time-picker/date-time-picker.d.ts.map +1 -1
  27. package/dist/components/features/date-time-picker/types.d.ts +6 -0
  28. package/dist/components/features/date-time-picker/types.d.ts.map +1 -1
  29. package/dist/components/features/form/adapter-types.d.ts +6 -0
  30. package/dist/components/features/form/adapter-types.d.ts.map +1 -1
  31. package/dist/components/features/form/adapters/conform/conform-adapter.d.ts.map +1 -1
  32. package/dist/components/features/form/adapters/rhf/rhf-adapter.d.ts.map +1 -1
  33. package/dist/components/features/form/components/form-autocomplete.d.ts.map +1 -1
  34. package/dist/components/features/form/components/form-autosearch.d.ts +23 -11
  35. package/dist/components/features/form/components/form-autosearch.d.ts.map +1 -1
  36. package/dist/components/features/form/components/form-checkbox.d.ts.map +1 -1
  37. package/dist/components/features/form/components/form-combobox.d.ts +5 -1
  38. package/dist/components/features/form/components/form-combobox.d.ts.map +1 -1
  39. package/dist/components/features/form/components/form-date-picker.d.ts +3 -1
  40. package/dist/components/features/form/components/form-date-picker.d.ts.map +1 -1
  41. package/dist/components/features/form/components/form-date-time-picker.d.ts +3 -1
  42. package/dist/components/features/form/components/form-date-time-picker.d.ts.map +1 -1
  43. package/dist/components/features/form/components/form-field.d.ts.map +1 -1
  44. package/dist/components/features/form/components/form-radio-group.d.ts.map +1 -1
  45. package/dist/components/features/form/components/form-root.d.ts.map +1 -1
  46. package/dist/components/features/form/components/form-switch.d.ts.map +1 -1
  47. package/dist/components/features/form/components/form-time-picker.d.ts.map +1 -1
  48. package/dist/components/features/form/components/form-transfer.d.ts.map +1 -1
  49. package/dist/components/features/form/components/stepper/form-stepper.d.ts +3 -1
  50. package/dist/components/features/form/components/stepper/form-stepper.d.ts.map +1 -1
  51. package/dist/components/features/form/hooks/use-display-touched.d.ts +20 -0
  52. package/dist/components/features/form/hooks/use-display-touched.d.ts.map +1 -0
  53. package/dist/components/features/form/hooks/use-field.d.ts +4 -0
  54. package/dist/components/features/form/hooks/use-field.d.ts.map +1 -1
  55. package/dist/components/features/form/types/index.d.ts +10 -0
  56. package/dist/components/features/form/types/index.d.ts.map +1 -1
  57. package/dist/data-table/index.mjs +1 -1
  58. package/dist/date-picker/index.mjs +1 -1
  59. package/dist/date-time-picker/index.mjs +1 -1
  60. package/dist/{date-time-picker-Dy2jrJoN.mjs → date-time-picker-BomrW07W.mjs} +49 -46
  61. package/dist/form/adapters/conform/index.mjs +10 -3
  62. package/dist/form/adapters/rhf/index.mjs +11 -8
  63. package/dist/form/index.mjs +1 -1
  64. package/dist/form/stepper/index.mjs +19 -41
  65. package/dist/{form-mlNLKaB5.mjs → form-B3rQ4CH9.mjs} +130 -46
  66. package/dist/index.mjs +3 -3
  67. package/dist/transfer/index.mjs +1 -1
  68. package/dist/{transfer-B2n8pgEQ.mjs → transfer-46C-rFFW.mjs} +12 -8
  69. package/dist/{get-field-constraints-BicgDkfH.mjs → use-display-touched-I39aXEBD.mjs} +33 -1
  70. package/package.json +8 -3
@@ -6,7 +6,6 @@ import { t as defineStepper } from "../../stepper-DvIOp0hh.mjs";
6
6
  import { CheckIcon } from "lucide-react";
7
7
  import * as React$1 from "react";
8
8
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
9
- import { z } from "zod";
10
9
  //#region src/components/features/form/components/stepper/form-stepper.tsx
11
10
  const FormStepperContext = React$1.createContext(null);
12
11
  function useFormStepperContext() {
@@ -15,39 +14,6 @@ function useFormStepperContext() {
15
14
  return context;
16
15
  }
17
16
  /**
18
- * Recursively unwrap ZodIntersection (from .and()) to extract the base ZodObject.
19
- *
20
- * Zod v4 schema types use `def.type` as a string discriminant:
21
- * - "intersection" (from .and()): merge left + right base objects
22
- * - "object": return directly
23
- *
24
- * Note: In Zod v4, .superRefine() and .refine() return `this` (no wrapper),
25
- * so only ZodIntersection needs unwrapping.
26
- */
27
- function getBaseObject(schema) {
28
- if (schema.def.type === "intersection") {
29
- const intersectionDef = schema.def;
30
- const left = getBaseObject(intersectionDef.left);
31
- const right = getBaseObject(intersectionDef.right);
32
- return left.merge(right);
33
- }
34
- if (schema.def.type !== "object") return z.object({});
35
- return schema;
36
- }
37
- /**
38
- * Merge multiple zod schemas into one ZodObject for HTML constraint generation.
39
- * Handles ZodIntersection (.and()) by unwrapping to base ZodObject shapes.
40
- * Per-step validation still uses the original schemas with all refinements intact.
41
- */
42
- function mergeSchemas(steps) {
43
- if (steps.length === 0) throw new Error("Form.Stepper requires at least one step");
44
- return steps.reduce((acc, step, index) => {
45
- const base = getBaseObject(step.schema);
46
- if (index === 0) return base;
47
- return acc.merge(base);
48
- }, {});
49
- }
50
- /**
51
17
  * Convert StepConfig[] to Stepperize step format
52
18
  */
53
19
  function toStepperizeSteps(steps) {
@@ -89,7 +55,7 @@ function toStepperizeSteps(steps) {
89
55
  * </Form.Stepper>
90
56
  * ```
91
57
  */
92
- function FormStepper({ steps, children, onComplete, onStepChange, initialStep, className, defaultValues, id, formComponent }) {
58
+ function FormStepper({ steps, children, onComplete, onStepChange, initialStep, className, defaultValues, id, formComponent, mode = "onSubmit" }) {
93
59
  const stepperDef = React$1.useMemo(() => {
94
60
  return defineStepper(...toStepperizeSteps(steps));
95
61
  }, [steps]);
@@ -111,19 +77,19 @@ function FormStepper({ steps, children, onComplete, onStepChange, initialStep, c
111
77
  defaultValues,
112
78
  id,
113
79
  formComponent,
80
+ mode,
114
81
  children
115
82
  })
116
83
  });
117
84
  }
118
85
  FormStepper.displayName = "Form.Stepper";
119
- function FormStepperContent({ steps, stepperDef, children, onComplete, onStepChange, className, defaultValues, id, formComponent }) {
86
+ function FormStepperContent({ steps, stepperDef, children, onComplete, onStepChange, className, defaultValues, id, formComponent, mode }) {
120
87
  const { useStepper } = stepperDef;
121
88
  const stepper = useStepper();
122
89
  return /* @__PURE__ */ jsx(StepForm, {
123
90
  steps,
124
91
  stepper,
125
92
  currentStepConfig: React$1.useMemo(() => steps.find((s) => s.id === stepper.state.current.data.id) ?? steps[0], [steps, stepper.state.current.data.id]),
126
- combinedSchema: React$1.useMemo(() => mergeSchemas(steps), [steps]),
127
93
  storedValues: React$1.useMemo(() => {
128
94
  const allMetadata = steps.reduce((acc, step) => ({
129
95
  ...acc,
@@ -144,15 +110,20 @@ function FormStepperContent({ steps, stepperDef, children, onComplete, onStepCha
144
110
  className,
145
111
  id,
146
112
  formComponent,
113
+ mode,
147
114
  children
148
115
  }, stepper.state.current.data.id);
149
116
  }
150
- function StepForm({ steps, stepper, currentStepConfig, combinedSchema: _combinedSchema, storedValues, children, onComplete, onStepChange, className, id, formComponent: FormComp = "form" }) {
117
+ function StepForm({ steps, stepper, currentStepConfig, storedValues, children, onComplete, onStepChange, className, id, formComponent: FormComp = "form", mode }) {
151
118
  const adapter = useAdapter();
152
119
  const [isSubmitting, setIsSubmitting] = React$1.useState(false);
120
+ const [isSubmitted, setIsSubmitted] = React$1.useState(false);
121
+ const [submitCount, setSubmitCount] = React$1.useState(0);
153
122
  const formRef = React$1.useRef(null);
154
123
  const currentIndex = stepper.lookup.getIndex(stepper.state.current.data.id);
155
124
  const handleStepSubmit = React$1.useCallback(async (data) => {
125
+ setIsSubmitted(true);
126
+ setSubmitCount((prev) => prev + 1);
156
127
  stepper.metadata.set(stepper.state.current.data.id, data);
157
128
  if (stepper.state.isLast) {
158
129
  setIsSubmitting(true);
@@ -183,7 +154,7 @@ function StepForm({ steps, stepper, currentStepConfig, combinedSchema: _combined
183
154
  const instance = adapter.useCreateForm({
184
155
  schema: currentStepConfig.schema,
185
156
  defaultValues: storedValues,
186
- mode: "onSubmit",
157
+ mode,
187
158
  id: `${id ?? "stepper"}-${currentStepConfig.id}`,
188
159
  onSubmit: handleStepSubmit,
189
160
  formRef
@@ -253,16 +224,23 @@ function StepForm({ steps, stepper, currentStepConfig, combinedSchema: _combined
253
224
  isSubmitting,
254
225
  isDirty: instance.formState.isDirty,
255
226
  isValid: instance.formState.isValid,
256
- isSubmitted: instance.formState.isSubmitted,
257
- submitCount: instance.formState.submitCount,
227
+ isSubmitted,
228
+ submitCount,
258
229
  dirtyFields: instance.formState.dirtyFields,
259
230
  touchedFields: instance.formState.touchedFields,
231
+ mode,
232
+ displayTouchedFields: instance.touchedFields,
233
+ markFieldTouched: instance.markFieldTouched,
234
+ markAllFieldsTouched: instance.markAllFieldsTouched,
260
235
  submit: () => formRef.current?.requestSubmit(),
261
236
  reset: () => instance.reset(),
262
237
  formId: instance.id
263
238
  }), [
264
239
  instance,
265
240
  isSubmitting,
241
+ isSubmitted,
242
+ submitCount,
243
+ mode,
266
244
  instance.formState
267
245
  ]);
268
246
  const renderProps = {
@@ -10,17 +10,18 @@ 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-V5-qslzS.mjs";
14
- import { t as CalendarDatePicker } from "./calendar-date-picker-DWK94_DC.mjs";
13
+ import { t as Autocomplete } from "./autocomplete-CkYJueBL.mjs";
14
+ import { t as CalendarDatePicker } from "./calendar-date-picker-CDT-8Ha8.mjs";
15
15
  import { t as toast } from "./toast-BWnN5fax.mjs";
16
+ import { Autosearch } from "./autosearch/index.mjs";
16
17
  import { n as useFormContext$1, t as FormProvider } from "./form-context-Ccxm-wqL.mjs";
17
- import { t as Combobox } from "./combobox-cKTFK4uN.mjs";
18
+ import { t as Combobox } from "./combobox-B-C9lJeD.mjs";
18
19
  import { t as useCopyToClipboard } from "./use-copy-to-clipboard-uNeeVHC4.mjs";
19
- import { t as DateTimePicker } from "./date-time-picker-Dy2jrJoN.mjs";
20
+ import { t as DateTimePicker } from "./date-time-picker-BomrW07W.mjs";
20
21
  import { n as useAdapter } from "./adapter-context-rWveHhDd.mjs";
21
22
  import { InputWithAddons } from "./input-with-addons/index.mjs";
22
23
  import { t as TimePicker } from "./time-picker-BoF7pZZ2.mjs";
23
- import { t as Transfer } from "./transfer-B2n8pgEQ.mjs";
24
+ import { t as Transfer } from "./transfer-46C-rFFW.mjs";
24
25
  import { CheckIcon, CircleHelp, CopyIcon } from "lucide-react";
25
26
  import * as React$1 from "react";
26
27
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -94,7 +95,10 @@ function FormAutocomplete({ disabled, className, ...props }) {
94
95
  name: fieldState?.name,
95
96
  id,
96
97
  value,
97
- onValueChange: (val) => fieldState?.change(val),
98
+ onValueChange: (val) => {
99
+ fieldState?.change(val);
100
+ fieldState?.blur();
101
+ },
98
102
  disabled: isDisabled,
99
103
  triggerClassName: cn(hasErrors && "border-destructive", props.triggerClassName),
100
104
  className
@@ -104,23 +108,54 @@ FormAutocomplete.displayName = "Form.Autocomplete";
104
108
  //#endregion
105
109
  //#region src/components/features/form/components/form-autosearch.tsx
106
110
  /**
107
- * Form.Autosearch - Alias to Form.Autocomplete with search-first focus
111
+ * Form.Autosearch - Search-first input with dropdown results
108
112
  *
109
- * This is a convenience wrapper around Form.Autocomplete that emphasizes
110
- * the search functionality. It's functionally identical to Form.Autocomplete.
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.
111
116
  *
112
117
  * @example Basic usage
113
118
  * ```tsx
114
- * <Form.Field name="search" label="Search">
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">
115
132
  * <Form.Autosearch
116
- * options={options}
117
- * placeholder="Type to search..."
133
+ * options={searchResults}
134
+ * onSearch={debouncedSearch}
135
+ * searchDebounceMs={500}
136
+ * placeholder="Type email to search..."
118
137
  * />
119
138
  * </Form.Field>
120
139
  * ```
121
140
  */
122
- function FormAutosearch(props) {
123
- return /* @__PURE__ */ jsx(FormAutocomplete, { ...props });
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,
150
+ value,
151
+ onValueChange: (val) => {
152
+ fieldState?.change(val);
153
+ fieldState?.blur();
154
+ },
155
+ disabled: isDisabled,
156
+ inputClassName: cn(hasErrors && "border-destructive", inputClassName),
157
+ className
158
+ });
124
159
  }
125
160
  FormAutosearch.displayName = "Form.Autosearch";
126
161
  //#endregion
@@ -180,7 +215,10 @@ function FormCheckbox({ label, disabled, className }) {
180
215
  children: [/* @__PURE__ */ jsx(Checkbox, {
181
216
  id,
182
217
  checked,
183
- onCheckedChange: (value) => fieldState?.change(Boolean(value)),
218
+ onCheckedChange: (value) => {
219
+ fieldState?.change(Boolean(value));
220
+ fieldState?.blur();
221
+ },
184
222
  disabled: isDisabled,
185
223
  "aria-invalid": hasErrors || void 0,
186
224
  "aria-describedby": hasErrors ? `${id}-error` : void 0
@@ -194,12 +232,13 @@ function FormCheckbox({ label, disabled, className }) {
194
232
  FormCheckbox.displayName = "Form.Checkbox";
195
233
  //#endregion
196
234
  //#region src/components/features/form/components/form-combobox.tsx
197
- function FormCombobox({ options, placeholder, searchPlaceholder, emptyMessage, disabled, className, triggerClassName, contentClassName, searchable = true, showDropdownArrow = true, clearable = false, "data-testid": testId }) {
235
+ function FormCombobox({ options, placeholder, searchPlaceholder, emptyMessage, disabled, className, triggerClassName, contentClassName, searchable = true, showDropdownArrow = true, clearable = false, "data-testid": testId, modal }) {
198
236
  const { id, errors, disabled: fieldDisabled, fieldState } = useFieldContext$1();
199
237
  const isDisabled = disabled ?? fieldDisabled;
200
238
  const hasErrors = errors && errors.length > 0;
201
239
  const handleChange = React$1.useCallback((value) => {
202
240
  fieldState?.change(value ?? "");
241
+ fieldState?.blur();
203
242
  }, [fieldState]);
204
243
  return /* @__PURE__ */ jsx(Combobox, {
205
244
  id,
@@ -213,6 +252,7 @@ function FormCombobox({ options, placeholder, searchPlaceholder, emptyMessage, d
213
252
  searchable,
214
253
  showDropdownArrow,
215
254
  clearable,
255
+ modal,
216
256
  className,
217
257
  triggerClassName: cn(hasErrors && "border-destructive", triggerClassName),
218
258
  contentClassName,
@@ -329,7 +369,7 @@ function FormCustom({ children }) {
329
369
  FormCustom.displayName = "Form.Custom";
330
370
  //#endregion
331
371
  //#region src/components/features/form/components/form-date-picker.tsx
332
- function FormDatePicker({ placeholder, disabled, className, triggerClassName, numberOfMonths = 1, minDate: minDateProp, maxDate: maxDateProp, disableFuture, disablePast }) {
372
+ function FormDatePicker({ placeholder, disabled, className, triggerClassName, numberOfMonths = 1, minDate: minDateProp, maxDate: maxDateProp, disableFuture, disablePast, modal }) {
333
373
  const { id, errors, disabled: fieldDisabled, fieldState } = useFieldContext$1();
334
374
  const isDisabled = disabled ?? fieldDisabled;
335
375
  const hasErrors = errors && errors.length > 0;
@@ -364,6 +404,7 @@ function FormDatePicker({ placeholder, disabled, className, triggerClassName, nu
364
404
  onDateSelect: React$1.useCallback((range) => {
365
405
  if (!range) {
366
406
  fieldState?.change(void 0);
407
+ fieldState?.blur();
367
408
  return;
368
409
  }
369
410
  if (numberOfMonths === 1) fieldState?.change(range.from.toISOString());
@@ -371,6 +412,7 @@ function FormDatePicker({ placeholder, disabled, className, triggerClassName, nu
371
412
  from: range.from.toISOString(),
372
413
  to: range.to?.toISOString()
373
414
  });
415
+ fieldState?.blur();
374
416
  }, [fieldState, numberOfMonths]),
375
417
  numberOfMonths,
376
418
  placeholder,
@@ -380,6 +422,7 @@ function FormDatePicker({ placeholder, disabled, className, triggerClassName, nu
380
422
  disableFuture,
381
423
  disablePast,
382
424
  variant: "outline",
425
+ modal,
383
426
  className: cn(className),
384
427
  triggerClassName: cn(triggerClassName),
385
428
  "aria-invalid": hasErrors || void 0,
@@ -389,7 +432,7 @@ function FormDatePicker({ placeholder, disabled, className, triggerClassName, nu
389
432
  FormDatePicker.displayName = "Form.DatePicker";
390
433
  //#endregion
391
434
  //#region src/components/features/form/components/form-date-time-picker.tsx
392
- function FormDateTimePicker({ minDate: minDateProp, maxDate: maxDateProp, disabledDates, timezone, showTimezoneIndicator, placeholder, disabled, className }) {
435
+ function FormDateTimePicker({ minDate: minDateProp, maxDate: maxDateProp, disabledDates, timezone, showTimezoneIndicator, placeholder, disabled, className, modal }) {
393
436
  const { id, errors, disabled: fieldDisabled, fieldState } = useFieldContext$1();
394
437
  const isDisabled = disabled ?? fieldDisabled;
395
438
  const hasErrors = errors && errors.length > 0;
@@ -404,6 +447,7 @@ function FormDateTimePicker({ minDate: minDateProp, maxDate: maxDateProp, disabl
404
447
  value: currentValue,
405
448
  onChange: React$1.useCallback((value) => {
406
449
  fieldState?.change(value);
450
+ fieldState?.blur();
407
451
  }, [fieldState]),
408
452
  minDate,
409
453
  maxDate,
@@ -412,6 +456,7 @@ function FormDateTimePicker({ minDate: minDateProp, maxDate: maxDateProp, disabl
412
456
  showTimezoneIndicator,
413
457
  placeholder,
414
458
  disabled: isDisabled,
459
+ modal,
415
460
  className: cn(className),
416
461
  "aria-invalid": hasErrors || void 0,
417
462
  "aria-describedby": hasErrors ? `${id}-error` : void 0
@@ -661,10 +706,33 @@ function FieldLabel({ htmlFor, label, hasErrors, required, tooltip, className })
661
706
  */
662
707
  function FormField({ name, children, label, description, tooltip, required = false, disabled = false, className, labelClassName }) {
663
708
  const adapter = useAdapter();
664
- const { fields, isSubmitting, form } = useFormContext$1();
709
+ const { fields, isSubmitting, form, mode, displayTouchedFields, markFieldTouched } = useFormContext$1();
665
710
  const fieldState = adapter.useField(name);
666
- const errors = fieldState.errors;
711
+ const isDisplayTouched = displayTouchedFields.includes(name);
712
+ const rawErrors = fieldState.errors;
713
+ const errors = isDisplayTouched ? rawErrors : [];
667
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
+ ]);
668
736
  const fieldId = fieldState.id;
669
737
  const descriptionId = description ? `${fieldId}-description` : void 0;
670
738
  const errorId = hasErrors ? `${fieldId}-error` : void 0;
@@ -711,6 +779,9 @@ function FormField({ name, children, label, description, tooltip, required = fal
711
779
  value: contextValue,
712
780
  children: /* @__PURE__ */ jsxs("div", {
713
781
  className: cn("flex flex-col space-y-2", className),
782
+ onFocusCapture: handleFocus,
783
+ onChange: handleChange,
784
+ onBlur: handleBlur,
714
785
  children: [
715
786
  label && /* @__PURE__ */ jsx(FieldLabel, {
716
787
  htmlFor: fieldId,
@@ -866,7 +937,10 @@ function FormRadioGroup({ orientation = "vertical", disabled, className, childre
866
937
  const hasErrors = errors && errors.length > 0;
867
938
  return /* @__PURE__ */ jsx(RadioGroup, {
868
939
  value: fieldState?.value != null ? String(fieldState.value) : void 0,
869
- onValueChange: (val) => fieldState?.change(val),
940
+ onValueChange: (val) => {
941
+ fieldState?.change(val);
942
+ fieldState?.blur();
943
+ },
870
944
  disabled: isDisabled,
871
945
  "aria-invalid": hasErrors || void 0,
872
946
  "aria-describedby": hasErrors ? `${id}-error` : void 0,
@@ -931,7 +1005,7 @@ FormRadioItem.displayName = "Form.RadioItem";
931
1005
  * </Form.Root>
932
1006
  * ```
933
1007
  */
934
- function FormRoot({ schema, children, onSubmit, action, method = "POST", formComponent: FormComp = "form", id, name, defaultValues, mode = "onBlur", isSubmitting: externalIsSubmitting, onError, onSuccess, telemetry, className }) {
1008
+ function FormRoot({ schema, children, onSubmit, action, method = "POST", formComponent: FormComp = "form", id, name, defaultValues, mode = "onChange", isSubmitting: externalIsSubmitting, onError, onSuccess, telemetry, className }) {
935
1009
  const adapter = useAdapter();
936
1010
  const [internalIsSubmitting, setInternalIsSubmitting] = React$1.useState(false);
937
1011
  const isSubmitting = externalIsSubmitting ?? internalIsSubmitting;
@@ -991,6 +1065,10 @@ function FormRoot({ schema, children, onSubmit, action, method = "POST", formCom
991
1065
  submitCount,
992
1066
  dirtyFields: formState.dirtyFields,
993
1067
  touchedFields: formState.touchedFields,
1068
+ mode,
1069
+ displayTouchedFields: instance.touchedFields,
1070
+ markFieldTouched: instance.markFieldTouched,
1071
+ markAllFieldsTouched: instance.markAllFieldsTouched,
994
1072
  submit: () => formRef.current?.requestSubmit(),
995
1073
  reset: () => instance.reset(),
996
1074
  formId: instance.id
@@ -999,31 +1077,26 @@ function FormRoot({ schema, children, onSubmit, action, method = "POST", formCom
999
1077
  isSubmitting,
1000
1078
  formState,
1001
1079
  isSubmitted,
1002
- submitCount
1003
- ]);
1004
- const isRenderFunction = typeof children === "function";
1005
- const renderProps = React$1.useMemo(() => ({
1006
- form: instance,
1007
- fields: instance.fields,
1008
- isSubmitting,
1009
- isDirty: formState.isDirty,
1010
- isValid: formState.isValid,
1011
- isSubmitted,
1012
1080
  submitCount,
1013
- dirtyFields: formState.dirtyFields,
1014
- touchedFields: formState.touchedFields,
1015
- submit: () => formRef.current?.requestSubmit(),
1016
- reset: () => instance.reset()
1017
- }), [
1018
- instance,
1019
- isSubmitting,
1020
- formState,
1021
- isSubmitted,
1022
- submitCount
1081
+ mode
1023
1082
  ]);
1083
+ const isRenderFunction = typeof children === "function";
1024
1084
  const renderChildren = () => {
1025
- if (isRenderFunction) return children(renderProps);
1026
- 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
+ });
1027
1100
  };
1028
1101
  return /* @__PURE__ */ jsx(FormProvider, {
1029
1102
  value: contextValue,
@@ -1038,7 +1111,9 @@ function FormRoot({ schema, children, onSubmit, action, method = "POST", formCom
1038
1111
  autoComplete: "off",
1039
1112
  noValidate: true,
1040
1113
  onSubmit: (e) => {
1114
+ const submitter = e.nativeEvent.submitter;
1041
1115
  e.stopPropagation();
1116
+ if (submitter && !submitter.hidden) instance.markAllFieldsTouched();
1042
1117
  telemetry?.onSubmit?.({
1043
1118
  formName: name ?? "",
1044
1119
  formId: id
@@ -1153,7 +1228,10 @@ function FormSwitch({ label, disabled, className }) {
1153
1228
  children: [/* @__PURE__ */ jsx(Switch, {
1154
1229
  id,
1155
1230
  checked,
1156
- onCheckedChange: (value) => fieldState?.change(Boolean(value)),
1231
+ onCheckedChange: (value) => {
1232
+ fieldState?.change(Boolean(value));
1233
+ fieldState?.blur();
1234
+ },
1157
1235
  disabled: isDisabled,
1158
1236
  "aria-invalid": hasErrors || void 0,
1159
1237
  "aria-describedby": hasErrors ? `${id}-error` : void 0
@@ -1210,6 +1288,7 @@ function FormTimePicker({ min, max, step, placeholder, disabled, className }) {
1210
1288
  value: fieldState?.value ?? "",
1211
1289
  onChange: React$1.useCallback((value) => {
1212
1290
  fieldState?.change(value || void 0);
1291
+ fieldState?.blur();
1213
1292
  }, [fieldState]),
1214
1293
  min,
1215
1294
  max,
@@ -1250,6 +1329,7 @@ function FormTransfer({ disabled, minItems, maxItems, ...props }) {
1250
1329
  const value = Array.isArray(fieldState?.value) ? fieldState.value : [];
1251
1330
  const handleChange = React$1.useCallback((newValue) => {
1252
1331
  fieldState?.change(newValue);
1332
+ fieldState?.blur();
1253
1333
  }, [fieldState]);
1254
1334
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
1255
1335
  "aria-invalid": hasErrors || void 0,
@@ -1261,7 +1341,7 @@ function FormTransfer({ disabled, minItems, maxItems, ...props }) {
1261
1341
  disabled: isDisabled
1262
1342
  })
1263
1343
  }), (minItems != null || maxItems != null) && /* @__PURE__ */ jsx("p", {
1264
- className: "text-sm text-muted-foreground mt-2",
1344
+ className: "text-ring text-xs text-wrap mt-2",
1265
1345
  children: minItems != null && maxItems != null ? `Select between ${minItems} and ${maxItems} items` : minItems != null ? `Select at least ${minItems} items` : `Select up to ${maxItems} items`
1266
1346
  })] });
1267
1347
  }
@@ -1348,6 +1428,10 @@ FormWhen.displayName = "Form.When";
1348
1428
  * Hook to access and control a specific field.
1349
1429
  * Delegates to the active adapter's useField implementation.
1350
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
+ *
1351
1435
  * @example
1352
1436
  * ```tsx
1353
1437
  * function MyCustomInput({ name }: { name: string }) {
package/dist/index.mjs CHANGED
@@ -39,9 +39,9 @@ import { a as ListItem, c as Title, d as titleVariants, i as List, l as paragrap
39
39
  import { t as VisuallyHidden } from "./visuallyhidden-BLUsJpYH.mjs";
40
40
  import { n as NavMenu, t as AppNavigation } from "./app-navigation-84ro28PU.mjs";
41
41
  import { t as LoaderOverlay } from "./loader-overlay-BTFdkp7W.mjs";
42
- import { t as Autocomplete } from "./autocomplete-V5-qslzS.mjs";
42
+ import { t as Autocomplete } from "./autocomplete-CkYJueBL.mjs";
43
43
  import { n as avatarStackVariants, t as AvatarStack } from "./avatar-stack-oVr8tsU7.mjs";
44
- import { t as CalendarDatePicker } from "./calendar-date-picker-DWK94_DC.mjs";
44
+ import { t as CalendarDatePicker } from "./calendar-date-picker-CDT-8Ha8.mjs";
45
45
  import { i as ClientOnly, n as useTheme, r as ThemeScript, t as ThemeProvider } from "./themes-DG1md8FI.mjs";
46
46
  import { a as formatJson, c as isValidYaml, d as CodeEditor, i as CodeEditorTabs, l as jsonToYaml, n as jsonSchema, o as formatYaml, r as yamlSchema, s as isValidJson, t as createCodeEditorSchema, u as yamlToJson } from "./types-BZNk3q65.mjs";
47
47
  import { t as toast } from "./toast-BWnN5fax.mjs";
@@ -49,7 +49,7 @@ import { n as Toaster, t as useToast } from "./toast-DpxlFNNx.mjs";
49
49
  import { a as DropdownMenuItem, c as DropdownMenuRadioGroup, d as DropdownMenuShortcut, f as DropdownMenuSub, h as DropdownMenuTrigger, i as DropdownMenuGroup, l as DropdownMenuRadioItem, m as DropdownMenuSubTrigger, n as DropdownMenuCheckboxItem, o as DropdownMenuLabel, p as DropdownMenuSubContent, r as DropdownMenuContent, s as DropdownMenuPortal, t as DropdownMenu, u as DropdownMenuSeparator } from "./dropdown-DZiAt-jS.mjs";
50
50
  import { i as FileInputButton, n as DropzoneContent, r as DropzoneEmptyState, t as Dropzone } from "./dropzone-ogtpQ4fy.mjs";
51
51
  import { t as EmptyContent } from "./empty-content-C63GPJ5d.mjs";
52
- import { A as FormCheckbox, C as FormDialog, D as FormCustom, E as FormDatePicker, M as FormAutosearch, N as FormAutocomplete, O as FormCopyBox, S as FormError, T as FormDateTimePicker, _ as FormRadioGroup, a as useField, b as FormFieldArray, c as useWatchAll, d as FormTextarea, f as FormSwitch, g as FormRoot, h as FormSelectItem, i as useFieldContext, j as FormButton, k as FormCombobox, l as FormTransfer, m as FormSelect, n as useFormState, o as FormWhen, p as FormSubmit, r as useFormContext, s as useWatch, t as Form, u as FormTimePicker, v as FormRadioItem, w as FormDescription, x as FormField, y as FormInput } from "./form-mlNLKaB5.mjs";
52
+ import { A as FormCheckbox, C as FormDialog, D as FormCustom, E as FormDatePicker, M as FormAutosearch, N as FormAutocomplete, O as FormCopyBox, S as FormError, T as FormDateTimePicker, _ as FormRadioGroup, a as useField, b as FormFieldArray, c as useWatchAll, d as FormTextarea, f as FormSwitch, g as FormRoot, h as FormSelectItem, i as useFieldContext, j as FormButton, k as FormCombobox, l as FormTransfer, m as FormSelect, n as useFormState, o as FormWhen, p as FormSubmit, r as useFormContext, s as useWatch, t as Form, u as FormTimePicker, v as FormRadioItem, w as FormDescription, x as FormField, y as FormInput } from "./form-B3rQ4CH9.mjs";
53
53
  import { t as useCopyToClipboard } from "./use-copy-to-clipboard-uNeeVHC4.mjs";
54
54
  import { n as useAdapter, t as FormAdapterProvider } from "./adapter-context-rWveHhDd.mjs";
55
55
  import { InputWithAddons } from "./input-with-addons/index.mjs";
@@ -1,2 +1,2 @@
1
- import { t as Transfer } from "../transfer-B2n8pgEQ.mjs";
1
+ import { t as Transfer } from "../transfer-46C-rFFW.mjs";
2
2
  export { Transfer };
@@ -203,22 +203,26 @@ function Transfer({ items, value, onChange, itemKey, itemLabel, itemGroup, searc
203
203
  itemLabel,
204
204
  itemGroup
205
205
  });
206
- const handleSelect = (key) => {
206
+ const handleSelect = React$1.useCallback((key) => {
207
207
  if (value.includes(key)) return;
208
208
  onChange([...value, key]);
209
- };
210
- const handleDeselect = (key) => {
209
+ }, [value, onChange]);
210
+ const handleDeselect = React$1.useCallback((key) => {
211
211
  onChange(value.filter((k) => k !== key));
212
- };
213
- const handleSelectAll = () => {
212
+ }, [value, onChange]);
213
+ const handleSelectAll = React$1.useCallback(() => {
214
214
  const sourceKeys = state.filteredSourceItems.map((item) => item.key);
215
215
  const existing = new Set(value);
216
216
  const newKeys = sourceKeys.filter((k) => !existing.has(k));
217
217
  onChange([...value, ...newKeys]);
218
- };
219
- const handleClearAll = () => {
218
+ }, [
219
+ state.filteredSourceItems,
220
+ value,
221
+ onChange
222
+ ]);
223
+ const handleClearAll = React$1.useCallback(() => {
220
224
  onChange([]);
221
- };
225
+ }, [onChange]);
222
226
  return /* @__PURE__ */ jsxs("div", {
223
227
  className: cn("flex h-[400px]", className),
224
228
  children: [/* @__PURE__ */ jsx("div", {
@@ -1,3 +1,4 @@
1
+ import * as React$1 from "react";
1
2
  import "zod";
2
3
  //#region src/components/features/form/utils/zod-helpers.ts
3
4
  /**
@@ -48,4 +49,35 @@ function isRequired(schema) {
48
49
  return true;
49
50
  }
50
51
  //#endregion
51
- export { getObjectShape as n, getFieldConstraints as t };
52
+ //#region src/components/features/form/hooks/use-display-touched.ts
53
+ /**
54
+ * Shared display-level touched tracking, independent of adapter-specific blur tracking.
55
+ *
56
+ * "Display-touched" controls whether a field's errors are shown in the UI.
57
+ * This is separate from the adapter's `isTouched` (which tracks focusout events).
58
+ *
59
+ * Used by both RHF and Conform adapters to provide consistent error display
60
+ * behaviour across validation modes (onChange, onBlur, onSubmit).
61
+ */
62
+ function useDisplayTouched(schema) {
63
+ const [touchedSet, setTouchedSet] = React$1.useState(/* @__PURE__ */ new Set());
64
+ const markFieldTouched = React$1.useCallback((fieldName) => {
65
+ setTouchedSet((prev) => {
66
+ if (prev.has(fieldName)) return prev;
67
+ const next = new Set(prev);
68
+ next.add(fieldName);
69
+ return next;
70
+ });
71
+ }, []);
72
+ const markAllFieldsTouched = React$1.useCallback(() => {
73
+ const allFieldNames = Object.keys(getFieldConstraints(schema));
74
+ setTouchedSet(new Set(allFieldNames));
75
+ }, [schema]);
76
+ return {
77
+ displayTouchedFields: React$1.useMemo(() => Array.from(touchedSet), [touchedSet]),
78
+ markFieldTouched,
79
+ markAllFieldsTouched
80
+ };
81
+ }
82
+ //#endregion
83
+ export { getFieldConstraints as n, getObjectShape as r, useDisplayTouched as t };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@datum-cloud/datum-ui",
3
3
  "type": "module",
4
- "version": "0.6.0-alpha.b8a44ac",
4
+ "version": "0.6.1",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "url": "https://github.com/datum-cloud/datum-ui"
@@ -204,6 +204,11 @@
204
204
  "types": "./dist/components/features/autocomplete/index.d.ts",
205
205
  "default": "./dist/autocomplete/index.mjs"
206
206
  },
207
+ "./autosearch": {
208
+ "source": "./src/components/features/autosearch/index.ts",
209
+ "types": "./dist/components/features/autosearch/index.d.ts",
210
+ "default": "./dist/autosearch/index.mjs"
211
+ },
207
212
  "./combobox": {
208
213
  "source": "./src/components/features/combobox/index.ts",
209
214
  "types": "./dist/components/features/combobox/index.d.ts",
@@ -358,7 +363,7 @@
358
363
  "@conform-to/zod": ">=1",
359
364
  "@dnd-kit/core": ">=6",
360
365
  "@dnd-kit/sortable": ">=8",
361
- "@hookform/resolvers": ">=5",
366
+ "@hookform/resolvers": ">=5.2.2",
362
367
  "@monaco-editor/react": "^4.7.0",
363
368
  "@stepperize/react": ">=4",
364
369
  "@tanstack/react-table": ">=8",
@@ -495,7 +500,7 @@
495
500
  "@radix-ui/react-tabs": "^1",
496
501
  "@radix-ui/react-tooltip": "^1",
497
502
  "@radix-ui/react-visually-hidden": "^1",
498
- "class-variance-authority": "^0.7",
503
+ "class-variance-authority": "^0.7.1",
499
504
  "clsx": "^2.1.1",
500
505
  "cmdk": "^1",
501
506
  "tailwind-merge": "^3.5.0",