@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.
- package/.claude/settings.local.json +55 -0
- package/compose/build.gradle.kts +1 -1
- package/compose/compose.podspec +1 -1
- package/gradle.properties +1 -1
- package/ios/Application/Components.swift +34 -43
- package/ios/Application/FloatingButton.swift +10 -8
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +102 -0
- package/ios/Application/Navigation/BottomTab/BottomTabBar.swift +215 -0
- package/ios/Application/Navigation/BottomTab/CurvedContainer.swift +65 -0
- package/ios/Application/Navigation/HeaderBackProps.swift +66 -0
- package/ios/Application/Navigation/HeaderTitle.swift +57 -0
- package/ios/Application/Navigation/HeaderUser.swift +209 -0
- package/ios/Application/Navigation/NavigationContainer.swift +151 -0
- package/ios/Application/Navigation/NavigationOptions.swift +109 -0
- package/ios/Application/Navigation/Navigator.swift +360 -0
- package/ios/Application/Navigation/Overplay/BottomSheet.swift +176 -0
- package/ios/Application/Navigation/Overplay/ModalScreen.swift +68 -0
- package/ios/Application/Navigation/Overplay/SnackBar.swift +144 -0
- package/ios/Application/Navigation/component/Header.swift +178 -0
- package/ios/Application/Navigation/component/HeaderBackground.swift +83 -0
- package/ios/Application/Navigation/component/HeaderColor.swift +65 -0
- package/ios/Application/StackScreen.swift +449 -0
- package/ios/Avatar/Avatar.swift +193 -0
- package/ios/Badge/Badge.swift +11 -13
- package/ios/Badge/BadgeRibbon.swift +12 -19
- package/ios/BaselineView/BaselineView.swift +163 -0
- package/ios/Button/Button.swift +62 -23
- package/ios/Carousel/Carousel.swift +181 -0
- package/ios/Checkbox/Checkbox.swift +14 -12
- package/ios/Chip/Chip.swift +23 -9
- package/ios/Collapse/Collapse.swift +182 -0
- package/ios/Colors+Radius+Spacing/Colors.swift +1 -1
- package/ios/Colors+Radius+Spacing/Theme.swift +295 -0
- package/ios/DateTimePicker/DateTimePicker.swift +210 -0
- package/ios/DateTimePicker/DateTimePickerTypes.swift +55 -0
- package/ios/DateTimePicker/DateTimePickerUtils.swift +167 -0
- package/ios/DateTimePicker/WheelPicker.swift +161 -0
- package/ios/Divider/Divider.swift +16 -0
- package/ios/Icon/Icon.swift +6 -1
- package/ios/IconButton/IconButton.swift +112 -0
- package/ios/Information/Information.swift +151 -0
- package/ios/Input/Input.swift +42 -25
- package/ios/Input/InputPhoneNumber.swift +21 -19
- package/ios/Input/InputSearch.swift +101 -45
- package/ios/Input/InputTextArea.swift +67 -40
- package/ios/InputDropDown/InputDropDown.swift +198 -0
- package/ios/InputMoney/InputMoney.swift +335 -0
- package/ios/InputOTP/InputOTP.swift +210 -0
- package/ios/Loader/Loader.swift +113 -0
- package/ios/MoMoUIKits.podspec +5 -0
- package/ios/OTPKeyboard/KeyboardButton.swift +1 -1
- package/ios/Pagination/PaginationDot.swift +61 -0
- package/ios/Pagination/PaginationNumber.swift +35 -0
- package/ios/Pagination/PaginationScroll.swift +101 -0
- package/ios/Pagination/PaginationWhiteDot.swift +37 -0
- package/ios/Popup/PopupDisplay.swift +10 -12
- package/ios/Popup/PopupInput.swift +2 -6
- package/ios/Popup/PopupNotify.swift +219 -0
- package/ios/ProgressInfo/ProgressInfo.swift +294 -0
- package/ios/Radio/Radio.swift +70 -0
- package/ios/Rating/Rating.swift +82 -0
- package/ios/Skeleton/Skeleton.swift +99 -0
- package/ios/Slider/Slider.swift +237 -0
- package/ios/Stepper/Stepper.swift +220 -0
- package/ios/Steps/Steps.swift +451 -0
- package/ios/SuggestAction/SuggestAction.swift +97 -0
- package/ios/Swipeable/SwipeCell.swift +185 -0
- package/ios/Swipeable/SwipeCellModel.swift +21 -0
- package/ios/TabView/TabView.swift +530 -0
- package/ios/Tag/Tag.swift +98 -0
- package/ios/Template/TrustBanner/TrustBanner.swift +2 -0
- package/ios/Title/Title.swift +269 -0
- package/ios/Tooltip/Tooltip.swift +482 -0
- package/ios/Typography/Text.swift +77 -5
- package/ios/Typography/Typography.swift +9 -0
- package/ios/Uploader/Uploader.swift +183 -0
- package/ios/native-kits.podspec +58 -8
- package/ios-demo/MoMoUIKitsDemo.podspec +18 -0
- package/ios-demo/README.md +120 -0
- package/ios-demo/Resources/dot_loading.json +558 -0
- package/ios-demo/Resources/icon.json +4052 -0
- package/ios-demo/Resources/lottie_circle_loader.json +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/DemoScaffold.swift +48 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsBadgeDemoView.swift +25 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsButtonDemoView.swift +26 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsCheckboxDemoView.swift +19 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsChipDemoView.swift +33 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsInputDemoView.swift +48 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsPopupDemoView.swift +21 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsRadioDemoView.swift +17 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsTypographyDemoView.swift +28 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/MoMoUIKitsDemoContext.swift +31 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/MoMoUIKitsDemoFactory.swift +72 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/AnimatedHeaderDemo.swift +110 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/AvatarDemo.swift +93 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BadgeDemo.swift +269 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BaselineView.swift +117 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BottomTabDemo.swift +89 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ButtonDemo.swift +100 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CarouselDemo.swift +126 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CheckboxDemo.swift +63 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ChipDemo.swift +20 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CollapseDemo.swift +93 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +107 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DateTimePickerDemo.swift +124 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DividerDemo.swift +14 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/HeaderUserDemo.swift +49 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +261 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IconButtonDemo.swift +58 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ImageDemo.swift +78 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InformationDemo.swift +138 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDemo.swift +111 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDropDownDemo.swift +136 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputMoneyDemo.swift +107 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputOTPDemo.swift +73 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputPhoneNumberDemo.swift +100 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputSearchDemo.swift +32 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputTextAreaDemo.swift +57 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/LoaderDemo.swift +70 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +227 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PaginationDemo.swift +72 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +175 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +121 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +76 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ProgressInfoDemo.swift +89 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RadioDemo.swift +32 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RatingDemo.swift +56 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SkeletonDemo.swift +32 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SliderDemo.swift +150 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SnackBarDemo.swift +138 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepperDemo.swift +103 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepsDemo.swift +94 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SuggestActionDemo.swift +79 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwipeDemo.swift +152 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwitchDemo.swift +33 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TabViewDemo.swift +190 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TagDemo.swift +34 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ThemeDemo.swift +182 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TitleDemo.swift +109 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TooltipDemo.swift +187 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TypographyDemo.swift +87 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/UploaderDemo.swift +131 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SwiftUIDemoBrowserView.swift +50 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/UIKitsDemoModels.swift +15 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/UIKitsDemoRegistry.swift +73 -0
- package/local.properties +8 -0
- package/package.json +1 -1
- package/publish.sh +50 -0
- package/scripts/gen-ios-color.mjs +136 -0
|
@@ -0,0 +1,451 @@
|
|
|
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
|
+
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
|
+
.onTapGesture {
|
|
254
|
+
if clickable { onPress?(item, index) }
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private func horizontalAlignment(for index: Int) -> HorizontalAlignment {
|
|
259
|
+
switch align {
|
|
260
|
+
case .left: return .leading
|
|
261
|
+
case .right: return .trailing
|
|
262
|
+
case .center: return .center
|
|
263
|
+
case .stretch:
|
|
264
|
+
if index == 0 { return .leading }
|
|
265
|
+
if index == steps.count - 1 { return .trailing }
|
|
266
|
+
return .center
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private func textAlign(for index: Int) -> TextAlignment {
|
|
271
|
+
switch align {
|
|
272
|
+
case .left: return .leading
|
|
273
|
+
case .right: return .trailing
|
|
274
|
+
case .center: return .center
|
|
275
|
+
case .stretch:
|
|
276
|
+
if index == 0 { return .leading }
|
|
277
|
+
if index == steps.count - 1 { return .trailing }
|
|
278
|
+
return .center
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private func frameAlignment(_ h: HorizontalAlignment) -> Alignment {
|
|
283
|
+
switch h {
|
|
284
|
+
case .leading: return .leading
|
|
285
|
+
case .trailing: return .trailing
|
|
286
|
+
default: return .center
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ─── Vertical layout ───────────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
private struct StepsVertical: View {
|
|
294
|
+
@Environment(\.appTheme) private var theme
|
|
295
|
+
|
|
296
|
+
let steps: [StepItem]
|
|
297
|
+
let activeIndex: Int
|
|
298
|
+
let size: StepsSize
|
|
299
|
+
let useNumber: Bool
|
|
300
|
+
let customIcon: String?
|
|
301
|
+
let disabled: Bool
|
|
302
|
+
let onPress: ((StepItem, Int) -> Void)?
|
|
303
|
+
|
|
304
|
+
private var completedLine: Color { theme.colors.primary.opacity(0.2) }
|
|
305
|
+
private var defaultLine: Color { theme.colors.background.default }
|
|
306
|
+
private var useSmall: Bool { size == .small }
|
|
307
|
+
private var connectorHeight: CGFloat { useSmall ? 24 : 32 }
|
|
308
|
+
|
|
309
|
+
var body: some View {
|
|
310
|
+
VStack(spacing: 0) {
|
|
311
|
+
ForEach(Array(steps.enumerated()), id: \.element.id) { index, item in
|
|
312
|
+
row(index: index, item: item)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
.frame(maxWidth: .infinity)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
@ViewBuilder
|
|
319
|
+
private func row(index: Int, item: StepItem) -> some View {
|
|
320
|
+
let status = resolveStatus(
|
|
321
|
+
activeIndex: activeIndex,
|
|
322
|
+
currentIndex: index,
|
|
323
|
+
error: item.error,
|
|
324
|
+
disabled: disabled
|
|
325
|
+
)
|
|
326
|
+
let isActiveOrError = status == .current || status == .error
|
|
327
|
+
let isLast = index == steps.count - 1
|
|
328
|
+
|
|
329
|
+
let titleStyle: TypographyStyle = isActiveOrError
|
|
330
|
+
? (useSmall ? .headerXsSemibold : .headerSSemibold)
|
|
331
|
+
: (useSmall ? .descriptionDefaultRegular : .bodyDefaultRegular)
|
|
332
|
+
let descStyle: TypographyStyle = useSmall
|
|
333
|
+
? .descriptionXsRegular : .descriptionDefaultRegular
|
|
334
|
+
|
|
335
|
+
let titleColor = resolveTitleColor(status, theme: theme)
|
|
336
|
+
let subColor = disabled ? theme.colors.text.disable : theme.colors.text.hint
|
|
337
|
+
let lineColor = activeIndex > index ? completedLine : defaultLine
|
|
338
|
+
|
|
339
|
+
let clickable = status == .completed && onPress != nil && !disabled
|
|
340
|
+
|
|
341
|
+
HStack(alignment: .top, spacing: 0) {
|
|
342
|
+
VStack(spacing: 0) {
|
|
343
|
+
StepBubble(
|
|
344
|
+
index: index,
|
|
345
|
+
isActive: activeIndex == index,
|
|
346
|
+
error: item.error,
|
|
347
|
+
size: size,
|
|
348
|
+
status: status,
|
|
349
|
+
useNumber: useNumber,
|
|
350
|
+
customIcon: customIcon
|
|
351
|
+
)
|
|
352
|
+
if !isLast {
|
|
353
|
+
lineColor
|
|
354
|
+
.frame(width: 2, height: connectorHeight)
|
|
355
|
+
.padding(.vertical, Spacing.XS)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
.padding(.trailing, Spacing.M)
|
|
359
|
+
|
|
360
|
+
VStack(alignment: .leading, spacing: 0) {
|
|
361
|
+
HStack(alignment: .top, spacing: 0) {
|
|
362
|
+
MomoText(item.title, typography: titleStyle, color: titleColor)
|
|
363
|
+
.lineLimit(2)
|
|
364
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
365
|
+
.padding(.trailing, Spacing.S)
|
|
366
|
+
if let time = item.time, !time.isEmpty {
|
|
367
|
+
MomoText(time, typography: .descriptionXsRegular, color: subColor)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
.frame(maxWidth: .infinity)
|
|
371
|
+
|
|
372
|
+
if let desc = item.description, !desc.isEmpty {
|
|
373
|
+
MomoText(desc, typography: descStyle, color: subColor)
|
|
374
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
375
|
+
.padding(.top, Spacing.XXS)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
.frame(maxWidth: .infinity)
|
|
379
|
+
.padding(.bottom, isLast ? 0 : Spacing.S)
|
|
380
|
+
}
|
|
381
|
+
.frame(maxWidth: .infinity)
|
|
382
|
+
.contentShape(Rectangle())
|
|
383
|
+
.onTapGesture {
|
|
384
|
+
if clickable { onPress?(item, index) }
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ─── Public API ────────────────────────────────────────────────────────────────
|
|
390
|
+
|
|
391
|
+
/// Steps — guided progress indicator through a sequence of procedural steps.
|
|
392
|
+
///
|
|
393
|
+
/// Mirrors the Compose `Steps` component.
|
|
394
|
+
public struct Steps: View {
|
|
395
|
+
private let steps: [StepItem]
|
|
396
|
+
private let activeIndex: Int
|
|
397
|
+
private let horizontal: Bool
|
|
398
|
+
private let size: StepsSize
|
|
399
|
+
private let useNumber: Bool
|
|
400
|
+
private let align: StepsAlign
|
|
401
|
+
private let customIcon: String?
|
|
402
|
+
private let disabled: Bool
|
|
403
|
+
private let onPress: ((StepItem, Int) -> Void)?
|
|
404
|
+
|
|
405
|
+
public init(
|
|
406
|
+
steps: [StepItem],
|
|
407
|
+
activeIndex: Int,
|
|
408
|
+
horizontal: Bool = false,
|
|
409
|
+
size: StepsSize = .large,
|
|
410
|
+
useNumber: Bool = false,
|
|
411
|
+
align: StepsAlign = .center,
|
|
412
|
+
customIcon: String? = nil,
|
|
413
|
+
disabled: Bool = false,
|
|
414
|
+
onPress: ((StepItem, Int) -> Void)? = nil
|
|
415
|
+
) {
|
|
416
|
+
self.steps = steps
|
|
417
|
+
self.activeIndex = activeIndex
|
|
418
|
+
self.horizontal = horizontal
|
|
419
|
+
self.size = size
|
|
420
|
+
self.useNumber = useNumber
|
|
421
|
+
self.align = align
|
|
422
|
+
self.customIcon = customIcon
|
|
423
|
+
self.disabled = disabled
|
|
424
|
+
self.onPress = onPress
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
public var body: some View {
|
|
428
|
+
if horizontal {
|
|
429
|
+
StepsHorizontal(
|
|
430
|
+
steps: steps,
|
|
431
|
+
activeIndex: activeIndex,
|
|
432
|
+
size: size,
|
|
433
|
+
useNumber: useNumber,
|
|
434
|
+
align: align,
|
|
435
|
+
customIcon: customIcon,
|
|
436
|
+
disabled: disabled,
|
|
437
|
+
onPress: onPress
|
|
438
|
+
)
|
|
439
|
+
} else {
|
|
440
|
+
StepsVertical(
|
|
441
|
+
steps: steps,
|
|
442
|
+
activeIndex: activeIndex,
|
|
443
|
+
size: size,
|
|
444
|
+
useNumber: useNumber,
|
|
445
|
+
customIcon: customIcon,
|
|
446
|
+
disabled: disabled,
|
|
447
|
+
onPress: onPress
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
Icon(source: "navigation_close", size: 24, color: theme.colors.text.default)
|
|
81
|
+
.contentShape(Rectangle())
|
|
82
|
+
.onTapGesture { onClose() }
|
|
83
|
+
}
|
|
84
|
+
.padding(Spacing.M)
|
|
85
|
+
.frame(maxWidth: .infinity)
|
|
86
|
+
.background(
|
|
87
|
+
RoundedRectangle(cornerRadius: Radius.S, style: .continuous)
|
|
88
|
+
.fill(theme.colors.background.surface)
|
|
89
|
+
)
|
|
90
|
+
.clipShape(RoundedRectangle(cornerRadius: Radius.S, style: .continuous))
|
|
91
|
+
.shadow(color: theme.colors.text.default.opacity(0.12), radius: 8, x: 0, y: 4)
|
|
92
|
+
.transition(.opacity)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
.animation(.easeInOut, value: visible)
|
|
96
|
+
}
|
|
97
|
+
}
|