@momo-kits/native-kits 0.167.1-sizescale.2-debug → 0.168.1-beta.1-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 (223) hide show
  1. package/CLAUDE.md +45 -3
  2. package/README.md +2 -2
  3. package/compose/build.gradle.kts +9 -1
  4. package/compose/build.gradle.kts.backup +8 -0
  5. package/compose/compose.podspec +1 -1
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +3 -4
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +353 -113
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +2 -2
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +24 -14
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +1 -1
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +55 -22
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -1
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +16 -9
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +6 -3
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +11 -15
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +10 -4
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +7 -23
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +13 -11
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +21 -9
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +309 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +11 -9
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +8 -7
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +197 -60
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +134 -81
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +54 -62
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +126 -29
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +34 -14
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +116 -26
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +4 -2
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +2 -3
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  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 +13 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +13 -0
  65. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +1 -3
  66. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +10 -11
  67. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +94 -0
  68. package/gradle/libs.versions.toml +1 -1
  69. package/gradle.properties +1 -1
  70. package/ios/Application/ApplicationEnvironment.swift +17 -0
  71. package/ios/Application/MaxApi.swift +30 -3
  72. package/ios/Avatar/Avatar.swift +4 -3
  73. package/ios/Badge/Badge.swift +6 -4
  74. package/ios/Badge/BadgeDot.swift +1 -1
  75. package/ios/Badge/BadgeRibbon.swift +0 -1
  76. package/ios/Button/Button.swift +15 -12
  77. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  78. package/ios/Carousel/Carousel.swift +29 -12
  79. package/ios/Checkbox/Checkbox.swift +7 -3
  80. package/ios/Chip/Chip.swift +5 -4
  81. package/ios/Collapse/Collapse.swift +70 -19
  82. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  83. package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
  84. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  85. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  86. package/ios/Divider/Divider.swift +37 -4
  87. package/ios/Extensions/Color++.swift +38 -9
  88. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  89. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  90. package/ios/Icon/Icon.swift +21 -23
  91. package/ios/Icon/Icons.swift +1329 -0
  92. package/ios/IconButton/IconButton.swift +15 -7
  93. package/ios/Image/Image.swift +7 -23
  94. package/ios/Information/Information.swift +27 -9
  95. package/ios/Input/ErrorView.swift +2 -2
  96. package/ios/Input/Input.swift +160 -147
  97. package/ios/Input/InputPhoneNumber.swift +12 -9
  98. package/ios/Input/InputSearch.swift +107 -77
  99. package/ios/Input/InputTextArea.swift +128 -145
  100. package/ios/InputDropDown/InputDropDown.swift +10 -7
  101. package/ios/InputMoney/InputMoney.swift +33 -11
  102. package/ios/InputOTP/InputOTP.swift +22 -12
  103. package/ios/Layout/Card.swift +50 -0
  104. package/ios/Layout/GridContext.swift +187 -3
  105. package/ios/Layout/Item.swift +5 -4
  106. package/ios/Layout/ScreenLayout.swift +380 -0
  107. package/ios/Layout/Section.swift +4 -100
  108. package/ios/Loader/Loader.swift +5 -9
  109. package/ios/Lottie/LottieView.swift +14 -2
  110. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +169 -29
  111. package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
  112. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
  113. package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
  114. package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
  115. package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
  116. package/ios/Navigation/Component/Header.swift +545 -0
  117. package/ios/Navigation/Component/HeaderBackground.swift +133 -0
  118. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  119. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  120. package/ios/Navigation/Component/HeaderRight.swift +425 -0
  121. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  122. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  123. package/ios/Navigation/Component/SideBar.swift +183 -0
  124. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
  125. package/ios/Navigation/HostBackGestureBridge.swift +78 -0
  126. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
  127. package/ios/Navigation/Navigation.swift +204 -0
  128. package/ios/Navigation/NavigationContainer.swift +195 -0
  129. package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
  130. package/ios/Navigation/StackScreen.swift +934 -0
  131. package/ios/Navigation/StackTransition.swift +400 -0
  132. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  133. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  134. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  135. package/ios/Pagination/PaginationScroll.swift +2 -2
  136. package/ios/Popup/PopupDisplay.swift +72 -135
  137. package/ios/Popup/PopupInput.swift +10 -13
  138. package/ios/Popup/PopupLayout.swift +140 -0
  139. package/ios/Popup/PopupNotify.swift +59 -69
  140. package/ios/Popup/PopupPromotion.swift +31 -47
  141. package/ios/PopupView/FullscreenPopup.swift +13 -19
  142. package/ios/PopupView/PopupView.swift +1 -1
  143. package/ios/PopupView/Utils++.swift +0 -8
  144. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  145. package/ios/Radio/Radio.swift +2 -1
  146. package/ios/Rating/Rating.swift +9 -1
  147. package/ios/Resources/dot_loading.json +558 -0
  148. package/ios/Resources/lottie_circle_loader.json +1 -0
  149. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  150. package/ios/Skeleton/Skeleton.swift +22 -27
  151. package/ios/Slider/Slider.swift +16 -18
  152. package/ios/Stepper/Stepper.swift +6 -1
  153. package/ios/Steps/Steps.swift +8 -6
  154. package/ios/SuggestAction/SuggestAction.swift +5 -0
  155. package/ios/Swipeable/SwipeCell.swift +10 -6
  156. package/ios/Switch/Switch.swift +22 -20
  157. package/ios/TabView/TabView.swift +102 -21
  158. package/ios/Tag/Tag.swift +15 -4
  159. package/ios/Template/Logo/Logo.swift +2 -1
  160. package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
  161. package/ios/Title/Title.swift +6 -17
  162. package/ios/Tooltip/Tooltip.swift +29 -14
  163. package/ios/Typography/FontScale.swift +3 -3
  164. package/ios/Typography/Text.swift +81 -19
  165. package/ios/Uploader/Uploader.swift +3 -1
  166. package/ios/native-kits.podspec +2 -1
  167. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  168. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  169. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  170. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
  171. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  172. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  173. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
  174. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
  175. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
  176. package/local.properties +2 -2
  177. package/package.json +1 -1
  178. package/scripts/gen-ios-icons.mjs +36 -0
  179. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  180. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  181. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  182. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  183. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  184. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  185. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  186. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  187. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  188. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  189. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  190. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  191. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitSizeScale.kt +0 -56
  192. package/example/ios/Example.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/Example.xcscheme +0 -32
  193. package/example/ios/Example.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
  194. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  195. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/WorkspaceSettings.xcsettings +0 -16
  196. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +0 -6
  197. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +0 -5
  198. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/MoMoUIKits.xcscheme +0 -58
  199. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/Pods-Example.xcscheme +0 -58
  200. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SDWebImage.xcscheme +0 -58
  201. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SDWebImageSwiftUI.xcscheme +0 -58
  202. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SkeletonUI.xcscheme +0 -58
  203. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/lottie-ios-LottiePrivacyInfo.xcscheme +0 -58
  204. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/lottie-ios.xcscheme +0 -58
  205. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +0 -60
  206. package/ios/Application/Components.swift +0 -270
  207. package/ios/Application/ComposeApi.swift +0 -22
  208. package/ios/Application/HeaderRight.swift +0 -306
  209. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  210. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  211. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  212. package/ios/Application/Navigation/NavigationContainer.swift +0 -213
  213. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  214. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  215. package/ios/Application/Navigation/component/Header.swift +0 -178
  216. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  217. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  218. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  219. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  220. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  221. package/ios/Application/Screen.swift +0 -250
  222. package/ios/Application/StackScreen.swift +0 -492
  223. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -0,0 +1,309 @@
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.getScreenDimensions
18
+ import vn.momo.kits.platform.isPhoneIdiom
19
+ import vn.momo.kits.platform.rememberReservedRegions
20
+ import vn.momo.kits.utils.getAppStatusBarHeight
21
+ import vn.momo.kits.utils.horizontalSafeAreaInsets
22
+
23
+ enum class SideBarEdge { LEFT, RIGHT }
24
+
25
+ @Immutable
26
+ data class SideBarLayout(
27
+ val edge: SideBarEdge,
28
+ val width: Dp,
29
+ // Preferred window y of the first control; the column is still kept above footer and bottom bars.
30
+ val top: Dp,
31
+ // Room kept free above the window bottom, clear of a camera at the bottom end of the strip.
32
+ val bottom: Dp,
33
+ )
34
+
35
+ // Room the header row keeps from each side for the status bar clock and battery clusters.
36
+ @Immutable
37
+ data class StatusBarClearance(val left: Dp, val right: Dp)
38
+
39
+ @Immutable
40
+ data class ScreenLayout(
41
+ val width: Dp,
42
+ val height: Dp,
43
+ val isIPhoneDuo: Boolean,
44
+ // Raw top / bottom safe area; overlays and the Duo floating tab bar keep clear of them.
45
+ val insetTop: Dp,
46
+ val insetBottom: Dp,
47
+ val insetLeft: Dp,
48
+ val insetRight: Dp,
49
+ val contentWidth: Dp,
50
+ val headerHeight: Dp,
51
+ // 0 when the status bar is off the top edge, so the bar row fills the header and keeps the title centered.
52
+ val headerStatusBarHeight: Dp,
53
+ // iPhone Duo moves back and toolkit into a column in the side safe area; null while they stay in the bar.
54
+ val sideBar: SideBarLayout? = null,
55
+ // Port of RN `device.ts` isIPhoneDuo: the screen stays on the outer display, so Split View still matches.
56
+ val isIPhoneDuoDevice: Boolean = isIPhoneDuo,
57
+ // Portrait iPhone Duo puts the status bar in the side strip, about twice as tall as the strip is wide.
58
+ // Stored so it survives consumeHorizontalInsets().
59
+ val duoTopInset: Dp = insetTop,
60
+ // iPhone Duo inner display in portrait: the header row shares the status bar band, between its clusters.
61
+ val statusBarClearance: StatusBarClearance? = null,
62
+ ) {
63
+ val headerBarHeight: Dp get() = headerHeight - headerStatusBarHeight
64
+ }
65
+
66
+ // Measured by momo-kits RN (Layout/utils.ts) on the iOS 27.1 iPhone Duo simulator; keep both in sync.
67
+ internal object DuoMetrics {
68
+ // Inner display is ~1.42, outer ~1.45; every slab iPhone is 1.77 or taller.
69
+ const val MAX_ASPECT_RATIO = 1.6f
70
+ val controlSize = 48.dp
71
+ val itemPitch = 50.dp
72
+ // Portrait: below the camera/clock cluster in the strip (ends near y 151).
73
+ val firstItemTop = 170.dp
74
+ // Inner display in landscape: below its status cluster (ends near y 98).
75
+ val firstItemTopLandscape = 120.dp
76
+ // Window height from which the inner display is in use (outer is 466pt tall in landscape, inner 669pt).
77
+ val innerMinHeight = 600.dp
78
+ // Outer display in portrait: the header is 4pt taller than the iPhone floor and its title
79
+ // lines up with the camera punchout (y 24–66 at the top of the strip).
80
+ val outerHeaderHeight = 76.dp
81
+ val outerCameraCenterY = 45.dp
82
+ // Outer display in landscape has no status bar in the strip: controls keep this far from
83
+ // both ends unless a reserved region (the camera at either end) asks for more.
84
+ val edgeInset = 24.dp
85
+ // Inner display in portrait: the clock and connectivity cluster sits at the top right corner
86
+ // (x 550–643 of 669, clock digits centered on y 47); its reserved region is taller than the glyphs.
87
+ val innerStatusBarCenterY = 47.dp
88
+ val innerStatusBarWidth = 120.dp
89
+ }
90
+
91
+ // Widest short side of any single-display iPhone.
92
+ private val IPHONE_MAX_SHORT_SIDE = 440.dp
93
+
94
+ private val IPHONE_HEADER_MIN_HEIGHT = 72.dp
95
+
96
+ // A smaller top inset means the status bar is not on the top edge.
97
+ private val STATUS_BAR_MIN_INSET = 20.dp
98
+
99
+ internal val LocalScreenLayout = compositionLocalOf<ScreenLayout?> { null }
100
+
101
+ // Port of RN `useScreenLayout`; follows fold/unfold and rotation through the window size and safe area.
102
+ @Composable
103
+ fun getScreenLayout(): ScreenLayout =
104
+ LocalScreenLayout.current ?: calculateScreenLayout(getAppStatusBarHeight())
105
+
106
+ @Composable
107
+ internal fun calculateScreenLayout(topInset: Dp, parent: ScreenLayout? = null): ScreenLayout {
108
+ val isPhone = isPhoneIdiom()
109
+ if (parent != null) {
110
+ // the parent's row center reproduces its own centered geometry
111
+ val titleCenterY = parent.headerStatusBarHeight + parent.headerBarHeight / 2
112
+ val statusBarClearance = parent.statusBarClearance?.takeIf { topInset >= STATUS_BAR_MIN_INSET }
113
+ val (headerHeight, headerStatusBarHeight) = if (statusBarClearance != null) {
114
+ statusBarRowGeometry()
115
+ } else {
116
+ headerGeometry(topInset, isPhone, parent.width, parent.height, parent.sideBar != null, titleCenterY)
117
+ }
118
+ return parent.copy(
119
+ headerHeight = headerHeight,
120
+ headerStatusBarHeight = headerStatusBarHeight,
121
+ statusBarClearance = statusBarClearance,
122
+ )
123
+ }
124
+
125
+ val density = LocalDensity.current
126
+ val layoutDirection = LocalLayoutDirection.current
127
+ val size = LocalWindowInfo.current.containerSize
128
+ val insets = horizontalSafeAreaInsets
129
+ val leftPx = insets.getLeft(density, layoutDirection)
130
+ val rightPx = insets.getRight(density, layoutDirection)
131
+ val bottomPx = WindowInsets.systemBars.getBottom(density)
132
+ val screen = getScreenDimensions()
133
+ val reservedRegions = rememberReservedRegions()
134
+
135
+ return remember(size, leftPx, rightPx, bottomPx, topInset, screen, 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 && maxOf(
149
+ minOf(screen.width, screen.height).dp,
150
+ minOf(width, height),
151
+ ) > IPHONE_MAX_SHORT_SIDE,
152
+ reservedRegions = reservedRegions,
153
+ )
154
+ }
155
+ }
156
+ }
157
+
158
+ // Content inside the safe-area padding: the insets are spent and the side bar belongs to the screen.
159
+ internal fun ScreenLayout.consumeHorizontalInsets(): ScreenLayout =
160
+ copy(width = contentWidth, insetLeft = 0.dp, insetRight = 0.dp, sideBar = null)
161
+
162
+ // titleCenterY: window y the title row centers on for the outer display in landscape.
163
+ private fun headerGeometry(
164
+ topInset: Dp,
165
+ isPhone: Boolean,
166
+ width: Dp,
167
+ height: Dp,
168
+ hasSideBar: Boolean,
169
+ titleCenterY: Dp,
170
+ ): Pair<Dp, Dp> {
171
+ val headerHeight = topInset + HEADER_HEIGHT.dp
172
+ if (!isPhone) return headerHeight to topInset
173
+ if (hasSideBar && topInset < STATUS_BAR_MIN_INSET) {
174
+ if (height > width && width < DuoMetrics.innerMinHeight) {
175
+ // outer display: the row starts below the top so the title centers on the camera
176
+ return DuoMetrics.outerHeaderHeight to DuoMetrics.outerCameraCenterY * 2 - DuoMetrics.outerHeaderHeight
177
+ }
178
+ if (width > height && height < DuoMetrics.innerMinHeight) {
179
+ // reaches 8pt below a side bar control centered on the row
180
+ val centered = maxOf(IPHONE_HEADER_MIN_HEIGHT, titleCenterY + DuoMetrics.controlSize / 2 + Spacing.S)
181
+ return centered to maxOf(0.dp, titleCenterY * 2 - centered)
182
+ }
183
+ }
184
+ return maxOf(headerHeight, IPHONE_HEADER_MIN_HEIGHT) to if (topInset < STATUS_BAR_MIN_INSET) 0.dp else topInset
185
+ }
186
+
187
+ // Centered on the clock digits; the cluster's region center sits above them.
188
+ private fun statusBarRowGeometry(): Pair<Dp, Dp> = DuoMetrics.innerStatusBarCenterY * 2 to 0.dp
189
+
190
+ private data class StripOcclusion(val top: Dp, val topCenter: Dp, val bottom: Dp)
191
+
192
+ // How far system regions reach into a side strip from its top and from its bottom end
193
+ // (e.g. the camera, which sits at either end depending on the rotation), and the center y
194
+ // of the region reaching furthest from the top.
195
+ private fun stripOcclusion(regions: List<ReservedRegion>, stripLeft: Dp, stripRight: Dp, windowHeight: Dp): StripOcclusion {
196
+ var top = 0.dp
197
+ var topCenter = 0.dp
198
+ var bottom = 0.dp
199
+ regions.forEach { region ->
200
+ if (region.x >= stripRight || region.x + region.width <= stripLeft) return@forEach
201
+ val centerY = region.y + region.height / 2
202
+ if (centerY < windowHeight / 2) {
203
+ if (region.y + region.height > top) {
204
+ top = region.y + region.height
205
+ topCenter = centerY
206
+ }
207
+ } else {
208
+ bottom = maxOf(bottom, windowHeight - region.y)
209
+ }
210
+ }
211
+ return StripOcclusion(top, topCenter, bottom)
212
+ }
213
+
214
+ // How far the clock and battery clusters of a status bar on the top edge reach in from each side;
215
+ // null without reserved regions there.
216
+ private fun statusBarClusters(regions: List<ReservedRegion>, windowWidth: Dp, topInset: Dp): StatusBarClearance? {
217
+ val midX = windowWidth / 2
218
+ val clusters = regions.filter {
219
+ it.y < topInset && it.y + it.height <= topInset && (it.x + it.width <= midX || it.x >= midX)
220
+ }
221
+ if (clusters.isEmpty()) return null
222
+ var left = 0.dp
223
+ var right = 0.dp
224
+ clusters.forEach {
225
+ if (it.x < midX) left = maxOf(left, it.x + it.width) else right = maxOf(right, windowWidth - it.x)
226
+ }
227
+ return StatusBarClearance(left, right)
228
+ }
229
+
230
+ // Slab iPhones report equal side insets in landscape; iPhone Duo moves the status bar to one side.
231
+ private fun isIPhoneDuoWindow(width: Dp, height: Dp, asymmetricInsets: Boolean): Boolean {
232
+ val shortSide = minOf(width, height)
233
+ if (shortSide <= 0.dp) return asymmetricInsets
234
+ return maxOf(width, height) / shortSide < DuoMetrics.MAX_ASPECT_RATIO || asymmetricInsets
235
+ }
236
+
237
+ private fun screenLayoutOf(
238
+ width: Dp,
239
+ height: Dp,
240
+ insetLeft: Dp,
241
+ insetRight: Dp,
242
+ topInset: Dp,
243
+ bottomInset: Dp,
244
+ isPhone: Boolean,
245
+ asymmetricInsets: Boolean,
246
+ isDuoDevice: Boolean,
247
+ reservedRegions: List<ReservedRegion>,
248
+ ): ScreenLayout {
249
+ val isIPhoneDuo = isPhone && isIPhoneDuoWindow(width, height, asymmetricInsets)
250
+ val isInnerPortrait = height > width && width >= DuoMetrics.innerMinHeight
251
+ val statusBarClearance = if (isIPhoneDuo && isInnerPortrait && topInset >= STATUS_BAR_MIN_INSET) {
252
+ statusBarClusters(reservedRegions, width, topInset)
253
+ ?: StatusBarClearance(left = 0.dp, right = DuoMetrics.innerStatusBarWidth + Spacing.S)
254
+ } else {
255
+ null
256
+ }
257
+
258
+ var sideBar: SideBarLayout? = null
259
+ var titleCenterY = 0.dp
260
+ val sideInset = maxOf(insetLeft, insetRight)
261
+ // narrower strips cannot host a control 12pt from the inner edge
262
+ if (isIPhoneDuo && sideInset >= DuoMetrics.controlSize + Spacing.M) {
263
+ val edge = if (insetRight >= insetLeft) SideBarEdge.RIGHT else SideBarEdge.LEFT
264
+ val stripLeft = if (edge == SideBarEdge.RIGHT) width - sideInset else 0.dp
265
+ // clear of the camera and of the status bar cluster, which grows for calls and Live
266
+ // Activities; region frames already include the system's margin
267
+ val occlusion = stripOcclusion(reservedRegions, stripLeft, stripLeft + sideInset, height)
268
+ // UIKit reports no usable status bar frame here (a 2pt top strip), so the spot is inferred like RN
269
+ val top = when {
270
+ statusBarClearance != null -> statusBarRowGeometry().first + Spacing.M
271
+ topInset >= STATUS_BAR_MIN_INSET -> topInset + HEADER_HEIGHT.dp + Spacing.M
272
+ width <= height -> DuoMetrics.firstItemTop
273
+ height >= DuoMetrics.innerMinHeight -> DuoMetrics.firstItemTopLandscape
274
+ // outer display in landscape: no status bar in the strip
275
+ else -> DuoMetrics.edgeInset
276
+ }
277
+ // outer display in landscape: the title row centers on the camera when it sits at the
278
+ // top end, otherwise on the back button
279
+ titleCenterY = if (occlusion.top > top) occlusion.topCenter else top + DuoMetrics.controlSize / 2
280
+ sideBar = SideBarLayout(
281
+ edge = edge,
282
+ width = sideInset,
283
+ top = maxOf(top, occlusion.top),
284
+ bottom = maxOf(DuoMetrics.edgeInset, occlusion.bottom),
285
+ )
286
+ }
287
+ val (headerHeight, headerStatusBarHeight) = if (statusBarClearance != null) {
288
+ statusBarRowGeometry()
289
+ } else {
290
+ headerGeometry(topInset, isPhone, width, height, sideBar != null, titleCenterY)
291
+ }
292
+
293
+ return ScreenLayout(
294
+ width = width,
295
+ height = height,
296
+ isIPhoneDuo = isIPhoneDuo,
297
+ insetTop = topInset,
298
+ insetBottom = bottomInset,
299
+ insetLeft = insetLeft,
300
+ insetRight = insetRight,
301
+ contentWidth = width - insetLeft - insetRight,
302
+ headerHeight = headerHeight,
303
+ headerStatusBarHeight = headerStatusBarHeight,
304
+ sideBar = sideBar,
305
+ isIPhoneDuoDevice = isDuoDevice,
306
+ duoTopInset = maxOf(topInset, if (height > width) sideInset * 2 else 0.dp),
307
+ statusBarClearance = statusBarClearance,
308
+ )
309
+ }
@@ -3,6 +3,7 @@ package vn.momo.kits.layout
3
3
  import androidx.compose.foundation.layout.Arrangement
4
4
  import androidx.compose.foundation.layout.Box
5
5
  import androidx.compose.foundation.layout.BoxWithConstraints
6
+ import androidx.compose.foundation.layout.ExperimentalLayoutApi
6
7
  import androidx.compose.foundation.layout.FlowRow
7
8
  import androidx.compose.foundation.layout.fillMaxWidth
8
9
  import androidx.compose.foundation.layout.padding
@@ -11,27 +12,28 @@ import androidx.compose.runtime.CompositionLocalProvider
11
12
  import androidx.compose.ui.Modifier
12
13
  import androidx.compose.ui.unit.dp
13
14
  import vn.momo.kits.components.Image
15
+ import vn.momo.kits.const.Spacing
14
16
 
15
17
  @Composable
16
- internal fun Section(
18
+ @OptIn(ExperimentalLayoutApi::class)
19
+ fun Section(
17
20
  useMargin: Boolean = true,
18
21
  backgroundImage: String? = null,
19
22
  modifier: Modifier = Modifier,
20
23
  content: @Composable () -> Unit,
21
24
  ) {
22
- val columns = 12
23
- val gutter = 12.dp
24
- val margin = if (useMargin) 12.dp else 0.dp
25
- Box(modifier = modifier.fillMaxWidth().padding(horizontal = margin)) {
25
+ val columns = gridColumns()
26
+ val gap = Spacing.M
27
+ val margin = if (useMargin) Spacing.M else 0.dp
28
+ Box(modifier = Modifier.fillMaxWidth().padding(horizontal = margin).then(modifier)) {
26
29
  if (backgroundImage != null) {
27
30
  Image(source = backgroundImage, modifier = Modifier.matchParentSize())
28
31
  }
29
32
  BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
30
- val sizePerSpan = (maxWidth - gutter * (columns - 1)) / columns - 1.dp / columns
31
- CompositionLocalProvider(LocalGridContext provides GridContext(columns, gutter, sizePerSpan)) {
33
+ CompositionLocalProvider(LocalGridContext provides gridContext(constraints.maxWidth, columns, gap)) {
32
34
  FlowRow(
33
- horizontalArrangement = Arrangement.spacedBy(gutter),
34
- verticalArrangement = Arrangement.spacedBy(gutter),
35
+ horizontalArrangement = Arrangement.spacedBy(gap),
36
+ verticalArrangement = Arrangement.spacedBy(gap),
35
37
  ) { content() }
36
38
  }
37
39
  }
@@ -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,
@@ -30,10 +30,11 @@ import vn.momo.kits.const.AppNavigationBar
30
30
  import vn.momo.kits.const.AppStatusBar
31
31
  import vn.momo.kits.const.AppTheme
32
32
  import vn.momo.kits.const.Theme
33
- import vn.momo.kits.const.ProvideKitScale
34
33
  import vn.momo.kits.const.defaultTheme
35
- import vn.momo.kits.const.pendingSizeScale
34
+ import vn.momo.kits.layout.LocalScreenLayout
35
+ import vn.momo.kits.layout.calculateScreenLayout
36
36
  import vn.momo.kits.platform.ProvideNavigationEventDispatcherOwner
37
+ import vn.momo.kits.platform.ProvideOsFontScale
37
38
  import vn.momo.kits.utils.getAppStatusBarHeight
38
39
  import vn.momo.kits.utils.getNavigationBarHeight
39
40
  import vn.momo.maxapi.IMaxApi
@@ -55,10 +56,9 @@ fun NavigationContainer(
55
56
  val navController = rememberNavController()
56
57
  val registry = remember { DynamicScreenRegistry() }
57
58
  val navigator = remember { Navigator(navController = navController, maxApi = maxApi, registry = registry) }
58
- // Insets are measured in real units; convert to the dp of the size-scaled container.
59
- val sizeScale = pendingSizeScale()
60
- val statusBarHeight = (statusBarHeight ?: getAppStatusBarHeight()) / sizeScale
61
- val navigationBarHeight = getNavigationBarHeight() / sizeScale
59
+ val statusBarHeight = statusBarHeight ?: getAppStatusBarHeight()
60
+ val navigationBarHeight = getNavigationBarHeight()
61
+ val screenLayout = calculateScreenLayout(statusBarHeight, parent = LocalScreenLayout.current)
62
62
 
63
63
  val parentContext = LocalContext.current
64
64
  val mergedContext = MiniAppContext.merge(parentContext, miniAppContext)
@@ -95,7 +95,7 @@ fun NavigationContainer(
95
95
  registry.bind(screenId, initialScreenName, initialScreen, options)
96
96
  val startDestination = remember(screenId) { DynamicScreenRoute(screenId) }
97
97
 
98
- ProvideKitScale {
98
+ ProvideOsFontScale {
99
99
  ProvideNavigationEventDispatcherOwner(navigator) {
100
100
  CompositionLocalProvider(
101
101
  LocalNavigator provides navigator,
@@ -104,6 +104,7 @@ fun NavigationContainer(
104
104
  AppTheme provides resolvedTheme,
105
105
  AppStatusBar provides statusBarHeight,
106
106
  AppNavigationBar provides navigationBarHeight,
107
+ LocalScreenLayout provides screenLayout,
107
108
  LocalContext provides mergedContext,
108
109
  LocalDesignSystemConfig provides resolvedDesignSystemConfig,
109
110
  LocalLocalize provides resolvedLocalize,
@@ -55,6 +55,12 @@ class Navigator(
55
55
  }
56
56
 
57
57
  fun onBackSafe(callBack: (() -> Unit)? = null) {
58
+ // An open modal or bottom sheet takes the back first, as its BackHandler does for gestures.
59
+ val overplayType = OverplayComponentRegistry.getOverplayType()
60
+ if (overplayType != null && overplayType != OverplayComponentType.SNACK_BAR) {
61
+ pop(1, callBack)
62
+ return
63
+ }
58
64
  tabBackHandler?.let {
59
65
  it()
60
66
  return