@momo-kits/native-kits 0.300.3-debug → 0.300.4-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 (218) hide show
  1. package/CLAUDE.md +51 -7
  2. package/README.md +2 -2
  3. package/compose/build.gradle.kts +10 -2
  4. package/compose/build.gradle.kts.backup +8 -0
  5. package/compose/compose.podspec +1 -3
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +4 -3
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
  8. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -14
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +62 -0
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +351 -111
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +55 -22
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -1
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +2 -1
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +20 -15
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +1 -2
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +306 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +6 -2
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +197 -60
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +132 -83
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +54 -62
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +97 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +377 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +122 -24
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +34 -14
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +113 -26
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +2 -2
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HelpCenterRequest.kt +37 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +8 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +20 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +13 -0
  65. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/HelpCenterRequestTest.kt +44 -0
  66. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt +15 -0
  67. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +3 -1
  68. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  69. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +138 -0
  70. package/gradle/libs.versions.toml +3 -14
  71. package/gradle/wrapper/gradle-wrapper.properties +0 -2
  72. package/gradle.properties +3 -5
  73. package/ios/Application/ApplicationEnvironment.swift +24 -6
  74. package/ios/Application/DesignSystemKits.swift +100 -0
  75. package/ios/Application/DesignSystemKitsPayload.swift +68 -0
  76. package/ios/Application/MaxApi.swift +30 -3
  77. package/ios/Avatar/Avatar.swift +4 -3
  78. package/ios/Badge/Badge.swift +6 -4
  79. package/ios/Badge/BadgeDot.swift +1 -1
  80. package/ios/Badge/BadgeRibbon.swift +8 -2
  81. package/ios/Button/Button.swift +15 -12
  82. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  83. package/ios/Carousel/Carousel.swift +29 -12
  84. package/ios/Checkbox/Checkbox.swift +7 -3
  85. package/ios/Chip/Chip.swift +5 -4
  86. package/ios/Collapse/Collapse.swift +70 -19
  87. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  88. package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
  89. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  90. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  91. package/ios/Divider/Divider.swift +37 -4
  92. package/ios/Extensions/Color++.swift +38 -9
  93. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  94. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  95. package/ios/Icon/Icon.swift +21 -23
  96. package/ios/Icon/Icons.swift +1329 -0
  97. package/ios/IconButton/IconButton.swift +15 -7
  98. package/ios/Image/Image.swift +7 -23
  99. package/ios/Information/Information.swift +27 -9
  100. package/ios/Input/ErrorView.swift +2 -2
  101. package/ios/Input/Input.swift +171 -148
  102. package/ios/Input/InputPhoneNumber.swift +10 -9
  103. package/ios/Input/InputSearch.swift +107 -78
  104. package/ios/Input/InputTextArea.swift +133 -148
  105. package/ios/InputDropDown/InputDropDown.swift +10 -7
  106. package/ios/InputMoney/InputMoney.swift +32 -11
  107. package/ios/InputOTP/InputOTP.swift +22 -12
  108. package/ios/Layout/Card.swift +50 -0
  109. package/ios/Layout/GridContext.swift +187 -3
  110. package/ios/Layout/Item.swift +5 -4
  111. package/ios/Layout/ScreenLayout.swift +415 -0
  112. package/ios/Layout/Section.swift +4 -100
  113. package/ios/Loader/Loader.swift +5 -9
  114. package/ios/Lottie/LottieView.swift +14 -2
  115. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +169 -29
  116. package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
  117. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
  118. package/ios/Navigation/BottomTab/DuoBarCompression.swift +104 -0
  119. package/ios/Navigation/BottomTab/DuoTabBar.swift +329 -0
  120. package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
  121. package/ios/Navigation/Component/Header.swift +545 -0
  122. package/ios/Navigation/Component/HeaderBackground.swift +154 -0
  123. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  124. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  125. package/ios/Navigation/Component/HeaderRight.swift +425 -0
  126. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  127. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  128. package/ios/Navigation/Component/SideBar.swift +183 -0
  129. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
  130. package/ios/Navigation/HostBackGestureBridge.swift +78 -0
  131. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
  132. package/ios/Navigation/Navigation.swift +204 -0
  133. package/ios/Navigation/NavigationContainer.swift +195 -0
  134. package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
  135. package/ios/Navigation/StackScreen.swift +930 -0
  136. package/ios/Navigation/StackTransition.swift +400 -0
  137. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  138. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  139. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  140. package/ios/Pagination/PaginationScroll.swift +2 -2
  141. package/ios/Popup/PopupDisplay.swift +72 -135
  142. package/ios/Popup/PopupInput.swift +10 -13
  143. package/ios/Popup/PopupLayout.swift +138 -0
  144. package/ios/Popup/PopupNotify.swift +59 -69
  145. package/ios/Popup/PopupPromotion.swift +31 -47
  146. package/ios/PopupView/FullscreenPopup.swift +13 -19
  147. package/ios/PopupView/PopupView.swift +1 -1
  148. package/ios/PopupView/Utils++.swift +0 -8
  149. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  150. package/ios/Radio/Radio.swift +2 -1
  151. package/ios/Rating/Rating.swift +9 -1
  152. package/ios/Resources/dot_loading.json +558 -0
  153. package/ios/Resources/lottie_circle_loader.json +1 -0
  154. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  155. package/ios/Skeleton/Skeleton.swift +22 -27
  156. package/ios/Slider/Slider.swift +16 -18
  157. package/ios/Stepper/Stepper.swift +7 -3
  158. package/ios/Steps/Steps.swift +8 -6
  159. package/ios/SuggestAction/SuggestAction.swift +5 -0
  160. package/ios/Swipeable/SwipeCell.swift +10 -6
  161. package/ios/Switch/Switch.swift +22 -20
  162. package/ios/TabView/TabView.swift +102 -21
  163. package/ios/Tag/Tag.swift +15 -4
  164. package/ios/Template/Logo/Logo.swift +2 -1
  165. package/ios/Template/TrustBanner/TrustBanner.swift +109 -55
  166. package/ios/Title/Title.swift +6 -17
  167. package/ios/Tooltip/Tooltip.swift +29 -14
  168. package/ios/Typography/FontScale.swift +17 -14
  169. package/ios/Typography/Text.swift +156 -20
  170. package/ios/Typography/Typography.swift +2 -2
  171. package/ios/Uploader/Uploader.swift +3 -1
  172. package/ios/native-kits.podspec +2 -3
  173. package/ios-demo/MoMoUIKitsDemo.podspec +1 -3
  174. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  175. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  176. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
  177. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  178. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  179. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
  180. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
  181. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
  182. package/package.json +1 -1
  183. package/publish.sh +19 -2
  184. package/scripts/gen-ios-icons.mjs +36 -0
  185. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  186. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  187. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  188. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  189. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  190. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  191. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  192. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  193. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  194. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  195. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  196. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  197. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
  198. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
  199. package/ios/Application/Components.swift +0 -270
  200. package/ios/Application/ComposeApi.swift +0 -22
  201. package/ios/Application/HeaderRight.swift +0 -306
  202. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  203. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  204. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  205. package/ios/Application/Navigation/NavigationContainer.swift +0 -213
  206. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  207. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  208. package/ios/Application/Navigation/component/Header.swift +0 -178
  209. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  210. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  211. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  212. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  213. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  214. package/ios/Application/Screen.swift +0 -250
  215. package/ios/Application/StackScreen.swift +0 -492
  216. package/ios/Typography/FontScaleStore.swift +0 -58
  217. package/ios/Typography/KitDesignSystem.swift +0 -17
  218. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -0,0 +1,306 @@
1
+ package vn.momo.kits.layout
2
+
3
+ import androidx.compose.foundation.layout.WindowInsets
4
+ import androidx.compose.foundation.layout.systemBars
5
+ import androidx.compose.runtime.Composable
6
+ import androidx.compose.runtime.Immutable
7
+ import androidx.compose.runtime.compositionLocalOf
8
+ import androidx.compose.runtime.remember
9
+ import androidx.compose.ui.platform.LocalDensity
10
+ import androidx.compose.ui.platform.LocalLayoutDirection
11
+ import androidx.compose.ui.platform.LocalWindowInfo
12
+ import androidx.compose.ui.unit.Dp
13
+ import androidx.compose.ui.unit.dp
14
+ import vn.momo.kits.const.Spacing
15
+ import vn.momo.kits.navigation.component.HEADER_HEIGHT
16
+ import vn.momo.kits.platform.ReservedRegion
17
+ import vn.momo.kits.platform.isPhoneIdiom
18
+ import vn.momo.kits.platform.rememberReservedRegions
19
+ import vn.momo.kits.utils.getAppStatusBarHeight
20
+ import vn.momo.kits.utils.horizontalSafeAreaInsets
21
+
22
+ enum class SideBarEdge { LEFT, RIGHT }
23
+
24
+ @Immutable
25
+ data class SideBarLayout(
26
+ val edge: SideBarEdge,
27
+ val width: Dp,
28
+ // Preferred window y of the first control; the column is still kept above footer and bottom bars.
29
+ val top: Dp,
30
+ // Room kept free above the window bottom, clear of a camera at the bottom end of the strip.
31
+ val bottom: Dp,
32
+ )
33
+
34
+ // Room the header row keeps from each side for the status bar clock and battery clusters.
35
+ @Immutable
36
+ data class StatusBarClearance(val left: Dp, val right: Dp)
37
+
38
+ @Immutable
39
+ data class ScreenLayout(
40
+ val width: Dp,
41
+ val height: Dp,
42
+ val isIPhoneDuo: Boolean,
43
+ // Raw top / bottom safe area; overlays and the Duo floating tab bar keep clear of them.
44
+ val insetTop: Dp,
45
+ val insetBottom: Dp,
46
+ val insetLeft: Dp,
47
+ val insetRight: Dp,
48
+ val contentWidth: Dp,
49
+ val headerHeight: Dp,
50
+ // 0 when the status bar is off the top edge, so the bar row fills the header and keeps the title centered.
51
+ val headerStatusBarHeight: Dp,
52
+ // iPhone Duo moves back and toolkit into a column in the side safe area; null while they stay in the bar.
53
+ val sideBar: SideBarLayout? = null,
54
+ // The device has a hinge, folded or not, whatever the window size; drives popups, modal and bottom sheet.
55
+ val isIPhoneDuoDevice: Boolean = isIPhoneDuo,
56
+ // Portrait iPhone Duo puts the status bar in the side strip, about twice as tall as the strip is wide.
57
+ // Stored so it survives consumeHorizontalInsets().
58
+ val duoTopInset: Dp = insetTop,
59
+ // iPhone Duo inner display in portrait: the header row shares the status bar band, between its clusters.
60
+ val statusBarClearance: StatusBarClearance? = null,
61
+ ) {
62
+ val headerBarHeight: Dp get() = headerHeight - headerStatusBarHeight
63
+ }
64
+
65
+ // Measured by momo-kits RN (Layout/utils.ts) on the iOS 27.1 iPhone Duo simulator; keep both in sync.
66
+ internal object DuoMetrics {
67
+ // Inner display is ~1.42, outer ~1.45; every slab iPhone is 1.77 or taller.
68
+ const val MAX_ASPECT_RATIO = 1.6f
69
+ val controlSize = 48.dp
70
+ val itemPitch = 50.dp
71
+ // Portrait: below the camera/clock cluster in the strip (ends near y 151).
72
+ val firstItemTop = 170.dp
73
+ // Inner display in landscape: below its status cluster (ends near y 98).
74
+ val firstItemTopLandscape = 120.dp
75
+ // Window height from which the inner display is in use (outer is 466pt tall in landscape, inner 669pt).
76
+ val innerMinHeight = 600.dp
77
+ // Outer display in portrait: the header is 4pt taller than the iPhone floor and its title
78
+ // lines up with the camera punchout (y 24–66 at the top of the strip).
79
+ val outerHeaderHeight = 76.dp
80
+ val outerCameraCenterY = 45.dp
81
+ // Outer display in landscape has no status bar in the strip: controls keep this far from
82
+ // both ends unless a reserved region (the camera at either end) asks for more.
83
+ val edgeInset = 24.dp
84
+ // Inner display in portrait: the clock and connectivity cluster sits at the top right corner
85
+ // (x 550–643 of 669, clock digits centered on y 47); its reserved region is taller than the glyphs.
86
+ val innerStatusBarCenterY = 47.dp
87
+ val innerStatusBarWidth = 120.dp
88
+ // Host app's iOS 26 tab bar on the inner display in portrait: a 62pt glass pill sized to its tabs,
89
+ // QR as a 62pt platter beside it, ≥ 20pt from the sides, 13pt into the home indicator inset.
90
+ val floatingTabBarHeight = 62.dp
91
+ val floatingTabBarMargin = 20.dp
92
+ val floatingTabBarInsetOverlap = 13.dp
93
+ }
94
+
95
+ private val IPHONE_HEADER_MIN_HEIGHT = 72.dp
96
+
97
+ // A smaller top inset means the status bar is not on the top edge.
98
+ private val STATUS_BAR_MIN_INSET = 20.dp
99
+
100
+ internal val LocalScreenLayout = compositionLocalOf<ScreenLayout?> { null }
101
+
102
+ // Port of RN `useScreenLayout`; follows fold/unfold and rotation through the window size and safe area.
103
+ @Composable
104
+ fun getScreenLayout(): ScreenLayout =
105
+ LocalScreenLayout.current ?: calculateScreenLayout(getAppStatusBarHeight())
106
+
107
+ @Composable
108
+ internal fun calculateScreenLayout(topInset: Dp, parent: ScreenLayout? = null): ScreenLayout {
109
+ val isPhone = isPhoneIdiom()
110
+ if (parent != null) {
111
+ // the parent's row center reproduces its own centered geometry
112
+ val titleCenterY = parent.headerStatusBarHeight + parent.headerBarHeight / 2
113
+ val statusBarClearance = parent.statusBarClearance?.takeIf { topInset >= STATUS_BAR_MIN_INSET }
114
+ val (headerHeight, headerStatusBarHeight) = if (statusBarClearance != null) {
115
+ statusBarRowGeometry()
116
+ } else {
117
+ headerGeometry(topInset, isPhone, parent.width, parent.height, parent.sideBar != null, titleCenterY)
118
+ }
119
+ return parent.copy(
120
+ headerHeight = headerHeight,
121
+ headerStatusBarHeight = headerStatusBarHeight,
122
+ statusBarClearance = statusBarClearance,
123
+ )
124
+ }
125
+
126
+ val density = LocalDensity.current
127
+ val layoutDirection = LocalLayoutDirection.current
128
+ val size = LocalWindowInfo.current.containerSize
129
+ val insets = horizontalSafeAreaInsets
130
+ val leftPx = insets.getLeft(density, layoutDirection)
131
+ val rightPx = insets.getRight(density, layoutDirection)
132
+ val bottomPx = WindowInsets.systemBars.getBottom(density)
133
+ val reservedRegions = rememberReservedRegions()
134
+
135
+ return remember(size, leftPx, rightPx, bottomPx, topInset, density, reservedRegions) {
136
+ with(density) {
137
+ val width = size.width.toDp()
138
+ val height = size.height.toDp()
139
+ screenLayoutOf(
140
+ width = width,
141
+ height = height,
142
+ insetLeft = leftPx.toDp(),
143
+ insetRight = rightPx.toDp(),
144
+ topInset = topInset,
145
+ bottomInset = bottomPx.toDp(),
146
+ isPhone = isPhone,
147
+ asymmetricInsets = leftPx != rightPx,
148
+ isDuoDevice = isPhone && reservedRegions.isFoldable,
149
+ reservedRegions = reservedRegions.occlusion,
150
+ )
151
+ }
152
+ }
153
+ }
154
+
155
+ // Content inside the safe-area padding: the insets are spent and the side bar belongs to the screen.
156
+ internal fun ScreenLayout.consumeHorizontalInsets(): ScreenLayout =
157
+ copy(width = contentWidth, insetLeft = 0.dp, insetRight = 0.dp, sideBar = null)
158
+
159
+ // titleCenterY: window y the title row centers on for the outer display in landscape.
160
+ private fun headerGeometry(
161
+ topInset: Dp,
162
+ isPhone: Boolean,
163
+ width: Dp,
164
+ height: Dp,
165
+ hasSideBar: Boolean,
166
+ titleCenterY: Dp,
167
+ ): Pair<Dp, Dp> {
168
+ val headerHeight = topInset + HEADER_HEIGHT.dp
169
+ if (!isPhone) return headerHeight to topInset
170
+ if (hasSideBar && topInset < STATUS_BAR_MIN_INSET) {
171
+ if (height > width && width < DuoMetrics.innerMinHeight) {
172
+ // outer display: the row starts below the top so the title centers on the camera
173
+ return DuoMetrics.outerHeaderHeight to DuoMetrics.outerCameraCenterY * 2 - DuoMetrics.outerHeaderHeight
174
+ }
175
+ if (width > height && height < DuoMetrics.innerMinHeight) {
176
+ // reaches 8pt below a side bar control centered on the row
177
+ val centered = maxOf(IPHONE_HEADER_MIN_HEIGHT, titleCenterY + DuoMetrics.controlSize / 2 + Spacing.S)
178
+ return centered to maxOf(0.dp, titleCenterY * 2 - centered)
179
+ }
180
+ }
181
+ return maxOf(headerHeight, IPHONE_HEADER_MIN_HEIGHT) to if (topInset < STATUS_BAR_MIN_INSET) 0.dp else topInset
182
+ }
183
+
184
+ // Centered on the clock digits; the cluster's region center sits above them.
185
+ private fun statusBarRowGeometry(): Pair<Dp, Dp> = DuoMetrics.innerStatusBarCenterY * 2 to 0.dp
186
+
187
+ private data class StripOcclusion(val top: Dp, val topCenter: Dp, val bottom: Dp)
188
+
189
+ // How far system regions reach into a side strip from its top and from its bottom end
190
+ // (e.g. the camera, which sits at either end depending on the rotation), and the center y
191
+ // of the region reaching furthest from the top.
192
+ private fun stripOcclusion(regions: List<ReservedRegion>, stripLeft: Dp, stripRight: Dp, windowHeight: Dp): StripOcclusion {
193
+ var top = 0.dp
194
+ var topCenter = 0.dp
195
+ var bottom = 0.dp
196
+ regions.forEach { region ->
197
+ if (region.x >= stripRight || region.x + region.width <= stripLeft) return@forEach
198
+ val centerY = region.y + region.height / 2
199
+ if (centerY < windowHeight / 2) {
200
+ if (region.y + region.height > top) {
201
+ top = region.y + region.height
202
+ topCenter = centerY
203
+ }
204
+ } else {
205
+ bottom = maxOf(bottom, windowHeight - region.y)
206
+ }
207
+ }
208
+ return StripOcclusion(top, topCenter, bottom)
209
+ }
210
+
211
+ // How far the clock and battery clusters of a status bar on the top edge reach in from each side;
212
+ // null without reserved regions there.
213
+ private fun statusBarClusters(regions: List<ReservedRegion>, windowWidth: Dp, topInset: Dp): StatusBarClearance? {
214
+ val midX = windowWidth / 2
215
+ val clusters = regions.filter {
216
+ it.y < topInset && it.y + it.height <= topInset && (it.x + it.width <= midX || it.x >= midX)
217
+ }
218
+ if (clusters.isEmpty()) return null
219
+ var left = 0.dp
220
+ var right = 0.dp
221
+ clusters.forEach {
222
+ if (it.x < midX) left = maxOf(left, it.x + it.width) else right = maxOf(right, windowWidth - it.x)
223
+ }
224
+ return StatusBarClearance(left, right)
225
+ }
226
+
227
+ // Slab iPhones report equal side insets in landscape; iPhone Duo moves the status bar to one side.
228
+ private fun isIPhoneDuoWindow(width: Dp, height: Dp, asymmetricInsets: Boolean): Boolean {
229
+ val shortSide = minOf(width, height)
230
+ if (shortSide <= 0.dp) return asymmetricInsets
231
+ return maxOf(width, height) / shortSide < DuoMetrics.MAX_ASPECT_RATIO || asymmetricInsets
232
+ }
233
+
234
+ private fun screenLayoutOf(
235
+ width: Dp,
236
+ height: Dp,
237
+ insetLeft: Dp,
238
+ insetRight: Dp,
239
+ topInset: Dp,
240
+ bottomInset: Dp,
241
+ isPhone: Boolean,
242
+ asymmetricInsets: Boolean,
243
+ isDuoDevice: Boolean,
244
+ reservedRegions: List<ReservedRegion>,
245
+ ): ScreenLayout {
246
+ val isIPhoneDuo = isPhone && isIPhoneDuoWindow(width, height, asymmetricInsets)
247
+ val isInnerPortrait = height > width && width >= DuoMetrics.innerMinHeight
248
+ val statusBarClearance = if (isIPhoneDuo && isInnerPortrait && topInset >= STATUS_BAR_MIN_INSET) {
249
+ statusBarClusters(reservedRegions, width, topInset)
250
+ ?: StatusBarClearance(left = 0.dp, right = DuoMetrics.innerStatusBarWidth + Spacing.S)
251
+ } else {
252
+ null
253
+ }
254
+
255
+ var sideBar: SideBarLayout? = null
256
+ var titleCenterY = 0.dp
257
+ val sideInset = maxOf(insetLeft, insetRight)
258
+ // narrower strips cannot host a control 12pt from the inner edge
259
+ if (isIPhoneDuo && sideInset >= DuoMetrics.controlSize + Spacing.M) {
260
+ val edge = if (insetRight >= insetLeft) SideBarEdge.RIGHT else SideBarEdge.LEFT
261
+ val stripLeft = if (edge == SideBarEdge.RIGHT) width - sideInset else 0.dp
262
+ // clear of the camera and of the status bar cluster, which grows for calls and Live
263
+ // Activities; region frames already include the system's margin
264
+ val occlusion = stripOcclusion(reservedRegions, stripLeft, stripLeft + sideInset, height)
265
+ // UIKit reports no usable status bar frame here (a 2pt top strip), so the spot is inferred like RN
266
+ val top = when {
267
+ statusBarClearance != null -> statusBarRowGeometry().first + Spacing.M
268
+ topInset >= STATUS_BAR_MIN_INSET -> topInset + HEADER_HEIGHT.dp + Spacing.M
269
+ width <= height -> DuoMetrics.firstItemTop
270
+ height >= DuoMetrics.innerMinHeight -> DuoMetrics.firstItemTopLandscape
271
+ // outer display in landscape: no status bar in the strip
272
+ else -> DuoMetrics.edgeInset
273
+ }
274
+ // outer display in landscape: the title row centers on the camera when it sits at the
275
+ // top end, otherwise on the back button
276
+ titleCenterY = if (occlusion.top > top) occlusion.topCenter else top + DuoMetrics.controlSize / 2
277
+ sideBar = SideBarLayout(
278
+ edge = edge,
279
+ width = sideInset,
280
+ top = maxOf(top, occlusion.top),
281
+ bottom = maxOf(DuoMetrics.edgeInset, occlusion.bottom),
282
+ )
283
+ }
284
+ val (headerHeight, headerStatusBarHeight) = if (statusBarClearance != null) {
285
+ statusBarRowGeometry()
286
+ } else {
287
+ headerGeometry(topInset, isPhone, width, height, sideBar != null, titleCenterY)
288
+ }
289
+
290
+ return ScreenLayout(
291
+ width = width,
292
+ height = height,
293
+ isIPhoneDuo = isIPhoneDuo,
294
+ insetTop = topInset,
295
+ insetBottom = bottomInset,
296
+ insetLeft = insetLeft,
297
+ insetRight = insetRight,
298
+ contentWidth = width - insetLeft - insetRight,
299
+ headerHeight = headerHeight,
300
+ headerStatusBarHeight = headerStatusBarHeight,
301
+ sideBar = sideBar,
302
+ isIPhoneDuoDevice = isDuoDevice,
303
+ duoTopInset = maxOf(topInset, if (height > width) sideInset * 2 else 0.dp),
304
+ statusBarClearance = statusBarClearance,
305
+ )
306
+ }
@@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.offset
20
20
  import androidx.compose.foundation.layout.padding
21
21
  import androidx.compose.foundation.layout.size
22
22
  import androidx.compose.foundation.layout.width
23
+ import androidx.compose.foundation.layout.absolutePadding
23
24
  import androidx.compose.foundation.shape.RoundedCornerShape
24
25
  import androidx.compose.runtime.Composable
25
26
  import androidx.compose.runtime.DisposableEffect
@@ -51,6 +52,10 @@ import vn.momo.kits.modifier.conditional
51
52
  import vn.momo.kits.modifier.noFeedbackClickable
52
53
  import vn.momo.kits.platform.BackHandler
53
54
  import vn.momo.kits.platform.getScreenHeight
55
+ import vn.momo.kits.layout.getScreenLayout
56
+
57
+ // iPhone Duo inner display: center the sheet instead of spanning the width (RN BottomSheet).
58
+ private val DUO_REGULAR_MIN_WIDTH = 600.dp
54
59
 
55
60
  @Composable
56
61
  internal fun BottomSheet(
@@ -63,7 +68,11 @@ internal fun BottomSheet(
63
68
  footerComponent: (@Composable () -> Unit)? = null,
64
69
  onDismiss: (() -> Unit)?
65
70
  ) {
66
- val screenHeightDp = getScreenHeight()
71
+ val layout = getScreenLayout()
72
+ val isDuo = layout.isIPhoneDuoDevice
73
+ val isDuoCentered = isDuo && layout.width >= DUO_REGULAR_MIN_WIDTH
74
+ // the screen is the outer display on iPhone Duo, even when unfolded
75
+ val screenHeightDp = if (isDuo) layout.height else getScreenHeight()
67
76
  val screenHeightPx = with(LocalDensity.current) { screenHeightDp.toPx() }
68
77
  val sheetOffset = remember { Animatable(screenHeightPx) }
69
78
 
@@ -148,13 +157,32 @@ internal fun BottomSheet(
148
157
  }
149
158
  ) {
150
159
  val parentHeight = with(LocalDensity.current) { constraints.maxHeight.toDp() }
160
+ val maxHeight = if (isDuo) {
161
+ parentHeight - maxOf(layout.duoTopInset, Spacing.XL)
162
+ } else {
163
+ parentHeight - 90.dp
164
+ }
165
+ // landscape side buttons span the full height; in portrait the side status bar only covers the top
166
+ val sideInset = isDuo && !isDuoCentered && layout.width > layout.height
151
167
 
152
168
  Box(
153
169
  modifier = Modifier
154
170
  .align(Alignment.BottomCenter)
155
171
  .offset { IntOffset(x = 0, y = sheetOffset.value.toInt()) }
156
- .fillMaxWidth()
157
- .heightIn(max = parentHeight - 90.dp)
172
+ .then(
173
+ if (isDuoCentered) {
174
+ // keeps the portrait width in landscape, like system sheets
175
+ Modifier.width(
176
+ minOf(
177
+ minOf(layout.width, layout.height) - Spacing.L * 2,
178
+ layout.width - maxOf(layout.insetLeft, layout.insetRight, Spacing.L) * 2,
179
+ )
180
+ )
181
+ } else {
182
+ Modifier.fillMaxWidth()
183
+ }
184
+ )
185
+ .heightIn(max = maxHeight)
158
186
  .background(
159
187
  color = if (isSurface) AppTheme.current.colors.background.surface else AppTheme.current.colors.background.default,
160
188
  shape = RoundedCornerShape(topStart = Radius.M, topEnd = Radius.M)
@@ -168,7 +196,13 @@ internal fun BottomSheet(
168
196
  val bottomInset = if (useBottomInset) {
169
197
  (AppNavigationBar.current - keyboardSizeState().value).coerceAtLeast(0.dp)
170
198
  } else 0.dp
171
- Column(Modifier.padding(bottom = bottomInset)) {
199
+ Column(
200
+ Modifier
201
+ .padding(bottom = bottomInset)
202
+ .conditional(sideInset) {
203
+ absolutePadding(left = layout.insetLeft, right = layout.insetRight)
204
+ }
205
+ ) {
172
206
  Box(
173
207
  modifier = Modifier
174
208
  .height(72.dp)
@@ -249,6 +283,11 @@ internal fun BottomSheet(
249
283
  content()
250
284
  }
251
285
  BottomSheetFooter(footerComponent)
286
+ } else if (isDuo) {
287
+ // content shrinks so a tall sheet keeps its header
288
+ Box(Modifier.weight(1f, fill = false)) {
289
+ content()
290
+ }
252
291
  } else {
253
292
  content()
254
293
  }
@@ -9,6 +9,7 @@ import androidx.compose.foundation.clickable
9
9
  import androidx.compose.foundation.interaction.MutableInteractionSource
10
10
  import androidx.compose.foundation.layout.Box
11
11
  import androidx.compose.foundation.layout.fillMaxSize
12
+ import androidx.compose.foundation.layout.padding
12
13
  import androidx.compose.foundation.layout.wrapContentSize
13
14
  import androidx.compose.runtime.*
14
15
  import androidx.compose.ui.Alignment
@@ -25,6 +26,7 @@ import vn.momo.kits.modifier.conditional
25
26
  import vn.momo.kits.modifier.noFeedbackClickable
26
27
  import vn.momo.kits.navigation.tracking.ScreenTracker
27
28
  import vn.momo.kits.platform.BackHandler
29
+ import vn.momo.kits.layout.getScreenLayout
28
30
 
29
31
  @Composable
30
32
  internal fun ModalScreen(
@@ -100,12 +102,18 @@ internal fun ModalScreen(
100
102
  closeEvent()
101
103
  }
102
104
 
105
+ val layout = getScreenLayout()
106
+
103
107
  Box(
104
108
  modifier = Modifier
105
109
  .fillMaxSize()
106
110
  .background(Color.Black.copy(alpha = 0.6f * alpha.value))
107
111
  .noFeedbackClickable {
108
112
  if(barrierDismissible) closeEvent()
113
+ }
114
+ // center below the side status bar and above the home indicator
115
+ .conditional(layout.isIPhoneDuoDevice) {
116
+ padding(top = layout.duoTopInset, bottom = layout.insetBottom)
109
117
  },
110
118
  contentAlignment = Alignment.Center
111
119
  ) {
@@ -30,6 +30,9 @@ class Navigation(
30
30
  headerType: HeaderType? = null,
31
31
  scrollData: ScrollData? = null,
32
32
  backgroundColor: Color? = null,
33
+ // AI-GENERATED START: custom screen background
34
+ backgroundComponent: (@Composable () -> Unit)? = null,
35
+ // AI-GENERATED END: custom screen background
33
36
  tintColor: Color? = null,
34
37
  footerComponent: (@Composable () -> Unit)? = null,
35
38
  footerTransparent: Boolean? = null,
@@ -48,6 +51,9 @@ class Navigation(
48
51
  headerType = headerType ?: options.headerType,
49
52
  scrollData = scrollData ?: options.scrollData,
50
53
  backgroundColor = backgroundColor ?: options.backgroundColor,
54
+ // AI-GENERATED START: custom screen background
55
+ backgroundComponent = backgroundComponent ?: options.backgroundComponent,
56
+ // AI-GENERATED END: custom screen background
51
57
  tintColor = tintColor ?: options.tintColor,
52
58
  footerComponent = footerComponent ?: options.footerComponent,
53
59
  footerTransparent = footerTransparent ?: options.footerTransparent,
@@ -85,6 +91,10 @@ data class NavigationOptions(
85
91
  val headerType: HeaderType = HeaderType.Default(),
86
92
  val scrollData: ScrollData = ScrollData(),
87
93
  val backgroundColor: Color? = null,
94
+ // AI-GENERATED START: custom screen background (SwiftUI `NavigationOptions.background`)
95
+ /** Drawn instead of `backgroundColor` over the whole screen: under the header, outside the keyboard padding. */
96
+ val backgroundComponent: @Composable (() -> Unit)? = null,
97
+ // AI-GENERATED END: custom screen background (SwiftUI `NavigationOptions.background`)
88
98
  val tintColor: Color? = null,
89
99
  val footerComponent: @Composable (() -> Unit)? = null,
90
100
  val footerTransparent: Boolean = false,
@@ -20,7 +20,7 @@ import androidx.navigation.toRoute
20
20
  import kotlinx.coroutines.delay
21
21
  import kotlinx.coroutines.flow.collectLatest
22
22
  import vn.momo.kits.application.DesignSystemConfig
23
- import vn.momo.kits.application.KitDesignSystemConfig
23
+ import vn.momo.kits.application.DesignSystemKits
24
24
  import vn.momo.kits.application.LocalContext
25
25
  import vn.momo.kits.application.LocalDesignSystemConfig
26
26
  import vn.momo.kits.application.LocalLocalize
@@ -31,6 +31,8 @@ import vn.momo.kits.const.AppStatusBar
31
31
  import vn.momo.kits.const.AppTheme
32
32
  import vn.momo.kits.const.Theme
33
33
  import vn.momo.kits.const.defaultTheme
34
+ import vn.momo.kits.layout.LocalScreenLayout
35
+ import vn.momo.kits.layout.calculateScreenLayout
34
36
  import vn.momo.kits.platform.ProvideNavigationEventDispatcherOwner
35
37
  import vn.momo.kits.platform.ProvideOsFontScale
36
38
  import vn.momo.kits.utils.getAppStatusBarHeight
@@ -63,6 +65,7 @@ fun NavigationContainer(
63
65
  // AI-GENERATED END: retain the current host client after recomposition
64
66
  val statusBarHeight = statusBarHeight ?: getAppStatusBarHeight()
65
67
  val navigationBarHeight = getNavigationBarHeight()
68
+ val screenLayout = calculateScreenLayout(statusBarHeight, parent = LocalScreenLayout.current)
66
69
 
67
70
  val parentContext = LocalContext.current
68
71
  val mergedContext = MiniAppContext.merge(parentContext, miniAppContext)
@@ -70,7 +73,7 @@ fun NavigationContainer(
70
73
  val stackTransition = remember { StackTransitionCoordinator() }
71
74
  val theme = remember { mutableStateOf(initialTheme) }
72
75
  val resolvedDesignSystemConfig =
73
- designSystemConfig ?: LocalDesignSystemConfig.current ?: KitDesignSystemConfig.config
76
+ designSystemConfig ?: LocalDesignSystemConfig.current ?: DesignSystemKits.getConfigs().designSystemConfig
74
77
  val headerBar = resolvedDesignSystemConfig?.headerBar
75
78
  // RN parity: the theme's own header image wins; config headerBar is only the fallback.
76
79
  val resolvedTheme = remember(theme.value, headerBar) {
@@ -117,6 +120,7 @@ fun NavigationContainer(
117
120
  AppTheme provides resolvedTheme,
118
121
  AppStatusBar provides statusBarHeight,
119
122
  AppNavigationBar provides navigationBarHeight,
123
+ LocalScreenLayout provides screenLayout,
120
124
  LocalContext provides mergedContext,
121
125
  LocalDesignSystemConfig provides resolvedDesignSystemConfig,
122
126
  LocalLocalize provides resolvedLocalize,
@@ -58,6 +58,12 @@ class Navigator(
58
58
  }
59
59
 
60
60
  fun onBackSafe(callBack: (() -> Unit)? = null) {
61
+ // An open modal or bottom sheet takes the back first, as its BackHandler does for gestures.
62
+ val overplayType = OverplayComponentRegistry.getOverplayType()
63
+ if (overplayType != null && overplayType != OverplayComponentType.SNACK_BAR) {
64
+ pop(1, callBack)
65
+ return
66
+ }
61
67
  tabBackHandler?.let {
62
68
  it()
63
69
  return