@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,219 @@
|
|
|
1
|
+
import SDWebImageSwiftUI
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
public typealias PopupActionDirection = PopupButtonDirection
|
|
5
|
+
|
|
6
|
+
public struct PopupNotifyProps {
|
|
7
|
+
public var image: String
|
|
8
|
+
public var title: String
|
|
9
|
+
public var description: String
|
|
10
|
+
public var error: String
|
|
11
|
+
public var primary: PopupAction?
|
|
12
|
+
public var secondary: PopupAction?
|
|
13
|
+
public var buttonDirection: PopupActionDirection
|
|
14
|
+
public var onIconClose: () -> Void
|
|
15
|
+
public var isShowCloseIcon: Bool
|
|
16
|
+
|
|
17
|
+
public init(
|
|
18
|
+
image: String = "",
|
|
19
|
+
title: String = "Title",
|
|
20
|
+
description: String = "Description",
|
|
21
|
+
error: String = "",
|
|
22
|
+
primary: PopupAction? = nil,
|
|
23
|
+
secondary: PopupAction? = nil,
|
|
24
|
+
buttonDirection: PopupActionDirection = .row,
|
|
25
|
+
onIconClose: @escaping () -> Void,
|
|
26
|
+
isShowCloseIcon: Bool = true
|
|
27
|
+
) {
|
|
28
|
+
self.image = image
|
|
29
|
+
self.title = title
|
|
30
|
+
self.description = description
|
|
31
|
+
self.error = error
|
|
32
|
+
self.primary = primary
|
|
33
|
+
self.secondary = secondary
|
|
34
|
+
self.buttonDirection = buttonDirection
|
|
35
|
+
self.onIconClose = onIconClose
|
|
36
|
+
self.isShowCloseIcon = isShowCloseIcon
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public struct PopupNotify: View {
|
|
41
|
+
public let props: PopupNotifyProps
|
|
42
|
+
|
|
43
|
+
@State private var descriptionHeight: CGFloat = 0
|
|
44
|
+
|
|
45
|
+
private var errorCodePrefix: String {
|
|
46
|
+
let language = UserDefaults.standard
|
|
47
|
+
.string(forKey: "language")?
|
|
48
|
+
.replacingOccurrences(of: "\"", with: "") ?? "vi"
|
|
49
|
+
return language == "en" ? "Error code: " : "Mã lỗi: "
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private var maxDescriptionHeight: CGFloat {
|
|
53
|
+
scaleSize(TypographyStyle.bodyDefaultRegular.lineHeight) * 8.5
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private var shouldUseColumnButtons: Bool {
|
|
57
|
+
switch props.buttonDirection {
|
|
58
|
+
case .column:
|
|
59
|
+
return true
|
|
60
|
+
case .row:
|
|
61
|
+
return false
|
|
62
|
+
case .auto:
|
|
63
|
+
return (props.secondary?.title.count ?? 0) > 12 || (props.primary?.title.count ?? 0) > 12
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public init(props: PopupNotifyProps) {
|
|
68
|
+
self.props = props
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public var body: some View {
|
|
72
|
+
ZStack(alignment: .topTrailing) {
|
|
73
|
+
VStack(spacing: 0) {
|
|
74
|
+
if !props.image.isEmpty {
|
|
75
|
+
popupImage
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
VStack(alignment: .leading, spacing: 0) {
|
|
79
|
+
MomoText(
|
|
80
|
+
props.title,
|
|
81
|
+
typography: .headerDefaultBold,
|
|
82
|
+
maxLines: 2,
|
|
83
|
+
accessibilityId: "title_popup_permission"
|
|
84
|
+
)
|
|
85
|
+
.padding(.bottom, Spacing.S)
|
|
86
|
+
|
|
87
|
+
descriptionContent
|
|
88
|
+
|
|
89
|
+
if !props.error.isEmpty {
|
|
90
|
+
MomoText(
|
|
91
|
+
errorCodePrefix + props.error,
|
|
92
|
+
typography: .descriptionXsRegular,
|
|
93
|
+
color: Colors.black12,
|
|
94
|
+
maxLines: 1
|
|
95
|
+
)
|
|
96
|
+
.padding(.top, Spacing.S)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
100
|
+
.padding(Spacing.XL)
|
|
101
|
+
|
|
102
|
+
actionButtons
|
|
103
|
+
.padding(.horizontal, Spacing.XL)
|
|
104
|
+
.padding(.bottom, Spacing.XL)
|
|
105
|
+
}
|
|
106
|
+
.background(Colors.black01)
|
|
107
|
+
.clipShape(RoundedRectangle(cornerRadius: Radius.L))
|
|
108
|
+
|
|
109
|
+
if props.isShowCloseIcon {
|
|
110
|
+
closeButton
|
|
111
|
+
.offset(x: 1, y: -11)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
.padding(.horizontal, Spacing.M)
|
|
115
|
+
.accessibilityElement(children: .ignore)
|
|
116
|
+
.accessibilityIdentifier("popup_notify")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private var popupImage: some View {
|
|
120
|
+
WebImage(url: URL(string: props.image), isAnimating: .constant(true))
|
|
121
|
+
.resizable()
|
|
122
|
+
.placeholder {
|
|
123
|
+
Rectangle()
|
|
124
|
+
.fill(Colors.black03)
|
|
125
|
+
.aspectRatio(1.777, contentMode: .fit)
|
|
126
|
+
.frame(maxWidth: .infinity)
|
|
127
|
+
}
|
|
128
|
+
.aspectRatio(1.777, contentMode: .fit)
|
|
129
|
+
.frame(maxWidth: .infinity)
|
|
130
|
+
.clipShape(RoundedCorner(radius: Radius.L, corners: [.topLeft, .topRight]))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@ViewBuilder
|
|
134
|
+
private var descriptionContent: some View {
|
|
135
|
+
if descriptionHeight > maxDescriptionHeight {
|
|
136
|
+
ScrollView(.vertical, showsIndicators: false) {
|
|
137
|
+
descriptionText
|
|
138
|
+
}
|
|
139
|
+
.frame(height: maxDescriptionHeight)
|
|
140
|
+
} else {
|
|
141
|
+
descriptionText
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private var descriptionText: some View {
|
|
146
|
+
MomoText(
|
|
147
|
+
props.description,
|
|
148
|
+
typography: .bodyDefaultRegular,
|
|
149
|
+
accessibilityId: "description_popup_permission"
|
|
150
|
+
)
|
|
151
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
152
|
+
.background(
|
|
153
|
+
GeometryReader { proxy in
|
|
154
|
+
Color.clear
|
|
155
|
+
.onAppear { descriptionHeight = proxy.size.height }
|
|
156
|
+
.onChange(of: props.description) { _ in
|
|
157
|
+
descriptionHeight = proxy.size.height
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@ViewBuilder
|
|
164
|
+
private var actionButtons: some View {
|
|
165
|
+
if shouldUseColumnButtons {
|
|
166
|
+
VStack(spacing: Spacing.S) {
|
|
167
|
+
primaryButton
|
|
168
|
+
if props.secondary != nil {
|
|
169
|
+
secondaryButton
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
HStack(spacing: Spacing.S) {
|
|
174
|
+
if props.secondary != nil {
|
|
175
|
+
secondaryButton
|
|
176
|
+
.frame(maxWidth: .infinity)
|
|
177
|
+
}
|
|
178
|
+
primaryButton
|
|
179
|
+
.frame(maxWidth: .infinity)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private var primaryButton: some View {
|
|
185
|
+
Button(
|
|
186
|
+
title: props.primary?.title ?? "",
|
|
187
|
+
action: { props.primary?.onPress?() },
|
|
188
|
+
size: .medium
|
|
189
|
+
)
|
|
190
|
+
.accessibilityIdentifier("btn_popup_allow")
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private var secondaryButton: some View {
|
|
194
|
+
Button(
|
|
195
|
+
title: props.secondary?.title ?? "",
|
|
196
|
+
action: { props.secondary?.onPress?() },
|
|
197
|
+
type: .text,
|
|
198
|
+
size: .medium
|
|
199
|
+
)
|
|
200
|
+
.accessibilityIdentifier("btn_popup_cancel")
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private var closeButton: some View {
|
|
204
|
+
SwiftUI.Button(action: props.onIconClose) {
|
|
205
|
+
ZStack {
|
|
206
|
+
Circle()
|
|
207
|
+
.fill(Colors.black17)
|
|
208
|
+
.overlay(
|
|
209
|
+
Circle()
|
|
210
|
+
.stroke(Colors.black01, lineWidth: 2)
|
|
211
|
+
)
|
|
212
|
+
.frame(width: 22, height: 22)
|
|
213
|
+
|
|
214
|
+
Icon(source: "navigation_close", size: 16, color: Colors.black01)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
.accessibilityIdentifier("ic_popup_close")
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
// MARK: - Data model
|
|
4
|
+
|
|
5
|
+
public struct ProgressInfoItem: Identifiable {
|
|
6
|
+
public let id = UUID()
|
|
7
|
+
public let title: String
|
|
8
|
+
public let description: String?
|
|
9
|
+
|
|
10
|
+
public init(title: String, description: String? = nil) {
|
|
11
|
+
self.title = title
|
|
12
|
+
self.description = description
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public enum ProgressInfoSize {
|
|
17
|
+
case small
|
|
18
|
+
case large
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public enum ProgressInfoAlign {
|
|
22
|
+
case left
|
|
23
|
+
case right
|
|
24
|
+
case center
|
|
25
|
+
case stretch
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// MARK: - Public entry-point
|
|
29
|
+
|
|
30
|
+
/// Display-only step progress indicator.
|
|
31
|
+
///
|
|
32
|
+
/// - Parameters:
|
|
33
|
+
/// - steps: Ordered list of steps; each has a title and an optional description.
|
|
34
|
+
/// - horizontal: When `true` renders steps in a horizontal row with connector lines;
|
|
35
|
+
/// when `false` (default) renders a vertical column.
|
|
36
|
+
/// - size: Icon diameter: `.small` = 16, `.large` = 24 (default).
|
|
37
|
+
/// - useNumber: Show the 1-based step index inside the icon instead of the default dot.
|
|
38
|
+
/// - align: Alignment of icon + text for the horizontal variant.
|
|
39
|
+
/// - showTitle: Whether step titles are rendered (default `true`).
|
|
40
|
+
/// - showDescription: Whether step descriptions are rendered (default `true`).
|
|
41
|
+
/// - customIcon: Icon source key rendered inside each step circle (overrides the dot;
|
|
42
|
+
/// itself overridden by `useNumber`).
|
|
43
|
+
public struct ProgressInfo: View {
|
|
44
|
+
private let steps: [ProgressInfoItem]
|
|
45
|
+
private let horizontal: Bool
|
|
46
|
+
private let size: ProgressInfoSize
|
|
47
|
+
private let useNumber: Bool
|
|
48
|
+
private let align: ProgressInfoAlign
|
|
49
|
+
private let showTitle: Bool
|
|
50
|
+
private let showDescription: Bool
|
|
51
|
+
private let customIcon: String?
|
|
52
|
+
|
|
53
|
+
public init(
|
|
54
|
+
steps: [ProgressInfoItem],
|
|
55
|
+
horizontal: Bool = false,
|
|
56
|
+
size: ProgressInfoSize = .large,
|
|
57
|
+
useNumber: Bool = false,
|
|
58
|
+
align: ProgressInfoAlign = .center,
|
|
59
|
+
showTitle: Bool = true,
|
|
60
|
+
showDescription: Bool = true,
|
|
61
|
+
customIcon: String? = nil
|
|
62
|
+
) {
|
|
63
|
+
self.steps = steps
|
|
64
|
+
self.horizontal = horizontal
|
|
65
|
+
self.size = size
|
|
66
|
+
self.useNumber = useNumber
|
|
67
|
+
self.align = align
|
|
68
|
+
self.showTitle = showTitle
|
|
69
|
+
self.showDescription = showDescription
|
|
70
|
+
self.customIcon = customIcon
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public var body: some View {
|
|
74
|
+
if horizontal {
|
|
75
|
+
ProgressInfoHorizontal(
|
|
76
|
+
steps: steps,
|
|
77
|
+
size: size,
|
|
78
|
+
useNumber: useNumber,
|
|
79
|
+
align: align,
|
|
80
|
+
showTitle: showTitle,
|
|
81
|
+
showDescription: showDescription,
|
|
82
|
+
customIcon: customIcon
|
|
83
|
+
)
|
|
84
|
+
} else {
|
|
85
|
+
ProgressInfoVertical(
|
|
86
|
+
steps: steps,
|
|
87
|
+
size: size,
|
|
88
|
+
useNumber: useNumber,
|
|
89
|
+
showTitle: showTitle,
|
|
90
|
+
showDescription: showDescription,
|
|
91
|
+
customIcon: customIcon
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// MARK: - Tokens
|
|
98
|
+
|
|
99
|
+
private let progressInfoLineColor = Colors.pink08
|
|
100
|
+
private let progressInfoAccentColor = Color(hex: "a50064") // pink_MoMo_Branding
|
|
101
|
+
|
|
102
|
+
// MARK: - Horizontal variant
|
|
103
|
+
|
|
104
|
+
private struct ProgressInfoHorizontal: View {
|
|
105
|
+
let steps: [ProgressInfoItem]
|
|
106
|
+
let size: ProgressInfoSize
|
|
107
|
+
let useNumber: Bool
|
|
108
|
+
let align: ProgressInfoAlign
|
|
109
|
+
let showTitle: Bool
|
|
110
|
+
let showDescription: Bool
|
|
111
|
+
let customIcon: String?
|
|
112
|
+
|
|
113
|
+
var body: some View {
|
|
114
|
+
HStack(alignment: .top, spacing: 0) {
|
|
115
|
+
ForEach(Array(steps.enumerated()), id: \.element.id) { index, item in
|
|
116
|
+
let isFirst = index == 0
|
|
117
|
+
let isLast = index == steps.count - 1
|
|
118
|
+
|
|
119
|
+
let alignment = columnAlignment(isFirst: isFirst, isLast: isLast)
|
|
120
|
+
let textAlign = textAlignment(isFirst: isFirst, isLast: isLast)
|
|
121
|
+
let endPadding: CGFloat = (!isLast && align != .center && align != .stretch) ? Spacing.XS : 0
|
|
122
|
+
let hideLineLeft = align == .left || (align == .stretch && isFirst)
|
|
123
|
+
let hideLineRight = align == .right || (align == .stretch && isLast)
|
|
124
|
+
|
|
125
|
+
VStack(alignment: alignment, spacing: 0) {
|
|
126
|
+
HStack(spacing: 0) {
|
|
127
|
+
if !hideLineLeft {
|
|
128
|
+
ProgressInfoHorizontalLine()
|
|
129
|
+
}
|
|
130
|
+
ProgressInfoStepIcon(
|
|
131
|
+
index: index,
|
|
132
|
+
size: size,
|
|
133
|
+
useNumber: useNumber,
|
|
134
|
+
customIcon: customIcon
|
|
135
|
+
)
|
|
136
|
+
.padding(.vertical, Spacing.XS)
|
|
137
|
+
if !hideLineRight {
|
|
138
|
+
ProgressInfoHorizontalLine()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
.frame(maxWidth: .infinity)
|
|
142
|
+
|
|
143
|
+
if showTitle && !item.title.isEmpty {
|
|
144
|
+
MomoText(item.title, typography: .headerXsSemibold)
|
|
145
|
+
.multilineTextAlignment(textAlign)
|
|
146
|
+
.frame(maxWidth: .infinity, alignment: frameAlignment(alignment))
|
|
147
|
+
.padding(.bottom, Spacing.XS)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if showDescription, let desc = item.description, !desc.isEmpty {
|
|
151
|
+
MomoText(desc, typography: .descriptionDefaultRegular, color: Colors.black12)
|
|
152
|
+
.multilineTextAlignment(textAlign)
|
|
153
|
+
.frame(maxWidth: .infinity, alignment: frameAlignment(alignment))
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
.frame(maxWidth: .infinity)
|
|
157
|
+
.padding(.trailing, endPadding)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
.frame(maxWidth: .infinity)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private func columnAlignment(isFirst: Bool, isLast: Bool) -> HorizontalAlignment {
|
|
164
|
+
switch align {
|
|
165
|
+
case .left: return .leading
|
|
166
|
+
case .right: return .trailing
|
|
167
|
+
case .center: return .center
|
|
168
|
+
case .stretch:
|
|
169
|
+
if isFirst { return .leading }
|
|
170
|
+
if isLast { return .trailing }
|
|
171
|
+
return .center
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private func frameAlignment(_ h: HorizontalAlignment) -> Alignment {
|
|
176
|
+
switch h {
|
|
177
|
+
case .leading: return .leading
|
|
178
|
+
case .trailing: return .trailing
|
|
179
|
+
default: return .center
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private func textAlignment(isFirst: Bool, isLast: Bool) -> TextAlignment {
|
|
184
|
+
switch align {
|
|
185
|
+
case .left: return .leading
|
|
186
|
+
case .right: return .trailing
|
|
187
|
+
case .center: return .center
|
|
188
|
+
case .stretch:
|
|
189
|
+
if isFirst { return .leading }
|
|
190
|
+
if isLast { return .trailing }
|
|
191
|
+
return .center
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// MARK: - Vertical variant
|
|
197
|
+
|
|
198
|
+
private struct ProgressInfoVertical: View {
|
|
199
|
+
let steps: [ProgressInfoItem]
|
|
200
|
+
let size: ProgressInfoSize
|
|
201
|
+
let useNumber: Bool
|
|
202
|
+
let showTitle: Bool
|
|
203
|
+
let showDescription: Bool
|
|
204
|
+
let customIcon: String?
|
|
205
|
+
|
|
206
|
+
var body: some View {
|
|
207
|
+
VStack(alignment: .leading, spacing: 0) {
|
|
208
|
+
ForEach(Array(steps.enumerated()), id: \.element.id) { index, item in
|
|
209
|
+
let isLast = index == steps.count - 1
|
|
210
|
+
|
|
211
|
+
HStack(alignment: .top, spacing: 0) {
|
|
212
|
+
VStack(spacing: 0) {
|
|
213
|
+
ProgressInfoStepIcon(
|
|
214
|
+
index: index,
|
|
215
|
+
size: size,
|
|
216
|
+
useNumber: useNumber,
|
|
217
|
+
customIcon: customIcon
|
|
218
|
+
)
|
|
219
|
+
if !isLast {
|
|
220
|
+
Rectangle()
|
|
221
|
+
.fill(Colors.primary.opacity(0.2))
|
|
222
|
+
.frame(width: 2)
|
|
223
|
+
.frame(maxHeight: .infinity)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
.frame(minHeight: isLast ? 40 : 48)
|
|
227
|
+
|
|
228
|
+
VStack(alignment: .leading, spacing: 0) {
|
|
229
|
+
if showTitle && !item.title.isEmpty {
|
|
230
|
+
MomoText(item.title, typography: .headerXsSemibold)
|
|
231
|
+
.lineLimit(2)
|
|
232
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
233
|
+
.padding(.trailing, Spacing.S)
|
|
234
|
+
}
|
|
235
|
+
if showDescription, let desc = item.description, !desc.isEmpty {
|
|
236
|
+
MomoText(desc, typography: .descriptionDefaultRegular, color: Colors.black12)
|
|
237
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
241
|
+
.padding(.trailing, Spacing.S)
|
|
242
|
+
.padding(.bottom, isLast ? 0 : Spacing.S)
|
|
243
|
+
}
|
|
244
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
245
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// MARK: - Step icon
|
|
253
|
+
|
|
254
|
+
private struct ProgressInfoStepIcon: View {
|
|
255
|
+
let index: Int
|
|
256
|
+
let size: ProgressInfoSize
|
|
257
|
+
let useNumber: Bool
|
|
258
|
+
let customIcon: String?
|
|
259
|
+
|
|
260
|
+
var body: some View {
|
|
261
|
+
let containerSize = scaleSize(size == .small ? 16 : 24)
|
|
262
|
+
let dotSize = scaleSize(size == .small ? 6 : 8)
|
|
263
|
+
let iconSize = scaleSize(size == .small ? 12 : 16)
|
|
264
|
+
|
|
265
|
+
ZStack {
|
|
266
|
+
Circle()
|
|
267
|
+
.fill(progressInfoLineColor)
|
|
268
|
+
.overlay(Circle().strokeBorder(progressInfoLineColor, lineWidth: 2))
|
|
269
|
+
|
|
270
|
+
if useNumber {
|
|
271
|
+
MomoText("\(index + 1)", typography: .headerXsSemibold, color: progressInfoAccentColor)
|
|
272
|
+
.multilineTextAlignment(.center)
|
|
273
|
+
} else if let icon = customIcon, !icon.isEmpty {
|
|
274
|
+
Icon(source: icon, size: iconSize, color: progressInfoAccentColor)
|
|
275
|
+
} else {
|
|
276
|
+
Circle()
|
|
277
|
+
.fill(progressInfoAccentColor)
|
|
278
|
+
.frame(width: dotSize, height: dotSize)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
.frame(width: containerSize, height: containerSize)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// MARK: - Horizontal connector line
|
|
286
|
+
|
|
287
|
+
private struct ProgressInfoHorizontalLine: View {
|
|
288
|
+
var body: some View {
|
|
289
|
+
Rectangle()
|
|
290
|
+
.fill(progressInfoLineColor)
|
|
291
|
+
.frame(height: 2)
|
|
292
|
+
.frame(maxWidth: .infinity)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
public struct Radio: View {
|
|
5
|
+
@Environment(\.appTheme) private var theme
|
|
6
|
+
|
|
7
|
+
var value: AnyHashable
|
|
8
|
+
@Binding var groupValue: AnyHashable
|
|
9
|
+
var disabled: Bool
|
|
10
|
+
var onChange: (() -> Void)?
|
|
11
|
+
var label: String
|
|
12
|
+
|
|
13
|
+
public init(
|
|
14
|
+
value: AnyHashable,
|
|
15
|
+
groupValue: Binding<AnyHashable>,
|
|
16
|
+
disabled: Bool = false,
|
|
17
|
+
onChange: (() -> Void)? = nil,
|
|
18
|
+
label: String = ""
|
|
19
|
+
) {
|
|
20
|
+
self.value = value
|
|
21
|
+
self._groupValue = groupValue
|
|
22
|
+
self.disabled = disabled
|
|
23
|
+
self.onChange = onChange
|
|
24
|
+
self.label = label
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private var selected: Bool {
|
|
28
|
+
value == groupValue
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private func onTap() {
|
|
32
|
+
groupValue = value
|
|
33
|
+
onChange?()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public var body: some View {
|
|
37
|
+
HStack(spacing: 0) {
|
|
38
|
+
RoundedRectangle(cornerRadius: Radius.M)
|
|
39
|
+
.stroke(borderColor, lineWidth: borderWidth)
|
|
40
|
+
.frame(width: 20, height: 20)
|
|
41
|
+
.padding(.trailing, label.isEmpty ? 0 : Spacing.XS)
|
|
42
|
+
|
|
43
|
+
if !label.isEmpty {
|
|
44
|
+
MomoText(label, typography: .descriptionDefaultRegular)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
.contentShape(Rectangle())
|
|
48
|
+
.onTapGesture {
|
|
49
|
+
if !disabled {
|
|
50
|
+
onTap()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private var borderWidth: CGFloat {
|
|
56
|
+
selected ? 6 : 2
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private var borderColor: Color {
|
|
60
|
+
if disabled && selected {
|
|
61
|
+
return theme.colors.background.tonal
|
|
62
|
+
} else if disabled {
|
|
63
|
+
return theme.colors.text.disable
|
|
64
|
+
} else if selected {
|
|
65
|
+
return theme.colors.primary
|
|
66
|
+
} else {
|
|
67
|
+
return theme.colors.text.default
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
public enum RatingSize {
|
|
4
|
+
case small
|
|
5
|
+
case medium
|
|
6
|
+
case large
|
|
7
|
+
|
|
8
|
+
var iconSize: CGFloat {
|
|
9
|
+
switch self {
|
|
10
|
+
case .small: return 16
|
|
11
|
+
case .medium: return 20
|
|
12
|
+
case .large: return 24
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public struct Rating: View {
|
|
18
|
+
private let numOfStars: Int
|
|
19
|
+
private let rating: Float
|
|
20
|
+
private let onRatingChange: ((Int) -> Void)?
|
|
21
|
+
private let size: RatingSize
|
|
22
|
+
|
|
23
|
+
public init(
|
|
24
|
+
numOfStars: Int,
|
|
25
|
+
rating: Float,
|
|
26
|
+
onRatingChange: ((Int) -> Void)? = nil,
|
|
27
|
+
size: RatingSize = .large
|
|
28
|
+
) {
|
|
29
|
+
self.numOfStars = numOfStars
|
|
30
|
+
self.rating = rating
|
|
31
|
+
self.onRatingChange = onRatingChange
|
|
32
|
+
self.size = size
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public var body: some View {
|
|
36
|
+
HStack(spacing: 2) {
|
|
37
|
+
ForEach(0..<numOfStars, id: \.self) { index in
|
|
38
|
+
star(at: index)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@ViewBuilder
|
|
44
|
+
private func star(at index: Int) -> some View {
|
|
45
|
+
let icon = Icon(
|
|
46
|
+
source: starIcon(at: index),
|
|
47
|
+
size: scaleSize(size.iconSize),
|
|
48
|
+
color: starColor(at: index)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if let onRatingChange = onRatingChange {
|
|
52
|
+
SwiftUI.Button(action: { onRatingChange(index + 1) }) {
|
|
53
|
+
icon
|
|
54
|
+
}
|
|
55
|
+
.buttonStyle(.plain)
|
|
56
|
+
} else {
|
|
57
|
+
icon
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private func starIcon(at index: Int) -> String {
|
|
62
|
+
let intRating = Int(rating)
|
|
63
|
+
if index < intRating {
|
|
64
|
+
return "reaction_star_full"
|
|
65
|
+
} else if index == intRating && rating.truncatingRemainder(dividingBy: 1) != 0 {
|
|
66
|
+
return "reaction_star_half"
|
|
67
|
+
} else {
|
|
68
|
+
return "reaction_star_non"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private func starColor(at index: Int) -> Color {
|
|
73
|
+
let intRating = Int(rating)
|
|
74
|
+
if index < intRating {
|
|
75
|
+
return Colors.yellow03
|
|
76
|
+
} else if index == intRating && rating.truncatingRemainder(dividingBy: 1) != 0 {
|
|
77
|
+
return Colors.yellow03
|
|
78
|
+
} else {
|
|
79
|
+
return Colors.black07
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|