@momo-kits/native-kits 0.152.4-beta.2 → 0.152.4-beta.3

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 (155) hide show
  1. package/CODE_OF_CONDUCT.md +133 -0
  2. package/CONTRIBUTING.md +114 -0
  3. package/LICENSE +20 -0
  4. package/README.md +7 -0
  5. package/build.gradle.kts +32 -0
  6. package/compose/MoMoComposeKits.podspec +54 -0
  7. package/compose/build.gradle.kts +149 -0
  8. package/compose/src/androidMain/AndroidManifest.xml +2 -0
  9. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +105 -0
  10. package/compose/src/commonMain/composeResources/files/lottie_circle_loader.json +1 -0
  11. package/compose/src/commonMain/composeResources/font/momosignature.otf +0 -0
  12. package/compose/src/commonMain/composeResources/font/momotrustdisplay.otf +0 -0
  13. package/compose/src/commonMain/composeResources/font/sfprotext_black.otf +0 -0
  14. package/compose/src/commonMain/composeResources/font/sfprotext_black.ttf +0 -0
  15. package/compose/src/commonMain/composeResources/font/sfprotext_bold.ttf +0 -0
  16. package/compose/src/commonMain/composeResources/font/sfprotext_heavy.ttf +0 -0
  17. package/compose/src/commonMain/composeResources/font/sfprotext_light.ttf +0 -0
  18. package/compose/src/commonMain/composeResources/font/sfprotext_medium.ttf +0 -0
  19. package/compose/src/commonMain/composeResources/font/sfprotext_regular.ttf +0 -0
  20. package/compose/src/commonMain/composeResources/font/sfprotext_semibold.ttf +0 -0
  21. package/compose/src/commonMain/composeResources/font/sfprotext_thin.otf +0 -0
  22. package/compose/src/commonMain/composeResources/font/sfprotext_thin.ttf +0 -0
  23. package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.otf +0 -0
  24. package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.ttf +0 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +57 -0
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +201 -0
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +222 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +48 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +306 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +715 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +214 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +236 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +77 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +27 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +334 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +345 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +90 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +131 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +543 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +23 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +69 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +143 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +179 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +111 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +384 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +160 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +234 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +223 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +232 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +236 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +228 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +364 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +50 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +34 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +85 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +33 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +338 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +95 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +64 -0
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +89 -0
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +91 -0
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +86 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +84 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +208 -0
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +172 -0
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +199 -0
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +237 -0
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +191 -0
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +306 -0
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +13 -0
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +191 -0
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +258 -0
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +2 -0
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +35 -0
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +2 -0
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +59 -0
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +68 -0
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +49 -0
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +232 -0
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +111 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +94 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +159 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +232 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScaleSizeScope.kt +17 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +459 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +169 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +216 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +180 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +251 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +80 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +306 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +31 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +385 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +38 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +62 -0
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +88 -0
  108. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +144 -0
  109. package/gradle.properties +19 -0
  110. package/gradlew +240 -0
  111. package/gradlew.bat +91 -0
  112. package/ios/Application/ApplicationEnvironment.swift +50 -0
  113. package/ios/Application/Components.swift +263 -0
  114. package/ios/Application/ComposeApi.swift +22 -0
  115. package/ios/Application/FloatingButton.swift +172 -0
  116. package/ios/Application/HeaderRight.swift +271 -0
  117. package/ios/Application/Screen.swift +249 -0
  118. package/ios/Badge/BadgeDot.swift +31 -0
  119. package/ios/Button/Button.swift +211 -0
  120. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +126 -0
  121. package/ios/Checkbox/Checkbox.swift +81 -0
  122. package/ios/Chip/Chip.swift +96 -0
  123. package/ios/Colors+Radius+Spacing/Colors.swift +172 -0
  124. package/ios/Colors+Radius+Spacing/Radius.swift +22 -0
  125. package/ios/Colors+Radius+Spacing/Spacing.swift +12 -0
  126. package/ios/Extensions/Color++.swift +25 -0
  127. package/ios/Icon/Icon.swift +51 -0
  128. package/ios/Image/Image.swift +70 -0
  129. package/ios/Input/Input.swift +207 -0
  130. package/ios/Input/InputPhoneNumber.swift +176 -0
  131. package/ios/Input/InputSearch.swift +238 -0
  132. package/ios/Input/InputTextArea.swift +242 -0
  133. package/ios/Lottie/LottieView.swift +86 -0
  134. package/ios/OTPKeyboard/KeyboardButton.swift +41 -0
  135. package/ios/OTPKeyboard/OTPKeyboard.swift +145 -0
  136. package/ios/Popup/PopupDisplay.swift +284 -0
  137. package/ios/Popup/PopupInput.swift +96 -0
  138. package/ios/Popup/PopupPromotion.swift +73 -0
  139. package/ios/PopupView/FullscreenPopup.swift +251 -0
  140. package/ios/PopupView/Modifiers.swift +158 -0
  141. package/ios/PopupView/PopupView.swift +289 -0
  142. package/ios/PopupView/Utils++.swift +281 -0
  143. package/ios/ScrollIndicator/ScrollIndicator.swift +110 -0
  144. package/ios/Swipeable/SwipeCell.swift +278 -0
  145. package/ios/Swipeable/SwipeCellModel.swift +86 -0
  146. package/ios/Switch/Switch.swift +44 -0
  147. package/ios/Template/Logo/Logo.swift +75 -0
  148. package/ios/Template/TrustBanner/TrustBanner.swift +120 -0
  149. package/ios/Theme.md +18 -0
  150. package/ios/Typography/Text.swift +140 -0
  151. package/ios/Typography/Typography.swift +95 -0
  152. package/ios/native-kits.podspec +18 -0
  153. package/package.json +6 -7
  154. package/settings.gradle.kts +25 -0
  155. package/shared/build.gradle.kts +0 -74
@@ -0,0 +1,169 @@
1
+ package vn.momo.kits.navigation.bottomtab
2
+
3
+ import androidx.compose.animation.core.tween
4
+ import androidx.compose.animation.fadeIn
5
+ import androidx.compose.animation.fadeOut
6
+ import androidx.compose.animation.scaleIn
7
+ import androidx.compose.animation.scaleOut
8
+ import androidx.compose.foundation.background
9
+ import androidx.compose.foundation.layout.Box
10
+ import androidx.compose.foundation.layout.Column
11
+ import androidx.compose.foundation.layout.Spacer
12
+ import androidx.compose.foundation.layout.fillMaxSize
13
+ import androidx.compose.foundation.layout.fillMaxWidth
14
+ import androidx.compose.foundation.layout.height
15
+ import androidx.compose.foundation.layout.padding
16
+ import androidx.compose.runtime.Composable
17
+ import androidx.compose.runtime.LaunchedEffect
18
+ import androidx.compose.ui.Alignment
19
+ import androidx.compose.ui.Modifier
20
+ import androidx.compose.ui.unit.dp
21
+ import androidx.compose.ui.unit.min
22
+ import androidx.navigation.compose.NavHost
23
+ import androidx.navigation.compose.composable
24
+ import androidx.navigation.compose.rememberNavController
25
+ import vn.momo.kits.const.AppNavigationBar
26
+ import vn.momo.kits.const.AppTheme
27
+ import vn.momo.kits.const.Spacing
28
+ import vn.momo.kits.navigation.LocalNavigation
29
+ import vn.momo.kits.navigation.LocalNavigator
30
+ import vn.momo.kits.navigation.NavigationOptions
31
+ import vn.momo.kits.navigation.StackScreen
32
+ import vn.momo.kits.navigation.component.HeaderType
33
+ import vn.momo.kits.platform.getScreenHeight
34
+
35
+ private var bottomTabOptionItems : MutableList<NavigationOptions?> = mutableListOf()
36
+ fun setBottomTabOption(index: Int, options: NavigationOptions){
37
+ if (index in bottomTabOptionItems.indices) {
38
+ bottomTabOptionItems[index] = options
39
+ }
40
+ }
41
+ fun getBottomTabOption(index: Int): NavigationOptions? {
42
+ return if (index in bottomTabOptionItems.indices) {
43
+ bottomTabOptionItems[index]
44
+ } else null
45
+ }
46
+
47
+ @Composable
48
+ fun BottomTab(
49
+ items: List<BottomTabItem>,
50
+ floatingButton: BottomTabFloatingButton? = null
51
+ ) {
52
+ val navigation = LocalNavigation.current
53
+ val navigator = LocalNavigator.current
54
+ val navController = rememberNavController()
55
+
56
+ bottomTabOptionItems = items.mapIndexed { index, item ->
57
+ val options = item.options ?: NavigationOptions()
58
+ options.copy(
59
+ onBackHandler = {
60
+ if (index != 0) {
61
+ navController.popBackStack()
62
+ } else {
63
+ navigator.pop()
64
+ }
65
+ }
66
+ )
67
+ }.toMutableList()
68
+
69
+
70
+ LaunchedEffect(Unit){
71
+ navigation.setOptions(
72
+ headerType = HeaderType.None
73
+ )
74
+ }
75
+
76
+ Box(modifier = Modifier.fillMaxWidth().height(getScreenHeight()), contentAlignment = Alignment.BottomCenter) {
77
+ Box(modifier = Modifier
78
+ .fillMaxSize()
79
+ .padding(bottom = BOTTOM_TAB_BAR_HEIGHT.dp + AppNavigationBar.current)
80
+ ) {
81
+ NavHost(
82
+ navController = navController,
83
+ startDestination = "option0"
84
+ ) {
85
+ items.forEachIndexed { index, item ->
86
+ composable(
87
+ route = "option$index",
88
+ enterTransition = {
89
+ fadeIn(animationSpec = tween(200)) +
90
+ scaleIn(
91
+ initialScale = 0.97f,
92
+ animationSpec = tween(200)
93
+ )
94
+ },
95
+ exitTransition = {
96
+ fadeOut(animationSpec = tween(200)) +
97
+ scaleOut(
98
+ targetScale = 0.97f,
99
+ animationSpec = tween(200)
100
+ )
101
+ },
102
+ popEnterTransition = {
103
+ fadeIn(animationSpec = tween(200)) +
104
+ scaleIn(
105
+ initialScale = 0.97f,
106
+ animationSpec = tween(200)
107
+ )
108
+ },
109
+ popExitTransition = {
110
+ fadeOut(animationSpec = tween(200)) +
111
+ scaleOut(
112
+ targetScale = 0.97f,
113
+ animationSpec = tween(200)
114
+ )
115
+ }
116
+ ) {
117
+ val option = getBottomTabOption(index)?.copy(
118
+ onBackHandler = {
119
+ if (index != 0) {
120
+ navController.popBackStack()
121
+ } else {
122
+ navigator.pop()
123
+ }
124
+ }
125
+ )
126
+
127
+ StackScreen(
128
+ content = item.screen,
129
+ navigationOptions = option,
130
+ bottomTabIndex = index
131
+ )
132
+ }
133
+ }
134
+ }
135
+ }
136
+ Column {
137
+ BottomTabBar(
138
+ items = items,
139
+ floatingButton = floatingButton,
140
+ navController = navController,
141
+ onTabSelected = {
142
+ val currentRoute = navController.currentBackStackEntry?.destination?.route
143
+ val targetRoute = "option$it"
144
+ if (currentRoute != targetRoute){
145
+ navController.navigate(targetRoute)
146
+ }
147
+ }
148
+ )
149
+ Spacer(modifier = Modifier.fillMaxWidth().height(min(AppNavigationBar.current, 21.dp) + Spacing.S).background(AppTheme.current.colors.background.surface))
150
+ }
151
+ }
152
+ }
153
+
154
+ data class BottomTabItem(
155
+ val name: String,
156
+ val label: String,
157
+ val icon: String,
158
+ val showDot: Boolean = false,
159
+ val badgeLabel: String? = null,
160
+ val screen: @Composable () -> Unit,
161
+ val options: NavigationOptions? = null,
162
+ val initialParams: Any? = null
163
+ )
164
+
165
+ data class BottomTabFloatingButton(
166
+ val icon: String,
167
+ val label: String,
168
+ val onPress: () -> Unit,
169
+ )
@@ -0,0 +1,216 @@
1
+ package vn.momo.kits.navigation.bottomtab
2
+
3
+ import androidx.compose.animation.core.FastOutSlowInEasing
4
+ import androidx.compose.animation.core.animateDpAsState
5
+ import androidx.compose.animation.core.tween
6
+ import androidx.compose.foundation.background
7
+ import androidx.compose.foundation.layout.Arrangement
8
+ import androidx.compose.foundation.layout.Box
9
+ import androidx.compose.foundation.layout.Column
10
+ import androidx.compose.foundation.layout.Row
11
+ import androidx.compose.foundation.layout.RowScope
12
+ import androidx.compose.foundation.layout.Spacer
13
+ import androidx.compose.foundation.layout.fillMaxSize
14
+ import androidx.compose.foundation.layout.fillMaxWidth
15
+ import androidx.compose.foundation.layout.height
16
+ import androidx.compose.foundation.layout.offset
17
+ import androidx.compose.foundation.layout.padding
18
+ import androidx.compose.foundation.layout.size
19
+ import androidx.compose.foundation.layout.width
20
+ import androidx.compose.foundation.shape.CircleShape
21
+ import androidx.compose.foundation.shape.RoundedCornerShape
22
+ import androidx.compose.runtime.Composable
23
+ import androidx.compose.runtime.getValue
24
+ import androidx.compose.ui.Alignment
25
+ import androidx.compose.ui.Modifier
26
+ import androidx.compose.ui.draw.clip
27
+ import androidx.compose.ui.graphics.Brush
28
+ import androidx.compose.ui.graphics.Color
29
+ import androidx.compose.ui.text.style.TextAlign
30
+ import androidx.compose.ui.text.style.TextOverflow
31
+ import androidx.compose.ui.unit.dp
32
+ import androidx.navigation.NavController
33
+ import androidx.navigation.compose.currentBackStackEntryAsState
34
+ import vn.momo.kits.components.Badge
35
+ import vn.momo.kits.components.Icon
36
+ import vn.momo.kits.components.Text
37
+ import vn.momo.kits.const.AppTheme
38
+ import vn.momo.kits.const.Colors
39
+ import vn.momo.kits.const.Radius
40
+ import vn.momo.kits.const.Spacing
41
+ import vn.momo.kits.const.Typography
42
+ import vn.momo.kits.modifier.noFeedbackClickable
43
+ import vn.momo.kits.platform.getScreenDimensions
44
+
45
+ val floatingButtonWidth = 75.dp
46
+ const val BOTTOM_TAB_BAR_HEIGHT = 56
47
+
48
+ @Composable
49
+ fun BottomTabBar(
50
+ items: List<BottomTabItem>,
51
+ floatingButton: BottomTabFloatingButton? = null,
52
+ navController: NavController,
53
+ onTabSelected: (Int) -> Unit,
54
+ ) {
55
+ val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
56
+ val selectedIndex = items.indexOfFirst { "option${items.indexOf(it)}" == currentRoute }
57
+
58
+ val screenWidth = getScreenDimensions().width.dp
59
+ val mid = items.size / 2
60
+
61
+ val tabWidth = if (floatingButton != null) {
62
+ (screenWidth - floatingButtonWidth) / items.size
63
+ } else {
64
+ screenWidth / items.size
65
+ }
66
+ val adjustSize = if(floatingButton != null && selectedIndex >= mid) floatingButtonWidth else 0.dp
67
+ val targetOffset = tabWidth * selectedIndex + tabWidth / 2 - screenWidth / 2 + adjustSize
68
+
69
+ val indicatorOffsetX by animateDpAsState(
70
+ targetValue = targetOffset,
71
+ animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing),
72
+ label = "IndicatorX"
73
+ )
74
+
75
+ Box(
76
+ contentAlignment = Alignment.BottomCenter
77
+ ) {
78
+ Row(
79
+ modifier = Modifier
80
+ .fillMaxWidth()
81
+ .height(BOTTOM_TAB_BAR_HEIGHT.dp)
82
+ .background(AppTheme.current.colors.background.surface),
83
+ horizontalArrangement = Arrangement.SpaceAround,
84
+ ) {
85
+ if (floatingButton == null) {
86
+ renderTabBarItem(items, 0, items.size, selectedIndex, onTabSelected)
87
+ } else {
88
+ renderTabBarItem(items, 0, mid, selectedIndex, onTabSelected)
89
+ Spacer(modifier = Modifier.width(floatingButtonWidth).padding(horizontal = Spacing.XXS))
90
+ renderTabBarItem(items, mid, items.size, selectedIndex, onTabSelected)
91
+ }
92
+ }
93
+
94
+ Box(modifier = Modifier
95
+ .offset(x = indicatorOffsetX, y = (-BOTTOM_TAB_BAR_HEIGHT + 2).dp)
96
+ .height(2.dp)
97
+ .width(44.dp)
98
+ .background(
99
+ color = AppTheme.current.colors.primary ,
100
+ shape = RoundedCornerShape(bottomStart = 2.dp, bottomEnd = 2.dp)
101
+ )
102
+ )
103
+
104
+ Box(modifier = Modifier
105
+ .fillMaxWidth()
106
+ .height(6.dp)
107
+ .offset(x = 0.dp, y = (-BOTTOM_TAB_BAR_HEIGHT).dp)
108
+ .background(
109
+ brush = Brush.verticalGradient(
110
+ colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.05f))
111
+ )
112
+ )
113
+ )
114
+ floatingButton?.let { FloatingButton(it) }
115
+ }
116
+ }
117
+
118
+ @Composable
119
+ fun RowScope.renderTabBarItem(
120
+ items: List<BottomTabItem>,
121
+ fromIndex: Int,
122
+ toIndex: Int,
123
+ selectedIndex: Int,
124
+ onTabSelected: (Int) -> Unit
125
+ ){
126
+ for (index in fromIndex until toIndex) {
127
+ Box(modifier = Modifier.weight(1f)) {
128
+ TabBarItem(
129
+ item = items[index],
130
+ selected = index == selectedIndex,
131
+ onClick = { onTabSelected(index) }
132
+ )
133
+ }
134
+ }
135
+ }
136
+
137
+ @Composable
138
+ fun TabBarItem(item: BottomTabItem, selected: Boolean, onClick: () -> Unit) {
139
+ Box(modifier = Modifier
140
+ .fillMaxSize()
141
+ .padding(horizontal = Spacing.XXS)
142
+ .noFeedbackClickable {
143
+ onClick()
144
+ },
145
+ contentAlignment = Alignment.BottomStart
146
+ ){
147
+ Column(
148
+ modifier = Modifier
149
+ .fillMaxSize()
150
+ .padding(horizontal = Spacing.XXS)
151
+ .noFeedbackClickable {
152
+ onClick()
153
+ },
154
+ horizontalAlignment = Alignment.CenterHorizontally,
155
+ verticalArrangement = Arrangement.Bottom
156
+ ) {
157
+ Icon(
158
+ source = item.icon,
159
+ color = if (selected) AppTheme.current.colors.primary else AppTheme.current.colors.text.hint)
160
+ Spacer(modifier = Modifier.height(6.dp))
161
+ Box(modifier = Modifier.height(14.dp), contentAlignment = Alignment.Center){
162
+ Text(
163
+ text = item.label,
164
+ color = if (selected) AppTheme.current.colors.primary else AppTheme.current.colors.text.hint,
165
+ style = Typography.labelXsMedium,
166
+ maxLines = 1,
167
+ overflow = TextOverflow.Ellipsis
168
+ )
169
+ }
170
+ }
171
+ if(item.badgeLabel != null){
172
+ Box(modifier = Modifier
173
+ .offset(x = 44.dp, y = (-32).dp)
174
+ ){
175
+ Badge(item.badgeLabel)
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ @Composable
182
+ fun FloatingButton(data: BottomTabFloatingButton) {
183
+ Column(
184
+ modifier = Modifier
185
+ .width(floatingButtonWidth)
186
+ .padding(horizontal = Spacing.XXS)
187
+ .noFeedbackClickable {
188
+ data.onPress()
189
+ },
190
+ verticalArrangement = Arrangement.Bottom,
191
+ horizontalAlignment = Alignment.CenterHorizontally
192
+
193
+ ){
194
+ CurvedContainer()
195
+ Box(
196
+ modifier = Modifier
197
+ .size(48.dp)
198
+ .clip(CircleShape)
199
+ .background(AppTheme.current.colors.primary),
200
+ contentAlignment = Alignment.Center
201
+ ) {
202
+ Icon(source = data.icon, color = Color.White, size = 28.dp)
203
+ }
204
+ Spacer(modifier = Modifier.height(4.dp))
205
+ Text(
206
+ text = data.label,
207
+ style = Typography.labelXsMedium,
208
+ color = Colors.black_01,
209
+ textAlign = TextAlign.Center,
210
+ modifier = Modifier
211
+ .height(14.dp)
212
+ .fillMaxWidth()
213
+ .background(AppTheme.current.colors.primary, shape = RoundedCornerShape(size = Radius.XL))
214
+ )
215
+ }
216
+ }
@@ -0,0 +1,86 @@
1
+ package vn.momo.kits.navigation.bottomtab
2
+
3
+ import androidx.compose.foundation.Canvas
4
+ import androidx.compose.foundation.layout.Box
5
+ import androidx.compose.foundation.layout.height
6
+ import androidx.compose.foundation.layout.offset
7
+ import androidx.compose.foundation.layout.width
8
+ import androidx.compose.runtime.Composable
9
+ import androidx.compose.ui.Alignment
10
+ import androidx.compose.ui.Modifier
11
+ import androidx.compose.ui.graphics.Path
12
+ import androidx.compose.ui.platform.LocalDensity
13
+ import androidx.compose.ui.unit.Dp
14
+ import androidx.compose.ui.unit.IntOffset
15
+ import androidx.compose.ui.unit.dp
16
+ import androidx.compose.ui.zIndex
17
+ import vn.momo.kits.const.AppTheme
18
+
19
+ @Composable
20
+ fun CurvedContainer(
21
+ circleSize: Dp = 48.dp,
22
+ tabBarItemIconSize: Dp = 28.dp,
23
+ circleOverSize: Dp = 2.dp,
24
+ width: Dp = 71.dp,
25
+ height: Dp = 16.dp
26
+ ) {
27
+ val theme = AppTheme.current
28
+ val density = LocalDensity.current
29
+ val widthPx = with(density) { width.toPx()}
30
+
31
+ val circleSizePx = with(density) { circleSize.toPx() }
32
+ val tabBarItemIconSizePx = with(density) { tabBarItemIconSize.toPx() }
33
+ val circleOverSizePx = with(density) { circleOverSize.toPx() }
34
+
35
+ val circlePositionTop = circleSizePx - tabBarItemIconSizePx - 10f
36
+ val circleOverTop = circlePositionTop + circleOverSizePx
37
+ val circleR = circleSizePx / 2
38
+ val circleOverR = circleR + circleOverSizePx
39
+
40
+ val centerX = widthPx / 2
41
+
42
+ val curvedPath = getPath(
43
+ circleOverTop = circleOverTop,
44
+ centerX = centerX,
45
+ circleOverR = circleOverR,
46
+ widthPx = widthPx
47
+ )
48
+
49
+ Box(
50
+ contentAlignment = Alignment.BottomCenter
51
+ ){
52
+ Canvas(
53
+ modifier = Modifier
54
+ .width(width)
55
+ .height(height)
56
+ .offset{ IntOffset(x = 0, y = 24) }
57
+ .zIndex(-1f)
58
+ ) {
59
+ drawPath(path = curvedPath, color = theme.colors.background.surface)
60
+ }
61
+ }
62
+ }
63
+
64
+ private fun getPath(circleOverTop: Float, centerX: Float, circleOverR: Float, widthPx: Float): Path{
65
+ return Path().apply {
66
+ moveTo(0f, circleOverTop)
67
+ lineTo(centerX - circleOverR - 40f, circleOverTop)
68
+
69
+ cubicTo(
70
+ centerX - circleOverR, circleOverTop,
71
+ centerX - circleOverR, 0f,
72
+ centerX, 0f
73
+ )
74
+
75
+ cubicTo(
76
+ centerX + circleOverR, 0f,
77
+ centerX + circleOverR, circleOverTop,
78
+ centerX + circleOverR + 40f, circleOverTop
79
+ )
80
+
81
+ lineTo(widthPx, circleOverTop)
82
+ lineTo(widthPx, 60f)
83
+ lineTo(0f, 60f)
84
+ close()
85
+ }
86
+ }
@@ -0,0 +1,180 @@
1
+ package vn.momo.kits.navigation.component
2
+
3
+ import androidx.compose.animation.AnimatedVisibility
4
+ import androidx.compose.animation.core.CubicBezierEasing
5
+ import androidx.compose.animation.core.LinearEasing
6
+ import androidx.compose.animation.core.animateDpAsState
7
+ import androidx.compose.animation.core.tween
8
+ import androidx.compose.animation.expandHorizontally
9
+ import androidx.compose.animation.fadeIn
10
+ import androidx.compose.animation.fadeOut
11
+ import androidx.compose.animation.shrinkHorizontally
12
+ import androidx.compose.foundation.ScrollState
13
+ import androidx.compose.foundation.background
14
+ import androidx.compose.foundation.interaction.MutableInteractionSource
15
+ import androidx.compose.foundation.layout.Arrangement
16
+ import androidx.compose.foundation.layout.Box
17
+ import androidx.compose.foundation.layout.Row
18
+ import androidx.compose.foundation.layout.Spacer
19
+ import androidx.compose.foundation.layout.fillMaxSize
20
+ import androidx.compose.foundation.layout.height
21
+ import androidx.compose.foundation.layout.offset
22
+ import androidx.compose.foundation.layout.padding
23
+ import androidx.compose.foundation.layout.sizeIn
24
+ import androidx.compose.foundation.layout.width
25
+ import androidx.compose.material3.AlertDialogDefaults
26
+ import androidx.compose.material3.FloatingActionButton
27
+ import androidx.compose.material3.FloatingActionButtonDefaults
28
+ import androidx.compose.material3.FloatingActionButtonElevation
29
+ import androidx.compose.runtime.Composable
30
+ import androidx.compose.runtime.LaunchedEffect
31
+ import androidx.compose.runtime.getValue
32
+ import androidx.compose.runtime.mutableStateOf
33
+ import androidx.compose.runtime.remember
34
+ import androidx.compose.runtime.rememberCoroutineScope
35
+ import androidx.compose.runtime.saveable.rememberSaveable
36
+ import androidx.compose.runtime.setValue
37
+ import androidx.compose.ui.Alignment
38
+ import androidx.compose.ui.Modifier
39
+ import androidx.compose.ui.graphics.Color
40
+ import androidx.compose.ui.unit.Dp
41
+ import androidx.compose.ui.unit.dp
42
+ import kotlinx.coroutines.delay
43
+ import kotlinx.coroutines.flow.MutableStateFlow
44
+ import kotlinx.coroutines.flow.collectLatest
45
+ import kotlinx.coroutines.launch
46
+ import vn.momo.kits.components.Icon
47
+ import vn.momo.kits.components.Text
48
+ import vn.momo.kits.const.Typography
49
+
50
+ enum class FABSize { SMALL, DEFAULT }
51
+ enum class FABPosition { END, CENTER }
52
+
53
+ data class FloatingButtonProps(
54
+ val icon: String,
55
+ val iconColor: Color? = null,
56
+ val label: String? = null,
57
+ val onClick: () -> Unit,
58
+ val size: FABSize = FABSize.DEFAULT,
59
+ val bottom: Dp? = null,
60
+ val scrollState: ScrollState? = null,
61
+ val position: FABPosition? = FABPosition.END,
62
+ )
63
+
64
+ private val FabSmallSize = 36.dp
65
+ private val FabDefaultSize = 48.dp
66
+ private val FabExpandedWidth = 80.dp
67
+ private val FabIconSizeSmall = 12.dp
68
+ private val FabIconSizeDefault = 24.dp
69
+ private val FabPaddingHorizontal = 12.dp
70
+
71
+ @Composable
72
+ fun FloatingButton(
73
+ scrollPosition: Int,
74
+ bottom: Dp,
75
+ onClick: () -> Unit,
76
+ containerColor: Color,
77
+ contentColor: Color = containerColor,
78
+ icon: String,
79
+ iconColor: Color? = null,
80
+ text: String? = null,
81
+ elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(4.dp),
82
+ size: FABSize = FABSize.DEFAULT,
83
+ position: FABPosition = FABPosition.END,
84
+ keyboardSize: Dp = 0.dp,
85
+ ) {
86
+ val scrollStateFlow = remember { MutableStateFlow(scrollPosition) }
87
+ var lastScrollPosition by rememberSaveable { mutableStateOf(0) }
88
+ var isExpanded by rememberSaveable { mutableStateOf(false) }
89
+
90
+ val coroutineScope = rememberCoroutineScope()
91
+
92
+ // Animations
93
+ val baseSize = if (size == FABSize.SMALL) FabSmallSize else FabDefaultSize
94
+ val width by animateDpAsState(
95
+ targetValue = if (isExpanded && text != null) FabExpandedWidth else baseSize,
96
+ animationSpec = tween(100, easing = CubicBezierEasing(0.2f, 0.2f, 0.2f, 0.2f))
97
+ )
98
+
99
+ // Scroll listener
100
+ LaunchedEffect(text) {
101
+ if (text != null) {
102
+ coroutineScope.launch {
103
+ scrollStateFlow.collectLatest { newScroll ->
104
+ isExpanded = newScroll <= lastScrollPosition
105
+ delay(100)
106
+ lastScrollPosition = newScroll
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ LaunchedEffect(scrollPosition) {
113
+ scrollStateFlow.value = scrollPosition
114
+ }
115
+
116
+ Box(
117
+ modifier = Modifier
118
+ .fillMaxSize()
119
+ .padding(end = 12.dp, bottom = bottom)
120
+ .offset(y = -keyboardSize)
121
+ .background(Color.Transparent),
122
+ contentAlignment = if (position == FABPosition.END) Alignment.BottomEnd else Alignment.BottomCenter,
123
+ ) {
124
+ FloatingActionButton(
125
+ onClick = onClick,
126
+ shape = AlertDialogDefaults.shape,
127
+ containerColor = containerColor,
128
+ contentColor = contentColor,
129
+ elevation = elevation,
130
+ interactionSource = remember { MutableInteractionSource() },
131
+ modifier = Modifier
132
+ .sizeIn(minWidth = width)
133
+ .height(baseSize)
134
+ ) {
135
+ Row(
136
+ modifier = Modifier.padding(
137
+ start = if (isExpanded) FabPaddingHorizontal else 0.dp,
138
+ end = if (isExpanded) FabPaddingHorizontal else 0.dp,
139
+ ),
140
+ verticalAlignment = Alignment.CenterVertically,
141
+ horizontalArrangement = if (isExpanded) Arrangement.Start else Arrangement.Center
142
+ ) {
143
+ Icon(
144
+ source = icon,
145
+ size = if (size == FABSize.SMALL) FabIconSizeSmall else FabIconSizeDefault,
146
+ color = iconColor ?: Color.White
147
+ )
148
+
149
+ AnimatedVisibility(
150
+ visible = isExpanded,
151
+ enter = fadeInExpandAnimation,
152
+ exit = fadeOutShrinkAnimation,
153
+ ) {
154
+ Row {
155
+ Spacer(Modifier.width(12.dp))
156
+ Text(
157
+ text = text ?: "",
158
+ color = Color.White,
159
+ style = Typography.actionDefaultBold
160
+ )
161
+ }
162
+ }
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ private val fadeOutShrinkAnimation = fadeOut(
169
+ animationSpec = tween(100, easing = LinearEasing)
170
+ ) + shrinkHorizontally(
171
+ animationSpec = tween(100, easing = LinearEasing),
172
+ shrinkTowards = Alignment.Start
173
+ )
174
+
175
+ private val fadeInExpandAnimation = fadeIn(
176
+ animationSpec = tween(100, easing = LinearEasing)
177
+ ) + expandHorizontally(
178
+ animationSpec = tween(100, easing = LinearEasing),
179
+ expandFrom = Alignment.Start
180
+ )