@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,530 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Style enums (prefixed with `Tab` to avoid collisions with existing public
|
|
5
|
+
// types like ButtonSize / InputSize / TagColor)
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
public enum TabViewType {
|
|
9
|
+
case `default`
|
|
10
|
+
case card
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public enum TabViewDirection {
|
|
14
|
+
case row
|
|
15
|
+
case column
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Data model — mirrors Compose `TabViewItem`
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
public struct TabViewItem {
|
|
23
|
+
public let title: String
|
|
24
|
+
public let icon: String?
|
|
25
|
+
public let renderIcon: ((Bool) -> AnyView)?
|
|
26
|
+
public let showDot: Bool
|
|
27
|
+
public let dotSize: DotSize
|
|
28
|
+
public let badgeValue: String?
|
|
29
|
+
public let accessibilityLabel: String?
|
|
30
|
+
public let content: AnyView
|
|
31
|
+
|
|
32
|
+
public init(
|
|
33
|
+
title: String,
|
|
34
|
+
icon: String? = nil,
|
|
35
|
+
renderIcon: ((Bool) -> AnyView)? = nil,
|
|
36
|
+
showDot: Bool = false,
|
|
37
|
+
dotSize: DotSize = .small,
|
|
38
|
+
badgeValue: String? = nil,
|
|
39
|
+
accessibilityLabel: String? = nil,
|
|
40
|
+
@ViewBuilder content: () -> some View
|
|
41
|
+
) {
|
|
42
|
+
self.title = title
|
|
43
|
+
self.icon = icon
|
|
44
|
+
self.renderIcon = renderIcon
|
|
45
|
+
self.showDot = showDot
|
|
46
|
+
self.dotSize = dotSize
|
|
47
|
+
self.badgeValue = badgeValue
|
|
48
|
+
self.accessibilityLabel = accessibilityLabel
|
|
49
|
+
self.content = AnyView(content())
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Convenience init for tabs without slot content (Compose default `content = {}`).
|
|
53
|
+
public init(
|
|
54
|
+
title: String,
|
|
55
|
+
icon: String? = nil,
|
|
56
|
+
renderIcon: ((Bool) -> AnyView)? = nil,
|
|
57
|
+
showDot: Bool = false,
|
|
58
|
+
dotSize: DotSize = .small,
|
|
59
|
+
badgeValue: String? = nil,
|
|
60
|
+
accessibilityLabel: String? = nil
|
|
61
|
+
) {
|
|
62
|
+
self.init(
|
|
63
|
+
title: title,
|
|
64
|
+
icon: icon,
|
|
65
|
+
renderIcon: renderIcon,
|
|
66
|
+
showDot: showDot,
|
|
67
|
+
dotSize: dotSize,
|
|
68
|
+
badgeValue: badgeValue,
|
|
69
|
+
accessibilityLabel: accessibilityLabel,
|
|
70
|
+
content: { EmptyView() }
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Public component — named `MomoTabView` to avoid shadowing SwiftUI.TabView
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
public struct MomoTabView: View {
|
|
80
|
+
@Environment(\.appTheme) private var theme
|
|
81
|
+
|
|
82
|
+
private let tabs: [TabViewItem]
|
|
83
|
+
private let type: TabViewType
|
|
84
|
+
private let scrollable: Bool
|
|
85
|
+
private let fitContent: Bool
|
|
86
|
+
private let selectedColor: Color?
|
|
87
|
+
private let unselectedColor: Color?
|
|
88
|
+
private let direction: TabViewDirection
|
|
89
|
+
private let userScrollEnabled: Bool
|
|
90
|
+
private let onPressTabItem: (Int) -> Void
|
|
91
|
+
private let onPageSelected: (Int) -> Void
|
|
92
|
+
|
|
93
|
+
@State private var selectedIndex: Int
|
|
94
|
+
|
|
95
|
+
public init(
|
|
96
|
+
tabs: [TabViewItem],
|
|
97
|
+
initialPage: Int = 0,
|
|
98
|
+
type: TabViewType = .default,
|
|
99
|
+
scrollable: Bool = false,
|
|
100
|
+
fitContent: Bool = false,
|
|
101
|
+
selectedColor: Color? = nil,
|
|
102
|
+
unselectedColor: Color? = nil,
|
|
103
|
+
direction: TabViewDirection = .row,
|
|
104
|
+
userScrollEnabled: Bool = true,
|
|
105
|
+
onPressTabItem: @escaping (Int) -> Void = { _ in },
|
|
106
|
+
onPageSelected: @escaping (Int) -> Void = { _ in }
|
|
107
|
+
) {
|
|
108
|
+
self.tabs = tabs
|
|
109
|
+
self.type = type
|
|
110
|
+
self.scrollable = scrollable
|
|
111
|
+
self.fitContent = fitContent
|
|
112
|
+
self.selectedColor = selectedColor
|
|
113
|
+
self.unselectedColor = unselectedColor
|
|
114
|
+
self.direction = direction
|
|
115
|
+
self.userScrollEnabled = userScrollEnabled
|
|
116
|
+
self.onPressTabItem = onPressTabItem
|
|
117
|
+
self.onPageSelected = onPageSelected
|
|
118
|
+
let start = tabs.isEmpty ? 0 : min(max(initialPage, 0), tabs.count - 1)
|
|
119
|
+
_selectedIndex = State(initialValue: start)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private var resolvedSelectedColor: Color { selectedColor ?? theme.colors.primary }
|
|
123
|
+
private var resolvedUnselectedColor: Color { unselectedColor ?? theme.colors.text.default }
|
|
124
|
+
|
|
125
|
+
public var body: some View {
|
|
126
|
+
if tabs.isEmpty {
|
|
127
|
+
EmptyView()
|
|
128
|
+
} else {
|
|
129
|
+
VStack(spacing: 0) {
|
|
130
|
+
tabBar
|
|
131
|
+
|
|
132
|
+
if fitContent {
|
|
133
|
+
tabs[safe: selectedIndex]?.content
|
|
134
|
+
.frame(maxWidth: .infinity)
|
|
135
|
+
} else {
|
|
136
|
+
pager
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
.frame(maxWidth: .infinity)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// MARK: - Tab bar dispatch
|
|
144
|
+
|
|
145
|
+
@ViewBuilder
|
|
146
|
+
private var tabBar: some View {
|
|
147
|
+
switch type {
|
|
148
|
+
case .card:
|
|
149
|
+
CardTabBar(
|
|
150
|
+
tabs: tabs,
|
|
151
|
+
selectedIndex: selectedIndex,
|
|
152
|
+
selectedColor: resolvedSelectedColor,
|
|
153
|
+
unselectedColor: resolvedUnselectedColor,
|
|
154
|
+
direction: direction,
|
|
155
|
+
onPress: onTabPressed
|
|
156
|
+
)
|
|
157
|
+
case .default:
|
|
158
|
+
if scrollable {
|
|
159
|
+
ScrollableTabBar(
|
|
160
|
+
tabs: tabs,
|
|
161
|
+
selectedIndex: selectedIndex,
|
|
162
|
+
selectedColor: resolvedSelectedColor,
|
|
163
|
+
unselectedColor: resolvedUnselectedColor,
|
|
164
|
+
direction: direction,
|
|
165
|
+
onPress: onTabPressed
|
|
166
|
+
)
|
|
167
|
+
} else {
|
|
168
|
+
DefaultTabBar(
|
|
169
|
+
tabs: tabs,
|
|
170
|
+
selectedIndex: selectedIndex,
|
|
171
|
+
selectedColor: resolvedSelectedColor,
|
|
172
|
+
unselectedColor: resolvedUnselectedColor,
|
|
173
|
+
direction: direction,
|
|
174
|
+
onPress: onTabPressed
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// MARK: - Pager content
|
|
181
|
+
|
|
182
|
+
@ViewBuilder
|
|
183
|
+
private var pager: some View {
|
|
184
|
+
if userScrollEnabled {
|
|
185
|
+
SwiftUI.TabView(selection: $selectedIndex) {
|
|
186
|
+
ForEach(tabs.indices, id: \.self) { index in
|
|
187
|
+
tabs[index].content
|
|
188
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
189
|
+
.tag(index)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
.tabViewStyle(.page(indexDisplayMode: .never))
|
|
193
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
194
|
+
.onChange(of: selectedIndex) { newValue in
|
|
195
|
+
onPageSelected(newValue)
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
tabs[safe: selectedIndex]?.content
|
|
199
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// MARK: - Selection
|
|
204
|
+
|
|
205
|
+
private func onTabPressed(_ index: Int) {
|
|
206
|
+
onPressTabItem(index)
|
|
207
|
+
withAnimation(.easeInOut(duration: 0.2)) {
|
|
208
|
+
selectedIndex = index
|
|
209
|
+
}
|
|
210
|
+
if fitContent || !userScrollEnabled {
|
|
211
|
+
onPageSelected(index)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// Default fixed-width tab bar (equal-width items + animated underline)
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
private struct DefaultTabBar: View {
|
|
221
|
+
@Environment(\.appTheme) private var theme
|
|
222
|
+
|
|
223
|
+
let tabs: [TabViewItem]
|
|
224
|
+
let selectedIndex: Int
|
|
225
|
+
let selectedColor: Color
|
|
226
|
+
let unselectedColor: Color
|
|
227
|
+
let direction: TabViewDirection
|
|
228
|
+
let onPress: (Int) -> Void
|
|
229
|
+
|
|
230
|
+
var body: some View {
|
|
231
|
+
GeometryReader { geo in
|
|
232
|
+
let itemWidth = tabs.isEmpty ? 0 : geo.size.width / CGFloat(tabs.count)
|
|
233
|
+
let indicatorWidth = max(itemWidth - Spacing.XS * 2, 0)
|
|
234
|
+
let indicatorX = itemWidth * CGFloat(selectedIndex) + Spacing.XS
|
|
235
|
+
|
|
236
|
+
ZStack(alignment: .bottomLeading) {
|
|
237
|
+
HStack(spacing: 0) {
|
|
238
|
+
ForEach(tabs.indices, id: \.self) { index in
|
|
239
|
+
TabItemView(
|
|
240
|
+
tab: tabs[index],
|
|
241
|
+
active: selectedIndex == index,
|
|
242
|
+
selectedColor: selectedColor,
|
|
243
|
+
unselectedColor: unselectedColor,
|
|
244
|
+
direction: direction,
|
|
245
|
+
scrollable: false,
|
|
246
|
+
onPress: { onPress(index) }
|
|
247
|
+
)
|
|
248
|
+
.frame(maxWidth: .infinity)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
UnstyledRoundedRectangle(topLeft: Radius.XXS, topRight: Radius.XXS)
|
|
253
|
+
.fill(selectedColor)
|
|
254
|
+
.frame(width: indicatorWidth, height: 2)
|
|
255
|
+
.offset(x: indicatorX)
|
|
256
|
+
.animation(.easeInOut(duration: 0.2), value: selectedIndex)
|
|
257
|
+
}
|
|
258
|
+
.frame(maxWidth: .infinity)
|
|
259
|
+
.background(theme.colors.background.surface)
|
|
260
|
+
.overlay(
|
|
261
|
+
Rectangle()
|
|
262
|
+
.stroke(theme.colors.border.default, lineWidth: 0.5)
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
.frame(height: barHeight(direction))
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// Scrollable tab bar (wrap-width items, horizontal scroll, animated underline)
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
private struct ScrollableTabBar: View {
|
|
274
|
+
@Environment(\.appTheme) private var theme
|
|
275
|
+
|
|
276
|
+
let tabs: [TabViewItem]
|
|
277
|
+
let selectedIndex: Int
|
|
278
|
+
let selectedColor: Color
|
|
279
|
+
let unselectedColor: Color
|
|
280
|
+
let direction: TabViewDirection
|
|
281
|
+
let onPress: (Int) -> Void
|
|
282
|
+
|
|
283
|
+
@State private var frames: [Int: CGRect] = [:]
|
|
284
|
+
|
|
285
|
+
var body: some View {
|
|
286
|
+
ScrollViewReader { proxy in
|
|
287
|
+
ScrollView(.horizontal, showsIndicators: false) {
|
|
288
|
+
ZStack(alignment: .bottomLeading) {
|
|
289
|
+
HStack(spacing: 0) {
|
|
290
|
+
ForEach(tabs.indices, id: \.self) { index in
|
|
291
|
+
TabItemView(
|
|
292
|
+
tab: tabs[index],
|
|
293
|
+
active: selectedIndex == index,
|
|
294
|
+
selectedColor: selectedColor,
|
|
295
|
+
unselectedColor: unselectedColor,
|
|
296
|
+
direction: direction,
|
|
297
|
+
scrollable: true,
|
|
298
|
+
onPress: { onPress(index) }
|
|
299
|
+
)
|
|
300
|
+
.background(
|
|
301
|
+
GeometryReader { g in
|
|
302
|
+
Color.clear.preference(
|
|
303
|
+
key: TabFramePreferenceKey.self,
|
|
304
|
+
value: [index: g.frame(in: .named("scrollableTabBar"))]
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
)
|
|
308
|
+
.id(index)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if let frame = frames[selectedIndex] {
|
|
313
|
+
UnstyledRoundedRectangle(topLeft: Radius.XXS, topRight: Radius.XXS)
|
|
314
|
+
.fill(selectedColor)
|
|
315
|
+
.frame(width: max(frame.width - Spacing.XS * 2, 0), height: 2)
|
|
316
|
+
.offset(x: frame.minX + Spacing.XS)
|
|
317
|
+
.animation(.easeInOut(duration: 0.25), value: selectedIndex)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
.coordinateSpace(name: "scrollableTabBar")
|
|
321
|
+
.onPreferenceChange(TabFramePreferenceKey.self) { value in
|
|
322
|
+
frames.merge(value) { _, new in new }
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
.frame(height: barHeight(direction))
|
|
326
|
+
.background(theme.colors.background.surface)
|
|
327
|
+
.overlay(
|
|
328
|
+
Rectangle()
|
|
329
|
+
.stroke(theme.colors.border.default, lineWidth: 0.5)
|
|
330
|
+
)
|
|
331
|
+
.onChange(of: selectedIndex) { index in
|
|
332
|
+
withAnimation { proxy.scrollTo(index, anchor: .center) }
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private struct TabFramePreferenceKey: PreferenceKey {
|
|
339
|
+
static var defaultValue: [Int: CGRect] = [:]
|
|
340
|
+
static func reduce(value: inout [Int: CGRect], nextValue: () -> [Int: CGRect]) {
|
|
341
|
+
value.merge(nextValue()) { _, new in new }
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// Card tab bar
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
private struct CardTabBar: View {
|
|
350
|
+
@Environment(\.appTheme) private var theme
|
|
351
|
+
|
|
352
|
+
let tabs: [TabViewItem]
|
|
353
|
+
let selectedIndex: Int
|
|
354
|
+
let selectedColor: Color
|
|
355
|
+
let unselectedColor: Color
|
|
356
|
+
let direction: TabViewDirection
|
|
357
|
+
let onPress: (Int) -> Void
|
|
358
|
+
|
|
359
|
+
var body: some View {
|
|
360
|
+
HStack(alignment: .bottom, spacing: 0) {
|
|
361
|
+
ForEach(tabs.indices, id: \.self) { index in
|
|
362
|
+
let tab = tabs[index]
|
|
363
|
+
let isActive = selectedIndex == index
|
|
364
|
+
let iconColor = isActive ? selectedColor : unselectedColor
|
|
365
|
+
|
|
366
|
+
cardItem(tab: tab, isActive: isActive, iconColor: iconColor)
|
|
367
|
+
.frame(maxWidth: .infinity)
|
|
368
|
+
.frame(height: isActive ? 44 : 40)
|
|
369
|
+
.background(isActive ? theme.colors.background.surface : theme.colors.background.tonal)
|
|
370
|
+
.clipShape(UnstyledRoundedRectangle(topLeft: Radius.M, topRight: Radius.M))
|
|
371
|
+
.contentShape(Rectangle())
|
|
372
|
+
.onTapGesture { onPress(index) }
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
.frame(maxWidth: .infinity)
|
|
376
|
+
.padding(.top, Spacing.XS)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
@ViewBuilder
|
|
380
|
+
private func cardItem(tab: TabViewItem, isActive: Bool, iconColor: Color) -> some View {
|
|
381
|
+
HStack(spacing: 0) {
|
|
382
|
+
if let renderIcon = tab.renderIcon {
|
|
383
|
+
renderIcon(isActive)
|
|
384
|
+
Spacer().frame(width: Spacing.XS)
|
|
385
|
+
} else if let icon = tab.icon {
|
|
386
|
+
Icon(source: icon, size: 18, color: iconColor)
|
|
387
|
+
Spacer().frame(width: Spacing.XS)
|
|
388
|
+
}
|
|
389
|
+
MomoText(
|
|
390
|
+
tab.title,
|
|
391
|
+
typography: isActive ? .headerSSemibold : .bodyDefaultRegular,
|
|
392
|
+
color: isActive ? selectedColor : unselectedColor
|
|
393
|
+
)
|
|
394
|
+
.lineLimit(1)
|
|
395
|
+
|
|
396
|
+
tabBadge(tab)
|
|
397
|
+
}
|
|
398
|
+
.padding(.horizontal, Spacing.S)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
// Shared tab item (Default + Scrollable bars)
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
private struct TabItemView: View {
|
|
407
|
+
let tab: TabViewItem
|
|
408
|
+
let active: Bool
|
|
409
|
+
let selectedColor: Color
|
|
410
|
+
let unselectedColor: Color
|
|
411
|
+
let direction: TabViewDirection
|
|
412
|
+
let scrollable: Bool
|
|
413
|
+
let onPress: () -> Void
|
|
414
|
+
|
|
415
|
+
private var textColor: Color { active ? selectedColor : unselectedColor }
|
|
416
|
+
private var textStyle: TypographyStyle { active ? .headerSSemibold : .bodyDefaultRegular }
|
|
417
|
+
|
|
418
|
+
var body: some View {
|
|
419
|
+
Group {
|
|
420
|
+
if direction == .column {
|
|
421
|
+
columnBody
|
|
422
|
+
} else {
|
|
423
|
+
rowBody
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
.contentShape(Rectangle())
|
|
427
|
+
.onTapGesture { onPress() }
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
@ViewBuilder
|
|
431
|
+
private var columnBody: some View {
|
|
432
|
+
VStack(spacing: 0) {
|
|
433
|
+
ZStack(alignment: .topTrailing) {
|
|
434
|
+
if let renderIcon = tab.renderIcon {
|
|
435
|
+
renderIcon(active)
|
|
436
|
+
} else if let icon = tab.icon {
|
|
437
|
+
Icon(source: icon, size: 24, color: textColor)
|
|
438
|
+
}
|
|
439
|
+
badgeOverlay
|
|
440
|
+
}
|
|
441
|
+
if tab.icon != nil || tab.renderIcon != nil {
|
|
442
|
+
Spacer().frame(height: Spacing.XXS)
|
|
443
|
+
}
|
|
444
|
+
MomoText(tab.title, typography: textStyle, color: textColor)
|
|
445
|
+
.lineLimit(1)
|
|
446
|
+
}
|
|
447
|
+
.frame(height: 68)
|
|
448
|
+
.padding(.horizontal, Spacing.S)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
@ViewBuilder
|
|
452
|
+
private var rowBody: some View {
|
|
453
|
+
HStack(spacing: 0) {
|
|
454
|
+
if let renderIcon = tab.renderIcon {
|
|
455
|
+
renderIcon(active)
|
|
456
|
+
Spacer().frame(width: Spacing.XS)
|
|
457
|
+
} else if let icon = tab.icon {
|
|
458
|
+
Icon(source: icon, size: 18, color: textColor)
|
|
459
|
+
Spacer().frame(width: Spacing.XS)
|
|
460
|
+
}
|
|
461
|
+
MomoText(tab.title, typography: textStyle, color: textColor)
|
|
462
|
+
.lineLimit(1)
|
|
463
|
+
tabBadge(tab)
|
|
464
|
+
}
|
|
465
|
+
.frame(height: 48)
|
|
466
|
+
.padding(.horizontal, Spacing.M)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
@ViewBuilder
|
|
470
|
+
private var badgeOverlay: some View {
|
|
471
|
+
if tab.showDot {
|
|
472
|
+
BadgeDot(size: tab.dotSize)
|
|
473
|
+
.offset(x: 4, y: -4)
|
|
474
|
+
} else if let badge = tab.badgeValue, !badge.isEmpty {
|
|
475
|
+
Badge(label: badge)
|
|
476
|
+
.offset(x: 4, y: -4)
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ---------------------------------------------------------------------------
|
|
482
|
+
// Shared helpers
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
|
|
485
|
+
private func barHeight(_ direction: TabViewDirection) -> CGFloat {
|
|
486
|
+
direction == .column ? 68 : 48
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
@ViewBuilder
|
|
490
|
+
private func tabBadge(_ tab: TabViewItem) -> some View {
|
|
491
|
+
if tab.showDot {
|
|
492
|
+
Spacer().frame(width: Spacing.XS)
|
|
493
|
+
BadgeDot(size: tab.dotSize)
|
|
494
|
+
} else if let badge = tab.badgeValue, !badge.isEmpty {
|
|
495
|
+
Spacer().frame(width: Spacing.XS)
|
|
496
|
+
Badge(label: badge)
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/// A rectangle rounded only on the top corners (mirrors Compose top-only radius).
|
|
501
|
+
private struct UnstyledRoundedRectangle: Shape {
|
|
502
|
+
let topLeft: CGFloat
|
|
503
|
+
let topRight: CGFloat
|
|
504
|
+
|
|
505
|
+
func path(in rect: CGRect) -> Path {
|
|
506
|
+
var path = Path()
|
|
507
|
+
let tl = min(topLeft, min(rect.width, rect.height) / 2)
|
|
508
|
+
let tr = min(topRight, min(rect.width, rect.height) / 2)
|
|
509
|
+
path.move(to: CGPoint(x: rect.minX, y: rect.maxY))
|
|
510
|
+
path.addLine(to: CGPoint(x: rect.minX, y: rect.minY + tl))
|
|
511
|
+
path.addArc(
|
|
512
|
+
center: CGPoint(x: rect.minX + tl, y: rect.minY + tl),
|
|
513
|
+
radius: tl, startAngle: .degrees(180), endAngle: .degrees(270), clockwise: false
|
|
514
|
+
)
|
|
515
|
+
path.addLine(to: CGPoint(x: rect.maxX - tr, y: rect.minY))
|
|
516
|
+
path.addArc(
|
|
517
|
+
center: CGPoint(x: rect.maxX - tr, y: rect.minY + tr),
|
|
518
|
+
radius: tr, startAngle: .degrees(270), endAngle: .degrees(0), clockwise: false
|
|
519
|
+
)
|
|
520
|
+
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
|
|
521
|
+
path.closeSubpath()
|
|
522
|
+
return path
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private extension Array {
|
|
527
|
+
subscript(safe index: Int) -> Element? {
|
|
528
|
+
indices.contains(index) ? self[index] : nil
|
|
529
|
+
}
|
|
530
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
public struct Tag: View {
|
|
4
|
+
public enum TagSize {
|
|
5
|
+
case large
|
|
6
|
+
case medium
|
|
7
|
+
|
|
8
|
+
var height: CGFloat {
|
|
9
|
+
switch self {
|
|
10
|
+
case .large: return 24
|
|
11
|
+
case .medium: return 18
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public enum TagColor {
|
|
17
|
+
case `default`
|
|
18
|
+
case orange
|
|
19
|
+
case green
|
|
20
|
+
case red
|
|
21
|
+
case blue
|
|
22
|
+
case grey
|
|
23
|
+
|
|
24
|
+
var backgroundColor: Color {
|
|
25
|
+
switch self {
|
|
26
|
+
case .default: return Colors.black04
|
|
27
|
+
case .orange: return Colors.orange08
|
|
28
|
+
case .green: return Colors.green08
|
|
29
|
+
case .red: return Colors.red08
|
|
30
|
+
case .blue: return Colors.blue08
|
|
31
|
+
case .grey: return Colors.black04
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
var textColor: Color {
|
|
36
|
+
switch self {
|
|
37
|
+
case .default: return Colors.black17
|
|
38
|
+
case .orange: return Colors.orange03
|
|
39
|
+
case .green: return Colors.green03
|
|
40
|
+
case .red: return Colors.red03
|
|
41
|
+
case .blue: return Colors.blue03
|
|
42
|
+
case .grey: return Colors.black12
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private let label: String
|
|
48
|
+
private let icon: String?
|
|
49
|
+
private let color: TagColor
|
|
50
|
+
private let size: TagSize
|
|
51
|
+
|
|
52
|
+
public init(
|
|
53
|
+
label: String = "Label",
|
|
54
|
+
icon: String? = nil,
|
|
55
|
+
color: TagColor = .default,
|
|
56
|
+
size: TagSize = .large
|
|
57
|
+
) {
|
|
58
|
+
self.label = label
|
|
59
|
+
self.icon = icon
|
|
60
|
+
self.color = color
|
|
61
|
+
self.size = size
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public var body: some View {
|
|
65
|
+
let tagColor = color.backgroundColor
|
|
66
|
+
let labelColor = color.textColor
|
|
67
|
+
|
|
68
|
+
HStack(spacing: 0) {
|
|
69
|
+
if let icon {
|
|
70
|
+
Icon(source: icon, size: 16, color: labelColor)
|
|
71
|
+
}
|
|
72
|
+
MomoText(label, typography: .labelSMedium, color: labelColor)
|
|
73
|
+
}
|
|
74
|
+
.frame(height: scaleSize(size.height))
|
|
75
|
+
.padding(.horizontal, Spacing.S)
|
|
76
|
+
.background(
|
|
77
|
+
RoundedRectangle(cornerRadius: Radius.S, style: .continuous)
|
|
78
|
+
.fill(tagColor)
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// MARK: - Preview
|
|
84
|
+
#if DEBUG
|
|
85
|
+
struct Tag_Previews: PreviewProvider {
|
|
86
|
+
static var previews: some View {
|
|
87
|
+
VStack(spacing: 12) {
|
|
88
|
+
Tag()
|
|
89
|
+
Tag(color: .red)
|
|
90
|
+
Tag(color: .blue)
|
|
91
|
+
Tag(icon: "ic_momo", color: .grey)
|
|
92
|
+
Tag(label: "Medium", color: .green, size: .medium)
|
|
93
|
+
}
|
|
94
|
+
.padding()
|
|
95
|
+
.previewLayout(.sizeThatFits)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
#endif
|
|
@@ -79,6 +79,7 @@ public struct TrustBanner: View {
|
|
|
79
79
|
.foregroundColor(Color(hex: "484848"))
|
|
80
80
|
.lineLimit(2)
|
|
81
81
|
.font(.system(size: 13, weight: Font.Weight.regular))
|
|
82
|
+
.ignoreBoldTextSetting()
|
|
82
83
|
.lineSpacing(3)
|
|
83
84
|
.padding(.bottom, 8)
|
|
84
85
|
HStack {
|
|
@@ -86,6 +87,7 @@ public struct TrustBanner: View {
|
|
|
86
87
|
Text(applicationEnvironment.config?.trustBanner?.subContent[language ?? "vi"] as? String ?? defaultBanner.subContent[language ?? "vi"])
|
|
87
88
|
.foregroundColor(Colors.pink03)
|
|
88
89
|
.font(.system(size: 13, weight: .bold))
|
|
90
|
+
.ignoreBoldTextSetting()
|
|
89
91
|
.padding(.trailing, 4)
|
|
90
92
|
|
|
91
93
|
Icon(source: "arrow_chevron_right_small", color: Colors.pink03)
|