@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,467 @@
1
+ import SwiftUI
2
+
3
+ // ─── Data model ────────────────────────────────────────────────────────────────
4
+
5
+ /// A single step rendered inside a ``Steps`` indicator.
6
+ public struct StepItem: Identifiable {
7
+ public let id = UUID()
8
+ public let title: String
9
+ public let description: String?
10
+ public let error: Bool
11
+ public let time: String?
12
+
13
+ public init(
14
+ title: String,
15
+ description: String? = nil,
16
+ error: Bool = false,
17
+ time: String? = nil
18
+ ) {
19
+ self.title = title
20
+ self.description = description
21
+ self.error = error
22
+ self.time = time
23
+ }
24
+ }
25
+
26
+ // ─── Enumerations ──────────────────────────────────────────────────────────────
27
+
28
+ public enum StepsSize {
29
+ case small
30
+ case large
31
+ }
32
+
33
+ public enum StepsAlign {
34
+ case left
35
+ case right
36
+ case center
37
+ case stretch
38
+ }
39
+
40
+ // ─── Internal status ───────────────────────────────────────────────────────────
41
+
42
+ private enum StepStatus {
43
+ case incomplete
44
+ case current
45
+ case completed
46
+ case error
47
+ case disabled
48
+ }
49
+
50
+ private func resolveStatus(
51
+ activeIndex: Int,
52
+ currentIndex: Int,
53
+ error: Bool,
54
+ disabled: Bool
55
+ ) -> StepStatus {
56
+ if disabled { return .disabled }
57
+ if error { return .error }
58
+ if activeIndex == currentIndex { return .current }
59
+ if activeIndex > currentIndex { return .completed }
60
+ return .incomplete
61
+ }
62
+
63
+ // ─── Color helpers ─────────────────────────────────────────────────────────────
64
+
65
+ private func resolveIconBgAndBorder(_ status: StepStatus, theme: Theme) -> (bg: Color, border: Color) {
66
+ switch status {
67
+ case .current, .completed:
68
+ return (theme.colors.primary, theme.colors.background.tonal)
69
+ case .error:
70
+ return (theme.colors.error.primary, theme.colors.error.container)
71
+ case .disabled:
72
+ return (Colors.pink08, Colors.pink10)
73
+ case .incomplete:
74
+ return (Colors.black06, theme.colors.border.default)
75
+ }
76
+ }
77
+
78
+ private func resolveTitleColor(_ status: StepStatus, theme: Theme) -> Color {
79
+ switch status {
80
+ case .current:
81
+ return theme.colors.primary
82
+ case .error:
83
+ return theme.colors.error.primary
84
+ case .disabled:
85
+ return theme.colors.text.disable
86
+ default:
87
+ return theme.colors.text.default
88
+ }
89
+ }
90
+
91
+ // ─── Step bubble ───────────────────────────────────────────────────────────────
92
+
93
+ private struct StepBubble: View {
94
+ @Environment(\.appTheme) private var theme
95
+
96
+ let index: Int
97
+ let isActive: Bool
98
+ let error: Bool
99
+ let size: StepsSize
100
+ let status: StepStatus
101
+ let useNumber: Bool
102
+ let customIcon: String?
103
+
104
+ private var bubbleSize: CGFloat { size == .small ? 16 : 24 }
105
+ private var innerSize: CGFloat { size == .small ? 12 : 16 }
106
+ private var dotSize: CGFloat { size == .small ? 6 : 8 }
107
+
108
+ var body: some View {
109
+ let colors = resolveIconBgAndBorder(status, theme: theme)
110
+ ZStack {
111
+ Circle()
112
+ .fill(colors.bg)
113
+ .overlay(Circle().strokeBorder(colors.border, lineWidth: 2))
114
+
115
+ bubbleContent
116
+ }
117
+ .frame(width: bubbleSize, height: bubbleSize)
118
+ }
119
+
120
+ @ViewBuilder
121
+ private var bubbleContent: some View {
122
+ if useNumber {
123
+ MomoText(
124
+ "\(index + 1)",
125
+ typography: .headerXsSemibold,
126
+ color: Colors.black01
127
+ )
128
+ } else if isActive {
129
+ if let customIcon = customIcon, !customIcon.isEmpty {
130
+ Icon(source: customIcon, size: innerSize, color: Colors.black01)
131
+ } else {
132
+ Circle()
133
+ .fill(Colors.black01)
134
+ .frame(width: dotSize, height: dotSize)
135
+ }
136
+ } else {
137
+ Icon(
138
+ source: error ? "navigation_close" : "notifications_check",
139
+ size: innerSize,
140
+ color: Colors.black01
141
+ )
142
+ }
143
+ }
144
+ }
145
+
146
+ // ─── Horizontal layout ─────────────────────────────────────────────────────────
147
+
148
+ private struct StepsHorizontal: View {
149
+ @Environment(\.appTheme) private var theme
150
+
151
+ let steps: [StepItem]
152
+ let activeIndex: Int
153
+ let size: StepsSize
154
+ let useNumber: Bool
155
+ let align: StepsAlign
156
+ let customIcon: String?
157
+ let disabled: Bool
158
+ let onPress: ((StepItem, Int) -> Void)?
159
+
160
+ private var completedLine: Color { theme.colors.primary.opacity(0.2) }
161
+ private var defaultLine: Color { theme.colors.background.default }
162
+
163
+ var body: some View {
164
+ HStack(alignment: .top, spacing: 0) {
165
+ ForEach(Array(steps.enumerated()), id: \.element.id) { index, item in
166
+ column(index: index, item: item)
167
+ .frame(maxWidth: .infinity)
168
+ }
169
+ }
170
+ .frame(maxWidth: .infinity)
171
+ }
172
+
173
+ @ViewBuilder
174
+ private func column(index: Int, item: StepItem) -> some View {
175
+ let status = resolveStatus(
176
+ activeIndex: activeIndex,
177
+ currentIndex: index,
178
+ error: item.error,
179
+ disabled: disabled
180
+ )
181
+ let isActiveOrError = status == .current || status == .error
182
+ let titleStyle: TypographyStyle = isActiveOrError
183
+ ? .headerXsSemibold : .descriptionDefaultRegular
184
+ let descStyle: TypographyStyle = size == .small
185
+ ? .descriptionXsRegular : .descriptionDefaultRegular
186
+ let titleColor = resolveTitleColor(status, theme: theme)
187
+ let subColor = disabled ? theme.colors.text.disable : theme.colors.text.hint
188
+
189
+ let colAlignment = horizontalAlignment(for: index)
190
+ let textAlignment = textAlign(for: index)
191
+
192
+ let hideLineLeft = align == .left || (align == .stretch && index == 0)
193
+ let hideLineRight = align == .right || (align == .stretch && index == steps.count - 1)
194
+
195
+ let lineLeft: Color = index == 0
196
+ ? Color.clear
197
+ : (activeIndex >= index ? completedLine : defaultLine)
198
+ let lineRight: Color = index == steps.count - 1
199
+ ? Color.clear
200
+ : (activeIndex > index ? completedLine : defaultLine)
201
+
202
+ let clickable = status == .completed && onPress != nil && !disabled
203
+
204
+ let columnBody = VStack(alignment: colAlignment, spacing: 0) {
205
+ if let time = item.time, !time.isEmpty {
206
+ MomoText(time, typography: .descriptionXsRegular, color: subColor)
207
+ .multilineTextAlignment(textAlignment)
208
+ .frame(maxWidth: .infinity, alignment: frameAlignment(colAlignment))
209
+ }
210
+
211
+ HStack(spacing: 0) {
212
+ if !hideLineLeft {
213
+ lineLeft
214
+ .frame(height: 2)
215
+ .frame(maxWidth: .infinity)
216
+ .clipShape(RoundedCorner(radius: Radius.XS, corners: [.topRight, .bottomRight]))
217
+ }
218
+ StepBubble(
219
+ index: index,
220
+ isActive: activeIndex == index,
221
+ error: item.error,
222
+ size: size,
223
+ status: status,
224
+ useNumber: useNumber,
225
+ customIcon: customIcon
226
+ )
227
+ .padding(.leading, hideLineLeft ? 0 : Spacing.XS)
228
+ .padding(.trailing, hideLineRight ? 0 : Spacing.XS)
229
+ .padding(.vertical, Spacing.XS)
230
+ if !hideLineRight {
231
+ lineRight
232
+ .frame(height: 2)
233
+ .frame(maxWidth: .infinity)
234
+ .clipShape(RoundedCorner(radius: Radius.XS, corners: [.topLeft, .bottomLeft]))
235
+ }
236
+ }
237
+ .frame(maxWidth: .infinity)
238
+
239
+ if !item.title.isEmpty {
240
+ MomoText(item.title, typography: titleStyle, color: titleColor)
241
+ .multilineTextAlignment(textAlignment)
242
+ .frame(maxWidth: .infinity, alignment: frameAlignment(colAlignment))
243
+ .padding(.bottom, Spacing.XXS)
244
+ }
245
+
246
+ if let desc = item.description, !desc.isEmpty {
247
+ MomoText(desc, typography: descStyle, color: subColor)
248
+ .multilineTextAlignment(textAlignment)
249
+ .frame(maxWidth: .infinity, alignment: frameAlignment(colAlignment))
250
+ }
251
+ }
252
+ .contentShape(Rectangle())
253
+
254
+ if clickable {
255
+ // Mirrors Compose's `activeOpacityClickable` press feedback; non-clickable
256
+ // steps render with no press interaction at all.
257
+ SwiftUI.Button(action: { onPress?(item, index) }) {
258
+ columnBody
259
+ }
260
+ .buttonStyle(ActiveOpacityButtonStyle())
261
+ } else {
262
+ columnBody
263
+ }
264
+ }
265
+
266
+ private func horizontalAlignment(for index: Int) -> HorizontalAlignment {
267
+ switch align {
268
+ case .left: return .leading
269
+ case .right: return .trailing
270
+ case .center: return .center
271
+ case .stretch:
272
+ if index == 0 { return .leading }
273
+ if index == steps.count - 1 { return .trailing }
274
+ return .center
275
+ }
276
+ }
277
+
278
+ private func textAlign(for index: Int) -> TextAlignment {
279
+ switch align {
280
+ case .left: return .leading
281
+ case .right: return .trailing
282
+ case .center: return .center
283
+ case .stretch:
284
+ if index == 0 { return .leading }
285
+ if index == steps.count - 1 { return .trailing }
286
+ return .center
287
+ }
288
+ }
289
+
290
+ private func frameAlignment(_ h: HorizontalAlignment) -> Alignment {
291
+ switch h {
292
+ case .leading: return .leading
293
+ case .trailing: return .trailing
294
+ default: return .center
295
+ }
296
+ }
297
+ }
298
+
299
+ // ─── Vertical layout ───────────────────────────────────────────────────────────
300
+
301
+ private struct StepsVertical: View {
302
+ @Environment(\.appTheme) private var theme
303
+
304
+ let steps: [StepItem]
305
+ let activeIndex: Int
306
+ let size: StepsSize
307
+ let useNumber: Bool
308
+ let customIcon: String?
309
+ let disabled: Bool
310
+ let onPress: ((StepItem, Int) -> Void)?
311
+
312
+ private var completedLine: Color { theme.colors.primary.opacity(0.2) }
313
+ private var defaultLine: Color { theme.colors.background.default }
314
+ private var useSmall: Bool { size == .small }
315
+ private var connectorHeight: CGFloat { useSmall ? 24 : 32 }
316
+
317
+ var body: some View {
318
+ VStack(spacing: 0) {
319
+ ForEach(Array(steps.enumerated()), id: \.element.id) { index, item in
320
+ row(index: index, item: item)
321
+ }
322
+ }
323
+ .frame(maxWidth: .infinity)
324
+ }
325
+
326
+ @ViewBuilder
327
+ private func row(index: Int, item: StepItem) -> some View {
328
+ let status = resolveStatus(
329
+ activeIndex: activeIndex,
330
+ currentIndex: index,
331
+ error: item.error,
332
+ disabled: disabled
333
+ )
334
+ let isActiveOrError = status == .current || status == .error
335
+ let isLast = index == steps.count - 1
336
+
337
+ let titleStyle: TypographyStyle = isActiveOrError
338
+ ? (useSmall ? .headerXsSemibold : .headerSSemibold)
339
+ : (useSmall ? .descriptionDefaultRegular : .bodyDefaultRegular)
340
+ let descStyle: TypographyStyle = useSmall
341
+ ? .descriptionXsRegular : .descriptionDefaultRegular
342
+
343
+ let titleColor = resolveTitleColor(status, theme: theme)
344
+ let subColor = disabled ? theme.colors.text.disable : theme.colors.text.hint
345
+ let lineColor = activeIndex > index ? completedLine : defaultLine
346
+
347
+ let clickable = status == .completed && onPress != nil && !disabled
348
+
349
+ let rowBody = HStack(alignment: .top, spacing: 0) {
350
+ VStack(spacing: 0) {
351
+ StepBubble(
352
+ index: index,
353
+ isActive: activeIndex == index,
354
+ error: item.error,
355
+ size: size,
356
+ status: status,
357
+ useNumber: useNumber,
358
+ customIcon: customIcon
359
+ )
360
+ if !isLast {
361
+ lineColor
362
+ .frame(width: 2, height: connectorHeight)
363
+ .padding(.vertical, Spacing.XS)
364
+ }
365
+ }
366
+ .padding(.trailing, Spacing.M)
367
+
368
+ VStack(alignment: .leading, spacing: 0) {
369
+ HStack(alignment: .top, spacing: 0) {
370
+ MomoText(item.title, typography: titleStyle, color: titleColor)
371
+ .lineLimit(2)
372
+ .frame(maxWidth: .infinity, alignment: .leading)
373
+ .padding(.trailing, Spacing.S)
374
+ if let time = item.time, !time.isEmpty {
375
+ MomoText(time, typography: .descriptionXsRegular, color: subColor)
376
+ }
377
+ }
378
+ .frame(maxWidth: .infinity)
379
+
380
+ if let desc = item.description, !desc.isEmpty {
381
+ MomoText(desc, typography: descStyle, color: subColor)
382
+ .frame(maxWidth: .infinity, alignment: .leading)
383
+ .padding(.top, Spacing.XXS)
384
+ }
385
+ }
386
+ .frame(maxWidth: .infinity)
387
+ .padding(.bottom, isLast ? 0 : Spacing.S)
388
+ }
389
+ .frame(maxWidth: .infinity)
390
+ .contentShape(Rectangle())
391
+
392
+ if clickable {
393
+ // Mirrors Compose's `activeOpacityClickable` press feedback; non-clickable
394
+ // steps render with no press interaction at all.
395
+ SwiftUI.Button(action: { onPress?(item, index) }) {
396
+ rowBody
397
+ }
398
+ .buttonStyle(ActiveOpacityButtonStyle())
399
+ } else {
400
+ rowBody
401
+ }
402
+ }
403
+ }
404
+
405
+ // ─── Public API ────────────────────────────────────────────────────────────────
406
+
407
+ /// Steps — guided progress indicator through a sequence of procedural steps.
408
+ ///
409
+ /// Mirrors the Compose `Steps` component.
410
+ public struct Steps: View {
411
+ private let steps: [StepItem]
412
+ private let activeIndex: Int
413
+ private let horizontal: Bool
414
+ private let size: StepsSize
415
+ private let useNumber: Bool
416
+ private let align: StepsAlign
417
+ private let customIcon: String?
418
+ private let disabled: Bool
419
+ private let onPress: ((StepItem, Int) -> Void)?
420
+
421
+ public init(
422
+ steps: [StepItem],
423
+ activeIndex: Int,
424
+ horizontal: Bool = false,
425
+ size: StepsSize = .large,
426
+ useNumber: Bool = false,
427
+ align: StepsAlign = .center,
428
+ customIcon: String? = nil,
429
+ disabled: Bool = false,
430
+ onPress: ((StepItem, Int) -> Void)? = nil
431
+ ) {
432
+ self.steps = steps
433
+ self.activeIndex = activeIndex
434
+ self.horizontal = horizontal
435
+ self.size = size
436
+ self.useNumber = useNumber
437
+ self.align = align
438
+ self.customIcon = customIcon
439
+ self.disabled = disabled
440
+ self.onPress = onPress
441
+ }
442
+
443
+ public var body: some View {
444
+ if horizontal {
445
+ StepsHorizontal(
446
+ steps: steps,
447
+ activeIndex: activeIndex,
448
+ size: size,
449
+ useNumber: useNumber,
450
+ align: align,
451
+ customIcon: customIcon,
452
+ disabled: disabled,
453
+ onPress: onPress
454
+ )
455
+ } else {
456
+ StepsVertical(
457
+ steps: steps,
458
+ activeIndex: activeIndex,
459
+ size: size,
460
+ useNumber: useNumber,
461
+ customIcon: customIcon,
462
+ disabled: disabled,
463
+ onPress: onPress
464
+ )
465
+ }
466
+ }
467
+ }
@@ -0,0 +1,99 @@
1
+ import SwiftUI
2
+ import Foundation
3
+
4
+ public struct SuggestAction: View {
5
+ @Environment(\.appTheme) private var theme
6
+
7
+ private let message: String
8
+ private let buttonTitle: String
9
+ private let boldMessage: String
10
+ private let image: String
11
+ private let visible: Bool
12
+ private let onClose: () -> Void
13
+ private let onPressButton: () -> Void
14
+
15
+ public init(
16
+ message: String,
17
+ buttonTitle: String,
18
+ boldMessage: String = "",
19
+ image: String = "",
20
+ visible: Bool = false,
21
+ onClose: @escaping () -> Void = {},
22
+ onPressButton: @escaping () -> Void = {}
23
+ ) {
24
+ self.message = message
25
+ self.buttonTitle = buttonTitle
26
+ self.boldMessage = boldMessage
27
+ self.image = image
28
+ self.visible = visible
29
+ self.onClose = onClose
30
+ self.onPressButton = onPressButton
31
+ }
32
+
33
+ // Builds an attributed string with `boldMessage` (case-insensitive) emphasized.
34
+ private var annotatedMessage: AttributedString {
35
+ let baseFont = Font.system(size: scaleSize(TypographyStyle.bodyDefaultRegular.fontSize), weight: .regular)
36
+ var attributed = AttributedString(message)
37
+ attributed.font = baseFont
38
+
39
+ guard !boldMessage.isEmpty,
40
+ let range = message.range(of: boldMessage, options: .caseInsensitive)
41
+ else {
42
+ return attributed
43
+ }
44
+
45
+ if let boldRange = Range(range, in: attributed) {
46
+ attributed[boldRange].font = Font.system(
47
+ size: scaleSize(TypographyStyle.bodyDefaultRegular.fontSize),
48
+ weight: .bold
49
+ )
50
+ }
51
+ return attributed
52
+ }
53
+
54
+ public var body: some View {
55
+ Group {
56
+ if visible {
57
+ HStack(spacing: 0) {
58
+ if !image.isEmpty {
59
+ ImageView(image)
60
+ .frame(width: 32, height: 32)
61
+ .clipShape(RoundedRectangle(cornerRadius: Radius.XS, style: .continuous))
62
+ Spacer().frame(width: Spacing.S)
63
+ }
64
+
65
+ Text(annotatedMessage)
66
+ .foregroundColor(theme.colors.text.default)
67
+ .frame(maxWidth: .infinity, alignment: .leading)
68
+ .padding(.trailing, Spacing.M)
69
+
70
+ Button(
71
+ title: buttonTitle,
72
+ action: { onPressButton() },
73
+ type: .tonal,
74
+ size: .small,
75
+ isFull: false
76
+ )
77
+
78
+ Spacer().frame(width: Spacing.S)
79
+
80
+ SwiftUI.Button(action: onClose) {
81
+ Icon(source: "navigation_close", size: 24, color: theme.colors.text.default)
82
+ .contentShape(Rectangle())
83
+ }
84
+ .buttonStyle(ActiveOpacityButtonStyle())
85
+ }
86
+ .padding(Spacing.M)
87
+ .frame(maxWidth: .infinity)
88
+ .background(
89
+ RoundedRectangle(cornerRadius: Radius.S, style: .continuous)
90
+ .fill(theme.colors.background.surface)
91
+ )
92
+ .clipShape(RoundedRectangle(cornerRadius: Radius.S, style: .continuous))
93
+ .shadow(color: theme.colors.text.default.opacity(0.12), radius: 8, x: 0, y: 4)
94
+ .transition(.opacity)
95
+ }
96
+ }
97
+ .animation(.easeInOut, value: visible)
98
+ }
99
+ }