@momo-kits/native-kits 0.163.1-beta.8-debug → 0.163.1-beta.9-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 (220) hide show
  1. package/CLAUDE.md +1 -1
  2. package/compose/build.gradle.kts +1 -1
  3. package/compose/compose.podspec +1 -1
  4. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
  5. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +30 -3
  6. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +5 -0
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +1 -1
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +3 -0
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +4 -0
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +8 -2
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +63 -0
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +32 -19
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +49 -0
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +1 -1
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +1 -1
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +3 -1
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +93 -75
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +26 -7
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +3 -1
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +23 -2
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +11 -0
  25. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
  26. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +26 -3
  27. package/gradle.properties +1 -1
  28. package/ios/Application/ApplicationEnvironment.swift +15 -2
  29. package/ios/Application/Components.swift +50 -44
  30. package/ios/Application/FloatingButton.swift +10 -9
  31. package/ios/Application/HeaderRight.swift +79 -45
  32. package/ios/Application/Localize.swift +277 -0
  33. package/ios/Application/MaxApi.swift +18 -0
  34. package/ios/Application/Navigation/BottomTab/BottomTab.swift +104 -0
  35. package/ios/Application/Navigation/BottomTab/BottomTabBar.swift +215 -0
  36. package/ios/Application/Navigation/BottomTab/CurvedContainer.swift +65 -0
  37. package/ios/Application/Navigation/HeaderBackProps.swift +66 -0
  38. package/ios/Application/Navigation/HeaderTitle.swift +57 -0
  39. package/ios/Application/Navigation/HeaderUser.swift +209 -0
  40. package/ios/Application/Navigation/NavigationContainer.swift +200 -0
  41. package/ios/Application/Navigation/NavigationOptions.swift +109 -0
  42. package/ios/Application/Navigation/Navigator.swift +385 -0
  43. package/ios/Application/Navigation/Overplay/BottomSheet.swift +256 -0
  44. package/ios/Application/Navigation/Overplay/ModalScreen.swift +74 -0
  45. package/ios/Application/Navigation/Overplay/SnackBar.swift +144 -0
  46. package/ios/Application/Navigation/Tracking/ScreenTracker.swift +117 -0
  47. package/ios/Application/Navigation/component/Header.swift +178 -0
  48. package/ios/Application/Navigation/component/HeaderBackground.swift +83 -0
  49. package/ios/Application/Navigation/component/HeaderColor.swift +65 -0
  50. package/ios/Application/Screen.swift +2 -1
  51. package/ios/Application/StackScreen.swift +501 -0
  52. package/ios/Avatar/Avatar.swift +193 -0
  53. package/ios/Badge/Badge.swift +11 -14
  54. package/ios/Badge/BadgeRibbon.swift +92 -21
  55. package/ios/BaselineView/BaselineView.swift +163 -0
  56. package/ios/Button/Button.swift +124 -29
  57. package/ios/Carousel/Carousel.swift +196 -0
  58. package/ios/Checkbox/Checkbox.swift +23 -28
  59. package/ios/Chip/Chip.swift +56 -33
  60. package/ios/Collapse/Collapse.swift +192 -0
  61. package/ios/Colors+Radius+Spacing/Colors.swift +20 -17
  62. package/ios/Colors+Radius+Spacing/Spacing.swift +3 -1
  63. package/ios/Colors+Radius+Spacing/Theme.swift +295 -0
  64. package/ios/DateTimePicker/DateTimePicker.swift +212 -0
  65. package/ios/DateTimePicker/DateTimePickerTypes.swift +55 -0
  66. package/ios/DateTimePicker/DateTimePickerUtils.swift +167 -0
  67. package/ios/DateTimePicker/WheelPicker.swift +165 -0
  68. package/ios/Divider/Divider.swift +16 -0
  69. package/ios/Extensions/ActiveOpacityButtonStyle.swift +14 -0
  70. package/ios/Extensions/Color++.swift +32 -0
  71. package/ios/Icon/Icon.swift +10 -1
  72. package/ios/IconButton/IconButton.swift +140 -0
  73. package/ios/Image/Image.swift +16 -3
  74. package/ios/Information/Information.swift +3 -1
  75. package/ios/Input/ErrorView.swift +3 -1
  76. package/ios/Input/Input.swift +72 -33
  77. package/ios/Input/InputPhoneNumber.swift +33 -21
  78. package/ios/Input/InputSearch.swift +107 -48
  79. package/ios/Input/InputTextArea.swift +134 -64
  80. package/ios/InputDropDown/InputDropDown.swift +201 -0
  81. package/ios/InputMoney/InputMoney.swift +347 -0
  82. package/ios/InputOTP/InputOTP.swift +210 -0
  83. package/ios/Loader/Loader.swift +113 -0
  84. package/ios/MoMoUIKits.podspec +5 -0
  85. package/ios/Pagination/PaginationDot.swift +61 -0
  86. package/ios/Pagination/PaginationNumber.swift +35 -0
  87. package/ios/Pagination/PaginationScroll.swift +101 -0
  88. package/ios/Pagination/PaginationWhiteDot.swift +37 -0
  89. package/ios/Popup/PopupDisplay.swift +11 -20
  90. package/ios/Popup/PopupInput.swift +2 -8
  91. package/ios/Popup/PopupNotify.swift +218 -0
  92. package/ios/Popup/PopupPromotion.swift +16 -8
  93. package/ios/ProgressInfo/ProgressInfo.swift +296 -0
  94. package/ios/Radio/Radio.swift +69 -0
  95. package/ios/Rating/Rating.swift +82 -0
  96. package/ios/Resources/MoMoUIKitsColors.xcassets/AccentColor.colorset/Contents.json +11 -0
  97. package/ios/Resources/MoMoUIKitsColors.xcassets/Background.colorset/Contents.json +38 -0
  98. package/ios/Resources/MoMoUIKitsColors.xcassets/Border.colorset/Contents.json +38 -0
  99. package/ios/Resources/MoMoUIKitsColors.xcassets/Card.colorset/Contents.json +38 -0
  100. package/ios/Resources/MoMoUIKitsColors.xcassets/Contents.json +6 -0
  101. package/ios/Resources/MoMoUIKitsColors.xcassets/Error.colorset/Contents.json +38 -0
  102. package/ios/Resources/MoMoUIKitsColors.xcassets/Primary.colorset/Contents.json +38 -0
  103. package/ios/Resources/MoMoUIKitsColors.xcassets/PrimaryDark.colorset/Contents.json +38 -0
  104. package/ios/Resources/MoMoUIKitsColors.xcassets/PrimaryLight.colorset/Contents.json +38 -0
  105. package/ios/Resources/MoMoUIKitsColors.xcassets/Secondary.colorset/Contents.json +38 -0
  106. package/ios/Resources/MoMoUIKitsColors.xcassets/Success.colorset/Contents.json +38 -0
  107. package/ios/Resources/MoMoUIKitsColors.xcassets/Text.colorset/Contents.json +38 -0
  108. package/ios/Resources/MoMoUIKitsColors.xcassets/TextSecondary.colorset/Contents.json +38 -0
  109. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/Contents.json +9 -0
  110. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-default.colorset/Contents.json +38 -0
  111. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-disable.colorset/Contents.json +38 -0
  112. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-pressed.colorset/Contents.json +38 -0
  113. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-selected.colorset/Contents.json +38 -0
  114. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-surface.colorset/Contents.json +38 -0
  115. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-tonal.colorset/Contents.json +38 -0
  116. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/border-default.colorset/Contents.json +38 -0
  117. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/border-disable.colorset/Contents.json +38 -0
  118. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/gradient.colorset/Contents.json +38 -0
  119. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/primary.colorset/Contents.json +38 -0
  120. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/secondary.colorset/Contents.json +38 -0
  121. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/success-container.colorset/Contents.json +38 -0
  122. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/success-primary.colorset/Contents.json +38 -0
  123. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/success-secondary.colorset/Contents.json +38 -0
  124. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/text-default.colorset/Contents.json +38 -0
  125. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/text-disable.colorset/Contents.json +38 -0
  126. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/text-hint.colorset/Contents.json +38 -0
  127. package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/text-secondary.colorset/Contents.json +38 -0
  128. package/ios/Resources/MoMoUIKitsColors.xcassets/Warning.colorset/Contents.json +38 -0
  129. package/ios/Skeleton/Skeleton.swift +99 -0
  130. package/ios/Slider/Slider.swift +237 -0
  131. package/ios/Stepper/Stepper.swift +220 -0
  132. package/ios/Steps/Steps.swift +467 -0
  133. package/ios/SuggestAction/SuggestAction.swift +99 -0
  134. package/ios/Swipeable/SwipeCell.swift +187 -0
  135. package/ios/Swipeable/SwipeCellModel.swift +21 -0
  136. package/ios/Switch/Switch.swift +53 -28
  137. package/ios/TabView/TabView.swift +534 -0
  138. package/ios/Tag/Tag.swift +98 -0
  139. package/ios/Template/TrustBanner/TrustBanner.swift +6 -7
  140. package/ios/Title/Title.swift +269 -0
  141. package/ios/Tooltip/Tooltip.swift +485 -0
  142. package/ios/Typography/FontScaleStore.swift +19 -0
  143. package/ios/Typography/Text.swift +79 -5
  144. package/ios/Typography/Typography.swift +3 -0
  145. package/ios/Uploader/Uploader.swift +174 -0
  146. package/ios/native-kits.podspec +61 -8
  147. package/ios-demo/MoMoUIKitsDemo.podspec +20 -0
  148. package/ios-demo/README.md +120 -0
  149. package/ios-demo/Resources/dot_loading.json +558 -0
  150. package/ios-demo/Resources/icon.json +4052 -0
  151. package/ios-demo/Resources/lottie_circle_loader.json +1 -0
  152. package/ios-demo/Sources/MoMoUIKitsDemo/DemoScaffold.swift +48 -0
  153. package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsBadgeDemoView.swift +25 -0
  154. package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsButtonDemoView.swift +26 -0
  155. package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsCheckboxDemoView.swift +19 -0
  156. package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsChipDemoView.swift +33 -0
  157. package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsInputDemoView.swift +48 -0
  158. package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsPopupDemoView.swift +21 -0
  159. package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsRadioDemoView.swift +17 -0
  160. package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsTypographyDemoView.swift +28 -0
  161. package/ios-demo/Sources/MoMoUIKitsDemo/MoMoUIKitsDemoContext.swift +31 -0
  162. package/ios-demo/Sources/MoMoUIKitsDemo/MoMoUIKitsDemoFactory.swift +24 -0
  163. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/AnimatedHeaderDemo.swift +110 -0
  164. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/AvatarDemo.swift +93 -0
  165. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BadgeDemo.swift +269 -0
  166. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BaselineView.swift +117 -0
  167. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BottomTabDemo.swift +89 -0
  168. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ButtonDemo.swift +100 -0
  169. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CarouselDemo.swift +126 -0
  170. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CheckboxDemo.swift +63 -0
  171. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ChipDemo.swift +20 -0
  172. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CollapseDemo.swift +93 -0
  173. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +107 -0
  174. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DateTimePickerDemo.swift +124 -0
  175. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DividerDemo.swift +14 -0
  176. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/HeaderUserDemo.swift +49 -0
  177. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +279 -0
  178. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IconButtonDemo.swift +58 -0
  179. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ImageDemo.swift +78 -0
  180. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InformationDemo.swift +138 -0
  181. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDemo.swift +111 -0
  182. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDropDownDemo.swift +136 -0
  183. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputMoneyDemo.swift +107 -0
  184. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputOTPDemo.swift +73 -0
  185. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputPhoneNumberDemo.swift +100 -0
  186. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputSearchDemo.swift +32 -0
  187. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputTextAreaDemo.swift +57 -0
  188. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/LoaderDemo.swift +70 -0
  189. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/LocalizeDemo.swift +188 -0
  190. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +229 -0
  191. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PaginationDemo.swift +72 -0
  192. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +175 -0
  193. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +121 -0
  194. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +76 -0
  195. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ProgressInfoDemo.swift +89 -0
  196. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RadioDemo.swift +32 -0
  197. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RatingDemo.swift +56 -0
  198. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SkeletonDemo.swift +32 -0
  199. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SliderDemo.swift +150 -0
  200. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SnackBarDemo.swift +138 -0
  201. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepperDemo.swift +103 -0
  202. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepsDemo.swift +94 -0
  203. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SuggestActionDemo.swift +79 -0
  204. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwipeDemo.swift +152 -0
  205. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwitchDemo.swift +33 -0
  206. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TabViewDemo.swift +190 -0
  207. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TagDemo.swift +34 -0
  208. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ThemeDemo.swift +182 -0
  209. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TitleDemo.swift +109 -0
  210. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TooltipDemo.swift +187 -0
  211. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TypographyDemo.swift +87 -0
  212. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/UploaderDemo.swift +131 -0
  213. package/ios-demo/Sources/MoMoUIKitsDemo/SwiftUIDemoBrowserView.swift +51 -0
  214. package/ios-demo/Sources/MoMoUIKitsDemo/UIKitsDemoModels.swift +15 -0
  215. package/ios-demo/Sources/MoMoUIKitsDemo/UIKitsDemoRegistry.swift +73 -0
  216. package/local.properties +2 -2
  217. package/package.json +1 -1
  218. package/publish.sh +53 -0
  219. package/scripts/gen-ios-color.mjs +136 -0
  220. package/.claude/settings.local.json +0 -62
@@ -0,0 +1,385 @@
1
+ //
2
+ // Navigator.swift
3
+ // MoMoUIKits
4
+ //
5
+ // SwiftUI mirror of Compose's Navigator + DynamicScreenRegistry.
6
+ // Screens are registered by Int id; the NavigationStack path stores routes
7
+ // by id and looks up content from the registry on render.
8
+ //
9
+
10
+ import SwiftUI
11
+ import Combine
12
+
13
+ // MARK: - Routes
14
+
15
+ public struct DynamicScreenRoute: Hashable, Identifiable {
16
+ public let id: Int
17
+ }
18
+
19
+ public struct DynamicDialogRoute: Hashable, Identifiable {
20
+ public let id: Int
21
+ }
22
+
23
+ // MARK: - Registry
24
+
25
+ public struct DynamicScreen {
26
+ public let id: Int
27
+ public let name: String
28
+ public let content: () -> AnyView
29
+ public var options: NavigationOptions?
30
+ }
31
+
32
+ public final class DynamicScreenRegistry {
33
+ private var screens: [Int: DynamicScreen] = [:]
34
+ private var idCounter: Int = 1
35
+
36
+ public init() {}
37
+
38
+ @discardableResult
39
+ public func register(
40
+ screenName: String = "",
41
+ content: @escaping () -> AnyView,
42
+ options: NavigationOptions? = nil
43
+ ) -> DynamicScreenRoute {
44
+ let id = idCounter
45
+ idCounter += 1
46
+ screens[id] = DynamicScreen(id: id, name: screenName, content: content, options: options)
47
+ return DynamicScreenRoute(id: id)
48
+ }
49
+
50
+ public func unregister(id: Int) {
51
+ screens.removeValue(forKey: id)
52
+ }
53
+
54
+ public func unregisterAll() {
55
+ screens.removeAll()
56
+ }
57
+
58
+ public func getScreen(id: Int) -> DynamicScreen? { screens[id] }
59
+
60
+ public func latest() -> DynamicScreen? {
61
+ screens.max(by: { $0.key < $1.key })?.value
62
+ }
63
+
64
+ public func setOptions(id: Int, options: NavigationOptions) {
65
+ screens[id]?.options = options
66
+ }
67
+ }
68
+
69
+ // MARK: - Overplay
70
+
71
+ public enum OverplayType {
72
+ case modal, bottomSheet, snackBar
73
+ }
74
+
75
+ public struct OverplayItem: Identifiable {
76
+ public let id = UUID()
77
+ public let type: OverplayType
78
+ public let content: () -> AnyView
79
+ public let barrierDismissible: Bool
80
+ public let onDismiss: (() -> Void)?
81
+ // Bottom-sheet params (mirrors Compose's OverplayComponentParams.BottomSheet).
82
+ public var isSurface: Bool = false
83
+ public var header: BottomSheetHeader? = nil
84
+ // Snack-bar payload (mirrors Compose's SnackBar value).
85
+ public var snackBar: SnackBar? = nil
86
+ }
87
+
88
+ // MARK: - Navigator
89
+
90
+ @available(iOS 16.0, *)
91
+ public final class Navigator: ObservableObject {
92
+ @Published public var path: [DynamicScreenRoute] = []
93
+ @Published public var presented: DynamicDialogRoute?
94
+ @Published public var overplay: OverplayItem?
95
+
96
+ /// Owned per-Navigator (mirrors Compose's `remember { DynamicScreenRegistry() }` +
97
+ /// disposal): screens registered by this navigator are released via ARC when the
98
+ /// navigator itself deallocates, instead of accumulating in a process-wide singleton.
99
+ public let registry = DynamicScreenRegistry()
100
+
101
+ /// Animated-close handler registered by the active overplay component
102
+ /// (mirrors Compose's `OverplayComponentRegistry.bindClose`). When set, a
103
+ /// dismiss request runs the component's exit animation before the item is
104
+ /// actually removed via `finishOverplayDismiss`.
105
+ private var overplayDismissHandler: (() -> Void)?
106
+
107
+ public private(set) var rootRoute: DynamicScreenRoute
108
+ public var composeApi: KitComposeApi?
109
+
110
+ public init(
111
+ initialScreenName: String = "",
112
+ initialContent: @escaping () -> AnyView,
113
+ options: NavigationOptions? = nil,
114
+ composeApi: KitComposeApi? = nil
115
+ ) {
116
+ self.composeApi = composeApi
117
+ self.rootRoute = registry.register(
118
+ screenName: initialScreenName,
119
+ content: initialContent,
120
+ options: options
121
+ )
122
+ }
123
+
124
+ // MARK: Stack
125
+
126
+ public func push<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
127
+ let route = registry.register(
128
+ screenName: options?.screenName ?? "",
129
+ content: { AnyView(content()) },
130
+ options: options
131
+ )
132
+ path.append(route)
133
+ }
134
+
135
+ public func push<V: View>(
136
+ screenName: String,
137
+ @ViewBuilder _ content: @escaping () -> V,
138
+ options: NavigationOptions? = nil
139
+ ) {
140
+ let route = registry.register(
141
+ screenName: screenName,
142
+ content: { AnyView(content()) },
143
+ options: options
144
+ )
145
+ path.append(route)
146
+ }
147
+
148
+ public func replace<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
149
+ guard !path.isEmpty else { return }
150
+ let removed = path.removeLast()
151
+ registry.unregister(id: removed.id)
152
+ push(content, options: options)
153
+ }
154
+
155
+ public func replace<V: View>(
156
+ screenName: String,
157
+ @ViewBuilder _ content: @escaping () -> V,
158
+ options: NavigationOptions? = nil
159
+ ) {
160
+ guard !path.isEmpty else { return }
161
+ let removed = path.removeLast()
162
+ registry.unregister(id: removed.id)
163
+ push(screenName: screenName, content, options: options)
164
+ }
165
+
166
+ public func reset<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
167
+ for route in path { registry.unregister(id: route.id) }
168
+ registry.unregister(id: rootRoute.id)
169
+ path.removeAll()
170
+ let route = registry.register(
171
+ screenName: options?.screenName ?? "",
172
+ content: { AnyView(content()) },
173
+ options: options
174
+ )
175
+ rootRoute = route
176
+ }
177
+
178
+ public func reset<V: View>(
179
+ screenName: String,
180
+ @ViewBuilder _ content: @escaping () -> V,
181
+ options: NavigationOptions? = nil
182
+ ) {
183
+ for route in path { registry.unregister(id: route.id) }
184
+ registry.unregister(id: rootRoute.id)
185
+ path.removeAll()
186
+ let route = registry.register(
187
+ screenName: screenName,
188
+ content: { AnyView(content()) },
189
+ options: options
190
+ )
191
+ rootRoute = route
192
+ }
193
+
194
+ public func pop(_ count: Int = 1, callback: (() -> Void)? = nil) {
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
208
+ }
209
+ remaining -= 1
210
+ }
211
+ callback?()
212
+ }
213
+
214
+ /// Pops the topmost presented screen or stack entry. Returns `false` (and requests
215
+ /// a host dismiss) when there is nothing left to pop.
216
+ @discardableResult
217
+ private func popScreen() -> Bool {
218
+ if presented != nil {
219
+ let dismissedId = presented?.id
220
+ presented = nil
221
+ if let id = dismissedId { registry.unregister(id: id) }
222
+ return true
223
+ } else if !path.isEmpty {
224
+ let removed = path.removeLast()
225
+ registry.unregister(id: removed.id)
226
+ return true
227
+ } else {
228
+ composeApi?.requestCallback(funcName: "dismiss", params: nil, onResponse: nil)
229
+ return false
230
+ }
231
+ }
232
+
233
+ public func popToRoot() {
234
+ for route in path { registry.unregister(id: route.id) }
235
+ path.removeAll()
236
+ }
237
+
238
+ public func onBackSafe(callback: (() -> Void)? = nil) {
239
+ let latest = registry.latest()
240
+ if let onBack = latest?.options?.onBackHandler {
241
+ onBack()
242
+ return
243
+ }
244
+ if let preventBack = latest?.options?.headerBackProps?.preventBack {
245
+ showModal {
246
+ PopupDisplay(
247
+ isPresented: .constant(true),
248
+ title: preventBack.title,
249
+ description: preventBack.description,
250
+ url: preventBack.image,
251
+ buttonDirection: preventBack.buttonDirection,
252
+ buttonType: .button,
253
+ actionButtonTitle: preventBack.primary?.title ?? "",
254
+ closeButtonTitle: preventBack.secondary?.title ?? "",
255
+ onPressAction: { [weak self] in
256
+ preventBack.primary?.onPress?()
257
+ // Close the popup, then leave the screen (matches Compose's pop(2)).
258
+ self?.pop(2, callback: callback)
259
+ },
260
+ onPressCloseButton: { [weak self] in
261
+ preventBack.secondary?.onPress?()
262
+ self?.hideOverplay()
263
+ },
264
+ onClose: { [weak self] in
265
+ preventBack.onIconClose?()
266
+ self?.hideOverplay()
267
+ },
268
+ errorCode: preventBack.error,
269
+ isShowCloseIcon: preventBack.isShowCloseIcon
270
+ )
271
+ }
272
+ return
273
+ }
274
+ pop(1, callback: callback)
275
+ }
276
+
277
+ // MARK: Modal-style screen (vertical slide)
278
+
279
+ public func present<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
280
+ let route = registry.register(
281
+ screenName: options?.screenName ?? "",
282
+ content: { AnyView(content()) },
283
+ options: options
284
+ )
285
+ presented = DynamicDialogRoute(id: route.id)
286
+ }
287
+
288
+ public func present<V: View>(
289
+ screenName: String,
290
+ @ViewBuilder _ content: @escaping () -> V,
291
+ options: NavigationOptions? = nil
292
+ ) {
293
+ let route = registry.register(
294
+ screenName: screenName,
295
+ content: { AnyView(content()) },
296
+ options: options
297
+ )
298
+ presented = DynamicDialogRoute(id: route.id)
299
+ }
300
+
301
+ public func dismiss() {
302
+ if let id = presented?.id {
303
+ registry.unregister(id: id)
304
+ }
305
+ presented = nil
306
+ }
307
+
308
+ // MARK: Overplay
309
+
310
+ public func showModal<V: View>(
311
+ @ViewBuilder _ content: @escaping () -> V,
312
+ barrierDismissible: Bool = true,
313
+ onDismiss: (() -> Void)? = nil
314
+ ) {
315
+ overplayDismissHandler = nil
316
+ overplay = OverplayItem(
317
+ type: .modal,
318
+ content: { AnyView(content()) },
319
+ barrierDismissible: barrierDismissible,
320
+ onDismiss: onDismiss
321
+ )
322
+ }
323
+
324
+ public func showBottomSheet<V: View>(
325
+ @ViewBuilder _ content: @escaping () -> V,
326
+ isSurface: Bool = false,
327
+ barrierDismissible: Bool = true,
328
+ header: BottomSheetHeader? = nil,
329
+ onDismiss: (() -> Void)? = nil
330
+ ) {
331
+ overplayDismissHandler = nil
332
+ overplay = OverplayItem(
333
+ type: .bottomSheet,
334
+ content: { AnyView(content()) },
335
+ barrierDismissible: barrierDismissible,
336
+ onDismiss: onDismiss,
337
+ isSurface: isSurface,
338
+ header: header
339
+ )
340
+ }
341
+
342
+ /// Mirrors Compose's `showSnackBar(snackBar:)`.
343
+ public func showSnackBar(_ snackBar: SnackBar, onDismiss: (() -> Void)? = nil) {
344
+ overplayDismissHandler = nil
345
+ overplay = OverplayItem(
346
+ type: .snackBar,
347
+ content: { AnyView(EmptyView()) },
348
+ barrierDismissible: false,
349
+ onDismiss: onDismiss,
350
+ snackBar: snackBar
351
+ )
352
+ }
353
+
354
+ // MARK: Overplay dismissal
355
+
356
+ /// Registered by the active overplay component so external dismiss requests
357
+ /// run its exit animation (mirrors Compose's `bindClose`).
358
+ public func registerOverplayDismiss(_ handler: @escaping () -> Void) {
359
+ overplayDismissHandler = handler
360
+ }
361
+
362
+ /// Removes the overplay and fires its `onDismiss` (mirrors Compose's
363
+ /// `hardClearAfterDismiss`). Called by a component once its exit animation
364
+ /// has completed.
365
+ public func finishOverplayDismiss() {
366
+ let dismissCallback = overplay?.onDismiss
367
+ overplayDismissHandler = nil
368
+ overplay = nil
369
+ dismissCallback?()
370
+ }
371
+
372
+ public func hideOverplay() {
373
+ if let close = overplayDismissHandler {
374
+ // Component runs its exit animation, then calls finishOverplayDismiss.
375
+ overplayDismissHandler = nil
376
+ close()
377
+ } else {
378
+ finishOverplayDismiss()
379
+ }
380
+ }
381
+
382
+ public func hideSnackBar() {
383
+ if overplay?.type == .snackBar { hideOverplay() }
384
+ }
385
+ }
@@ -0,0 +1,256 @@
1
+ //
2
+ // BottomSheet.swift
3
+ // MoMoUIKits
4
+ //
5
+ // SwiftUI mirror of Compose's `BottomSheet` (navigation/BottomSheet.kt).
6
+ // A sheet that slides up from the bottom over a dimmed scrim, with a drag
7
+ // handle, a header (title or custom), a divider and the content body.
8
+ // Dragging down past a threshold (or tapping the scrim) dismisses it.
9
+ // Driven by `Navigator.showBottomSheet`.
10
+ //
11
+
12
+ import SwiftUI
13
+
14
+ // MARK: - Header model
15
+
16
+ /// Mirrors Compose's `sealed class BottomHeader` (Title / Custom).
17
+ public struct BottomSheetHeader {
18
+ enum Style {
19
+ case title(String)
20
+ case custom(() -> AnyView)
21
+ }
22
+
23
+ let style: Style
24
+
25
+ /// Mirrors `BottomHeader.Title`.
26
+ public static func title(_ text: String = "Bottom Sheet Title") -> BottomSheetHeader {
27
+ BottomSheetHeader(style: .title(text))
28
+ }
29
+
30
+ /// Mirrors `BottomHeader.Custom`.
31
+ public static func custom<V: View>(@ViewBuilder _ content: @escaping () -> V) -> BottomSheetHeader {
32
+ BottomSheetHeader(style: .custom({ AnyView(content()) }))
33
+ }
34
+ }
35
+
36
+ // MARK: - Bottom sheet view
37
+
38
+ @available(iOS 16.0, *)
39
+ struct BottomSheetView<Content: View>: View {
40
+ let header: BottomSheetHeader
41
+ let isSurface: Bool
42
+ let barrierDismissible: Bool
43
+ let content: () -> Content
44
+
45
+ @EnvironmentObject private var navigator: Navigator
46
+ @Environment(\.appTheme) private var theme
47
+ @Environment(\.applicationEnvironment) private var appEnv
48
+
49
+ /// Vertical translation of the sheet; starts off-screen and animates to 0.
50
+ @State private var sheetOffset: CGFloat = UIScreen.main.bounds.height
51
+ /// Mirrors Compose's `backgroundAlpha`: only touched by open()/close(), never by drag.
52
+ @State private var backgroundAlpha: CGFloat = 0
53
+ @State private var sheetHeight: CGFloat = 0
54
+ @State private var didAppear = false
55
+ @State private var keyboardHeight: CGFloat = 0
56
+ /// Live drag offset, isolated from `sheetOffset` so a touch-move only invalidates
57
+ /// this gesture-scoped value instead of forcing the whole body (including
58
+ /// `content()`) to reconstruct every frame — mirrors the pattern already used in
59
+ /// `ios/PopupView/PopupView.swift` for smooth drag-to-dismiss.
60
+ @GestureState private var dragTranslation: CGFloat = 0
61
+
62
+ /// Mirrors Compose's `sheetCloseOffset` (100dp drag threshold to dismiss).
63
+ private let closeThreshold: CGFloat = 100
64
+
65
+ /// Mirrors Compose's per-term peak (0.3f). Combined with `backgroundAlpha` the
66
+ /// steady-state (fully open) scrim peaks at 0.6, matching ModalScreen's peak.
67
+ private let maxDynamicAlpha: CGFloat = 0.3
68
+
69
+ private var screenHeight: CGFloat { UIScreen.main.bounds.height }
70
+ private var bottomInset: CGFloat { safeAreaBottomInset() }
71
+ /// While the keyboard is up, it already covers the home-indicator area, so it
72
+ /// replaces (rather than adds to) the usual bottom-inset filler.
73
+ private var effectiveBottomInset: CGFloat { keyboardHeight > 0 ? keyboardHeight : bottomInset }
74
+
75
+ private var backgroundColor: Color {
76
+ isSurface ? theme.colors.background.surface : theme.colors.background.default
77
+ }
78
+
79
+ private var effectiveOffset: CGFloat { sheetOffset + dragTranslation }
80
+
81
+ /// Mirrors Compose's `dynamicAlpha`: a plain derived value (not itself animated)
82
+ /// that tracks the current offset live, shrinking toward 0 as the sheet is
83
+ /// dragged down.
84
+ private var dynamicAlpha: CGFloat {
85
+ let denom = sheetHeight > 0 ? sheetHeight : screenHeight
86
+ guard denom > 0 else { return maxDynamicAlpha }
87
+ let progress = min(max(1 - effectiveOffset / denom, 0), 1)
88
+ return progress * maxDynamicAlpha
89
+ }
90
+
91
+ /// Mirrors Compose's `alpha = backgroundAlpha + dynamicAlpha`: additive, so the
92
+ /// scrim peaks at 0.6 at rest and only fades to a 0.3 floor while dragging
93
+ /// (backgroundAlpha holds steady until an explicit close()).
94
+ private var totalScrimAlpha: CGFloat { backgroundAlpha + dynamicAlpha }
95
+
96
+ var body: some View {
97
+ ZStack(alignment: .bottom) {
98
+ // Dimmed scrim — additive backgroundAlpha + dynamicAlpha (see above).
99
+ Color.black.opacity(totalScrimAlpha)
100
+ .contentShape(Rectangle())
101
+ .onTapGesture {
102
+ if barrierDismissible { close() }
103
+ }
104
+
105
+ sheet
106
+ .offset(y: effectiveOffset)
107
+ }
108
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
109
+ .ignoresSafeArea()
110
+ .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)) { notification in
111
+ guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
112
+ withAnimation(.easeOut(duration: 0.25)) {
113
+ keyboardHeight = UIScreen.main.bounds.height - endFrame.origin.y
114
+ }
115
+ }
116
+ .onAppear {
117
+ guard !didAppear else { return }
118
+ didAppear = true
119
+ ScreenTracker.trackPopupDisplayed(
120
+ maxApi: appEnv.maxApi,
121
+ context: appEnv.applicationContext,
122
+ parentScreenName: ScreenTracker.getLastScreenName()
123
+ )
124
+ navigator.registerOverplayDismiss { close() }
125
+ open()
126
+ }
127
+ }
128
+
129
+ private var sheet: some View {
130
+ VStack(spacing: 0) {
131
+ // Drag handle + header (the draggable region).
132
+ VStack(spacing: 0) {
133
+ Spacer().frame(height: 8)
134
+ Capsule()
135
+ .fill(Colors.black06)
136
+ .frame(width: 40, height: 4)
137
+ Spacer().frame(height: 4)
138
+ headerView
139
+ .frame(height: 56)
140
+ .padding(.horizontal, Spacing.M)
141
+ }
142
+ .frame(maxWidth: .infinity)
143
+ .frame(height: 72)
144
+ .contentShape(Rectangle())
145
+ .gesture(dragGesture)
146
+
147
+ Rectangle()
148
+ .fill(theme.colors.border.default)
149
+ .frame(height: 1)
150
+
151
+ content()
152
+
153
+ backgroundColor
154
+ .frame(height: effectiveBottomInset)
155
+ }
156
+ .frame(maxWidth: .infinity)
157
+ .frame(alignment: .bottom)
158
+ .background(
159
+ GeometryReader { geo in
160
+ Color.clear.onAppear { sheetHeight = geo.size.height }
161
+ }
162
+ )
163
+ .background(backgroundColor)
164
+ .clipShape(RoundedCorner(radius: Radius.M, corners: [.topLeft, .topRight]))
165
+ }
166
+
167
+ @ViewBuilder
168
+ private var headerView: some View {
169
+ switch header.style {
170
+ case .title(let text):
171
+ HStack(spacing: 0) {
172
+ Color.clear.frame(width: 24, height: 24)
173
+ MomoText(text, typography: .headerDefaultBold, color: Colors.black17)
174
+ .frame(maxWidth: .infinity, alignment: .center)
175
+ Icon(source: "navigation_close", size: 24, color: Colors.black17)
176
+ .frame(width: 24, height: 24)
177
+ .contentShape(Rectangle())
178
+ .onTapGesture { close() }
179
+ }
180
+ case .custom(let builder):
181
+ builder()
182
+ }
183
+ }
184
+
185
+ // MARK: - Drag handling (mirrors Compose's detectDragGestures)
186
+
187
+ private var dragGesture: some Gesture {
188
+ // `.global` (not the default `.local`) so the gesture tracks against the
189
+ // window rather than the handle's own view, which is itself repositioned
190
+ // every frame by this same gesture's output (`sheet.offset(y: effectiveOffset)`
191
+ // in `body`) — the local coordinate space chasing its own moving reference
192
+ // frame is a plausible source of a continuous drag-time jitter.
193
+ DragGesture(coordinateSpace: .global)
194
+ .onChanged { _ in
195
+ // Touching the handle should immediately settle `sheetOffset` to its
196
+ // resting value (0) with no animation, so a drag that starts while the
197
+ // open animation — or the keyboard-driven resize, which shifts this
198
+ // same bottom-anchored top edge — is still in flight doesn't fight a
199
+ // moving target. Without this, the live drag value chases an animated
200
+ // one, producing a couple of corrective jumps before it settles.
201
+ if sheetOffset != 0 {
202
+ sheetOffset = 0
203
+ }
204
+ }
205
+ .updating($dragTranslation) { value, state, _ in
206
+ state = max(value.translation.height, 0)
207
+ }
208
+ .onEnded { value in
209
+ let finalOffset = max(value.translation.height, 0)
210
+ // Capture into persisted state BEFORE @GestureState resets to 0,
211
+ // avoiding a one-frame snap back to rest before close()'s own
212
+ // slide-out animation starts (mirrors PopupView's `lastDragPosition`).
213
+ sheetOffset = finalOffset
214
+ if finalOffset > closeThreshold {
215
+ close()
216
+ } else {
217
+ withAnimation(.spring(response: 0.3, dampingFraction: 0.9)) {
218
+ sheetOffset = 0
219
+ }
220
+ }
221
+ }
222
+ }
223
+
224
+ // MARK: - Open / close animations
225
+
226
+ private func open() {
227
+ DispatchQueue.main.async {
228
+ withAnimation(.timingCurve(0.05, 0.7, 0.1, 1, duration: 0.1)) { backgroundAlpha = maxDynamicAlpha }
229
+ withAnimation(.timingCurve(0.05, 0.7, 0.1, 1, duration: 0.35)) { sheetOffset = 0 }
230
+ }
231
+ }
232
+
233
+ /// Mirrors Compose's `closeEvent`: slide out over 200ms, then fade the scrim
234
+ /// out over 100ms only after the slide finishes, then dismiss.
235
+ private func close() {
236
+ withAnimation(.timingCurve(0.3, 0, 0.8, 0.15, duration: 0.2)) {
237
+ sheetOffset = max(sheetHeight, screenHeight)
238
+ }
239
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
240
+ withAnimation(.timingCurve(0.3, 0, 0.8, 0.15, duration: 0.1)) {
241
+ backgroundAlpha = 0
242
+ }
243
+ }
244
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
245
+ navigator.finishOverplayDismiss()
246
+ }
247
+ }
248
+ }
249
+
250
+ @available(iOS 16.0, *)
251
+ private func safeAreaBottomInset() -> CGFloat {
252
+ let bottomInset = UIApplication.shared.connectedScenes
253
+ .compactMap { ($0 as? UIWindowScene)?.keyWindow?.safeAreaInsets.bottom }
254
+ .first ?? 0
255
+ return min(bottomInset, 21)
256
+ }