@korsolutions/ui 0.0.19 → 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.
- package/dist/components/index.d.mts +73 -4
- package/dist/components/index.mjs +156 -8
- package/dist/hooks/index.d.mts +32 -2
- package/dist/hooks/index.mjs +79 -2
- package/dist/{index-BLsiF42Z.d.mts → index-pCM7YTs1.d.mts} +165 -9
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +3 -2
- package/dist/primitives/index.d.mts +3 -2
- package/dist/primitives/index.mjs +3 -2
- package/dist/{primitives-CyDqzNcp.mjs → primitives-DNeYBN-3.mjs} +330 -12
- package/dist/{toast-manager-BOORCQn8.mjs → toast-manager-BfoJ-_dB.mjs} +1 -1
- package/dist/use-numeric-mask-B9WZG25o.d.mts +33 -0
- package/dist/use-numeric-mask-BQlz1Pus.mjs +113 -0
- package/dist/use-relative-position-BTKEyT1F.mjs +106 -0
- package/dist/use-relative-position-DBzhrBU7.d.mts +61 -0
- package/package.json +1 -1
- package/src/components/button/button.tsx +7 -4
- package/src/components/dropdown-menu/dropdown-menu.tsx +49 -0
- package/src/components/dropdown-menu/variants/default.tsx +40 -0
- package/src/components/dropdown-menu/variants/index.ts +5 -0
- package/src/components/index.ts +3 -1
- package/src/components/input/index.ts +2 -0
- package/src/components/input/numeric-input.tsx +73 -0
- package/src/components/popover/popover.tsx +51 -0
- package/src/components/popover/variants/default.tsx +26 -0
- package/src/components/popover/variants/index.ts +5 -0
- package/src/hooks/index.ts +4 -1
- package/src/hooks/use-currency-mask.ts +141 -0
- package/src/hooks/use-numeric-mask.ts +202 -0
- package/src/hooks/use-relative-position.ts +188 -0
- package/src/primitives/button/button-root.tsx +2 -4
- package/src/primitives/dropdown-menu/context.ts +25 -0
- package/src/primitives/dropdown-menu/dropdown-menu-button.tsx +47 -0
- package/src/primitives/dropdown-menu/dropdown-menu-content.tsx +39 -0
- package/src/primitives/dropdown-menu/dropdown-menu-divider.tsx +18 -0
- package/src/primitives/dropdown-menu/dropdown-menu-overlay.tsx +29 -0
- package/src/primitives/dropdown-menu/dropdown-menu-portal.tsx +21 -0
- package/src/primitives/dropdown-menu/dropdown-menu-root.tsx +35 -0
- package/src/primitives/dropdown-menu/dropdown-menu-trigger.tsx +47 -0
- package/src/primitives/dropdown-menu/index.ts +26 -0
- package/src/primitives/dropdown-menu/types.ts +13 -0
- package/src/primitives/index.ts +2 -0
- package/src/primitives/popover/context.ts +25 -0
- package/src/primitives/popover/index.ts +24 -0
- package/src/primitives/popover/popover-close.tsx +29 -0
- package/src/primitives/popover/popover-content.tsx +39 -0
- package/src/primitives/popover/popover-overlay.tsx +37 -0
- package/src/primitives/popover/popover-portal.tsx +21 -0
- package/src/primitives/popover/popover-root.tsx +35 -0
- package/src/primitives/popover/popover-trigger.tsx +47 -0
- package/src/primitives/popover/types.ts +7 -0
- package/src/utils/get-ref-layout.ts +16 -0
- /package/src/hooks/{useScreenSize.ts → use-screen-size.ts} +0 -0
|
@@ -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,10 +1,12 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { ButtonPrimitive } from "@/primitives";
|
|
2
|
+
import { ButtonPrimitive, ButtonPrimitiveRootProps } from "@/primitives";
|
|
3
3
|
import { ButtonVariants } from "./variants";
|
|
4
4
|
|
|
5
5
|
interface ButtonProps {
|
|
6
|
-
onPress?: () => void;
|
|
7
6
|
children?: string;
|
|
7
|
+
|
|
8
|
+
onPress?: ButtonPrimitiveRootProps["onPress"];
|
|
9
|
+
onLayout?: ButtonPrimitiveRootProps["onLayout"];
|
|
8
10
|
isDisabled?: boolean;
|
|
9
11
|
isLoading?: boolean;
|
|
10
12
|
|
|
@@ -12,11 +14,12 @@ interface ButtonProps {
|
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export function Button(props: ButtonProps) {
|
|
15
|
-
const
|
|
17
|
+
const { children, variant = "default", ...rootProps } = props;
|
|
18
|
+
const useVariantStyles = ButtonVariants[variant];
|
|
16
19
|
const variantStyles = useVariantStyles();
|
|
17
20
|
|
|
18
21
|
return (
|
|
19
|
-
<ButtonPrimitive.Root
|
|
22
|
+
<ButtonPrimitive.Root {...rootProps} styles={variantStyles}>
|
|
20
23
|
{props.isLoading && <ButtonPrimitive.Spinner />}
|
|
21
24
|
<ButtonPrimitive.Label>{props.children}</ButtonPrimitive.Label>
|
|
22
25
|
</ButtonPrimitive.Root>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { DropdownMenuPrimitive } from "@/primitives";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { DropdownMenuVariants } from "./variants";
|
|
4
|
+
import { PressableProps } from "react-native";
|
|
5
|
+
|
|
6
|
+
type DropdownMenuOption =
|
|
7
|
+
| {
|
|
8
|
+
type: "button";
|
|
9
|
+
label: string;
|
|
10
|
+
onPress: () => void;
|
|
11
|
+
}
|
|
12
|
+
| {
|
|
13
|
+
type: "divider";
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
interface DropdownMenuProps {
|
|
17
|
+
trigger: React.ReactElement<PressableProps>;
|
|
18
|
+
options: DropdownMenuOption[];
|
|
19
|
+
|
|
20
|
+
variant?: keyof typeof DropdownMenuVariants;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function DropdownMenu(props: DropdownMenuProps) {
|
|
24
|
+
const useVariantStyles = DropdownMenuVariants[props.variant || "default"];
|
|
25
|
+
const styles = useVariantStyles();
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<DropdownMenuPrimitive.Root styles={styles}>
|
|
29
|
+
<DropdownMenuPrimitive.Trigger>{props.trigger}</DropdownMenuPrimitive.Trigger>
|
|
30
|
+
<DropdownMenuPrimitive.Portal>
|
|
31
|
+
<DropdownMenuPrimitive.Overlay>
|
|
32
|
+
<DropdownMenuPrimitive.Content>
|
|
33
|
+
{props.options.map((option) => {
|
|
34
|
+
if (option.type === "button") {
|
|
35
|
+
return (
|
|
36
|
+
<DropdownMenuPrimitive.Button onPress={option.onPress} key={option.label}>
|
|
37
|
+
{option.label}
|
|
38
|
+
</DropdownMenuPrimitive.Button>
|
|
39
|
+
);
|
|
40
|
+
} else if (option.type === "divider") {
|
|
41
|
+
return <DropdownMenuPrimitive.Divider key={Math.random().toString()} />;
|
|
42
|
+
}
|
|
43
|
+
})}
|
|
44
|
+
</DropdownMenuPrimitive.Content>
|
|
45
|
+
</DropdownMenuPrimitive.Overlay>
|
|
46
|
+
</DropdownMenuPrimitive.Portal>
|
|
47
|
+
</DropdownMenuPrimitive.Root>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { DropdownMenuStyles } from "@/primitives";
|
|
2
|
+
import { useThemedStyles } from "@/utils/use-themed-styles";
|
|
3
|
+
|
|
4
|
+
export const useDropdownMenuVariantDefault = (): DropdownMenuStyles => {
|
|
5
|
+
return useThemedStyles(
|
|
6
|
+
({ colors, radius, fontFamily, fontSize }): DropdownMenuStyles => ({
|
|
7
|
+
overlay: {
|
|
8
|
+
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
9
|
+
},
|
|
10
|
+
content: {
|
|
11
|
+
overflow: "hidden",
|
|
12
|
+
backgroundColor: colors.surface,
|
|
13
|
+
borderRadius: radius,
|
|
14
|
+
borderWidth: 1,
|
|
15
|
+
borderColor: colors.border,
|
|
16
|
+
shadowColor: "#000",
|
|
17
|
+
shadowOffset: { width: 0, height: 4 },
|
|
18
|
+
shadowOpacity: 0.15,
|
|
19
|
+
shadowRadius: 12,
|
|
20
|
+
elevation: 8,
|
|
21
|
+
},
|
|
22
|
+
button: {
|
|
23
|
+
default: {
|
|
24
|
+
paddingVertical: 12,
|
|
25
|
+
paddingHorizontal: 16,
|
|
26
|
+
fontFamily: fontFamily,
|
|
27
|
+
fontSize: fontSize,
|
|
28
|
+
color: colors.foreground,
|
|
29
|
+
},
|
|
30
|
+
hovered: {
|
|
31
|
+
backgroundColor: colors.muted,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
divider: {
|
|
35
|
+
height: 1,
|
|
36
|
+
backgroundColor: colors.border,
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
};
|
package/src/components/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * from "./button/button";
|
|
2
2
|
export * from "./card/card";
|
|
3
|
-
export * from "./input
|
|
3
|
+
export * from "./input";
|
|
4
4
|
export * from "./field/field";
|
|
5
5
|
export * from "./select/select";
|
|
6
6
|
export * from "./typography/typography";
|
|
@@ -10,3 +10,5 @@ export * from "./avatar/avatar";
|
|
|
10
10
|
export * from "./toast/index";
|
|
11
11
|
export * from "./badge/badge";
|
|
12
12
|
export * from "./textarea/textarea";
|
|
13
|
+
export * from "./dropdown-menu/dropdown-menu";
|
|
14
|
+
export * from "./popover/popover";
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { PopoverPrimitive, usePopover } from "@/primitives";
|
|
2
|
+
import React, { forwardRef } from "react";
|
|
3
|
+
import { PopoverVariants } from "./variants";
|
|
4
|
+
import type { PopoverTriggerRef } from "@/primitives";
|
|
5
|
+
import { PressableProps } from "react-native";
|
|
6
|
+
|
|
7
|
+
interface PopoverChildrenProps {
|
|
8
|
+
close: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PopoverProps {
|
|
12
|
+
children: ((props: PopoverChildrenProps) => React.ReactNode) | React.ReactNode;
|
|
13
|
+
trigger: React.ReactElement<PressableProps>;
|
|
14
|
+
closeOnOverlayPress?: boolean;
|
|
15
|
+
|
|
16
|
+
variant?: keyof typeof PopoverVariants;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const PopoverContentComponent = (props: { children: PopoverProps["children"] }) => {
|
|
20
|
+
const popover = usePopover();
|
|
21
|
+
if (typeof props.children === "function") {
|
|
22
|
+
return (
|
|
23
|
+
<PopoverPrimitive.Content>
|
|
24
|
+
{props.children({
|
|
25
|
+
close: () => popover.setIsOpen(false),
|
|
26
|
+
})}
|
|
27
|
+
</PopoverPrimitive.Content>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return <PopoverPrimitive.Content>{props.children}</PopoverPrimitive.Content>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const PopoverComponent = forwardRef<PopoverTriggerRef, PopoverProps>((props, ref) => {
|
|
34
|
+
const useVariantStyles = PopoverVariants[props.variant || "default"];
|
|
35
|
+
const styles = useVariantStyles();
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<PopoverPrimitive.Root styles={styles}>
|
|
39
|
+
<PopoverPrimitive.Trigger ref={ref}>{props.trigger}</PopoverPrimitive.Trigger>
|
|
40
|
+
<PopoverPrimitive.Portal>
|
|
41
|
+
<PopoverPrimitive.Overlay closeOnPress={props.closeOnOverlayPress}>
|
|
42
|
+
<PopoverContentComponent>{props.children}</PopoverContentComponent>
|
|
43
|
+
</PopoverPrimitive.Overlay>
|
|
44
|
+
</PopoverPrimitive.Portal>
|
|
45
|
+
</PopoverPrimitive.Root>
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
PopoverComponent.displayName = "Popover";
|
|
50
|
+
|
|
51
|
+
export const Popover = PopoverComponent;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { PopoverStyles } from "@/primitives/popover/types";
|
|
2
|
+
import { useThemedStyles } from "@/utils/use-themed-styles";
|
|
3
|
+
|
|
4
|
+
export const usePopoverVariantDefault = (): PopoverStyles => {
|
|
5
|
+
return useThemedStyles(
|
|
6
|
+
({ colors, radius }): PopoverStyles => ({
|
|
7
|
+
overlay: {
|
|
8
|
+
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
9
|
+
},
|
|
10
|
+
content: {
|
|
11
|
+
backgroundColor: colors.surface,
|
|
12
|
+
borderRadius: radius,
|
|
13
|
+
borderWidth: 1,
|
|
14
|
+
borderColor: colors.border,
|
|
15
|
+
padding: 16,
|
|
16
|
+
minWidth: 280,
|
|
17
|
+
maxWidth: 320,
|
|
18
|
+
shadowColor: "#000",
|
|
19
|
+
shadowOffset: { width: 0, height: 4 },
|
|
20
|
+
shadowOpacity: 0.15,
|
|
21
|
+
shadowRadius: 12,
|
|
22
|
+
elevation: 8,
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
);
|
|
26
|
+
};
|
package/src/hooks/index.ts
CHANGED
|
@@ -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
|
+
}
|