@korsolutions/ui 0.0.18 → 0.0.20
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 +70 -4
- package/dist/components/index.mjs +224 -8
- package/dist/{index-Bfae0NgJ.d.mts → index-CGY0mO6z.d.mts} +216 -8
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +2 -2
- package/dist/primitives/index.d.mts +2 -2
- package/dist/primitives/index.mjs +2 -2
- package/dist/{primitives-B4L9y32H.mjs → primitives-P_8clvQr.mjs} +513 -11
- package/dist/{toast-manager-Vq38WK76.mjs → toast-manager-DSo9oN8w.mjs} +1 -1
- package/package.json +1 -1
- package/src/components/badge/badge.tsx +23 -0
- package/src/components/badge/variants/default.tsx +26 -0
- package/src/components/badge/variants/index.ts +7 -0
- package/src/components/badge/variants/secondary.tsx +26 -0
- 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 +4 -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/components/textarea/textarea.tsx +14 -0
- package/src/components/textarea/variants/default.tsx +38 -0
- package/src/components/textarea/variants/index.ts +5 -0
- package/src/hooks/useRelativePosition.ts +188 -0
- package/src/primitives/badge/badge-label.tsx +21 -0
- package/src/primitives/badge/badge-root.tsx +30 -0
- package/src/primitives/badge/context.ts +17 -0
- package/src/primitives/badge/index.ts +11 -0
- package/src/primitives/badge/types.ts +9 -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 +4 -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/primitives/textarea/index.ts +2 -0
- package/src/primitives/textarea/textarea.tsx +56 -0
- package/src/primitives/textarea/types.ts +5 -0
- package/src/utils/get-ref-layout.ts +16 -0
|
@@ -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
|
@@ -8,3 +8,7 @@ export * from "./link/link";
|
|
|
8
8
|
export * from "./empty/empty";
|
|
9
9
|
export * from "./avatar/avatar";
|
|
10
10
|
export * from "./toast/index";
|
|
11
|
+
export * from "./badge/badge";
|
|
12
|
+
export * from "./textarea/textarea";
|
|
13
|
+
export * from "./dropdown-menu/dropdown-menu";
|
|
14
|
+
export * from "./popover/popover";
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { TextareaVariants } from "./variants";
|
|
3
|
+
import { TextareaPrimitive, TextareaPrimitiveBaseProps } from "@/primitives";
|
|
4
|
+
|
|
5
|
+
interface TextareaProps extends TextareaPrimitiveBaseProps {
|
|
6
|
+
variant?: keyof typeof TextareaVariants;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Textarea(props: TextareaProps) {
|
|
10
|
+
const useVariantStyles = TextareaVariants[props.variant ?? "default"];
|
|
11
|
+
const variantStyles = useVariantStyles();
|
|
12
|
+
|
|
13
|
+
return <TextareaPrimitive {...props} styles={variantStyles} />;
|
|
14
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { TextareaStyles } from "@/primitives";
|
|
2
|
+
import { useThemedStyles } from "@/utils/use-themed-styles";
|
|
3
|
+
|
|
4
|
+
export function useTextareaVariantDefault(): TextareaStyles {
|
|
5
|
+
return useThemedStyles(
|
|
6
|
+
({ colors, radius, fontFamily, fontSize }): TextareaStyles => ({
|
|
7
|
+
default: {
|
|
8
|
+
placeholderTextColor: colors.mutedForeground,
|
|
9
|
+
selectionColor: colors.primary,
|
|
10
|
+
style: {
|
|
11
|
+
borderWidth: 1,
|
|
12
|
+
borderColor: colors.border,
|
|
13
|
+
borderRadius: radius,
|
|
14
|
+
backgroundColor: colors.surface,
|
|
15
|
+
paddingVertical: 12,
|
|
16
|
+
paddingHorizontal: 16,
|
|
17
|
+
outlineWidth: 0,
|
|
18
|
+
fontFamily,
|
|
19
|
+
fontSize,
|
|
20
|
+
minHeight: 120,
|
|
21
|
+
textAlignVertical: "top",
|
|
22
|
+
color: colors.foreground,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
focused: {
|
|
26
|
+
style: {
|
|
27
|
+
borderColor: colors.primary,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
disabled: {
|
|
31
|
+
style: {
|
|
32
|
+
color: colors.mutedForeground,
|
|
33
|
+
backgroundColor: colors.muted,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { useWindowDimensions, type LayoutRectangle, type ScaledSize } from "react-native";
|
|
3
|
+
|
|
4
|
+
interface Insets {
|
|
5
|
+
top?: number;
|
|
6
|
+
bottom?: number;
|
|
7
|
+
left?: number;
|
|
8
|
+
right?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type UseRelativePositionArgs = Omit<GetContentStyleArgs, "triggerPosition" | "contentLayout" | "dimensions"> & {
|
|
12
|
+
triggerPosition: LayoutPosition | null;
|
|
13
|
+
contentLayout: LayoutRectangle | null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function useRelativePosition({
|
|
17
|
+
align,
|
|
18
|
+
avoidCollisions,
|
|
19
|
+
triggerPosition,
|
|
20
|
+
contentLayout,
|
|
21
|
+
alignOffset,
|
|
22
|
+
insets,
|
|
23
|
+
sideOffset,
|
|
24
|
+
side,
|
|
25
|
+
}: UseRelativePositionArgs) {
|
|
26
|
+
const dimensions = useWindowDimensions();
|
|
27
|
+
|
|
28
|
+
return React.useMemo(() => {
|
|
29
|
+
if (!triggerPosition || !contentLayout) {
|
|
30
|
+
return {
|
|
31
|
+
position: "absolute",
|
|
32
|
+
opacity: 0,
|
|
33
|
+
top: dimensions.height,
|
|
34
|
+
zIndex: -9999999,
|
|
35
|
+
} as const;
|
|
36
|
+
}
|
|
37
|
+
return getContentStyle({
|
|
38
|
+
align,
|
|
39
|
+
avoidCollisions,
|
|
40
|
+
contentLayout,
|
|
41
|
+
side,
|
|
42
|
+
triggerPosition,
|
|
43
|
+
alignOffset,
|
|
44
|
+
insets,
|
|
45
|
+
sideOffset,
|
|
46
|
+
dimensions,
|
|
47
|
+
});
|
|
48
|
+
}, [align, avoidCollisions, side, alignOffset, insets, triggerPosition, contentLayout, dimensions.width, dimensions.height]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface LayoutPosition {
|
|
52
|
+
pageY: number;
|
|
53
|
+
pageX: number;
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface GetPositionArgs {
|
|
59
|
+
dimensions: ScaledSize;
|
|
60
|
+
avoidCollisions: boolean;
|
|
61
|
+
triggerPosition: LayoutPosition;
|
|
62
|
+
contentLayout: LayoutRectangle;
|
|
63
|
+
insets?: Insets;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface GetSidePositionArgs extends GetPositionArgs {
|
|
67
|
+
side: "top" | "bottom";
|
|
68
|
+
sideOffset: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const DEFAULT_LAYOUT: LayoutRectangle = { x: 0, y: 0, width: 0, height: 0 };
|
|
72
|
+
export const DEFAULT_POSITION: LayoutPosition = { height: 0, width: 0, pageX: 0, pageY: 0 };
|
|
73
|
+
|
|
74
|
+
function getSidePosition({ side, triggerPosition, contentLayout, sideOffset, insets, avoidCollisions, dimensions }: GetSidePositionArgs) {
|
|
75
|
+
const insetTop = insets?.top ?? 0;
|
|
76
|
+
const insetBottom = insets?.bottom ?? 0;
|
|
77
|
+
const positionTop = triggerPosition?.pageY - sideOffset - contentLayout.height;
|
|
78
|
+
const positionBottom = triggerPosition.pageY + triggerPosition.height + sideOffset;
|
|
79
|
+
|
|
80
|
+
if (!avoidCollisions) {
|
|
81
|
+
return {
|
|
82
|
+
top: side === "top" ? positionTop : positionBottom,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (side === "top") {
|
|
87
|
+
return {
|
|
88
|
+
top: Math.min(Math.max(insetTop, positionTop), dimensions.height - insetBottom - contentLayout.height),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
top: Math.min(dimensions.height - insetBottom - contentLayout.height, positionBottom),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface GetAlignPositionArgs extends GetPositionArgs {
|
|
98
|
+
align: "start" | "center" | "end";
|
|
99
|
+
alignOffset: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getAlignPosition({ align, avoidCollisions, contentLayout, triggerPosition, alignOffset, insets, dimensions }: GetAlignPositionArgs) {
|
|
103
|
+
const insetLeft = insets?.left ?? 0;
|
|
104
|
+
const insetRight = insets?.right ?? 0;
|
|
105
|
+
const maxContentWidth = dimensions.width - insetLeft - insetRight;
|
|
106
|
+
|
|
107
|
+
const contentWidth = Math.min(contentLayout.width, maxContentWidth);
|
|
108
|
+
|
|
109
|
+
let left = getLeftPosition(align, triggerPosition.pageX, triggerPosition.width, contentWidth, alignOffset, insetLeft, insetRight, dimensions);
|
|
110
|
+
|
|
111
|
+
if (avoidCollisions) {
|
|
112
|
+
const doesCollide = left < insetLeft || left + contentWidth > dimensions.width - insetRight;
|
|
113
|
+
if (doesCollide) {
|
|
114
|
+
const spaceLeft = left - insetLeft;
|
|
115
|
+
const spaceRight = dimensions.width - insetRight - (left + contentWidth);
|
|
116
|
+
|
|
117
|
+
if (spaceLeft > spaceRight && spaceLeft >= contentWidth) {
|
|
118
|
+
left = insetLeft;
|
|
119
|
+
} else if (spaceRight >= contentWidth) {
|
|
120
|
+
left = dimensions.width - insetRight - contentWidth;
|
|
121
|
+
} else {
|
|
122
|
+
const centeredPosition = Math.max(insetLeft, (dimensions.width - contentWidth - insetRight) / 2);
|
|
123
|
+
left = centeredPosition;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { left, maxWidth: maxContentWidth };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getLeftPosition(
|
|
132
|
+
align: "start" | "center" | "end",
|
|
133
|
+
triggerPageX: number,
|
|
134
|
+
triggerWidth: number,
|
|
135
|
+
contentWidth: number,
|
|
136
|
+
alignOffset: number,
|
|
137
|
+
insetLeft: number,
|
|
138
|
+
insetRight: number,
|
|
139
|
+
dimensions: ScaledSize
|
|
140
|
+
) {
|
|
141
|
+
let left = 0;
|
|
142
|
+
if (align === "start") {
|
|
143
|
+
left = triggerPageX;
|
|
144
|
+
}
|
|
145
|
+
if (align === "center") {
|
|
146
|
+
left = triggerPageX + triggerWidth / 2 - contentWidth / 2;
|
|
147
|
+
}
|
|
148
|
+
if (align === "end") {
|
|
149
|
+
left = triggerPageX + triggerWidth - contentWidth;
|
|
150
|
+
}
|
|
151
|
+
return Math.max(insetLeft, Math.min(left + alignOffset, dimensions.width - contentWidth - insetRight));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
type GetContentStyleArgs = GetPositionArgs & GetSidePositionArgs & GetAlignPositionArgs;
|
|
155
|
+
|
|
156
|
+
function getContentStyle({
|
|
157
|
+
align,
|
|
158
|
+
avoidCollisions,
|
|
159
|
+
contentLayout,
|
|
160
|
+
side,
|
|
161
|
+
triggerPosition,
|
|
162
|
+
alignOffset,
|
|
163
|
+
insets,
|
|
164
|
+
sideOffset,
|
|
165
|
+
dimensions,
|
|
166
|
+
}: GetContentStyleArgs) {
|
|
167
|
+
return Object.assign(
|
|
168
|
+
{ position: "absolute" } as const,
|
|
169
|
+
getSidePosition({
|
|
170
|
+
side,
|
|
171
|
+
triggerPosition,
|
|
172
|
+
contentLayout,
|
|
173
|
+
sideOffset,
|
|
174
|
+
insets,
|
|
175
|
+
avoidCollisions,
|
|
176
|
+
dimensions,
|
|
177
|
+
}),
|
|
178
|
+
getAlignPosition({
|
|
179
|
+
align,
|
|
180
|
+
avoidCollisions,
|
|
181
|
+
triggerPosition,
|
|
182
|
+
contentLayout,
|
|
183
|
+
alignOffset,
|
|
184
|
+
insets,
|
|
185
|
+
dimensions,
|
|
186
|
+
})
|
|
187
|
+
);
|
|
188
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { StyleProp, Text, TextStyle } from "react-native";
|
|
3
|
+
import { useBadge } from "./context";
|
|
4
|
+
import { calculateComposedStyles } from "@/utils/calculate-styles";
|
|
5
|
+
|
|
6
|
+
export interface BadgeLabelProps {
|
|
7
|
+
children?: string;
|
|
8
|
+
|
|
9
|
+
render?: (props: BadgeLabelProps) => React.ReactNode;
|
|
10
|
+
|
|
11
|
+
style?: StyleProp<TextStyle>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function BadgeLabel(props: BadgeLabelProps) {
|
|
15
|
+
const badge = useBadge();
|
|
16
|
+
|
|
17
|
+
const composedStyle = calculateComposedStyles(badge.styles, badge.state, "label", props.style);
|
|
18
|
+
|
|
19
|
+
const Component = props.render ?? Text;
|
|
20
|
+
return <Component {...props} style={composedStyle} />;
|
|
21
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { StyleProp, View, ViewStyle } from "react-native";
|
|
3
|
+
import { BadgeStyles } from "./types";
|
|
4
|
+
import { calculateComposedStyles } from "@/utils/calculate-styles";
|
|
5
|
+
import { BadgeContext } from "./context";
|
|
6
|
+
|
|
7
|
+
export interface BadgeRootProps {
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
|
|
10
|
+
render?: (props: BadgeRootProps) => React.ReactNode;
|
|
11
|
+
|
|
12
|
+
style?: StyleProp<ViewStyle>;
|
|
13
|
+
styles?: BadgeStyles;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function BadgeRoot(props: BadgeRootProps) {
|
|
17
|
+
const composedStyle = calculateComposedStyles(props.styles, "default", "root", props.style);
|
|
18
|
+
|
|
19
|
+
const Component = props.render ?? View;
|
|
20
|
+
return (
|
|
21
|
+
<BadgeContext.Provider
|
|
22
|
+
value={{
|
|
23
|
+
state: "default",
|
|
24
|
+
styles: props.styles,
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
<Component {...props} style={composedStyle} />
|
|
28
|
+
</BadgeContext.Provider>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import { BadgeState, BadgeStyles } from "./types";
|
|
3
|
+
|
|
4
|
+
export interface BadgeContext {
|
|
5
|
+
state: BadgeState;
|
|
6
|
+
styles?: BadgeStyles;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const BadgeContext = createContext<BadgeContext | undefined>(undefined);
|
|
10
|
+
|
|
11
|
+
export const useBadge = () => {
|
|
12
|
+
const context = useContext(BadgeContext);
|
|
13
|
+
if (!context) {
|
|
14
|
+
throw new Error("useBadge must be used within a BadgeProvider");
|
|
15
|
+
}
|
|
16
|
+
return context;
|
|
17
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BadgeRoot } from "./badge-root";
|
|
2
|
+
import { BadgeLabel } from "./badge-label";
|
|
3
|
+
|
|
4
|
+
export const BadgePrimitive = {
|
|
5
|
+
Root: BadgeRoot,
|
|
6
|
+
Label: BadgeLabel,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type { BadgeRootProps } from "./badge-root";
|
|
10
|
+
export type { BadgeLabelProps } from "./badge-label";
|
|
11
|
+
export type { BadgeStyles } from "./types";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BadgeRootProps } from "./badge-root";
|
|
2
|
+
import { BadgeLabelProps } from "./badge-label";
|
|
3
|
+
|
|
4
|
+
export type BadgeState = "default";
|
|
5
|
+
|
|
6
|
+
export interface BadgeStyles {
|
|
7
|
+
root?: Partial<Record<BadgeState, BadgeRootProps["style"]>>;
|
|
8
|
+
label?: Partial<Record<BadgeState, BadgeLabelProps["style"]>>;
|
|
9
|
+
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
|
-
import { Pressable, StyleProp, ViewStyle } from "react-native";
|
|
2
|
+
import { Pressable, PressableProps, StyleProp, ViewStyle } from "react-native";
|
|
3
3
|
import { ButtonStyles, ButtonState } from "./types";
|
|
4
4
|
import { ButtonPrimitiveContext } from "./button-context";
|
|
5
5
|
|
|
6
|
-
export interface ButtonPrimitiveRootProps {
|
|
6
|
+
export interface ButtonPrimitiveRootProps extends PressableProps {
|
|
7
7
|
children?: React.ReactNode;
|
|
8
8
|
|
|
9
|
-
onPress?: () => void;
|
|
10
|
-
|
|
11
9
|
isDisabled?: boolean;
|
|
12
10
|
isLoading?: boolean;
|
|
13
11
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createContext, Dispatch, useContext } from "react";
|
|
2
|
+
import { LayoutRectangle } from "react-native";
|
|
3
|
+
import { DropdownMenuStyles } from "./types";
|
|
4
|
+
import { LayoutPosition } from "@/hooks/useRelativePosition";
|
|
5
|
+
|
|
6
|
+
export interface DropdownMenuContext {
|
|
7
|
+
isOpen: boolean;
|
|
8
|
+
setIsOpen: Dispatch<React.SetStateAction<boolean>>;
|
|
9
|
+
triggerPosition: LayoutPosition;
|
|
10
|
+
setTriggerPosition: Dispatch<React.SetStateAction<LayoutPosition>>;
|
|
11
|
+
contentLayout: LayoutRectangle;
|
|
12
|
+
setContentLayout: Dispatch<React.SetStateAction<LayoutRectangle>>;
|
|
13
|
+
|
|
14
|
+
styles?: DropdownMenuStyles;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const DropdownMenuContext = createContext<DropdownMenuContext | undefined>(undefined);
|
|
18
|
+
|
|
19
|
+
export const useDropdownMenu = () => {
|
|
20
|
+
const context = useContext(DropdownMenuContext);
|
|
21
|
+
if (!context) {
|
|
22
|
+
throw new Error("useDropdownMenu must be used within a DropdownMenuRoot");
|
|
23
|
+
}
|
|
24
|
+
return context;
|
|
25
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { StyleProp, Text, TextStyle } from "react-native";
|
|
3
|
+
import { useDropdownMenu } from "./context";
|
|
4
|
+
import { DropdownMenuButtonState } from "./types";
|
|
5
|
+
|
|
6
|
+
export interface DropdownMenuButtonProps {
|
|
7
|
+
children?: string;
|
|
8
|
+
onPress?: () => void;
|
|
9
|
+
onMouseEnter?: () => void;
|
|
10
|
+
onMouseLeave?: () => void;
|
|
11
|
+
|
|
12
|
+
render?: (props: DropdownMenuButtonProps) => React.ReactElement;
|
|
13
|
+
|
|
14
|
+
style?: StyleProp<TextStyle>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const calculateState = (isHovered: boolean): DropdownMenuButtonState => {
|
|
18
|
+
if (isHovered) {
|
|
19
|
+
return "hovered";
|
|
20
|
+
}
|
|
21
|
+
return "default";
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function DropdownMenuButton(props: DropdownMenuButtonProps) {
|
|
25
|
+
const menu = useDropdownMenu();
|
|
26
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
27
|
+
const state = calculateState(isHovered);
|
|
28
|
+
const composedStyle = [menu.styles?.button?.default, menu.styles?.button?.[state], props.style];
|
|
29
|
+
|
|
30
|
+
const handlePress = () => {
|
|
31
|
+
props.onPress?.();
|
|
32
|
+
menu.setIsOpen((prev) => !prev);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const Component = props.render ?? Text;
|
|
36
|
+
return (
|
|
37
|
+
<Component
|
|
38
|
+
{...props}
|
|
39
|
+
onPress={handlePress}
|
|
40
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
41
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
42
|
+
style={composedStyle}
|
|
43
|
+
>
|
|
44
|
+
{props.children}
|
|
45
|
+
</Component>
|
|
46
|
+
);
|
|
47
|
+
}
|