@korsolutions/ui 0.0.20 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/components/index.d.mts +33 -2
  2. package/dist/components/index.mjs +41 -4
  3. package/dist/hooks/index.d.mts +32 -2
  4. package/dist/hooks/index.mjs +79 -2
  5. package/dist/{index-CGY0mO6z.d.mts → index-pCM7YTs1.d.mts} +1 -8
  6. package/dist/index.mjs +3 -2
  7. package/dist/primitives/index.d.mts +2 -1
  8. package/dist/primitives/index.mjs +2 -1
  9. package/dist/{primitives-P_8clvQr.mjs → primitives-DNeYBN-3.mjs} +3 -105
  10. package/dist/{toast-manager-DSo9oN8w.mjs → toast-manager-BfoJ-_dB.mjs} +1 -1
  11. package/dist/use-numeric-mask-B9WZG25o.d.mts +33 -0
  12. package/dist/use-numeric-mask-BQlz1Pus.mjs +113 -0
  13. package/dist/use-relative-position-BTKEyT1F.mjs +106 -0
  14. package/dist/use-relative-position-DBzhrBU7.d.mts +61 -0
  15. package/package.json +1 -1
  16. package/src/components/index.ts +1 -1
  17. package/src/components/input/index.ts +2 -0
  18. package/src/components/input/numeric-input.tsx +73 -0
  19. package/src/hooks/index.ts +4 -1
  20. package/src/hooks/use-currency-mask.ts +141 -0
  21. package/src/hooks/use-numeric-mask.ts +202 -0
  22. package/src/primitives/dropdown-menu/context.ts +1 -1
  23. package/src/primitives/dropdown-menu/dropdown-menu-content.tsx +1 -1
  24. package/src/primitives/dropdown-menu/dropdown-menu-root.tsx +1 -1
  25. package/src/primitives/popover/context.ts +1 -1
  26. package/src/primitives/popover/popover-content.tsx +1 -1
  27. package/src/primitives/popover/popover-root.tsx +1 -1
  28. /package/src/hooks/{useRelativePosition.ts → use-relative-position.ts} +0 -0
  29. /package/src/hooks/{useScreenSize.ts → use-screen-size.ts} +0 -0
@@ -1,4 +1,6 @@
1
- import { D as BadgeStyles, I as AvatarStyles, S as TextareaPrimitiveBaseProps, V as EmptyStyles, d as PopoverTriggerRef, dt as ButtonStyles, et as SelectRootBaseProps, gt as InputStyles, h as DropdownMenuStyles, j as ToastStyles, lt as ButtonPrimitiveRootProps, mt as InputPrimitiveBaseProps, nt as SelectStyles, q as CardStyles, s as PopoverStyles, w as TextareaStyles, yt as FieldStyles } from "../index-CGY0mO6z.mjs";
1
+ import { D as BadgeStyles, I as AvatarStyles, S as TextareaPrimitiveBaseProps, V as EmptyStyles, d as PopoverTriggerRef, dt as ButtonStyles, et as SelectRootBaseProps, gt as InputStyles, h as DropdownMenuStyles, j as ToastStyles, lt as ButtonPrimitiveRootProps, mt as InputPrimitiveBaseProps, nt as SelectStyles, q as CardStyles, s as PopoverStyles, w as TextareaStyles, yt as FieldStyles } from "../index-pCM7YTs1.mjs";
2
+ import "../use-relative-position-DBzhrBU7.mjs";
3
+ import { t as NumericMaskFormat } from "../use-numeric-mask-B9WZG25o.mjs";
2
4
  import React from "react";
3
5
  import { ImageSource, PressableProps, TextProps, TextStyle } from "react-native";
4
6
 
@@ -49,6 +51,35 @@ interface InputProps extends InputPrimitiveBaseProps {
49
51
  }
50
52
  declare function Input(props: InputProps): React.JSX.Element;
51
53
  //#endregion
54
+ //#region src/components/input/numeric-input.d.ts
55
+ interface NumericInputProps extends Omit<InputPrimitiveBaseProps, "value" | "onChange" | "keyboardType"> {
56
+ variant?: "default";
57
+ value?: number | null;
58
+ onChange?: (value: number | null) => void;
59
+ format?: NumericMaskFormat;
60
+ locale?: string;
61
+ currency?: string;
62
+ precision?: number;
63
+ min?: number;
64
+ max?: number;
65
+ allowNegative?: boolean;
66
+ }
67
+ declare function NumericInput({
68
+ value,
69
+ onChange,
70
+ format,
71
+ locale,
72
+ currency,
73
+ precision,
74
+ min,
75
+ max,
76
+ allowNegative,
77
+ variant,
78
+ onBlur,
79
+ onFocus,
80
+ ...props
81
+ }: NumericInputProps): React.JSX.Element;
82
+ //#endregion
52
83
  //#region src/components/field/variants/index.d.ts
53
84
  declare const FieldVariants: {
54
85
  default: () => FieldStyles;
@@ -262,4 +293,4 @@ interface PopoverProps {
262
293
  }
263
294
  declare const Popover: React.ForwardRefExoticComponent<PopoverProps & React.RefAttributes<PopoverTriggerRef>>;
264
295
  //#endregion
265
- export { Avatar, AvatarProps, Badge, Button, Card, DropdownMenu, Empty, EmptyProps, Field, FieldProps, Input, Link, LinkProps, Popover, PopoverProps, Select, SelectOption, SelectProps, Textarea, Toast, Typography, TypographyProps };
296
+ export { Avatar, AvatarProps, Badge, Button, Card, DropdownMenu, Empty, EmptyProps, Field, FieldProps, Input, Link, LinkProps, NumericInput, NumericInputProps, Popover, PopoverProps, Select, SelectOption, SelectProps, Textarea, Toast, Typography, TypographyProps };
@@ -1,6 +1,8 @@
1
- import { i as useThemedStyles, r as ToastComponent, t as ToastAPI } from "../toast-manager-DSo9oN8w.mjs";
2
- import { a as BadgePrimitive, c as EmptyPrimitive, d as ButtonPrimitive, f as InputPrimitive, i as TextareaPrimitive, l as CardPrimitive, n as usePopover, p as FieldPrimitive, r as DropdownMenuPrimitive, s as AvatarPrimitive, t as PopoverPrimitive, u as SelectPrimitive } from "../primitives-P_8clvQr.mjs";
3
- import React, { forwardRef, useState } from "react";
1
+ import { i as useThemedStyles, r as ToastComponent, t as ToastAPI } from "../toast-manager-BfoJ-_dB.mjs";
2
+ import { a as BadgePrimitive, c as EmptyPrimitive, d as ButtonPrimitive, f as InputPrimitive, i as TextareaPrimitive, l as CardPrimitive, n as usePopover, p as FieldPrimitive, r as DropdownMenuPrimitive, s as AvatarPrimitive, t as PopoverPrimitive, u as SelectPrimitive } from "../primitives-DNeYBN-3.mjs";
3
+ import "../use-relative-position-BTKEyT1F.mjs";
4
+ import { t as useNumericMask } from "../use-numeric-mask-BQlz1Pus.mjs";
5
+ import React, { forwardRef, useEffect, useState } from "react";
4
6
  import { Linking, Text } from "react-native";
5
7
  import { jsx, jsxs } from "react/jsx-runtime";
6
8
 
@@ -205,6 +207,41 @@ function Input(props) {
205
207
  });
206
208
  }
207
209
 
210
+ //#endregion
211
+ //#region src/components/input/numeric-input.tsx
212
+ function NumericInput({ value, onChange, format = "decimal", locale = "en-US", currency = "USD", precision = 2, min, max, allowNegative = true, variant = "default", onBlur, onFocus, ...props }) {
213
+ const numericMask = useNumericMask({
214
+ format,
215
+ locale,
216
+ currency,
217
+ precision,
218
+ min,
219
+ max,
220
+ allowNegative,
221
+ onChange
222
+ });
223
+ useEffect(() => {
224
+ if (value !== numericMask.numericValue) numericMask.setValue(value ?? null);
225
+ }, [value]);
226
+ const handleBlur = (e) => {
227
+ numericMask.onBlur();
228
+ onBlur?.(e);
229
+ };
230
+ const handleFocus = (e) => {
231
+ numericMask.onFocus();
232
+ onFocus?.(e);
233
+ };
234
+ return /* @__PURE__ */ jsx(Input, {
235
+ ...props,
236
+ variant,
237
+ value: numericMask.value,
238
+ onChange: numericMask.onChangeText,
239
+ onBlur: handleBlur,
240
+ onFocus: handleFocus,
241
+ keyboardType: numericMask.keyboardType
242
+ });
243
+ }
244
+
208
245
  //#endregion
209
246
  //#region src/components/field/variants/default.tsx
210
247
  const useFieldVariantDefault = () => {
@@ -790,4 +827,4 @@ PopoverComponent.displayName = "Popover";
790
827
  const Popover = PopoverComponent;
791
828
 
792
829
  //#endregion
793
- export { Avatar, Badge, Button, Card, DropdownMenu, Empty, Field, Input, Link, Popover, Select, Textarea, Toast, Typography };
830
+ export { Avatar, Badge, Button, Card, DropdownMenu, Empty, Field, Input, Link, NumericInput, Popover, Select, Textarea, Toast, Typography };
@@ -1,4 +1,7 @@
1
- //#region src/hooks/useScreenSize.d.ts
1
+ import { i as useRelativePosition, n as DEFAULT_POSITION, r as LayoutPosition, t as DEFAULT_LAYOUT } from "../use-relative-position-DBzhrBU7.mjs";
2
+ import { i as useNumericMask, n as UseNumericMaskOptions, r as UseNumericMaskReturn, t as NumericMaskFormat } from "../use-numeric-mask-B9WZG25o.mjs";
3
+
4
+ //#region src/hooks/use-screen-size.d.ts
2
5
  type ScreenSize = "mobile" | "tablet" | "desktop";
3
6
  interface Response {
4
7
  readonly size: ScreenSize;
@@ -14,4 +17,31 @@ interface Response {
14
17
  }
15
18
  declare function useScreenSize(): Response;
16
19
  //#endregion
17
- export { ScreenSize, useScreenSize };
20
+ //#region src/hooks/use-currency-mask.d.ts
21
+ interface UseCurrencyMaskOptions {
22
+ locale?: string;
23
+ currency?: string;
24
+ precision?: number;
25
+ min?: number;
26
+ max?: number;
27
+ onValueChange?: (value: number | null) => void;
28
+ }
29
+ interface UseCurrencyMaskReturn {
30
+ value: string;
31
+ numericValue: number | null;
32
+ onChangeText: (text: string) => void;
33
+ onBlur: () => void;
34
+ onFocus: () => void;
35
+ keyboardType: "decimal-pad";
36
+ setValue: (value: number | null) => void;
37
+ }
38
+ declare function useCurrencyMask({
39
+ locale,
40
+ currency,
41
+ precision,
42
+ min,
43
+ max,
44
+ onValueChange
45
+ }?: UseCurrencyMaskOptions): UseCurrencyMaskReturn;
46
+ //#endregion
47
+ export { DEFAULT_LAYOUT, DEFAULT_POSITION, LayoutPosition, NumericMaskFormat, ScreenSize, UseCurrencyMaskOptions, UseCurrencyMaskReturn, UseNumericMaskOptions, UseNumericMaskReturn, useCurrencyMask, useNumericMask, useRelativePosition, useScreenSize };
@@ -1,6 +1,9 @@
1
+ import { n as DEFAULT_POSITION, r as useRelativePosition, t as DEFAULT_LAYOUT } from "../use-relative-position-BTKEyT1F.mjs";
2
+ import { t as useNumericMask } from "../use-numeric-mask-BQlz1Pus.mjs";
3
+ import { useCallback, useState } from "react";
1
4
  import { useWindowDimensions } from "react-native";
2
5
 
3
- //#region src/hooks/useScreenSize.ts
6
+ //#region src/hooks/use-screen-size.ts
4
7
  function useScreenSize() {
5
8
  const windowDimensions = useWindowDimensions();
6
9
  const size = windowDimensions.width < 768 ? "mobile" : windowDimensions.width < 1024 ? "tablet" : "desktop";
@@ -23,4 +26,78 @@ function useScreenSize() {
23
26
  }
24
27
 
25
28
  //#endregion
26
- export { useScreenSize };
29
+ //#region src/hooks/use-currency-mask.ts
30
+ function useCurrencyMask({ locale = "en-US", currency = "USD", precision = 2, min, max, onValueChange } = {}) {
31
+ const [numericValue, setNumericValue] = useState(null);
32
+ const [displayValue, setDisplayValue] = useState("");
33
+ const [isFocused, setIsFocused] = useState(false);
34
+ const formatCurrency = useCallback((num) => {
35
+ if (num === null || isNaN(num)) return "";
36
+ return new Intl.NumberFormat(locale, {
37
+ style: "currency",
38
+ currency,
39
+ minimumFractionDigits: precision,
40
+ maximumFractionDigits: precision
41
+ }).format(num);
42
+ }, [
43
+ locale,
44
+ currency,
45
+ precision
46
+ ]);
47
+ const parseCurrency = useCallback((text) => {
48
+ const cleaned = text.replace(/[^\d.-]/g, "");
49
+ const parsed = parseFloat(cleaned);
50
+ if (isNaN(parsed) || cleaned === "") return null;
51
+ let constrained = parsed;
52
+ if (min !== void 0 && constrained < min) constrained = min;
53
+ if (max !== void 0 && constrained > max) constrained = max;
54
+ return constrained;
55
+ }, [min, max]);
56
+ return {
57
+ value: displayValue,
58
+ numericValue,
59
+ onChangeText: useCallback((text) => {
60
+ if (isFocused) {
61
+ const cleaned = text.replace(/[^\d.-]/g, "");
62
+ const decimalIndex = cleaned.indexOf(".");
63
+ if (decimalIndex !== -1) {
64
+ if (cleaned.substring(decimalIndex + 1).length > precision) return;
65
+ }
66
+ if ((cleaned.match(/\./g) || []).length > 1) return;
67
+ }
68
+ setDisplayValue(text);
69
+ const value = parseCurrency(text);
70
+ setNumericValue(value);
71
+ onValueChange?.(value);
72
+ }, [
73
+ parseCurrency,
74
+ onValueChange,
75
+ isFocused,
76
+ precision
77
+ ]),
78
+ onBlur: useCallback(() => {
79
+ setIsFocused(false);
80
+ if (numericValue !== null) setDisplayValue(formatCurrency(numericValue));
81
+ else setDisplayValue("");
82
+ }, [numericValue, formatCurrency]),
83
+ onFocus: useCallback(() => {
84
+ setIsFocused(true);
85
+ if (numericValue !== null) setDisplayValue(numericValue.toString());
86
+ }, [numericValue]),
87
+ keyboardType: "decimal-pad",
88
+ setValue: useCallback((value) => {
89
+ setNumericValue(value);
90
+ if (value !== null) if (isFocused) setDisplayValue(value.toString());
91
+ else setDisplayValue(formatCurrency(value));
92
+ else setDisplayValue("");
93
+ onValueChange?.(value);
94
+ }, [
95
+ isFocused,
96
+ formatCurrency,
97
+ onValueChange
98
+ ])
99
+ };
100
+ }
101
+
102
+ //#endregion
103
+ export { DEFAULT_LAYOUT, DEFAULT_POSITION, useCurrencyMask, useNumericMask, useRelativePosition, useScreenSize };
@@ -1,3 +1,4 @@
1
+ import { r as LayoutPosition } from "./use-relative-position-DBzhrBU7.mjs";
1
2
  import * as react7 from "react";
2
3
  import React$1, { Dispatch, RefAttributes } from "react";
3
4
  import { ImageSource, ImageStyle, LayoutRectangle, PressableProps, StyleProp, TextInputProps, TextProps, TextStyle, View, ViewStyle } from "react-native";
@@ -567,14 +568,6 @@ interface PopoverCloseProps extends Omit<PressableProps, "onPress"> {
567
568
  }
568
569
  declare function PopoverClose(props: PopoverCloseProps): React$1.JSX.Element;
569
570
  //#endregion
570
- //#region src/hooks/useRelativePosition.d.ts
571
- interface LayoutPosition {
572
- pageY: number;
573
- pageX: number;
574
- width: number;
575
- height: number;
576
- }
577
- //#endregion
578
571
  //#region src/primitives/popover/context.d.ts
579
572
  interface PopoverContext {
580
573
  isOpen: boolean;
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
- import { a as ThemeProvider, n as ToastContainer, o as useTheme } from "./toast-manager-DSo9oN8w.mjs";
2
- import { m as PortalHost } from "./primitives-P_8clvQr.mjs";
1
+ import { a as ThemeProvider, n as ToastContainer, o as useTheme } from "./toast-manager-BfoJ-_dB.mjs";
2
+ import { m as PortalHost } from "./primitives-DNeYBN-3.mjs";
3
+ import "./use-relative-position-BTKEyT1F.mjs";
3
4
  import { jsx, jsxs } from "react/jsx-runtime";
4
5
 
5
6
  //#region src/index.tsx
@@ -1,2 +1,3 @@
1
- import { $ as SelectPortalProps, A as ToastRootProps, B as EmptyRootProps, C as TextareaPrimitiveProps, D as BadgeStyles, E as BadgeRootProps, F as AvatarRootProps, G as CardPrimitive, H as EmptyTitleProps, I as AvatarStyles, J as CardFooterProps, K as CardRootProps, L as AvatarImageProps, M as ToastDescriptionProps, N as ToastTitleProps, O as BadgeLabelProps, P as AvatarPrimitive, Q as SelectPrimitive, R as AvatarFallbackProps, S as TextareaPrimitiveBaseProps, St as FieldLabelProps, T as BadgePrimitive, U as EmptyMediaProps, V as EmptyStyles, W as EmptyDescriptionProps, X as CardTitleProps, Y as CardBodyProps, Z as CardHeaderProps, _ as DropdownMenuDividerProps, _t as FieldPrimitive, a as PopoverPortalProps, at as SelectOverlayProps, b as DropdownMenuTriggerProps, bt as FieldErrorProps, c as PopoverOverlayProps, ct as ButtonPrimitive, d as PopoverTriggerRef, dt as ButtonStyles, et as SelectRootBaseProps, f as DropdownMenuPrimitive, ft as ButtonPrimitiveLabelProps, g as DropdownMenuOverlayProps, gt as InputStyles, h as DropdownMenuStyles, ht as InputPrimitiveProps, i as PopoverCloseProps, it as SelectContentProps, j as ToastStyles, k as ToastPrimitive, l as PopoverContentProps, lt as ButtonPrimitiveRootProps, m as DropdownMenuRootProps, mt as InputPrimitiveBaseProps, n as PopoverContext, nt as SelectStyles, o as PopoverRootProps, ot as SelectValueProps, p as DropdownMenuPortalProps, pt as InputPrimitive, q as CardStyles, r as usePopover, rt as SelectOptionProps, st as SelectTriggerProps, t as PopoverPrimitive, tt as SelectRootProps, u as PopoverTriggerProps, ut as ButtonState, v as DropdownMenuButtonProps, vt as FieldPrimitiveRootProps, w as TextareaStyles, x as TextareaPrimitive, xt as FieldDescriptionProps, y as DropdownMenuContentProps, yt as FieldStyles, z as EmptyPrimitive } from "../index-CGY0mO6z.mjs";
1
+ import { $ as SelectPortalProps, A as ToastRootProps, B as EmptyRootProps, C as TextareaPrimitiveProps, D as BadgeStyles, E as BadgeRootProps, F as AvatarRootProps, G as CardPrimitive, H as EmptyTitleProps, I as AvatarStyles, J as CardFooterProps, K as CardRootProps, L as AvatarImageProps, M as ToastDescriptionProps, N as ToastTitleProps, O as BadgeLabelProps, P as AvatarPrimitive, Q as SelectPrimitive, R as AvatarFallbackProps, S as TextareaPrimitiveBaseProps, St as FieldLabelProps, T as BadgePrimitive, U as EmptyMediaProps, V as EmptyStyles, W as EmptyDescriptionProps, X as CardTitleProps, Y as CardBodyProps, Z as CardHeaderProps, _ as DropdownMenuDividerProps, _t as FieldPrimitive, a as PopoverPortalProps, at as SelectOverlayProps, b as DropdownMenuTriggerProps, bt as FieldErrorProps, c as PopoverOverlayProps, ct as ButtonPrimitive, d as PopoverTriggerRef, dt as ButtonStyles, et as SelectRootBaseProps, f as DropdownMenuPrimitive, ft as ButtonPrimitiveLabelProps, g as DropdownMenuOverlayProps, gt as InputStyles, h as DropdownMenuStyles, ht as InputPrimitiveProps, i as PopoverCloseProps, it as SelectContentProps, j as ToastStyles, k as ToastPrimitive, l as PopoverContentProps, lt as ButtonPrimitiveRootProps, m as DropdownMenuRootProps, mt as InputPrimitiveBaseProps, n as PopoverContext, nt as SelectStyles, o as PopoverRootProps, ot as SelectValueProps, p as DropdownMenuPortalProps, pt as InputPrimitive, q as CardStyles, r as usePopover, rt as SelectOptionProps, st as SelectTriggerProps, t as PopoverPrimitive, tt as SelectRootProps, u as PopoverTriggerProps, ut as ButtonState, v as DropdownMenuButtonProps, vt as FieldPrimitiveRootProps, w as TextareaStyles, x as TextareaPrimitive, xt as FieldDescriptionProps, y as DropdownMenuContentProps, yt as FieldStyles, z as EmptyPrimitive } from "../index-pCM7YTs1.mjs";
2
+ import "../use-relative-position-DBzhrBU7.mjs";
2
3
  export { AvatarFallbackProps, AvatarImageProps, AvatarPrimitive, AvatarRootProps, AvatarStyles, BadgeLabelProps, BadgePrimitive, BadgeRootProps, BadgeStyles, ButtonPrimitive, ButtonPrimitiveLabelProps, ButtonPrimitiveRootProps, ButtonState, ButtonStyles, CardBodyProps, CardFooterProps, CardHeaderProps, CardPrimitive, CardRootProps, CardStyles, CardTitleProps, DropdownMenuButtonProps, DropdownMenuContentProps, DropdownMenuDividerProps, DropdownMenuOverlayProps, DropdownMenuPortalProps, DropdownMenuPrimitive, DropdownMenuRootProps, DropdownMenuStyles, DropdownMenuTriggerProps, EmptyDescriptionProps, EmptyMediaProps, EmptyPrimitive, EmptyRootProps, EmptyStyles, EmptyTitleProps, FieldDescriptionProps, FieldErrorProps, FieldLabelProps, FieldPrimitive, FieldPrimitiveRootProps, FieldStyles, InputPrimitive, InputPrimitiveBaseProps, InputPrimitiveProps, InputStyles, PopoverCloseProps, PopoverContentProps, PopoverContext, PopoverOverlayProps, PopoverPortalProps, PopoverPrimitive, PopoverRootProps, PopoverTriggerProps, PopoverTriggerRef, SelectContentProps, SelectOptionProps, SelectOverlayProps, SelectPortalProps, SelectPrimitive, SelectRootBaseProps, SelectRootProps, SelectStyles, SelectTriggerProps, SelectValueProps, TextareaPrimitive, TextareaPrimitiveBaseProps, TextareaPrimitiveProps, TextareaStyles, ToastDescriptionProps, ToastPrimitive, ToastRootProps, ToastStyles, ToastTitleProps, usePopover };
@@ -1,3 +1,4 @@
1
- import { a as BadgePrimitive, c as EmptyPrimitive, d as ButtonPrimitive, f as InputPrimitive, i as TextareaPrimitive, l as CardPrimitive, n as usePopover, o as ToastPrimitive, p as FieldPrimitive, r as DropdownMenuPrimitive, s as AvatarPrimitive, t as PopoverPrimitive, u as SelectPrimitive } from "../primitives-P_8clvQr.mjs";
1
+ import { a as BadgePrimitive, c as EmptyPrimitive, d as ButtonPrimitive, f as InputPrimitive, i as TextareaPrimitive, l as CardPrimitive, n as usePopover, o as ToastPrimitive, p as FieldPrimitive, r as DropdownMenuPrimitive, s as AvatarPrimitive, t as PopoverPrimitive, u as SelectPrimitive } from "../primitives-DNeYBN-3.mjs";
2
+ import "../use-relative-position-BTKEyT1F.mjs";
2
3
 
3
4
  export { AvatarPrimitive, BadgePrimitive, ButtonPrimitive, CardPrimitive, DropdownMenuPrimitive, EmptyPrimitive, FieldPrimitive, InputPrimitive, PopoverPrimitive, SelectPrimitive, TextareaPrimitive, ToastPrimitive, usePopover };
@@ -1,6 +1,6 @@
1
- import * as React$1 from "react";
2
- import React, { createContext, forwardRef, useContext, useEffect, useImperativeHandle, useRef, useState, useSyncExternalStore } from "react";
3
- import { ActivityIndicator, Image, Pressable, StyleSheet, Text, TextInput, View, useWindowDimensions } from "react-native";
1
+ import { n as DEFAULT_POSITION, r as useRelativePosition, t as DEFAULT_LAYOUT } from "./use-relative-position-BTKEyT1F.mjs";
2
+ import React, { createContext, forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useRef, useState, useSyncExternalStore } from "react";
3
+ import { ActivityIndicator, Image, Pressable, StyleSheet, Text, TextInput, View } from "react-native";
4
4
  import { Fragment, jsx } from "react/jsx-runtime";
5
5
 
6
6
  //#region src/primitives/portal/portal.tsx
@@ -752,108 +752,6 @@ const useDropdownMenu = () => {
752
752
  return context;
753
753
  };
754
754
 
755
- //#endregion
756
- //#region src/hooks/useRelativePosition.ts
757
- function useRelativePosition({ align, avoidCollisions, triggerPosition, contentLayout, alignOffset, insets, sideOffset, side }) {
758
- const dimensions = useWindowDimensions();
759
- return React$1.useMemo(() => {
760
- if (!triggerPosition || !contentLayout) return {
761
- position: "absolute",
762
- opacity: 0,
763
- top: dimensions.height,
764
- zIndex: -9999999
765
- };
766
- return getContentStyle({
767
- align,
768
- avoidCollisions,
769
- contentLayout,
770
- side,
771
- triggerPosition,
772
- alignOffset,
773
- insets,
774
- sideOffset,
775
- dimensions
776
- });
777
- }, [
778
- align,
779
- avoidCollisions,
780
- side,
781
- alignOffset,
782
- insets,
783
- triggerPosition,
784
- contentLayout,
785
- dimensions.width,
786
- dimensions.height
787
- ]);
788
- }
789
- const DEFAULT_LAYOUT = {
790
- x: 0,
791
- y: 0,
792
- width: 0,
793
- height: 0
794
- };
795
- const DEFAULT_POSITION = {
796
- height: 0,
797
- width: 0,
798
- pageX: 0,
799
- pageY: 0
800
- };
801
- function getSidePosition({ side, triggerPosition, contentLayout, sideOffset, insets, avoidCollisions, dimensions }) {
802
- const insetTop = insets?.top ?? 0;
803
- const insetBottom = insets?.bottom ?? 0;
804
- const positionTop = triggerPosition?.pageY - sideOffset - contentLayout.height;
805
- const positionBottom = triggerPosition.pageY + triggerPosition.height + sideOffset;
806
- if (!avoidCollisions) return { top: side === "top" ? positionTop : positionBottom };
807
- if (side === "top") return { top: Math.min(Math.max(insetTop, positionTop), dimensions.height - insetBottom - contentLayout.height) };
808
- return { top: Math.min(dimensions.height - insetBottom - contentLayout.height, positionBottom) };
809
- }
810
- function getAlignPosition({ align, avoidCollisions, contentLayout, triggerPosition, alignOffset, insets, dimensions }) {
811
- const insetLeft = insets?.left ?? 0;
812
- const insetRight = insets?.right ?? 0;
813
- const maxContentWidth = dimensions.width - insetLeft - insetRight;
814
- const contentWidth = Math.min(contentLayout.width, maxContentWidth);
815
- let left = getLeftPosition(align, triggerPosition.pageX, triggerPosition.width, contentWidth, alignOffset, insetLeft, insetRight, dimensions);
816
- if (avoidCollisions) {
817
- if (left < insetLeft || left + contentWidth > dimensions.width - insetRight) {
818
- const spaceLeft = left - insetLeft;
819
- const spaceRight = dimensions.width - insetRight - (left + contentWidth);
820
- if (spaceLeft > spaceRight && spaceLeft >= contentWidth) left = insetLeft;
821
- else if (spaceRight >= contentWidth) left = dimensions.width - insetRight - contentWidth;
822
- else left = Math.max(insetLeft, (dimensions.width - contentWidth - insetRight) / 2);
823
- }
824
- }
825
- return {
826
- left,
827
- maxWidth: maxContentWidth
828
- };
829
- }
830
- function getLeftPosition(align, triggerPageX, triggerWidth, contentWidth, alignOffset, insetLeft, insetRight, dimensions) {
831
- let left = 0;
832
- if (align === "start") left = triggerPageX;
833
- if (align === "center") left = triggerPageX + triggerWidth / 2 - contentWidth / 2;
834
- if (align === "end") left = triggerPageX + triggerWidth - contentWidth;
835
- return Math.max(insetLeft, Math.min(left + alignOffset, dimensions.width - contentWidth - insetRight));
836
- }
837
- function getContentStyle({ align, avoidCollisions, contentLayout, side, triggerPosition, alignOffset, insets, sideOffset, dimensions }) {
838
- return Object.assign({ position: "absolute" }, getSidePosition({
839
- side,
840
- triggerPosition,
841
- contentLayout,
842
- sideOffset,
843
- insets,
844
- avoidCollisions,
845
- dimensions
846
- }), getAlignPosition({
847
- align,
848
- avoidCollisions,
849
- triggerPosition,
850
- contentLayout,
851
- alignOffset,
852
- insets,
853
- dimensions
854
- }));
855
- }
856
-
857
755
  //#endregion
858
756
  //#region src/primitives/dropdown-menu/dropdown-menu-root.tsx
859
757
  function DropdownMenuRoot(props) {
@@ -1,4 +1,4 @@
1
- import { o as ToastPrimitive } from "./primitives-P_8clvQr.mjs";
1
+ import { o as ToastPrimitive } from "./primitives-DNeYBN-3.mjs";
2
2
  import React, { createContext, useContext, useEffect, useState, useSyncExternalStore } from "react";
3
3
  import { StyleSheet, View, useColorScheme } from "react-native";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -0,0 +1,33 @@
1
+ //#region src/hooks/use-numeric-mask.d.ts
2
+ type NumericMaskFormat = "currency" | "decimal" | "integer" | "percentage";
3
+ interface UseNumericMaskOptions {
4
+ format?: NumericMaskFormat;
5
+ locale?: string;
6
+ currency?: string;
7
+ precision?: number;
8
+ min?: number;
9
+ max?: number;
10
+ allowNegative?: boolean;
11
+ onChange?: (value: number | null) => void;
12
+ }
13
+ interface UseNumericMaskReturn {
14
+ value: string;
15
+ numericValue: number | null;
16
+ onChangeText: (text: string) => void;
17
+ onBlur: () => void;
18
+ onFocus: () => void;
19
+ keyboardType: "numeric" | "decimal-pad" | "number-pad";
20
+ setValue: (value: number | null) => void;
21
+ }
22
+ declare function useNumericMask({
23
+ format,
24
+ locale,
25
+ currency,
26
+ precision,
27
+ min,
28
+ max,
29
+ allowNegative,
30
+ onChange
31
+ }?: UseNumericMaskOptions): UseNumericMaskReturn;
32
+ //#endregion
33
+ export { useNumericMask as i, UseNumericMaskOptions as n, UseNumericMaskReturn as r, NumericMaskFormat as t };
@@ -0,0 +1,113 @@
1
+ import { useCallback, useState } from "react";
2
+
3
+ //#region src/hooks/use-numeric-mask.ts
4
+ function useNumericMask({ format = "decimal", locale = "en-US", currency = "USD", precision = 2, min, max, allowNegative = true, onChange } = {}) {
5
+ const [numericValue, setNumericValue] = useState(null);
6
+ const [displayValue, setDisplayValue] = useState("");
7
+ const [isFocused, setIsFocused] = useState(false);
8
+ const effectivePrecision = format === "integer" ? 0 : precision;
9
+ const formatValue = useCallback((num) => {
10
+ if (num === null || isNaN(num)) return "";
11
+ switch (format) {
12
+ case "currency": return new Intl.NumberFormat(locale, {
13
+ style: "currency",
14
+ currency,
15
+ minimumFractionDigits: effectivePrecision,
16
+ maximumFractionDigits: effectivePrecision
17
+ }).format(num);
18
+ case "percentage": return new Intl.NumberFormat(locale, {
19
+ style: "percent",
20
+ minimumFractionDigits: effectivePrecision,
21
+ maximumFractionDigits: effectivePrecision
22
+ }).format(num / 100);
23
+ case "integer": return new Intl.NumberFormat(locale, {
24
+ minimumFractionDigits: 0,
25
+ maximumFractionDigits: 0
26
+ }).format(num);
27
+ case "decimal":
28
+ default: return new Intl.NumberFormat(locale, {
29
+ minimumFractionDigits: effectivePrecision,
30
+ maximumFractionDigits: effectivePrecision
31
+ }).format(num);
32
+ }
33
+ }, [
34
+ format,
35
+ locale,
36
+ currency,
37
+ effectivePrecision
38
+ ]);
39
+ const parseValue = useCallback((text) => {
40
+ let cleaned = text.replace(/[^\d.-]/g, "");
41
+ if (!allowNegative) cleaned = cleaned.replace(/-/g, "");
42
+ const parsed = parseFloat(cleaned);
43
+ if (isNaN(parsed) || cleaned === "" || cleaned === "-") return null;
44
+ let constrained = parsed;
45
+ if (min !== void 0 && constrained < min) constrained = min;
46
+ if (max !== void 0 && constrained > max) constrained = max;
47
+ return constrained;
48
+ }, [
49
+ min,
50
+ max,
51
+ allowNegative
52
+ ]);
53
+ const handleChangeText = useCallback((text) => {
54
+ if (isFocused) {
55
+ let cleaned = text.replace(/[^\d.-]/g, "");
56
+ if (!allowNegative && cleaned.includes("-")) return;
57
+ if (allowNegative) {
58
+ if ((cleaned.match(/-/g) || []).length > 1 || cleaned.includes("-") && cleaned.indexOf("-") !== 0) return;
59
+ }
60
+ if (effectivePrecision >= 0) {
61
+ const decimalIndex = cleaned.indexOf(".");
62
+ if (decimalIndex !== -1) {
63
+ if (cleaned.substring(decimalIndex + 1).length > effectivePrecision) return;
64
+ }
65
+ if ((cleaned.match(/\./g) || []).length > 1) return;
66
+ if (format === "integer" && cleaned.includes(".")) return;
67
+ }
68
+ }
69
+ setDisplayValue(text);
70
+ const value = parseValue(text);
71
+ setNumericValue(value);
72
+ onChange?.(value);
73
+ }, [
74
+ parseValue,
75
+ onChange,
76
+ isFocused,
77
+ effectivePrecision,
78
+ allowNegative,
79
+ format
80
+ ]);
81
+ const handleBlur = useCallback(() => {
82
+ setIsFocused(false);
83
+ if (numericValue !== null) setDisplayValue(formatValue(numericValue));
84
+ else setDisplayValue("");
85
+ }, [numericValue, formatValue]);
86
+ const handleFocus = useCallback(() => {
87
+ setIsFocused(true);
88
+ if (numericValue !== null) setDisplayValue(numericValue.toString());
89
+ }, [numericValue]);
90
+ const setValue = useCallback((value) => {
91
+ setNumericValue(value);
92
+ if (value !== null) if (isFocused) setDisplayValue(value.toString());
93
+ else setDisplayValue(formatValue(value));
94
+ else setDisplayValue("");
95
+ onChange?.(value);
96
+ }, [
97
+ isFocused,
98
+ formatValue,
99
+ onChange
100
+ ]);
101
+ return {
102
+ value: displayValue,
103
+ numericValue,
104
+ onChangeText: handleChangeText,
105
+ onBlur: handleBlur,
106
+ onFocus: handleFocus,
107
+ keyboardType: format === "integer" ? allowNegative ? "numeric" : "number-pad" : "decimal-pad",
108
+ setValue
109
+ };
110
+ }
111
+
112
+ //#endregion
113
+ export { useNumericMask as t };
@@ -0,0 +1,106 @@
1
+ import * as React$1 from "react";
2
+ import { useWindowDimensions } from "react-native";
3
+
4
+ //#region src/hooks/use-relative-position.ts
5
+ function useRelativePosition({ align, avoidCollisions, triggerPosition, contentLayout, alignOffset, insets, sideOffset, side }) {
6
+ const dimensions = useWindowDimensions();
7
+ return React$1.useMemo(() => {
8
+ if (!triggerPosition || !contentLayout) return {
9
+ position: "absolute",
10
+ opacity: 0,
11
+ top: dimensions.height,
12
+ zIndex: -9999999
13
+ };
14
+ return getContentStyle({
15
+ align,
16
+ avoidCollisions,
17
+ contentLayout,
18
+ side,
19
+ triggerPosition,
20
+ alignOffset,
21
+ insets,
22
+ sideOffset,
23
+ dimensions
24
+ });
25
+ }, [
26
+ align,
27
+ avoidCollisions,
28
+ side,
29
+ alignOffset,
30
+ insets,
31
+ triggerPosition,
32
+ contentLayout,
33
+ dimensions.width,
34
+ dimensions.height
35
+ ]);
36
+ }
37
+ const DEFAULT_LAYOUT = {
38
+ x: 0,
39
+ y: 0,
40
+ width: 0,
41
+ height: 0
42
+ };
43
+ const DEFAULT_POSITION = {
44
+ height: 0,
45
+ width: 0,
46
+ pageX: 0,
47
+ pageY: 0
48
+ };
49
+ function getSidePosition({ side, triggerPosition, contentLayout, sideOffset, insets, avoidCollisions, dimensions }) {
50
+ const insetTop = insets?.top ?? 0;
51
+ const insetBottom = insets?.bottom ?? 0;
52
+ const positionTop = triggerPosition?.pageY - sideOffset - contentLayout.height;
53
+ const positionBottom = triggerPosition.pageY + triggerPosition.height + sideOffset;
54
+ if (!avoidCollisions) return { top: side === "top" ? positionTop : positionBottom };
55
+ if (side === "top") return { top: Math.min(Math.max(insetTop, positionTop), dimensions.height - insetBottom - contentLayout.height) };
56
+ return { top: Math.min(dimensions.height - insetBottom - contentLayout.height, positionBottom) };
57
+ }
58
+ function getAlignPosition({ align, avoidCollisions, contentLayout, triggerPosition, alignOffset, insets, dimensions }) {
59
+ const insetLeft = insets?.left ?? 0;
60
+ const insetRight = insets?.right ?? 0;
61
+ const maxContentWidth = dimensions.width - insetLeft - insetRight;
62
+ const contentWidth = Math.min(contentLayout.width, maxContentWidth);
63
+ let left = getLeftPosition(align, triggerPosition.pageX, triggerPosition.width, contentWidth, alignOffset, insetLeft, insetRight, dimensions);
64
+ if (avoidCollisions) {
65
+ if (left < insetLeft || left + contentWidth > dimensions.width - insetRight) {
66
+ const spaceLeft = left - insetLeft;
67
+ const spaceRight = dimensions.width - insetRight - (left + contentWidth);
68
+ if (spaceLeft > spaceRight && spaceLeft >= contentWidth) left = insetLeft;
69
+ else if (spaceRight >= contentWidth) left = dimensions.width - insetRight - contentWidth;
70
+ else left = Math.max(insetLeft, (dimensions.width - contentWidth - insetRight) / 2);
71
+ }
72
+ }
73
+ return {
74
+ left,
75
+ maxWidth: maxContentWidth
76
+ };
77
+ }
78
+ function getLeftPosition(align, triggerPageX, triggerWidth, contentWidth, alignOffset, insetLeft, insetRight, dimensions) {
79
+ let left = 0;
80
+ if (align === "start") left = triggerPageX;
81
+ if (align === "center") left = triggerPageX + triggerWidth / 2 - contentWidth / 2;
82
+ if (align === "end") left = triggerPageX + triggerWidth - contentWidth;
83
+ return Math.max(insetLeft, Math.min(left + alignOffset, dimensions.width - contentWidth - insetRight));
84
+ }
85
+ function getContentStyle({ align, avoidCollisions, contentLayout, side, triggerPosition, alignOffset, insets, sideOffset, dimensions }) {
86
+ return Object.assign({ position: "absolute" }, getSidePosition({
87
+ side,
88
+ triggerPosition,
89
+ contentLayout,
90
+ sideOffset,
91
+ insets,
92
+ avoidCollisions,
93
+ dimensions
94
+ }), getAlignPosition({
95
+ align,
96
+ avoidCollisions,
97
+ triggerPosition,
98
+ contentLayout,
99
+ alignOffset,
100
+ insets,
101
+ dimensions
102
+ }));
103
+ }
104
+
105
+ //#endregion
106
+ export { DEFAULT_POSITION as n, useRelativePosition as r, DEFAULT_LAYOUT as t };
@@ -0,0 +1,61 @@
1
+ import { LayoutRectangle, ScaledSize } from "react-native";
2
+
3
+ //#region src/hooks/use-relative-position.d.ts
4
+ interface Insets {
5
+ top?: number;
6
+ bottom?: number;
7
+ left?: number;
8
+ right?: number;
9
+ }
10
+ type UseRelativePositionArgs = Omit<GetContentStyleArgs, "triggerPosition" | "contentLayout" | "dimensions"> & {
11
+ triggerPosition: LayoutPosition | null;
12
+ contentLayout: LayoutRectangle | null;
13
+ };
14
+ declare function useRelativePosition({
15
+ align,
16
+ avoidCollisions,
17
+ triggerPosition,
18
+ contentLayout,
19
+ alignOffset,
20
+ insets,
21
+ sideOffset,
22
+ side
23
+ }: UseRelativePositionArgs): ({
24
+ readonly position: "absolute";
25
+ } & {
26
+ top: number;
27
+ } & {
28
+ left: number;
29
+ maxWidth: number;
30
+ }) | {
31
+ readonly position: "absolute";
32
+ readonly opacity: 0;
33
+ readonly top: number;
34
+ readonly zIndex: -9999999;
35
+ };
36
+ interface LayoutPosition {
37
+ pageY: number;
38
+ pageX: number;
39
+ width: number;
40
+ height: number;
41
+ }
42
+ interface GetPositionArgs {
43
+ dimensions: ScaledSize;
44
+ avoidCollisions: boolean;
45
+ triggerPosition: LayoutPosition;
46
+ contentLayout: LayoutRectangle;
47
+ insets?: Insets;
48
+ }
49
+ interface GetSidePositionArgs extends GetPositionArgs {
50
+ side: "top" | "bottom";
51
+ sideOffset: number;
52
+ }
53
+ declare const DEFAULT_LAYOUT: LayoutRectangle;
54
+ declare const DEFAULT_POSITION: LayoutPosition;
55
+ interface GetAlignPositionArgs extends GetPositionArgs {
56
+ align: "start" | "center" | "end";
57
+ alignOffset: number;
58
+ }
59
+ type GetContentStyleArgs = GetPositionArgs & GetSidePositionArgs & GetAlignPositionArgs;
60
+ //#endregion
61
+ export { useRelativePosition as i, DEFAULT_POSITION as n, LayoutPosition as r, DEFAULT_LAYOUT as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@korsolutions/ui",
3
- "version": "0.0.20",
3
+ "version": "0.0.21",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.mjs",
6
6
  "module": "dist/index.mjs",
@@ -1,6 +1,6 @@
1
1
  export * from "./button/button";
2
2
  export * from "./card/card";
3
- export * from "./input/input";
3
+ export * from "./input";
4
4
  export * from "./field/field";
5
5
  export * from "./select/select";
6
6
  export * from "./typography/typography";
@@ -0,0 +1,2 @@
1
+ export * from "./input";
2
+ export * from "./numeric-input";
@@ -0,0 +1,73 @@
1
+ import React, { useEffect } from "react";
2
+ import { Input } from "./input";
3
+ import { InputPrimitiveBaseProps } from "@/primitives";
4
+ import { useNumericMask, NumericMaskFormat } from "@/hooks/use-numeric-mask";
5
+
6
+ export interface NumericInputProps extends Omit<InputPrimitiveBaseProps, "value" | "onChange" | "keyboardType"> {
7
+ variant?: "default";
8
+ value?: number | null;
9
+ onChange?: (value: number | null) => void;
10
+ format?: NumericMaskFormat;
11
+ locale?: string;
12
+ currency?: string;
13
+ precision?: number;
14
+ min?: number;
15
+ max?: number;
16
+ allowNegative?: boolean;
17
+ }
18
+
19
+ export function NumericInput({
20
+ value,
21
+ onChange,
22
+ format = "decimal",
23
+ locale = "en-US",
24
+ currency = "USD",
25
+ precision = 2,
26
+ min,
27
+ max,
28
+ allowNegative = true,
29
+ variant = "default",
30
+ onBlur,
31
+ onFocus,
32
+ ...props
33
+ }: NumericInputProps) {
34
+ const numericMask = useNumericMask({
35
+ format,
36
+ locale,
37
+ currency,
38
+ precision,
39
+ min,
40
+ max,
41
+ allowNegative,
42
+ onChange,
43
+ });
44
+
45
+ // Sync external value changes with internal state
46
+ useEffect(() => {
47
+ if (value !== numericMask.numericValue) {
48
+ numericMask.setValue(value ?? null);
49
+ }
50
+ }, [value]);
51
+
52
+ const handleBlur = (e: any) => {
53
+ numericMask.onBlur();
54
+ onBlur?.(e);
55
+ };
56
+
57
+ const handleFocus = (e: any) => {
58
+ numericMask.onFocus();
59
+ onFocus?.(e);
60
+ };
61
+
62
+ return (
63
+ <Input
64
+ {...props}
65
+ variant={variant}
66
+ value={numericMask.value}
67
+ onChange={numericMask.onChangeText}
68
+ onBlur={handleBlur}
69
+ onFocus={handleFocus}
70
+ keyboardType={numericMask.keyboardType}
71
+ />
72
+ );
73
+ }
@@ -1 +1,4 @@
1
- export * from "./useScreenSize";
1
+ export * from "./use-screen-size";
2
+ export * from "./use-currency-mask";
3
+ export * from "./use-numeric-mask";
4
+ export * from "./use-relative-position";
@@ -0,0 +1,141 @@
1
+ import { useState, useCallback } from "react";
2
+
3
+ export interface UseCurrencyMaskOptions {
4
+ locale?: string;
5
+ currency?: string;
6
+ precision?: number;
7
+ min?: number;
8
+ max?: number;
9
+ onValueChange?: (value: number | null) => void;
10
+ }
11
+
12
+ export interface UseCurrencyMaskReturn {
13
+ value: string;
14
+ numericValue: number | null;
15
+ onChangeText: (text: string) => void;
16
+ onBlur: () => void;
17
+ onFocus: () => void;
18
+ keyboardType: "decimal-pad";
19
+ setValue: (value: number | null) => void;
20
+ }
21
+
22
+ export function useCurrencyMask({
23
+ locale = "en-US",
24
+ currency = "USD",
25
+ precision = 2,
26
+ min,
27
+ max,
28
+ onValueChange,
29
+ }: UseCurrencyMaskOptions = {}): UseCurrencyMaskReturn {
30
+ const [numericValue, setNumericValue] = useState<number | null>(null);
31
+ const [displayValue, setDisplayValue] = useState("");
32
+ const [isFocused, setIsFocused] = useState(false);
33
+
34
+ const formatCurrency = useCallback(
35
+ (num: number | null): string => {
36
+ if (num === null || isNaN(num)) return "";
37
+
38
+ return new Intl.NumberFormat(locale, {
39
+ style: "currency",
40
+ currency,
41
+ minimumFractionDigits: precision,
42
+ maximumFractionDigits: precision,
43
+ }).format(num);
44
+ },
45
+ [locale, currency, precision]
46
+ );
47
+
48
+ const parseCurrency = useCallback(
49
+ (text: string): number | null => {
50
+ // Remove currency symbols, spaces, and thousand separators
51
+ const cleaned = text.replace(/[^\d.-]/g, "");
52
+ const parsed = parseFloat(cleaned);
53
+
54
+ if (isNaN(parsed) || cleaned === "") return null;
55
+
56
+ // Apply min/max constraints
57
+ let constrained = parsed;
58
+ if (min !== undefined && constrained < min) constrained = min;
59
+ if (max !== undefined && constrained > max) constrained = max;
60
+
61
+ return constrained;
62
+ },
63
+ [min, max]
64
+ );
65
+
66
+ const handleChangeText = useCallback(
67
+ (text: string) => {
68
+ // When focused, validate decimal precision before allowing input
69
+ if (isFocused) {
70
+ // Remove currency symbols and thousand separators to get raw input
71
+ const cleaned = text.replace(/[^\d.-]/g, "");
72
+
73
+ // Check if input has a decimal point
74
+ const decimalIndex = cleaned.indexOf(".");
75
+ if (decimalIndex !== -1) {
76
+ const decimalPart = cleaned.substring(decimalIndex + 1);
77
+
78
+ // Prevent typing more decimals than allowed precision
79
+ if (decimalPart.length > precision) {
80
+ return; // Don't update state if exceeds precision
81
+ }
82
+ }
83
+
84
+ // Also prevent multiple decimal points
85
+ const decimalCount = (cleaned.match(/\./g) || []).length;
86
+ if (decimalCount > 1) {
87
+ return;
88
+ }
89
+ }
90
+
91
+ setDisplayValue(text);
92
+ const value = parseCurrency(text);
93
+ setNumericValue(value);
94
+ onValueChange?.(value);
95
+ },
96
+ [parseCurrency, onValueChange, isFocused, precision]
97
+ );
98
+
99
+ const handleBlur = useCallback(() => {
100
+ setIsFocused(false);
101
+ if (numericValue !== null) {
102
+ setDisplayValue(formatCurrency(numericValue));
103
+ } else {
104
+ setDisplayValue("");
105
+ }
106
+ }, [numericValue, formatCurrency]);
107
+
108
+ const handleFocus = useCallback(() => {
109
+ setIsFocused(true);
110
+ if (numericValue !== null) {
111
+ setDisplayValue(numericValue.toString());
112
+ }
113
+ }, [numericValue]);
114
+
115
+ const setValue = useCallback(
116
+ (value: number | null) => {
117
+ setNumericValue(value);
118
+ if (value !== null) {
119
+ if (isFocused) {
120
+ setDisplayValue(value.toString());
121
+ } else {
122
+ setDisplayValue(formatCurrency(value));
123
+ }
124
+ } else {
125
+ setDisplayValue("");
126
+ }
127
+ onValueChange?.(value);
128
+ },
129
+ [isFocused, formatCurrency, onValueChange]
130
+ );
131
+
132
+ return {
133
+ value: displayValue,
134
+ numericValue,
135
+ onChangeText: handleChangeText,
136
+ onBlur: handleBlur,
137
+ onFocus: handleFocus,
138
+ keyboardType: "decimal-pad",
139
+ setValue,
140
+ };
141
+ }
@@ -0,0 +1,202 @@
1
+ import { useState, useCallback } from "react";
2
+
3
+ export type NumericMaskFormat = "currency" | "decimal" | "integer" | "percentage";
4
+
5
+ export interface UseNumericMaskOptions {
6
+ format?: NumericMaskFormat;
7
+ locale?: string;
8
+ currency?: string;
9
+ precision?: number;
10
+ min?: number;
11
+ max?: number;
12
+ allowNegative?: boolean;
13
+ onChange?: (value: number | null) => void;
14
+ }
15
+
16
+ export interface UseNumericMaskReturn {
17
+ value: string;
18
+ numericValue: number | null;
19
+ onChangeText: (text: string) => void;
20
+ onBlur: () => void;
21
+ onFocus: () => void;
22
+ keyboardType: "numeric" | "decimal-pad" | "number-pad";
23
+ setValue: (value: number | null) => void;
24
+ }
25
+
26
+ export function useNumericMask({
27
+ format = "decimal",
28
+ locale = "en-US",
29
+ currency = "USD",
30
+ precision = 2,
31
+ min,
32
+ max,
33
+ allowNegative = true,
34
+ onChange,
35
+ }: UseNumericMaskOptions = {}): UseNumericMaskReturn {
36
+ const [numericValue, setNumericValue] = useState<number | null>(null);
37
+ const [displayValue, setDisplayValue] = useState("");
38
+ const [isFocused, setIsFocused] = useState(false);
39
+
40
+ // Determine actual precision based on format
41
+ const effectivePrecision = format === "integer" ? 0 : precision;
42
+
43
+ const formatValue = useCallback(
44
+ (num: number | null): string => {
45
+ if (num === null || isNaN(num)) return "";
46
+
47
+ switch (format) {
48
+ case "currency":
49
+ return new Intl.NumberFormat(locale, {
50
+ style: "currency",
51
+ currency,
52
+ minimumFractionDigits: effectivePrecision,
53
+ maximumFractionDigits: effectivePrecision,
54
+ }).format(num);
55
+
56
+ case "percentage":
57
+ return new Intl.NumberFormat(locale, {
58
+ style: "percent",
59
+ minimumFractionDigits: effectivePrecision,
60
+ maximumFractionDigits: effectivePrecision,
61
+ }).format(num / 100);
62
+
63
+ case "integer":
64
+ return new Intl.NumberFormat(locale, {
65
+ minimumFractionDigits: 0,
66
+ maximumFractionDigits: 0,
67
+ }).format(num);
68
+
69
+ case "decimal":
70
+ default:
71
+ return new Intl.NumberFormat(locale, {
72
+ minimumFractionDigits: effectivePrecision,
73
+ maximumFractionDigits: effectivePrecision,
74
+ }).format(num);
75
+ }
76
+ },
77
+ [format, locale, currency, effectivePrecision]
78
+ );
79
+
80
+ const parseValue = useCallback(
81
+ (text: string): number | null => {
82
+ // Remove currency symbols, spaces, thousand separators, and percentage signs
83
+ let cleaned = text.replace(/[^\d.-]/g, "");
84
+
85
+ // Handle negative sign
86
+ if (!allowNegative) {
87
+ cleaned = cleaned.replace(/-/g, "");
88
+ }
89
+
90
+ const parsed = parseFloat(cleaned);
91
+
92
+ if (isNaN(parsed) || cleaned === "" || cleaned === "-") return null;
93
+
94
+ // Apply min/max constraints
95
+ let constrained = parsed;
96
+ if (min !== undefined && constrained < min) constrained = min;
97
+ if (max !== undefined && constrained > max) constrained = max;
98
+
99
+ return constrained;
100
+ },
101
+ [min, max, allowNegative]
102
+ );
103
+
104
+ const handleChangeText = useCallback(
105
+ (text: string) => {
106
+ // When focused, validate input before allowing it
107
+ if (isFocused) {
108
+ // Remove formatting characters to get raw input
109
+ let cleaned = text.replace(/[^\d.-]/g, "");
110
+
111
+ // Validate negative sign
112
+ if (!allowNegative && cleaned.includes("-")) {
113
+ return;
114
+ }
115
+
116
+ // Ensure negative sign is only at the start
117
+ if (allowNegative) {
118
+ const negativeCount = (cleaned.match(/-/g) || []).length;
119
+ if (negativeCount > 1 || (cleaned.includes("-") && cleaned.indexOf("-") !== 0)) {
120
+ return;
121
+ }
122
+ }
123
+
124
+ // Check decimal precision (skip for integer format)
125
+ if (effectivePrecision >= 0) {
126
+ const decimalIndex = cleaned.indexOf(".");
127
+ if (decimalIndex !== -1) {
128
+ const decimalPart = cleaned.substring(decimalIndex + 1);
129
+
130
+ // Prevent typing more decimals than allowed precision
131
+ if (decimalPart.length > effectivePrecision) {
132
+ return;
133
+ }
134
+ }
135
+
136
+ // Prevent multiple decimal points
137
+ const decimalCount = (cleaned.match(/\./g) || []).length;
138
+ if (decimalCount > 1) {
139
+ return;
140
+ }
141
+
142
+ // Prevent decimal point for integer format
143
+ if (format === "integer" && cleaned.includes(".")) {
144
+ return;
145
+ }
146
+ }
147
+ }
148
+
149
+ setDisplayValue(text);
150
+ const value = parseValue(text);
151
+ setNumericValue(value);
152
+ onChange?.(value);
153
+ },
154
+ [parseValue, onChange, isFocused, effectivePrecision, allowNegative, format]
155
+ );
156
+
157
+ const handleBlur = useCallback(() => {
158
+ setIsFocused(false);
159
+ if (numericValue !== null) {
160
+ setDisplayValue(formatValue(numericValue));
161
+ } else {
162
+ setDisplayValue("");
163
+ }
164
+ }, [numericValue, formatValue]);
165
+
166
+ const handleFocus = useCallback(() => {
167
+ setIsFocused(true);
168
+ if (numericValue !== null) {
169
+ setDisplayValue(numericValue.toString());
170
+ }
171
+ }, [numericValue]);
172
+
173
+ const setValue = useCallback(
174
+ (value: number | null) => {
175
+ setNumericValue(value);
176
+ if (value !== null) {
177
+ if (isFocused) {
178
+ setDisplayValue(value.toString());
179
+ } else {
180
+ setDisplayValue(formatValue(value));
181
+ }
182
+ } else {
183
+ setDisplayValue("");
184
+ }
185
+ onChange?.(value);
186
+ },
187
+ [isFocused, formatValue, onChange]
188
+ );
189
+
190
+ // Determine keyboard type based on format
191
+ const keyboardType = format === "integer" ? (allowNegative ? ("numeric" as const) : ("number-pad" as const)) : ("decimal-pad" as const);
192
+
193
+ return {
194
+ value: displayValue,
195
+ numericValue,
196
+ onChangeText: handleChangeText,
197
+ onBlur: handleBlur,
198
+ onFocus: handleFocus,
199
+ keyboardType,
200
+ setValue,
201
+ };
202
+ }
@@ -1,7 +1,7 @@
1
1
  import { createContext, Dispatch, useContext } from "react";
2
2
  import { LayoutRectangle } from "react-native";
3
3
  import { DropdownMenuStyles } from "./types";
4
- import { LayoutPosition } from "@/hooks/useRelativePosition";
4
+ import { LayoutPosition } from "@/hooks/use-relative-position";
5
5
 
6
6
  export interface DropdownMenuContext {
7
7
  isOpen: boolean;
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import { StyleProp, View, ViewStyle } from "react-native";
3
3
  import { useDropdownMenu } from "./context";
4
- import { useRelativePosition } from "@/hooks/useRelativePosition";
4
+ import { useRelativePosition } from "@/hooks/use-relative-position";
5
5
 
6
6
  export interface DropdownMenuContentProps {
7
7
  children?: React.ReactNode;
@@ -2,7 +2,7 @@ import React, { useState } from "react";
2
2
  import { LayoutRectangle } from "react-native";
3
3
  import { DropdownMenuStyles } from "./types";
4
4
  import { DropdownMenuContext } from "./context";
5
- import { DEFAULT_LAYOUT, DEFAULT_POSITION, LayoutPosition } from "@/hooks/useRelativePosition";
5
+ import { DEFAULT_LAYOUT, DEFAULT_POSITION, LayoutPosition } from "@/hooks/use-relative-position";
6
6
 
7
7
  export interface DropdownMenuRootProps {
8
8
  children?: React.ReactNode;
@@ -1,7 +1,7 @@
1
1
  import { createContext, Dispatch, useContext } from "react";
2
2
  import { PopoverStyles } from "./types";
3
3
  import { LayoutRectangle } from "react-native";
4
- import { LayoutPosition } from "@/hooks/useRelativePosition";
4
+ import { LayoutPosition } from "@/hooks/use-relative-position";
5
5
 
6
6
  export interface PopoverContext {
7
7
  isOpen: boolean;
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import { StyleProp, View, ViewStyle } from "react-native";
3
3
  import { usePopover } from "./context";
4
- import { useRelativePosition } from "@/hooks/useRelativePosition";
4
+ import { useRelativePosition } from "@/hooks/use-relative-position";
5
5
 
6
6
  export interface PopoverContentProps {
7
7
  children?: React.ReactNode;
@@ -2,7 +2,7 @@ import React, { useState } from "react";
2
2
  import { PopoverContext } from "./context";
3
3
  import { PopoverStyles } from "./types";
4
4
  import { LayoutRectangle } from "react-native";
5
- import { DEFAULT_LAYOUT, DEFAULT_POSITION, LayoutPosition } from "@/hooks/useRelativePosition";
5
+ import { DEFAULT_LAYOUT, DEFAULT_POSITION, LayoutPosition } from "@/hooks/use-relative-position";
6
6
 
7
7
  export interface PopoverRootProps {
8
8
  children?: React.ReactNode;