@momo-kits/native-kits 0.162.2-debug → 0.162.2-sp.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 (149) hide show
  1. package/.claude/settings.local.json +55 -0
  2. package/compose/build.gradle.kts +1 -1
  3. package/compose/compose.podspec +1 -1
  4. package/gradle.properties +1 -1
  5. package/ios/Application/Components.swift +34 -43
  6. package/ios/Application/FloatingButton.swift +10 -8
  7. package/ios/Application/Navigation/BottomTab/BottomTab.swift +102 -0
  8. package/ios/Application/Navigation/BottomTab/BottomTabBar.swift +215 -0
  9. package/ios/Application/Navigation/BottomTab/CurvedContainer.swift +65 -0
  10. package/ios/Application/Navigation/HeaderBackProps.swift +66 -0
  11. package/ios/Application/Navigation/HeaderTitle.swift +57 -0
  12. package/ios/Application/Navigation/HeaderUser.swift +209 -0
  13. package/ios/Application/Navigation/NavigationContainer.swift +151 -0
  14. package/ios/Application/Navigation/NavigationOptions.swift +109 -0
  15. package/ios/Application/Navigation/Navigator.swift +360 -0
  16. package/ios/Application/Navigation/Overplay/BottomSheet.swift +176 -0
  17. package/ios/Application/Navigation/Overplay/ModalScreen.swift +68 -0
  18. package/ios/Application/Navigation/Overplay/SnackBar.swift +144 -0
  19. package/ios/Application/Navigation/component/Header.swift +178 -0
  20. package/ios/Application/Navigation/component/HeaderBackground.swift +83 -0
  21. package/ios/Application/Navigation/component/HeaderColor.swift +65 -0
  22. package/ios/Application/StackScreen.swift +449 -0
  23. package/ios/Avatar/Avatar.swift +193 -0
  24. package/ios/Badge/Badge.swift +11 -13
  25. package/ios/Badge/BadgeRibbon.swift +12 -19
  26. package/ios/BaselineView/BaselineView.swift +163 -0
  27. package/ios/Button/Button.swift +62 -23
  28. package/ios/Carousel/Carousel.swift +181 -0
  29. package/ios/Checkbox/Checkbox.swift +14 -12
  30. package/ios/Chip/Chip.swift +23 -9
  31. package/ios/Collapse/Collapse.swift +182 -0
  32. package/ios/Colors+Radius+Spacing/Colors.swift +1 -1
  33. package/ios/Colors+Radius+Spacing/Theme.swift +295 -0
  34. package/ios/DateTimePicker/DateTimePicker.swift +210 -0
  35. package/ios/DateTimePicker/DateTimePickerTypes.swift +55 -0
  36. package/ios/DateTimePicker/DateTimePickerUtils.swift +167 -0
  37. package/ios/DateTimePicker/WheelPicker.swift +161 -0
  38. package/ios/Divider/Divider.swift +16 -0
  39. package/ios/Icon/Icon.swift +6 -1
  40. package/ios/IconButton/IconButton.swift +112 -0
  41. package/ios/Information/Information.swift +151 -0
  42. package/ios/Input/Input.swift +42 -25
  43. package/ios/Input/InputPhoneNumber.swift +21 -19
  44. package/ios/Input/InputSearch.swift +101 -45
  45. package/ios/Input/InputTextArea.swift +67 -40
  46. package/ios/InputDropDown/InputDropDown.swift +198 -0
  47. package/ios/InputMoney/InputMoney.swift +335 -0
  48. package/ios/InputOTP/InputOTP.swift +210 -0
  49. package/ios/Loader/Loader.swift +113 -0
  50. package/ios/MoMoUIKits.podspec +5 -0
  51. package/ios/OTPKeyboard/KeyboardButton.swift +1 -1
  52. package/ios/Pagination/PaginationDot.swift +61 -0
  53. package/ios/Pagination/PaginationNumber.swift +35 -0
  54. package/ios/Pagination/PaginationScroll.swift +101 -0
  55. package/ios/Pagination/PaginationWhiteDot.swift +37 -0
  56. package/ios/Popup/PopupDisplay.swift +10 -12
  57. package/ios/Popup/PopupInput.swift +2 -6
  58. package/ios/Popup/PopupNotify.swift +219 -0
  59. package/ios/ProgressInfo/ProgressInfo.swift +294 -0
  60. package/ios/Radio/Radio.swift +70 -0
  61. package/ios/Rating/Rating.swift +82 -0
  62. package/ios/Skeleton/Skeleton.swift +99 -0
  63. package/ios/Slider/Slider.swift +237 -0
  64. package/ios/Stepper/Stepper.swift +220 -0
  65. package/ios/Steps/Steps.swift +451 -0
  66. package/ios/SuggestAction/SuggestAction.swift +97 -0
  67. package/ios/Swipeable/SwipeCell.swift +185 -0
  68. package/ios/Swipeable/SwipeCellModel.swift +21 -0
  69. package/ios/TabView/TabView.swift +530 -0
  70. package/ios/Tag/Tag.swift +98 -0
  71. package/ios/Template/TrustBanner/TrustBanner.swift +2 -0
  72. package/ios/Title/Title.swift +269 -0
  73. package/ios/Tooltip/Tooltip.swift +482 -0
  74. package/ios/Typography/Text.swift +77 -5
  75. package/ios/Typography/Typography.swift +9 -0
  76. package/ios/Uploader/Uploader.swift +183 -0
  77. package/ios/native-kits.podspec +58 -8
  78. package/ios-demo/MoMoUIKitsDemo.podspec +18 -0
  79. package/ios-demo/README.md +120 -0
  80. package/ios-demo/Resources/dot_loading.json +558 -0
  81. package/ios-demo/Resources/icon.json +4052 -0
  82. package/ios-demo/Resources/lottie_circle_loader.json +1 -0
  83. package/ios-demo/Sources/MoMoUIKitsDemo/DemoScaffold.swift +48 -0
  84. package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsBadgeDemoView.swift +25 -0
  85. package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsButtonDemoView.swift +26 -0
  86. package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsCheckboxDemoView.swift +19 -0
  87. package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsChipDemoView.swift +33 -0
  88. package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsInputDemoView.swift +48 -0
  89. package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsPopupDemoView.swift +21 -0
  90. package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsRadioDemoView.swift +17 -0
  91. package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsTypographyDemoView.swift +28 -0
  92. package/ios-demo/Sources/MoMoUIKitsDemo/MoMoUIKitsDemoContext.swift +31 -0
  93. package/ios-demo/Sources/MoMoUIKitsDemo/MoMoUIKitsDemoFactory.swift +72 -0
  94. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/AnimatedHeaderDemo.swift +110 -0
  95. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/AvatarDemo.swift +93 -0
  96. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BadgeDemo.swift +269 -0
  97. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BaselineView.swift +117 -0
  98. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BottomTabDemo.swift +89 -0
  99. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ButtonDemo.swift +100 -0
  100. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CarouselDemo.swift +126 -0
  101. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CheckboxDemo.swift +63 -0
  102. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ChipDemo.swift +20 -0
  103. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CollapseDemo.swift +93 -0
  104. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +107 -0
  105. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DateTimePickerDemo.swift +124 -0
  106. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DividerDemo.swift +14 -0
  107. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/HeaderUserDemo.swift +49 -0
  108. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +261 -0
  109. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IconButtonDemo.swift +58 -0
  110. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ImageDemo.swift +78 -0
  111. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InformationDemo.swift +138 -0
  112. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDemo.swift +111 -0
  113. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDropDownDemo.swift +136 -0
  114. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputMoneyDemo.swift +107 -0
  115. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputOTPDemo.swift +73 -0
  116. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputPhoneNumberDemo.swift +100 -0
  117. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputSearchDemo.swift +32 -0
  118. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputTextAreaDemo.swift +57 -0
  119. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/LoaderDemo.swift +70 -0
  120. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +227 -0
  121. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PaginationDemo.swift +72 -0
  122. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +175 -0
  123. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +121 -0
  124. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +76 -0
  125. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ProgressInfoDemo.swift +89 -0
  126. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RadioDemo.swift +32 -0
  127. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RatingDemo.swift +56 -0
  128. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SkeletonDemo.swift +32 -0
  129. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SliderDemo.swift +150 -0
  130. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SnackBarDemo.swift +138 -0
  131. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepperDemo.swift +103 -0
  132. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepsDemo.swift +94 -0
  133. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SuggestActionDemo.swift +79 -0
  134. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwipeDemo.swift +152 -0
  135. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwitchDemo.swift +33 -0
  136. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TabViewDemo.swift +190 -0
  137. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TagDemo.swift +34 -0
  138. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ThemeDemo.swift +182 -0
  139. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TitleDemo.swift +109 -0
  140. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TooltipDemo.swift +187 -0
  141. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TypographyDemo.swift +87 -0
  142. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/UploaderDemo.swift +131 -0
  143. package/ios-demo/Sources/MoMoUIKitsDemo/SwiftUIDemoBrowserView.swift +50 -0
  144. package/ios-demo/Sources/MoMoUIKitsDemo/UIKitsDemoModels.swift +15 -0
  145. package/ios-demo/Sources/MoMoUIKitsDemo/UIKitsDemoRegistry.swift +73 -0
  146. package/local.properties +8 -0
  147. package/package.json +1 -1
  148. package/publish.sh +50 -0
  149. package/scripts/gen-ios-color.mjs +136 -0
@@ -0,0 +1,482 @@
1
+ import SwiftUI
2
+
3
+ // MARK: - Types
4
+
5
+ /// Tooltip placement relative to the anchor element.
6
+ public enum TooltipPlacement {
7
+ case top
8
+ case bottom
9
+ case left
10
+ case right
11
+ }
12
+
13
+ /// Cross-axis alignment of the tooltip relative to the anchor.
14
+ public enum TooltipAlign {
15
+ case start
16
+ case center
17
+ case end
18
+ }
19
+
20
+ /// Describes a single action button displayed in the tooltip.
21
+ public struct TooltipButton {
22
+ public let title: String?
23
+ public let icon: String?
24
+ public let onPress: (() -> Void)?
25
+
26
+ public init(
27
+ title: String? = nil,
28
+ icon: String? = nil,
29
+ onPress: (() -> Void)? = nil
30
+ ) {
31
+ self.title = title
32
+ self.icon = icon
33
+ self.onPress = onPress
34
+ }
35
+ }
36
+
37
+ /// State holder for controlling tooltip visibility imperatively.
38
+ /// Mirrors the Compose `TooltipState` / `rememberTooltipState()` pattern.
39
+ public final class TooltipState: ObservableObject {
40
+ @Published public private(set) var isVisible: Bool = false
41
+
42
+ public init() {}
43
+
44
+ /// Shows the tooltip.
45
+ public func show() {
46
+ isVisible = true
47
+ }
48
+
49
+ /// Hides the tooltip.
50
+ public func hide() {
51
+ isVisible = false
52
+ }
53
+
54
+ /// Toggles the tooltip visibility.
55
+ public func toggle() {
56
+ isVisible.toggle()
57
+ }
58
+ }
59
+
60
+ // MARK: - Constants
61
+
62
+ private enum TooltipConst {
63
+ static let offset = Spacing.S // TOOLTIP_OFFSET
64
+ static let arrowSize: CGFloat = 6 // ARROW_SIZE
65
+ static let maxWidth: CGFloat = 300
66
+ static let cornerRadius = Radius.S
67
+ static let arrowCornerRadius: CGFloat = 2
68
+ static let arrowEdgeMargin = Spacing.M
69
+ static let arrowOverlap: CGFloat = 1
70
+ }
71
+
72
+ // MARK: - Arrow Shape
73
+
74
+ /// Triangular arrow pointing away from the anchor in the given placement direction.
75
+ struct TooltipArrowShape: Shape {
76
+ let placement: TooltipPlacement
77
+
78
+ func path(in rect: CGRect) -> Path {
79
+ let w = rect.width
80
+ let h = rect.height
81
+ var path = Path()
82
+ switch placement {
83
+ case .top:
84
+ path.move(to: CGPoint(x: 0, y: 0))
85
+ path.addLine(to: CGPoint(x: w, y: 0))
86
+ path.addLine(to: CGPoint(x: w / 2, y: h))
87
+ case .bottom:
88
+ path.move(to: CGPoint(x: 0, y: h))
89
+ path.addLine(to: CGPoint(x: w, y: h))
90
+ path.addLine(to: CGPoint(x: w / 2, y: 0))
91
+ case .left:
92
+ path.move(to: CGPoint(x: 0, y: 0))
93
+ path.addLine(to: CGPoint(x: 0, y: h))
94
+ path.addLine(to: CGPoint(x: w, y: h / 2))
95
+ case .right:
96
+ path.move(to: CGPoint(x: w, y: 0))
97
+ path.addLine(to: CGPoint(x: w, y: h))
98
+ path.addLine(to: CGPoint(x: 0, y: h / 2))
99
+ }
100
+ path.closeSubpath()
101
+ return path
102
+ }
103
+ }
104
+
105
+ // MARK: - Anchor bounds preference
106
+
107
+ private struct TooltipAnchorBoundsKey: PreferenceKey {
108
+ static var defaultValue: Anchor<CGRect>? = nil
109
+ static func reduce(value: inout Anchor<CGRect>?, nextValue: () -> Anchor<CGRect>?) {
110
+ value = nextValue() ?? value
111
+ }
112
+ }
113
+
114
+ // MARK: - Tooltip
115
+
116
+ /// A tooltip component that wraps `content` (the anchor) and shows a positioned
117
+ /// tooltip popup with title, description, close button, and action buttons.
118
+ ///
119
+ /// Port of the Compose / React Native `Tooltip` component.
120
+ public struct Tooltip<Content: View>: View {
121
+ @ObservedObject private var state: TooltipState
122
+ private let title: String?
123
+ private let description: String
124
+ private let buttons: [TooltipButton]
125
+ private let placement: TooltipPlacement
126
+ private let align: TooltipAlign
127
+ private let onVisibleChange: ((Bool) -> Void)?
128
+ private let onPressClose: (() -> Void)?
129
+ private let content: Content
130
+
131
+ public init(
132
+ state: TooltipState,
133
+ title: String? = nil,
134
+ description: String,
135
+ buttons: [TooltipButton] = [],
136
+ placement: TooltipPlacement = .top,
137
+ align: TooltipAlign = .center,
138
+ onVisibleChange: ((Bool) -> Void)? = nil,
139
+ onPressClose: (() -> Void)? = nil,
140
+ @ViewBuilder content: () -> Content
141
+ ) {
142
+ self.state = state
143
+ self.title = title
144
+ self.description = description
145
+ self.buttons = buttons
146
+ self.placement = placement
147
+ self.align = align
148
+ self.onVisibleChange = onVisibleChange
149
+ self.onPressClose = onPressClose
150
+ self.content = content()
151
+ }
152
+
153
+ public var body: some View {
154
+ content
155
+ .anchorPreference(key: TooltipAnchorBoundsKey.self, value: .bounds) { $0 }
156
+ .overlayPreferenceValue(TooltipAnchorBoundsKey.self) { anchor in
157
+ GeometryReader { proxy in
158
+ if state.isVisible, let anchor = anchor {
159
+ let anchorRect = proxy[anchor]
160
+ TooltipPopupContent(
161
+ title: title,
162
+ description: description,
163
+ buttons: buttons,
164
+ placement: placement,
165
+ align: align,
166
+ onPressClose: onPressClose
167
+ )
168
+ .fixedSize()
169
+ .modifier(
170
+ TooltipPositionModifier(
171
+ anchorRect: anchorRect,
172
+ containerSize: proxy.size,
173
+ placement: placement,
174
+ align: align
175
+ )
176
+ )
177
+ }
178
+ }
179
+ }
180
+ .onChange(of: state.isVisible) { newValue in
181
+ onVisibleChange?(newValue)
182
+ }
183
+ }
184
+ }
185
+
186
+ // MARK: - Positioning
187
+
188
+ /// Positions the tooltip (already sized via `.fixedSize()`) relative to the anchor,
189
+ /// mirroring `TooltipPositionProvider.calculatePosition`.
190
+ private struct TooltipPositionModifier: ViewModifier {
191
+ let anchorRect: CGRect
192
+ let containerSize: CGSize
193
+ let placement: TooltipPlacement
194
+ let align: TooltipAlign
195
+
196
+ @State private var popupSize: CGSize = .zero
197
+
198
+ private var offsetPx: CGFloat {
199
+ TooltipConst.offset + TooltipConst.arrowSize
200
+ }
201
+
202
+ func body(content: Content) -> some View {
203
+ content
204
+ .background(
205
+ GeometryReader { p in
206
+ Color.clear
207
+ .onAppear { popupSize = p.size }
208
+ .onChange(of: p.size) { popupSize = $0 }
209
+ }
210
+ )
211
+ .offset(x: originX, y: originY)
212
+ }
213
+
214
+ private var originX: CGFloat {
215
+ let popW = popupSize.width
216
+ switch placement {
217
+ case .top, .bottom:
218
+ return alignHorizontal(anchorRect.minX, anchorRect.width, popW)
219
+ case .left:
220
+ return anchorRect.minX - popW - offsetPx
221
+ case .right:
222
+ return anchorRect.maxX + offsetPx
223
+ }
224
+ }
225
+
226
+ private var originY: CGFloat {
227
+ let popH = popupSize.height
228
+ switch placement {
229
+ case .top:
230
+ return anchorRect.minY - popH - offsetPx
231
+ case .bottom:
232
+ return anchorRect.maxY + offsetPx
233
+ case .left, .right:
234
+ return alignVertical(anchorRect.minY, anchorRect.height, popH)
235
+ }
236
+ }
237
+
238
+ private func alignHorizontal(_ anchorX: CGFloat, _ anchorW: CGFloat, _ popW: CGFloat) -> CGFloat {
239
+ switch align {
240
+ case .start: return anchorX
241
+ case .end: return anchorX + anchorW - popW
242
+ case .center: return anchorX + (anchorW - popW) / 2
243
+ }
244
+ }
245
+
246
+ private func alignVertical(_ anchorY: CGFloat, _ anchorH: CGFloat, _ popH: CGFloat) -> CGFloat {
247
+ switch align {
248
+ case .start: return anchorY
249
+ case .end: return anchorY + anchorH - popH
250
+ case .center: return anchorY + (anchorH - popH) / 2
251
+ }
252
+ }
253
+ }
254
+
255
+ // MARK: - Popup content
256
+
257
+ private struct TooltipPopupContent: View {
258
+ let title: String?
259
+ let description: String?
260
+ let buttons: [TooltipButton]
261
+ let placement: TooltipPlacement
262
+ let align: TooltipAlign
263
+ let onPressClose: (() -> Void)?
264
+
265
+ var body: some View {
266
+ ZStack {
267
+ VStack(alignment: .leading, spacing: 0) {
268
+ HStack(alignment: .top, spacing: 0) {
269
+ VStack(alignment: .leading, spacing: 0) {
270
+ if let title = title, !title.isEmpty {
271
+ MomoText(title, typography: .headerSSemibold, color: Colors.black01)
272
+ .lineLimit(1)
273
+ .truncationMode(.tail)
274
+ .padding(.bottom, Spacing.XS)
275
+ }
276
+ if let description = description, !description.isEmpty {
277
+ MomoText(description, typography: .descriptionDefaultRegular, color: Colors.black01)
278
+ .lineLimit(2)
279
+ .truncationMode(.tail)
280
+ }
281
+ }
282
+ .frame(maxWidth: .infinity, alignment: .leading)
283
+
284
+ if let onPressClose = onPressClose {
285
+ Spacer().frame(width: Spacing.M)
286
+ SwiftUI.Button(action: onPressClose) {
287
+ Icon(source: "navigation_close", size: 20, color: Colors.black01)
288
+ .frame(width: 20, height: 20)
289
+ }
290
+ .buttonStyle(.plain)
291
+ }
292
+ }
293
+
294
+ if !buttons.isEmpty {
295
+ Spacer().frame(height: Spacing.M)
296
+ TooltipButtons(buttons: buttons)
297
+ .frame(maxWidth: .infinity, alignment: .trailing)
298
+ }
299
+ }
300
+ .padding(Spacing.M)
301
+ .frame(maxWidth: TooltipConst.maxWidth, alignment: .leading)
302
+ .background(Colors.black17)
303
+ .clipShape(RoundedRectangle(cornerRadius: TooltipConst.cornerRadius))
304
+ }
305
+ .overlay(arrow, alignment: arrowAlignment)
306
+ }
307
+
308
+ // MARK: Arrow
309
+
310
+ @ViewBuilder
311
+ private var arrow: some View {
312
+ let arrowWidth = TooltipConst.arrowSize * 2
313
+ let arrowHeight = TooltipConst.arrowSize
314
+ let isVertical = placement == .top || placement == .bottom
315
+ TooltipArrowShape(placement: placement)
316
+ .fill(Colors.black17)
317
+ .frame(
318
+ width: isVertical ? arrowWidth : arrowHeight,
319
+ height: isVertical ? arrowHeight : arrowWidth
320
+ )
321
+ .offset(x: arrowOffsetX, y: arrowOffsetY)
322
+ }
323
+
324
+ private var arrowAlignment: Alignment {
325
+ switch placement {
326
+ case .top:
327
+ switch align {
328
+ case .start: return .bottomLeading
329
+ case .center: return .bottom
330
+ case .end: return .bottomTrailing
331
+ }
332
+ case .bottom:
333
+ switch align {
334
+ case .start: return .topLeading
335
+ case .center: return .top
336
+ case .end: return .topTrailing
337
+ }
338
+ case .left:
339
+ switch align {
340
+ case .start: return .topTrailing
341
+ case .center: return .trailing
342
+ case .end: return .bottomTrailing
343
+ }
344
+ case .right:
345
+ switch align {
346
+ case .start: return .topLeading
347
+ case .center: return .leading
348
+ case .end: return .bottomLeading
349
+ }
350
+ }
351
+ }
352
+
353
+ private var arrowOffsetX: CGFloat {
354
+ let arrowHeight = TooltipConst.arrowSize
355
+ switch placement {
356
+ case .left: return arrowHeight - TooltipConst.arrowOverlap
357
+ case .right: return -arrowHeight + TooltipConst.arrowOverlap
358
+ default:
359
+ switch align {
360
+ case .start: return TooltipConst.arrowEdgeMargin
361
+ case .end: return -TooltipConst.arrowEdgeMargin
362
+ case .center: return 0
363
+ }
364
+ }
365
+ }
366
+
367
+ private var arrowOffsetY: CGFloat {
368
+ let arrowHeight = TooltipConst.arrowSize
369
+ switch placement {
370
+ case .top: return arrowHeight - TooltipConst.arrowOverlap
371
+ case .bottom: return -arrowHeight + TooltipConst.arrowOverlap
372
+ default:
373
+ switch align {
374
+ case .start: return TooltipConst.arrowEdgeMargin
375
+ case .end: return -TooltipConst.arrowEdgeMargin
376
+ case .center: return 0
377
+ }
378
+ }
379
+ }
380
+ }
381
+
382
+ // MARK: - Buttons row
383
+
384
+ private struct TooltipButtons: View {
385
+ let buttons: [TooltipButton]
386
+
387
+ var body: some View {
388
+ HStack(spacing: 0) {
389
+ content
390
+ }
391
+ }
392
+
393
+ @ViewBuilder
394
+ private var content: some View {
395
+ if buttons.count == 1 {
396
+ TooltipSingleButton(btn: buttons[0])
397
+ } else if buttons.count == 2 {
398
+ let primary = buttons[0]
399
+ let secondary = buttons[1]
400
+ let bothIcons = primary.icon != nil && secondary.icon != nil
401
+ if bothIcons {
402
+ TooltipIconButton(icon: secondary.icon ?? "", onPress: secondary.onPress ?? {})
403
+ Spacer().frame(width: Spacing.S)
404
+ TooltipIconButton(icon: primary.icon ?? "", onPress: primary.onPress ?? {})
405
+ } else {
406
+ TooltipSecondaryButton(title: secondary.title ?? "", onPress: secondary.onPress ?? {})
407
+ Spacer().frame(width: Spacing.S)
408
+ TooltipPrimaryButton(title: primary.title ?? "", onPress: primary.onPress ?? {})
409
+ }
410
+ } else {
411
+ ForEach(Array(buttons.enumerated()), id: \.offset) { index, btn in
412
+ if index > 0 {
413
+ Spacer().frame(width: Spacing.XXS)
414
+ }
415
+ TooltipSingleButton(btn: btn)
416
+ }
417
+ }
418
+ }
419
+ }
420
+
421
+ private func tooltipLimitWithEllipsis(_ value: String, max: Int = 16) -> String {
422
+ value.count > max ? String(value.prefix(max)) + "…" : value
423
+ }
424
+
425
+ private struct TooltipSingleButton: View {
426
+ let btn: TooltipButton
427
+
428
+ var body: some View {
429
+ if let icon = btn.icon {
430
+ TooltipIconButton(icon: icon, onPress: btn.onPress ?? {})
431
+ } else {
432
+ TooltipPrimaryButton(title: btn.title ?? "", onPress: btn.onPress ?? {})
433
+ }
434
+ }
435
+ }
436
+
437
+ private struct TooltipPrimaryButton: View {
438
+ let title: String
439
+ let onPress: () -> Void
440
+
441
+ var body: some View {
442
+ Button(
443
+ title: tooltipLimitWithEllipsis(title),
444
+ action: onPress,
445
+ type: .secondary,
446
+ size: .medium,
447
+ isFull: false
448
+ )
449
+ }
450
+ }
451
+
452
+ private struct TooltipSecondaryButton: View {
453
+ let title: String
454
+ let onPress: () -> Void
455
+
456
+ var body: some View {
457
+ SwiftUI.Button(action: onPress) {
458
+ MomoText(tooltipLimitWithEllipsis(title), typography: .actionSBold, color: Colors.black01)
459
+ .lineLimit(1)
460
+ .truncationMode(.tail)
461
+ .padding(.horizontal, Spacing.M)
462
+ .padding(.vertical, Spacing.S)
463
+ }
464
+ .buttonStyle(.plain)
465
+ }
466
+ }
467
+
468
+ private struct TooltipIconButton: View {
469
+ let icon: String
470
+ let onPress: () -> Void
471
+
472
+ var body: some View {
473
+ let iconButtonSize = scaleSize(36)
474
+ SwiftUI.Button(action: onPress) {
475
+ Icon(source: icon, size: 24)
476
+ .frame(width: iconButtonSize, height: iconButtonSize)
477
+ .background(Colors.black01)
478
+ .clipShape(RoundedRectangle(cornerRadius: Radius.XL))
479
+ }
480
+ .buttonStyle(.plain)
481
+ }
482
+ }
@@ -115,26 +115,98 @@ public extension TypographyStyle {
115
115
  }
116
116
 
117
117
  public struct MomoText: View {
118
+ @Environment(\.appTheme) private var theme
118
119
  private let content: String
119
120
  private let typography: TypographyStyle
120
- private let color: Color
121
+ private let color: Color?
122
+ private let textAlign: TextAlignment
123
+ private let maxLines: Int
124
+ private let minLines: Int
125
+ private let lineLimitRange: ClosedRange<Int>?
126
+ private let truncationMode: Text.TruncationMode
127
+ private let strikethrough: Bool
128
+ private let underline: Bool
129
+ private let letterSpacing: CGFloat?
130
+ private let softWrap: Bool
131
+ private let accessibilityId: String?
121
132
 
122
133
  public init(
123
134
  _ content: String,
124
135
  typography: TypographyStyle = .bodyDefaultRegular,
125
- color: Color = Colors.text
136
+ color: Color? = nil,
137
+ textAlign: TextAlignment = .leading,
138
+ maxLines: Int = Int.max,
139
+ minLines: Int = 1,
140
+ truncationMode: Text.TruncationMode = .tail,
141
+ strikethrough: Bool = false,
142
+ underline: Bool = false,
143
+ letterSpacing: CGFloat? = nil,
144
+ softWrap: Bool = true,
145
+ accessibilityId: String? = nil
126
146
  ) {
127
147
  self.content = content
128
148
  self.typography = typography
129
149
  self.color = color
150
+ self.textAlign = textAlign
151
+ self.maxLines = maxLines
152
+ self.minLines = minLines
153
+ // SwiftUI's range-based `lineLimit` is iOS 16+ only; keep the range for
154
+ // newer OS versions and fall back to the legacy single-value overload.
155
+ if maxLines == Int.max && minLines <= 1 {
156
+ self.lineLimitRange = nil
157
+ } else {
158
+ let upper = maxLines == Int.max ? Int.max : Swift.max(maxLines, minLines)
159
+ self.lineLimitRange = Swift.max(1, minLines)...upper
160
+ }
161
+ self.truncationMode = truncationMode
162
+ self.strikethrough = strikethrough
163
+ self.underline = underline
164
+ self.letterSpacing = letterSpacing
165
+ self.softWrap = softWrap
166
+ self.accessibilityId = accessibilityId
130
167
  }
131
168
 
132
169
  public var body: some View {
133
- let text = SwiftUI.Text(content)
170
+ let textColor = color ?? theme.colors.text.default
171
+ // Strikethrough is driven either by the typography variant (mirrors Compose
172
+ // *Strikethrough styles) or the explicit `strikethrough` flag.
173
+ let isStrikethrough = strikethrough || typography.isStrikethrough
174
+
175
+ var text = SwiftUI.Text(content)
134
176
  .font(.system(size: scaleSize(typography.fontSize), weight: typography.fontWeight))
135
- .foregroundColor(color)
136
- .lineSpacing(scaleSize(typography.lineHeight) - scaleSize(typography.fontSize))
177
+ .foregroundColor(textColor)
178
+ .strikethrough(isStrikethrough, color: textColor)
179
+ .underline(underline, color: textColor)
180
+
181
+ if let letterSpacing = letterSpacing {
182
+ text = text.tracking(letterSpacing)
183
+ }
137
184
 
138
185
  return text
186
+ .multilineTextAlignment(textAlign)
187
+ .lineSpacing(scaleSize(typography.lineHeight) - scaleSize(typography.fontSize))
188
+ .modifier(LineLimitModifier(range: lineLimitRange))
189
+ .truncationMode(truncationMode)
190
+ .fixedSize(horizontal: !softWrap, vertical: false)
191
+ .accessibilityIdentifier(accessibilityId ?? content)
192
+ .ignoreBoldTextSetting()
193
+ }
194
+ }
195
+
196
+ // Applies a line-limit range only on iOS 16+. Older targets use the legacy
197
+ // single-value overload so the file still compiles for iOS 15.
198
+ private struct LineLimitModifier: ViewModifier {
199
+ let range: ClosedRange<Int>?
200
+
201
+ func body(content: Content) -> some View {
202
+ if let range = range {
203
+ if #available(iOS 16.0, *) {
204
+ content.lineLimit(range)
205
+ } else {
206
+ content.lineLimit(range.upperBound)
207
+ }
208
+ } else {
209
+ content
210
+ }
139
211
  }
140
212
  }
@@ -1,5 +1,14 @@
1
1
  import SwiftUI
2
2
 
3
+ public extension View {
4
+ /// Forces SwiftUI to ignore the OS "Bold Text" accessibility setting so the
5
+ /// kit's typography weight stays fixed and matches the Compose implementation
6
+ /// (which does not honor that OS setting). Apply it wherever a `.font(...)` is set.
7
+ func ignoreBoldTextSetting() -> some View {
8
+ environment(\.legibilityWeight, .regular)
9
+ }
10
+ }
11
+
3
12
  public extension Font {
4
13
  static func appFont(size: CGFloat) -> Font {
5
14
  let defaultScreenSize: CGFloat = 375