@umituz/react-native-design-system 2.9.30 → 2.9.31
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/package.json +1 -1
- package/src/exports/molecules.ts +1 -5
- package/src/molecules/circular-menu/CircularMenu.tsx +151 -0
- package/src/molecules/circular-menu/CircularMenuBackground.tsx +32 -0
- package/src/molecules/circular-menu/CircularMenuCloseButton.tsx +44 -0
- package/src/molecules/circular-menu/CircularMenuItem.tsx +69 -0
- package/src/molecules/circular-menu/constants.ts +73 -0
- package/src/molecules/circular-menu/index.ts +4 -0
- package/src/molecules/index.ts +1 -0
- package/src/molecules/navigation/components/NavigationContainer.tsx +13 -0
- package/src/molecules/navigation/components/index.ts +4 -0
- package/src/molecules/navigation/index.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-design-system",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.31",
|
|
4
4
|
"description": "Universal design system for React Native apps - Consolidated package with atoms, molecules, organisms, theme, typography, responsive, safe area, exception, infinite scroll, UUID, image, timezone, offline, onboarding, and loading utilities",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
package/src/exports/molecules.ts
CHANGED
|
@@ -29,7 +29,6 @@ export {
|
|
|
29
29
|
// Bottom Sheet
|
|
30
30
|
BottomSheet,
|
|
31
31
|
BottomSheetModal,
|
|
32
|
-
|
|
33
32
|
FilterBottomSheet,
|
|
34
33
|
FilterSheet,
|
|
35
34
|
useBottomSheet,
|
|
@@ -114,8 +113,6 @@ export {
|
|
|
114
113
|
type FabButtonProps,
|
|
115
114
|
type TabScreen,
|
|
116
115
|
type TabNavigatorConfig,
|
|
117
|
-
type StackScreen,
|
|
118
|
-
type StackNavigatorConfig,
|
|
119
116
|
type BaseScreen,
|
|
120
117
|
type BaseNavigatorConfig,
|
|
121
118
|
type IconRendererProps,
|
|
@@ -190,5 +187,4 @@ export {
|
|
|
190
187
|
InfoGrid,
|
|
191
188
|
type InfoGridProps,
|
|
192
189
|
type InfoGridItem,
|
|
193
|
-
} from
|
|
194
|
-
|
|
190
|
+
} from "../molecules";
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Modal,
|
|
5
|
+
TouchableWithoutFeedback,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
ViewStyle,
|
|
8
|
+
} from "react-native";
|
|
9
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
10
|
+
import {
|
|
11
|
+
ARC_BACKGROUND,
|
|
12
|
+
OVERLAY,
|
|
13
|
+
ROW_CONFIG,
|
|
14
|
+
LAYOUT,
|
|
15
|
+
getTopRowPosition,
|
|
16
|
+
getBottomRowPosition,
|
|
17
|
+
} from "./constants";
|
|
18
|
+
import { CircularMenuBackground } from "./CircularMenuBackground";
|
|
19
|
+
import { CircularMenuItem } from "./CircularMenuItem";
|
|
20
|
+
import { CircularMenuCloseButton } from "./CircularMenuCloseButton";
|
|
21
|
+
|
|
22
|
+
export interface CircularMenuAction {
|
|
23
|
+
id: string;
|
|
24
|
+
label: string;
|
|
25
|
+
icon: string;
|
|
26
|
+
onPress: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CircularMenuProps {
|
|
30
|
+
visible: boolean;
|
|
31
|
+
onClose: () => void;
|
|
32
|
+
actions: CircularMenuAction[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const CircularMenu: React.FC<CircularMenuProps> = ({
|
|
36
|
+
visible,
|
|
37
|
+
onClose,
|
|
38
|
+
actions,
|
|
39
|
+
}) => {
|
|
40
|
+
const insets = useSafeAreaInsets();
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Determine layout based on number of items.
|
|
44
|
+
* If strictly 3 items, use the specific "Top Triangle" layout requested.
|
|
45
|
+
* Otherwise, use a generic algorithm.
|
|
46
|
+
*/
|
|
47
|
+
const layoutItems = useMemo(() => {
|
|
48
|
+
if (actions.length === 3) {
|
|
49
|
+
// Triangle Layout: Top Center (Index 1), Bottom Left (Index 0), Bottom Right (Index 2)
|
|
50
|
+
return [
|
|
51
|
+
{ ...actions[1], position: getTopRowPosition(0, 1) }, // Text to Video (Top Center)
|
|
52
|
+
{ ...actions[0], position: getBottomRowPosition("left") }, // Text to Image (Bottom Left)
|
|
53
|
+
{ ...actions[2], position: getBottomRowPosition("right") }, // Image to Video (Bottom Right)
|
|
54
|
+
];
|
|
55
|
+
} else {
|
|
56
|
+
// Default: 2 on top, 2 on-bottom, etc.
|
|
57
|
+
// This is a fallback if actions change.
|
|
58
|
+
// For now, implementing simple distribute:
|
|
59
|
+
const topCount = Math.min(actions.length, 2);
|
|
60
|
+
const bottomCount = Math.max(0, actions.length - 2);
|
|
61
|
+
|
|
62
|
+
const mapped = [];
|
|
63
|
+
// Top row
|
|
64
|
+
for (let i = 0; i < topCount; i++) {
|
|
65
|
+
mapped.push({
|
|
66
|
+
...actions[i],
|
|
67
|
+
position: getTopRowPosition(i, topCount),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// Bottom row (left/right)
|
|
71
|
+
if (bottomCount > 0) {
|
|
72
|
+
mapped.push({
|
|
73
|
+
...actions[2],
|
|
74
|
+
position: getBottomRowPosition("left")
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
if (bottomCount > 1) {
|
|
78
|
+
mapped.push({
|
|
79
|
+
...actions[3],
|
|
80
|
+
position: getBottomRowPosition("right")
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
return mapped;
|
|
84
|
+
}
|
|
85
|
+
}, [actions]);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<Modal
|
|
89
|
+
visible={visible}
|
|
90
|
+
transparent
|
|
91
|
+
animationType="fade"
|
|
92
|
+
onRequestClose={onClose}
|
|
93
|
+
statusBarTranslucent
|
|
94
|
+
>
|
|
95
|
+
<TouchableWithoutFeedback onPress={onClose}>
|
|
96
|
+
<View style={styles.overlay}>
|
|
97
|
+
<TouchableWithoutFeedback>
|
|
98
|
+
<View
|
|
99
|
+
style={[
|
|
100
|
+
styles.container,
|
|
101
|
+
{
|
|
102
|
+
paddingBottom: insets.bottom + OVERLAY.PADDING_BOTTOM_OFFSET,
|
|
103
|
+
},
|
|
104
|
+
]}
|
|
105
|
+
>
|
|
106
|
+
<CircularMenuBackground />
|
|
107
|
+
|
|
108
|
+
<View style={styles.itemsContainer}>
|
|
109
|
+
{layoutItems.map((item) => (
|
|
110
|
+
<View key={item.id} style={item.position as ViewStyle}>
|
|
111
|
+
<CircularMenuItem
|
|
112
|
+
icon={item.icon}
|
|
113
|
+
label={item.label}
|
|
114
|
+
onPress={item.onPress}
|
|
115
|
+
/>
|
|
116
|
+
</View>
|
|
117
|
+
))}
|
|
118
|
+
|
|
119
|
+
<View style={styles.closeButton}>
|
|
120
|
+
<CircularMenuCloseButton onPress={onClose} />
|
|
121
|
+
</View>
|
|
122
|
+
</View>
|
|
123
|
+
</View>
|
|
124
|
+
</TouchableWithoutFeedback>
|
|
125
|
+
</View>
|
|
126
|
+
</TouchableWithoutFeedback>
|
|
127
|
+
</Modal>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const styles = StyleSheet.create({
|
|
132
|
+
overlay: {
|
|
133
|
+
flex: 1,
|
|
134
|
+
justifyContent: "flex-end",
|
|
135
|
+
backgroundColor: "rgba(0, 0, 0, 0.85)",
|
|
136
|
+
},
|
|
137
|
+
container: {
|
|
138
|
+
alignItems: "center",
|
|
139
|
+
},
|
|
140
|
+
itemsContainer: {
|
|
141
|
+
width: ARC_BACKGROUND.WIDTH,
|
|
142
|
+
height: ARC_BACKGROUND.HEIGHT,
|
|
143
|
+
position: "relative",
|
|
144
|
+
},
|
|
145
|
+
closeButton: {
|
|
146
|
+
position: "absolute",
|
|
147
|
+
left: ARC_BACKGROUND.WIDTH / 2 - LAYOUT.CLOSE_BUTTON_SIZE / 2,
|
|
148
|
+
top:
|
|
149
|
+
ROW_CONFIG.CENTER_Y + ROW_CONFIG.BOTTOM_Y - LAYOUT.CLOSE_BUTTON_SIZE / 2,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View, StyleSheet } from "react-native";
|
|
3
|
+
import { useAppDesignTokens } from "../../theme/useAppDesignTokens";
|
|
4
|
+
import { ARC_BACKGROUND } from "./constants";
|
|
5
|
+
|
|
6
|
+
export const CircularMenuBackground: React.FC = () => {
|
|
7
|
+
const tokens = useAppDesignTokens();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<View
|
|
11
|
+
style={[
|
|
12
|
+
styles.arc,
|
|
13
|
+
{
|
|
14
|
+
backgroundColor: tokens.colors.surface,
|
|
15
|
+
width: ARC_BACKGROUND.WIDTH,
|
|
16
|
+
height: ARC_BACKGROUND.HEIGHT,
|
|
17
|
+
borderTopLeftRadius: ARC_BACKGROUND.BORDER_RADIUS_TOP,
|
|
18
|
+
borderTopRightRadius: ARC_BACKGROUND.BORDER_RADIUS_TOP,
|
|
19
|
+
},
|
|
20
|
+
]}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const styles = StyleSheet.create({
|
|
26
|
+
arc: {
|
|
27
|
+
position: "absolute",
|
|
28
|
+
bottom: 0,
|
|
29
|
+
borderBottomLeftRadius: 0,
|
|
30
|
+
borderBottomRightRadius: 0,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { TouchableOpacity, StyleSheet } from "react-native";
|
|
3
|
+
import { AtomicIcon } from "../../atoms/AtomicIcon";
|
|
4
|
+
import { useAppDesignTokens } from "../../theme/useAppDesignTokens";
|
|
5
|
+
import { LAYOUT } from "./constants";
|
|
6
|
+
|
|
7
|
+
export interface CircularMenuCloseButtonProps {
|
|
8
|
+
onPress: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const CircularMenuCloseButton: React.FC<CircularMenuCloseButtonProps> = ({
|
|
12
|
+
onPress,
|
|
13
|
+
}) => {
|
|
14
|
+
const tokens = useAppDesignTokens();
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<TouchableOpacity
|
|
18
|
+
onPress={onPress}
|
|
19
|
+
style={[
|
|
20
|
+
styles.container,
|
|
21
|
+
{
|
|
22
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
23
|
+
width: LAYOUT.CLOSE_BUTTON_SIZE,
|
|
24
|
+
height: LAYOUT.CLOSE_BUTTON_SIZE,
|
|
25
|
+
borderRadius: LAYOUT.CLOSE_BUTTON_SIZE / 2,
|
|
26
|
+
borderWidth: 1,
|
|
27
|
+
borderColor: tokens.colors.border,
|
|
28
|
+
},
|
|
29
|
+
]}
|
|
30
|
+
activeOpacity={0.8}
|
|
31
|
+
accessibilityRole="button"
|
|
32
|
+
accessibilityLabel="Close menu"
|
|
33
|
+
>
|
|
34
|
+
<AtomicIcon name="close" size="md" color="secondary" />
|
|
35
|
+
</TouchableOpacity>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const styles = StyleSheet.create({
|
|
40
|
+
container: {
|
|
41
|
+
justifyContent: "center",
|
|
42
|
+
alignItems: "center",
|
|
43
|
+
},
|
|
44
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
3
|
+
import { AtomicIcon } from "../../atoms/AtomicIcon";
|
|
4
|
+
import { AtomicText } from "../../atoms/AtomicText";
|
|
5
|
+
import { useAppDesignTokens } from "../../theme/useAppDesignTokens";
|
|
6
|
+
import { LAYOUT } from "./constants";
|
|
7
|
+
|
|
8
|
+
export interface CircularMenuItemProps {
|
|
9
|
+
icon: string;
|
|
10
|
+
label: string;
|
|
11
|
+
onPress: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const CircularMenuItem: React.FC<CircularMenuItemProps> = ({
|
|
15
|
+
icon,
|
|
16
|
+
label,
|
|
17
|
+
onPress,
|
|
18
|
+
}) => {
|
|
19
|
+
const tokens = useAppDesignTokens();
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<TouchableOpacity
|
|
23
|
+
onPress={onPress}
|
|
24
|
+
style={styles.container}
|
|
25
|
+
activeOpacity={0.7}
|
|
26
|
+
accessibilityRole="button"
|
|
27
|
+
accessibilityLabel={label}
|
|
28
|
+
>
|
|
29
|
+
<View
|
|
30
|
+
style={[
|
|
31
|
+
styles.iconContainer,
|
|
32
|
+
{
|
|
33
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
34
|
+
width: LAYOUT.ITEM_SIZE,
|
|
35
|
+
height: LAYOUT.ITEM_SIZE,
|
|
36
|
+
borderRadius: LAYOUT.ITEM_SIZE / 2,
|
|
37
|
+
borderWidth: 1,
|
|
38
|
+
borderColor: tokens.colors.border,
|
|
39
|
+
},
|
|
40
|
+
]}
|
|
41
|
+
>
|
|
42
|
+
<AtomicIcon name={icon as any} size="lg" color="primary" />
|
|
43
|
+
</View>
|
|
44
|
+
<AtomicText
|
|
45
|
+
type="labelSmall"
|
|
46
|
+
style={[styles.label, { color: tokens.colors.textPrimary }]}
|
|
47
|
+
>
|
|
48
|
+
{label}
|
|
49
|
+
</AtomicText>
|
|
50
|
+
</TouchableOpacity>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const styles = StyleSheet.create({
|
|
55
|
+
container: {
|
|
56
|
+
alignItems: "center",
|
|
57
|
+
gap: 6,
|
|
58
|
+
width: 90,
|
|
59
|
+
},
|
|
60
|
+
iconContainer: {
|
|
61
|
+
justifyContent: "center",
|
|
62
|
+
alignItems: "center",
|
|
63
|
+
},
|
|
64
|
+
label: {
|
|
65
|
+
fontSize: 11,
|
|
66
|
+
fontWeight: "500",
|
|
67
|
+
textAlign: "center",
|
|
68
|
+
},
|
|
69
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ViewStyle } from "react-native";
|
|
2
|
+
|
|
3
|
+
export const LAYOUT = {
|
|
4
|
+
ITEM_SIZE: 52,
|
|
5
|
+
CLOSE_BUTTON_SIZE: 48,
|
|
6
|
+
} as const;
|
|
7
|
+
|
|
8
|
+
export const ROW_CONFIG = {
|
|
9
|
+
TOP_RADIUS: 80,
|
|
10
|
+
TOP_START_ANGLE: -135,
|
|
11
|
+
TOP_END_ANGLE: -45,
|
|
12
|
+
BOTTOM_OFFSET_X: 160,
|
|
13
|
+
BOTTOM_Y: 60,
|
|
14
|
+
CENTER_Y: 150,
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export const ARC_BACKGROUND = {
|
|
18
|
+
WIDTH: 450,
|
|
19
|
+
HEIGHT: 280,
|
|
20
|
+
BORDER_RADIUS_TOP: 225,
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
export const OVERLAY = {
|
|
24
|
+
PADDING_BOTTOM_OFFSET: 0,
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
const CENTER_X = ARC_BACKGROUND.WIDTH / 2;
|
|
28
|
+
|
|
29
|
+
function toRadians(degrees: number): number {
|
|
30
|
+
return (degrees * Math.PI) / 180;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getTopRowPosition(
|
|
34
|
+
index: number,
|
|
35
|
+
totalInRow: number
|
|
36
|
+
): ViewStyle {
|
|
37
|
+
if (totalInRow <= 1) {
|
|
38
|
+
const radian = toRadians(-90);
|
|
39
|
+
const x = ROW_CONFIG.TOP_RADIUS * Math.cos(radian);
|
|
40
|
+
const y = ROW_CONFIG.TOP_RADIUS * Math.sin(radian);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
position: "absolute",
|
|
44
|
+
left: CENTER_X + x - LAYOUT.ITEM_SIZE / 2,
|
|
45
|
+
top: ROW_CONFIG.CENTER_Y + y - LAYOUT.ITEM_SIZE / 2,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const arcSpan = ROW_CONFIG.TOP_END_ANGLE - ROW_CONFIG.TOP_START_ANGLE;
|
|
50
|
+
const step = arcSpan / (totalInRow - 1);
|
|
51
|
+
const angle = ROW_CONFIG.TOP_START_ANGLE + step * index;
|
|
52
|
+
const radian = toRadians(angle);
|
|
53
|
+
|
|
54
|
+
const x = ROW_CONFIG.TOP_RADIUS * Math.cos(radian);
|
|
55
|
+
const y = ROW_CONFIG.TOP_RADIUS * Math.sin(radian);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
position: "absolute",
|
|
59
|
+
left: CENTER_X + x - LAYOUT.ITEM_SIZE / 2,
|
|
60
|
+
top: ROW_CONFIG.CENTER_Y + y - LAYOUT.ITEM_SIZE / 2,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getBottomRowPosition(side: "left" | "right"): ViewStyle {
|
|
65
|
+
const offsetX =
|
|
66
|
+
side === "left" ? -ROW_CONFIG.BOTTOM_OFFSET_X : ROW_CONFIG.BOTTOM_OFFSET_X;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
position: "absolute",
|
|
70
|
+
left: CENTER_X + offsetX - LAYOUT.ITEM_SIZE / 2,
|
|
71
|
+
top: ROW_CONFIG.CENTER_Y + ROW_CONFIG.BOTTOM_Y - LAYOUT.ITEM_SIZE / 2,
|
|
72
|
+
};
|
|
73
|
+
}
|
package/src/molecules/index.ts
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NavigationContainerRef } from "@react-navigation/native";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* NavigationContainer Component
|
|
5
|
+
*
|
|
6
|
+
* Wrapper around React Navigation's NavigationContainerRef
|
|
7
|
+
* Provides navigation support to applications.
|
|
8
|
+
*/
|
|
9
|
+
export const NavigationContainer: React.FC<{
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
}> = ({ children }) => {
|
|
12
|
+
return <NavigationContainerRef>{children}</NavigationContainerRef>;
|
|
13
|
+
};
|
|
@@ -3,6 +3,7 @@ export { createStackNavigator } from "./createStackNavigator";
|
|
|
3
3
|
export { TabsNavigator, type TabsNavigatorProps } from "./TabsNavigator";
|
|
4
4
|
export { StackNavigator, type StackNavigatorProps } from "./StackNavigator";
|
|
5
5
|
export { FabButton, type FabButtonProps } from "./components/FabButton";
|
|
6
|
+
export { NavigationContainer } from "./components/NavigationContainer";
|
|
6
7
|
|
|
7
8
|
export type {
|
|
8
9
|
TabScreen,
|
|
@@ -34,7 +35,7 @@ export type { NavigationCleanup } from "./utils/NavigationCleanup";
|
|
|
34
35
|
// Navigation Utilities
|
|
35
36
|
export { AppNavigation } from "./utils/AppNavigation";
|
|
36
37
|
export { TabLabel, type TabLabelProps } from "./components/TabLabel";
|
|
37
|
-
export * from
|
|
38
|
+
export * from "./components/NavigationHeader";
|
|
38
39
|
export { useTabBarStyles, type TabBarConfig } from "./hooks/useTabBarStyles";
|
|
39
40
|
export { useTabConfig, type UseTabConfigProps } from "./hooks/useTabConfig";
|
|
40
41
|
export { useAppNavigation } from "./hooks/useAppNavigation";
|