@2urgseui/core 0.1.1 → 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,11 @@ __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,
229
234
  formValueToAsyncSelectOption: () => formValueToAsyncSelectOption,
230
235
  getIcon: () => getIcon,
231
236
  headingVariants: () => headingVariants,
@@ -2756,6 +2761,106 @@ Textarea.displayName = "Textarea";
2756
2761
 
2757
2762
  // source/components/primitive/FormField/form-field.tsx
2758
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
+ }
2759
2864
  var VARIANTS_NEED_CONTROL = [
2760
2865
  "checkbox",
2761
2866
  "switch",
@@ -2794,6 +2899,8 @@ function FormField({
2794
2899
  richTextProps,
2795
2900
  dropzoneProps,
2796
2901
  asyncSelectProps,
2902
+ numericInput,
2903
+ maskInput,
2797
2904
  className,
2798
2905
  renderInput
2799
2906
  }) {
@@ -2801,6 +2908,39 @@ function FormField({
2801
2908
  const inputId = `field-${generatedId}`;
2802
2909
  const descriptionId = description ? `${inputId}-description` : void 0;
2803
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
+ }
2804
2944
  if (!control && VARIANTS_NEED_CONTROL.includes(variant)) {
2805
2945
  throw new Error(
2806
2946
  `FormField variant "${variant}" requires the control prop (React Hook Form Controller).`
@@ -2854,7 +2994,9 @@ function FormField({
2854
2994
  otpProps,
2855
2995
  richTextProps,
2856
2996
  dropzoneProps,
2857
- asyncSelectProps
2997
+ asyncSelectProps,
2998
+ numericInput: renderInput ? void 0 : numericInput,
2999
+ maskInput: renderInput ? void 0 : maskInput
2858
3000
  }
2859
3001
  );
2860
3002
  const labelBlock = isCheckboxInline || !hasFieldLabel2 ? null : /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
@@ -3003,7 +3145,9 @@ function FormFieldVariantControl({
3003
3145
  otpProps,
3004
3146
  richTextProps,
3005
3147
  dropzoneProps,
3006
- asyncSelectProps
3148
+ asyncSelectProps,
3149
+ numericInput,
3150
+ maskInput
3007
3151
  }) {
3008
3152
  switch (variant) {
3009
3153
  case "textarea":
@@ -3241,22 +3385,100 @@ function FormFieldVariantControl({
3241
3385
  `${String(field.name)}-${field.value === null ? "cleared" : typeof FileList !== "undefined" && field.value instanceof FileList ? field.value.length : "open"}`
3242
3386
  ) });
3243
3387
  case "input":
3244
- 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
+ }
3245
3461
  return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { className: "w-full min-w-0", children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
3246
3462
  Input,
3247
3463
  {
3248
- ...inputProps,
3464
+ ...restInputProps,
3249
3465
  id: inputId,
3250
3466
  name: field.name,
3251
3467
  "aria-describedby": describedBy,
3252
3468
  error: hasError,
3469
+ type: inputTypeProp,
3470
+ inputMode: inputModeProp,
3253
3471
  value: field.value ?? "",
3254
- onChange: field.onChange,
3472
+ onChange: (e) => {
3473
+ inputOnChangeFromProps?.(e);
3474
+ field.onChange(e);
3475
+ },
3255
3476
  onBlur: field.onBlur,
3256
3477
  ref: field.ref,
3257
3478
  disabled: field.disabled
3258
3479
  }
3259
3480
  ) });
3481
+ }
3260
3482
  }
3261
3483
  }
3262
3484
 
@@ -5483,6 +5705,11 @@ var typography = {
5483
5705
  colors,
5484
5706
  containerVariants,
5485
5707
  dismissToast,
5708
+ formFieldCanonicalizeNumericInput,
5709
+ formFieldFormatNumericDisplay,
5710
+ formFieldMaskExtractCanonical,
5711
+ formFieldMaskFormatDisplay,
5712
+ formFieldMaskSlotCount,
5486
5713
  formValueToAsyncSelectOption,
5487
5714
  getIcon,
5488
5715
  headingVariants,
package/dist/index.d.cts CHANGED
@@ -488,6 +488,44 @@ type FormFieldDropzoneConfig = Omit<FileDropzoneProps, "id" | "name" | "onChange
488
488
  * The controlled field value is `{ value, label } | null | undefined` (legacy plain `string` is still read as id-only).
489
489
  */
490
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
+ };
491
529
  interface FormFieldProps<TFieldValues extends FieldValues> {
492
530
  name: Path<TFieldValues>;
493
531
  /** When omitted, no visible label is rendered; use `aria-label` on controls or descriptions for a11y. */
@@ -511,6 +549,16 @@ interface FormFieldProps<TFieldValues extends FieldValues> {
511
549
  dropzoneProps?: FormFieldDropzoneConfig;
512
550
  /** With `variant="async-select"`: URL fetch config; form state is `AsyncSelectOption | null` (see `FormFieldAsyncSelectConfig`). */
513
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;
514
562
  className?: string;
515
563
  renderInput?: (props: Omit<FormFieldRenderProps, "value"> & {
516
564
  id: string;
@@ -521,7 +569,7 @@ interface FormFieldProps<TFieldValues extends FieldValues> {
521
569
  value?: InputProps["value"] | AsyncSelectOption | null;
522
570
  }) => React.ReactNode;
523
571
  }
524
- 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;
525
573
  /** Maps react-hook-form field values to `AsyncSelect`’s `value` prop (supports `{ value, label }`, legacy `string`, `null`/`undefined`). */
526
574
  declare function formValueToAsyncSelectOption(v: unknown): AsyncSelectOption | undefined;
527
575
 
@@ -892,4 +940,4 @@ declare namespace Toaster {
892
940
  var displayName: string;
893
941
  }
894
942
 
895
- 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 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, formValueToAsyncSelectOption, 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
@@ -488,6 +488,44 @@ type FormFieldDropzoneConfig = Omit<FileDropzoneProps, "id" | "name" | "onChange
488
488
  * The controlled field value is `{ value, label } | null | undefined` (legacy plain `string` is still read as id-only).
489
489
  */
490
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
+ };
491
529
  interface FormFieldProps<TFieldValues extends FieldValues> {
492
530
  name: Path<TFieldValues>;
493
531
  /** When omitted, no visible label is rendered; use `aria-label` on controls or descriptions for a11y. */
@@ -511,6 +549,16 @@ interface FormFieldProps<TFieldValues extends FieldValues> {
511
549
  dropzoneProps?: FormFieldDropzoneConfig;
512
550
  /** With `variant="async-select"`: URL fetch config; form state is `AsyncSelectOption | null` (see `FormFieldAsyncSelectConfig`). */
513
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;
514
562
  className?: string;
515
563
  renderInput?: (props: Omit<FormFieldRenderProps, "value"> & {
516
564
  id: string;
@@ -521,7 +569,7 @@ interface FormFieldProps<TFieldValues extends FieldValues> {
521
569
  value?: InputProps["value"] | AsyncSelectOption | null;
522
570
  }) => React.ReactNode;
523
571
  }
524
- 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;
525
573
  /** Maps react-hook-form field values to `AsyncSelect`’s `value` prop (supports `{ value, label }`, legacy `string`, `null`/`undefined`). */
526
574
  declare function formValueToAsyncSelectOption(v: unknown): AsyncSelectOption | undefined;
527
575
 
@@ -892,4 +940,4 @@ declare namespace Toaster {
892
940
  var displayName: string;
893
941
  }
894
942
 
895
- 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 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, formValueToAsyncSelectOption, 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
@@ -2521,6 +2521,106 @@ Textarea.displayName = "Textarea";
2521
2521
 
2522
2522
  // source/components/primitive/FormField/form-field.tsx
2523
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
+ }
2524
2624
  var VARIANTS_NEED_CONTROL = [
2525
2625
  "checkbox",
2526
2626
  "switch",
@@ -2559,6 +2659,8 @@ function FormField({
2559
2659
  richTextProps,
2560
2660
  dropzoneProps,
2561
2661
  asyncSelectProps,
2662
+ numericInput,
2663
+ maskInput,
2562
2664
  className,
2563
2665
  renderInput
2564
2666
  }) {
@@ -2566,6 +2668,39 @@ function FormField({
2566
2668
  const inputId = `field-${generatedId}`;
2567
2669
  const descriptionId = description ? `${inputId}-description` : void 0;
2568
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
+ }
2569
2704
  if (!control && VARIANTS_NEED_CONTROL.includes(variant)) {
2570
2705
  throw new Error(
2571
2706
  `FormField variant "${variant}" requires the control prop (React Hook Form Controller).`
@@ -2619,7 +2754,9 @@ function FormField({
2619
2754
  otpProps,
2620
2755
  richTextProps,
2621
2756
  dropzoneProps,
2622
- asyncSelectProps
2757
+ asyncSelectProps,
2758
+ numericInput: renderInput ? void 0 : numericInput,
2759
+ maskInput: renderInput ? void 0 : maskInput
2623
2760
  }
2624
2761
  );
2625
2762
  const labelBlock = isCheckboxInline || !hasFieldLabel2 ? null : /* @__PURE__ */ jsx32(
@@ -2768,7 +2905,9 @@ function FormFieldVariantControl({
2768
2905
  otpProps,
2769
2906
  richTextProps,
2770
2907
  dropzoneProps,
2771
- asyncSelectProps
2908
+ asyncSelectProps,
2909
+ numericInput,
2910
+ maskInput
2772
2911
  }) {
2773
2912
  switch (variant) {
2774
2913
  case "textarea":
@@ -3006,22 +3145,100 @@ function FormFieldVariantControl({
3006
3145
  `${String(field.name)}-${field.value === null ? "cleared" : typeof FileList !== "undefined" && field.value instanceof FileList ? field.value.length : "open"}`
3007
3146
  ) });
3008
3147
  case "input":
3009
- 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
+ }
3010
3221
  return /* @__PURE__ */ jsx32("div", { className: "w-full min-w-0", children: /* @__PURE__ */ jsx32(
3011
3222
  Input,
3012
3223
  {
3013
- ...inputProps,
3224
+ ...restInputProps,
3014
3225
  id: inputId,
3015
3226
  name: field.name,
3016
3227
  "aria-describedby": describedBy,
3017
3228
  error: hasError,
3229
+ type: inputTypeProp,
3230
+ inputMode: inputModeProp,
3018
3231
  value: field.value ?? "",
3019
- onChange: field.onChange,
3232
+ onChange: (e) => {
3233
+ inputOnChangeFromProps?.(e);
3234
+ field.onChange(e);
3235
+ },
3020
3236
  onBlur: field.onBlur,
3021
3237
  ref: field.ref,
3022
3238
  disabled: field.disabled
3023
3239
  }
3024
3240
  ) });
3241
+ }
3025
3242
  }
3026
3243
  }
3027
3244
 
@@ -4940,6 +5157,11 @@ export {
4940
5157
  colors,
4941
5158
  containerVariants,
4942
5159
  dismissToast,
5160
+ formFieldCanonicalizeNumericInput,
5161
+ formFieldFormatNumericDisplay,
5162
+ formFieldMaskExtractCanonical,
5163
+ formFieldMaskFormatDisplay,
5164
+ formFieldMaskSlotCount,
4943
5165
  formValueToAsyncSelectOption,
4944
5166
  getIcon,
4945
5167
  headingVariants,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2urgseui/core",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "2URGSE Shared UI component library",
5
5
  "license": "UNLICENSED",
6
6
  "main": "./dist/index.cjs",