@momo-kits/native-kits 0.167.1-duo.1-debug → 0.167.1-duo.10-debug

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CLAUDE.md +32 -0
  2. package/compose/build.gradle.kts +15 -1
  3. package/compose/build.gradle.kts.backup +14 -0
  4. package/compose/compose.podspec +1 -1
  5. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +1 -1
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +53 -26
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -2
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +13 -11
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +21 -9
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +232 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +11 -9
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +4 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +143 -31
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +100 -66
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +22 -16
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +66 -18
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +23 -14
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +81 -24
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +185 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +4 -1
  56. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  57. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +86 -0
  58. package/gradle.properties +1 -1
  59. package/ios/Application/ApplicationEnvironment.swift +17 -0
  60. package/ios/Application/ComposeApi.swift +2 -1
  61. package/ios/Application/{Components.swift → LegacyHeader.swift} +7 -65
  62. package/ios/Application/Screen.swift +3 -2
  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/DateTimePicker/DateTimePicker.swift +54 -20
  75. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  76. package/ios/Divider/Divider.swift +37 -4
  77. package/ios/Extensions/Color++.swift +38 -9
  78. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  79. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  80. package/ios/Icon/Icon.swift +21 -23
  81. package/ios/Icon/Icons.swift +1329 -0
  82. package/ios/IconButton/IconButton.swift +15 -7
  83. package/ios/Image/Image.swift +7 -23
  84. package/ios/Information/Information.swift +27 -9
  85. package/ios/Input/ErrorView.swift +2 -2
  86. package/ios/Input/Input.swift +160 -147
  87. package/ios/Input/InputPhoneNumber.swift +12 -9
  88. package/ios/Input/InputSearch.swift +102 -75
  89. package/ios/Input/InputTextArea.swift +128 -145
  90. package/ios/InputDropDown/InputDropDown.swift +10 -7
  91. package/ios/InputMoney/InputMoney.swift +33 -11
  92. package/ios/InputOTP/InputOTP.swift +22 -12
  93. package/ios/Layout/Card.swift +50 -0
  94. package/ios/Layout/GridContext.swift +123 -3
  95. package/ios/Layout/Item.swift +5 -4
  96. package/ios/Layout/ScreenLayout.swift +312 -0
  97. package/ios/Layout/Section.swift +4 -100
  98. package/ios/Loader/Loader.swift +5 -9
  99. package/ios/Lottie/LottieView.swift +14 -2
  100. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +97 -8
  101. package/ios/Navigation/BottomTab/BottomTab.swift +187 -0
  102. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +15 -7
  103. package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
  104. package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
  105. package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
  106. package/ios/Navigation/Component/Header.swift +493 -0
  107. package/ios/Navigation/Component/HeaderBackground.swift +134 -0
  108. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  109. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  110. package/ios/Navigation/Component/HeaderRight.swift +386 -0
  111. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  112. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  113. package/ios/Navigation/Component/SideBar.swift +176 -0
  114. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -5
  115. package/ios/Navigation/HostBackGestureBridge.swift +57 -0
  116. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +5 -1
  117. package/ios/Navigation/Navigation.swift +190 -0
  118. package/ios/{Application/Navigation → Navigation}/NavigationContainer.swift +92 -81
  119. package/ios/{Application/Navigation → Navigation}/Navigator.swift +185 -36
  120. package/ios/Navigation/StackScreen.swift +782 -0
  121. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  122. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  123. package/ios/Pagination/PaginationScroll.swift +2 -2
  124. package/ios/Popup/PopupDisplay.swift +72 -136
  125. package/ios/Popup/PopupInput.swift +10 -13
  126. package/ios/Popup/PopupLayout.swift +140 -0
  127. package/ios/Popup/PopupNotify.swift +56 -73
  128. package/ios/Popup/PopupPromotion.swift +31 -47
  129. package/ios/PopupView/FullscreenPopup.swift +13 -19
  130. package/ios/PopupView/PopupView.swift +1 -1
  131. package/ios/PopupView/Utils++.swift +0 -8
  132. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  133. package/ios/Radio/Radio.swift +2 -1
  134. package/ios/Rating/Rating.swift +9 -1
  135. package/ios/Resources/dot_loading.json +558 -0
  136. package/ios/Resources/lottie_circle_loader.json +1 -0
  137. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  138. package/ios/Skeleton/Skeleton.swift +22 -27
  139. package/ios/Slider/Slider.swift +16 -18
  140. package/ios/Stepper/Stepper.swift +6 -1
  141. package/ios/Steps/Steps.swift +8 -6
  142. package/ios/SuggestAction/SuggestAction.swift +5 -0
  143. package/ios/Swipeable/SwipeCell.swift +10 -6
  144. package/ios/Switch/Switch.swift +22 -20
  145. package/ios/TabView/TabView.swift +102 -21
  146. package/ios/Tag/Tag.swift +15 -4
  147. package/ios/Template/Logo/Logo.swift +2 -1
  148. package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
  149. package/ios/Title/Title.swift +6 -17
  150. package/ios/Tooltip/Tooltip.swift +29 -14
  151. package/ios/Typography/Text.swift +61 -19
  152. package/ios/Uploader/Uploader.swift +3 -1
  153. package/ios/native-kits.podspec +2 -1
  154. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  155. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  156. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  157. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
  158. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  159. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  160. package/package.json +1 -1
  161. package/scripts/gen-ios-icons.mjs +36 -0
  162. package/.claude/settings.local.json +0 -185
  163. package/ios/Application/HeaderRight.swift +0 -306
  164. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  165. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  166. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  167. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  168. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  169. package/ios/Application/Navigation/component/Header.swift +0 -178
  170. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  171. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  172. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  173. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  174. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  175. package/ios/Application/StackScreen.swift +0 -492
  176. package/local.properties +0 -8
  177. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -0,0 +1,232 @@
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
+ @Immutable
36
+ data class ScreenLayout(
37
+ val width: Dp,
38
+ val height: Dp,
39
+ val isIPhoneDuo: Boolean,
40
+ // Raw top / bottom safe area; overlays and the Duo floating tab bar keep clear of them.
41
+ val insetTop: Dp,
42
+ val insetBottom: Dp,
43
+ val insetLeft: Dp,
44
+ val insetRight: Dp,
45
+ val contentWidth: Dp,
46
+ val headerHeight: Dp,
47
+ // 0 when the status bar is off the top edge, so the bar row fills the header and keeps the title centered.
48
+ val headerStatusBarHeight: Dp,
49
+ // iPhone Duo moves back and toolkit into a column in the side safe area; null while they stay in the bar.
50
+ val sideBar: SideBarLayout? = null,
51
+ // Port of RN `device.ts` isIPhoneDuo: the screen stays on the outer display, so Split View still matches.
52
+ val isIPhoneDuoDevice: Boolean = isIPhoneDuo,
53
+ // Portrait iPhone Duo puts the status bar in the side strip, about twice as tall as the strip is wide.
54
+ // Stored so it survives consumeHorizontalInsets().
55
+ val duoTopInset: Dp = insetTop,
56
+ ) {
57
+ val headerBarHeight: Dp get() = headerHeight - headerStatusBarHeight
58
+ }
59
+
60
+ // Measured by momo-kits RN (Layout/utils.ts) on the iOS 27.1 iPhone Duo simulator; keep both in sync.
61
+ internal object DuoMetrics {
62
+ // Inner display is ~1.42, outer ~1.45; every slab iPhone is 1.77 or taller.
63
+ const val MAX_ASPECT_RATIO = 1.6f
64
+ val controlSize = 48.dp
65
+ val itemPitch = 50.dp
66
+ // Portrait: below the camera/clock cluster in the strip (ends near y 151).
67
+ val firstItemTop = 170.dp
68
+ // Inner display in landscape: below its status cluster (ends near y 98).
69
+ val firstItemTopLandscape = 120.dp
70
+ // Window height from which the inner display is in use (outer is 466pt tall in landscape, inner 669pt).
71
+ val innerMinHeight = 600.dp
72
+ // Outer display in portrait: the header is 4pt taller than the iPhone floor and its title
73
+ // lines up with the camera punchout (y 24–66 at the top of the strip).
74
+ val outerHeaderHeight = 76.dp
75
+ val outerCameraCenterY = 45.dp
76
+ // Outer display in landscape has no status bar in the strip: controls keep this far from
77
+ // both ends unless a reserved region (the camera at either end) asks for more.
78
+ val edgeInset = 24.dp
79
+ }
80
+
81
+ // Widest short side of any single-display iPhone.
82
+ private val IPHONE_MAX_SHORT_SIDE = 440.dp
83
+
84
+ private val IPHONE_HEADER_MIN_HEIGHT = 72.dp
85
+
86
+ // A smaller top inset means the status bar is not on the top edge.
87
+ private val STATUS_BAR_MIN_INSET = 20.dp
88
+
89
+ internal val LocalScreenLayout = compositionLocalOf<ScreenLayout?> { null }
90
+
91
+ // Port of RN `useScreenLayout`; follows fold/unfold and rotation through the window size and safe area.
92
+ @Composable
93
+ fun getScreenLayout(): ScreenLayout =
94
+ LocalScreenLayout.current ?: calculateScreenLayout(getAppStatusBarHeight())
95
+
96
+ @Composable
97
+ internal fun calculateScreenLayout(topInset: Dp, parent: ScreenLayout? = null): ScreenLayout {
98
+ val isPhone = isPhoneIdiom()
99
+ if (parent != null) {
100
+ val (headerHeight, headerStatusBarHeight) =
101
+ headerGeometry(topInset, isPhone, parent.width, parent.height, parent.sideBar != null)
102
+ return parent.copy(headerHeight = headerHeight, headerStatusBarHeight = headerStatusBarHeight)
103
+ }
104
+
105
+ val density = LocalDensity.current
106
+ val layoutDirection = LocalLayoutDirection.current
107
+ val size = LocalWindowInfo.current.containerSize
108
+ val insets = horizontalSafeAreaInsets
109
+ val leftPx = insets.getLeft(density, layoutDirection)
110
+ val rightPx = insets.getRight(density, layoutDirection)
111
+ val bottomPx = WindowInsets.systemBars.getBottom(density)
112
+ val screen = getScreenDimensions()
113
+ val reservedRegions = rememberReservedRegions()
114
+
115
+ return remember(size, leftPx, rightPx, bottomPx, topInset, screen, density, reservedRegions) {
116
+ with(density) {
117
+ val width = size.width.toDp()
118
+ val height = size.height.toDp()
119
+ screenLayoutOf(
120
+ width = width,
121
+ height = height,
122
+ insetLeft = leftPx.toDp(),
123
+ insetRight = rightPx.toDp(),
124
+ topInset = topInset,
125
+ bottomInset = bottomPx.toDp(),
126
+ isPhone = isPhone,
127
+ asymmetricInsets = leftPx != rightPx,
128
+ isDuoDevice = isPhone && maxOf(
129
+ minOf(screen.width, screen.height).dp,
130
+ minOf(width, height),
131
+ ) > IPHONE_MAX_SHORT_SIDE,
132
+ reservedRegions = reservedRegions,
133
+ )
134
+ }
135
+ }
136
+ }
137
+
138
+ // Content inside the safe-area padding: the insets are spent and the side bar belongs to the screen.
139
+ internal fun ScreenLayout.consumeHorizontalInsets(): ScreenLayout =
140
+ copy(width = contentWidth, insetLeft = 0.dp, insetRight = 0.dp, sideBar = null)
141
+
142
+ private fun headerGeometry(topInset: Dp, isPhone: Boolean, width: Dp, height: Dp, hasSideBar: Boolean): Pair<Dp, Dp> {
143
+ val headerHeight = topInset + HEADER_HEIGHT.dp
144
+ if (!isPhone) return headerHeight to topInset
145
+ if (hasSideBar && topInset < STATUS_BAR_MIN_INSET && height > width && width < DuoMetrics.innerMinHeight) {
146
+ // outer display: the row starts below the top so the title centers on the camera
147
+ return DuoMetrics.outerHeaderHeight to DuoMetrics.outerCameraCenterY * 2 - DuoMetrics.outerHeaderHeight
148
+ }
149
+ return maxOf(headerHeight, IPHONE_HEADER_MIN_HEIGHT) to if (topInset < STATUS_BAR_MIN_INSET) 0.dp else topInset
150
+ }
151
+
152
+ private data class StripOcclusion(val top: Dp, val bottom: Dp)
153
+
154
+ // How far system regions reach into a side strip from its top and from its bottom end
155
+ // (e.g. the camera, which sits at either end depending on the rotation).
156
+ private fun stripOcclusion(regions: List<ReservedRegion>, stripLeft: Dp, stripRight: Dp, windowHeight: Dp): StripOcclusion {
157
+ var top = 0.dp
158
+ var bottom = 0.dp
159
+ regions.forEach { region ->
160
+ if (region.x >= stripRight || region.x + region.width <= stripLeft) return@forEach
161
+ if (region.y + region.height / 2 < windowHeight / 2) {
162
+ top = maxOf(top, region.y + region.height)
163
+ } else {
164
+ bottom = maxOf(bottom, windowHeight - region.y)
165
+ }
166
+ }
167
+ return StripOcclusion(top, bottom)
168
+ }
169
+
170
+ // Slab iPhones report equal side insets in landscape; iPhone Duo moves the status bar to one side.
171
+ private fun isIPhoneDuoWindow(width: Dp, height: Dp, asymmetricInsets: Boolean): Boolean {
172
+ val shortSide = minOf(width, height)
173
+ if (shortSide <= 0.dp) return asymmetricInsets
174
+ return maxOf(width, height) / shortSide < DuoMetrics.MAX_ASPECT_RATIO || asymmetricInsets
175
+ }
176
+
177
+ private fun screenLayoutOf(
178
+ width: Dp,
179
+ height: Dp,
180
+ insetLeft: Dp,
181
+ insetRight: Dp,
182
+ topInset: Dp,
183
+ bottomInset: Dp,
184
+ isPhone: Boolean,
185
+ asymmetricInsets: Boolean,
186
+ isDuoDevice: Boolean,
187
+ reservedRegions: List<ReservedRegion>,
188
+ ): ScreenLayout {
189
+ val isIPhoneDuo = isPhone && isIPhoneDuoWindow(width, height, asymmetricInsets)
190
+
191
+ var sideBar: SideBarLayout? = null
192
+ val sideInset = maxOf(insetLeft, insetRight)
193
+ // narrower strips cannot host a control 12pt from the inner edge
194
+ if (isIPhoneDuo && sideInset >= DuoMetrics.controlSize + Spacing.M) {
195
+ // UIKit reports no usable status bar frame here (a 2pt top strip), so the spot is inferred like RN
196
+ val top = when {
197
+ topInset >= STATUS_BAR_MIN_INSET -> topInset + HEADER_HEIGHT.dp + Spacing.M
198
+ width <= height -> DuoMetrics.firstItemTop
199
+ height >= DuoMetrics.innerMinHeight -> DuoMetrics.firstItemTopLandscape
200
+ // outer display in landscape: no status bar in the strip
201
+ else -> DuoMetrics.edgeInset
202
+ }
203
+ val edge = if (insetRight >= insetLeft) SideBarEdge.RIGHT else SideBarEdge.LEFT
204
+ val stripLeft = if (edge == SideBarEdge.RIGHT) width - sideInset else 0.dp
205
+ // clear of the camera and of the status bar cluster, which grows for calls and Live
206
+ // Activities; region frames already include the system's margin
207
+ val occlusion = stripOcclusion(reservedRegions, stripLeft, stripLeft + sideInset, height)
208
+ sideBar = SideBarLayout(
209
+ edge = edge,
210
+ width = sideInset,
211
+ top = maxOf(top, occlusion.top),
212
+ bottom = maxOf(DuoMetrics.edgeInset, occlusion.bottom),
213
+ )
214
+ }
215
+ val (headerHeight, headerStatusBarHeight) = headerGeometry(topInset, isPhone, width, height, sideBar != null)
216
+
217
+ return ScreenLayout(
218
+ width = width,
219
+ height = height,
220
+ isIPhoneDuo = isIPhoneDuo,
221
+ insetTop = topInset,
222
+ insetBottom = bottomInset,
223
+ insetLeft = insetLeft,
224
+ insetRight = insetRight,
225
+ contentWidth = width - insetLeft - insetRight,
226
+ headerHeight = headerHeight,
227
+ headerStatusBarHeight = headerStatusBarHeight,
228
+ sideBar = sideBar,
229
+ isIPhoneDuoDevice = isDuoDevice,
230
+ duoTopInset = maxOf(topInset, if (height > width) sideInset * 2 else 0.dp),
231
+ )
232
+ }
@@ -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
  ) {
@@ -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
@@ -56,6 +58,7 @@ fun NavigationContainer(
56
58
  val navigator = remember { Navigator(navController = navController, maxApi = maxApi, registry = registry) }
57
59
  val statusBarHeight = statusBarHeight ?: getAppStatusBarHeight()
58
60
  val navigationBarHeight = getNavigationBarHeight()
61
+ val screenLayout = calculateScreenLayout(statusBarHeight, parent = LocalScreenLayout.current)
59
62
 
60
63
  val parentContext = LocalContext.current
61
64
  val mergedContext = MiniAppContext.merge(parentContext, miniAppContext)
@@ -101,6 +104,7 @@ fun NavigationContainer(
101
104
  AppTheme provides resolvedTheme,
102
105
  AppStatusBar provides statusBarHeight,
103
106
  AppNavigationBar provides navigationBarHeight,
107
+ LocalScreenLayout provides screenLayout,
104
108
  LocalContext provides mergedContext,
105
109
  LocalDesignSystemConfig provides resolvedDesignSystemConfig,
106
110
  LocalLocalize provides resolvedLocalize,