@momo-kits/native-kits 0.162.49 → 0.162.50-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 (148) hide show
  1. package/CLAUDE.md +4 -2
  2. package/build.gradle.kts +11 -0
  3. package/compose/build.gradle.kts +198 -0
  4. package/compose/build.gradle.kts.backup +198 -0
  5. package/compose/compose.podspec +47 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +20 -0
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
  8. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +12 -0
  9. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
  10. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +153 -0
  11. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +110 -0
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +62 -0
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +1071 -0
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +81 -0
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +357 -0
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +115 -0
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +542 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +49 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +86 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +189 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +478 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +265 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +235 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +223 -0
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +265 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +185 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +370 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +108 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +308 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +330 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +220 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +207 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +92 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +560 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +86 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +125 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +207 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +501 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +176 -0
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +236 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +180 -0
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +325 -0
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +17 -0
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +194 -0
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +287 -0
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +54 -0
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +50 -0
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +321 -0
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +41 -0
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +502 -0
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +58 -0
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +148 -0
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +116 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +205 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +422 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +732 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +148 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +35 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +228 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +217 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +97 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +377 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +455 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +105 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +106 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +74 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +399 -0
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +39 -0
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +363 -0
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +133 -0
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +127 -0
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +12 -0
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +91 -0
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +74 -0
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +20 -0
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +118 -0
  121. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +42 -0
  122. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +45 -0
  123. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +32 -0
  124. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +38 -0
  125. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
  126. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +218 -0
  127. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +138 -0
  128. package/gradle/libs.versions.toml +69 -0
  129. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  130. package/gradle/wrapper/gradle-wrapper.properties +8 -0
  131. package/gradle.properties +26 -0
  132. package/gradlew +252 -0
  133. package/gradlew.bat +94 -0
  134. package/ios/Application/DesignSystemKits.swift +64 -1
  135. package/ios/Layout/ScreenLayout.swift +23 -3
  136. package/ios/Navigation/BottomSheet.swift +150 -22
  137. package/ios/Navigation/Component/SnackBar.swift +4 -0
  138. package/ios/Navigation/ModalScreen.swift +7 -3
  139. package/ios/Popup/PopupDisplay.swift +2 -2
  140. package/ios/Popup/PopupInput.swift +2 -2
  141. package/ios/Popup/PopupLayout.swift +15 -8
  142. package/ios/Popup/PopupNotify.swift +2 -2
  143. package/ios/Typography/FontScale.swift +15 -4
  144. package/ios/native-kits.podspec +1 -1
  145. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  146. package/ios-demo/README.md +120 -0
  147. package/package.json +1 -1
  148. package/settings.gradle.kts +64 -0
@@ -32,7 +32,8 @@ public struct ScreenLayout: Equatable {
32
32
  public let width: CGFloat
33
33
  public let height: CGFloat
34
34
  public let isIPhoneDuo: Bool
35
- /// The device has a hinge, folded or not, whatever the window size; drives popups, modal and bottom sheet.
35
+ /// iPhone Duo folded or not, whatever the window size (its hinge, or the outer display's one-sided strip);
36
+ /// drives popups, modal and bottom sheet.
36
37
  public let isDuoDevice: Bool
37
38
  public let insets: EdgeInsets
38
39
  public let insetLeft: CGFloat
@@ -75,6 +76,11 @@ enum DuoMetrics {
75
76
  static let floatingTabBarHeight: CGFloat = 62
76
77
  static let floatingTabBarMargin: CGFloat = 20
77
78
  static let floatingTabBarInsetOverlap: CGFloat = 13
79
+ // Width of a system alert on the inner display, in both orientations.
80
+ static let modalMaxWidth: CGFloat = 440
81
+ // Fits a PopupNotify with image, scrolled description and actions at the max width (RN
82
+ // Application/device.ts); taller content scrolls instead of filling the inner display.
83
+ static let modalMaxHeight: CGFloat = 640
78
84
  }
79
85
 
80
86
  enum HeaderMetrics {
@@ -178,8 +184,10 @@ extension ScreenLayout {
178
184
  )
179
185
  }
180
186
 
187
+ // the outer display reports no hinge; its one-sided strip still gives it away
181
188
  return ScreenLayout(
182
- width: width, height: height, isIPhoneDuo: true, isDuoDevice: isDuoDevice, insets: insets,
189
+ width: width, height: height, isIPhoneDuo: true,
190
+ isDuoDevice: isDuoDevice || insets.leading != insets.trailing, insets: insets,
183
191
  insetLeft: insets.leading, insetRight: insets.trailing,
184
192
  contentWidth: width - insets.leading - insets.trailing,
185
193
  headerHeight: headerHeight, headerStatusBarHeight: headerStatusBarHeight, sideBar: sideBar,
@@ -256,12 +264,24 @@ extension ScreenLayout {
256
264
  public var duoTopInset: CGFloat {
257
265
  max(insets.top, height > width ? max(insets.leading, insets.trailing) * 2 : 0)
258
266
  }
267
+
268
+ /// Room for a dialog on iPhone Duo: inside the safe area, so clear of the side toolbar strip, and
269
+ /// capped so it stays a dialog on the inner display.
270
+ var duoModalMaxWidth: CGFloat {
271
+ min(width - insets.leading - insets.trailing - Spacing.XL * 2, DuoMetrics.modalMaxWidth)
272
+ }
273
+
274
+ var duoModalMaxHeight: CGFloat {
275
+ min(height - insets.top - insets.bottom - Spacing.XL * 2, DuoMetrics.modalMaxHeight)
276
+ }
259
277
  }
260
278
 
261
279
  /// Snapshot for call sites without `\.screenLayout`; not reactive. `windowSize` is ignored,
262
280
  /// kept for host call sites.
263
281
  public func isIPhoneDuoDevice(windowSize: CGSize? = nil) -> Bool {
264
- isPhoneIdiom && FoldableDevice.isFoldable(measuredIn: keyWindow)
282
+ guard isPhoneIdiom else { return false }
283
+ let insets = keyWindow?.safeAreaInsets
284
+ return FoldableDevice.isFoldable(measuredIn: keyWindow) || insets.map { $0.left != $0.right } == true
265
285
  }
266
286
 
267
287
  /// Apple's foldable check (as the host's `SideTabRailView.isFoldableDevice`): only a device with a
@@ -15,16 +15,25 @@ import SwiftUI
15
15
 
16
16
  /// Mirrors Compose's `sealed class BottomHeader` (Title / Custom).
17
17
  public struct BottomSheetHeader {
18
+ struct Back {
19
+ let icon: String
20
+ let action: () -> Void
21
+ }
22
+
18
23
  enum Style {
19
- case title(String)
24
+ case title(String, back: Back?)
20
25
  case custom(() -> AnyView)
21
26
  }
22
27
 
23
28
  let style: Style
24
29
 
25
- /// Mirrors `BottomHeader.Title`.
26
- public static func title(_ text: String = "Bottom Sheet Title") -> BottomSheetHeader {
27
- BottomSheetHeader(style: .title(text))
30
+ /// Mirrors `BottomHeader.Title`. `onBack` shows a back button; the sheet closes, then it runs.
31
+ public static func title(
32
+ _ text: String = "Bottom Sheet Title",
33
+ backIcon: String = "arrow-back",
34
+ onBack: (() -> Void)? = nil
35
+ ) -> BottomSheetHeader {
36
+ BottomSheetHeader(style: .title(text, back: onBack.map { Back(icon: backIcon, action: $0) }))
28
37
  }
29
38
 
30
39
  /// Mirrors `BottomHeader.Custom`.
@@ -68,6 +77,9 @@ struct BottomSheetView<Content: View>: View {
68
77
  /// Mirrors Compose's `sheetCloseOffset` (100dp drag threshold to dismiss).
69
78
  private let closeThreshold: CGFloat = 100
70
79
 
80
+ private let indicatorHeight: CGFloat = 16
81
+ private let headerRowHeight: CGFloat = 56
82
+
71
83
  /// Mirrors Compose's per-term peak (0.3f). Combined with `backgroundAlpha` the
72
84
  /// steady-state (fully open) scrim peaks at 0.6, matching ModalScreen's peak.
73
85
  private let maxDynamicAlpha: CGFloat = 0.3
@@ -78,6 +90,21 @@ struct BottomSheetView<Content: View>: View {
78
90
  private var isDuo: Bool { layout.isDuoDevice }
79
91
  // iPhone Duo inner display: center the sheet instead of spanning the width
80
92
  private var isDuoCentered: Bool { isDuo && layout.width >= 600 }
93
+ /// Outer display: the sheet floats Spacing.M off the side edges like system sheets, its content
94
+ /// stays in the safe area and its buttons move to the side bar column (HIG).
95
+ private var isDuoFloating: Bool { isDuo && !isDuoCentered }
96
+
97
+ private var titleBack: BottomSheetHeader.Back? {
98
+ if case .title(_, let back) = header.style { return back }
99
+ return nil
100
+ }
101
+
102
+ private var sideColumn: SideBarLayout? {
103
+ guard isDuoFloating, case .title = header.style, let sideBar = layout.sideBar,
104
+ sideBar.width - Spacing.M >= DuoMetrics.controlSize + Spacing.M
105
+ else { return nil }
106
+ return sideBar
107
+ }
81
108
 
82
109
  /// Keeps the portrait width in landscape, like system sheets.
83
110
  private var duoCenteredWidth: CGFloat {
@@ -87,18 +114,19 @@ struct BottomSheetView<Content: View>: View {
87
114
  )
88
115
  }
89
116
 
90
- /// Landscape side buttons span the full height, so the content is inset; in portrait the
91
- /// side status bar only covers the top corner.
117
+ /// The floating sheet spans the side safe area, its content stays clear of it.
92
118
  private var sideInsetLeading: CGFloat {
93
- isDuo && !isDuoCentered && layout.width > layout.height ? layout.insets.leading : 0
119
+ isDuoFloating ? max(layout.insets.leading - Spacing.M, 0) : 0
94
120
  }
95
121
 
96
122
  private var sideInsetTrailing: CGFloat {
97
- isDuo && !isDuoCentered && layout.width > layout.height ? layout.insets.trailing : 0
123
+ isDuoFloating ? max(layout.insets.trailing - Spacing.M, 0) : 0
98
124
  }
99
125
 
100
126
  private var maxHeight: CGFloat {
101
- isDuo ? layout.height - max(layout.duoTopInset, Spacing.XL) : screenHeight - 90
127
+ guard isDuo else { return screenHeight - 90 }
128
+ // with its buttons in the side column the sheet can pass the side status bar
129
+ return layout.height - max(sideColumn != nil ? layout.insets.top : layout.duoTopInset, Spacing.XL)
102
130
  }
103
131
  private var bottomInset: CGFloat { useBottomInset ? safeAreaBottomInset() : 0 }
104
132
  /// While the keyboard is up, it already covers the home-indicator area, so it
@@ -170,19 +198,21 @@ struct BottomSheetView<Content: View>: View {
170
198
  .frame(width: 40, height: 4)
171
199
  Spacer().frame(height: 4)
172
200
  headerView
173
- .frame(height: 56)
201
+ .frame(height: headerRowHeight)
174
202
  .padding(.horizontal, Spacing.M)
175
- .padding(.leading, sideInsetLeading)
176
- .padding(.trailing, sideInsetTrailing)
177
203
  }
204
+ .padding(.leading, sideInsetLeading)
205
+ .padding(.trailing, sideInsetTrailing)
178
206
  .frame(maxWidth: .infinity)
179
- .frame(height: 72)
207
+ .frame(height: indicatorHeight + headerRowHeight)
180
208
  .contentShape(Rectangle())
181
209
  .gesture(dragGesture, including: draggable ? .all : .subviews)
182
210
 
183
211
  Rectangle()
184
212
  .fill(theme.colors.border.default)
185
213
  .frame(height: 1)
214
+ .padding(.leading, sideInsetLeading)
215
+ .padding(.trailing, sideInsetTrailing)
186
216
 
187
217
  content()
188
218
  .padding(.leading, sideInsetLeading)
@@ -191,7 +221,7 @@ struct BottomSheetView<Content: View>: View {
191
221
  .layoutPriority(-1)
192
222
 
193
223
  if let footer {
194
- BottomSheetFooter(content: footer)
224
+ BottomSheetFooter(content: footer, leadingInset: sideInsetLeading, trailingInset: sideInsetTrailing)
195
225
  }
196
226
  }
197
227
  // The filler stays outside the cap, which shrinks by it instead: the sheet still ends at
@@ -209,27 +239,61 @@ struct BottomSheetView<Content: View>: View {
209
239
  }
210
240
  )
211
241
  .background(backgroundColor)
212
- .clipShape(RoundedCorner(radius: Radius.M, corners: [.topLeft, .topRight]))
242
+ .overlay {
243
+ if let sideColumn {
244
+ BottomSheetSideColumn(
245
+ sideBar: sideColumn,
246
+ windowHeight: layout.height,
247
+ topOffset: indicatorHeight + (headerRowHeight - DuoMetrics.controlSize) / 2,
248
+ back: titleBack,
249
+ onBack: goBack,
250
+ onClose: { close() }
251
+ )
252
+ }
253
+ }
254
+ .clipShape(RoundedCorner(radius: isDuo ? Radius.XL : Radius.M, corners: [.topLeft, .topRight]))
255
+ .padding(.horizontal, isDuoFloating ? Spacing.M : 0)
213
256
  }
214
257
 
215
258
  @ViewBuilder
216
259
  private var headerView: some View {
217
260
  switch header.style {
218
- case .title(let text):
261
+ case .title(let text, let back):
262
+ // the side column holds back and close, the row keeps only the title
263
+ let showsButtons = sideColumn == nil
219
264
  HStack(spacing: 0) {
220
- Color.clear.frame(width: 24, height: 24)
265
+ if showsButtons {
266
+ if let back {
267
+ headerButton(icon: back.icon, accessibilityId: "btn_bottom_sheet_back") { goBack(back) }
268
+ } else {
269
+ Color.clear.frame(width: 24, height: 24)
270
+ }
271
+ }
221
272
  MomoText(text, typography: .headerDefaultBold, color: Colors.black17)
222
273
  .frame(maxWidth: .infinity, alignment: .center)
223
- Icon(source: "navigation_close", size: 24, color: Colors.black17)
224
- .frame(width: 24, height: 24)
225
- .contentShape(Rectangle())
226
- .onTapGesture { close() }
274
+ .padding(.horizontal, showsButtons ? 0 : Spacing.M)
275
+ if showsButtons {
276
+ headerButton(icon: "navigation_close", accessibilityId: "btn_bottom_sheet_close") { close() }
277
+ }
227
278
  }
228
279
  case .custom(let builder):
229
280
  builder()
230
281
  }
231
282
  }
232
283
 
284
+ private func headerButton(icon: String, accessibilityId: String, action: @escaping () -> Void) -> some View {
285
+ Icon(source: icon, size: 24, color: Colors.black17)
286
+ .frame(width: 24, height: 24)
287
+ .contentShape(Rectangle())
288
+ .onTapGesture(perform: action)
289
+ .accessibilityIdentifier(accessibilityId)
290
+ }
291
+
292
+ /// RN `leftOptions.onPressIconLeft`: the sheet closes first, then the back runs.
293
+ private func goBack(_ back: BottomSheetHeader.Back) {
294
+ close(then: back.action)
295
+ }
296
+
233
297
  // MARK: - Drag handling (mirrors Compose's detectDragGestures)
234
298
 
235
299
  private var dragGesture: some Gesture {
@@ -282,7 +346,7 @@ struct BottomSheetView<Content: View>: View {
282
346
 
283
347
  /// Mirrors Compose's `closeEvent`: slide out over 200ms, then fade the scrim
284
348
  /// out over 100ms only after the slide finishes, then dismiss.
285
- private func close() {
349
+ private func close(then onClosed: (() -> Void)? = nil) {
286
350
  guard !isClosing else { return }
287
351
  isClosing = true
288
352
  withAnimation(.timingCurve(0.3, 0, 0.8, 0.15, duration: 0.2)) {
@@ -294,7 +358,67 @@ struct BottomSheetView<Content: View>: View {
294
358
  }
295
359
  }
296
360
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
361
+ // a newer overlay replaced this one: it stays, and this sheet's back is dropped
362
+ guard navigator.overplay?.id == overplayId else { return }
297
363
  navigator.finishOverplayDismiss(id: overplayId)
364
+ onClosed?()
365
+ }
366
+ }
367
+ }
368
+
369
+ /// iPhone Duo outer display: back and close sit on the part of the sheet under the side safe area,
370
+ /// 12pt from the strip's inner edge like the screen side bar. They follow the header row while the
371
+ /// sheet is short and stay below the status bar cluster while it is tall.
372
+ private struct BottomSheetSideColumn: View {
373
+ let sideBar: SideBarLayout
374
+ let windowHeight: CGFloat
375
+ /// Lines the controls up with the header row.
376
+ let topOffset: CGFloat
377
+ let back: BottomSheetHeader.Back?
378
+ let onBack: (BottomSheetHeader.Back) -> Void
379
+ let onClose: () -> Void
380
+
381
+ @Environment(\.appTheme) private var theme
382
+
383
+ private var isRight: Bool { sideBar.edge == .right }
384
+
385
+ private var columnHeight: CGFloat {
386
+ let count: CGFloat = back != nil ? 2 : 1
387
+ return count * DuoMetrics.controlSize + (count - 1) * Spacing.M
388
+ }
389
+
390
+ private var headerColor: HeaderColor {
391
+ getHeaderColor(isAnimated: false, isSurface: true, opacity: 1, tintColor: nil, defaultColor: theme.colors.text.default)
392
+ }
393
+
394
+ var body: some View {
395
+ GeometryReader { proxy in
396
+ let sheetHeight = proxy.size.height
397
+ // the sheet rests on the window bottom; its slide is an offset outside this layout
398
+ let sheetTop = windowHeight - sheetHeight
399
+ // clear of a camera at the bottom end of the strip when it can
400
+ let lowest = sheetHeight - sideBar.bottom - columnHeight
401
+ let top = max(min(max(sideBar.top - sheetTop, topOffset), lowest), 0)
402
+ VStack(alignment: isRight ? .leading : .trailing, spacing: Spacing.M) {
403
+ if let back {
404
+ SideBarButton(
405
+ icon: back.icon,
406
+ headerColor: headerColor,
407
+ accessibilityId: "btn_bottom_sheet_back",
408
+ action: { onBack(back) }
409
+ )
410
+ }
411
+ SideBarButton(
412
+ icon: "navigation_close",
413
+ headerColor: headerColor,
414
+ accessibilityId: "btn_bottom_sheet_close",
415
+ action: onClose
416
+ )
417
+ }
418
+ .padding(isRight ? .leading : .trailing, Spacing.M)
419
+ .frame(width: sideBar.width - Spacing.M, alignment: isRight ? .leading : .trailing)
420
+ .padding(.top, top)
421
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: isRight ? .topTrailing : .topLeading)
298
422
  }
299
423
  }
300
424
  }
@@ -371,12 +495,16 @@ private struct CapHeightLayout: Layout {
371
495
  /// Mirrors Compose's `BottomSheetFooter`: a surface band with a soft shadow cast on the content.
372
496
  private struct BottomSheetFooter: View {
373
497
  let content: () -> AnyView
498
+ var leadingInset: CGFloat = 0
499
+ var trailingInset: CGFloat = 0
374
500
 
375
501
  @Environment(\.appTheme) private var theme
376
502
 
377
503
  var body: some View {
378
504
  content()
379
505
  .frame(maxWidth: .infinity)
506
+ .padding(.leading, leadingInset)
507
+ .padding(.trailing, trailingInset)
380
508
  .padding(.vertical, Spacing.S)
381
509
  .background(theme.colors.background.surface)
382
510
  .overlay(alignment: .top) {
@@ -57,6 +57,7 @@ struct SnackBarView: View {
57
57
  let bottomInset: CGFloat
58
58
 
59
59
  @EnvironmentObject private var navigator: Navigator
60
+ @Environment(\.screenLayout) private var layout
60
61
 
61
62
  @State private var contentHeight: CGFloat = 0
62
63
  @State private var offsetY: CGFloat = 0
@@ -75,6 +76,9 @@ struct SnackBarView: View {
75
76
  )
76
77
  .offset(y: offsetY)
77
78
  .padding(.bottom, bottomInset)
79
+ // iPhone Duo: clear of the side toolbar strip, like the screen content (0 elsewhere)
80
+ .padding(.leading, layout.insetLeft)
81
+ .padding(.trailing, layout.insetRight)
78
82
  }
79
83
  .frame(maxWidth: .infinity, maxHeight: .infinity)
80
84
  .ignoresSafeArea()
@@ -34,11 +34,15 @@ struct ModalScreen<Content: View>: View {
34
34
  }
35
35
 
36
36
  content()
37
+ // iPhone Duo: capped so it stays a dialog on the inner display; nil leaves the size alone
38
+ .frame(
39
+ maxWidth: layout.isDuoDevice ? layout.duoModalMaxWidth : nil,
40
+ maxHeight: layout.isDuoDevice ? layout.duoModalMaxHeight : nil
41
+ )
37
42
  .scaleEffect(contentScale)
38
43
  .opacity(contentOpacity)
39
- // iPhone Duo: center below the side status bar and above the home indicator
40
- .padding(.top, layout.isDuoDevice ? layout.duoTopInset : 0)
41
- .padding(.bottom, layout.isDuoDevice ? layout.insets.bottom : 0)
44
+ // iPhone Duo: center in the safe area, clear of the side toolbar strip and the home indicator
45
+ .padding(layout.isDuoDevice ? layout.insets : EdgeInsets())
42
46
  }
43
47
  // Full screen like RN's Modal, so asymmetric landscape insets don't shift the content sideways.
44
48
  .frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -107,7 +107,7 @@ public struct PopupDisplay: View {
107
107
  }
108
108
  .fixedSize(horizontal: false, vertical: true)
109
109
  .background(theme.colors.background.surface)
110
- .clipShape(RoundedRectangle(cornerRadius: Radius.L))
110
+ .clipShape(RoundedRectangle(cornerRadius: layout.popupRadius()))
111
111
 
112
112
  if isShowCloseIcon {
113
113
  PopupCloseButton(action: onClose)
@@ -115,7 +115,7 @@ public struct PopupDisplay: View {
115
115
  }
116
116
  }
117
117
  .frame(maxWidth: layout.popupMaxWidth)
118
- .padding(.horizontal, 12)
118
+ .padding(.horizontal, layout.popupHorizontalPadding(12))
119
119
  .accessibilityElement(children: .contain)
120
120
  .accessibilityIdentifier("popup_notify")
121
121
  }
@@ -59,11 +59,11 @@ public struct PopupInput: View {
59
59
  .offset(x: 11, y: -11)
60
60
  }
61
61
  }
62
- .background(theme.colors.background.surface.cornerRadius(8))
62
+ .background(theme.colors.background.surface.cornerRadius(layout.popupRadius(8)))
63
63
  .shadow(color: .black.opacity(0.08), radius: 2, x: 0, y: 0)
64
64
  .shadow(color: .black.opacity(0.16), radius: 24, x: 0, y: 0)
65
65
  .frame(maxWidth: layout.popupMaxWidth)
66
- .padding(.horizontal, 40)
66
+ .padding(.horizontal, layout.popupHorizontalPadding(40))
67
67
  }
68
68
 
69
69
  // MARK: Internal
@@ -1,19 +1,26 @@
1
1
  import SDWebImageSwiftUI
2
2
  import SwiftUI
3
3
 
4
- // Width of a system alert on the iPhone Duo inner display, in both orientations (RN Popup/utils.ts).
5
- private let duoPopupMaxWidth: CGFloat = 440
6
-
7
4
  extension ScreenLayout {
8
- /// Card width cap (Compose `popupMaxWidth`); unbounded off iPhone Duo, where the popup's own padding sets the width.
5
+ /// Card width cap on iPhone Duo (the modal bounds, Compose `popupMaxWidth`); unbounded elsewhere,
6
+ /// where the popup's own padding sets the width.
9
7
  var popupMaxWidth: CGFloat {
10
- isDuoDevice ? duoPopupMaxWidth : .infinity
8
+ isDuoDevice ? duoModalMaxWidth : .infinity
9
+ }
10
+
11
+ /// The modal bounds already keep iPhone Duo popups off the edges, so their own side padding goes.
12
+ func popupHorizontalPadding(_ padding: CGFloat) -> CGFloat {
13
+ isDuoDevice ? 0 : padding
11
14
  }
12
15
 
13
- /// Room left for a popup on iPhone Duo, where landscape is too short for it; nil elsewhere.
16
+ /// Tallest a popup gets on iPhone Duo, landscape and the inner display included; nil elsewhere.
14
17
  var popupMaxHeight: CGFloat? {
15
- guard isDuoDevice else { return nil }
16
- return height - duoTopInset - insets.bottom - Spacing.XL * 2
18
+ isDuoDevice ? duoModalMaxHeight : nil
19
+ }
20
+
21
+ /// Rounder on iPhone Duo, closer to its system alerts.
22
+ func popupRadius(_ regular: CGFloat = Radius.L) -> CGFloat {
23
+ isDuoDevice ? Radius.XL : regular
17
24
  }
18
25
  }
19
26
 
@@ -105,7 +105,7 @@ public struct PopupNotify: View {
105
105
  .popupReadHeight($actionsHeight)
106
106
  }
107
107
  .background(theme.colors.background.surface)
108
- .clipShape(RoundedRectangle(cornerRadius: Radius.L))
108
+ .clipShape(RoundedRectangle(cornerRadius: layout.popupRadius()))
109
109
 
110
110
  if props.isShowCloseIcon {
111
111
  PopupCloseButton(action: props.onIconClose)
@@ -113,7 +113,7 @@ public struct PopupNotify: View {
113
113
  }
114
114
  }
115
115
  .frame(maxWidth: layout.popupMaxWidth)
116
- .padding(.horizontal, Spacing.M)
116
+ .padding(.horizontal, layout.popupHorizontalPadding(Spacing.M))
117
117
  .accessibilityElement(children: .contain)
118
118
  .accessibilityIdentifier("popup_notify")
119
119
  }
@@ -19,11 +19,19 @@ public struct FontScale: Equatable {
19
19
  public let useOSScaleRate: Bool
20
20
  /// OS Dynamic Type factor at the time the snapshot was taken.
21
21
  public let osFontScale: CGFloat
22
+ // AI-GENERATED START: device scale from the app window, not UIScreen.main (fixed to one display on iPhone Duo)
23
+ public let windowShortSide: CGFloat
22
24
 
23
- public init(userScaleRate: CGFloat = 1, useOSScaleRate: Bool = true, osFontScale: CGFloat = 1) {
25
+ public init(
26
+ userScaleRate: CGFloat = 1,
27
+ useOSScaleRate: Bool = true,
28
+ osFontScale: CGFloat = 1,
29
+ windowShortSide: CGFloat? = nil
30
+ ) {
24
31
  self.userScaleRate = userScaleRate
25
32
  self.useOSScaleRate = useOSScaleRate
26
33
  self.osFontScale = osFontScale
34
+ self.windowShortSide = windowShortSide ?? DesignSystemKits.currentWindowShortSide()
27
35
  }
28
36
 
29
37
  /// `callAsFunction` so call sites keep reading like Compose's `scaleSize(16)`.
@@ -31,8 +39,8 @@ public struct FontScale: Equatable {
31
39
  guard useOSScaleRate else { return size * userScaleRate }
32
40
 
33
41
  // Short side, not width: landscape must not inflate every font (and so every popup's height).
34
- let bounds = UIScreen.main.bounds.size
35
- let deviceScale = min(bounds.width, bounds.height) / Self.defaultScreenSize
42
+ let deviceScale = windowShortSide / Self.defaultScreenSize
43
+ // AI-GENERATED END: device scale from the app window, not UIScreen.main (fixed to one display on iPhone Duo)
36
44
 
37
45
  var deviceScaled = size
38
46
  var osScaled = size
@@ -58,7 +66,10 @@ public struct FontScale: Equatable {
58
66
  return FontScale(
59
67
  userScaleRate: userScaleRate ?? self.userScaleRate,
60
68
  useOSScaleRate: useOSScaleRate ?? self.useOSScaleRate,
61
- osFontScale: osFontScale
69
+ osFontScale: osFontScale,
70
+ // AI-GENERATED START: keep the window size through context overrides
71
+ windowShortSide: windowShortSide
72
+ // AI-GENERATED END: keep the window size through context overrides
62
73
  )
63
74
  }
64
75
  }
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'MoMoUIKits'
3
- s.version = '0.167.1-beta.22'
3
+ s.version = '0.168.1-beta.8'
4
4
  s.summary = 'MoMoUIKits for iOS'
5
5
  s.homepage = 'https://momo.vn'
6
6
  s.license = { :type => 'MIT' }
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'MoMoUIKitsDemo'
3
- s.version = '0.167.1-beta.22'
3
+ s.version = '0.168.1-beta.8'
4
4
  s.summary = 'Demo browser for MoMoUIKits SwiftUI components'
5
5
  s.homepage = 'https://momo.vn'
6
6
  s.license = { :type => 'MIT' }
@@ -0,0 +1,120 @@
1
+ # MoMoUIKitsDemo
2
+
3
+ SwiftUI demo browser for `MoMoUIKits`.
4
+
5
+ The package can optionally host a Compose UIKits screen by receiving a
6
+ `UIViewController` factory from the host app. It does not import the KMP
7
+ `shared` module directly.
8
+
9
+ SwiftUI mode reuses the full demo screen set from
10
+ `sample/iosApp/iosApp/Demo`, including the sample `HomeView` registry and demo
11
+ screens for all currently available components.
12
+
13
+ ## CocoaPods
14
+
15
+ In the host app `Podfile`, include both local pods:
16
+
17
+ ```ruby
18
+ pod 'MoMoUIKits', :path => '../momo-native-kits/ios/native-kits.podspec'
19
+ pod 'MoMoUIKitsDemo', :path => '../momo-native-kits/ios-demo'
20
+ ```
21
+
22
+ Adjust the relative paths to match the host app checkout.
23
+
24
+ ## Host App Usage
25
+
26
+ ```swift
27
+ import MoMoUIKitsDemo
28
+ import shared
29
+
30
+ class UIKitsViewController: BaseComposeAppViewController {
31
+ override func getViewController() -> UIViewController? {
32
+ let params = self.application.data as? [String: Any]
33
+
34
+ return MoMoUIKitsDemoViewControllerFactory.make(
35
+ context: MoMoUIKitsDemoContext(
36
+ appId: params?["appId"] as? String ?? "",
37
+ appCode: params?["code"] as? String ?? "",
38
+ appName: params?["name"] as? String,
39
+ appIcon: params?["icon"] as? String ?? ""
40
+ ),
41
+ composeViewController: {
42
+ UIKitsScreenComposeViewControllerKt.UIKitsScreenComposeViewController(
43
+ params: params
44
+ )
45
+ }
46
+ )
47
+ }
48
+
49
+ override func viewDidDisappear(_ animated: Bool) {
50
+ super.viewDidDisappear(animated)
51
+ composeViewController = nil
52
+ }
53
+ }
54
+ ```
55
+
56
+ If the host does not pass `composeViewController`, the demo opens directly in
57
+ SwiftUI mode.
58
+
59
+ ## Included SwiftUI Demos
60
+
61
+ The pod includes the sample demo screens for:
62
+
63
+ - Animated Header
64
+ - Avatar
65
+ - Badge
66
+ - Baseline View
67
+ - Bottom Tab
68
+ - Button
69
+ - Carousel
70
+ - Checkbox
71
+ - Chip
72
+ - Collapse
73
+ - DateTimePicker
74
+ - Divider
75
+ - Header User
76
+ - Icon Button
77
+ - Image
78
+ - Information
79
+ - Input
80
+ - Input DropDown
81
+ - Input Money
82
+ - Input OTP
83
+ - Input Phone Number
84
+ - Input Search
85
+ - Input TextArea
86
+ - Loader
87
+ - Navigation
88
+ - Pagination
89
+ - Popup Display
90
+ - Popup Notify
91
+ - Popup Promotion
92
+ - Progress Info
93
+ - Radio
94
+ - Rating
95
+ - Skeleton
96
+ - Slider
97
+ - SnackBar
98
+ - Stepper
99
+ - Steps
100
+ - Suggest Action
101
+ - Swipe
102
+ - Switch
103
+ - TabView
104
+ - Tag
105
+ - Theme
106
+ - Title
107
+ - Tooltip
108
+ - Typography
109
+ - Uploader
110
+
111
+ ## Validation Notes
112
+
113
+ Validated locally with:
114
+
115
+ ```sh
116
+ pod ipc spec ios/MoMoUIKits.podspec
117
+ pod ipc spec ios-demo/MoMoUIKitsDemo.podspec
118
+ pod lib lint ios/MoMoUIKits.podspec --allow-warnings --skip-import-validation
119
+ pod lib lint ios-demo/MoMoUIKitsDemo.podspec --allow-warnings --skip-import-validation --include-podspecs=ios/MoMoUIKits.podspec
120
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/native-kits",
3
- "version": "0.162.49",
3
+ "version": "0.162.50-debug",
4
4
  "private": false,
5
5
  "dependeNavigationncies": {},
6
6
  "devDependencies": {},