@momo-kits/native-kits 0.300.3-debug → 0.300.4-debug

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/CLAUDE.md +51 -7
  2. package/README.md +2 -2
  3. package/compose/build.gradle.kts +10 -2
  4. package/compose/build.gradle.kts.backup +8 -0
  5. package/compose/compose.podspec +1 -3
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +4 -3
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
  8. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -14
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +62 -0
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +351 -111
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +55 -22
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -1
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +2 -1
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +20 -15
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +1 -2
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +306 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +6 -2
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +197 -60
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +132 -83
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +54 -62
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +97 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +377 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +122 -24
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +34 -14
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +113 -26
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +2 -2
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HelpCenterRequest.kt +37 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +8 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +20 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +13 -0
  65. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/HelpCenterRequestTest.kt +44 -0
  66. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt +15 -0
  67. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +3 -1
  68. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  69. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +138 -0
  70. package/gradle/libs.versions.toml +3 -14
  71. package/gradle/wrapper/gradle-wrapper.properties +0 -2
  72. package/gradle.properties +3 -5
  73. package/ios/Application/ApplicationEnvironment.swift +24 -6
  74. package/ios/Application/DesignSystemKits.swift +100 -0
  75. package/ios/Application/DesignSystemKitsPayload.swift +68 -0
  76. package/ios/Application/MaxApi.swift +30 -3
  77. package/ios/Avatar/Avatar.swift +4 -3
  78. package/ios/Badge/Badge.swift +6 -4
  79. package/ios/Badge/BadgeDot.swift +1 -1
  80. package/ios/Badge/BadgeRibbon.swift +8 -2
  81. package/ios/Button/Button.swift +15 -12
  82. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  83. package/ios/Carousel/Carousel.swift +29 -12
  84. package/ios/Checkbox/Checkbox.swift +7 -3
  85. package/ios/Chip/Chip.swift +5 -4
  86. package/ios/Collapse/Collapse.swift +70 -19
  87. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  88. package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
  89. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  90. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  91. package/ios/Divider/Divider.swift +37 -4
  92. package/ios/Extensions/Color++.swift +38 -9
  93. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  94. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  95. package/ios/Icon/Icon.swift +21 -23
  96. package/ios/Icon/Icons.swift +1329 -0
  97. package/ios/IconButton/IconButton.swift +15 -7
  98. package/ios/Image/Image.swift +7 -23
  99. package/ios/Information/Information.swift +27 -9
  100. package/ios/Input/ErrorView.swift +2 -2
  101. package/ios/Input/Input.swift +171 -148
  102. package/ios/Input/InputPhoneNumber.swift +10 -9
  103. package/ios/Input/InputSearch.swift +107 -78
  104. package/ios/Input/InputTextArea.swift +133 -148
  105. package/ios/InputDropDown/InputDropDown.swift +10 -7
  106. package/ios/InputMoney/InputMoney.swift +32 -11
  107. package/ios/InputOTP/InputOTP.swift +22 -12
  108. package/ios/Layout/Card.swift +50 -0
  109. package/ios/Layout/GridContext.swift +187 -3
  110. package/ios/Layout/Item.swift +5 -4
  111. package/ios/Layout/ScreenLayout.swift +415 -0
  112. package/ios/Layout/Section.swift +4 -100
  113. package/ios/Loader/Loader.swift +5 -9
  114. package/ios/Lottie/LottieView.swift +14 -2
  115. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +169 -29
  116. package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
  117. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
  118. package/ios/Navigation/BottomTab/DuoBarCompression.swift +104 -0
  119. package/ios/Navigation/BottomTab/DuoTabBar.swift +329 -0
  120. package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
  121. package/ios/Navigation/Component/Header.swift +545 -0
  122. package/ios/Navigation/Component/HeaderBackground.swift +154 -0
  123. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  124. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  125. package/ios/Navigation/Component/HeaderRight.swift +425 -0
  126. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  127. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  128. package/ios/Navigation/Component/SideBar.swift +183 -0
  129. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
  130. package/ios/Navigation/HostBackGestureBridge.swift +78 -0
  131. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
  132. package/ios/Navigation/Navigation.swift +204 -0
  133. package/ios/Navigation/NavigationContainer.swift +195 -0
  134. package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
  135. package/ios/Navigation/StackScreen.swift +930 -0
  136. package/ios/Navigation/StackTransition.swift +400 -0
  137. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  138. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  139. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  140. package/ios/Pagination/PaginationScroll.swift +2 -2
  141. package/ios/Popup/PopupDisplay.swift +72 -135
  142. package/ios/Popup/PopupInput.swift +10 -13
  143. package/ios/Popup/PopupLayout.swift +138 -0
  144. package/ios/Popup/PopupNotify.swift +59 -69
  145. package/ios/Popup/PopupPromotion.swift +31 -47
  146. package/ios/PopupView/FullscreenPopup.swift +13 -19
  147. package/ios/PopupView/PopupView.swift +1 -1
  148. package/ios/PopupView/Utils++.swift +0 -8
  149. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  150. package/ios/Radio/Radio.swift +2 -1
  151. package/ios/Rating/Rating.swift +9 -1
  152. package/ios/Resources/dot_loading.json +558 -0
  153. package/ios/Resources/lottie_circle_loader.json +1 -0
  154. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  155. package/ios/Skeleton/Skeleton.swift +22 -27
  156. package/ios/Slider/Slider.swift +16 -18
  157. package/ios/Stepper/Stepper.swift +7 -3
  158. package/ios/Steps/Steps.swift +8 -6
  159. package/ios/SuggestAction/SuggestAction.swift +5 -0
  160. package/ios/Swipeable/SwipeCell.swift +10 -6
  161. package/ios/Switch/Switch.swift +22 -20
  162. package/ios/TabView/TabView.swift +102 -21
  163. package/ios/Tag/Tag.swift +15 -4
  164. package/ios/Template/Logo/Logo.swift +2 -1
  165. package/ios/Template/TrustBanner/TrustBanner.swift +109 -55
  166. package/ios/Title/Title.swift +6 -17
  167. package/ios/Tooltip/Tooltip.swift +29 -14
  168. package/ios/Typography/FontScale.swift +17 -14
  169. package/ios/Typography/Text.swift +156 -20
  170. package/ios/Typography/Typography.swift +2 -2
  171. package/ios/Uploader/Uploader.swift +3 -1
  172. package/ios/native-kits.podspec +2 -3
  173. package/ios-demo/MoMoUIKitsDemo.podspec +1 -3
  174. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  175. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  176. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
  177. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  178. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  179. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
  180. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
  181. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
  182. package/package.json +1 -1
  183. package/publish.sh +19 -2
  184. package/scripts/gen-ios-icons.mjs +36 -0
  185. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  186. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  187. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  188. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  189. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  190. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  191. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  192. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  193. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  194. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  195. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  196. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  197. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
  198. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
  199. package/ios/Application/Components.swift +0 -270
  200. package/ios/Application/ComposeApi.swift +0 -22
  201. package/ios/Application/HeaderRight.swift +0 -306
  202. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  203. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  204. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  205. package/ios/Application/Navigation/NavigationContainer.swift +0 -213
  206. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  207. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  208. package/ios/Application/Navigation/component/Header.swift +0 -178
  209. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  210. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  211. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  212. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  213. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  214. package/ios/Application/Screen.swift +0 -250
  215. package/ios/Application/StackScreen.swift +0 -492
  216. package/ios/Typography/FontScaleStore.swift +0 -58
  217. package/ios/Typography/KitDesignSystem.swift +0 -17
  218. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -3,23 +3,27 @@ package vn.momo.kits.navigation.component
3
3
  import androidx.compose.animation.core.animateFloatAsState
4
4
  import androidx.compose.foundation.background
5
5
  import androidx.compose.foundation.layout.Box
6
+ import androidx.compose.foundation.layout.fillMaxHeight
6
7
  import androidx.compose.foundation.layout.fillMaxSize
7
8
  import androidx.compose.foundation.layout.fillMaxWidth
8
9
  import androidx.compose.foundation.layout.height
9
10
  import androidx.compose.runtime.Composable
10
11
  import androidx.compose.runtime.getValue
11
12
  import androidx.compose.ui.Modifier
12
- import androidx.compose.ui.geometry.Offset
13
+ import androidx.compose.ui.draw.clipToBounds
14
+ import androidx.compose.ui.graphics.graphicsLayer
13
15
  import androidx.compose.ui.graphics.Brush
14
16
  import androidx.compose.ui.graphics.Color
15
17
  import androidx.compose.ui.platform.LocalDensity
16
18
  import androidx.compose.ui.unit.dp
17
19
  import vn.momo.kits.components.Image
18
- import vn.momo.kits.const.AppStatusBar
19
20
  import vn.momo.kits.const.AppTheme
21
+ import vn.momo.kits.layout.getScreenLayout
20
22
  import vn.momo.kits.modifier.conditional
21
23
  import vn.momo.kits.navigation.LocalOptions
22
24
  import vn.momo.kits.navigation.LocalScrollState
25
+ import vn.momo.kits.navigation.getInputSearchType
26
+ import vn.momo.kits.navigation.usesDuoGlassHeader
23
27
 
24
28
  enum class AnimatedHeaderRatio(val value: Float){
25
29
  RATIO_16_9(16f / 9f),
@@ -29,6 +33,9 @@ enum class AnimatedHeaderRatio(val value: Float){
29
33
 
30
34
  internal val HEADER_BACKGROUND_HEIGHT = 154.dp
31
35
 
36
+ // RN DuoScrollGradient: extended gradient runs this far below the header
37
+ private val DUO_GRADIENT_EXTRA = 52.dp
38
+
32
39
  @Suppress("FrequentlyChangingValue")
33
40
  @Composable
34
41
  internal fun HeaderBackground() {
@@ -38,7 +45,8 @@ internal fun HeaderBackground() {
38
45
  val scrollState = LocalScrollState.current
39
46
  val backgroundColor = options.backgroundColor ?: AppTheme.current.colors.background.default
40
47
 
41
- val minHeight = AppStatusBar.current + HEADER_HEIGHT.dp
48
+ val layout = getScreenLayout()
49
+ val minHeight = layout.headerHeight
42
50
  val maxHeight = HEADER_BACKGROUND_HEIGHT
43
51
  val opacity by animateFloatAsState(
44
52
  targetValue = (1 - (scrollState.value * 1f / HEADER_HEIGHT * 1f)).coerceIn(0f, 1f),
@@ -57,20 +65,32 @@ internal fun HeaderBackground() {
57
65
  else -> 0.dp
58
66
  }
59
67
 
60
- Box(Modifier.fillMaxWidth().height(height).background(backgroundColor)) {
68
+ // iPhone Duo glass header: the gradient leaves with the content, as the quick return header does
69
+ val useDuoGlass = options.usesDuoGlassHeader(layout)
70
+ // RN HeaderExtendHeader with search on the side bar layout: the gradient fills the header box,
71
+ // which for extended shrinks from 52pt below the header over the first 100pt of scroll
72
+ val duoSearch = layout.sideBar != null && getInputSearchType(options) != InputSearchType.None
73
+ val fillsBox = useDuoGlass || duoSearch
74
+ val duoModifier = when {
75
+ useDuoGlass -> {
76
+ val extra = if (options.headerType is HeaderType.Extended) DUO_GRADIENT_EXTRA else 0.dp
77
+ Modifier.height(minHeight + extra).graphicsLayer { translationY = -scrollState.value.toFloat() }
78
+ }
79
+ duoSearch && options.headerType is HeaderType.Extended -> {
80
+ val progress = (with(density) { scrollState.value.toDp() } / 100.dp).coerceIn(0f, 1f)
81
+ Modifier.height(minHeight + DUO_GRADIENT_EXTRA * (1 - progress))
82
+ }
83
+ duoSearch -> Modifier.height(minHeight)
84
+ else -> Modifier.height(height)
85
+ }
86
+
87
+ Box(Modifier.fillMaxWidth().then(duoModifier).clipToBounds().background(backgroundColor)) {
88
+ // on Duo the fade spans the gradient box; a banner image keeps its 375:154 shape, clipped
61
89
  Box(Modifier
62
- .height(maxHeight)
63
90
  .fillMaxWidth()
91
+ .then(if (fillsBox && !theme.isHeaderImage()) Modifier.fillMaxHeight() else Modifier.height(maxHeight))
64
92
  .conditional(!theme.isHeaderImage()) {
65
- background(Brush.linearGradient(
66
- colors = listOf(
67
- color,
68
- backgroundColor
69
- ),
70
- start = Offset(0f, 0f),
71
- end = Offset(0f, with(density) { maxHeight.toPx() })
72
- )
73
- )
93
+ background(Brush.verticalGradient(colors = listOf(color, backgroundColor)))
74
94
  }
75
95
  ){
76
96
  if (!theme.isHeaderImage()) return@Box
@@ -23,7 +23,6 @@ import vn.momo.kits.components.Text
23
23
  import vn.momo.kits.const.Spacing
24
24
  import vn.momo.kits.const.Typography
25
25
  import vn.momo.kits.modifier.activeOpacityClickable
26
- import vn.momo.kits.platform.getScreenDimensions
27
26
 
28
27
  @Composable
29
28
  internal fun HeaderJourney(
@@ -33,7 +32,7 @@ internal fun HeaderJourney(
33
32
  if (data !is HeaderTitle.Journey) return
34
33
  if (data.isLoading) return TitleJourneyShimmer()
35
34
 
36
- val maxWidth = getScreenDimensions().width.dp - 172.dp
35
+ val maxWidth = headerTitleMaxWidth()
37
36
  val tint = parseHeaderTint(data.tintColor) ?: tintIconColor
38
37
  val iconTint = parseHeaderTint(data.iconColor) ?: tint
39
38
 
@@ -98,7 +97,8 @@ internal fun parseHeaderTint(value: String?): Color? {
98
97
  val hex = when (raw.length) {
99
98
  3 -> "FF" + raw.map { "$it$it" }.joinToString("")
100
99
  6 -> "FF$raw"
101
- 8 -> raw
100
+ // CSS #RRGGBBAA, as RN passes it; Compose wants AARRGGBB
101
+ 8 -> raw.substring(6) + raw.substring(0, 6)
102
102
  else -> return null
103
103
  }
104
104
  val v = hex.toLongOrNull(16) ?: return null
@@ -22,7 +22,6 @@ import vn.momo.kits.components.Text
22
22
  import vn.momo.kits.const.Spacing
23
23
  import vn.momo.kits.const.Typography
24
24
  import vn.momo.kits.modifier.activeOpacityClickable
25
- import vn.momo.kits.platform.getScreenDimensions
26
25
 
27
26
  @Composable
28
27
  internal fun HeaderLocation(
@@ -32,7 +31,7 @@ internal fun HeaderLocation(
32
31
  if (data !is HeaderTitle.Location) return
33
32
  if (data.isLoading) return TitleLocationShimmer()
34
33
 
35
- val maxWidth = getScreenDimensions().width.dp - 172.dp
34
+ val maxWidth = headerTitleMaxWidth()
36
35
  val tint = parseHeaderTint(data.tintColor) ?: tintIconColor
37
36
 
38
37
  Column(
@@ -6,6 +6,7 @@ import androidx.compose.foundation.background
6
6
  import androidx.compose.foundation.border
7
7
  import androidx.compose.foundation.layout.Arrangement
8
8
  import androidx.compose.foundation.layout.Box
9
+ import androidx.compose.foundation.layout.Column
9
10
  import androidx.compose.foundation.layout.Row
10
11
  import androidx.compose.foundation.layout.height
11
12
  import androidx.compose.foundation.layout.offset
@@ -36,7 +37,9 @@ import androidx.compose.ui.Alignment
36
37
  import androidx.compose.ui.Modifier
37
38
  import androidx.compose.ui.draw.clip
38
39
  import androidx.compose.ui.graphics.Color
40
+ import androidx.compose.ui.unit.Dp
39
41
  import androidx.compose.ui.unit.dp
42
+ import vn.momo.kits.layout.DuoMetrics
40
43
  import vn.momo.kits.application.LocalContext
41
44
  import vn.momo.kits.application.MiniAppContext
42
45
  import vn.momo.kits.components.BadgeDot
@@ -70,11 +73,32 @@ fun HeaderRight(
70
73
  }
71
74
  }
72
75
 
76
+ private const val HELP_CENTER_APP_ID = "vn.momo.helpcenter"
77
+
78
+ private fun usesHelpCenter(context: MiniAppContext?): Boolean =
79
+ context != null && context.appId != HELP_CENTER_APP_ID
80
+
81
+ private fun showsShortcut(headerRight: HeaderRight.Toolkit, context: MiniAppContext?): Boolean =
82
+ headerRight.useShortcut && !(headerRight.useMore && context == null)
83
+
84
+ // Height of the vertical toolkit: the optional shortcut, then the help/home capsule.
85
+ @Composable
86
+ internal fun toolkitColumnHeight(headerRight: HeaderRight.Toolkit): Dp {
87
+ val context = LocalContext.current
88
+ val shortcut = if (showsShortcut(headerRight, context)) DuoMetrics.controlSize + Spacing.M else 0.dp
89
+ val groupCount = if (usesHelpCenter(context)) 2 else 1
90
+ return shortcut + if (groupCount > 1) DuoMetrics.itemPitch * groupCount else DuoMetrics.controlSize
91
+ }
92
+
93
+ // SideBarColumn: the iPhone Duo side bar; SideBarRow: the Duo header row on the status bar, same 48pt controls.
94
+ internal enum class ToolkitLayout { Bar, SideBarColumn, SideBarRow }
95
+
73
96
  @Composable
74
97
  internal fun Toolkit(
75
98
  headerRight: HeaderRight.Toolkit,
76
99
  tintColor: Color? = null,
77
- headerColor: HeaderColor
100
+ headerColor: HeaderColor,
101
+ layout: ToolkitLayout = ToolkitLayout.Bar,
78
102
  ) {
79
103
  val api = LocalMaxApi.current
80
104
  val context = LocalContext.current
@@ -120,6 +144,23 @@ internal fun Toolkit(
120
144
  }
121
145
  // AI-GENERATED END: retain custom tool data and typed callback outcomes
122
146
 
147
+ fun onPressHelpCenter() {
148
+ if (context == null) return
149
+ // AI-GENERATED START: preserve Help Center context and own its event subscription
150
+ val client = api ?: return
151
+ requests.observe({ callback -> client.showBottomSheet(context.helpCenterRequest(), callback) }) {}
152
+ // AI-GENERATED END: preserve Help Center context and own its event subscription
153
+ }
154
+
155
+ fun onPressClose() {
156
+ // AI-GENERATED START: preserve current-container and whole-stack dismissal
157
+ api?.dismiss(if (headerRight.useCloseIcon) DismissTarget.Current() else DismissTarget.All) {}
158
+ // AI-GENERATED END: preserve current-container and whole-stack dismissal
159
+ }
160
+
161
+ val useHelpCenter = usesHelpCenter(context)
162
+ val closeIcon = if (headerRight.useCloseIcon) "16_navigation_close_circle" else "16_basic_home"
163
+
123
164
  val navButtonConfig = getNavigationButtonConfig(
124
165
  headerRight,
125
166
  ::onPressShortcut,
@@ -130,12 +171,7 @@ internal fun Toolkit(
130
171
  group.items.any { it.showBadge }
131
172
  }
132
173
 
133
- val isShowShortcut by remember(headerRight.useShortcut, headerRight.useMore, context) {
134
- derivedStateOf {
135
- (headerRight.useShortcut == true) &&
136
- !(headerRight.useMore == true && context == null)
137
- }
138
- }
174
+ val isShowShortcut = showsShortcut(headerRight, context)
139
175
 
140
176
  val icon by remember(navButtonConfig.icon, isFavorite) {
141
177
  derivedStateOf {
@@ -144,6 +180,47 @@ internal fun Toolkit(
144
180
  }
145
181
  }
146
182
 
183
+ if (layout != ToolkitLayout.Bar) {
184
+ val horizontal = layout == ToolkitLayout.SideBarRow
185
+ val controls = @Composable {
186
+ if (isShowShortcut) {
187
+ SideBarButton(
188
+ icon = icon,
189
+ headerColor = headerColor,
190
+ enabled = !isLoading,
191
+ showBadge = showBadge,
192
+ onClick = navButtonConfig.onPress,
193
+ )
194
+ }
195
+ SideBarGroup(headerColor = headerColor, itemCount = if (useHelpCenter) 2 else 1, horizontal = horizontal) { item ->
196
+ if (useHelpCenter) {
197
+ SideBarGroupItem(
198
+ icon = "help_center",
199
+ headerColor = headerColor,
200
+ automationId = "btn_navigation_help_center",
201
+ onClick = ::onPressHelpCenter,
202
+ modifier = item,
203
+ )
204
+ }
205
+ SideBarGroupItem(
206
+ icon = closeIcon,
207
+ headerColor = headerColor,
208
+ automationId = "btn_navigation_close",
209
+ onClick = ::onPressClose,
210
+ modifier = item,
211
+ )
212
+ }
213
+ }
214
+ if (horizontal) {
215
+ Row(horizontalArrangement = Arrangement.spacedBy(Spacing.M), verticalAlignment = Alignment.CenterVertically) {
216
+ controls()
217
+ }
218
+ } else {
219
+ Column(verticalArrangement = Arrangement.spacedBy(Spacing.M)) { controls() }
220
+ }
221
+ return
222
+ }
223
+
147
224
  Row(
148
225
  verticalAlignment = Alignment.CenterVertically
149
226
  ) {
@@ -167,20 +244,12 @@ internal fun Toolkit(
167
244
  .clip(shape = RoundedCornerShape(14.dp))
168
245
  .background(headerColor.backgroundButton)
169
246
  ) {
170
- if (context != null) {
247
+ if (useHelpCenter) {
171
248
  Icon(
172
249
  source = "help_center",
173
250
  size = 20.dp,
174
251
  color = headerColor.tintIconColor,
175
- modifier = Modifier.padding(4.dp).activeOpacityClickable {
176
- // AI-GENERATED START: open support through the typed action
177
- api?.let { client ->
178
- requests.launch({ callback ->
179
- client.performToolAction(ToolAction.SUPPORT, context.appCode, callback)
180
- }) {}
181
- }
182
- // AI-GENERATED END: open support through the typed action
183
- }
252
+ modifier = Modifier.padding(4.dp).activeOpacityClickable(onClick = ::onPressHelpCenter)
184
253
  )
185
254
  Box(
186
255
  modifier = Modifier
@@ -190,14 +259,10 @@ internal fun Toolkit(
190
259
  )
191
260
  }
192
261
  Icon(
193
- source = "16_basic_home",
262
+ source = closeIcon,
194
263
  size = 20.dp,
195
264
  color = headerColor.tintIconColor,
196
- modifier = Modifier.padding(4.dp).activeOpacityClickable {
197
- // AI-GENERATED START: preserve dismiss-all without selecting Home
198
- api?.dismiss(DismissTarget.All) {}
199
- // AI-GENERATED END: preserve dismiss-all without selecting Home
200
- }
265
+ modifier = Modifier.padding(4.dp).activeOpacityClickable(onClick = ::onPressClose)
201
266
  )
202
267
  }
203
268
  }
@@ -205,16 +270,36 @@ internal fun Toolkit(
205
270
 
206
271
 
207
272
 
273
+ // Colors follow the enclosing header, so it fits a `HeaderRight.Custom` next to the toolkit.
274
+ @Composable
275
+ fun NavigationButton(
276
+ icon: String,
277
+ onClick: () -> Unit,
278
+ showBadge: Boolean = false,
279
+ disabled: Boolean = false,
280
+ modifier: Modifier = Modifier,
281
+ ) {
282
+ NavigationButton(
283
+ disabled = disabled,
284
+ icon = icon,
285
+ showBadge = showBadge,
286
+ onClick = onClick,
287
+ headerColor = LocalHeaderColor.current ?: getHeaderColor(HeaderType.Default(), 1f, null, Colors.black_17),
288
+ modifier = modifier,
289
+ )
290
+ }
291
+
208
292
  @Composable
209
293
  internal fun NavigationButton(
210
294
  disabled: Boolean,
211
295
  icon: String,
212
296
  showBadge: Boolean? = false,
213
297
  onClick: () -> Unit,
214
- headerColor: HeaderColor
298
+ headerColor: HeaderColor,
299
+ modifier: Modifier = Modifier,
215
300
  ) {
216
301
  Box(
217
- modifier = Modifier
302
+ modifier = modifier
218
303
  .size(28.dp)
219
304
  .activeOpacityClickable(enabled = !disabled, onClick = onClick)
220
305
  ) {
@@ -287,7 +372,9 @@ sealed interface HeaderRight {
287
372
  val useMore: Boolean = false,
288
373
  val useSystemTools: Boolean = true,
289
374
  val tools: List<ToolGroup> = emptyList(),
290
- val toolCallback: ((String) -> Unit)? = { _ -> }
375
+ val toolCallback: ((String) -> Unit)? = { _ -> },
376
+ // Close dismisses only this mini app instead of returning home.
377
+ val useCloseIcon: Boolean = false,
291
378
  ) : HeaderRight
292
379
  }
293
380
 
@@ -8,13 +8,13 @@ import androidx.compose.ui.text.style.TextAlign
8
8
  import androidx.compose.ui.text.style.TextOverflow
9
9
  import androidx.compose.ui.unit.sp
10
10
  import androidx.compose.ui.zIndex
11
+ import vn.momo.kits.application.DesignSystemKits
11
12
  import vn.momo.kits.components.Text
12
- import vn.momo.kits.const.KitDesignSystem
13
13
  import vn.momo.kits.const.Typography
14
14
  import vn.momo.kits.modifier.setAutomationId
15
15
 
16
16
  internal val headerFontFamily: String?
17
- get() = if (KitDesignSystem.isWhiteList) "MoMoTrustDisplay" else null
17
+ get() = if (DesignSystemKits.getConfigs().isWhiteList) "MoMoTrustDisplay" else null
18
18
 
19
19
  @Composable
20
20
  internal fun HeaderTitle(
@@ -32,7 +32,6 @@ import vn.momo.kits.const.Typography
32
32
  import vn.momo.kits.const.getFontFamily
33
33
  import vn.momo.kits.modifier.activeOpacityClickable
34
34
  import vn.momo.kits.navigation.component.HeaderTitle.User
35
- import vn.momo.kits.platform.getScreenDimensions
36
35
 
37
36
  @Composable
38
37
  internal fun HeaderUser(
@@ -44,7 +43,7 @@ internal fun HeaderUser(
44
43
  return TitleUserShimmer()
45
44
  }
46
45
 
47
- val maxWidth = getScreenDimensions().width.dp - 172.dp
46
+ val maxWidth = headerTitleMaxWidth()
48
47
 
49
48
  Row(
50
49
  modifier = Modifier
@@ -0,0 +1,37 @@
1
+ // File: HelpCenterRequest.kt
2
+ // Created At: 2026-10-19 +07:00
3
+ // Created By: AI | AI Agent: Codex | Model: GPT-6
4
+ // AI-GENERATED START: preserve the toolkit Help Center payload in the typed request
5
+ package vn.momo.kits.navigation.component
6
+
7
+ import kotlinx.serialization.json.JsonArray
8
+ import kotlinx.serialization.json.JsonElement
9
+ import kotlinx.serialization.json.JsonNull
10
+ import kotlinx.serialization.json.JsonObject
11
+ import kotlinx.serialization.json.JsonPrimitive
12
+ import vn.momo.kits.application.MiniAppContext
13
+ import vn.momo.maxapi.contract.navigation.model.BottomSheetRequest
14
+ import vn.momo.maxapi.contract.navigation.model.HelpCenterContext
15
+
16
+ internal fun MiniAppContext.helpCenterRequest() = BottomSheetRequest.HelpCenter(
17
+ HelpCenterContext(
18
+ appId = appId,
19
+ code = appCode,
20
+ name = appName?.helpCenterJson(),
21
+ icon = appIcon,
22
+ description = description?.helpCenterJson(),
23
+ ),
24
+ )
25
+
26
+ private fun Any?.helpCenterJson(): JsonElement = when (this) {
27
+ null -> JsonNull
28
+ is JsonElement -> this
29
+ is String -> JsonPrimitive(this)
30
+ is Boolean -> JsonPrimitive(this)
31
+ is Number -> JsonPrimitive(this)
32
+ is Map<*, *> -> JsonObject(entries.associate { (key, value) -> key.toString() to value.helpCenterJson() })
33
+ is Iterable<*> -> JsonArray(map { it.helpCenterJson() })
34
+ is Array<*> -> JsonArray(map { it.helpCenterJson() })
35
+ else -> JsonPrimitive(toString())
36
+ }
37
+ // AI-GENERATED END: preserve the toolkit Help Center payload in the typed request
@@ -0,0 +1,192 @@
1
+ package vn.momo.kits.navigation.component
2
+
3
+ import androidx.compose.foundation.background
4
+ import androidx.compose.foundation.border
5
+ import androidx.compose.foundation.layout.Arrangement
6
+ import androidx.compose.foundation.layout.Box
7
+ import androidx.compose.foundation.layout.BoxScope
8
+ import androidx.compose.foundation.layout.Column
9
+ import androidx.compose.foundation.layout.Row
10
+ import androidx.compose.foundation.layout.Spacer
11
+ import androidx.compose.foundation.layout.absolutePadding
12
+ import androidx.compose.foundation.layout.offset
13
+ import androidx.compose.foundation.layout.size
14
+ import androidx.compose.foundation.layout.width
15
+ import androidx.compose.foundation.shape.CircleShape
16
+ import androidx.compose.foundation.shape.RoundedCornerShape
17
+ import androidx.compose.runtime.Composable
18
+ import androidx.compose.runtime.LaunchedEffect
19
+ import androidx.compose.ui.AbsoluteAlignment
20
+ import androidx.compose.ui.Alignment
21
+ import androidx.compose.ui.Modifier
22
+ import androidx.compose.ui.draw.clip
23
+ import androidx.compose.ui.draw.shadow
24
+ import androidx.compose.ui.graphics.Shape
25
+ import androidx.compose.ui.layout.layout
26
+ import androidx.compose.ui.unit.Dp
27
+ import androidx.compose.ui.unit.constrainHeight
28
+ import androidx.compose.ui.unit.dp
29
+ import vn.momo.kits.components.BadgeDot
30
+ import vn.momo.kits.components.DotSize
31
+ import vn.momo.kits.components.Icon
32
+ import vn.momo.kits.const.Colors
33
+ import vn.momo.kits.const.Radius
34
+ import vn.momo.kits.const.Spacing
35
+ import vn.momo.kits.layout.DuoMetrics
36
+ import vn.momo.kits.layout.SideBarEdge
37
+ import vn.momo.kits.layout.getScreenLayout
38
+ import vn.momo.kits.modifier.activeOpacityClickable
39
+ import vn.momo.kits.modifier.conditional
40
+ import vn.momo.kits.modifier.setAutomationId
41
+ import vn.momo.kits.navigation.LocalNavigator
42
+ import vn.momo.kits.navigation.LocalOptions
43
+ import vn.momo.kits.navigation.bottomtab.LocalDuoBarCompression
44
+
45
+ private val SIDE_BAR_ICON_SIZE = 24.dp
46
+
47
+ // iPhone Duo: back and toolkit leave the bar for a column in the side safe area, as the system bars do.
48
+ // bottomInset is what footer / tab bar / home indicator take from the bottom, gap included; the column stays above it.
49
+ @Composable
50
+ internal fun BoxScope.HeaderSideBar(bottomInset: Dp, modifier: Modifier = Modifier) {
51
+ val options = LocalOptions.current
52
+ val navigator = LocalNavigator.current
53
+ val screenLayout = getScreenLayout()
54
+ val sideBar = screenLayout.sideBar ?: return
55
+ val toolkit = options.headerRight as? HeaderRight.Toolkit
56
+ val empty = options.headerType == HeaderType.None || (options.hiddenBack && toolkit == null)
57
+ val compression = LocalDuoBarCompression.current
58
+ val fullHeight = if (empty) 0.dp else DuoMetrics.controlSize + (toolkit?.let { Spacing.M + toolkitColumnHeight(it) } ?: 0.dp)
59
+ LaunchedEffect(fullHeight, compression.setToolbarHeight) { compression.setToolbarHeight(fullHeight) }
60
+ if (empty) return
61
+
62
+ val headerColor = getHeaderColor(options.headerType, headerOpacity(), options.tintColor, Colors.black_17)
63
+ val isRight = sideBar.edge == SideBarEdge.RIGHT
64
+
65
+ Column(
66
+ modifier = modifier
67
+ .align(if (isRight) AbsoluteAlignment.TopRight else AbsoluteAlignment.TopLeft)
68
+ .width(sideBar.width)
69
+ .layout { measurable, constraints ->
70
+ val placeable = measurable.measure(constraints.copy(minHeight = 0))
71
+ val maxY = screenLayout.height.roundToPx() - bottomInset.roundToPx() - placeable.height
72
+ val y = minOf(sideBar.top.roundToPx(), maxY).coerceAtLeast(0)
73
+ layout(placeable.width, constraints.constrainHeight(y + placeable.height)) {
74
+ placeable.place(0, y)
75
+ }
76
+ }
77
+ .absolutePadding(left = if (isRight) Spacing.M else 0.dp, right = if (isRight) 0.dp else Spacing.M),
78
+ horizontalAlignment = if (isRight) AbsoluteAlignment.Left else AbsoluteAlignment.Right,
79
+ verticalArrangement = Arrangement.spacedBy(Spacing.M),
80
+ ) {
81
+ if (options.hiddenBack) {
82
+ // keep the slot so the toolkit does not jump between root and pushed screens
83
+ Spacer(Modifier.size(DuoMetrics.controlSize))
84
+ } else {
85
+ SideBarButton(
86
+ icon = "arrow-back",
87
+ headerColor = headerColor,
88
+ automationId = "btn_navigation_back",
89
+ onClick = { navigator.onBackSafe { } },
90
+ )
91
+ }
92
+ when {
93
+ toolkit == null || compression.toolbarHidden -> Unit
94
+ // HIG "Toolbar compressed": tools fold into one button, distinct from the useMore icon
95
+ compression.toolbarCompressed -> SideBarButton(
96
+ icon = "navigation_menu",
97
+ headerColor = headerColor,
98
+ showBadge = toolkit.tools.any { group -> group.items.any { it.showBadge } },
99
+ automationId = "btn_navigation_toolbar",
100
+ onClick = compression.expandToolbar,
101
+ )
102
+ else -> Toolkit(
103
+ headerRight = toolkit,
104
+ tintColor = options.tintColor,
105
+ headerColor = headerColor,
106
+ layout = ToolkitLayout.SideBarColumn,
107
+ )
108
+ }
109
+ }
110
+ }
111
+
112
+ @Composable
113
+ internal fun SideBarButton(
114
+ icon: String,
115
+ headerColor: HeaderColor,
116
+ onClick: () -> Unit,
117
+ enabled: Boolean = true,
118
+ showBadge: Boolean = false,
119
+ automationId: String? = null,
120
+ ) {
121
+ Box {
122
+ Box(
123
+ modifier = Modifier
124
+ .size(DuoMetrics.controlSize)
125
+ .sideBarGlass(headerColor, CircleShape)
126
+ .activeOpacityClickable(enabled = enabled, onClick = onClick)
127
+ .conditional(automationId != null) { setAutomationId(automationId.orEmpty()) },
128
+ contentAlignment = Alignment.Center,
129
+ ) {
130
+ Icon(source = icon, size = SIDE_BAR_ICON_SIZE, color = headerColor.tintIconColor)
131
+ }
132
+ if (showBadge) {
133
+ BadgeDot(
134
+ size = DotSize.Small,
135
+ modifier = Modifier.align(Alignment.TopEnd).offset(x = -Spacing.XXS, y = Spacing.XXS),
136
+ )
137
+ }
138
+ }
139
+ }
140
+
141
+ // Capsule grouping related controls, one item per 50pt: vertical in the side bar, horizontal on a header row.
142
+ @Composable
143
+ internal fun SideBarGroup(
144
+ headerColor: HeaderColor,
145
+ itemCount: Int,
146
+ horizontal: Boolean = false,
147
+ content: @Composable (itemModifier: Modifier) -> Unit,
148
+ ) {
149
+ val length = if (itemCount > 1) DuoMetrics.itemPitch * itemCount else DuoMetrics.controlSize
150
+ val item = length / itemCount.coerceAtLeast(1)
151
+ val glass = Modifier.sideBarGlass(headerColor, RoundedCornerShape(Radius.XL))
152
+ if (horizontal) {
153
+ Row(Modifier.size(width = length, height = DuoMetrics.controlSize).then(glass)) {
154
+ content(Modifier.size(width = item, height = DuoMetrics.controlSize))
155
+ }
156
+ } else {
157
+ Column(Modifier.size(width = DuoMetrics.controlSize, height = length).then(glass)) {
158
+ content(Modifier.size(width = DuoMetrics.controlSize, height = item))
159
+ }
160
+ }
161
+ }
162
+
163
+ @Composable
164
+ internal fun SideBarGroupItem(
165
+ icon: String,
166
+ headerColor: HeaderColor,
167
+ automationId: String,
168
+ onClick: () -> Unit,
169
+ modifier: Modifier = Modifier,
170
+ ) {
171
+ Box(
172
+ modifier = modifier
173
+ .activeOpacityClickable(onClick = onClick)
174
+ .setAutomationId(automationId),
175
+ contentAlignment = Alignment.Center,
176
+ ) {
177
+ Icon(source = icon, size = SIDE_BAR_ICON_SIZE, color = headerColor.tintIconColor)
178
+ }
179
+ }
180
+
181
+ // Dark glass under light icons (banner headers), light glass otherwise. The light one is opaque:
182
+ // Compose has no backdrop blur and a translucent fill shows its own shadow through (RN uses 85% white).
183
+ private fun Modifier.sideBarGlass(headerColor: HeaderColor, shape: Shape): Modifier =
184
+ if (headerColor.tintIconColor == Colors.black_01) {
185
+ clip(shape)
186
+ .background(Colors.black_20.copy(alpha = 0.5f))
187
+ .border(0.5.dp, Colors.black_01.copy(alpha = 0.2f), shape)
188
+ } else {
189
+ shadow(3.dp, shape)
190
+ .background(Colors.black_01)
191
+ .border(0.5.dp, Colors.black_20.copy(alpha = 0.08f), shape)
192
+ }
@@ -83,7 +83,8 @@ internal fun SnackBar(
83
83
  durationMillis = 200,
84
84
  )
85
85
  )
86
- navigator.hideSnackBar()
86
+ // hideSnackBar() would call clear() -> this close again while still bound
87
+ OverplayComponentRegistry.hardClearAfterDismiss()
87
88
  }
88
89
  }
89
90
 
@@ -23,6 +23,14 @@ internal class ToolkitRequests {
23
23
  if (!active) created.cancel() else if (!completed) handles.add(created)
24
24
  }
25
25
 
26
+ // AI-GENERATED START: retain subscriptions across nonterminal sheet events
27
+ fun <T> observe(request: (ApiCallback<T>) -> ApiRequestHandle, onResult: ApiCallback<T>) {
28
+ if (!active) return
29
+ val handle = request { result -> if (active) onResult(result) }
30
+ if (active) handles.add(handle) else handle.cancel()
31
+ }
32
+ // AI-GENERATED END: retain subscriptions across nonterminal sheet events
33
+
26
34
  fun close() {
27
35
  active = false
28
36
  val pending = handles.toList()
@@ -14,6 +14,9 @@ data class ScreenDimension(
14
14
 
15
15
  expect fun getPlatformName(): String
16
16
 
17
+ // iPhone idiom only; iPad windows and Android never get the iPhone / iPhone Duo layout.
18
+ expect fun isPhoneIdiom(): Boolean
19
+
17
20
  @Composable
18
21
  expect fun getScreenDimensions(): ScreenDimension
19
22