@momo-kits/native-kits 0.167.1-duo.2-debug → 0.167.1-duo.20-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 (194) hide show
  1. package/CLAUDE.md +36 -8
  2. package/README.md +2 -2
  3. package/compose/build.gradle.kts +15 -1
  4. package/compose/build.gradle.kts.backup +14 -0
  5. package/compose/compose.podspec +1 -1
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
  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 +168 -52
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +24 -14
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +1 -1
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +53 -26
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -2
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +4 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +10 -4
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +3 -17
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +173 -21
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +153 -48
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +114 -71
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +8 -9
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +84 -18
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +33 -13
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +2 -1
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +51 -19
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +4 -2
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -1
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +46 -23
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
  57. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +38 -0
  58. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  59. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +94 -0
  60. package/gradle.properties +1 -1
  61. package/ios/Application/ApplicationEnvironment.swift +17 -0
  62. package/ios/Application/MaxApi.swift +12 -3
  63. package/ios/Avatar/Avatar.swift +4 -3
  64. package/ios/Badge/Badge.swift +6 -4
  65. package/ios/Badge/BadgeDot.swift +1 -1
  66. package/ios/Badge/BadgeRibbon.swift +0 -1
  67. package/ios/Button/Button.swift +15 -12
  68. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  69. package/ios/Carousel/Carousel.swift +29 -12
  70. package/ios/Checkbox/Checkbox.swift +7 -3
  71. package/ios/Chip/Chip.swift +5 -4
  72. package/ios/Collapse/Collapse.swift +70 -19
  73. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  74. package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
  75. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  76. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  77. package/ios/Divider/Divider.swift +37 -4
  78. package/ios/Extensions/Color++.swift +38 -9
  79. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  80. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  81. package/ios/Icon/Icon.swift +21 -23
  82. package/ios/Icon/Icons.swift +1329 -0
  83. package/ios/IconButton/IconButton.swift +15 -7
  84. package/ios/Image/Image.swift +7 -23
  85. package/ios/Information/Information.swift +27 -9
  86. package/ios/Input/ErrorView.swift +2 -2
  87. package/ios/Input/Input.swift +160 -147
  88. package/ios/Input/InputPhoneNumber.swift +12 -9
  89. package/ios/Input/InputSearch.swift +102 -75
  90. package/ios/Input/InputTextArea.swift +128 -145
  91. package/ios/InputDropDown/InputDropDown.swift +10 -7
  92. package/ios/InputMoney/InputMoney.swift +33 -11
  93. package/ios/InputOTP/InputOTP.swift +22 -12
  94. package/ios/Layout/Card.swift +50 -0
  95. package/ios/Layout/GridContext.swift +187 -3
  96. package/ios/Layout/Item.swift +5 -4
  97. package/ios/Layout/ScreenLayout.swift +380 -0
  98. package/ios/Layout/Section.swift +4 -100
  99. package/ios/Loader/Loader.swift +5 -9
  100. package/ios/Lottie/LottieView.swift +14 -2
  101. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +128 -9
  102. package/ios/Navigation/BottomTab/BottomTab.swift +185 -0
  103. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +15 -7
  104. package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
  105. package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
  106. package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
  107. package/ios/Navigation/Component/Header.swift +531 -0
  108. package/ios/Navigation/Component/HeaderBackground.swift +133 -0
  109. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  110. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  111. package/ios/Navigation/Component/HeaderRight.swift +377 -0
  112. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  113. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  114. package/ios/Navigation/Component/SideBar.swift +183 -0
  115. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -6
  116. package/ios/Navigation/HostBackGestureBridge.swift +78 -0
  117. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +5 -2
  118. package/ios/Navigation/Navigation.swift +204 -0
  119. package/ios/Navigation/NavigationContainer.swift +194 -0
  120. package/ios/{Application/Navigation → Navigation}/Navigator.swift +177 -91
  121. package/ios/Navigation/StackScreen.swift +820 -0
  122. package/ios/Navigation/StackTransition.swift +400 -0
  123. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  124. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  125. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  126. package/ios/Pagination/PaginationScroll.swift +2 -2
  127. package/ios/Popup/PopupDisplay.swift +72 -136
  128. package/ios/Popup/PopupInput.swift +10 -13
  129. package/ios/Popup/PopupLayout.swift +140 -0
  130. package/ios/Popup/PopupNotify.swift +56 -73
  131. package/ios/Popup/PopupPromotion.swift +31 -47
  132. package/ios/PopupView/FullscreenPopup.swift +13 -19
  133. package/ios/PopupView/PopupView.swift +1 -1
  134. package/ios/PopupView/Utils++.swift +0 -8
  135. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  136. package/ios/Radio/Radio.swift +2 -1
  137. package/ios/Rating/Rating.swift +9 -1
  138. package/ios/Resources/dot_loading.json +558 -0
  139. package/ios/Resources/lottie_circle_loader.json +1 -0
  140. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  141. package/ios/Skeleton/Skeleton.swift +22 -27
  142. package/ios/Slider/Slider.swift +16 -18
  143. package/ios/Stepper/Stepper.swift +6 -1
  144. package/ios/Steps/Steps.swift +8 -6
  145. package/ios/SuggestAction/SuggestAction.swift +5 -0
  146. package/ios/Swipeable/SwipeCell.swift +10 -6
  147. package/ios/Switch/Switch.swift +22 -20
  148. package/ios/TabView/TabView.swift +102 -21
  149. package/ios/Tag/Tag.swift +15 -4
  150. package/ios/Template/Logo/Logo.swift +2 -1
  151. package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
  152. package/ios/Title/Title.swift +6 -17
  153. package/ios/Tooltip/Tooltip.swift +29 -14
  154. package/ios/Typography/Text.swift +61 -19
  155. package/ios/Uploader/Uploader.swift +3 -1
  156. package/ios/native-kits.podspec +2 -1
  157. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  158. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  159. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  160. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
  161. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  162. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  163. package/package.json +1 -1
  164. package/scripts/gen-ios-icons.mjs +36 -0
  165. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  166. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  167. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  168. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  169. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  170. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  171. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  172. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  173. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  174. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  175. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  176. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  177. package/ios/Application/Components.swift +0 -270
  178. package/ios/Application/ComposeApi.swift +0 -22
  179. package/ios/Application/HeaderRight.swift +0 -306
  180. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  181. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  182. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  183. package/ios/Application/Navigation/NavigationContainer.swift +0 -213
  184. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  185. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  186. package/ios/Application/Navigation/component/Header.swift +0 -178
  187. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  188. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  189. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  190. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  191. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  192. package/ios/Application/Screen.swift +0 -250
  193. package/ios/Application/StackScreen.swift +0 -492
  194. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -1,5 +1,7 @@
1
1
  package vn.momo.kits.layout
2
2
 
3
+ import androidx.compose.foundation.layout.WindowInsets
4
+ import androidx.compose.foundation.layout.systemBars
3
5
  import androidx.compose.runtime.Composable
4
6
  import androidx.compose.runtime.Immutable
5
7
  import androidx.compose.runtime.compositionLocalOf
@@ -11,7 +13,10 @@ import androidx.compose.ui.unit.Dp
11
13
  import androidx.compose.ui.unit.dp
12
14
  import vn.momo.kits.const.Spacing
13
15
  import vn.momo.kits.navigation.component.HEADER_HEIGHT
14
- import vn.momo.kits.platform.getPlatformName
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
15
20
  import vn.momo.kits.utils.getAppStatusBarHeight
16
21
  import vn.momo.kits.utils.horizontalSafeAreaInsets
17
22
 
@@ -23,13 +28,22 @@ data class SideBarLayout(
23
28
  val width: Dp,
24
29
  // Preferred window y of the first control; the column is still kept above footer and bottom bars.
25
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,
26
33
  )
27
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
+
28
39
  @Immutable
29
40
  data class ScreenLayout(
30
41
  val width: Dp,
31
42
  val height: Dp,
32
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,
33
47
  val insetLeft: Dp,
34
48
  val insetRight: Dp,
35
49
  val contentWidth: Dp,
@@ -38,6 +52,13 @@ data class ScreenLayout(
38
52
  val headerStatusBarHeight: Dp,
39
53
  // iPhone Duo moves back and toolkit into a column in the side safe area; null while they stay in the bar.
40
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,
41
62
  ) {
42
63
  val headerBarHeight: Dp get() = headerHeight - headerStatusBarHeight
43
64
  }
@@ -50,11 +71,26 @@ internal object DuoMetrics {
50
71
  val itemPitch = 50.dp
51
72
  // Portrait: below the camera/clock cluster in the strip (ends near y 151).
52
73
  val firstItemTop = 170.dp
53
- // Landscape: below the inner display's status cluster (ends near y 98); folded, it clears the camera
54
- // (ends near y 60) and leaves the longest toolkit ~36pt above the footer.
74
+ // Inner display in landscape: below its status cluster (ends near y 98).
55
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
56
89
  }
57
90
 
91
+ // Widest short side of any single-display iPhone.
92
+ private val IPHONE_MAX_SHORT_SIDE = 440.dp
93
+
58
94
  private val IPHONE_HEADER_MIN_HEIGHT = 72.dp
59
95
 
60
96
  // A smaller top inset means the status bar is not on the top edge.
@@ -69,10 +105,21 @@ fun getScreenLayout(): ScreenLayout =
69
105
 
70
106
  @Composable
71
107
  internal fun calculateScreenLayout(topInset: Dp, parent: ScreenLayout? = null): ScreenLayout {
72
- val isIOS = getPlatformName() == "iOS"
108
+ val isPhone = isPhoneIdiom()
73
109
  if (parent != null) {
74
- val (headerHeight, headerStatusBarHeight) = headerGeometry(topInset, isIOS)
75
- return parent.copy(headerHeight = headerHeight, headerStatusBarHeight = headerStatusBarHeight)
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
+ )
76
123
  }
77
124
 
78
125
  val density = LocalDensity.current
@@ -81,17 +128,28 @@ internal fun calculateScreenLayout(topInset: Dp, parent: ScreenLayout? = null):
81
128
  val insets = horizontalSafeAreaInsets
82
129
  val leftPx = insets.getLeft(density, layoutDirection)
83
130
  val rightPx = insets.getRight(density, layoutDirection)
131
+ val bottomPx = WindowInsets.systemBars.getBottom(density)
132
+ val screen = getScreenDimensions()
133
+ val reservedRegions = rememberReservedRegions()
84
134
 
85
- return remember(size, leftPx, rightPx, topInset, density) {
135
+ return remember(size, leftPx, rightPx, bottomPx, topInset, screen, density, reservedRegions) {
86
136
  with(density) {
137
+ val width = size.width.toDp()
138
+ val height = size.height.toDp()
87
139
  screenLayoutOf(
88
- width = size.width.toDp(),
89
- height = size.height.toDp(),
140
+ width = width,
141
+ height = height,
90
142
  insetLeft = leftPx.toDp(),
91
143
  insetRight = rightPx.toDp(),
92
144
  topInset = topInset,
93
- isIOS = isIOS,
145
+ bottomInset = bottomPx.toDp(),
146
+ isPhone = isPhone,
94
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,
95
153
  )
96
154
  }
97
155
  }
@@ -101,10 +159,72 @@ internal fun calculateScreenLayout(topInset: Dp, parent: ScreenLayout? = null):
101
159
  internal fun ScreenLayout.consumeHorizontalInsets(): ScreenLayout =
102
160
  copy(width = contentWidth, insetLeft = 0.dp, insetRight = 0.dp, sideBar = null)
103
161
 
104
- private fun headerGeometry(topInset: Dp, isIOS: Boolean): Pair<Dp, Dp> {
105
- val height = topInset + HEADER_HEIGHT.dp
106
- if (!isIOS) return height to topInset
107
- return maxOf(height, IPHONE_HEADER_MIN_HEIGHT) to if (topInset < STATUS_BAR_MIN_INSET) 0.dp else topInset
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)
108
228
  }
109
229
 
110
230
  // Slab iPhones report equal side insets in landscape; iPhone Duo moves the status bar to one side.
@@ -120,38 +240,70 @@ private fun screenLayoutOf(
120
240
  insetLeft: Dp,
121
241
  insetRight: Dp,
122
242
  topInset: Dp,
123
- isIOS: Boolean,
243
+ bottomInset: Dp,
244
+ isPhone: Boolean,
124
245
  asymmetricInsets: Boolean,
246
+ isDuoDevice: Boolean,
247
+ reservedRegions: List<ReservedRegion>,
125
248
  ): ScreenLayout {
126
- val (headerHeight, headerStatusBarHeight) = headerGeometry(topInset, isIOS)
127
- val isIPhoneDuo = isIOS && isIPhoneDuoWindow(width, height, asymmetricInsets)
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
+ }
128
257
 
129
258
  var sideBar: SideBarLayout? = null
259
+ var titleCenterY = 0.dp
130
260
  val sideInset = maxOf(insetLeft, insetRight)
131
261
  // narrower strips cannot host a control 12pt from the inner edge
132
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)
133
268
  // UIKit reports no usable status bar frame here (a 2pt top strip), so the spot is inferred like RN
134
269
  val top = when {
270
+ statusBarClearance != null -> statusBarRowGeometry().first + Spacing.M
135
271
  topInset >= STATUS_BAR_MIN_INSET -> topInset + HEADER_HEIGHT.dp + Spacing.M
136
- width > height -> DuoMetrics.firstItemTopLandscape
137
- else -> DuoMetrics.firstItemTop
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
138
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
139
280
  sideBar = SideBarLayout(
140
- edge = if (insetRight >= insetLeft) SideBarEdge.RIGHT else SideBarEdge.LEFT,
281
+ edge = edge,
141
282
  width = sideInset,
142
- top = top,
283
+ top = maxOf(top, occlusion.top),
284
+ bottom = maxOf(DuoMetrics.edgeInset, occlusion.bottom),
143
285
  )
144
286
  }
287
+ val (headerHeight, headerStatusBarHeight) = if (statusBarClearance != null) {
288
+ statusBarRowGeometry()
289
+ } else {
290
+ headerGeometry(topInset, isPhone, width, height, sideBar != null, titleCenterY)
291
+ }
145
292
 
146
293
  return ScreenLayout(
147
294
  width = width,
148
295
  height = height,
149
296
  isIPhoneDuo = isIPhoneDuo,
297
+ insetTop = topInset,
298
+ insetBottom = bottomInset,
150
299
  insetLeft = insetLeft,
151
300
  insetRight = insetRight,
152
301
  contentWidth = width - insetLeft - insetRight,
153
302
  headerHeight = headerHeight,
154
303
  headerStatusBarHeight = headerStatusBarHeight,
155
304
  sideBar = sideBar,
305
+ isIPhoneDuoDevice = isDuoDevice,
306
+ duoTopInset = maxOf(topInset, if (height > width) sideInset * 2 else 0.dp),
307
+ statusBarClearance = statusBarClearance,
156
308
  )
157
309
  }
@@ -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,