@2urgseui/core 0.1.0 → 0.1.2

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/dist/index.cjs CHANGED
@@ -226,6 +226,12 @@ __export(index_exports, {
226
226
  colors: () => colors,
227
227
  containerVariants: () => containerVariants,
228
228
  dismissToast: () => dismissToast,
229
+ formFieldCanonicalizeNumericInput: () => formFieldCanonicalizeNumericInput,
230
+ formFieldFormatNumericDisplay: () => formFieldFormatNumericDisplay,
231
+ formFieldMaskExtractCanonical: () => formFieldMaskExtractCanonical,
232
+ formFieldMaskFormatDisplay: () => formFieldMaskFormatDisplay,
233
+ formFieldMaskSlotCount: () => formFieldMaskSlotCount,
234
+ formValueToAsyncSelectOption: () => formValueToAsyncSelectOption,
229
235
  getIcon: () => getIcon,
230
236
  headingVariants: () => headingVariants,
231
237
  inputGroupSelectTriggerTextAlignClass: () => inputGroupSelectTriggerTextAlignClass,
@@ -638,6 +644,15 @@ function resolveLabel(item, labelKey) {
638
644
  if (typeof labelKey === "function") return labelKey(item);
639
645
  return String(item[labelKey] ?? "");
640
646
  }
647
+ function reactNodeToLabelString(node) {
648
+ if (node == null || typeof node === "boolean") return "";
649
+ if (typeof node === "string" || typeof node === "number") return String(node);
650
+ if (Array.isArray(node)) return node.map(reactNodeToLabelString).join("");
651
+ return "";
652
+ }
653
+ function resolveStringLabel(item, labelKey) {
654
+ return reactNodeToLabelString(resolveLabel(item, labelKey));
655
+ }
641
656
  function AsyncSelectInner(props) {
642
657
  const {
643
658
  url,
@@ -653,6 +668,7 @@ function AsyncSelectInner(props) {
653
668
  disabled = false,
654
669
  error: error2 = false,
655
670
  name,
671
+ id,
656
672
  className,
657
673
  fetcher,
658
674
  dataPath = "data",
@@ -674,8 +690,8 @@ function AsyncSelectInner(props) {
674
690
  const searchInputRef = React6.useRef(null);
675
691
  const abortRef = React6.useRef(null);
676
692
  React6.useEffect(() => {
677
- const id = setTimeout(() => setDebouncedSearch(search), debounceMs);
678
- return () => clearTimeout(id);
693
+ const id2 = setTimeout(() => setDebouncedSearch(search), debounceMs);
694
+ return () => clearTimeout(id2);
679
695
  }, [search, debounceMs]);
680
696
  React6.useEffect(() => {
681
697
  setPage(1);
@@ -749,38 +765,43 @@ function AsyncSelectInner(props) {
749
765
  }
750
766
  }, [open]);
751
767
  React6.useEffect(() => {
752
- if (!value) {
768
+ const v = value?.value;
769
+ if (v == null || v === "") {
753
770
  setSelectedItem(null);
754
771
  return;
755
772
  }
756
773
  const found = items.find(
757
- (it) => String(it[valueKey]) === value
774
+ (it) => String(it[valueKey]) === v
758
775
  );
759
- if (found) setSelectedItem(found);
776
+ setSelectedItem(found ?? null);
760
777
  }, [value, items, valueKey]);
761
778
  const handleSelect = (item) => {
762
779
  const itemValue = String(item[valueKey]);
763
- if (itemValue === value && clearable) {
764
- onValueChange?.("");
780
+ if (itemValue === value?.value && clearable) {
781
+ onValueChange?.(null);
765
782
  setSelectedItem(null);
766
783
  } else {
767
- onValueChange?.(itemValue);
784
+ onValueChange?.({
785
+ value: itemValue,
786
+ label: resolveStringLabel(item, labelKey)
787
+ });
768
788
  setSelectedItem(item);
769
789
  }
770
790
  setOpen(false);
771
791
  };
772
792
  const handleClear = (e) => {
773
793
  e.stopPropagation();
774
- onValueChange?.("");
794
+ onValueChange?.(null);
775
795
  setSelectedItem(null);
776
796
  };
777
- const triggerLabel = selectedItem ? resolveLabel(selectedItem, labelKey) : null;
797
+ const triggerLabel = selectedItem ? resolveLabel(selectedItem, labelKey) : value?.label ? value.label : null;
778
798
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Popover, { open, onOpenChange: setOpen, children: [
779
- name && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { type: "hidden", name, value: value ?? "" }),
799
+ name && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { type: "hidden", name, value: value?.value ?? "" }),
780
800
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
781
801
  "button",
782
802
  {
783
803
  ref: innerRef,
804
+ id,
784
805
  type: "button",
785
806
  role: "combobox",
786
807
  "aria-expanded": open,
@@ -802,7 +823,7 @@ function AsyncSelectInner(props) {
802
823
  }
803
824
  ),
804
825
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "flex shrink-0 items-center gap-1", children: [
805
- clearable && value && !disabled && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
826
+ clearable && value?.value && !disabled && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
806
827
  "span",
807
828
  {
808
829
  role: "button",
@@ -849,7 +870,7 @@ function AsyncSelectInner(props) {
849
870
  const itemValue = String(
850
871
  item[valueKey]
851
872
  );
852
- const isSelected = itemValue === value;
873
+ const isSelected = itemValue === value?.value;
853
874
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
854
875
  "button",
855
876
  {
@@ -2740,6 +2761,106 @@ Textarea.displayName = "Textarea";
2740
2761
 
2741
2762
  // source/components/primitive/FormField/form-field.tsx
2742
2763
  var import_jsx_runtime33 = require("react/jsx-runtime");
2764
+ function stripThousands(s) {
2765
+ return s.replace(/,/g, "");
2766
+ }
2767
+ function formFieldCanonicalizeNumericInput(raw, allowDecimal) {
2768
+ const withoutCommas = stripThousands(raw);
2769
+ const pattern = allowDecimal ? /[^\d.]/g : /\D/g;
2770
+ let cleaned = withoutCommas.replace(pattern, "");
2771
+ if (!allowDecimal) return cleaned;
2772
+ const parts = cleaned.split(".");
2773
+ if (parts.length <= 1) return cleaned;
2774
+ const intPart = parts[0] ?? "";
2775
+ const frac = parts.slice(1).join("");
2776
+ return intPart + "." + frac;
2777
+ }
2778
+ function formatThousands(intPart) {
2779
+ if (!intPart) return "";
2780
+ return intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
2781
+ }
2782
+ function formFieldFormatNumericDisplay(stored, allowDecimal, useGrouping) {
2783
+ if (stored == null) return "";
2784
+ const s = String(stored);
2785
+ if (s === "") return "";
2786
+ if (!useGrouping) return s;
2787
+ if (allowDecimal && s.includes(".")) {
2788
+ const [intPart, ...rest] = s.split(".");
2789
+ const frac = rest.join("");
2790
+ return formatThousands(intPart ?? "") + "." + frac;
2791
+ }
2792
+ return formatThousands(s);
2793
+ }
2794
+ var FORM_FIELD_MASK_SLOTS = /* @__PURE__ */ new Set(["#", "h", "H", "X", "x", "*"]);
2795
+ function isFormFieldMaskSlotChar(p) {
2796
+ return FORM_FIELD_MASK_SLOTS.has(p);
2797
+ }
2798
+ function matchesMaskSlot(slot, ch) {
2799
+ switch (slot) {
2800
+ case "#":
2801
+ return /\d/.test(ch);
2802
+ case "h":
2803
+ case "H":
2804
+ return /[0-9a-fA-F]/.test(ch);
2805
+ case "X":
2806
+ case "x":
2807
+ return /[A-Za-z]/.test(ch);
2808
+ case "*":
2809
+ return /[A-Za-z0-9]/.test(ch);
2810
+ default:
2811
+ return false;
2812
+ }
2813
+ }
2814
+ function normalizeMaskSlotChar(slot, ch) {
2815
+ switch (slot) {
2816
+ case "#":
2817
+ return ch;
2818
+ case "h":
2819
+ return ch.toLowerCase();
2820
+ case "H":
2821
+ return /\d/.test(ch) ? ch : ch.toUpperCase();
2822
+ case "X":
2823
+ return ch.toUpperCase();
2824
+ case "x":
2825
+ return ch.toLowerCase();
2826
+ case "*":
2827
+ return ch;
2828
+ default:
2829
+ return ch;
2830
+ }
2831
+ }
2832
+ function formFieldMaskSlotCount(pattern) {
2833
+ return [...pattern].filter(isFormFieldMaskSlotChar).length;
2834
+ }
2835
+ function formFieldMaskExtractCanonical(raw, pattern) {
2836
+ const slots = [...pattern].filter(isFormFieldMaskSlotChar);
2837
+ if (!slots.length) return "";
2838
+ const out = [];
2839
+ let si = 0;
2840
+ for (const ch of raw) {
2841
+ if (si >= slots.length) break;
2842
+ const slot = slots[si];
2843
+ if (matchesMaskSlot(slot, ch)) {
2844
+ out.push(normalizeMaskSlotChar(slot, ch));
2845
+ si++;
2846
+ }
2847
+ }
2848
+ return out.join("");
2849
+ }
2850
+ function formFieldMaskFormatDisplay(pattern, canonical) {
2851
+ let ci = 0;
2852
+ let result = "";
2853
+ for (const p of pattern) {
2854
+ if (isFormFieldMaskSlotChar(p)) {
2855
+ if (ci >= canonical.length) break;
2856
+ result += canonical[ci];
2857
+ ci++;
2858
+ } else if (ci < canonical.length) {
2859
+ result += p;
2860
+ }
2861
+ }
2862
+ return result;
2863
+ }
2743
2864
  var VARIANTS_NEED_CONTROL = [
2744
2865
  "checkbox",
2745
2866
  "switch",
@@ -2778,6 +2899,8 @@ function FormField({
2778
2899
  richTextProps,
2779
2900
  dropzoneProps,
2780
2901
  asyncSelectProps,
2902
+ numericInput,
2903
+ maskInput,
2781
2904
  className,
2782
2905
  renderInput
2783
2906
  }) {
@@ -2785,6 +2908,39 @@ function FormField({
2785
2908
  const inputId = `field-${generatedId}`;
2786
2909
  const descriptionId = description ? `${inputId}-description` : void 0;
2787
2910
  const externalError = error2 ? String(error2) : void 0;
2911
+ if (maskInput && numericInput) {
2912
+ throw new Error(
2913
+ "FormField does not support `maskInput` and `numericInput` together; use only one."
2914
+ );
2915
+ }
2916
+ if (maskInput) {
2917
+ if (!control) {
2918
+ throw new Error(
2919
+ "FormField `maskInput` requires the `control` prop (Controller). Use `control`, not `register`, for masked fields."
2920
+ );
2921
+ }
2922
+ if (variant !== "input") {
2923
+ throw new Error('FormField `maskInput` is only supported with the default `variant="input"`.');
2924
+ }
2925
+ if (!maskInput.pattern?.trim()) {
2926
+ throw new Error("FormField `maskInput.pattern` must be a non-empty string.");
2927
+ }
2928
+ if (formFieldMaskSlotCount(maskInput.pattern) === 0) {
2929
+ throw new Error(
2930
+ "FormField `maskInput.pattern` must include at least one slot token: `#`, `h`, `H`, `X`, `x`, or `*`."
2931
+ );
2932
+ }
2933
+ }
2934
+ if (numericInput) {
2935
+ if (!control) {
2936
+ throw new Error(
2937
+ "FormField `numericInput` requires the `control` prop (Controller). Use `control`, not `register`, for formatted numeric fields."
2938
+ );
2939
+ }
2940
+ if (variant !== "input") {
2941
+ throw new Error('FormField `numericInput` is only supported with the default `variant="input"`.');
2942
+ }
2943
+ }
2788
2944
  if (!control && VARIANTS_NEED_CONTROL.includes(variant)) {
2789
2945
  throw new Error(
2790
2946
  `FormField variant "${variant}" requires the control prop (React Hook Form Controller).`
@@ -2814,7 +2970,7 @@ function FormField({
2814
2970
  };
2815
2971
  const controlNode = renderInput ? renderInput({
2816
2972
  ...sharedInputShell,
2817
- value: field.value ?? "",
2973
+ value: variant === "async-select" ? formValueToAsyncSelectOption(field.value) ?? null : field.value ?? "",
2818
2974
  onChange: field.onChange,
2819
2975
  onBlur: field.onBlur,
2820
2976
  ref: field.ref
@@ -2838,7 +2994,9 @@ function FormField({
2838
2994
  otpProps,
2839
2995
  richTextProps,
2840
2996
  dropzoneProps,
2841
- asyncSelectProps
2997
+ asyncSelectProps,
2998
+ numericInput: renderInput ? void 0 : numericInput,
2999
+ maskInput: renderInput ? void 0 : maskInput
2842
3000
  }
2843
3001
  );
2844
3002
  const labelBlock = isCheckboxInline || !hasFieldLabel2 ? null : /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
@@ -2958,6 +3116,17 @@ function FormField({
2958
3116
  externalError && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(Text, { id: `${inputId}-error`, size: "sm", tone: "destructive", children: externalError })
2959
3117
  ] });
2960
3118
  }
3119
+ function formValueToAsyncSelectOption(v) {
3120
+ if (v == null || v === "") return void 0;
3121
+ if (typeof v === "object" && v !== null && "value" in v) {
3122
+ const o = v;
3123
+ return {
3124
+ value: String(o.value ?? ""),
3125
+ label: o.label != null ? String(o.label) : ""
3126
+ };
3127
+ }
3128
+ return { value: String(v), label: "" };
3129
+ }
2961
3130
  function FormFieldVariantControl({
2962
3131
  variant,
2963
3132
  inputId,
@@ -2976,7 +3145,9 @@ function FormFieldVariantControl({
2976
3145
  otpProps,
2977
3146
  richTextProps,
2978
3147
  dropzoneProps,
2979
- asyncSelectProps
3148
+ asyncSelectProps,
3149
+ numericInput,
3150
+ maskInput
2980
3151
  }) {
2981
3152
  switch (variant) {
2982
3153
  case "textarea":
@@ -3082,7 +3253,8 @@ function FormFieldVariantControl({
3082
3253
  AsyncSelect,
3083
3254
  {
3084
3255
  ...asyncSelectProps,
3085
- value: field.value == null || field.value === "" ? void 0 : String(field.value),
3256
+ id: inputId,
3257
+ value: formValueToAsyncSelectOption(field.value),
3086
3258
  onValueChange: field.onChange,
3087
3259
  disabled: field.disabled,
3088
3260
  error: hasError,
@@ -3213,22 +3385,100 @@ function FormFieldVariantControl({
3213
3385
  `${String(field.name)}-${field.value === null ? "cleared" : typeof FileList !== "undefined" && field.value instanceof FileList ? field.value.length : "open"}`
3214
3386
  ) });
3215
3387
  case "input":
3216
- default:
3388
+ default: {
3389
+ const {
3390
+ onChange: inputOnChangeFromProps,
3391
+ value: _omitValueFromInputProps,
3392
+ type: inputTypeProp,
3393
+ inputMode: inputModeProp,
3394
+ ...restInputProps
3395
+ } = inputProps ?? {};
3396
+ if (maskInput) {
3397
+ const pattern = maskInput.pattern;
3398
+ const rawStored = field.value == null || field.value === "" ? "" : String(field.value);
3399
+ const displayValue = formFieldMaskFormatDisplay(pattern, rawStored);
3400
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "w-full min-w-0", children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
3401
+ Input,
3402
+ {
3403
+ ...restInputProps,
3404
+ id: inputId,
3405
+ name: field.name,
3406
+ "aria-describedby": describedBy,
3407
+ error: hasError,
3408
+ type: "text",
3409
+ inputMode: maskInput.inputMode ?? "text",
3410
+ maxLength: pattern.length,
3411
+ autoComplete: "off",
3412
+ spellCheck: false,
3413
+ value: displayValue,
3414
+ onChange: (e) => {
3415
+ const canonical = formFieldMaskExtractCanonical(
3416
+ e.target.value,
3417
+ pattern
3418
+ );
3419
+ field.onChange(canonical);
3420
+ },
3421
+ onBlur: field.onBlur,
3422
+ ref: field.ref,
3423
+ disabled: field.disabled
3424
+ }
3425
+ ) });
3426
+ }
3427
+ if (numericInput) {
3428
+ const allowDecimal = numericInput.allowDecimal ?? false;
3429
+ const useGrouping = numericInput.useGrouping ?? true;
3430
+ const rawStored = field.value == null || field.value === "" ? "" : String(field.value);
3431
+ const displayValue = formFieldFormatNumericDisplay(
3432
+ rawStored,
3433
+ allowDecimal,
3434
+ useGrouping
3435
+ );
3436
+ const defaultInputMode = allowDecimal ? "decimal" : "numeric";
3437
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "w-full min-w-0", children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
3438
+ Input,
3439
+ {
3440
+ ...restInputProps,
3441
+ id: inputId,
3442
+ name: field.name,
3443
+ "aria-describedby": describedBy,
3444
+ error: hasError,
3445
+ type: "text",
3446
+ inputMode: numericInput.inputMode ?? defaultInputMode,
3447
+ value: displayValue,
3448
+ onChange: (e) => {
3449
+ const canonical = formFieldCanonicalizeNumericInput(
3450
+ e.target.value,
3451
+ allowDecimal
3452
+ );
3453
+ field.onChange(canonical);
3454
+ },
3455
+ onBlur: field.onBlur,
3456
+ ref: field.ref,
3457
+ disabled: field.disabled
3458
+ }
3459
+ ) });
3460
+ }
3217
3461
  return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "w-full min-w-0", children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
3218
3462
  Input,
3219
3463
  {
3220
- ...inputProps,
3464
+ ...restInputProps,
3221
3465
  id: inputId,
3222
3466
  name: field.name,
3223
3467
  "aria-describedby": describedBy,
3224
3468
  error: hasError,
3469
+ type: inputTypeProp,
3470
+ inputMode: inputModeProp,
3225
3471
  value: field.value ?? "",
3226
- onChange: field.onChange,
3472
+ onChange: (e) => {
3473
+ inputOnChangeFromProps?.(e);
3474
+ field.onChange(e);
3475
+ },
3227
3476
  onBlur: field.onBlur,
3228
3477
  ref: field.ref,
3229
3478
  disabled: field.disabled
3230
3479
  }
3231
3480
  ) });
3481
+ }
3232
3482
  }
3233
3483
  }
3234
3484
 
@@ -5455,6 +5705,12 @@ var typography = {
5455
5705
  colors,
5456
5706
  containerVariants,
5457
5707
  dismissToast,
5708
+ formFieldCanonicalizeNumericInput,
5709
+ formFieldFormatNumericDisplay,
5710
+ formFieldMaskExtractCanonical,
5711
+ formFieldMaskFormatDisplay,
5712
+ formFieldMaskSlotCount,
5713
+ formValueToAsyncSelectOption,
5458
5714
  getIcon,
5459
5715
  headingVariants,
5460
5716
  inputGroupSelectTriggerTextAlignClass,
package/dist/index.d.cts CHANGED
@@ -64,6 +64,11 @@ declare const AccordionItem: React.ForwardRefExoticComponent<Omit<AccordionPrimi
64
64
  declare const AccordionTrigger: React.ForwardRefExoticComponent<Omit<AccordionPrimitive.AccordionTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>>;
65
65
  declare const AccordionContent: React.ForwardRefExoticComponent<Omit<AccordionPrimitive.AccordionContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
66
66
 
67
+ /** Controlled value / `onValueChange` payload (same shape for prepopulation and after select). */
68
+ type AsyncSelectOption = {
69
+ value: string;
70
+ label: string;
71
+ };
67
72
  interface AsyncSelectPage<T = Record<string, unknown>> {
68
73
  data: T[];
69
74
  hasMore: boolean;
@@ -80,10 +85,10 @@ interface AsyncSelectProps<T = Record<string, unknown>> {
80
85
  valueKey: keyof T & string;
81
86
  /** Key (or render fn) for the display label. */
82
87
  labelKey: (keyof T & string) | ((item: T) => React.ReactNode);
83
- /** Controlled value. */
84
- value?: string;
85
- /** Change handler receives the selected item's value key, or `""` when cleared. */
86
- onValueChange?: (value: string) => void;
88
+ /** Controlled value (`value` + `label`, e.g. from the server). */
89
+ value?: AsyncSelectOption | null;
90
+ /** Called with `{ value, label }` for a selection, or `null` when cleared. */
91
+ onValueChange?: (option: AsyncSelectOption | null) => void;
87
92
  /** Placeholder shown in the trigger when nothing is selected. */
88
93
  placeholder?: string;
89
94
  /** Placeholder for the search input inside the dropdown. */
@@ -97,6 +102,8 @@ interface AsyncSelectProps<T = Record<string, unknown>> {
97
102
  disabled?: boolean;
98
103
  error?: boolean;
99
104
  name?: string;
105
+ /** Sets `id` on the combobox trigger (use with `Label htmlFor`). */
106
+ id?: string;
100
107
  className?: string;
101
108
  /** Custom fetcher. Defaults to a JSON fetch that appends `?search=&page=` to `url`. */
102
109
  fetcher?: AsyncSelectFetcher<T>;
@@ -124,8 +131,8 @@ interface AsyncSelectProps<T = Record<string, unknown>> {
124
131
  * url="/api/employees"
125
132
  * valueKey="id"
126
133
  * labelKey="fullName"
127
- * value={employeeId}
128
- * onValueChange={setEmployeeId}
134
+ * value={{ value: employee.id, label: employee.fullName }}
135
+ * onValueChange={setEmployee}
129
136
  * placeholder="Choose an employee…"
130
137
  * />
131
138
  * ```
@@ -476,7 +483,49 @@ type FormFieldOtpConfig = {
476
483
  containerClassName?: string;
477
484
  } & Omit<React.ComponentPropsWithoutRef<typeof InputOTP>, "maxLength" | "value" | "onChange" | "containerClassName" | "render" | "invalid">;
478
485
  type FormFieldDropzoneConfig = Omit<FileDropzoneProps, "id" | "name" | "onChange" | "onBlur" | "ref" | "disabled" | "error" | "value">;
479
- type FormFieldAsyncSelectConfig = Omit<AsyncSelectProps, "value" | "onValueChange" | "disabled" | "error" | "name">;
486
+ /**
487
+ * Props for `AsyncSelect` when used via `FormField` (`variant="async-select"`).
488
+ * The controlled field value is `{ value, label } | null | undefined` (legacy plain `string` is still read as id-only).
489
+ */
490
+ type FormFieldAsyncSelectConfig = Omit<AsyncSelectProps, "value" | "onValueChange" | "disabled" | "error" | "name" | "id">;
491
+ /**
492
+ * Form state holds a **canonical string** (digits only, or digits + a single `.` + fraction when `allowDecimal`).
493
+ * The input shows **grouped** thousands when `useGrouping` is true. Use with `control` and default `input` variant only.
494
+ */
495
+ type FormFieldNumericInputConfig = {
496
+ /** Allow a single decimal separator in the stored value. @default false */
497
+ allowDecimal?: boolean;
498
+ /** Show `,` thousands separators in the input while the stored value has none. @default true */
499
+ useGrouping?: boolean;
500
+ /** Passed to `<input inputMode>`. Defaults to `"decimal"` if `allowDecimal`, otherwise `"numeric"`. */
501
+ inputMode?: React.HTMLAttributes<HTMLInputElement>["inputMode"];
502
+ };
503
+ /** Raw input → canonical string for form state (no commas, optional single dot). */
504
+ declare function formFieldCanonicalizeNumericInput(raw: string, allowDecimal: boolean): string;
505
+ /** Canonical stored string → display string with optional grouping. */
506
+ declare function formFieldFormatNumericDisplay(stored: string, allowDecimal: boolean, useGrouping: boolean): string;
507
+ /** Number of editable slots in `pattern` (each `#`, `h`, `H`, `X`, `x`, or `*`). */
508
+ declare function formFieldMaskSlotCount(pattern: string): number;
509
+ /**
510
+ * Reads `raw` left-to-right and fills slots in order (ignores typed/pasted literals such as `-`).
511
+ * Form state should hold this canonical string (no separators).
512
+ */
513
+ declare function formFieldMaskExtractCanonical(raw: string, pattern: string): string;
514
+ /** Inserts pattern literals between filled slots; omits trailing literals when incomplete. */
515
+ declare function formFieldMaskFormatDisplay(pattern: string, canonical: string): string;
516
+ /**
517
+ * Fixed mask for `control` + default `input`. Form state = **slot characters only** (no `-` etc.).
518
+ *
519
+ * **Tokens:** `#` digit · `h` hex (stored lower) · `H` hex (stored upper for a–f) · `X` letter upper · `x` letter lower · `*` alphanumeric.
520
+ * Any other character in `pattern` is a **literal** shown in that position (e.g. `-`).
521
+ *
522
+ * Example: `XXX-xxx-xxx` (letters). Hex / UUID-style: `hhhhhhhh-hhhh-hhhh-hhhh-hhhh-hhhh-hhhh-hhhh` (use `h`, not `x`, for hex digits).
523
+ */
524
+ type FormFieldMaskInputConfig = {
525
+ pattern: string;
526
+ /** Passed to `<input inputMode>`. @default "text" */
527
+ inputMode?: React.HTMLAttributes<HTMLInputElement>["inputMode"];
528
+ };
480
529
  interface FormFieldProps<TFieldValues extends FieldValues> {
481
530
  name: Path<TFieldValues>;
482
531
  /** When omitted, no visible label is rendered; use `aria-label` on controls or descriptions for a11y. */
@@ -498,16 +547,31 @@ interface FormFieldProps<TFieldValues extends FieldValues> {
498
547
  otpProps?: FormFieldOtpConfig;
499
548
  richTextProps?: Omit<RichTextEditorProps, "value" | "onChange" | "disabled">;
500
549
  dropzoneProps?: FormFieldDropzoneConfig;
550
+ /** With `variant="async-select"`: URL fetch config; form state is `AsyncSelectOption | null` (see `FormFieldAsyncSelectConfig`). */
501
551
  asyncSelectProps?: FormFieldAsyncSelectConfig;
552
+ /**
553
+ * When set with `control` and default `input` variant (and no `renderInput`), formats integers/decimals in the field.
554
+ * Ignored when using `renderInput` (build a custom input there instead).
555
+ */
556
+ numericInput?: FormFieldNumericInputConfig;
557
+ /**
558
+ * Fixed character mask (`pattern` with slot tokens + literals). Mutually exclusive with `numericInput`.
559
+ * Ignored when using `renderInput`.
560
+ */
561
+ maskInput?: FormFieldMaskInputConfig;
502
562
  className?: string;
503
- renderInput?: (props: FormFieldRenderProps & {
563
+ renderInput?: (props: Omit<FormFieldRenderProps, "value"> & {
504
564
  id: string;
505
565
  name: string;
506
566
  "aria-describedby"?: string;
507
567
  error?: boolean;
568
+ /** With `variant="async-select"`, this is `AsyncSelectOption | null`; otherwise the field primitive value. */
569
+ value?: InputProps["value"] | AsyncSelectOption | null;
508
570
  }) => React.ReactNode;
509
571
  }
510
- declare function FormField<TFieldValues extends FieldValues>({ name, label, register, control, rules, description, required, error, variant, inputProps, textareaProps, checkboxProps, switchProps, selectProps, radioProps, otpProps, richTextProps, dropzoneProps, asyncSelectProps, className, renderInput, }: FormFieldProps<TFieldValues>): react_jsx_runtime.JSX.Element;
572
+ declare function FormField<TFieldValues extends FieldValues>({ name, label, register, control, rules, description, required, error, variant, inputProps, textareaProps, checkboxProps, switchProps, selectProps, radioProps, otpProps, richTextProps, dropzoneProps, asyncSelectProps, numericInput, maskInput, className, renderInput, }: FormFieldProps<TFieldValues>): react_jsx_runtime.JSX.Element;
573
+ /** Maps react-hook-form field values to `AsyncSelect`’s `value` prop (supports `{ value, label }`, legacy `string`, `null`/`undefined`). */
574
+ declare function formValueToAsyncSelectOption(v: unknown): AsyncSelectOption | undefined;
511
575
 
512
576
  /**
513
577
  * Heading levels map to the correct HTML element and default size/weight from
@@ -876,4 +940,4 @@ declare namespace Toaster {
876
940
  var displayName: string;
877
941
  }
878
942
 
879
- export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, type AlertProps, AlertTitle, AppShell, type AppShellProps, AspectRatio, type AspectRatioProps, AsyncSelect, type AsyncSelectFetcher, type AsyncSelectPage, type AsyncSelectProps, Avatar, AvatarFallback, AvatarImage, type AvatarProps, Badge, type BadgeProps, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, type ButtonProps, Calendar, CalendarDayButton, Caption, type CaptionProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, type CheckboxProps, Collapsible, CollapsibleContent, CollapsibleTrigger, Container, type ContainerProps, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, FileDropzone, type FileDropzoneProps, FormField, type FormFieldAsyncSelectConfig, type FormFieldDropzoneConfig, type FormFieldOtpConfig, type FormFieldProps, type FormFieldRadioConfig, type FormFieldRadioOption, type FormFieldSelectConfig, type FormFieldSelectItem, type FormFieldVariant, Heading, type HeadingProps, Icon, type IconComponent, type IconComponentProps, type IconProps, Input, InputGroup, InputGroupIcon, InputGroupInput, type InputGroupInputProps, type InputGroupProps, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, type InputProps, Label, type LabelProps, Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, Popover, PopoverAnchor, PopoverContent, type PopoverContentProps, PopoverTrigger, Progress, type ProgressProps, RadioGroup, RadioGroupItem, RichHtml, type RichHtmlProps, RichTextEditor, type RichTextEditorProps, ScrollArea, ScrollBar, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, type SeparatorProps, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarSeparator, SidebarTrigger, Skeleton, type SkeletonProps, Slider, type SliderProps, Switch, type SwitchProps, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Text, type TextProps, Textarea, type TextareaProps, Toast, ToastAction, ToastClose, ToastDescription, type ToastPayload, type ToastProps, ToastProvider, type ToastRecord, ToastTitle, ToastViewport, Toaster, type ToasterProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, badgeVariants, buttonVariants, captionVariants, checkboxVariants, containerVariants, dismissToast, getIcon, headingVariants, inputGroupSelectTriggerTextAlignClass, inputVariants, labelVariants, registerIcons, separatorVariants, sidebarMenuButtonVariants, skeletonVariants, switchRootVariants, switchThumbVariants, textVariants, toast, toastVariants, useSidebar, useToast };
943
+ export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, type AlertProps, AlertTitle, AppShell, type AppShellProps, AspectRatio, type AspectRatioProps, AsyncSelect, type AsyncSelectFetcher, type AsyncSelectOption, type AsyncSelectPage, type AsyncSelectProps, Avatar, AvatarFallback, AvatarImage, type AvatarProps, Badge, type BadgeProps, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, type ButtonProps, Calendar, CalendarDayButton, Caption, type CaptionProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, type CheckboxProps, Collapsible, CollapsibleContent, CollapsibleTrigger, Container, type ContainerProps, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, FileDropzone, type FileDropzoneProps, FormField, type FormFieldAsyncSelectConfig, type FormFieldDropzoneConfig, type FormFieldMaskInputConfig, type FormFieldNumericInputConfig, type FormFieldOtpConfig, type FormFieldProps, type FormFieldRadioConfig, type FormFieldRadioOption, type FormFieldSelectConfig, type FormFieldSelectItem, type FormFieldVariant, Heading, type HeadingProps, Icon, type IconComponent, type IconComponentProps, type IconProps, Input, InputGroup, InputGroupIcon, InputGroupInput, type InputGroupInputProps, type InputGroupProps, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, type InputProps, Label, type LabelProps, Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, Popover, PopoverAnchor, PopoverContent, type PopoverContentProps, PopoverTrigger, Progress, type ProgressProps, RadioGroup, RadioGroupItem, RichHtml, type RichHtmlProps, RichTextEditor, type RichTextEditorProps, ScrollArea, ScrollBar, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, type SeparatorProps, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarSeparator, SidebarTrigger, Skeleton, type SkeletonProps, Slider, type SliderProps, Switch, type SwitchProps, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Text, type TextProps, Textarea, type TextareaProps, Toast, ToastAction, ToastClose, ToastDescription, type ToastPayload, type ToastProps, ToastProvider, type ToastRecord, ToastTitle, ToastViewport, Toaster, type ToasterProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, badgeVariants, buttonVariants, captionVariants, checkboxVariants, containerVariants, dismissToast, formFieldCanonicalizeNumericInput, formFieldFormatNumericDisplay, formFieldMaskExtractCanonical, formFieldMaskFormatDisplay, formFieldMaskSlotCount, formValueToAsyncSelectOption, getIcon, headingVariants, inputGroupSelectTriggerTextAlignClass, inputVariants, labelVariants, registerIcons, separatorVariants, sidebarMenuButtonVariants, skeletonVariants, switchRootVariants, switchThumbVariants, textVariants, toast, toastVariants, useSidebar, useToast };
package/dist/index.d.ts CHANGED
@@ -64,6 +64,11 @@ declare const AccordionItem: React.ForwardRefExoticComponent<Omit<AccordionPrimi
64
64
  declare const AccordionTrigger: React.ForwardRefExoticComponent<Omit<AccordionPrimitive.AccordionTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>>;
65
65
  declare const AccordionContent: React.ForwardRefExoticComponent<Omit<AccordionPrimitive.AccordionContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
66
66
 
67
+ /** Controlled value / `onValueChange` payload (same shape for prepopulation and after select). */
68
+ type AsyncSelectOption = {
69
+ value: string;
70
+ label: string;
71
+ };
67
72
  interface AsyncSelectPage<T = Record<string, unknown>> {
68
73
  data: T[];
69
74
  hasMore: boolean;
@@ -80,10 +85,10 @@ interface AsyncSelectProps<T = Record<string, unknown>> {
80
85
  valueKey: keyof T & string;
81
86
  /** Key (or render fn) for the display label. */
82
87
  labelKey: (keyof T & string) | ((item: T) => React.ReactNode);
83
- /** Controlled value. */
84
- value?: string;
85
- /** Change handler receives the selected item's value key, or `""` when cleared. */
86
- onValueChange?: (value: string) => void;
88
+ /** Controlled value (`value` + `label`, e.g. from the server). */
89
+ value?: AsyncSelectOption | null;
90
+ /** Called with `{ value, label }` for a selection, or `null` when cleared. */
91
+ onValueChange?: (option: AsyncSelectOption | null) => void;
87
92
  /** Placeholder shown in the trigger when nothing is selected. */
88
93
  placeholder?: string;
89
94
  /** Placeholder for the search input inside the dropdown. */
@@ -97,6 +102,8 @@ interface AsyncSelectProps<T = Record<string, unknown>> {
97
102
  disabled?: boolean;
98
103
  error?: boolean;
99
104
  name?: string;
105
+ /** Sets `id` on the combobox trigger (use with `Label htmlFor`). */
106
+ id?: string;
100
107
  className?: string;
101
108
  /** Custom fetcher. Defaults to a JSON fetch that appends `?search=&page=` to `url`. */
102
109
  fetcher?: AsyncSelectFetcher<T>;
@@ -124,8 +131,8 @@ interface AsyncSelectProps<T = Record<string, unknown>> {
124
131
  * url="/api/employees"
125
132
  * valueKey="id"
126
133
  * labelKey="fullName"
127
- * value={employeeId}
128
- * onValueChange={setEmployeeId}
134
+ * value={{ value: employee.id, label: employee.fullName }}
135
+ * onValueChange={setEmployee}
129
136
  * placeholder="Choose an employee…"
130
137
  * />
131
138
  * ```
@@ -476,7 +483,49 @@ type FormFieldOtpConfig = {
476
483
  containerClassName?: string;
477
484
  } & Omit<React.ComponentPropsWithoutRef<typeof InputOTP>, "maxLength" | "value" | "onChange" | "containerClassName" | "render" | "invalid">;
478
485
  type FormFieldDropzoneConfig = Omit<FileDropzoneProps, "id" | "name" | "onChange" | "onBlur" | "ref" | "disabled" | "error" | "value">;
479
- type FormFieldAsyncSelectConfig = Omit<AsyncSelectProps, "value" | "onValueChange" | "disabled" | "error" | "name">;
486
+ /**
487
+ * Props for `AsyncSelect` when used via `FormField` (`variant="async-select"`).
488
+ * The controlled field value is `{ value, label } | null | undefined` (legacy plain `string` is still read as id-only).
489
+ */
490
+ type FormFieldAsyncSelectConfig = Omit<AsyncSelectProps, "value" | "onValueChange" | "disabled" | "error" | "name" | "id">;
491
+ /**
492
+ * Form state holds a **canonical string** (digits only, or digits + a single `.` + fraction when `allowDecimal`).
493
+ * The input shows **grouped** thousands when `useGrouping` is true. Use with `control` and default `input` variant only.
494
+ */
495
+ type FormFieldNumericInputConfig = {
496
+ /** Allow a single decimal separator in the stored value. @default false */
497
+ allowDecimal?: boolean;
498
+ /** Show `,` thousands separators in the input while the stored value has none. @default true */
499
+ useGrouping?: boolean;
500
+ /** Passed to `<input inputMode>`. Defaults to `"decimal"` if `allowDecimal`, otherwise `"numeric"`. */
501
+ inputMode?: React.HTMLAttributes<HTMLInputElement>["inputMode"];
502
+ };
503
+ /** Raw input → canonical string for form state (no commas, optional single dot). */
504
+ declare function formFieldCanonicalizeNumericInput(raw: string, allowDecimal: boolean): string;
505
+ /** Canonical stored string → display string with optional grouping. */
506
+ declare function formFieldFormatNumericDisplay(stored: string, allowDecimal: boolean, useGrouping: boolean): string;
507
+ /** Number of editable slots in `pattern` (each `#`, `h`, `H`, `X`, `x`, or `*`). */
508
+ declare function formFieldMaskSlotCount(pattern: string): number;
509
+ /**
510
+ * Reads `raw` left-to-right and fills slots in order (ignores typed/pasted literals such as `-`).
511
+ * Form state should hold this canonical string (no separators).
512
+ */
513
+ declare function formFieldMaskExtractCanonical(raw: string, pattern: string): string;
514
+ /** Inserts pattern literals between filled slots; omits trailing literals when incomplete. */
515
+ declare function formFieldMaskFormatDisplay(pattern: string, canonical: string): string;
516
+ /**
517
+ * Fixed mask for `control` + default `input`. Form state = **slot characters only** (no `-` etc.).
518
+ *
519
+ * **Tokens:** `#` digit · `h` hex (stored lower) · `H` hex (stored upper for a–f) · `X` letter upper · `x` letter lower · `*` alphanumeric.
520
+ * Any other character in `pattern` is a **literal** shown in that position (e.g. `-`).
521
+ *
522
+ * Example: `XXX-xxx-xxx` (letters). Hex / UUID-style: `hhhhhhhh-hhhh-hhhh-hhhh-hhhh-hhhh-hhhh-hhhh` (use `h`, not `x`, for hex digits).
523
+ */
524
+ type FormFieldMaskInputConfig = {
525
+ pattern: string;
526
+ /** Passed to `<input inputMode>`. @default "text" */
527
+ inputMode?: React.HTMLAttributes<HTMLInputElement>["inputMode"];
528
+ };
480
529
  interface FormFieldProps<TFieldValues extends FieldValues> {
481
530
  name: Path<TFieldValues>;
482
531
  /** When omitted, no visible label is rendered; use `aria-label` on controls or descriptions for a11y. */
@@ -498,16 +547,31 @@ interface FormFieldProps<TFieldValues extends FieldValues> {
498
547
  otpProps?: FormFieldOtpConfig;
499
548
  richTextProps?: Omit<RichTextEditorProps, "value" | "onChange" | "disabled">;
500
549
  dropzoneProps?: FormFieldDropzoneConfig;
550
+ /** With `variant="async-select"`: URL fetch config; form state is `AsyncSelectOption | null` (see `FormFieldAsyncSelectConfig`). */
501
551
  asyncSelectProps?: FormFieldAsyncSelectConfig;
552
+ /**
553
+ * When set with `control` and default `input` variant (and no `renderInput`), formats integers/decimals in the field.
554
+ * Ignored when using `renderInput` (build a custom input there instead).
555
+ */
556
+ numericInput?: FormFieldNumericInputConfig;
557
+ /**
558
+ * Fixed character mask (`pattern` with slot tokens + literals). Mutually exclusive with `numericInput`.
559
+ * Ignored when using `renderInput`.
560
+ */
561
+ maskInput?: FormFieldMaskInputConfig;
502
562
  className?: string;
503
- renderInput?: (props: FormFieldRenderProps & {
563
+ renderInput?: (props: Omit<FormFieldRenderProps, "value"> & {
504
564
  id: string;
505
565
  name: string;
506
566
  "aria-describedby"?: string;
507
567
  error?: boolean;
568
+ /** With `variant="async-select"`, this is `AsyncSelectOption | null`; otherwise the field primitive value. */
569
+ value?: InputProps["value"] | AsyncSelectOption | null;
508
570
  }) => React.ReactNode;
509
571
  }
510
- declare function FormField<TFieldValues extends FieldValues>({ name, label, register, control, rules, description, required, error, variant, inputProps, textareaProps, checkboxProps, switchProps, selectProps, radioProps, otpProps, richTextProps, dropzoneProps, asyncSelectProps, className, renderInput, }: FormFieldProps<TFieldValues>): react_jsx_runtime.JSX.Element;
572
+ declare function FormField<TFieldValues extends FieldValues>({ name, label, register, control, rules, description, required, error, variant, inputProps, textareaProps, checkboxProps, switchProps, selectProps, radioProps, otpProps, richTextProps, dropzoneProps, asyncSelectProps, numericInput, maskInput, className, renderInput, }: FormFieldProps<TFieldValues>): react_jsx_runtime.JSX.Element;
573
+ /** Maps react-hook-form field values to `AsyncSelect`’s `value` prop (supports `{ value, label }`, legacy `string`, `null`/`undefined`). */
574
+ declare function formValueToAsyncSelectOption(v: unknown): AsyncSelectOption | undefined;
511
575
 
512
576
  /**
513
577
  * Heading levels map to the correct HTML element and default size/weight from
@@ -876,4 +940,4 @@ declare namespace Toaster {
876
940
  var displayName: string;
877
941
  }
878
942
 
879
- export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, type AlertProps, AlertTitle, AppShell, type AppShellProps, AspectRatio, type AspectRatioProps, AsyncSelect, type AsyncSelectFetcher, type AsyncSelectPage, type AsyncSelectProps, Avatar, AvatarFallback, AvatarImage, type AvatarProps, Badge, type BadgeProps, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, type ButtonProps, Calendar, CalendarDayButton, Caption, type CaptionProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, type CheckboxProps, Collapsible, CollapsibleContent, CollapsibleTrigger, Container, type ContainerProps, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, FileDropzone, type FileDropzoneProps, FormField, type FormFieldAsyncSelectConfig, type FormFieldDropzoneConfig, type FormFieldOtpConfig, type FormFieldProps, type FormFieldRadioConfig, type FormFieldRadioOption, type FormFieldSelectConfig, type FormFieldSelectItem, type FormFieldVariant, Heading, type HeadingProps, Icon, type IconComponent, type IconComponentProps, type IconProps, Input, InputGroup, InputGroupIcon, InputGroupInput, type InputGroupInputProps, type InputGroupProps, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, type InputProps, Label, type LabelProps, Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, Popover, PopoverAnchor, PopoverContent, type PopoverContentProps, PopoverTrigger, Progress, type ProgressProps, RadioGroup, RadioGroupItem, RichHtml, type RichHtmlProps, RichTextEditor, type RichTextEditorProps, ScrollArea, ScrollBar, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, type SeparatorProps, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarSeparator, SidebarTrigger, Skeleton, type SkeletonProps, Slider, type SliderProps, Switch, type SwitchProps, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Text, type TextProps, Textarea, type TextareaProps, Toast, ToastAction, ToastClose, ToastDescription, type ToastPayload, type ToastProps, ToastProvider, type ToastRecord, ToastTitle, ToastViewport, Toaster, type ToasterProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, badgeVariants, buttonVariants, captionVariants, checkboxVariants, containerVariants, dismissToast, getIcon, headingVariants, inputGroupSelectTriggerTextAlignClass, inputVariants, labelVariants, registerIcons, separatorVariants, sidebarMenuButtonVariants, skeletonVariants, switchRootVariants, switchThumbVariants, textVariants, toast, toastVariants, useSidebar, useToast };
943
+ export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, type AlertProps, AlertTitle, AppShell, type AppShellProps, AspectRatio, type AspectRatioProps, AsyncSelect, type AsyncSelectFetcher, type AsyncSelectOption, type AsyncSelectPage, type AsyncSelectProps, Avatar, AvatarFallback, AvatarImage, type AvatarProps, Badge, type BadgeProps, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, type ButtonProps, Calendar, CalendarDayButton, Caption, type CaptionProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, type CheckboxProps, Collapsible, CollapsibleContent, CollapsibleTrigger, Container, type ContainerProps, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, FileDropzone, type FileDropzoneProps, FormField, type FormFieldAsyncSelectConfig, type FormFieldDropzoneConfig, type FormFieldMaskInputConfig, type FormFieldNumericInputConfig, type FormFieldOtpConfig, type FormFieldProps, type FormFieldRadioConfig, type FormFieldRadioOption, type FormFieldSelectConfig, type FormFieldSelectItem, type FormFieldVariant, Heading, type HeadingProps, Icon, type IconComponent, type IconComponentProps, type IconProps, Input, InputGroup, InputGroupIcon, InputGroupInput, type InputGroupInputProps, type InputGroupProps, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, type InputProps, Label, type LabelProps, Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, Popover, PopoverAnchor, PopoverContent, type PopoverContentProps, PopoverTrigger, Progress, type ProgressProps, RadioGroup, RadioGroupItem, RichHtml, type RichHtmlProps, RichTextEditor, type RichTextEditorProps, ScrollArea, ScrollBar, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, type SeparatorProps, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarSeparator, SidebarTrigger, Skeleton, type SkeletonProps, Slider, type SliderProps, Switch, type SwitchProps, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Text, type TextProps, Textarea, type TextareaProps, Toast, ToastAction, ToastClose, ToastDescription, type ToastPayload, type ToastProps, ToastProvider, type ToastRecord, ToastTitle, ToastViewport, Toaster, type ToasterProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, badgeVariants, buttonVariants, captionVariants, checkboxVariants, containerVariants, dismissToast, formFieldCanonicalizeNumericInput, formFieldFormatNumericDisplay, formFieldMaskExtractCanonical, formFieldMaskFormatDisplay, formFieldMaskSlotCount, formValueToAsyncSelectOption, getIcon, headingVariants, inputGroupSelectTriggerTextAlignClass, inputVariants, labelVariants, registerIcons, separatorVariants, sidebarMenuButtonVariants, skeletonVariants, switchRootVariants, switchThumbVariants, textVariants, toast, toastVariants, useSidebar, useToast };
package/dist/index.js CHANGED
@@ -398,6 +398,15 @@ function resolveLabel(item, labelKey) {
398
398
  if (typeof labelKey === "function") return labelKey(item);
399
399
  return String(item[labelKey] ?? "");
400
400
  }
401
+ function reactNodeToLabelString(node) {
402
+ if (node == null || typeof node === "boolean") return "";
403
+ if (typeof node === "string" || typeof node === "number") return String(node);
404
+ if (Array.isArray(node)) return node.map(reactNodeToLabelString).join("");
405
+ return "";
406
+ }
407
+ function resolveStringLabel(item, labelKey) {
408
+ return reactNodeToLabelString(resolveLabel(item, labelKey));
409
+ }
401
410
  function AsyncSelectInner(props) {
402
411
  const {
403
412
  url,
@@ -413,6 +422,7 @@ function AsyncSelectInner(props) {
413
422
  disabled = false,
414
423
  error = false,
415
424
  name,
425
+ id,
416
426
  className,
417
427
  fetcher,
418
428
  dataPath = "data",
@@ -434,8 +444,8 @@ function AsyncSelectInner(props) {
434
444
  const searchInputRef = React6.useRef(null);
435
445
  const abortRef = React6.useRef(null);
436
446
  React6.useEffect(() => {
437
- const id = setTimeout(() => setDebouncedSearch(search), debounceMs);
438
- return () => clearTimeout(id);
447
+ const id2 = setTimeout(() => setDebouncedSearch(search), debounceMs);
448
+ return () => clearTimeout(id2);
439
449
  }, [search, debounceMs]);
440
450
  React6.useEffect(() => {
441
451
  setPage(1);
@@ -509,38 +519,43 @@ function AsyncSelectInner(props) {
509
519
  }
510
520
  }, [open]);
511
521
  React6.useEffect(() => {
512
- if (!value) {
522
+ const v = value?.value;
523
+ if (v == null || v === "") {
513
524
  setSelectedItem(null);
514
525
  return;
515
526
  }
516
527
  const found = items.find(
517
- (it) => String(it[valueKey]) === value
528
+ (it) => String(it[valueKey]) === v
518
529
  );
519
- if (found) setSelectedItem(found);
530
+ setSelectedItem(found ?? null);
520
531
  }, [value, items, valueKey]);
521
532
  const handleSelect = (item) => {
522
533
  const itemValue = String(item[valueKey]);
523
- if (itemValue === value && clearable) {
524
- onValueChange?.("");
534
+ if (itemValue === value?.value && clearable) {
535
+ onValueChange?.(null);
525
536
  setSelectedItem(null);
526
537
  } else {
527
- onValueChange?.(itemValue);
538
+ onValueChange?.({
539
+ value: itemValue,
540
+ label: resolveStringLabel(item, labelKey)
541
+ });
528
542
  setSelectedItem(item);
529
543
  }
530
544
  setOpen(false);
531
545
  };
532
546
  const handleClear = (e) => {
533
547
  e.stopPropagation();
534
- onValueChange?.("");
548
+ onValueChange?.(null);
535
549
  setSelectedItem(null);
536
550
  };
537
- const triggerLabel = selectedItem ? resolveLabel(selectedItem, labelKey) : null;
551
+ const triggerLabel = selectedItem ? resolveLabel(selectedItem, labelKey) : value?.label ? value.label : null;
538
552
  return /* @__PURE__ */ jsxs5(Popover, { open, onOpenChange: setOpen, children: [
539
- name && /* @__PURE__ */ jsx6("input", { type: "hidden", name, value: value ?? "" }),
553
+ name && /* @__PURE__ */ jsx6("input", { type: "hidden", name, value: value?.value ?? "" }),
540
554
  /* @__PURE__ */ jsx6(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs5(
541
555
  "button",
542
556
  {
543
557
  ref: innerRef,
558
+ id,
544
559
  type: "button",
545
560
  role: "combobox",
546
561
  "aria-expanded": open,
@@ -562,7 +577,7 @@ function AsyncSelectInner(props) {
562
577
  }
563
578
  ),
564
579
  /* @__PURE__ */ jsxs5("span", { className: "flex shrink-0 items-center gap-1", children: [
565
- clearable && value && !disabled && /* @__PURE__ */ jsx6(
580
+ clearable && value?.value && !disabled && /* @__PURE__ */ jsx6(
566
581
  "span",
567
582
  {
568
583
  role: "button",
@@ -609,7 +624,7 @@ function AsyncSelectInner(props) {
609
624
  const itemValue = String(
610
625
  item[valueKey]
611
626
  );
612
- const isSelected = itemValue === value;
627
+ const isSelected = itemValue === value?.value;
613
628
  return /* @__PURE__ */ jsxs5(
614
629
  "button",
615
630
  {
@@ -2506,6 +2521,106 @@ Textarea.displayName = "Textarea";
2506
2521
 
2507
2522
  // source/components/primitive/FormField/form-field.tsx
2508
2523
  import { jsx as jsx32, jsxs as jsxs20 } from "react/jsx-runtime";
2524
+ function stripThousands(s) {
2525
+ return s.replace(/,/g, "");
2526
+ }
2527
+ function formFieldCanonicalizeNumericInput(raw, allowDecimal) {
2528
+ const withoutCommas = stripThousands(raw);
2529
+ const pattern = allowDecimal ? /[^\d.]/g : /\D/g;
2530
+ let cleaned = withoutCommas.replace(pattern, "");
2531
+ if (!allowDecimal) return cleaned;
2532
+ const parts = cleaned.split(".");
2533
+ if (parts.length <= 1) return cleaned;
2534
+ const intPart = parts[0] ?? "";
2535
+ const frac = parts.slice(1).join("");
2536
+ return intPart + "." + frac;
2537
+ }
2538
+ function formatThousands(intPart) {
2539
+ if (!intPart) return "";
2540
+ return intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
2541
+ }
2542
+ function formFieldFormatNumericDisplay(stored, allowDecimal, useGrouping) {
2543
+ if (stored == null) return "";
2544
+ const s = String(stored);
2545
+ if (s === "") return "";
2546
+ if (!useGrouping) return s;
2547
+ if (allowDecimal && s.includes(".")) {
2548
+ const [intPart, ...rest] = s.split(".");
2549
+ const frac = rest.join("");
2550
+ return formatThousands(intPart ?? "") + "." + frac;
2551
+ }
2552
+ return formatThousands(s);
2553
+ }
2554
+ var FORM_FIELD_MASK_SLOTS = /* @__PURE__ */ new Set(["#", "h", "H", "X", "x", "*"]);
2555
+ function isFormFieldMaskSlotChar(p) {
2556
+ return FORM_FIELD_MASK_SLOTS.has(p);
2557
+ }
2558
+ function matchesMaskSlot(slot, ch) {
2559
+ switch (slot) {
2560
+ case "#":
2561
+ return /\d/.test(ch);
2562
+ case "h":
2563
+ case "H":
2564
+ return /[0-9a-fA-F]/.test(ch);
2565
+ case "X":
2566
+ case "x":
2567
+ return /[A-Za-z]/.test(ch);
2568
+ case "*":
2569
+ return /[A-Za-z0-9]/.test(ch);
2570
+ default:
2571
+ return false;
2572
+ }
2573
+ }
2574
+ function normalizeMaskSlotChar(slot, ch) {
2575
+ switch (slot) {
2576
+ case "#":
2577
+ return ch;
2578
+ case "h":
2579
+ return ch.toLowerCase();
2580
+ case "H":
2581
+ return /\d/.test(ch) ? ch : ch.toUpperCase();
2582
+ case "X":
2583
+ return ch.toUpperCase();
2584
+ case "x":
2585
+ return ch.toLowerCase();
2586
+ case "*":
2587
+ return ch;
2588
+ default:
2589
+ return ch;
2590
+ }
2591
+ }
2592
+ function formFieldMaskSlotCount(pattern) {
2593
+ return [...pattern].filter(isFormFieldMaskSlotChar).length;
2594
+ }
2595
+ function formFieldMaskExtractCanonical(raw, pattern) {
2596
+ const slots = [...pattern].filter(isFormFieldMaskSlotChar);
2597
+ if (!slots.length) return "";
2598
+ const out = [];
2599
+ let si = 0;
2600
+ for (const ch of raw) {
2601
+ if (si >= slots.length) break;
2602
+ const slot = slots[si];
2603
+ if (matchesMaskSlot(slot, ch)) {
2604
+ out.push(normalizeMaskSlotChar(slot, ch));
2605
+ si++;
2606
+ }
2607
+ }
2608
+ return out.join("");
2609
+ }
2610
+ function formFieldMaskFormatDisplay(pattern, canonical) {
2611
+ let ci = 0;
2612
+ let result = "";
2613
+ for (const p of pattern) {
2614
+ if (isFormFieldMaskSlotChar(p)) {
2615
+ if (ci >= canonical.length) break;
2616
+ result += canonical[ci];
2617
+ ci++;
2618
+ } else if (ci < canonical.length) {
2619
+ result += p;
2620
+ }
2621
+ }
2622
+ return result;
2623
+ }
2509
2624
  var VARIANTS_NEED_CONTROL = [
2510
2625
  "checkbox",
2511
2626
  "switch",
@@ -2544,6 +2659,8 @@ function FormField({
2544
2659
  richTextProps,
2545
2660
  dropzoneProps,
2546
2661
  asyncSelectProps,
2662
+ numericInput,
2663
+ maskInput,
2547
2664
  className,
2548
2665
  renderInput
2549
2666
  }) {
@@ -2551,6 +2668,39 @@ function FormField({
2551
2668
  const inputId = `field-${generatedId}`;
2552
2669
  const descriptionId = description ? `${inputId}-description` : void 0;
2553
2670
  const externalError = error ? String(error) : void 0;
2671
+ if (maskInput && numericInput) {
2672
+ throw new Error(
2673
+ "FormField does not support `maskInput` and `numericInput` together; use only one."
2674
+ );
2675
+ }
2676
+ if (maskInput) {
2677
+ if (!control) {
2678
+ throw new Error(
2679
+ "FormField `maskInput` requires the `control` prop (Controller). Use `control`, not `register`, for masked fields."
2680
+ );
2681
+ }
2682
+ if (variant !== "input") {
2683
+ throw new Error('FormField `maskInput` is only supported with the default `variant="input"`.');
2684
+ }
2685
+ if (!maskInput.pattern?.trim()) {
2686
+ throw new Error("FormField `maskInput.pattern` must be a non-empty string.");
2687
+ }
2688
+ if (formFieldMaskSlotCount(maskInput.pattern) === 0) {
2689
+ throw new Error(
2690
+ "FormField `maskInput.pattern` must include at least one slot token: `#`, `h`, `H`, `X`, `x`, or `*`."
2691
+ );
2692
+ }
2693
+ }
2694
+ if (numericInput) {
2695
+ if (!control) {
2696
+ throw new Error(
2697
+ "FormField `numericInput` requires the `control` prop (Controller). Use `control`, not `register`, for formatted numeric fields."
2698
+ );
2699
+ }
2700
+ if (variant !== "input") {
2701
+ throw new Error('FormField `numericInput` is only supported with the default `variant="input"`.');
2702
+ }
2703
+ }
2554
2704
  if (!control && VARIANTS_NEED_CONTROL.includes(variant)) {
2555
2705
  throw new Error(
2556
2706
  `FormField variant "${variant}" requires the control prop (React Hook Form Controller).`
@@ -2580,7 +2730,7 @@ function FormField({
2580
2730
  };
2581
2731
  const controlNode = renderInput ? renderInput({
2582
2732
  ...sharedInputShell,
2583
- value: field.value ?? "",
2733
+ value: variant === "async-select" ? formValueToAsyncSelectOption(field.value) ?? null : field.value ?? "",
2584
2734
  onChange: field.onChange,
2585
2735
  onBlur: field.onBlur,
2586
2736
  ref: field.ref
@@ -2604,7 +2754,9 @@ function FormField({
2604
2754
  otpProps,
2605
2755
  richTextProps,
2606
2756
  dropzoneProps,
2607
- asyncSelectProps
2757
+ asyncSelectProps,
2758
+ numericInput: renderInput ? void 0 : numericInput,
2759
+ maskInput: renderInput ? void 0 : maskInput
2608
2760
  }
2609
2761
  );
2610
2762
  const labelBlock = isCheckboxInline || !hasFieldLabel2 ? null : /* @__PURE__ */ jsx32(
@@ -2724,6 +2876,17 @@ function FormField({
2724
2876
  externalError && /* @__PURE__ */ jsx32(Text, { id: `${inputId}-error`, size: "sm", tone: "destructive", children: externalError })
2725
2877
  ] });
2726
2878
  }
2879
+ function formValueToAsyncSelectOption(v) {
2880
+ if (v == null || v === "") return void 0;
2881
+ if (typeof v === "object" && v !== null && "value" in v) {
2882
+ const o = v;
2883
+ return {
2884
+ value: String(o.value ?? ""),
2885
+ label: o.label != null ? String(o.label) : ""
2886
+ };
2887
+ }
2888
+ return { value: String(v), label: "" };
2889
+ }
2727
2890
  function FormFieldVariantControl({
2728
2891
  variant,
2729
2892
  inputId,
@@ -2742,7 +2905,9 @@ function FormFieldVariantControl({
2742
2905
  otpProps,
2743
2906
  richTextProps,
2744
2907
  dropzoneProps,
2745
- asyncSelectProps
2908
+ asyncSelectProps,
2909
+ numericInput,
2910
+ maskInput
2746
2911
  }) {
2747
2912
  switch (variant) {
2748
2913
  case "textarea":
@@ -2848,7 +3013,8 @@ function FormFieldVariantControl({
2848
3013
  AsyncSelect,
2849
3014
  {
2850
3015
  ...asyncSelectProps,
2851
- value: field.value == null || field.value === "" ? void 0 : String(field.value),
3016
+ id: inputId,
3017
+ value: formValueToAsyncSelectOption(field.value),
2852
3018
  onValueChange: field.onChange,
2853
3019
  disabled: field.disabled,
2854
3020
  error: hasError,
@@ -2979,22 +3145,100 @@ function FormFieldVariantControl({
2979
3145
  `${String(field.name)}-${field.value === null ? "cleared" : typeof FileList !== "undefined" && field.value instanceof FileList ? field.value.length : "open"}`
2980
3146
  ) });
2981
3147
  case "input":
2982
- default:
3148
+ default: {
3149
+ const {
3150
+ onChange: inputOnChangeFromProps,
3151
+ value: _omitValueFromInputProps,
3152
+ type: inputTypeProp,
3153
+ inputMode: inputModeProp,
3154
+ ...restInputProps
3155
+ } = inputProps ?? {};
3156
+ if (maskInput) {
3157
+ const pattern = maskInput.pattern;
3158
+ const rawStored = field.value == null || field.value === "" ? "" : String(field.value);
3159
+ const displayValue = formFieldMaskFormatDisplay(pattern, rawStored);
3160
+ return /* @__PURE__ */ jsx32("div", { className: "w-full min-w-0", children: /* @__PURE__ */ jsx32(
3161
+ Input,
3162
+ {
3163
+ ...restInputProps,
3164
+ id: inputId,
3165
+ name: field.name,
3166
+ "aria-describedby": describedBy,
3167
+ error: hasError,
3168
+ type: "text",
3169
+ inputMode: maskInput.inputMode ?? "text",
3170
+ maxLength: pattern.length,
3171
+ autoComplete: "off",
3172
+ spellCheck: false,
3173
+ value: displayValue,
3174
+ onChange: (e) => {
3175
+ const canonical = formFieldMaskExtractCanonical(
3176
+ e.target.value,
3177
+ pattern
3178
+ );
3179
+ field.onChange(canonical);
3180
+ },
3181
+ onBlur: field.onBlur,
3182
+ ref: field.ref,
3183
+ disabled: field.disabled
3184
+ }
3185
+ ) });
3186
+ }
3187
+ if (numericInput) {
3188
+ const allowDecimal = numericInput.allowDecimal ?? false;
3189
+ const useGrouping = numericInput.useGrouping ?? true;
3190
+ const rawStored = field.value == null || field.value === "" ? "" : String(field.value);
3191
+ const displayValue = formFieldFormatNumericDisplay(
3192
+ rawStored,
3193
+ allowDecimal,
3194
+ useGrouping
3195
+ );
3196
+ const defaultInputMode = allowDecimal ? "decimal" : "numeric";
3197
+ return /* @__PURE__ */ jsx32("div", { className: "w-full min-w-0", children: /* @__PURE__ */ jsx32(
3198
+ Input,
3199
+ {
3200
+ ...restInputProps,
3201
+ id: inputId,
3202
+ name: field.name,
3203
+ "aria-describedby": describedBy,
3204
+ error: hasError,
3205
+ type: "text",
3206
+ inputMode: numericInput.inputMode ?? defaultInputMode,
3207
+ value: displayValue,
3208
+ onChange: (e) => {
3209
+ const canonical = formFieldCanonicalizeNumericInput(
3210
+ e.target.value,
3211
+ allowDecimal
3212
+ );
3213
+ field.onChange(canonical);
3214
+ },
3215
+ onBlur: field.onBlur,
3216
+ ref: field.ref,
3217
+ disabled: field.disabled
3218
+ }
3219
+ ) });
3220
+ }
2983
3221
  return /* @__PURE__ */ jsx32("div", { className: "w-full min-w-0", children: /* @__PURE__ */ jsx32(
2984
3222
  Input,
2985
3223
  {
2986
- ...inputProps,
3224
+ ...restInputProps,
2987
3225
  id: inputId,
2988
3226
  name: field.name,
2989
3227
  "aria-describedby": describedBy,
2990
3228
  error: hasError,
3229
+ type: inputTypeProp,
3230
+ inputMode: inputModeProp,
2991
3231
  value: field.value ?? "",
2992
- onChange: field.onChange,
3232
+ onChange: (e) => {
3233
+ inputOnChangeFromProps?.(e);
3234
+ field.onChange(e);
3235
+ },
2993
3236
  onBlur: field.onBlur,
2994
3237
  ref: field.ref,
2995
3238
  disabled: field.disabled
2996
3239
  }
2997
3240
  ) });
3241
+ }
2998
3242
  }
2999
3243
  }
3000
3244
 
@@ -4913,6 +5157,12 @@ export {
4913
5157
  colors,
4914
5158
  containerVariants,
4915
5159
  dismissToast,
5160
+ formFieldCanonicalizeNumericInput,
5161
+ formFieldFormatNumericDisplay,
5162
+ formFieldMaskExtractCanonical,
5163
+ formFieldMaskFormatDisplay,
5164
+ formFieldMaskSlotCount,
5165
+ formValueToAsyncSelectOption,
4916
5166
  getIcon,
4917
5167
  headingVariants,
4918
5168
  inputGroupSelectTriggerTextAlignClass,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2urgseui/core",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "2URGSE Shared UI component library",
5
5
  "license": "UNLICENSED",
6
6
  "main": "./dist/index.cjs",