@momo-kits/native-kits 0.167.1-duo.1-debug → 0.167.1-duo.10-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.
- package/CLAUDE.md +32 -0
- package/compose/build.gradle.kts +15 -1
- package/compose/build.gradle.kts.backup +14 -0
- package/compose/compose.podspec +1 -1
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +53 -26
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +13 -11
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +21 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +232 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +11 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +4 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +143 -31
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +100 -66
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +22 -16
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +66 -18
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +23 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +81 -24
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +185 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +4 -1
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +86 -0
- package/gradle.properties +1 -1
- package/ios/Application/ApplicationEnvironment.swift +17 -0
- package/ios/Application/ComposeApi.swift +2 -1
- package/ios/Application/{Components.swift → LegacyHeader.swift} +7 -65
- package/ios/Application/Screen.swift +3 -2
- package/ios/Avatar/Avatar.swift +4 -3
- package/ios/Badge/Badge.swift +6 -4
- package/ios/Badge/BadgeDot.swift +1 -1
- package/ios/Badge/BadgeRibbon.swift +0 -1
- package/ios/Button/Button.swift +15 -12
- package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
- package/ios/Carousel/Carousel.swift +29 -12
- package/ios/Checkbox/Checkbox.swift +7 -3
- package/ios/Chip/Chip.swift +5 -4
- package/ios/Collapse/Collapse.swift +70 -19
- package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
- package/ios/DateTimePicker/DateTimePicker.swift +54 -20
- package/ios/DateTimePicker/WheelPicker.swift +103 -105
- package/ios/Divider/Divider.swift +37 -4
- package/ios/Extensions/Color++.swift +38 -9
- package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
- package/ios/Extensions/SkeletonUICompat.swift +11 -0
- package/ios/Icon/Icon.swift +21 -23
- package/ios/Icon/Icons.swift +1329 -0
- package/ios/IconButton/IconButton.swift +15 -7
- package/ios/Image/Image.swift +7 -23
- package/ios/Information/Information.swift +27 -9
- package/ios/Input/ErrorView.swift +2 -2
- package/ios/Input/Input.swift +160 -147
- package/ios/Input/InputPhoneNumber.swift +12 -9
- package/ios/Input/InputSearch.swift +102 -75
- package/ios/Input/InputTextArea.swift +128 -145
- package/ios/InputDropDown/InputDropDown.swift +10 -7
- package/ios/InputMoney/InputMoney.swift +33 -11
- package/ios/InputOTP/InputOTP.swift +22 -12
- package/ios/Layout/Card.swift +50 -0
- package/ios/Layout/GridContext.swift +123 -3
- package/ios/Layout/Item.swift +5 -4
- package/ios/Layout/ScreenLayout.swift +312 -0
- package/ios/Layout/Section.swift +4 -100
- package/ios/Loader/Loader.swift +5 -9
- package/ios/Lottie/LottieView.swift +14 -2
- package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +97 -8
- package/ios/Navigation/BottomTab/BottomTab.swift +187 -0
- package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +15 -7
- package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
- package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
- package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
- package/ios/Navigation/Component/Header.swift +493 -0
- package/ios/Navigation/Component/HeaderBackground.swift +134 -0
- package/ios/Navigation/Component/HeaderJourney.swift +73 -0
- package/ios/Navigation/Component/HeaderLocation.swift +48 -0
- package/ios/Navigation/Component/HeaderRight.swift +386 -0
- package/ios/Navigation/Component/HeaderTitle.swift +36 -0
- package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
- package/ios/Navigation/Component/SideBar.swift +176 -0
- package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -5
- package/ios/Navigation/HostBackGestureBridge.swift +57 -0
- package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +5 -1
- package/ios/Navigation/Navigation.swift +190 -0
- package/ios/{Application/Navigation → Navigation}/NavigationContainer.swift +92 -81
- package/ios/{Application/Navigation → Navigation}/Navigator.swift +185 -36
- package/ios/Navigation/StackScreen.swift +782 -0
- package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
- package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
- package/ios/Pagination/PaginationScroll.swift +2 -2
- package/ios/Popup/PopupDisplay.swift +72 -136
- package/ios/Popup/PopupInput.swift +10 -13
- package/ios/Popup/PopupLayout.swift +140 -0
- package/ios/Popup/PopupNotify.swift +56 -73
- package/ios/Popup/PopupPromotion.swift +31 -47
- package/ios/PopupView/FullscreenPopup.swift +13 -19
- package/ios/PopupView/PopupView.swift +1 -1
- package/ios/PopupView/Utils++.swift +0 -8
- package/ios/ProgressInfo/ProgressInfo.swift +4 -2
- package/ios/Radio/Radio.swift +2 -1
- package/ios/Rating/Rating.swift +9 -1
- package/ios/Resources/dot_loading.json +558 -0
- package/ios/Resources/lottie_circle_loader.json +1 -0
- package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
- package/ios/Skeleton/Skeleton.swift +22 -27
- package/ios/Slider/Slider.swift +16 -18
- package/ios/Stepper/Stepper.swift +6 -1
- package/ios/Steps/Steps.swift +8 -6
- package/ios/SuggestAction/SuggestAction.swift +5 -0
- package/ios/Swipeable/SwipeCell.swift +10 -6
- package/ios/Switch/Switch.swift +22 -20
- package/ios/TabView/TabView.swift +102 -21
- package/ios/Tag/Tag.swift +15 -4
- package/ios/Template/Logo/Logo.swift +2 -1
- package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
- package/ios/Title/Title.swift +6 -17
- package/ios/Tooltip/Tooltip.swift +29 -14
- package/ios/Typography/Text.swift +61 -19
- package/ios/Uploader/Uploader.swift +3 -1
- package/ios/native-kits.podspec +2 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
- package/package.json +1 -1
- package/scripts/gen-ios-icons.mjs +36 -0
- package/.claude/settings.local.json +0 -185
- package/ios/Application/HeaderRight.swift +0 -306
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
- package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
- package/ios/Application/Navigation/HeaderTitle.swift +0 -57
- package/ios/Application/Navigation/NavigationOptions.swift +0 -109
- package/ios/Application/Navigation/NavigatorManager.swift +0 -22
- package/ios/Application/Navigation/component/Header.swift +0 -178
- package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
- package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
- package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
- package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
- package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
- package/ios/Application/StackScreen.swift +0 -492
- package/local.properties +0 -8
- /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
|
@@ -22,7 +22,6 @@ import vn.momo.kits.components.Text
|
|
|
22
22
|
import vn.momo.kits.const.Spacing
|
|
23
23
|
import vn.momo.kits.const.Typography
|
|
24
24
|
import vn.momo.kits.modifier.activeOpacityClickable
|
|
25
|
-
import vn.momo.kits.platform.getScreenDimensions
|
|
26
25
|
|
|
27
26
|
@Composable
|
|
28
27
|
internal fun HeaderLocation(
|
|
@@ -32,7 +31,7 @@ internal fun HeaderLocation(
|
|
|
32
31
|
if (data !is HeaderTitle.Location) return
|
|
33
32
|
if (data.isLoading) return TitleLocationShimmer()
|
|
34
33
|
|
|
35
|
-
val maxWidth =
|
|
34
|
+
val maxWidth = headerTitleMaxWidth()
|
|
36
35
|
val tint = parseHeaderTint(data.tintColor) ?: tintIconColor
|
|
37
36
|
|
|
38
37
|
Column(
|
|
@@ -6,6 +6,7 @@ import androidx.compose.foundation.background
|
|
|
6
6
|
import androidx.compose.foundation.border
|
|
7
7
|
import androidx.compose.foundation.layout.Arrangement
|
|
8
8
|
import androidx.compose.foundation.layout.Box
|
|
9
|
+
import androidx.compose.foundation.layout.Column
|
|
9
10
|
import androidx.compose.foundation.layout.Row
|
|
10
11
|
import androidx.compose.foundation.layout.height
|
|
11
12
|
import androidx.compose.foundation.layout.offset
|
|
@@ -25,7 +26,9 @@ import androidx.compose.ui.Alignment
|
|
|
25
26
|
import androidx.compose.ui.Modifier
|
|
26
27
|
import androidx.compose.ui.draw.clip
|
|
27
28
|
import androidx.compose.ui.graphics.Color
|
|
29
|
+
import androidx.compose.ui.unit.Dp
|
|
28
30
|
import androidx.compose.ui.unit.dp
|
|
31
|
+
import vn.momo.kits.layout.DuoMetrics
|
|
29
32
|
import vn.momo.kits.application.LocalContext
|
|
30
33
|
import vn.momo.kits.application.MiniAppContext
|
|
31
34
|
import vn.momo.kits.components.BadgeDot
|
|
@@ -59,11 +62,29 @@ fun HeaderRight(
|
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
private const val HELP_CENTER_APP_ID = "vn.momo.helpcenter"
|
|
66
|
+
|
|
67
|
+
private fun usesHelpCenter(context: MiniAppContext?): Boolean =
|
|
68
|
+
context != null && context.appId != HELP_CENTER_APP_ID
|
|
69
|
+
|
|
70
|
+
private fun showsShortcut(headerRight: HeaderRight.Toolkit, context: MiniAppContext?): Boolean =
|
|
71
|
+
headerRight.useShortcut && !(headerRight.useMore && context == null)
|
|
72
|
+
|
|
73
|
+
// Height of the vertical toolkit: the optional shortcut, then the help/home capsule.
|
|
74
|
+
@Composable
|
|
75
|
+
internal fun toolkitColumnHeight(headerRight: HeaderRight.Toolkit): Dp {
|
|
76
|
+
val context = LocalContext.current
|
|
77
|
+
val shortcut = if (showsShortcut(headerRight, context)) DuoMetrics.controlSize + Spacing.M else 0.dp
|
|
78
|
+
val groupCount = if (usesHelpCenter(context)) 2 else 1
|
|
79
|
+
return shortcut + if (groupCount > 1) DuoMetrics.itemPitch * groupCount else DuoMetrics.controlSize
|
|
80
|
+
}
|
|
81
|
+
|
|
62
82
|
@Composable
|
|
63
83
|
internal fun Toolkit(
|
|
64
84
|
headerRight: HeaderRight.Toolkit,
|
|
65
85
|
tintColor: Color? = null,
|
|
66
|
-
headerColor: HeaderColor
|
|
86
|
+
headerColor: HeaderColor,
|
|
87
|
+
vertical: Boolean = false,
|
|
67
88
|
) {
|
|
68
89
|
val api = LocalMaxApi.current
|
|
69
90
|
val context = LocalContext.current
|
|
@@ -103,6 +124,26 @@ internal fun Toolkit(
|
|
|
103
124
|
}
|
|
104
125
|
}
|
|
105
126
|
|
|
127
|
+
fun onPressHelpCenter() {
|
|
128
|
+
if (context == null) return
|
|
129
|
+
api?.showHelpCenter(
|
|
130
|
+
mapOf(
|
|
131
|
+
"appId" to context.appId,
|
|
132
|
+
"code" to context.appCode,
|
|
133
|
+
"name" to context.appName,
|
|
134
|
+
"icon" to context.appIcon,
|
|
135
|
+
"description" to context.description
|
|
136
|
+
)
|
|
137
|
+
) {}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fun onPressClose() {
|
|
141
|
+
if (headerRight.useCloseIcon) api?.dismiss { } else api?.dismissAll { }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
val useHelpCenter = usesHelpCenter(context)
|
|
145
|
+
val closeIcon = if (headerRight.useCloseIcon) "16_navigation_close_circle" else "16_basic_home"
|
|
146
|
+
|
|
106
147
|
val navButtonConfig = getNavigationButtonConfig(
|
|
107
148
|
headerRight,
|
|
108
149
|
::onPressShortcut,
|
|
@@ -113,12 +154,7 @@ internal fun Toolkit(
|
|
|
113
154
|
group.items.any { it.showBadge }
|
|
114
155
|
}
|
|
115
156
|
|
|
116
|
-
val isShowShortcut
|
|
117
|
-
derivedStateOf {
|
|
118
|
-
(headerRight.useShortcut == true) &&
|
|
119
|
-
!(headerRight.useMore == true && context == null)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
157
|
+
val isShowShortcut = showsShortcut(headerRight, context)
|
|
122
158
|
|
|
123
159
|
val icon by remember(navButtonConfig.icon, isFavorite) {
|
|
124
160
|
derivedStateOf {
|
|
@@ -127,6 +163,37 @@ internal fun Toolkit(
|
|
|
127
163
|
}
|
|
128
164
|
}
|
|
129
165
|
|
|
166
|
+
if (vertical) {
|
|
167
|
+
Column(verticalArrangement = Arrangement.spacedBy(Spacing.M)) {
|
|
168
|
+
if (isShowShortcut) {
|
|
169
|
+
SideBarButton(
|
|
170
|
+
icon = icon,
|
|
171
|
+
headerColor = headerColor,
|
|
172
|
+
enabled = !isLoading,
|
|
173
|
+
showBadge = showBadge,
|
|
174
|
+
onClick = navButtonConfig.onPress,
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
SideBarGroup(headerColor = headerColor, itemCount = if (useHelpCenter) 2 else 1) {
|
|
178
|
+
if (useHelpCenter) {
|
|
179
|
+
SideBarGroupItem(
|
|
180
|
+
icon = "help_center",
|
|
181
|
+
headerColor = headerColor,
|
|
182
|
+
automationId = "btn_navigation_help_center",
|
|
183
|
+
onClick = ::onPressHelpCenter,
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
SideBarGroupItem(
|
|
187
|
+
icon = closeIcon,
|
|
188
|
+
headerColor = headerColor,
|
|
189
|
+
automationId = "btn_navigation_close",
|
|
190
|
+
onClick = ::onPressClose,
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
130
197
|
Row(
|
|
131
198
|
verticalAlignment = Alignment.CenterVertically
|
|
132
199
|
) {
|
|
@@ -150,22 +217,12 @@ internal fun Toolkit(
|
|
|
150
217
|
.clip(shape = RoundedCornerShape(14.dp))
|
|
151
218
|
.background(headerColor.backgroundButton)
|
|
152
219
|
) {
|
|
153
|
-
if (
|
|
220
|
+
if (useHelpCenter) {
|
|
154
221
|
Icon(
|
|
155
222
|
source = "help_center",
|
|
156
223
|
size = 20.dp,
|
|
157
224
|
color = headerColor.tintIconColor,
|
|
158
|
-
modifier = Modifier.padding(4.dp).activeOpacityClickable
|
|
159
|
-
api?.showHelpCenter(
|
|
160
|
-
mapOf(
|
|
161
|
-
"appId" to context.appId,
|
|
162
|
-
"code" to context.appCode,
|
|
163
|
-
"name" to context.appName,
|
|
164
|
-
"icon" to context.appIcon,
|
|
165
|
-
"description" to context.description
|
|
166
|
-
)
|
|
167
|
-
) {}
|
|
168
|
-
}
|
|
225
|
+
modifier = Modifier.padding(4.dp).activeOpacityClickable(onClick = ::onPressHelpCenter)
|
|
169
226
|
)
|
|
170
227
|
Box(
|
|
171
228
|
modifier = Modifier
|
|
@@ -175,12 +232,10 @@ internal fun Toolkit(
|
|
|
175
232
|
)
|
|
176
233
|
}
|
|
177
234
|
Icon(
|
|
178
|
-
source =
|
|
235
|
+
source = closeIcon,
|
|
179
236
|
size = 20.dp,
|
|
180
237
|
color = headerColor.tintIconColor,
|
|
181
|
-
modifier = Modifier.padding(4.dp).activeOpacityClickable
|
|
182
|
-
api?.dismissAll { }
|
|
183
|
-
}
|
|
238
|
+
modifier = Modifier.padding(4.dp).activeOpacityClickable(onClick = ::onPressClose)
|
|
184
239
|
)
|
|
185
240
|
}
|
|
186
241
|
}
|
|
@@ -270,7 +325,9 @@ sealed interface HeaderRight {
|
|
|
270
325
|
val useMore: Boolean = false,
|
|
271
326
|
val useSystemTools: Boolean = true,
|
|
272
327
|
val tools: List<ToolGroup> = emptyList(),
|
|
273
|
-
val toolCallback: ((String) -> Unit)? = { _ -> }
|
|
328
|
+
val toolCallback: ((String) -> Unit)? = { _ -> },
|
|
329
|
+
// Close dismisses only this mini app instead of returning home.
|
|
330
|
+
val useCloseIcon: Boolean = false,
|
|
274
331
|
) : HeaderRight
|
|
275
332
|
}
|
|
276
333
|
|
|
@@ -32,7 +32,6 @@ import vn.momo.kits.const.Typography
|
|
|
32
32
|
import vn.momo.kits.const.getFontFamily
|
|
33
33
|
import vn.momo.kits.modifier.activeOpacityClickable
|
|
34
34
|
import vn.momo.kits.navigation.component.HeaderTitle.User
|
|
35
|
-
import vn.momo.kits.platform.getScreenDimensions
|
|
36
35
|
|
|
37
36
|
@Composable
|
|
38
37
|
internal fun HeaderUser(
|
|
@@ -44,7 +43,7 @@ internal fun HeaderUser(
|
|
|
44
43
|
return TitleUserShimmer()
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
val maxWidth =
|
|
46
|
+
val maxWidth = headerTitleMaxWidth()
|
|
48
47
|
|
|
49
48
|
Row(
|
|
50
49
|
modifier = Modifier
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
package vn.momo.kits.navigation.component
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.background
|
|
4
|
+
import androidx.compose.foundation.border
|
|
5
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
6
|
+
import androidx.compose.foundation.layout.Box
|
|
7
|
+
import androidx.compose.foundation.layout.BoxScope
|
|
8
|
+
import androidx.compose.foundation.layout.Column
|
|
9
|
+
import androidx.compose.foundation.layout.ColumnScope
|
|
10
|
+
import androidx.compose.foundation.layout.Spacer
|
|
11
|
+
import androidx.compose.foundation.layout.absolutePadding
|
|
12
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
13
|
+
import androidx.compose.foundation.layout.height
|
|
14
|
+
import androidx.compose.foundation.layout.offset
|
|
15
|
+
import androidx.compose.foundation.layout.size
|
|
16
|
+
import androidx.compose.foundation.layout.width
|
|
17
|
+
import androidx.compose.foundation.shape.CircleShape
|
|
18
|
+
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
19
|
+
import androidx.compose.runtime.Composable
|
|
20
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
21
|
+
import androidx.compose.ui.AbsoluteAlignment
|
|
22
|
+
import androidx.compose.ui.Alignment
|
|
23
|
+
import androidx.compose.ui.Modifier
|
|
24
|
+
import androidx.compose.ui.draw.clip
|
|
25
|
+
import androidx.compose.ui.draw.shadow
|
|
26
|
+
import androidx.compose.ui.graphics.Shape
|
|
27
|
+
import androidx.compose.ui.layout.layout
|
|
28
|
+
import androidx.compose.ui.unit.Dp
|
|
29
|
+
import androidx.compose.ui.unit.constrainHeight
|
|
30
|
+
import androidx.compose.ui.unit.dp
|
|
31
|
+
import vn.momo.kits.components.BadgeDot
|
|
32
|
+
import vn.momo.kits.components.DotSize
|
|
33
|
+
import vn.momo.kits.components.Icon
|
|
34
|
+
import vn.momo.kits.const.Colors
|
|
35
|
+
import vn.momo.kits.const.Radius
|
|
36
|
+
import vn.momo.kits.const.Spacing
|
|
37
|
+
import vn.momo.kits.layout.DuoMetrics
|
|
38
|
+
import vn.momo.kits.layout.SideBarEdge
|
|
39
|
+
import vn.momo.kits.layout.getScreenLayout
|
|
40
|
+
import vn.momo.kits.modifier.activeOpacityClickable
|
|
41
|
+
import vn.momo.kits.modifier.conditional
|
|
42
|
+
import vn.momo.kits.modifier.setAutomationId
|
|
43
|
+
import vn.momo.kits.navigation.LocalNavigator
|
|
44
|
+
import vn.momo.kits.navigation.LocalOptions
|
|
45
|
+
import vn.momo.kits.navigation.bottomtab.LocalDuoBarCompression
|
|
46
|
+
|
|
47
|
+
private val SIDE_BAR_ICON_SIZE = 24.dp
|
|
48
|
+
|
|
49
|
+
// iPhone Duo: back and toolkit leave the bar for a column in the side safe area, as the system bars do.
|
|
50
|
+
// bottomInset is what footer / tab bar / home indicator take from the bottom, gap included; the column stays above it.
|
|
51
|
+
@Composable
|
|
52
|
+
internal fun BoxScope.HeaderSideBar(bottomInset: Dp, modifier: Modifier = Modifier) {
|
|
53
|
+
val options = LocalOptions.current
|
|
54
|
+
val navigator = LocalNavigator.current
|
|
55
|
+
val sideBar = getScreenLayout().sideBar ?: return
|
|
56
|
+
val toolkit = options.headerRight as? HeaderRight.Toolkit
|
|
57
|
+
if (options.headerType == HeaderType.None || (options.hiddenBack && toolkit == null)) return
|
|
58
|
+
|
|
59
|
+
val headerColor = getHeaderColor(options.headerType, headerOpacity(), options.tintColor, Colors.black_17)
|
|
60
|
+
val isRight = sideBar.edge == SideBarEdge.RIGHT
|
|
61
|
+
val compression = LocalDuoBarCompression.current
|
|
62
|
+
|
|
63
|
+
// full height, so the tab bar compression decision matches this screen's toolkit
|
|
64
|
+
val fullHeight = DuoMetrics.controlSize + (toolkit?.let { Spacing.M + toolkitColumnHeight(it) } ?: 0.dp)
|
|
65
|
+
LaunchedEffect(fullHeight, compression.setToolbarHeight) { compression.setToolbarHeight(fullHeight) }
|
|
66
|
+
|
|
67
|
+
Column(
|
|
68
|
+
modifier = modifier
|
|
69
|
+
.align(if (isRight) AbsoluteAlignment.TopRight else AbsoluteAlignment.TopLeft)
|
|
70
|
+
.width(sideBar.width)
|
|
71
|
+
.layout { measurable, constraints ->
|
|
72
|
+
val placeable = measurable.measure(constraints.copy(minHeight = 0))
|
|
73
|
+
val maxY = constraints.maxHeight - bottomInset.roundToPx() - placeable.height
|
|
74
|
+
val y = minOf(sideBar.top.roundToPx(), maxY).coerceAtLeast(0)
|
|
75
|
+
layout(placeable.width, constraints.constrainHeight(y + placeable.height)) {
|
|
76
|
+
placeable.place(0, y)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
.absolutePadding(left = if (isRight) Spacing.M else 0.dp, right = if (isRight) 0.dp else Spacing.M),
|
|
80
|
+
horizontalAlignment = if (isRight) AbsoluteAlignment.Left else AbsoluteAlignment.Right,
|
|
81
|
+
verticalArrangement = Arrangement.spacedBy(Spacing.M),
|
|
82
|
+
) {
|
|
83
|
+
if (options.hiddenBack) {
|
|
84
|
+
// keep the slot so the toolkit does not jump between root and pushed screens
|
|
85
|
+
Spacer(Modifier.size(DuoMetrics.controlSize))
|
|
86
|
+
} else {
|
|
87
|
+
SideBarButton(
|
|
88
|
+
icon = "arrow-back",
|
|
89
|
+
headerColor = headerColor,
|
|
90
|
+
automationId = "btn_navigation_back",
|
|
91
|
+
onClick = { navigator.onBackSafe { } },
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
when {
|
|
95
|
+
toolkit == null || compression.toolbarHidden -> Unit
|
|
96
|
+
// HIG "Toolbar compressed": tools fold into one button, distinct from the useMore icon
|
|
97
|
+
compression.toolbarCompressed -> SideBarButton(
|
|
98
|
+
icon = "navigation_menu",
|
|
99
|
+
headerColor = headerColor,
|
|
100
|
+
showBadge = toolkit.tools.any { group -> group.items.any { it.showBadge } },
|
|
101
|
+
automationId = "btn_navigation_toolbar",
|
|
102
|
+
onClick = compression.expandToolbar,
|
|
103
|
+
)
|
|
104
|
+
else -> Toolkit(headerRight = toolkit, tintColor = options.tintColor, headerColor = headerColor, vertical = true)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@Composable
|
|
110
|
+
internal fun SideBarButton(
|
|
111
|
+
icon: String,
|
|
112
|
+
headerColor: HeaderColor,
|
|
113
|
+
onClick: () -> Unit,
|
|
114
|
+
enabled: Boolean = true,
|
|
115
|
+
showBadge: Boolean = false,
|
|
116
|
+
automationId: String? = null,
|
|
117
|
+
) {
|
|
118
|
+
Box {
|
|
119
|
+
Box(
|
|
120
|
+
modifier = Modifier
|
|
121
|
+
.size(DuoMetrics.controlSize)
|
|
122
|
+
.sideBarGlass(headerColor, CircleShape)
|
|
123
|
+
.activeOpacityClickable(enabled = enabled, onClick = onClick)
|
|
124
|
+
.conditional(automationId != null) { setAutomationId(automationId.orEmpty()) },
|
|
125
|
+
contentAlignment = Alignment.Center,
|
|
126
|
+
) {
|
|
127
|
+
Icon(source = icon, size = SIDE_BAR_ICON_SIZE, color = headerColor.tintIconColor)
|
|
128
|
+
}
|
|
129
|
+
if (showBadge) {
|
|
130
|
+
BadgeDot(
|
|
131
|
+
size = DotSize.Small,
|
|
132
|
+
modifier = Modifier.align(Alignment.TopEnd).offset(x = -Spacing.XXS, y = Spacing.XXS),
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Capsule grouping related controls, one item per 50pt.
|
|
139
|
+
@Composable
|
|
140
|
+
internal fun SideBarGroup(
|
|
141
|
+
headerColor: HeaderColor,
|
|
142
|
+
itemCount: Int,
|
|
143
|
+
content: @Composable ColumnScope.() -> Unit,
|
|
144
|
+
) {
|
|
145
|
+
val height = if (itemCount > 1) DuoMetrics.itemPitch * itemCount else DuoMetrics.controlSize
|
|
146
|
+
Column(
|
|
147
|
+
modifier = Modifier
|
|
148
|
+
.width(DuoMetrics.controlSize)
|
|
149
|
+
.height(height)
|
|
150
|
+
.sideBarGlass(headerColor, RoundedCornerShape(Radius.XL)),
|
|
151
|
+
content = content,
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@Composable
|
|
156
|
+
internal fun ColumnScope.SideBarGroupItem(
|
|
157
|
+
icon: String,
|
|
158
|
+
headerColor: HeaderColor,
|
|
159
|
+
automationId: String,
|
|
160
|
+
onClick: () -> Unit,
|
|
161
|
+
) {
|
|
162
|
+
Box(
|
|
163
|
+
modifier = Modifier
|
|
164
|
+
.weight(1f)
|
|
165
|
+
.fillMaxWidth()
|
|
166
|
+
.activeOpacityClickable(onClick = onClick)
|
|
167
|
+
.setAutomationId(automationId),
|
|
168
|
+
contentAlignment = Alignment.Center,
|
|
169
|
+
) {
|
|
170
|
+
Icon(source = icon, size = SIDE_BAR_ICON_SIZE, color = headerColor.tintIconColor)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Dark glass under light icons (banner headers), light glass otherwise. The light one is opaque:
|
|
175
|
+
// Compose has no backdrop blur and a translucent fill shows its own shadow through (RN uses 85% white).
|
|
176
|
+
private fun Modifier.sideBarGlass(headerColor: HeaderColor, shape: Shape): Modifier =
|
|
177
|
+
if (headerColor.tintIconColor == Colors.black_01) {
|
|
178
|
+
clip(shape)
|
|
179
|
+
.background(Colors.black_20.copy(alpha = 0.5f))
|
|
180
|
+
.border(0.5.dp, Colors.black_01.copy(alpha = 0.2f), shape)
|
|
181
|
+
} else {
|
|
182
|
+
shadow(3.dp, shape)
|
|
183
|
+
.background(Colors.black_01)
|
|
184
|
+
.border(0.5.dp, Colors.black_20.copy(alpha = 0.08f), shape)
|
|
185
|
+
}
|
|
@@ -14,6 +14,9 @@ data class ScreenDimension(
|
|
|
14
14
|
|
|
15
15
|
expect fun getPlatformName(): String
|
|
16
16
|
|
|
17
|
+
// iPhone idiom only; iPad windows and Android never get the iPhone / iPhone Duo layout.
|
|
18
|
+
expect fun isPhoneIdiom(): Boolean
|
|
19
|
+
|
|
17
20
|
@Composable
|
|
18
21
|
expect fun getScreenDimensions(): ScreenDimension
|
|
19
22
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package vn.momo.kits.platform
|
|
2
|
+
|
|
3
|
+
import androidx.compose.runtime.Composable
|
|
4
|
+
import androidx.compose.runtime.Immutable
|
|
5
|
+
import androidx.compose.ui.unit.Dp
|
|
6
|
+
|
|
7
|
+
@Immutable
|
|
8
|
+
internal data class ReservedRegion(val x: Dp, val y: Dp, val width: Dp, val height: Dp)
|
|
9
|
+
|
|
10
|
+
// Regions the system occludes (iOS 27.1+), in the coordinates of the Compose root view: the
|
|
11
|
+
// iPhone Duo camera, and the status bar cluster as it grows for a call or a Live Activity.
|
|
12
|
+
@Composable
|
|
13
|
+
internal expect fun rememberReservedRegions(): List<ReservedRegion>
|
|
@@ -95,9 +95,12 @@ fun getAppStatusBarHeight(): Dp {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
internal val horizontalSafeAreaInsets: WindowInsets
|
|
99
|
+
@Composable get() = WindowInsets.systemBars.union(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal)
|
|
100
|
+
|
|
98
101
|
// Left/right safe area like SwiftUI (landscape camera/notch); consumed, so nested StackScreens don't pad twice.
|
|
99
102
|
internal fun Modifier.horizontalSafeAreaPadding(): Modifier = composed {
|
|
100
|
-
windowInsetsPadding(
|
|
103
|
+
windowInsetsPadding(horizontalSafeAreaInsets)
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
@Composable
|
|
@@ -39,11 +39,15 @@ import platform.Foundation.NSBundle
|
|
|
39
39
|
import platform.UIKit.UIColor
|
|
40
40
|
import platform.UIKit.UIDevice
|
|
41
41
|
import platform.UIKit.UIScreen
|
|
42
|
+
import platform.UIKit.UIUserInterfaceIdiomPhone
|
|
42
43
|
import vn.momo.kits.navigation.AttachHostBackGesture
|
|
43
44
|
import vn.momo.kits.navigation.Navigator
|
|
44
45
|
|
|
45
46
|
actual fun getPlatformName(): String = "iOS"
|
|
46
47
|
|
|
48
|
+
actual fun isPhoneIdiom(): Boolean =
|
|
49
|
+
UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone
|
|
50
|
+
|
|
47
51
|
@OptIn(ExperimentalForeignApi::class)
|
|
48
52
|
@Composable
|
|
49
53
|
actual fun getScreenDimensions(): ScreenDimension = memScoped {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
package vn.momo.kits.platform
|
|
2
|
+
|
|
3
|
+
import androidx.compose.runtime.Composable
|
|
4
|
+
import androidx.compose.runtime.DisposableEffect
|
|
5
|
+
import androidx.compose.runtime.getValue
|
|
6
|
+
import androidx.compose.runtime.mutableStateOf
|
|
7
|
+
import androidx.compose.runtime.remember
|
|
8
|
+
import androidx.compose.runtime.setValue
|
|
9
|
+
import androidx.compose.ui.uikit.LocalUIViewController
|
|
10
|
+
import androidx.compose.ui.unit.dp
|
|
11
|
+
import kotlinx.cinterop.BetaInteropApi
|
|
12
|
+
import kotlinx.cinterop.ExperimentalForeignApi
|
|
13
|
+
import kotlinx.cinterop.readValue
|
|
14
|
+
import kotlinx.cinterop.useContents
|
|
15
|
+
import platform.CoreGraphics.CGRectZero
|
|
16
|
+
import platform.Foundation.NSSelectorFromString
|
|
17
|
+
import platform.Foundation.NSValue
|
|
18
|
+
import platform.Foundation.valueForKey
|
|
19
|
+
import platform.UIKit.CGRectValue
|
|
20
|
+
import platform.UIKit.UIView
|
|
21
|
+
import platform.UIKit.UIViewAutoresizingFlexibleHeight
|
|
22
|
+
import platform.UIKit.UIViewAutoresizingFlexibleWidth
|
|
23
|
+
import platform.darwin.NSObject
|
|
24
|
+
import platform.darwin.NSObjectProtocol
|
|
25
|
+
import platform.Foundation.NSClassFromString
|
|
26
|
+
|
|
27
|
+
// The API is newer than the SDK Kotlin/Native binds, so it is reached through the runtime.
|
|
28
|
+
@OptIn(ExperimentalForeignApi::class)
|
|
29
|
+
private val regionsOfKind = NSSelectorFromString("reservedRegionsOfKind:")
|
|
30
|
+
|
|
31
|
+
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
|
|
32
|
+
private fun UIView.occlusionRegions(target: UIView): List<ReservedRegion> {
|
|
33
|
+
if (window == null || !respondsToSelector(regionsOfKind)) return emptyList()
|
|
34
|
+
val kindClass = NSClassFromString("UIViewReservedRegionKind") as? NSObjectProtocol ?: return emptyList()
|
|
35
|
+
val kind = kindClass.performSelector(NSSelectorFromString("occlusionRegionKind")) ?: return emptyList()
|
|
36
|
+
val regions = performSelector(regionsOfKind, withObject = kind) as? List<*> ?: return emptyList()
|
|
37
|
+
return regions.mapNotNull { region ->
|
|
38
|
+
val frame = ((region as? NSObject)?.valueForKey("frame") as? NSValue)?.CGRectValue ?: return@mapNotNull null
|
|
39
|
+
convertRect(frame, toView = target).useContents {
|
|
40
|
+
ReservedRegion(origin.x.dp, origin.y.dp, size.width.dp, size.height.dp)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Invisible full-size probe: reading the regions in layoutSubviews makes UIKit run it again
|
|
46
|
+
// when they change, so nothing polls.
|
|
47
|
+
@OptIn(ExperimentalForeignApi::class)
|
|
48
|
+
private class ReservedRegionsProbe(
|
|
49
|
+
private val onChange: (List<ReservedRegion>) -> Unit,
|
|
50
|
+
) : UIView(frame = CGRectZero.readValue()) {
|
|
51
|
+
private var last: List<ReservedRegion>? = null
|
|
52
|
+
|
|
53
|
+
init {
|
|
54
|
+
userInteractionEnabled = false
|
|
55
|
+
autoresizingMask = UIViewAutoresizingFlexibleWidth or UIViewAutoresizingFlexibleHeight
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override fun layoutSubviews() {
|
|
59
|
+
super.layoutSubviews()
|
|
60
|
+
val regions = superview?.let { occlusionRegions(it) } ?: emptyList()
|
|
61
|
+
if (regions != last) {
|
|
62
|
+
last = regions
|
|
63
|
+
onChange(regions)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
override fun didMoveToWindow() {
|
|
68
|
+
super.didMoveToWindow()
|
|
69
|
+
setNeedsLayout()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@OptIn(ExperimentalForeignApi::class)
|
|
74
|
+
@Composable
|
|
75
|
+
internal actual fun rememberReservedRegions(): List<ReservedRegion> {
|
|
76
|
+
if (!isPhoneIdiom()) return emptyList()
|
|
77
|
+
val host = LocalUIViewController.current.view
|
|
78
|
+
var regions by remember { mutableStateOf(emptyList<ReservedRegion>()) }
|
|
79
|
+
DisposableEffect(host) {
|
|
80
|
+
val probe = ReservedRegionsProbe { regions = it }
|
|
81
|
+
probe.setFrame(host.bounds)
|
|
82
|
+
host.insertSubview(probe, atIndex = 0)
|
|
83
|
+
onDispose { probe.removeFromSuperview() }
|
|
84
|
+
}
|
|
85
|
+
return regions
|
|
86
|
+
}
|
package/gradle.properties
CHANGED
|
@@ -41,6 +41,12 @@ public class KitConfig {
|
|
|
41
41
|
public var trustBanner: TrustBannerData? = nil
|
|
42
42
|
public var headerBar: String? = nil
|
|
43
43
|
public var headerGradient: String? = nil
|
|
44
|
+
|
|
45
|
+
public init(trustBanner: TrustBannerData? = nil, headerBar: String? = nil, headerGradient: String? = nil) {
|
|
46
|
+
self.trustBanner = trustBanner
|
|
47
|
+
self.headerBar = headerBar
|
|
48
|
+
self.headerGradient = headerGradient
|
|
49
|
+
}
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
public class ApplicationEnvironment: ObservableObject {
|
|
@@ -59,6 +65,17 @@ public class ApplicationEnvironment: ObservableObject {
|
|
|
59
65
|
}
|
|
60
66
|
}
|
|
61
67
|
|
|
68
|
+
extension ApplicationEnvironment {
|
|
69
|
+
/// Closes the mini app: through `composeApi` as before, else `maxApi` (Compose `maxApi.dismiss`).
|
|
70
|
+
func requestDismiss() {
|
|
71
|
+
if let composeApi {
|
|
72
|
+
composeApi.requestCallback(funcName: "dismiss", params: nil, onResponse: nil)
|
|
73
|
+
} else {
|
|
74
|
+
maxApi?.dismiss(completion: nil)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
62
79
|
private struct ApplicationEnvironmentKey: EnvironmentKey {
|
|
63
80
|
static let defaultValue = ApplicationEnvironment()
|
|
64
81
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
|
+
@available(*, deprecated, message: "Use KitComposeApi or MaxApi")
|
|
3
4
|
public protocol ComposeApi {
|
|
4
5
|
func request(funcName: String, params: Any, _ completion: ((String) -> Void)?)
|
|
5
6
|
func request(funcName: String, params: Any) -> String
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
@available(*, deprecated, message: "Use KitComposeApi or MaxApi")
|
|
9
10
|
public class ComposeApiImpl: ComposeApi {
|
|
10
11
|
public static let shared = ComposeApiImpl()
|
|
11
12
|
|