@momo-kits/native-kits 0.168.1-beta.24 → 0.168.1-beta.25-debug

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 (146) hide show
  1. package/CLAUDE.md +1 -0
  2. package/build.gradle.kts +11 -0
  3. package/compose/build.gradle.kts +198 -0
  4. package/compose/build.gradle.kts.backup +198 -0
  5. package/compose/compose.podspec +47 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +20 -0
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
  8. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +12 -0
  9. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
  10. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +153 -0
  11. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReduceMotion.android.kt +17 -0
  12. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +110 -0
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +62 -0
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +1071 -0
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +81 -0
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +357 -0
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +115 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +542 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +49 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +86 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +189 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +478 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +265 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +235 -0
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +223 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +265 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +185 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +370 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +108 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +308 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +330 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +220 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +207 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +92 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +560 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +86 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +125 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +207 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +501 -0
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +176 -0
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +236 -0
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +180 -0
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +325 -0
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +17 -0
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +194 -0
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +287 -0
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +54 -0
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +50 -0
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +321 -0
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +41 -0
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +499 -0
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +58 -0
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +148 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +116 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +205 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +422 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +732 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +148 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +35 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +232 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +237 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +97 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoLiquidSelection.kt +380 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +436 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +455 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +105 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +106 -0
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +74 -0
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +399 -0
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +39 -0
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +363 -0
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +133 -0
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +127 -0
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +12 -0
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +91 -0
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +74 -0
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReduceMotion.kt +7 -0
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +20 -0
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +118 -0
  124. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +42 -0
  125. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +45 -0
  126. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +32 -0
  127. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +38 -0
  128. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
  129. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +218 -0
  130. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReduceMotion.ios.kt +48 -0
  131. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +138 -0
  132. package/gradle/libs.versions.toml +69 -0
  133. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  134. package/gradle/wrapper/gradle-wrapper.properties +8 -0
  135. package/gradle.properties +26 -0
  136. package/gradlew +252 -0
  137. package/gradlew.bat +94 -0
  138. package/ios/Navigation/BottomTab/BottomTab.swift +16 -3
  139. package/ios/Navigation/BottomTab/BottomTabBar.swift +47 -29
  140. package/ios/Navigation/BottomTab/DuoLiquidSelection.swift +364 -0
  141. package/ios/Navigation/BottomTab/DuoTabBar.swift +156 -69
  142. package/ios/native-kits.podspec +1 -1
  143. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  144. package/ios-demo/README.md +120 -0
  145. package/package.json +1 -1
  146. package/settings.gradle.kts +64 -0
@@ -0,0 +1,436 @@
1
+ package vn.momo.kits.navigation.bottomtab
2
+
3
+ import androidx.compose.foundation.background
4
+ import androidx.compose.foundation.border
5
+ import androidx.compose.foundation.clickable
6
+ import androidx.compose.foundation.combinedClickable
7
+ import androidx.compose.foundation.interaction.MutableInteractionSource
8
+ import androidx.compose.foundation.layout.Arrangement
9
+ import androidx.compose.foundation.layout.Box
10
+ import androidx.compose.foundation.layout.BoxScope
11
+ import androidx.compose.foundation.layout.Column
12
+ import androidx.compose.foundation.layout.Row
13
+ import androidx.compose.foundation.layout.absolutePadding
14
+ import androidx.compose.foundation.layout.fillMaxHeight
15
+ import androidx.compose.foundation.layout.fillMaxSize
16
+ import androidx.compose.foundation.layout.fillMaxWidth
17
+ import androidx.compose.foundation.layout.height
18
+ import androidx.compose.foundation.layout.offset
19
+ import androidx.compose.foundation.layout.padding
20
+ import androidx.compose.foundation.layout.size
21
+ import androidx.compose.foundation.layout.width
22
+ import androidx.compose.foundation.layout.widthIn
23
+ import androidx.compose.foundation.layout.wrapContentSize
24
+ import androidx.compose.foundation.shape.CircleShape
25
+ import androidx.compose.foundation.shape.RoundedCornerShape
26
+ import androidx.compose.runtime.Composable
27
+ import androidx.compose.runtime.CompositionLocalProvider
28
+ import androidx.compose.runtime.compositionLocalOf
29
+ import androidx.compose.runtime.getValue
30
+ import androidx.compose.runtime.mutableStateOf
31
+ import androidx.compose.runtime.remember
32
+ import androidx.compose.runtime.setValue
33
+ import androidx.compose.ui.AbsoluteAlignment
34
+ import androidx.compose.ui.Alignment
35
+ import androidx.compose.ui.Modifier
36
+ import androidx.compose.ui.composed
37
+ import androidx.compose.ui.draw.clip
38
+ import androidx.compose.ui.draw.shadow
39
+ import androidx.compose.ui.graphics.Color
40
+ import androidx.compose.ui.graphics.Shape
41
+ import androidx.compose.ui.graphics.graphicsLayer
42
+ import androidx.compose.ui.layout.onSizeChanged
43
+ import androidx.compose.ui.platform.LocalDensity
44
+ import androidx.compose.ui.semantics.Role
45
+ import androidx.compose.ui.semantics.clearAndSetSemantics
46
+ import androidx.compose.ui.text.style.TextOverflow
47
+ import androidx.compose.ui.unit.Dp
48
+ import androidx.compose.ui.unit.dp
49
+ import vn.momo.kits.application.LocalContext
50
+ import vn.momo.kits.application.MiniAppContext
51
+ import vn.momo.kits.components.Badge
52
+ import vn.momo.kits.components.BadgeDot
53
+ import vn.momo.kits.components.DotSize
54
+ import vn.momo.kits.components.Icon
55
+ import vn.momo.kits.components.Text
56
+ import vn.momo.kits.const.AppTheme
57
+ import vn.momo.kits.const.Spacing
58
+ import vn.momo.kits.const.Typography
59
+ import vn.momo.kits.layout.DuoMetrics
60
+ import vn.momo.kits.layout.ScreenLayout
61
+ import vn.momo.kits.layout.SideBarEdge
62
+ import vn.momo.kits.modifier.noFeedbackClickable
63
+ import vn.momo.kits.modifier.setAutomationId
64
+ import vn.momo.kits.navigation.LocalMaxApi
65
+
66
+ // Figma Fold-Design-System MT1/MT2, mirrored from RN DuoTabBar.tsx.
67
+ private val ITEM_SIZE = DuoMetrics.controlSize
68
+ private val ICON_SIZE = 28.dp
69
+ private val QR_ICON_SIZE = 24.dp
70
+ private val RAIL_PADDING = 6.dp
71
+ private val RAIL_GAP = Spacing.XXS
72
+ // selection bubble overflows the 48 rail cell by 5 above and below
73
+ private val RAIL_BUBBLE_WIDTH = 44.dp
74
+ private val RAIL_BUBBLE_HEIGHT = 58.dp
75
+ private val FLOATING_HEIGHT = DuoMetrics.floatingTabBarHeight
76
+ private val FLOATING_PADDING = Spacing.XS
77
+ // the system bar sizes its pill to the tabs instead of stretching it across the display
78
+ private val FLOATING_ITEM_WIDTH = 80.dp
79
+ // the rail's badge stays inside the capsule: its end sits XXS off the cell's end edge
80
+ private val RAIL_BADGE_MAX_WIDTH = ITEM_SIZE - Spacing.XXS * 2
81
+ private val RAIL_BADGE_END_OFFSET = (ITEM_SIZE - ICON_SIZE) / 2 - Spacing.XXS
82
+ // Figma Home-2026 tab bar "Selection": brand pink at 8%
83
+ private const val SELECTION_TINT_ALPHA = 0x14 / 255f
84
+
85
+ // Space from the window bottom the rail stack and its gap take, so the header side bar stays above it.
86
+ internal val LocalDuoRailReserve = compositionLocalOf { 0.dp }
87
+
88
+ // Rail plus the QR button under it; compressed, the rail is a single button.
89
+ internal fun duoRailHeight(itemCount: Int, hasQr: Boolean, compressed: Boolean = false): Dp {
90
+ val rail = if (compressed) ITEM_SIZE
91
+ else RAIL_PADDING * 2 + ITEM_SIZE * itemCount + RAIL_GAP * (itemCount - 1).coerceAtLeast(0)
92
+ return rail + if (hasQr) Spacing.M + ITEM_SIZE else 0.dp
93
+ }
94
+
95
+ // Overlaps the home indicator inset like the system tab bar (21pt above the bottom with a 34pt inset).
96
+ private fun floatingTabBarBottom(insetBottom: Dp): Dp =
97
+ maxOf(insetBottom - DuoMetrics.floatingTabBarInsetOverlap, Spacing.S)
98
+
99
+ // Space tab content leaves under the floating bar; 0 when the rail sits in the side strip or off Duo.
100
+ internal fun duoFloatingTabBarInset(layout: ScreenLayout): Dp =
101
+ if (layout.isIPhoneDuo && layout.sideBar == null) FLOATING_HEIGHT + floatingTabBarBottom(layout.insetBottom) else 0.dp
102
+
103
+ // iPhone Duo tab bar: rail in the side bar, or a floating bar when unfolded portrait.
104
+ @Composable
105
+ internal fun BoxScope.DuoTabBar(
106
+ layout: ScreenLayout,
107
+ items: List<BottomTabItem>,
108
+ floatingButton: BottomTabFloatingButton?,
109
+ selectedIndex: Int,
110
+ onTabSelected: (Int) -> Unit,
111
+ compression: DuoBarCompression = DuoBarCompression(),
112
+ onTabLongPress: ((Int) -> Unit)? = null,
113
+ ) {
114
+ val maxApi = LocalMaxApi.current
115
+ // a tick per tab the lens enters while dragged
116
+ val onTick = remember(maxApi) { { runCatching { maxApi?.triggerEventVibration("light", callback = null) }; Unit } }
117
+ if (layout.sideBar != null) {
118
+ DuoRailTabBar(layout, items, floatingButton, selectedIndex, onTabSelected, compression, onTabLongPress, onTick)
119
+ } else {
120
+ DuoFloatingTabBar(layout, items, floatingButton, selectedIndex, onTabSelected, onTabLongPress, onTick)
121
+ }
122
+ }
123
+
124
+ // Vertical bar anchored to the bottom of the side bar, QR button below it (Figma MT1-C/E). When the
125
+ // side bar is short it can shrink to one button showing the current tab (HIG "Tab bar compressed");
126
+ // tapping it expands the rail and compresses the toolbar.
127
+ @Composable
128
+ private fun BoxScope.DuoRailTabBar(
129
+ layout: ScreenLayout,
130
+ items: List<BottomTabItem>,
131
+ floatingButton: BottomTabFloatingButton?,
132
+ selectedIndex: Int,
133
+ onTabSelected: (Int) -> Unit,
134
+ compression: DuoBarCompression,
135
+ onTabLongPress: ((Int) -> Unit)?,
136
+ onTick: () -> Unit,
137
+ ) {
138
+ val sideBar = layout.sideBar ?: return
139
+ val isRight = sideBar.edge == SideBarEdge.RIGHT
140
+
141
+ Column(
142
+ modifier = Modifier
143
+ .align(if (isRight) AbsoluteAlignment.BottomRight else AbsoluteAlignment.BottomLeft)
144
+ .width(sideBar.width)
145
+ .padding(bottom = sideBar.bottom)
146
+ .absolutePadding(left = if (isRight) Spacing.M else 0.dp, right = if (isRight) 0.dp else Spacing.M),
147
+ horizontalAlignment = if (isRight) AbsoluteAlignment.Left else AbsoluteAlignment.Right,
148
+ verticalArrangement = Arrangement.spacedBy(Spacing.M),
149
+ ) {
150
+ val current = items.getOrNull(selectedIndex)
151
+ if (compression.tabBarCompressed && current != null) {
152
+ val interactionSource = remember { MutableInteractionSource() }
153
+ Box(
154
+ modifier = Modifier
155
+ .pressLift(interactionSource)
156
+ .size(ITEM_SIZE)
157
+ .tabBarMaterial(CircleShape)
158
+ .clickable(interactionSource = interactionSource, indication = null, onClick = compression.expandTabBar)
159
+ .setAutomationId("bottom_tab_bar"),
160
+ contentAlignment = Alignment.Center,
161
+ ) {
162
+ DuoTabIcon(current, selected = true, badgeMaxWidth = RAIL_BADGE_MAX_WIDTH, inRail = true)
163
+ }
164
+ } else {
165
+ Rail(items, selectedIndex, onTabLongPress, onTick) {
166
+ onTabSelected(it)
167
+ compression.onTabSelected()
168
+ }
169
+ }
170
+ if (floatingButton != null) {
171
+ QrButton(floatingButton, size = ITEM_SIZE)
172
+ }
173
+ }
174
+ }
175
+
176
+ // Column of icon-only tab items with the sliding selection lens.
177
+ @Composable
178
+ private fun Rail(
179
+ items: List<BottomTabItem>,
180
+ selectedIndex: Int,
181
+ onTabLongPress: ((Int) -> Unit)?,
182
+ onTick: () -> Unit,
183
+ onItemClick: (Int) -> Unit,
184
+ ) {
185
+ val selection = rememberDuoLiquidSelection(
186
+ count = items.size,
187
+ pitch = ITEM_SIZE + RAIL_GAP,
188
+ firstCenter = RAIL_PADDING + ITEM_SIZE / 2,
189
+ axis = LiquidAxis.Y,
190
+ index = selectedIndex,
191
+ onSelect = onItemClick,
192
+ onTick = onTick,
193
+ )
194
+ Box(
195
+ Modifier
196
+ .then(selection.barTransform)
197
+ .width(ITEM_SIZE)
198
+ .tabBarMaterial(CircleShape)
199
+ .setAutomationId("bottom_tab_bar")
200
+ .then(selection.dragGesture)
201
+ ) {
202
+ Box(
203
+ Modifier
204
+ .offset(
205
+ x = (ITEM_SIZE - RAIL_BUBBLE_WIDTH) / 2,
206
+ y = RAIL_PADDING - (RAIL_BUBBLE_HEIGHT - ITEM_SIZE) / 2,
207
+ )
208
+ .then(selection.bubbleTransform)
209
+ .size(RAIL_BUBBLE_WIDTH, RAIL_BUBBLE_HEIGHT)
210
+ .selectionTint()
211
+ )
212
+ Column(
213
+ modifier = Modifier.padding(vertical = RAIL_PADDING),
214
+ verticalArrangement = Arrangement.spacedBy(RAIL_GAP),
215
+ ) {
216
+ items.forEachIndexed { index, item ->
217
+ LiquidTab(
218
+ selection = selection,
219
+ index = index,
220
+ onSelect = { onItemClick(index) },
221
+ onLongPress = onTabLongPress?.let { { it(index) } },
222
+ modifier = Modifier.size(ITEM_SIZE),
223
+ ) { active ->
224
+ DuoTabIcon(item, active, badgeMaxWidth = RAIL_BADGE_MAX_WIDTH, inRail = true)
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
230
+
231
+ // Floating labelled bar for the unfolded portrait display, QR pinned in its own platter beside it.
232
+ @Composable
233
+ private fun BoxScope.DuoFloatingTabBar(
234
+ layout: ScreenLayout,
235
+ items: List<BottomTabItem>,
236
+ floatingButton: BottomTabFloatingButton?,
237
+ selectedIndex: Int,
238
+ onTabSelected: (Int) -> Unit,
239
+ onTabLongPress: ((Int) -> Unit)?,
240
+ onTick: () -> Unit,
241
+ ) {
242
+ val density = LocalDensity.current
243
+ var rowWidth by remember { mutableStateOf(0.dp) }
244
+ val cellWidth = rowWidth / items.size.coerceAtLeast(1)
245
+ val selection = rememberDuoLiquidSelection(
246
+ count = items.size,
247
+ pitch = cellWidth,
248
+ firstCenter = cellWidth / 2,
249
+ axis = LiquidAxis.X,
250
+ index = selectedIndex,
251
+ onSelect = onTabSelected,
252
+ onTick = onTick,
253
+ )
254
+
255
+ Row(
256
+ modifier = Modifier
257
+ .align(Alignment.BottomCenter)
258
+ .fillMaxWidth()
259
+ .absolutePadding(
260
+ left = layout.insetLeft + DuoMetrics.floatingTabBarMargin,
261
+ right = layout.insetRight + DuoMetrics.floatingTabBarMargin,
262
+ )
263
+ .padding(bottom = floatingTabBarBottom(layout.insetBottom)),
264
+ horizontalArrangement = Arrangement.spacedBy(Spacing.S, Alignment.CenterHorizontally),
265
+ ) {
266
+ Box(
267
+ modifier = Modifier
268
+ .weight(1f, fill = false)
269
+ .width(FLOATING_ITEM_WIDTH * items.size + FLOATING_PADDING * 2)
270
+ .height(FLOATING_HEIGHT)
271
+ .then(selection.barTransform)
272
+ .tabBarMaterial(RoundedCornerShape(FLOATING_HEIGHT / 2))
273
+ .padding(FLOATING_PADDING)
274
+ .setAutomationId("bottom_tab_bar")
275
+ ) {
276
+ Box(
277
+ Modifier
278
+ .fillMaxSize()
279
+ .onSizeChanged { rowWidth = with(density) { it.width.toDp() } }
280
+ .then(selection.dragGesture)
281
+ ) {
282
+ if (rowWidth > 0.dp) {
283
+ Box(
284
+ Modifier
285
+ .then(selection.bubbleTransform)
286
+ .width(cellWidth)
287
+ .fillMaxHeight()
288
+ .selectionTint()
289
+ )
290
+ }
291
+ Row(Modifier.fillMaxSize()) {
292
+ items.forEachIndexed { index, item ->
293
+ LiquidTab(
294
+ selection = selection,
295
+ index = index,
296
+ onSelect = { onTabSelected(index) },
297
+ onLongPress = onTabLongPress?.let { { it(index) } },
298
+ modifier = Modifier.weight(1f).fillMaxHeight(),
299
+ ) { active ->
300
+ FloatingCellContent(item, active, badgeMaxWidth = cellWidth / 2 - Spacing.XXS)
301
+ }
302
+ }
303
+ }
304
+ }
305
+ }
306
+ if (floatingButton != null) {
307
+ QrButton(floatingButton, size = FLOATING_HEIGHT, Modifier.shadow(3.dp, CircleShape))
308
+ }
309
+ }
310
+ }
311
+
312
+ // A tab drawn twice, inactive with the active layer over it fading in as the lens passes; the tab
313
+ // the lens lands on pops. A tap is the clickable's, a drag over the bar is the bar's.
314
+ @Composable
315
+ private fun LiquidTab(
316
+ selection: DuoLiquidSelection,
317
+ index: Int,
318
+ onSelect: () -> Unit,
319
+ onLongPress: (() -> Unit)?,
320
+ modifier: Modifier = Modifier,
321
+ content: @Composable (active: Boolean) -> Unit,
322
+ ) {
323
+ val interactionSource = remember { MutableInteractionSource() }
324
+ val onLongClick = selection.rememberPressTracking(index, interactionSource, onSelect, onLongPress)
325
+ val pop = selection.rememberIconPop(index)
326
+ Box(
327
+ modifier = modifier
328
+ .combinedClickable(
329
+ interactionSource = interactionSource,
330
+ indication = null,
331
+ role = Role.Tab,
332
+ onLongClick = onLongClick,
333
+ onClick = onSelect,
334
+ )
335
+ .graphicsLayer {
336
+ scaleX = pop.value
337
+ scaleY = pop.value
338
+ },
339
+ contentAlignment = Alignment.Center,
340
+ ) {
341
+ content(false)
342
+ Box(
343
+ Modifier
344
+ .matchParentSize()
345
+ .graphicsLayer { alpha = selection.activeAlpha(index) }
346
+ .clearAndSetSemantics {},
347
+ contentAlignment = Alignment.Center,
348
+ ) {
349
+ content(true)
350
+ }
351
+ }
352
+ }
353
+
354
+ @Composable
355
+ private fun FloatingCellContent(item: BottomTabItem, active: Boolean, badgeMaxWidth: Dp) {
356
+ val colors = AppTheme.current.colors
357
+ Column(
358
+ horizontalAlignment = Alignment.CenterHorizontally,
359
+ verticalArrangement = Arrangement.spacedBy(Spacing.XXS),
360
+ ) {
361
+ DuoTabIcon(item, active, badgeMaxWidth)
362
+ // like the system tab bar the label keeps its design size; Text grows it up to 5pt on the wide Duo displays
363
+ val context = LocalContext.current
364
+ val fixedScale = remember(context) {
365
+ (context ?: MiniAppContext()).copy(userScaleRate = 1f, useOSScaleRate = false)
366
+ }
367
+ CompositionLocalProvider(LocalContext provides fixedScale) {
368
+ Text(
369
+ text = item.label,
370
+ style = Typography.labelXsMedium,
371
+ color = if (active) colors.primary else colors.text.hint,
372
+ maxLines = 1,
373
+ overflow = TextOverflow.Ellipsis,
374
+ )
375
+ }
376
+ }
377
+ }
378
+
379
+ private fun Modifier.selectionTint(): Modifier = composed {
380
+ background(AppTheme.current.colors.primary.copy(alpha = SELECTION_TINT_ALPHA), CircleShape)
381
+ }
382
+
383
+ // Floating bar: the badge starts at the icon's center and grows toward the cell's end edge.
384
+ // Rail: the material clips to the capsule, so the badge ends just inside the rail and grows back
385
+ // over the icon (RN TabBarIcon `end: -3`). Either way it never resizes the icon box.
386
+ @Composable
387
+ private fun DuoTabIcon(item: BottomTabItem, selected: Boolean, badgeMaxWidth: Dp, inRail: Boolean = false) {
388
+ val colors = AppTheme.current.colors
389
+ val badgeLabel = item.badgeLabel?.takeUnless { it.toIntOrNull() == 0 }
390
+ Box {
391
+ Icon(
392
+ source = item.icon,
393
+ size = ICON_SIZE,
394
+ color = if (selected) colors.primary else colors.text.hint,
395
+ )
396
+ when {
397
+ badgeLabel == null && !item.showDot -> Unit
398
+ badgeLabel.isNullOrEmpty() -> BadgeDot(
399
+ size = DotSize.Small,
400
+ modifier = Modifier.align(Alignment.TopEnd),
401
+ )
402
+ else -> Badge(
403
+ badgeLabel,
404
+ modifier = Modifier
405
+ .matchParentSize()
406
+ .wrapContentSize(if (inRail) Alignment.TopEnd else Alignment.TopStart, unbounded = true)
407
+ .offset(x = if (inRail) RAIL_BADGE_END_OFFSET else ICON_SIZE / 2, y = -Spacing.XS)
408
+ .widthIn(max = badgeMaxWidth.coerceAtLeast(0.dp)),
409
+ )
410
+ }
411
+ }
412
+ }
413
+
414
+ @Composable
415
+ private fun QrButton(data: BottomTabFloatingButton, size: Dp, modifier: Modifier = Modifier) {
416
+ Box(
417
+ modifier = modifier
418
+ .size(size)
419
+ .clip(CircleShape)
420
+ .background(AppTheme.current.colors.primary)
421
+ .noFeedbackClickable(onClick = data.onPress)
422
+ .setAutomationId(data.label),
423
+ contentAlignment = Alignment.Center,
424
+ ) {
425
+ Icon(source = data.icon, color = Color.White, size = QR_ICON_SIZE)
426
+ }
427
+ }
428
+
429
+ // RN BarMaterial: surface with a 1dp border.
430
+ @Composable
431
+ private fun Modifier.tabBarMaterial(shape: Shape): Modifier {
432
+ val colors = AppTheme.current.colors
433
+ return shadow(3.dp, shape)
434
+ .background(colors.background.surface)
435
+ .border(1.dp, colors.border.default, shape)
436
+ }
@@ -0,0 +1,169 @@
1
+ package vn.momo.kits.navigation.component
2
+
3
+ import androidx.compose.animation.*
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.foundation.ScrollState
9
+ import androidx.compose.foundation.background
10
+ import androidx.compose.foundation.border
11
+ import androidx.compose.foundation.interaction.MutableInteractionSource
12
+ import androidx.compose.foundation.layout.*
13
+ import androidx.compose.material3.AlertDialogDefaults
14
+ import androidx.compose.material3.FloatingActionButton
15
+ import androidx.compose.material3.FloatingActionButtonDefaults
16
+ import androidx.compose.material3.FloatingActionButtonElevation
17
+ import androidx.compose.runtime.*
18
+ import androidx.compose.runtime.saveable.rememberSaveable
19
+ import androidx.compose.ui.Alignment
20
+ import androidx.compose.ui.Modifier
21
+ import androidx.compose.ui.graphics.Color
22
+ import androidx.compose.ui.unit.Dp
23
+ import androidx.compose.ui.unit.dp
24
+ import kotlinx.coroutines.delay
25
+ import kotlinx.coroutines.flow.MutableStateFlow
26
+ import kotlinx.coroutines.flow.collectLatest
27
+ import kotlinx.coroutines.launch
28
+ import vn.momo.kits.application.IsShowBaseLineDebug
29
+ import vn.momo.kits.components.Icon
30
+ import vn.momo.kits.components.Text
31
+ import vn.momo.kits.const.Colors
32
+ import vn.momo.kits.const.Typography
33
+ import vn.momo.kits.modifier.conditional
34
+ import kotlin.time.Duration.Companion.milliseconds
35
+
36
+ enum class FABSize { SMALL, DEFAULT }
37
+ enum class FABPosition { END, CENTER }
38
+
39
+ data class FloatingButtonProps(
40
+ val icon: String,
41
+ val iconColor: Color? = null,
42
+ val label: String? = null,
43
+ val onClick: () -> Unit,
44
+ val size: FABSize = FABSize.DEFAULT,
45
+ val bottom: Dp? = null,
46
+ val scrollState: ScrollState? = null,
47
+ val position: FABPosition? = FABPosition.END,
48
+ )
49
+
50
+ private val FabSmallSize = 36.dp
51
+ private val FabDefaultSize = 48.dp
52
+ private val FabExpandedWidth = 80.dp
53
+ private val FabIconSizeSmall = 12.dp
54
+ private val FabIconSizeDefault = 24.dp
55
+ private val FabPaddingHorizontal = 12.dp
56
+
57
+ @Composable
58
+ internal fun FloatingButton(
59
+ scrollPosition: Int,
60
+ bottom: Dp,
61
+ onClick: () -> Unit,
62
+ containerColor: Color,
63
+ contentColor: Color = containerColor,
64
+ icon: String,
65
+ iconColor: Color? = null,
66
+ text: String? = null,
67
+ elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(4.dp),
68
+ size: FABSize = FABSize.DEFAULT,
69
+ position: FABPosition = FABPosition.END,
70
+ keyboardSize: Dp = 0.dp,
71
+ ) {
72
+ val scrollStateFlow = remember { MutableStateFlow(scrollPosition) }
73
+ var lastScrollPosition by rememberSaveable { mutableStateOf(0) }
74
+ var isExpanded by rememberSaveable { mutableStateOf(false) }
75
+
76
+ val coroutineScope = rememberCoroutineScope()
77
+
78
+ // Animations
79
+ val baseSize = if (size == FABSize.SMALL) FabSmallSize else FabDefaultSize
80
+ val width by animateDpAsState(
81
+ targetValue = if (isExpanded && text != null) FabExpandedWidth else baseSize,
82
+ animationSpec = tween(100, easing = CubicBezierEasing(0.2f, 0.2f, 0.2f, 0.2f))
83
+ )
84
+
85
+ // Scroll listener
86
+ LaunchedEffect(text) {
87
+ if (text != null) {
88
+ coroutineScope.launch {
89
+ scrollStateFlow.collectLatest { newScroll ->
90
+ isExpanded = newScroll <= lastScrollPosition
91
+ delay(100.milliseconds)
92
+ lastScrollPosition = newScroll
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ LaunchedEffect(scrollPosition) {
99
+ scrollStateFlow.value = scrollPosition
100
+ }
101
+
102
+ Box(
103
+ modifier = Modifier
104
+ .fillMaxSize()
105
+ .padding(end = 12.dp, bottom = bottom)
106
+ .offset(y = -keyboardSize)
107
+ .background(Color.Transparent),
108
+ contentAlignment = if (position == FABPosition.END) Alignment.BottomEnd else Alignment.BottomCenter,
109
+ ) {
110
+ FloatingActionButton(
111
+ onClick = onClick,
112
+ shape = AlertDialogDefaults.shape,
113
+ containerColor = containerColor,
114
+ contentColor = contentColor,
115
+ elevation = elevation,
116
+ interactionSource = remember { MutableInteractionSource() },
117
+ modifier = Modifier
118
+ .sizeIn(minWidth = width)
119
+ .height(baseSize)
120
+ .conditional(IsShowBaseLineDebug) {
121
+ border(1.dp, Colors.blue_03)
122
+ }
123
+ ) {
124
+ Row(
125
+ modifier = Modifier.padding(
126
+ start = if (isExpanded) FabPaddingHorizontal else 0.dp,
127
+ end = if (isExpanded) FabPaddingHorizontal else 0.dp,
128
+ ),
129
+ verticalAlignment = Alignment.CenterVertically,
130
+ horizontalArrangement = if (isExpanded) Arrangement.Start else Arrangement.Center
131
+ ) {
132
+ Icon(
133
+ source = icon,
134
+ size = if (size == FABSize.SMALL) FabIconSizeSmall else FabIconSizeDefault,
135
+ color = iconColor ?: Color.White
136
+ )
137
+
138
+ AnimatedVisibility(
139
+ visible = isExpanded,
140
+ enter = fadeInExpandAnimation,
141
+ exit = fadeOutShrinkAnimation,
142
+ ) {
143
+ Row {
144
+ Spacer(Modifier.width(12.dp))
145
+ Text(
146
+ text = text ?: "",
147
+ color = Color.White,
148
+ style = Typography.actionDefaultBold
149
+ )
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+
157
+ private val fadeOutShrinkAnimation = fadeOut(
158
+ animationSpec = tween(100, easing = LinearEasing)
159
+ ) + shrinkHorizontally(
160
+ animationSpec = tween(100, easing = LinearEasing),
161
+ shrinkTowards = Alignment.Start
162
+ )
163
+
164
+ private val fadeInExpandAnimation = fadeIn(
165
+ animationSpec = tween(100, easing = LinearEasing)
166
+ ) + expandHorizontally(
167
+ animationSpec = tween(100, easing = LinearEasing),
168
+ expandFrom = Alignment.Start
169
+ )