@momo-kits/native-kits 0.163.1-sp.8 → 0.163.2-beta.1-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 (186) hide show
  1. package/.codegraph/.gitignore +5 -0
  2. package/CLAUDE.md +0 -10
  3. package/build.gradle.kts +11 -0
  4. package/compose/build.gradle.kts +190 -0
  5. package/compose/build.gradle.kts.backup +190 -0
  6. package/compose/compose.podspec +47 -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/OsFontScale.android.kt +7 -0
  9. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +150 -0
  10. package/compose/src/commonMain/composeResources/font/momosignature.otf +0 -0
  11. package/compose/src/commonMain/composeResources/font/momotrustdisplay.otf +0 -0
  12. package/compose/src/commonMain/composeResources/font/sfprotext_black.otf +0 -0
  13. package/compose/src/commonMain/composeResources/font/sfprotext_black.ttf +0 -0
  14. package/compose/src/commonMain/composeResources/font/sfprotext_bold.ttf +0 -0
  15. package/compose/src/commonMain/composeResources/font/sfprotext_heavy.ttf +0 -0
  16. package/compose/src/commonMain/composeResources/font/sfprotext_light.ttf +0 -0
  17. package/compose/src/commonMain/composeResources/font/sfprotext_medium.ttf +0 -0
  18. package/compose/src/commonMain/composeResources/font/sfprotext_regular.ttf +0 -0
  19. package/compose/src/commonMain/composeResources/font/sfprotext_semibold.ttf +0 -0
  20. package/compose/src/commonMain/composeResources/font/sfprotext_thin.otf +0 -0
  21. package/compose/src/commonMain/composeResources/font/sfprotext_thin.ttf +0 -0
  22. package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.otf +0 -0
  23. package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.ttf +0 -0
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +52 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +106 -0
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +183 -0
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +207 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +39 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +293 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +826 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +110 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +378 -0
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +82 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +356 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +113 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +542 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +23 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +76 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +112 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +477 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +214 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +240 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +216 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +259 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +182 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +328 -0
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +97 -0
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +310 -0
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +323 -0
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +217 -0
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +199 -0
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +91 -0
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +470 -0
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +88 -0
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +131 -0
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +203 -0
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +498 -0
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +175 -0
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +234 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +188 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +325 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +188 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +276 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +52 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +38 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +39 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +299 -0
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +129 -0
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +100 -0
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +172 -0
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +372 -0
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +581 -0
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +156 -0
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +225 -0
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +320 -0
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +81 -0
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +309 -0
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +34 -0
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +360 -0
  124. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +132 -0
  125. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +127 -0
  126. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +91 -0
  127. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
  128. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +69 -0
  129. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  130. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
  131. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
  132. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +105 -0
  133. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +32 -0
  134. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
  135. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +207 -0
  136. package/example/ios/Example.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/Example.xcscheme +32 -0
  137. package/example/ios/Example.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +14 -0
  138. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  139. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/WorkspaceSettings.xcsettings +16 -0
  140. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +6 -0
  141. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +5 -0
  142. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/MoMoUIKits.xcscheme +58 -0
  143. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/Pods-Example.xcscheme +58 -0
  144. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SDWebImage.xcscheme +58 -0
  145. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SDWebImageSwiftUI.xcscheme +58 -0
  146. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SkeletonUI.xcscheme +58 -0
  147. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/lottie-ios-LottiePrivacyInfo.xcscheme +58 -0
  148. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/lottie-ios.xcscheme +58 -0
  149. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +60 -0
  150. package/gradle/libs.versions.toml +69 -0
  151. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  152. package/gradle/wrapper/gradle-wrapper.properties +8 -0
  153. package/gradle.properties +26 -0
  154. package/gradlew +252 -0
  155. package/gradlew.bat +94 -0
  156. package/ios/Application/ApplicationEnvironment.swift +4 -6
  157. package/ios/Application/HeaderRight.swift +17 -1
  158. package/ios/Application/Localize.swift +4 -73
  159. package/ios/Application/Navigation/NavigationContainer.swift +18 -25
  160. package/ios/Application/Navigation/Navigator.swift +29 -91
  161. package/ios/Application/StackScreen.swift +21 -18
  162. package/ios/Badge/BadgeRibbon.swift +1 -4
  163. package/ios/Button/Button.swift +61 -41
  164. package/ios/Icon/Icon.swift +4 -13
  165. package/ios/native-kits.podspec +1 -2
  166. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  167. package/ios-demo/README.md +120 -0
  168. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ButtonDemo.swift +8 -8
  169. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CheckboxDemo.swift +2 -3
  170. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +12 -5
  171. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/LocalizeDemo.swift +4 -51
  172. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +2 -14
  173. package/ios-demo/Sources/MoMoUIKitsDemo/SwiftUIDemoBrowserView.swift +1 -3
  174. package/local.properties +8 -0
  175. package/package.json +1 -1
  176. package/settings.gradle.kts +64 -0
  177. package/.claude/settings.local.json +0 -158
  178. package/ios/Application/LanguageSource.swift +0 -93
  179. package/ios/Application/LocalizeModifier.swift +0 -30
  180. package/ios/Application/Navigation/NavigatorManager.swift +0 -49
  181. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  182. package/ios/Layout/GridContext.swift +0 -30
  183. package/ios/Layout/Item.swift +0 -42
  184. package/ios/Layout/Section.swift +0 -134
  185. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IconDemo.swift +0 -104
  186. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SectionDemo.swift +0 -100
@@ -89,11 +89,7 @@ public struct OverplayItem: Identifiable {
89
89
 
90
90
  @available(iOS 16.0, *)
91
91
  public final class Navigator: ObservableObject {
92
- @Published public var path: [DynamicScreenRoute] = [] {
93
- // Disposal hangs off the path so every route that leaves the stack — pop,
94
- // replace, reset — is released the same way, after its exit transition.
95
- didSet { disposeScreens(droppedFrom: oldValue) }
96
- }
92
+ @Published public var path: [DynamicScreenRoute] = []
97
93
  @Published public var presented: DynamicDialogRoute?
98
94
  @Published public var overplay: OverplayItem?
99
95
 
@@ -151,7 +147,8 @@ public final class Navigator: ObservableObject {
151
147
 
152
148
  public func replace<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
153
149
  guard !path.isEmpty else { return }
154
- path.removeLast()
150
+ let removed = path.removeLast()
151
+ registry.unregister(id: removed.id)
155
152
  push(content, options: options)
156
153
  }
157
154
 
@@ -161,12 +158,14 @@ public final class Navigator: ObservableObject {
161
158
  options: NavigationOptions? = nil
162
159
  ) {
163
160
  guard !path.isEmpty else { return }
164
- path.removeLast()
161
+ let removed = path.removeLast()
162
+ registry.unregister(id: removed.id)
165
163
  push(screenName: screenName, content, options: options)
166
164
  }
167
165
 
168
166
  public func reset<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
169
- let previousRoot = rootRoute.id
167
+ for route in path { registry.unregister(id: route.id) }
168
+ registry.unregister(id: rootRoute.id)
170
169
  path.removeAll()
171
170
  let route = registry.register(
172
171
  screenName: options?.screenName ?? "",
@@ -174,7 +173,6 @@ public final class Navigator: ObservableObject {
174
173
  options: options
175
174
  )
176
175
  rootRoute = route
177
- disposeScreens([previousRoot])
178
176
  }
179
177
 
180
178
  public func reset<V: View>(
@@ -182,7 +180,8 @@ public final class Navigator: ObservableObject {
182
180
  @ViewBuilder _ content: @escaping () -> V,
183
181
  options: NavigationOptions? = nil
184
182
  ) {
185
- let previousRoot = rootRoute.id
183
+ for route in path { registry.unregister(id: route.id) }
184
+ registry.unregister(id: rootRoute.id)
186
185
  path.removeAll()
187
186
  let route = registry.register(
188
187
  screenName: screenName,
@@ -190,50 +189,26 @@ public final class Navigator: ObservableObject {
190
189
  options: options
191
190
  )
192
191
  rootRoute = route
193
- disposeScreens([previousRoot])
194
192
  }
195
193
 
196
194
  public func pop(_ count: Int = 1, callback: (() -> Void)? = nil) {
197
- popStep(remaining: count, callback: callback)
198
- }
199
-
200
- /// One `pop` iteration. Compose runs the loop in a coroutine and suspends over the
201
- /// overplay's exit animation before the next step; the same wait is needed here, or
202
- /// a `pop(2)` (prevent-back: close the popup, then leave) would spend both steps on
203
- /// the overplay that is still animating out and never pop the screen.
204
- private func popStep(remaining: Int, callback: (() -> Void)?) {
205
- guard remaining > 0 else {
206
- callback?()
207
- return
208
- }
209
-
210
- if overplay?.type == .snackBar {
211
- // Mirrors Kotlin: a snackbar doesn't block back-navigation, so popping
212
- // while one is showing pops the screen underneath and clears the
213
- // snackbar immediately, without playing its dismiss animation.
214
- overplayDismissHandler = nil
215
- overplay = nil
216
- guard popScreen() else {
217
- callback?()
218
- return
219
- }
220
- popStep(remaining: remaining - 1, callback: callback)
221
- } else if overplay != nil {
222
- hideOverplay()
223
- DispatchQueue.main.asyncAfter(deadline: .now() + Self.overplayDismissDelay) { [weak self] in
224
- guard let self else { return }
225
- // The component reports back through `finishOverplayDismiss` once its exit
226
- // animation ends; clear it here if it never did, so the step is not wasted.
227
- if self.overplay != nil { self.finishOverplayDismiss() }
228
- self.popStep(remaining: remaining - 1, callback: callback)
195
+ var remaining = count
196
+ while remaining > 0 {
197
+ if overplay?.type == .snackBar {
198
+ // Mirrors Kotlin: a snackbar doesn't block back-navigation, so popping
199
+ // while one is showing pops the screen underneath and clears the
200
+ // snackbar immediately, without playing its dismiss animation.
201
+ overplayDismissHandler = nil
202
+ overplay = nil
203
+ if !popScreen() { break }
204
+ } else if overplay != nil {
205
+ hideOverplay()
206
+ } else if !popScreen() {
207
+ break
229
208
  }
230
- } else {
231
- guard popScreen() else {
232
- callback?()
233
- return
234
- }
235
- popStep(remaining: remaining - 1, callback: callback)
209
+ remaining -= 1
236
210
  }
211
+ callback?()
237
212
  }
238
213
 
239
214
  /// Pops the topmost presented screen or stack entry. Returns `false` (and requests
@@ -243,10 +218,11 @@ public final class Navigator: ObservableObject {
243
218
  if presented != nil {
244
219
  let dismissedId = presented?.id
245
220
  presented = nil
246
- if let id = dismissedId { disposeScreens([id]) }
221
+ if let id = dismissedId { registry.unregister(id: id) }
247
222
  return true
248
223
  } else if !path.isEmpty {
249
- path.removeLast()
224
+ let removed = path.removeLast()
225
+ registry.unregister(id: removed.id)
250
226
  return true
251
227
  } else {
252
228
  composeApi?.requestCallback(funcName: "dismiss", params: nil, onResponse: nil)
@@ -255,18 +231,12 @@ public final class Navigator: ObservableObject {
255
231
  }
256
232
 
257
233
  public func popToRoot() {
234
+ for route in path { registry.unregister(id: route.id) }
258
235
  path.removeAll()
259
236
  }
260
237
 
261
- /// The entry a back action applies to — mirrors Compose's `currentScreen()`
262
- /// (`navController.currentBackStackEntry`): a presented screen sits on top of
263
- /// the stack, otherwise it is the last pushed screen, otherwise the root.
264
- private func currentScreen() -> DynamicScreen? {
265
- registry.getScreen(id: presented?.id ?? path.last?.id ?? rootRoute.id)
266
- }
267
-
268
238
  public func onBackSafe(callback: (() -> Void)? = nil) {
269
- let latest = currentScreen()
239
+ let latest = registry.latest()
270
240
  if let onBack = latest?.options?.onBackHandler {
271
241
  onBack()
272
242
  return
@@ -412,36 +382,4 @@ public final class Navigator: ObservableObject {
412
382
  public func hideSnackBar() {
413
383
  if overplay?.type == .snackBar { hideOverplay() }
414
384
  }
415
-
416
- // MARK: Screen disposal
417
-
418
- /// Matches the `delay(300)` Compose waits in `dismissScreen` before unregistering,
419
- /// with a little headroom for the pop transition.
420
- private static let screenDisposeDelay: TimeInterval = 0.35
421
-
422
- /// Longest overplay exit animation (the bottom sheet's), so a queued pop step runs
423
- /// after the component has taken itself down.
424
- private static let overplayDismissDelay: TimeInterval = 0.3
425
-
426
- private func disposeScreens(droppedFrom previous: [DynamicScreenRoute]) {
427
- let live = Set(path.map(\.id))
428
- disposeScreens(previous.map(\.id).filter { !live.contains($0) })
429
- }
430
-
431
- /// Screens are released only once the pop transition has played out: the outgoing
432
- /// view is still on screen while it animates and the registry is what renders it —
433
- /// dropping it right away blanks the screen mid-transition. Liveness is re-checked
434
- /// when the timer fires, so a route pushed back before then keeps its screen.
435
- private func disposeScreens(_ ids: [Int]) {
436
- guard !ids.isEmpty else { return }
437
- DispatchQueue.main.asyncAfter(deadline: .now() + Self.screenDisposeDelay) { [weak self] in
438
- guard let self else { return }
439
- var live = Set(self.path.map(\.id))
440
- live.insert(self.rootRoute.id)
441
- if let presented = self.presented { live.insert(presented.id) }
442
- for id in ids where !live.contains(id) {
443
- self.registry.unregister(id: id)
444
- }
445
- }
446
- }
447
385
  }
@@ -5,7 +5,7 @@
5
5
  // SwiftUI mirror of Compose's `StackScreen`. This is a refactor of `Screen`:
6
6
  // the body is decomposed into the same focused pieces Compose uses — header
7
7
  // layer, scrollable content (`StackScreenContent`), floating button
8
- // (`FloatingContent`) and footer (`NavigationFooter`) — instead of one
8
+ // (`FloatingContent`) and footer (`FooterContent`) — instead of one
9
9
  // monolithic, partially-duplicated view. The public init matches `Screen`
10
10
  // so call sites (e.g. NavigationContainer's ScreenHost) can swap over.
11
11
  //
@@ -165,13 +165,12 @@ public struct StackScreen<Content: View>: View {
165
165
 
166
166
  FloatingContent(
167
167
  fabProps: fabProps,
168
- keyboardSize: keyboardSize,
169
- hasFooter: footer != nil,
168
+ keyboardOffset: 0,
170
169
  scrollOffset: scrollOffset
171
170
  )
172
171
  }
173
172
 
174
- NavigationFooter(footerComponent: footer, keyboardSize: keyboardSize)
173
+ FooterContent(footer: footer, keyboardSize: keyboardSize, bottomInset: bottomInset)
175
174
  }
176
175
 
177
176
  // Animated search bar overlay (mirrors Compose's SearchAnimated layer).
@@ -445,29 +444,17 @@ private struct SearchAnimatedHeader: View {
445
444
  /// Mirrors Compose's `FloatingContent`: no FAB props means nothing renders.
446
445
  private struct FloatingContent: View {
447
446
  let fabProps: FabProps?
448
- let keyboardSize: CGFloat
449
- /// With a footer the FAB is anchored to the footer's *layout* top, which does not move
450
- /// when the band offsets itself over the keyboard — so it rides the same offset.
451
- let hasFooter: Bool
447
+ let keyboardOffset: CGFloat
452
448
  let scrollOffset: CGFloat
453
449
 
454
- /// Compose keeps the FAB `Spacing.M + AppNavigationBar` above the bottom of the screen
455
- /// and lifts it with `imePadding()`; here it sits `Spacing.M` above the safe area and
456
- /// nothing lifts it, so the difference is applied as an offset (see NavigationFooter).
457
- private var verticalOffset: CGFloat {
458
- if hasFooter { return NavigationFooter.verticalOffset(keyboardSize: keyboardSize) }
459
- return NavigationFooter.safeAreaBottom - NavigationFooter.navigationBarInset - keyboardSize
460
- }
461
-
462
450
  var body: some View {
463
451
  if let fabProps = fabProps {
464
452
  FloatingButton(
465
453
  props: fabProps,
466
- keyboardOffset: 0,
454
+ keyboardOffset: keyboardOffset,
467
455
  scrollOffset: scrollOffset
468
456
  )
469
457
  .padding(.horizontal, Spacing.M)
470
- .offset(y: verticalOffset)
471
458
  .ignoresSafeArea(.keyboard, edges: .bottom)
472
459
  }
473
460
  }
@@ -496,3 +483,19 @@ private struct HideKeyboardOnTap: ViewModifier {
496
483
  }
497
484
  }
498
485
  }
486
+
487
+ // MARK: - Footer content
488
+
489
+ /// Mirrors Compose's `FooterContent`: renders the footer chrome only when a
490
+ /// footer component is supplied. Reuses the existing `Footer` view from Screen.swift.
491
+ private struct FooterContent: View {
492
+ let footer: (() -> AnyView)?
493
+ let keyboardSize: CGFloat
494
+ let bottomInset: CGFloat
495
+
496
+ var body: some View {
497
+ if let footerView = footer?() {
498
+ Footer(footerView: footerView, keyboardSize: keyboardSize, bottomInset: bottomInset)
499
+ }
500
+ }
501
+ }
@@ -174,7 +174,6 @@ public struct BadgeRibbon: View {
174
174
  )
175
175
  .frame(width: RibbonConstants.headTailWidth, height: RibbonConstants.headTailHeight)
176
176
  .rotationEffect(.degrees(180))
177
- .offset(x: 1)
178
177
  .clipped()
179
178
  }
180
179
 
@@ -184,7 +183,6 @@ public struct BadgeRibbon: View {
184
183
  placeholder: AnyView(Color.clear)
185
184
  )
186
185
  .frame(width: RibbonConstants.headTailWidth, height: RibbonConstants.headTailHeight)
187
- .offset(x: 1)
188
186
  .clipped()
189
187
  }
190
188
 
@@ -194,7 +192,6 @@ public struct BadgeRibbon: View {
194
192
  placeholder: AnyView(Color.clear)
195
193
  )
196
194
  .frame(width: RibbonConstants.skewTailWidth, height: RibbonConstants.skewTailHeight)
197
- .offset(x: -2)
198
195
  .clipped()
199
196
  }
200
197
  }
@@ -284,7 +281,7 @@ private struct RibbonConstants {
284
281
  static let ribbonHeight: CGFloat = 20
285
282
  static let roundHeight: CGFloat = 16
286
283
  static let skewBodyHeight: CGFloat = 16
287
- static let roundRightRadius: CGFloat = 8
284
+ static let roundRightRadius: CGFloat = 12
288
285
  static let roundPaddingEnd: CGFloat = 6
289
286
  static let skewTailWidth: CGFloat = 8
290
287
  static let skewTailHeight: CGFloat = 16
@@ -133,19 +133,19 @@ public struct Button: View {
133
133
  var action: () -> Void
134
134
  var type: ButtonType
135
135
  var size: ButtonSize
136
- var iconLeft: String
137
- var iconRight: String
136
+ var iconLeft: AnyView?
137
+ var iconRight: AnyView?
138
138
  var isFull: Bool
139
139
  var loading: Bool
140
140
  var useTintColor: Bool
141
141
 
142
142
  public init(
143
- title: String = "Button",
143
+ title: String = "",
144
144
  action: @escaping () -> Void,
145
145
  type: ButtonType = .primary,
146
146
  size: ButtonSize = .large,
147
- iconLeft: String = "",
148
- iconRight: String = "",
147
+ iconLeft: AnyView? = nil,
148
+ iconRight: AnyView? = nil,
149
149
  isFull: Bool = true,
150
150
  loading: Bool = false,
151
151
  useTintColor: Bool = true
@@ -161,9 +161,38 @@ public struct Button: View {
161
161
  self.useTintColor = useTintColor
162
162
  }
163
163
 
164
- private func shouldLoadingOnLeft(_ left: String, _ right: String) -> Bool {
165
- let hasLeft = !left.isEmpty
166
- let hasRight = !right.isEmpty
164
+ /// Mirrors Kotlin's `Button(iconLeft: String, iconRight: String, ...)`, which resolves
165
+ /// icons by design-system source name through the shared `Icon` component rather than
166
+ /// a caller-supplied view. `iconLeft` is required here (unlike Kotlin's `= ""` default)
167
+ /// solely to keep this initializer unambiguous alongside the `AnyView`-based one above —
168
+ /// pass `""` explicitly for an icon-right-only button.
169
+ public init(
170
+ title: String = "Button",
171
+ action: @escaping () -> Void,
172
+ type: ButtonType = .primary,
173
+ size: ButtonSize = .large,
174
+ iconLeft: String,
175
+ iconRight: String = "",
176
+ isFull: Bool = true,
177
+ loading: Bool = false,
178
+ useTintColor: Bool = true
179
+ ) {
180
+ self.init(
181
+ title: title,
182
+ action: action,
183
+ type: type,
184
+ size: size,
185
+ iconLeft: iconLeft.isEmpty ? nil : AnyView(Icon(source: iconLeft)),
186
+ iconRight: iconRight.isEmpty ? nil : AnyView(Icon(source: iconRight)),
187
+ isFull: isFull,
188
+ loading: loading,
189
+ useTintColor: useTintColor
190
+ )
191
+ }
192
+
193
+ private func shouldLoadingOnLeft(_ left: AnyView?, _ right: AnyView?) -> Bool {
194
+ let hasLeft = left != nil
195
+ let hasRight = right != nil
167
196
 
168
197
  switch (hasLeft, hasRight) {
169
198
  case (false, false): return true
@@ -188,13 +217,27 @@ public struct Button: View {
188
217
  }
189
218
  }) {
190
219
  HStack(spacing: size.iconSpacing) {
191
- renderIcon(icon: iconLeft, forceLoading: loading && loadingOnLeft, textColor: fg)
220
+ if loading && loadingOnLeft {
221
+ LottieView(name: "lottie_circle_loader", loopMode: .loop)
222
+ .frame(width: size.iconSize, height: size.iconSize)
223
+ .colorMultiply(fg)
224
+
225
+ } else if let iconLeft = iconLeft {
226
+ tintedIcon(iconLeft, color: fg)
227
+ }
192
228
 
193
229
  MomoText(title, typography: size.typographyStyle, color: fg)
194
230
  .lineLimit(1)
195
231
  .truncationMode(.tail)
196
232
 
197
- renderIcon(icon: iconRight, forceLoading: loading && !loadingOnLeft, textColor: fg)
233
+ if loading && !loadingOnLeft {
234
+ LottieView(name: "lottie_circle_loader", loopMode: .loop)
235
+ .frame(width: size.iconSize, height: size.iconSize)
236
+ .colorMultiply(fg)
237
+
238
+ } else if let iconRight = iconRight {
239
+ tintedIcon(iconRight, color: fg)
240
+ }
198
241
  }
199
242
  .frame(maxWidth: isFull ? .infinity : nil)
200
243
  .padding(.horizontal, size.padding)
@@ -206,24 +249,20 @@ public struct Button: View {
206
249
  border: border,
207
250
  borderWidth: borderWidth,
208
251
  radius: size.radius,
209
- height: size.height,
210
- isFull: isFull
252
+ height: size.height
211
253
  ))
212
254
  .disabled(!isEnabled)
213
255
  }
214
256
 
215
- /// Mirrors Kotlin `RenderIcon`: the loader replaces the icon slot entirely, and
216
- /// `useTintColor == false` leaves the icon at its own colors.
217
257
  @ViewBuilder
218
- private func renderIcon(icon: String, forceLoading: Bool, textColor: Color) -> some View {
219
- let color: Color? = useTintColor ? textColor : nil
220
-
221
- if forceLoading {
222
- LottieView(name: "lottie_circle_loader", loopMode: .loop)
258
+ private func tintedIcon(_ icon: AnyView, color: Color) -> some View {
259
+ if useTintColor {
260
+ icon
261
+ .frame(width: size.iconSize, height: size.iconSize)
262
+ .colorMultiply(color)
263
+ } else {
264
+ icon
223
265
  .frame(width: size.iconSize, height: size.iconSize)
224
- .colorMultiply(color ?? Colors.black01)
225
- } else if !icon.isEmpty {
226
- Icon(source: icon, size: size.iconSize, color: color)
227
266
  }
228
267
  }
229
268
  }
@@ -238,13 +277,11 @@ private struct ButtonPressFeedbackStyle: ButtonStyle {
238
277
  let borderWidth: CGFloat
239
278
  let radius: CGFloat
240
279
  let height: CGFloat
241
- let isFull: Bool
242
280
 
243
281
  func makeBody(configuration: Configuration) -> some View {
244
282
  let pressed = configuration.isPressed && isEnabled
245
283
 
246
284
  configuration.label
247
- .modifier(HugContentOnLegacyOS(isFull: isFull))
248
285
  .opacity(pressed ? 0.5 : 1)
249
286
  .background(
250
287
  RoundedRectangle(cornerRadius: radius)
@@ -260,20 +297,3 @@ private struct ButtonPressFeedbackStyle: ButtonStyle {
260
297
  .animation(.easeInOut(duration: 0.1), value: pressed)
261
298
  }
262
299
  }
263
-
264
- /// `ButtonStyleConfiguration.Label` is greedy on iOS 16 and earlier — it accepts
265
- /// the full proposed width, so the background drawn in `makeBody` stretches past
266
- /// the content even when `isFull == false`. iOS 17 fixed the label to hug its
267
- /// content, so only legacy OSes need the `fixedSize` clamp (kept off on 17+ to
268
- /// preserve tail truncation of long titles).
269
- private struct HugContentOnLegacyOS: ViewModifier {
270
- let isFull: Bool
271
-
272
- func body(content: Content) -> some View {
273
- if #available(iOS 17.0, *) {
274
- content
275
- } else {
276
- content.fixedSize(horizontal: !isFull, vertical: false)
277
- }
278
- }
279
- }
@@ -26,24 +26,17 @@ let iconSources: [String: IconSource] = loadIconSources()
26
26
  // Icons that must never be tinted (mirror Compose `noThemeIcons`)
27
27
  let noThemeIcons: [String] = ["ic_round_momo_tiny"]
28
28
 
29
- // Compose defaults `color` to the theme text color while `color = null` means "no tint".
30
- // Swift can't tell an omitted argument from an explicit `nil`, so the default is this
31
- // unrepresentable sentinel: omitted -> theme text color, `nil` -> untinted.
32
- public let iconThemeColor = Color(.sRGB, red: -1, green: -1, blue: -1, opacity: -1)
33
-
34
29
  // Icon ViewModel
35
30
  public struct Icon: View {
36
- @Environment(\.appTheme) private var theme
37
-
38
31
  var source: String
39
32
  var size: CGFloat
40
33
  var color: Color?
41
34
  var accessibilityLabel: String?
42
-
35
+
43
36
  public init(
44
37
  source: String,
45
38
  size: CGFloat = 24,
46
- color: Color? = iconThemeColor,
39
+ color: Color? = nil,
47
40
  accessibilityLabel: String? = nil
48
41
  ) {
49
42
  self.source = source
@@ -53,11 +46,9 @@ public struct Icon: View {
53
46
  }
54
47
 
55
48
  public var body: some View {
56
- // Mirror Compose: an https source is used as-is, anything else is a key into icon.json
57
- if let uri = source.contains("https") ? source : iconSources[source]?.uri {
58
- let requestedColor = color == iconThemeColor ? theme.colors.text.default : color
49
+ if let uri = iconSources[source]?.uri {
59
50
  // Mirror Compose: certain icons (e.g. branded glyphs) are never tinted
60
- let resolvedColor = noThemeIcons.contains(source) ? nil : requestedColor
51
+ let resolvedColor = noThemeIcons.contains(source) ? nil : color
61
52
  ImageView(uri, color: resolvedColor)
62
53
  .frame(width: size, height: size)
63
54
  // Mirrors Compose's auto-generated `contentDescription = "img|$iconUrl"`
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'MoMoUIKits'
3
- s.version = '0.163.1-sp.8'
3
+ s.version = '0.163.2-beta.1'
4
4
  s.summary = 'MoMoUIKits for iOS'
5
5
  s.homepage = 'https://momo.vn'
6
6
  s.license = { :type => 'MIT' }
@@ -34,7 +34,6 @@ Pod::Spec.new do |s|
34
34
  "InputDropDown/**/*.{swift,m,h}",
35
35
  "InputMoney/**/*.{swift,m,h}",
36
36
  "InputOTP/**/*.{swift,m,h}",
37
- "Layout/**/*.{swift,m,h}",
38
37
  "Loader/**/*.{swift,m,h}",
39
38
  "Lottie/**/*.{swift,m,h}",
40
39
  "OTPKeyboard/**/*.{swift,m,h}",
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'MoMoUIKitsDemo'
3
- s.version = '0.163.1-sp.8'
3
+ s.version = '0.163.2-beta.1'
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
+ ```