@momo-kits/native-kits 0.162.44-debug → 0.162.45-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 (199) 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 +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 +351 -111
  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 +1 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +55 -22
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -1
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +2 -1
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +1 -2
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +314 -0
  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/Navigation.kt +10 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +4 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +197 -60
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +127 -83
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +54 -62
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +97 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +377 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +122 -24
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +34 -14
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +116 -26
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
  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/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +13 -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/libs.versions.toml +1 -1
  61. package/gradle.properties +1 -1
  62. package/ios/Application/ApplicationEnvironment.swift +17 -0
  63. package/ios/Application/MaxApi.swift +30 -3
  64. package/ios/Avatar/Avatar.swift +4 -3
  65. package/ios/Badge/Badge.swift +6 -4
  66. package/ios/Badge/BadgeDot.swift +1 -1
  67. package/ios/Badge/BadgeRibbon.swift +0 -1
  68. package/ios/Button/Button.swift +15 -12
  69. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  70. package/ios/Carousel/Carousel.swift +29 -12
  71. package/ios/Checkbox/Checkbox.swift +7 -3
  72. package/ios/Chip/Chip.swift +5 -4
  73. package/ios/Collapse/Collapse.swift +70 -19
  74. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  75. package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
  76. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  77. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  78. package/ios/Divider/Divider.swift +37 -4
  79. package/ios/Extensions/Color++.swift +38 -9
  80. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  81. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  82. package/ios/Icon/Icon.swift +21 -23
  83. package/ios/Icon/Icons.swift +1329 -0
  84. package/ios/IconButton/IconButton.swift +15 -7
  85. package/ios/Image/Image.swift +7 -23
  86. package/ios/Information/Information.swift +27 -9
  87. package/ios/Input/ErrorView.swift +2 -2
  88. package/ios/Input/Input.swift +160 -147
  89. package/ios/Input/InputPhoneNumber.swift +12 -9
  90. package/ios/Input/InputSearch.swift +107 -77
  91. package/ios/Input/InputTextArea.swift +128 -145
  92. package/ios/InputDropDown/InputDropDown.swift +10 -7
  93. package/ios/InputMoney/InputMoney.swift +33 -11
  94. package/ios/InputOTP/InputOTP.swift +22 -12
  95. package/ios/Layout/Card.swift +50 -0
  96. package/ios/Layout/GridContext.swift +187 -3
  97. package/ios/Layout/Item.swift +5 -4
  98. package/ios/Layout/ScreenLayout.swift +385 -0
  99. package/ios/Layout/Section.swift +4 -100
  100. package/ios/Loader/Loader.swift +5 -9
  101. package/ios/Lottie/LottieView.swift +14 -2
  102. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +169 -29
  103. package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
  104. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
  105. package/ios/Navigation/BottomTab/DuoBarCompression.swift +104 -0
  106. package/ios/Navigation/BottomTab/DuoTabBar.swift +329 -0
  107. package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
  108. package/ios/Navigation/Component/Header.swift +545 -0
  109. package/ios/Navigation/Component/HeaderBackground.swift +154 -0
  110. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  111. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  112. package/ios/Navigation/Component/HeaderRight.swift +425 -0
  113. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  114. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  115. package/ios/Navigation/Component/SideBar.swift +183 -0
  116. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
  117. package/ios/Navigation/HostBackGestureBridge.swift +78 -0
  118. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
  119. package/ios/Navigation/Navigation.swift +204 -0
  120. package/ios/Navigation/NavigationContainer.swift +195 -0
  121. package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
  122. package/ios/Navigation/StackScreen.swift +930 -0
  123. package/ios/Navigation/StackTransition.swift +400 -0
  124. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  125. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  126. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  127. package/ios/Pagination/PaginationScroll.swift +2 -2
  128. package/ios/Popup/PopupDisplay.swift +72 -135
  129. package/ios/Popup/PopupInput.swift +10 -13
  130. package/ios/Popup/PopupLayout.swift +140 -0
  131. package/ios/Popup/PopupNotify.swift +59 -69
  132. package/ios/Popup/PopupPromotion.swift +31 -47
  133. package/ios/PopupView/FullscreenPopup.swift +13 -19
  134. package/ios/PopupView/PopupView.swift +1 -1
  135. package/ios/PopupView/Utils++.swift +0 -8
  136. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  137. package/ios/Radio/Radio.swift +2 -1
  138. package/ios/Rating/Rating.swift +9 -1
  139. package/ios/Resources/dot_loading.json +558 -0
  140. package/ios/Resources/lottie_circle_loader.json +1 -0
  141. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  142. package/ios/Skeleton/Skeleton.swift +22 -27
  143. package/ios/Slider/Slider.swift +16 -18
  144. package/ios/Stepper/Stepper.swift +6 -1
  145. package/ios/Steps/Steps.swift +8 -6
  146. package/ios/SuggestAction/SuggestAction.swift +5 -0
  147. package/ios/Swipeable/SwipeCell.swift +10 -6
  148. package/ios/Switch/Switch.swift +22 -20
  149. package/ios/TabView/TabView.swift +102 -21
  150. package/ios/Tag/Tag.swift +15 -4
  151. package/ios/Template/Logo/Logo.swift +2 -1
  152. package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
  153. package/ios/Title/Title.swift +6 -17
  154. package/ios/Tooltip/Tooltip.swift +29 -14
  155. package/ios/Typography/FontScale.swift +3 -3
  156. package/ios/Typography/Text.swift +81 -19
  157. package/ios/Uploader/Uploader.swift +3 -1
  158. package/ios/native-kits.podspec +2 -1
  159. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  160. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  161. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  162. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
  163. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  164. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  165. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
  166. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
  167. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
  168. package/package.json +1 -1
  169. package/scripts/gen-ios-icons.mjs +36 -0
  170. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  171. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  172. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  173. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  174. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  175. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  176. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  177. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  178. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  179. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  180. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  181. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  182. package/ios/Application/Components.swift +0 -270
  183. package/ios/Application/ComposeApi.swift +0 -22
  184. package/ios/Application/HeaderRight.swift +0 -306
  185. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  186. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  187. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  188. package/ios/Application/Navigation/NavigationContainer.swift +0 -213
  189. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  190. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  191. package/ios/Application/Navigation/component/Header.swift +0 -178
  192. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  193. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  194. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  195. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  196. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  197. package/ios/Application/Screen.swift +0 -250
  198. package/ios/Application/StackScreen.swift +0 -492
  199. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -43,9 +43,11 @@ private fun getValueForPosition(
43
43
  step: Float,
44
44
  ): Float {
45
45
  val availableSpace = containerWidth - thumbWidth
46
- if (availableSpace <= 0f) return min
47
- val relStepUnit = step / (max - min)
46
+ if (availableSpace <= 0f || max <= min) return min
48
47
  var relPosition = (positionInView - thumbWidth / 2f) / availableSpace
48
+ // A zero step would make the snap math NaN, and NaN.roundToInt() throws.
49
+ if (step <= 0f) return clamp(min + relPosition * (max - min), min, max)
50
+ val relStepUnit = step / (max - min)
49
51
  val relOffset = relPosition % relStepUnit
50
52
  relPosition -= relOffset
51
53
  if (relOffset / relStepUnit >= 0.5f) relPosition += relStepUnit
@@ -131,6 +133,11 @@ fun Slider(
131
133
  var draggingLow by remember { mutableStateOf(true) }
132
134
  var isDragging by remember { mutableStateOf(false) }
133
135
 
136
+ // pointerInput is not keyed on the callbacks, so read their latest values.
137
+ val currentOnValueChanged by rememberUpdatedState(onValueChanged)
138
+ val currentOnTouchStart by rememberUpdatedState(onSliderTouchStart)
139
+ val currentOnTouchEnd by rememberUpdatedState(onSliderTouchEnd)
140
+
134
141
  val thumbColor = if (disabled) theme.colors.text.disable else theme.colors.primary
135
142
  val trackActiveColor = if (disabled) theme.colors.text.disable else theme.colors.primary
136
143
  val trackInactiveColor = theme.colors.background.default
@@ -167,7 +174,7 @@ fun Slider(
167
174
 
168
175
  draggingLow = disableRange || isLowCloser(touchX, lowCenterX, highCenterX)
169
176
  isDragging = true
170
- onSliderTouchStart(lowValue, highValue)
177
+ currentOnTouchStart(lowValue, highValue)
171
178
 
172
179
  // Apply initial press
173
180
  applyDrag(touchX, trackWidthPx, thumbSizePx,
@@ -175,7 +182,7 @@ fun Slider(
175
182
  lowValue, highValue) { nl, nh ->
176
183
  lowValue = nl
177
184
  highValue = nh
178
- onValueChanged(nl, nh, true)
185
+ currentOnValueChanged(nl, nh, true)
179
186
  }
180
187
 
181
188
  // Track move / release
@@ -186,7 +193,7 @@ fun Slider(
186
193
 
187
194
  if (!moveChange.pressed) {
188
195
  isDragging = false
189
- onSliderTouchEnd(lowValue, highValue)
196
+ currentOnTouchEnd(lowValue, highValue)
190
197
  break
191
198
  }
192
199
 
@@ -195,7 +202,7 @@ fun Slider(
195
202
  lowValue, highValue) { nl, nh ->
196
203
  lowValue = nl
197
204
  highValue = nh
198
- onValueChanged(nl, nh, true)
205
+ currentOnValueChanged(nl, nh, true)
199
206
  }
200
207
  }
201
208
  }
@@ -8,8 +8,10 @@ import androidx.compose.foundation.gestures.detectHorizontalDragGestures
8
8
  import androidx.compose.foundation.layout.*
9
9
  import androidx.compose.foundation.shape.RoundedCornerShape
10
10
  import androidx.compose.runtime.Composable
11
+ import androidx.compose.runtime.getValue
11
12
  import androidx.compose.runtime.remember
12
13
  import androidx.compose.runtime.rememberCoroutineScope
14
+ import androidx.compose.runtime.rememberUpdatedState
13
15
  import androidx.compose.ui.Alignment
14
16
  import androidx.compose.ui.Modifier
15
17
  import androidx.compose.ui.draw.clip
@@ -62,6 +64,12 @@ fun Swipe(
62
64
 
63
65
  val offsetX = remember { Animatable(0f) }
64
66
 
67
+ // pointerInput is keyed on the action counts only, so read the latest callbacks.
68
+ val currentOnOpen by rememberUpdatedState(onSwipeableOpen)
69
+ val currentOnClose by rememberUpdatedState(onSwipeableClose)
70
+ val currentOnLeftOpen by rememberUpdatedState(onSwipeableLeftOpen)
71
+ val currentOnRightOpen by rememberUpdatedState(onSwipeableRightOpen)
72
+
65
73
  val shape = remember(itemRadius) {
66
74
  if (itemRadius > 0.dp) RoundedCornerShape(itemRadius) else RoundedCornerShape(0.dp)
67
75
  }
@@ -71,17 +79,17 @@ fun Swipe(
71
79
  val current = offsetX.value
72
80
  val target = when {
73
81
  current > leftWidthPx * 0.5f && left.isNotEmpty() -> {
74
- onSwipeableLeftOpen()
75
- onSwipeableOpen()
82
+ currentOnLeftOpen()
83
+ currentOnOpen()
76
84
  leftWidthPx
77
85
  }
78
86
  current < -rightWidthPx * 0.5f && right.isNotEmpty() -> {
79
- onSwipeableRightOpen()
80
- onSwipeableOpen()
87
+ currentOnRightOpen()
88
+ currentOnOpen()
81
89
  -rightWidthPx
82
90
  }
83
91
  else -> {
84
- onSwipeableClose()
92
+ currentOnClose()
85
93
  0f
86
94
  }
87
95
  }
@@ -43,9 +43,10 @@ fun Switch(
43
43
 
44
44
  val (circleBackgroundColor, backgroundColor) = colors
45
45
 
46
- // Memoize automation ID to avoid string concatenation on every render
46
+ // Both params default to "", so blank must fall back too or the id is always empty.
47
47
  val automationIdValue = remember(accessibilityId, title, value) {
48
- accessibilityId ?: "toggle_${title ?: "unknown"}_$value"
48
+ accessibilityId?.takeIf { it.isNotEmpty() }
49
+ ?: "toggle_${title?.takeIf { it.isNotEmpty() } ?: "unknown"}_$value"
49
50
  }
50
51
 
51
52
  // Memoize alignment calculation
@@ -10,9 +10,10 @@ import androidx.compose.foundation.shape.RoundedCornerShape
10
10
  import androidx.compose.runtime.*
11
11
  import androidx.compose.ui.Alignment
12
12
  import androidx.compose.ui.Modifier
13
+ import androidx.compose.ui.draw.clipToBounds
14
+ import androidx.compose.ui.geometry.Offset
13
15
  import androidx.compose.ui.graphics.Color
14
16
  import androidx.compose.ui.graphics.drawscope.withTransform
15
- import androidx.compose.ui.graphics.drawscope.DrawScope
16
17
  import androidx.compose.ui.graphics.Path
17
18
  import androidx.compose.ui.layout.onGloballyPositioned
18
19
  import androidx.compose.ui.layout.positionInParent
@@ -73,6 +74,8 @@ fun TabView(
73
74
  } else null
74
75
 
75
76
  val coroutineScope = rememberCoroutineScope()
77
+ // The collector below outlives recompositions; read the latest callback, not the first one.
78
+ val currentOnPageSelected by rememberUpdatedState(onPageSelected)
76
79
 
77
80
  // Keep selectedIndex in sync when user swipes the pager
78
81
  if (pagerState != null) {
@@ -80,7 +83,7 @@ fun TabView(
80
83
  snapshotFlow { pagerState.currentPage }.collect { page ->
81
84
  if (!lazyRendered.contains(page)) lazyRendered.add(page)
82
85
  selectedIndex = page
83
- onPageSelected(page)
86
+ currentOnPageSelected(page)
84
87
  }
85
88
  }
86
89
  }
@@ -362,7 +365,7 @@ private fun CardTabBar(
362
365
  }
363
366
  }
364
367
 
365
- val active = tabs[selectedIndex]
368
+ val active = tabs.getOrNull(selectedIndex) ?: return@BoxWithConstraints
366
369
  Row(
367
370
  modifier = Modifier
368
371
  // Pinned to the strip's BOTTOM edge so the two always end on the same
@@ -415,7 +418,8 @@ private val CARD_TAB_HEIGHT = 40.dp
415
418
  private val CARD_TAB_ACTIVE_HEIGHT = 44.dp
416
419
  @Composable
417
420
  private fun CardTabEar(width: Dp, mirrored: Boolean, color: Color, nudge: Dp) {
418
- Canvas(modifier = Modifier.offset(x = nudge).width(width).height(CARD_TAB_ACTIVE_HEIGHT)) {
421
+ // Clipped like react-kits' Svg, whose 28-wide path is cut at a 22pt `pathWidth`.
422
+ Canvas(modifier = Modifier.offset(x = nudge).width(width).height(CARD_TAB_ACTIVE_HEIGHT).clipToBounds()) {
419
423
  val path = Path().apply {
420
424
  moveTo(0f, 0f)
421
425
  lineTo(0.60765f, 0f)
@@ -425,8 +429,12 @@ private fun CardTabEar(width: Dp, mirrored: Boolean, color: Color, nudge: Dp) {
425
429
  lineTo(0f, 44f)
426
430
  close()
427
431
  }
428
- val draw: DrawScope.() -> Unit = { drawPath(path = path, color = color) }
429
- if (mirrored) withTransform({ scale(-1f, 1f) }) { draw() } else draw()
432
+ // Path coordinates are dp (react-kits viewBox), but DrawScope draws in px.
433
+ val pxPerDp = density
434
+ withTransform({
435
+ if (mirrored) scale(-1f, 1f)
436
+ scale(pxPerDp, pxPerDp, pivot = Offset.Zero)
437
+ }) { drawPath(path = path, color = color) }
430
438
  }
431
439
  }
432
440
 
@@ -30,6 +30,22 @@ enum class TagColor(val backgroundColor: Color, val textColor: Color) {
30
30
  Grey(Colors.black_04, Colors.black_12),
31
31
  }
32
32
 
33
+ // RN only accepts the `_03` palette as a custom tag color.
34
+ private val tagCustomColors = listOf(
35
+ Colors.black_03,
36
+ Colors.pink_03,
37
+ Colors.violet_03,
38
+ Colors.indigo_03,
39
+ Colors.blue_03,
40
+ Colors.mint_03,
41
+ Colors.green_03,
42
+ Colors.lime_03,
43
+ Colors.yellow_03,
44
+ Colors.gold_03,
45
+ Colors.orange_03,
46
+ Colors.red_03,
47
+ )
48
+
33
49
  @Composable
34
50
  fun Tag(
35
51
  label: String = "Label",
@@ -38,27 +54,9 @@ fun Tag(
38
54
  size: TagSize = TagSize.Large,
39
55
  customColor: Color? = null,
40
56
  ) {
41
- val primaryColors = listOf(
42
- Color(0xFFF0F0F0),
43
- Color(0xFFEB2F96),
44
- Color(0xFF962AF0),
45
- Color(0xFF4E4BFF),
46
- Color(0xFF007AFF),
47
- Color(0xFF13C2C2),
48
- Color(0xFF34C759),
49
- Color(0xFFA0D911),
50
- Color(0xFFFFCC00),
51
- Color(0xFFFA8C16),
52
- Color(0xFFFA541C),
53
- Color(0xFFF5222D)
54
- )
55
- var tagColor = color.backgroundColor
56
- var labelColor = color.textColor
57
-
58
- if (primaryColors.contains(customColor)) {
59
- tagColor = color.backgroundColor
60
- labelColor = color.textColor
61
- }
57
+ val custom = customColor?.takeIf { it in tagCustomColors }
58
+ val tagColor = custom ?: color.backgroundColor
59
+ val labelColor = if (custom != null) Colors.black_01 else color.textColor
62
60
 
63
61
  Box(
64
62
  modifier = Modifier
@@ -87,8 +87,8 @@ fun Text(
87
87
  color ?: theme.colors.text.default
88
88
  }
89
89
 
90
- // Memoize style copy operation
91
- val optimizedStyle = remember(style) {
90
+ // lineHeight is a key: it is scaled, so it changes with the font scale even when style doesn't.
91
+ val optimizedStyle = remember(style, lineHeight) {
92
92
  style.copy(
93
93
  fontWeight = null,
94
94
  lineHeight = if (lineHeight.value.isFinite() && lineHeight.value > 0) lineHeight else style.lineHeight
@@ -21,6 +21,7 @@ import androidx.compose.ui.unit.*
21
21
  import androidx.compose.ui.window.Popup
22
22
  import androidx.compose.ui.window.PopupPositionProvider
23
23
  import androidx.compose.ui.window.PopupProperties
24
+ import kotlinx.coroutines.flow.drop
24
25
  import vn.momo.kits.application.IsShowBaseLineDebug
25
26
  import vn.momo.kits.const.*
26
27
  import vn.momo.kits.modifier.activeOpacityClickable
@@ -147,8 +148,10 @@ fun Tooltip(
147
148
  val density = LocalDensity.current
148
149
  val offsetPx = with(density) { (TOOLTIP_OFFSET + ARROW_SIZE).roundToPx() }
149
150
 
150
- LaunchedEffect(state.isVisible) {
151
- onVisibleChange?.invoke(state.isVisible)
151
+ val currentOnVisibleChange by rememberUpdatedState(onVisibleChange)
152
+ LaunchedEffect(state) {
153
+ // changes only, like RN: the initial value is not reported
154
+ snapshotFlow { state.isVisible }.drop(1).collect { currentOnVisibleChange?.invoke(it) }
152
155
  }
153
156
 
154
157
  Box(modifier = modifier) {
@@ -128,11 +128,13 @@ fun getHours(hourMode: Int): List<String> {
128
128
  * Get a list of minutes
129
129
  */
130
130
  fun getMinutes(interval: Int): List<String> {
131
+ // A non-positive interval would never reach 60.
132
+ val step = interval.coerceAtLeast(1)
131
133
  val minutes = mutableListOf<String>()
132
134
  var min = 0
133
135
  while (min < 60) {
134
136
  minutes.add(paddingNum(min))
135
- min += interval
137
+ min += step
136
138
  }
137
139
  return minutes
138
140
  }
@@ -154,22 +154,14 @@ internal fun WheelPickerItem(
154
154
  .fillMaxWidth()
155
155
  .height(itemHeight)
156
156
  .graphicsLayer {
157
- val firstVisibleIndex = listState.firstVisibleItemIndex
158
- val visibleItemOffset = listState.firstVisibleItemScrollOffset
159
-
160
- val viewportHeight = 5 * itemHeight.value
161
- val itemHeightValue = itemHeight.value
162
-
163
- val absoluteOffset =
164
- (index - firstVisibleIndex) * itemHeightValue - (visibleItemOffset / itemHeightValue)
165
- val relativePosition = absoluteOffset / viewportHeight
166
-
167
- val distance = abs(2 - relativePosition)
168
-
157
+ // Rows away from the centre slot (two below the first visible row); RN fades 1 → 0.8 → 0.4.
158
+ val distance = abs(
159
+ index - listState.firstVisibleItemIndex -
160
+ listState.firstVisibleItemScrollOffset / itemHeight.toPx() - 2
161
+ )
169
162
  alpha = when {
170
- distance <= 1 -> 1f
171
- distance <= 2 -> 0.8f
172
- distance <= 3 -> 0.4f
163
+ distance <= 1f -> 1f - 0.2f * distance
164
+ distance <= 2f -> 0.8f - 0.4f * (distance - 1f)
173
165
  else -> 0.4f
174
166
  }
175
167
  },
@@ -26,7 +26,8 @@ fun scaleSize(size: Float): Float {
26
26
  return size * (context?.userScaleRate ?: KitFontScale.userScaleRate)
27
27
  }
28
28
  val scaleSizeMaxRate: Float = MAX_FONT_SCALE
29
- val deviceWidth = getScreenDimensions().width
29
+ // Short side, not width: landscape must not inflate every font.
30
+ val deviceWidth = getScreenDimensions().let { minOf(it.width, it.height) }
30
31
  val deviceScale = deviceWidth / DEFAULT_SCREEN_SIZE
31
32
 
32
33
  val density = LocalDensity.current
@@ -9,7 +9,6 @@ import androidx.compose.ui.Modifier
9
9
  import androidx.compose.ui.platform.LocalDensity
10
10
  import androidx.compose.ui.unit.Dp
11
11
  import androidx.compose.ui.unit.dp
12
- import vn.momo.kits.platform.getScreenDimensions
13
12
  import kotlin.math.floor
14
13
 
15
14
  internal data class GridContext(
@@ -24,7 +23,7 @@ internal data class GridContext(
24
23
  internal val LocalGridContext = compositionLocalOf<GridContext?> { null }
25
24
 
26
25
  @Composable
27
- internal fun gridColumns(): Int = if (getScreenDimensions().width > 600) 24 else 12
26
+ internal fun gridColumns(): Int = if (getScreenLayout().contentWidth > 600.dp) 24 else 12
28
27
 
29
28
  @Composable
30
29
  internal fun gridContext(maxWidthPx: Int, columns: Int, gap: Dp): GridContext {
@@ -0,0 +1,314 @@
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
+ // Host app's iOS 26 tab bar on the inner display in portrait: a 62pt glass pill sized to its tabs,
90
+ // QR as a 62pt platter beside it, ≥ 20pt from the sides, 13pt into the home indicator inset.
91
+ val floatingTabBarHeight = 62.dp
92
+ val floatingTabBarMargin = 20.dp
93
+ val floatingTabBarInsetOverlap = 13.dp
94
+ }
95
+
96
+ // Widest short side of any single-display iPhone.
97
+ private val IPHONE_MAX_SHORT_SIDE = 440.dp
98
+
99
+ private val IPHONE_HEADER_MIN_HEIGHT = 72.dp
100
+
101
+ // A smaller top inset means the status bar is not on the top edge.
102
+ private val STATUS_BAR_MIN_INSET = 20.dp
103
+
104
+ internal val LocalScreenLayout = compositionLocalOf<ScreenLayout?> { null }
105
+
106
+ // Port of RN `useScreenLayout`; follows fold/unfold and rotation through the window size and safe area.
107
+ @Composable
108
+ fun getScreenLayout(): ScreenLayout =
109
+ LocalScreenLayout.current ?: calculateScreenLayout(getAppStatusBarHeight())
110
+
111
+ @Composable
112
+ internal fun calculateScreenLayout(topInset: Dp, parent: ScreenLayout? = null): ScreenLayout {
113
+ val isPhone = isPhoneIdiom()
114
+ if (parent != null) {
115
+ // the parent's row center reproduces its own centered geometry
116
+ val titleCenterY = parent.headerStatusBarHeight + parent.headerBarHeight / 2
117
+ val statusBarClearance = parent.statusBarClearance?.takeIf { topInset >= STATUS_BAR_MIN_INSET }
118
+ val (headerHeight, headerStatusBarHeight) = if (statusBarClearance != null) {
119
+ statusBarRowGeometry()
120
+ } else {
121
+ headerGeometry(topInset, isPhone, parent.width, parent.height, parent.sideBar != null, titleCenterY)
122
+ }
123
+ return parent.copy(
124
+ headerHeight = headerHeight,
125
+ headerStatusBarHeight = headerStatusBarHeight,
126
+ statusBarClearance = statusBarClearance,
127
+ )
128
+ }
129
+
130
+ val density = LocalDensity.current
131
+ val layoutDirection = LocalLayoutDirection.current
132
+ val size = LocalWindowInfo.current.containerSize
133
+ val insets = horizontalSafeAreaInsets
134
+ val leftPx = insets.getLeft(density, layoutDirection)
135
+ val rightPx = insets.getRight(density, layoutDirection)
136
+ val bottomPx = WindowInsets.systemBars.getBottom(density)
137
+ val screen = getScreenDimensions()
138
+ val reservedRegions = rememberReservedRegions()
139
+
140
+ return remember(size, leftPx, rightPx, bottomPx, topInset, screen, density, reservedRegions) {
141
+ with(density) {
142
+ val width = size.width.toDp()
143
+ val height = size.height.toDp()
144
+ screenLayoutOf(
145
+ width = width,
146
+ height = height,
147
+ insetLeft = leftPx.toDp(),
148
+ insetRight = rightPx.toDp(),
149
+ topInset = topInset,
150
+ bottomInset = bottomPx.toDp(),
151
+ isPhone = isPhone,
152
+ asymmetricInsets = leftPx != rightPx,
153
+ isDuoDevice = isPhone && maxOf(
154
+ minOf(screen.width, screen.height).dp,
155
+ minOf(width, height),
156
+ ) > IPHONE_MAX_SHORT_SIDE,
157
+ reservedRegions = reservedRegions,
158
+ )
159
+ }
160
+ }
161
+ }
162
+
163
+ // Content inside the safe-area padding: the insets are spent and the side bar belongs to the screen.
164
+ internal fun ScreenLayout.consumeHorizontalInsets(): ScreenLayout =
165
+ copy(width = contentWidth, insetLeft = 0.dp, insetRight = 0.dp, sideBar = null)
166
+
167
+ // titleCenterY: window y the title row centers on for the outer display in landscape.
168
+ private fun headerGeometry(
169
+ topInset: Dp,
170
+ isPhone: Boolean,
171
+ width: Dp,
172
+ height: Dp,
173
+ hasSideBar: Boolean,
174
+ titleCenterY: Dp,
175
+ ): Pair<Dp, Dp> {
176
+ val headerHeight = topInset + HEADER_HEIGHT.dp
177
+ if (!isPhone) return headerHeight to topInset
178
+ if (hasSideBar && topInset < STATUS_BAR_MIN_INSET) {
179
+ if (height > width && width < DuoMetrics.innerMinHeight) {
180
+ // outer display: the row starts below the top so the title centers on the camera
181
+ return DuoMetrics.outerHeaderHeight to DuoMetrics.outerCameraCenterY * 2 - DuoMetrics.outerHeaderHeight
182
+ }
183
+ if (width > height && height < DuoMetrics.innerMinHeight) {
184
+ // reaches 8pt below a side bar control centered on the row
185
+ val centered = maxOf(IPHONE_HEADER_MIN_HEIGHT, titleCenterY + DuoMetrics.controlSize / 2 + Spacing.S)
186
+ return centered to maxOf(0.dp, titleCenterY * 2 - centered)
187
+ }
188
+ }
189
+ return maxOf(headerHeight, IPHONE_HEADER_MIN_HEIGHT) to if (topInset < STATUS_BAR_MIN_INSET) 0.dp else topInset
190
+ }
191
+
192
+ // Centered on the clock digits; the cluster's region center sits above them.
193
+ private fun statusBarRowGeometry(): Pair<Dp, Dp> = DuoMetrics.innerStatusBarCenterY * 2 to 0.dp
194
+
195
+ private data class StripOcclusion(val top: Dp, val topCenter: Dp, val bottom: Dp)
196
+
197
+ // How far system regions reach into a side strip from its top and from its bottom end
198
+ // (e.g. the camera, which sits at either end depending on the rotation), and the center y
199
+ // of the region reaching furthest from the top.
200
+ private fun stripOcclusion(regions: List<ReservedRegion>, stripLeft: Dp, stripRight: Dp, windowHeight: Dp): StripOcclusion {
201
+ var top = 0.dp
202
+ var topCenter = 0.dp
203
+ var bottom = 0.dp
204
+ regions.forEach { region ->
205
+ if (region.x >= stripRight || region.x + region.width <= stripLeft) return@forEach
206
+ val centerY = region.y + region.height / 2
207
+ if (centerY < windowHeight / 2) {
208
+ if (region.y + region.height > top) {
209
+ top = region.y + region.height
210
+ topCenter = centerY
211
+ }
212
+ } else {
213
+ bottom = maxOf(bottom, windowHeight - region.y)
214
+ }
215
+ }
216
+ return StripOcclusion(top, topCenter, bottom)
217
+ }
218
+
219
+ // How far the clock and battery clusters of a status bar on the top edge reach in from each side;
220
+ // null without reserved regions there.
221
+ private fun statusBarClusters(regions: List<ReservedRegion>, windowWidth: Dp, topInset: Dp): StatusBarClearance? {
222
+ val midX = windowWidth / 2
223
+ val clusters = regions.filter {
224
+ it.y < topInset && it.y + it.height <= topInset && (it.x + it.width <= midX || it.x >= midX)
225
+ }
226
+ if (clusters.isEmpty()) return null
227
+ var left = 0.dp
228
+ var right = 0.dp
229
+ clusters.forEach {
230
+ if (it.x < midX) left = maxOf(left, it.x + it.width) else right = maxOf(right, windowWidth - it.x)
231
+ }
232
+ return StatusBarClearance(left, right)
233
+ }
234
+
235
+ // Slab iPhones report equal side insets in landscape; iPhone Duo moves the status bar to one side.
236
+ private fun isIPhoneDuoWindow(width: Dp, height: Dp, asymmetricInsets: Boolean): Boolean {
237
+ val shortSide = minOf(width, height)
238
+ if (shortSide <= 0.dp) return asymmetricInsets
239
+ return maxOf(width, height) / shortSide < DuoMetrics.MAX_ASPECT_RATIO || asymmetricInsets
240
+ }
241
+
242
+ private fun screenLayoutOf(
243
+ width: Dp,
244
+ height: Dp,
245
+ insetLeft: Dp,
246
+ insetRight: Dp,
247
+ topInset: Dp,
248
+ bottomInset: Dp,
249
+ isPhone: Boolean,
250
+ asymmetricInsets: Boolean,
251
+ isDuoDevice: Boolean,
252
+ reservedRegions: List<ReservedRegion>,
253
+ ): ScreenLayout {
254
+ val isIPhoneDuo = isPhone && isIPhoneDuoWindow(width, height, asymmetricInsets)
255
+ val isInnerPortrait = height > width && width >= DuoMetrics.innerMinHeight
256
+ val statusBarClearance = if (isIPhoneDuo && isInnerPortrait && topInset >= STATUS_BAR_MIN_INSET) {
257
+ statusBarClusters(reservedRegions, width, topInset)
258
+ ?: StatusBarClearance(left = 0.dp, right = DuoMetrics.innerStatusBarWidth + Spacing.S)
259
+ } else {
260
+ null
261
+ }
262
+
263
+ var sideBar: SideBarLayout? = null
264
+ var titleCenterY = 0.dp
265
+ val sideInset = maxOf(insetLeft, insetRight)
266
+ // narrower strips cannot host a control 12pt from the inner edge
267
+ if (isIPhoneDuo && sideInset >= DuoMetrics.controlSize + Spacing.M) {
268
+ val edge = if (insetRight >= insetLeft) SideBarEdge.RIGHT else SideBarEdge.LEFT
269
+ val stripLeft = if (edge == SideBarEdge.RIGHT) width - sideInset else 0.dp
270
+ // clear of the camera and of the status bar cluster, which grows for calls and Live
271
+ // Activities; region frames already include the system's margin
272
+ val occlusion = stripOcclusion(reservedRegions, stripLeft, stripLeft + sideInset, height)
273
+ // UIKit reports no usable status bar frame here (a 2pt top strip), so the spot is inferred like RN
274
+ val top = when {
275
+ statusBarClearance != null -> statusBarRowGeometry().first + Spacing.M
276
+ topInset >= STATUS_BAR_MIN_INSET -> topInset + HEADER_HEIGHT.dp + Spacing.M
277
+ width <= height -> DuoMetrics.firstItemTop
278
+ height >= DuoMetrics.innerMinHeight -> DuoMetrics.firstItemTopLandscape
279
+ // outer display in landscape: no status bar in the strip
280
+ else -> DuoMetrics.edgeInset
281
+ }
282
+ // outer display in landscape: the title row centers on the camera when it sits at the
283
+ // top end, otherwise on the back button
284
+ titleCenterY = if (occlusion.top > top) occlusion.topCenter else top + DuoMetrics.controlSize / 2
285
+ sideBar = SideBarLayout(
286
+ edge = edge,
287
+ width = sideInset,
288
+ top = maxOf(top, occlusion.top),
289
+ bottom = maxOf(DuoMetrics.edgeInset, occlusion.bottom),
290
+ )
291
+ }
292
+ val (headerHeight, headerStatusBarHeight) = if (statusBarClearance != null) {
293
+ statusBarRowGeometry()
294
+ } else {
295
+ headerGeometry(topInset, isPhone, width, height, sideBar != null, titleCenterY)
296
+ }
297
+
298
+ return ScreenLayout(
299
+ width = width,
300
+ height = height,
301
+ isIPhoneDuo = isIPhoneDuo,
302
+ insetTop = topInset,
303
+ insetBottom = bottomInset,
304
+ insetLeft = insetLeft,
305
+ insetRight = insetRight,
306
+ contentWidth = width - insetLeft - insetRight,
307
+ headerHeight = headerHeight,
308
+ headerStatusBarHeight = headerStatusBarHeight,
309
+ sideBar = sideBar,
310
+ isIPhoneDuoDevice = isDuoDevice,
311
+ duoTopInset = maxOf(topInset, if (height > width) sideInset * 2 else 0.dp),
312
+ statusBarClearance = statusBarClearance,
313
+ )
314
+ }