@momo-kits/native-kits 0.162.44 → 0.162.45
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.md +45 -3
- package/README.md +2 -2
- package/ios/Application/ApplicationEnvironment.swift +17 -0
- package/ios/Application/MaxApi.swift +30 -3
- package/ios/Avatar/Avatar.swift +4 -3
- package/ios/Badge/Badge.swift +6 -4
- package/ios/Badge/BadgeDot.swift +1 -1
- package/ios/Badge/BadgeRibbon.swift +0 -1
- package/ios/Button/Button.swift +15 -12
- package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
- package/ios/Carousel/Carousel.swift +29 -12
- package/ios/Checkbox/Checkbox.swift +7 -3
- package/ios/Chip/Chip.swift +5 -4
- package/ios/Collapse/Collapse.swift +70 -19
- package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
- package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
- package/ios/DateTimePicker/DateTimePicker.swift +54 -20
- package/ios/DateTimePicker/WheelPicker.swift +103 -105
- package/ios/Divider/Divider.swift +37 -4
- package/ios/Extensions/Color++.swift +38 -9
- package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
- package/ios/Extensions/SkeletonUICompat.swift +11 -0
- package/ios/Icon/Icon.swift +21 -23
- package/ios/Icon/Icons.swift +1329 -0
- package/ios/IconButton/IconButton.swift +15 -7
- package/ios/Image/Image.swift +7 -23
- package/ios/Information/Information.swift +27 -9
- package/ios/Input/ErrorView.swift +2 -2
- package/ios/Input/Input.swift +160 -147
- package/ios/Input/InputPhoneNumber.swift +12 -9
- package/ios/Input/InputSearch.swift +107 -77
- package/ios/Input/InputTextArea.swift +128 -145
- package/ios/InputDropDown/InputDropDown.swift +10 -7
- package/ios/InputMoney/InputMoney.swift +33 -11
- package/ios/InputOTP/InputOTP.swift +22 -12
- package/ios/Layout/Card.swift +50 -0
- package/ios/Layout/GridContext.swift +187 -3
- package/ios/Layout/Item.swift +5 -4
- package/ios/Layout/ScreenLayout.swift +385 -0
- package/ios/Layout/Section.swift +4 -100
- package/ios/Loader/Loader.swift +5 -9
- package/ios/Lottie/LottieView.swift +14 -2
- package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +169 -29
- package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
- package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
- package/ios/Navigation/BottomTab/DuoBarCompression.swift +104 -0
- package/ios/Navigation/BottomTab/DuoTabBar.swift +329 -0
- package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
- package/ios/Navigation/Component/Header.swift +545 -0
- package/ios/Navigation/Component/HeaderBackground.swift +154 -0
- package/ios/Navigation/Component/HeaderJourney.swift +73 -0
- package/ios/Navigation/Component/HeaderLocation.swift +48 -0
- package/ios/Navigation/Component/HeaderRight.swift +425 -0
- package/ios/Navigation/Component/HeaderTitle.swift +36 -0
- package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
- package/ios/Navigation/Component/SideBar.swift +183 -0
- package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
- package/ios/Navigation/HostBackGestureBridge.swift +78 -0
- package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
- package/ios/Navigation/Navigation.swift +204 -0
- package/ios/Navigation/NavigationContainer.swift +195 -0
- package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
- package/ios/Navigation/StackScreen.swift +930 -0
- package/ios/Navigation/StackTransition.swift +400 -0
- package/ios/Navigation/SwipeBackHaptic.swift +33 -0
- package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
- package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
- package/ios/Pagination/PaginationScroll.swift +2 -2
- package/ios/Popup/PopupDisplay.swift +72 -135
- package/ios/Popup/PopupInput.swift +10 -13
- package/ios/Popup/PopupLayout.swift +140 -0
- package/ios/Popup/PopupNotify.swift +59 -69
- package/ios/Popup/PopupPromotion.swift +31 -47
- package/ios/PopupView/FullscreenPopup.swift +13 -19
- package/ios/PopupView/PopupView.swift +1 -1
- package/ios/PopupView/Utils++.swift +0 -8
- package/ios/ProgressInfo/ProgressInfo.swift +4 -2
- package/ios/Radio/Radio.swift +2 -1
- package/ios/Rating/Rating.swift +9 -1
- package/ios/Resources/dot_loading.json +558 -0
- package/ios/Resources/lottie_circle_loader.json +1 -0
- package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
- package/ios/Skeleton/Skeleton.swift +22 -27
- package/ios/Slider/Slider.swift +16 -18
- package/ios/Stepper/Stepper.swift +6 -1
- package/ios/Steps/Steps.swift +8 -6
- package/ios/SuggestAction/SuggestAction.swift +5 -0
- package/ios/Swipeable/SwipeCell.swift +10 -6
- package/ios/Switch/Switch.swift +22 -20
- package/ios/TabView/TabView.swift +102 -21
- package/ios/Tag/Tag.swift +15 -4
- package/ios/Template/Logo/Logo.swift +2 -1
- package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
- package/ios/Title/Title.swift +6 -17
- package/ios/Tooltip/Tooltip.swift +29 -14
- package/ios/Typography/FontScale.swift +3 -3
- package/ios/Typography/Text.swift +81 -19
- package/ios/Uploader/Uploader.swift +3 -1
- package/ios/native-kits.podspec +2 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
- package/package.json +1 -1
- package/scripts/gen-ios-icons.mjs +36 -0
- package/ios/Application/Components.swift +0 -270
- package/ios/Application/ComposeApi.swift +0 -22
- package/ios/Application/HeaderRight.swift +0 -306
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
- package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
- package/ios/Application/Navigation/HeaderTitle.swift +0 -57
- package/ios/Application/Navigation/NavigationContainer.swift +0 -213
- package/ios/Application/Navigation/NavigationOptions.swift +0 -109
- package/ios/Application/Navigation/NavigatorManager.swift +0 -22
- package/ios/Application/Navigation/component/Header.swift +0 -178
- package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
- package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
- package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
- package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
- package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
- package/ios/Application/Screen.swift +0 -250
- package/ios/Application/StackScreen.swift +0 -492
- /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
//
|
|
2
|
+
// IPhoneDuoDemo.swift
|
|
3
|
+
// Example
|
|
4
|
+
//
|
|
5
|
+
// Mirrors RN `app/screens/IPhoneDuo`: the live screen layout, an extended header + search
|
|
6
|
+
// + footer screen, the Duo tab bars, the popup / bottom sheet behaviour and a grid to scroll.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import MoMoUIKits
|
|
10
|
+
import SwiftUI
|
|
11
|
+
|
|
12
|
+
private let gridRows: [[Int]] = [[12], [6, 6], [4, 4, 4], [3, 3, 3, 3]]
|
|
13
|
+
|
|
14
|
+
// enough rows to scroll, so the header glass and side bar can be checked
|
|
15
|
+
private let scrollRows: [[Int]] = Array(repeating: gridRows, count: 4).flatMap { $0 }
|
|
16
|
+
|
|
17
|
+
struct IPhoneDuoDemo: View {
|
|
18
|
+
var body: some View {
|
|
19
|
+
if #available(iOS 16.0, *) {
|
|
20
|
+
IPhoneDuoDemoContent()
|
|
21
|
+
} else {
|
|
22
|
+
Text("Requires iOS 16+").padding()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@available(iOS 16.0, *)
|
|
28
|
+
private struct IPhoneDuoDemoContent: View {
|
|
29
|
+
@EnvironmentObject private var navigator: Navigator
|
|
30
|
+
@Environment(\.screenLayout) private var layout
|
|
31
|
+
|
|
32
|
+
var body: some View {
|
|
33
|
+
VStack(alignment: .leading, spacing: Spacing.M) {
|
|
34
|
+
VStack(spacing: 0) {
|
|
35
|
+
ForEach(info, id: \.0) { label, value in
|
|
36
|
+
HStack {
|
|
37
|
+
MomoText(label, typography: .bodyDefaultRegular)
|
|
38
|
+
Spacer()
|
|
39
|
+
MomoText(value, typography: .headerSSemibold)
|
|
40
|
+
}
|
|
41
|
+
.padding(.vertical, Spacing.XS)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
.padding(.horizontal, Spacing.M)
|
|
45
|
+
|
|
46
|
+
VStack(spacing: Spacing.S) {
|
|
47
|
+
demoButton("Extended header + footer") {
|
|
48
|
+
navigator.push({ DuoGridScreen() }, options: extendedOptions)
|
|
49
|
+
}
|
|
50
|
+
demoButton("Glass tab bar") {
|
|
51
|
+
navigator.push({ DuoTabDemo(useShortcut: false) }, options: tabOptions)
|
|
52
|
+
}
|
|
53
|
+
demoButton("Glass tab bar + useShortcut") {
|
|
54
|
+
navigator.push({ DuoTabDemo(useShortcut: true) }, options: tabOptions)
|
|
55
|
+
}
|
|
56
|
+
demoButton("Glass tab bar + compress tab bar") {
|
|
57
|
+
navigator.push({ DuoTabDemo(useShortcut: true, compressTabBar: true) }, options: tabOptions)
|
|
58
|
+
}
|
|
59
|
+
demoButton("Bottom sheet: iPhone Duo safe area") {
|
|
60
|
+
navigator.showBottomSheet({ BottomSheetSafeArea() }, isSurface: true, header: .title("Safe area"))
|
|
61
|
+
}
|
|
62
|
+
demoButton("PopupNotify") {
|
|
63
|
+
navigator.showModal {
|
|
64
|
+
PopupNotify(props: PopupNotifyProps(
|
|
65
|
+
image: "https://static.momocdn.net/app/img/popup_permission/img_permission_popup_camera.png",
|
|
66
|
+
title: "Title",
|
|
67
|
+
description: String(repeating: "Check lại một nhịp auto chuyển sang dạng CTA. ", count: 8),
|
|
68
|
+
primary: PopupAction(title: "Đồng ý", onPress: { navigator.hideOverplay() }),
|
|
69
|
+
secondary: PopupAction(title: "Đóng", onPress: { navigator.hideOverplay() }),
|
|
70
|
+
onIconClose: { navigator.hideOverplay() }
|
|
71
|
+
))
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
.padding(.horizontal, Spacing.M)
|
|
76
|
+
|
|
77
|
+
DuoGrid()
|
|
78
|
+
}
|
|
79
|
+
.padding(.vertical, Spacing.M)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private var info: [(String, String)] {
|
|
83
|
+
let insets = layout.insets
|
|
84
|
+
return [
|
|
85
|
+
("iPhone Duo", layout.isIPhoneDuo ? "yes" : "no"),
|
|
86
|
+
("Window", "\(Int(layout.width)) x \(Int(layout.height))"),
|
|
87
|
+
("Safe area (T R B L)", "\(Int(insets.top)) \(Int(insets.trailing)) \(Int(insets.bottom)) \(Int(insets.leading))"),
|
|
88
|
+
("Content inset (L R)", "\(Int(layout.insetLeft)) \(Int(layout.insetRight))"),
|
|
89
|
+
("Content width", "\(Int(layout.contentWidth))"),
|
|
90
|
+
("Header height", "\(Int(layout.headerHeight))"),
|
|
91
|
+
("Side bar", layout.sideBar.map { "\($0.edge == .left ? "left" : "right"), \(Int($0.width))pt, top \(Int($0.top))" } ?? "none"),
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private var extendedOptions: NavigationOptions {
|
|
96
|
+
NavigationOptions(
|
|
97
|
+
title: "Extended",
|
|
98
|
+
headerType: .extended,
|
|
99
|
+
headerRightData: HeaderRightData(useShortcut: true),
|
|
100
|
+
footer: { AnyView(MoMoUIKits.Button(title: "Footer action", action: {}, size: .large)) }
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private var tabOptions: NavigationOptions {
|
|
105
|
+
NavigationOptions(title: "Duo tabs", headerType: .none, scrollable: false)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private func demoButton(_ title: String, action: @escaping () -> Void) -> some View {
|
|
109
|
+
MoMoUIKits.Button(title: title, action: action, type: .secondary, size: .small)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private struct DuoGridScreen: View {
|
|
114
|
+
var body: some View {
|
|
115
|
+
DuoGrid().padding(.vertical, Spacing.M)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private struct DuoGrid: View {
|
|
120
|
+
var body: some View {
|
|
121
|
+
VStack(spacing: Spacing.M) {
|
|
122
|
+
ForEach(scrollRows.indices, id: \.self) { row in
|
|
123
|
+
SectionView {
|
|
124
|
+
ForEach(scrollRows[row].indices, id: \.self) { index in
|
|
125
|
+
let span = scrollRows[row][index]
|
|
126
|
+
ItemView(widthSpan: span) {
|
|
127
|
+
MomoText("span \(span)", typography: .descriptionDefaultRegular)
|
|
128
|
+
.frame(maxWidth: .infinity)
|
|
129
|
+
.frame(height: 48)
|
|
130
|
+
.background(Colors.black04)
|
|
131
|
+
.clipShape(RoundedRectangle(cornerRadius: Radius.S))
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Markers on the content edges show the side safe area inside the sheet.
|
|
141
|
+
private struct BottomSheetSafeArea: View {
|
|
142
|
+
@Environment(\.screenLayout) private var layout
|
|
143
|
+
|
|
144
|
+
var body: some View {
|
|
145
|
+
VStack(alignment: .leading, spacing: Spacing.XS) {
|
|
146
|
+
HStack {
|
|
147
|
+
marker("← content edge")
|
|
148
|
+
Spacer()
|
|
149
|
+
marker("content edge →")
|
|
150
|
+
}
|
|
151
|
+
.padding(.bottom, Spacing.S)
|
|
152
|
+
MomoText("Window: \(Int(layout.width)) × \(Int(layout.height))", typography: .bodyDefaultRegular)
|
|
153
|
+
MomoText(
|
|
154
|
+
"Insets: top \(Int(layout.insets.top)) · right \(Int(layout.insets.trailing)) · bottom \(Int(layout.insets.bottom)) · left \(Int(layout.insets.leading))",
|
|
155
|
+
typography: .bodyDefaultRegular
|
|
156
|
+
)
|
|
157
|
+
MomoText(
|
|
158
|
+
"On iPhone Duo: the outer display keeps a full-width sheet with content inside the side safe area; the inner display centers the sheet, still attached to the bottom edge. Other devices are unchanged.",
|
|
159
|
+
typography: .descriptionDefaultRegular,
|
|
160
|
+
color: Colors.black12
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
.padding(.vertical, Spacing.M)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private func marker(_ text: String) -> some View {
|
|
167
|
+
MomoText(text, typography: .labelSMedium, color: Colors.pink03)
|
|
168
|
+
.padding(.vertical, Spacing.XS)
|
|
169
|
+
.padding(.horizontal, Spacing.S)
|
|
170
|
+
.background(Colors.black04)
|
|
171
|
+
.clipShape(RoundedRectangle(cornerRadius: Radius.XS))
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Usage demo for the navigation flow. Mirrors Compose's `NavigationUsage`:
|
|
6
6
|
// exercises push / present / replace / reset, the modal & bottom-sheet
|
|
7
|
-
// overplays, the snack bar, the
|
|
7
|
+
// overplays, the snack bar, the header titles, runtime options and the bottom-tab layout.
|
|
8
8
|
//
|
|
9
9
|
|
|
10
10
|
import MoMoUIKits
|
|
@@ -149,6 +149,52 @@ private struct NavigationDemoContent: View {
|
|
|
149
149
|
)
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
row("Header Location Title") {
|
|
153
|
+
navigator.push(
|
|
154
|
+
{ DemoLeafScreen(title: "Location title", color: Colors.blue09) },
|
|
155
|
+
options: NavigationOptions(
|
|
156
|
+
headerTitle: .location(
|
|
157
|
+
HeaderLocationData(description: "Deliver to", location: "123 Nguyen Hue, District 1", onPress: { print("location") })
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
row("Header Journey Title") {
|
|
164
|
+
navigator.push(
|
|
165
|
+
{ DemoLeafScreen(title: "Journey title", color: Colors.pink09) },
|
|
166
|
+
options: NavigationOptions(
|
|
167
|
+
headerTitle: .journey(
|
|
168
|
+
HeaderJourneyData(start: "Ho Chi Minh", end: "Ha Noi", description: "Mon, 12/10 · 1 adult", icon: "16_arrow_chevron_right", onPress: { print("journey") })
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
row("Update Options At Runtime") {
|
|
175
|
+
navigator.push(
|
|
176
|
+
{ UpdateOptionsScreen() },
|
|
177
|
+
options: NavigationOptions(title: "Before update")
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
row("Bottom Sheet With Footer") {
|
|
182
|
+
navigator.showBottomSheet({
|
|
183
|
+
VStack(spacing: 12) {
|
|
184
|
+
ForEach(0..<6, id: \.self) { i in
|
|
185
|
+
MomoText("Bottom sheet row \(i + 1)")
|
|
186
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
.padding(Spacing.L)
|
|
190
|
+
}, header: .title("Footer"), draggable: false, footer: {
|
|
191
|
+
AnyView(
|
|
192
|
+
MoMoUIKits.Button(title: "Confirm", action: { navigator.hideOverplay() }, type: .primary, size: .large)
|
|
193
|
+
.padding(.horizontal, Spacing.M)
|
|
194
|
+
)
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
152
198
|
row("Header Animated") {
|
|
153
199
|
navigator.push(
|
|
154
200
|
{ AnimatedHeaderDemoContent() },
|
|
@@ -177,6 +223,36 @@ private struct NavigationDemoContent: View {
|
|
|
177
223
|
|
|
178
224
|
// MARK: - Leaf / presented screens
|
|
179
225
|
|
|
226
|
+
/// A screen changing its own header and footer after it is shown (Compose `LocalNavigation`).
|
|
227
|
+
@available(iOS 16.0, *)
|
|
228
|
+
private struct UpdateOptionsScreen: View {
|
|
229
|
+
@EnvironmentObject private var navigation: Navigation
|
|
230
|
+
@EnvironmentObject private var navigator: Navigator
|
|
231
|
+
|
|
232
|
+
var body: some View {
|
|
233
|
+
VStack(spacing: 16) {
|
|
234
|
+
MoMoUIKits.Button(title: "Change title", action: {
|
|
235
|
+
navigation.setOptions { $0.title = "Updated at \(Date().formatted(date: .omitted, time: .standard))" }
|
|
236
|
+
}, type: .primary, size: .large)
|
|
237
|
+
MoMoUIKits.Button(title: "Add footer", action: {
|
|
238
|
+
navigation.setOptions {
|
|
239
|
+
$0.footer = { AnyView(MoMoUIKits.Button(title: "Footer action", action: {}, type: .primary, size: .large)) }
|
|
240
|
+
}
|
|
241
|
+
}, type: .secondary, size: .large)
|
|
242
|
+
MoMoUIKits.Button(title: "Show snack bar above the footer", action: {
|
|
243
|
+
navigator.showSnackBar(.custom(duration: 2.0) {
|
|
244
|
+
MomoText("Snack bar", color: Colors.black01)
|
|
245
|
+
.frame(maxWidth: .infinity, minHeight: 48)
|
|
246
|
+
.background(Colors.black17)
|
|
247
|
+
.clipShape(RoundedRectangle(cornerRadius: Radius.M))
|
|
248
|
+
.padding(.horizontal, Spacing.M)
|
|
249
|
+
})
|
|
250
|
+
}, type: .secondary, size: .large)
|
|
251
|
+
}
|
|
252
|
+
.padding(16)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
180
256
|
@available(iOS 16.0, *)
|
|
181
257
|
private struct DemoLeafScreen: View {
|
|
182
258
|
@EnvironmentObject private var navigator: Navigator
|
|
@@ -26,7 +26,7 @@ private struct PopupNotifyPreview: View {
|
|
|
26
26
|
private func show() {
|
|
27
27
|
navigator.showModal {
|
|
28
28
|
PopupNotify(props: PopupNotifyProps(
|
|
29
|
-
image: "https://
|
|
29
|
+
image: "https://static.momocdn.net/app/img/popup_permission/img_permission_popup_camera.png",
|
|
30
30
|
title: "Title",
|
|
31
31
|
description: "Check lại một nhịp auto chuyển sang dạng CTA.",
|
|
32
32
|
error: "9004",
|
|
@@ -50,7 +50,7 @@ private let popupDirectionList: [PlaygroundOption] = [
|
|
|
50
50
|
@available(iOS 16.0, *)
|
|
51
51
|
private struct PopupNotifyPlayground: View {
|
|
52
52
|
@EnvironmentObject private var navigator: Navigator
|
|
53
|
-
@State private var image = "https://
|
|
53
|
+
@State private var image = "https://static.momocdn.net/app/img/popup_permission/img_permission_popup_camera.png"
|
|
54
54
|
@State private var title = "Title"
|
|
55
55
|
@State private var description = "Check lại một nhịp auto chuyển sang dạng CTA."
|
|
56
56
|
@State private var error = "9004"
|
|
@@ -37,6 +37,7 @@ private struct PopupPromotionPreview: View {
|
|
|
37
37
|
private func show(_ url: String) {
|
|
38
38
|
navigator.showModal {
|
|
39
39
|
PopupPromotion(imageUrl: url, onIconClose: { navigator.hideOverplay() }, onAction: { navigator.hideOverplay() })
|
|
40
|
+
.padding(.horizontal, Spacing.M)
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
}
|
|
@@ -62,6 +63,7 @@ private struct PopupPromotionPlayground: View {
|
|
|
62
63
|
private func show() {
|
|
63
64
|
navigator.showModal {
|
|
64
65
|
PopupPromotion(imageUrl: imageUrl, onIconClose: { navigator.hideOverplay() }, onAction: { navigator.hideOverplay() })
|
|
66
|
+
.padding(.horizontal, Spacing.M)
|
|
65
67
|
}
|
|
66
68
|
}
|
|
67
69
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Generates ios/Icon/Icons.swift from the Compose icon map so both kits ship the same
|
|
2
|
+
// compiled-in icons. Run from anywhere: `node scripts/gen-ios-icons.mjs`.
|
|
3
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
const source = join(root, "compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt");
|
|
9
|
+
const target = join(root, "ios/Icon/Icons.swift");
|
|
10
|
+
|
|
11
|
+
const kotlin = readFileSync(source, "utf8");
|
|
12
|
+
const kotlinString = String.raw`"((?:[^"\\]|\\.)*)"`;
|
|
13
|
+
const unescapeKotlin = (s) => s.replace(/\\(["\\$])/g, "$1");
|
|
14
|
+
const swiftString = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
15
|
+
|
|
16
|
+
const icons = [...kotlin.matchAll(new RegExp(`^\\s*${kotlinString} to ${kotlinString}`, "gm"))]
|
|
17
|
+
.map(([, key, url]) => [unescapeKotlin(key), unescapeKotlin(url)]);
|
|
18
|
+
const noThemeBlock = kotlin.match(/val noThemeIcons = listOf\(([^)]*)\)/);
|
|
19
|
+
const noTheme = noThemeBlock
|
|
20
|
+
? [...noThemeBlock[1].matchAll(new RegExp(kotlinString, "g"))].map(([, s]) => unescapeKotlin(s))
|
|
21
|
+
: [];
|
|
22
|
+
|
|
23
|
+
if (icons.length === 0) throw new Error(`No icons parsed from ${source}`);
|
|
24
|
+
if (new Set(icons.map(([key]) => key)).size !== icons.length) throw new Error("Duplicate icon keys");
|
|
25
|
+
|
|
26
|
+
const swift = `// Generated by scripts/gen-ios-icons.mjs from compose/.../utils/Icons.kt. Do not edit by hand.
|
|
27
|
+
|
|
28
|
+
let kitIcons: [String: String] = [
|
|
29
|
+
${icons.map(([key, url]) => ` ${swiftString(key)}: ${swiftString(url)},`).join("\n")}
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
let noThemeIcons: [String] = [${noTheme.map(swiftString).join(", ")}]
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
writeFileSync(target, swift);
|
|
36
|
+
console.log(`Wrote ${icons.length} icons (${noTheme.length} untinted) to ${target}`);
|
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Components.swift
|
|
3
|
-
// Pods
|
|
4
|
-
//
|
|
5
|
-
// Created by sophia on 24/1/25.
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
import SwiftUI
|
|
9
|
-
|
|
10
|
-
// MARK: - Enums
|
|
11
|
-
|
|
12
|
-
public enum TitlePosition {
|
|
13
|
-
case left, center
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
public enum HeaderType {
|
|
17
|
-
case `default`, extended, none
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
public enum AnimatedHeaderRatio {
|
|
21
|
-
case ratio16_9
|
|
22
|
-
case ratio1_1
|
|
23
|
-
case ratio3_2
|
|
24
|
-
|
|
25
|
-
var value: CGFloat {
|
|
26
|
-
switch self {
|
|
27
|
-
case .ratio16_9: return 16.0 / 9.0
|
|
28
|
-
case .ratio1_1: return 1.0
|
|
29
|
-
case .ratio3_2: return 3.0 / 2.0
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// MARK: - Animated Header Config
|
|
36
|
-
|
|
37
|
-
public struct AnimatedHeader {
|
|
38
|
-
public var aspectRatio: AnimatedHeaderRatio = .ratio16_9
|
|
39
|
-
public var isSurface: Bool = true
|
|
40
|
-
public var composable: (_ scrollState: CGFloat) -> AnyView = { _ in AnyView(EmptyView()) }
|
|
41
|
-
|
|
42
|
-
public init(
|
|
43
|
-
aspectRatio: AnimatedHeaderRatio = .ratio16_9,
|
|
44
|
-
isSurface: Bool = true,
|
|
45
|
-
composable: @escaping (_ scrollState: CGFloat) -> AnyView = { _ in AnyView(EmptyView()) }
|
|
46
|
-
) {
|
|
47
|
-
self.aspectRatio = aspectRatio
|
|
48
|
-
self.isSurface = isSurface
|
|
49
|
-
self.composable = composable
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// MARK: - Utils
|
|
54
|
-
|
|
55
|
-
func safeAreaTopInset() -> CGFloat {
|
|
56
|
-
if #available(iOS 15.0, *) {
|
|
57
|
-
return UIApplication.shared.connectedScenes
|
|
58
|
-
.compactMap { ($0 as? UIWindowScene)?.keyWindow?.safeAreaInsets.top }
|
|
59
|
-
.first ?? 0
|
|
60
|
-
} else {
|
|
61
|
-
return UIApplication.shared.connectedScenes
|
|
62
|
-
.compactMap { ($0 as? UIWindowScene)?.windows.first?.safeAreaInsets.top }
|
|
63
|
-
.first ?? 0
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// MARK: - Header Background
|
|
68
|
-
|
|
69
|
-
public struct HeaderBackground: View {
|
|
70
|
-
var headerType: HeaderType = .default
|
|
71
|
-
var scrollState: CGFloat
|
|
72
|
-
var headerTransparent: Bool = false
|
|
73
|
-
var fullScreenContent: Bool = false
|
|
74
|
-
|
|
75
|
-
public init(
|
|
76
|
-
headerType: HeaderType = .default,
|
|
77
|
-
scrollState: CGFloat = 0,
|
|
78
|
-
headerTransparent: Bool = false,
|
|
79
|
-
fullScreenContent: Bool = false
|
|
80
|
-
) {
|
|
81
|
-
self.headerType = headerType
|
|
82
|
-
self.scrollState = scrollState
|
|
83
|
-
self.headerTransparent = headerTransparent
|
|
84
|
-
self.fullScreenContent = fullScreenContent
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
public var body: some View {
|
|
88
|
-
let statusBarHeight = safeAreaTopInset()
|
|
89
|
-
let opacity = max(0, 1 - scrollState / 200)
|
|
90
|
-
let height = fullScreenContent ? 0 : statusBarHeight + 52
|
|
91
|
-
let backgroundColor = headerTransparent ? Color.clear : Colors.black01
|
|
92
|
-
|
|
93
|
-
Group {
|
|
94
|
-
switch headerType {
|
|
95
|
-
case .default:
|
|
96
|
-
ZStack(alignment: .bottom) {
|
|
97
|
-
Rectangle()
|
|
98
|
-
.fill(backgroundColor)
|
|
99
|
-
.opacity(headerTransparent ? 0 : opacity)
|
|
100
|
-
.frame(height: height)
|
|
101
|
-
.shadow(color: Colors.black20.opacity(0.2), radius: 10, x: 0, y: -2)
|
|
102
|
-
.background(backgroundColor)
|
|
103
|
-
.overlay(
|
|
104
|
-
headerTransparent ? AnyView(EmptyView()) :
|
|
105
|
-
AnyView(
|
|
106
|
-
Rectangle()
|
|
107
|
-
.fill(LinearGradient(
|
|
108
|
-
gradient: Gradient(colors: [
|
|
109
|
-
Color(red: 1, green: 0.8, blue: 0.87),
|
|
110
|
-
Color(red: 1, green: 0.8, blue: 0.87).opacity(0.5)
|
|
111
|
-
]),
|
|
112
|
-
startPoint: .top,
|
|
113
|
-
endPoint: .bottom))
|
|
114
|
-
.opacity(opacity)
|
|
115
|
-
.frame(height: height))
|
|
116
|
-
)
|
|
117
|
-
if !headerTransparent {
|
|
118
|
-
Rectangle()
|
|
119
|
-
.fill(Color.black.opacity(0.1))
|
|
120
|
-
.frame(height: 1)
|
|
121
|
-
.frame(maxWidth: .infinity)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
case .extended:
|
|
126
|
-
ZStack {
|
|
127
|
-
if !headerTransparent {
|
|
128
|
-
Colors.black01
|
|
129
|
-
LinearGradient(
|
|
130
|
-
gradient: Gradient(colors: [
|
|
131
|
-
Color(red: 1, green: 0.8, blue: 0.87),
|
|
132
|
-
Color(red: 1, green: 0.8, blue: 0.87).opacity(0)
|
|
133
|
-
]),
|
|
134
|
-
startPoint: .top,
|
|
135
|
-
endPoint: .bottom
|
|
136
|
-
)
|
|
137
|
-
.opacity(opacity)
|
|
138
|
-
.frame(height: 154)
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
case .none:
|
|
143
|
-
EmptyView()
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// MARK: - Bottom-tab root preference
|
|
150
|
-
|
|
151
|
-
/// Set by `BottomTab` on appear so its enclosing `StackScreen` can detect that
|
|
152
|
-
/// it's hosting a self-contained bottom-tab surface and skip its own bottom
|
|
153
|
-
/// safe-area reservation — mirrors Compose's `Navigation.markAsBottomTabRoot()`
|
|
154
|
-
/// (`navigation.isBottomTabRoot`, read by `StackScreen.kt`'s `MainContent`).
|
|
155
|
-
/// Without this, `BottomTab`'s own internal safe-area handling gets an extra,
|
|
156
|
-
/// unwanted inset stacked on top of it by the outer `StackScreen`, pushing the
|
|
157
|
-
/// tab bar up higher than the true bottom inset.
|
|
158
|
-
public struct IsBottomTabRootKey: PreferenceKey {
|
|
159
|
-
public static var defaultValue: Bool = false
|
|
160
|
-
public static func reduce(value: inout Bool, nextValue: () -> Bool) {
|
|
161
|
-
value = value || nextValue()
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// MARK: - Header View
|
|
166
|
-
|
|
167
|
-
public struct Header: View {
|
|
168
|
-
var headerType: HeaderType = .default
|
|
169
|
-
var titlePosition: TitlePosition
|
|
170
|
-
var title: String
|
|
171
|
-
/// Optional rich title (mirrors Compose's `HeaderTitle`); `.user` renders an
|
|
172
|
-
/// avatar group instead of `title`. Defaults to nil → plain `title` text.
|
|
173
|
-
var headerTitle: HeaderTitle?
|
|
174
|
-
var headerRight: (()->any View)? = {HeaderRight()}
|
|
175
|
-
var goBack: (() -> Void)?
|
|
176
|
-
var opacity: CGFloat = 1
|
|
177
|
-
var animatedHeader: AnimatedHeader?
|
|
178
|
-
var scrollState: CGFloat = 0
|
|
179
|
-
var inputSearchProps: InputSearchProps?
|
|
180
|
-
var tintColor: Color?
|
|
181
|
-
/// Title opacity; used by the animated search header to fade the title out on scroll.
|
|
182
|
-
var titleOpacity: CGFloat = 1
|
|
183
|
-
|
|
184
|
-
public init(
|
|
185
|
-
headerType: HeaderType = .default,
|
|
186
|
-
titlePosition: TitlePosition,
|
|
187
|
-
title: String,
|
|
188
|
-
headerTitle: HeaderTitle? = nil,
|
|
189
|
-
headerRight: (()->any View)? = {HeaderRight()},
|
|
190
|
-
goBack: (() -> Void)? = nil,
|
|
191
|
-
opacity: CGFloat = 1,
|
|
192
|
-
animatedHeader: AnimatedHeader? = nil,
|
|
193
|
-
scrollState: CGFloat = 0,
|
|
194
|
-
inputSearchProps: InputSearchProps? = nil,
|
|
195
|
-
tintColor: Color? = nil,
|
|
196
|
-
titleOpacity: CGFloat = 1
|
|
197
|
-
) {
|
|
198
|
-
self.headerType = headerType
|
|
199
|
-
self.titlePosition = titlePosition
|
|
200
|
-
self.title = title
|
|
201
|
-
self.headerTitle = headerTitle
|
|
202
|
-
self.headerRight = headerRight
|
|
203
|
-
self.goBack = goBack
|
|
204
|
-
self.opacity = opacity
|
|
205
|
-
self.animatedHeader = animatedHeader
|
|
206
|
-
self.scrollState = scrollState
|
|
207
|
-
self.inputSearchProps = inputSearchProps
|
|
208
|
-
self.tintColor = tintColor
|
|
209
|
-
self.titleOpacity = titleOpacity
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
public var body: some View {
|
|
213
|
-
|
|
214
|
-
let backgroundButtonColor: Color = tintColor == Colors.black01 ? Colors.black20.opacity(0.6) : Colors.black01.opacity(0.6)
|
|
215
|
-
let borderColor: Color = tintColor == Colors.black01 ? Colors.black01.opacity(0.2) : Colors.black20.opacity(0.2)
|
|
216
|
-
|
|
217
|
-
if headerType != .none {
|
|
218
|
-
VStack(spacing: 0) {
|
|
219
|
-
ZStack {
|
|
220
|
-
// MARK: - Left and Right Controls
|
|
221
|
-
HStack {
|
|
222
|
-
if let goBack = goBack {
|
|
223
|
-
SwiftUI.Button(action: goBack) {
|
|
224
|
-
Circle()
|
|
225
|
-
.stroke(borderColor, lineWidth: 0.2)
|
|
226
|
-
.background(Circle().fill(backgroundButtonColor))
|
|
227
|
-
.frame(width: 28, height: 28)
|
|
228
|
-
.overlay(
|
|
229
|
-
Icon(source: "arrow-back", size: 20, color: tintColor ?? Colors.black17)
|
|
230
|
-
)
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
Spacer()
|
|
235
|
-
|
|
236
|
-
if let headerRight = headerRight {
|
|
237
|
-
AnyView(headerRight())
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
.padding(.horizontal, 12)
|
|
241
|
-
|
|
242
|
-
// MARK: - Title or Search
|
|
243
|
-
HStack {
|
|
244
|
-
if titlePosition == .left && goBack != nil {
|
|
245
|
-
Spacer().frame(width: 38)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if let inputProps = inputSearchProps {
|
|
249
|
-
InputSearch(props: inputProps)
|
|
250
|
-
} else if case let .user(userData)? = headerTitle {
|
|
251
|
-
if #available(iOS 16.0, *) {
|
|
252
|
-
HeaderUserView(userData)
|
|
253
|
-
}
|
|
254
|
-
} else {
|
|
255
|
-
MomoText(headerTitle?.defaultTitle ?? title, typography: .headerDefaultBold, color: tintColor ?? Colors.black17, maxLines: 1)
|
|
256
|
-
.opacity(titleOpacity)
|
|
257
|
-
.frame(maxWidth: titlePosition == .center ? UIScreen.main.bounds.width - 252 : nil)
|
|
258
|
-
.frame(maxWidth: titlePosition == .center ? .infinity : UIScreen.main.bounds.width - 172, alignment: titlePosition == .center ? .center : .leading)
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
Spacer()
|
|
262
|
-
}
|
|
263
|
-
.padding(.horizontal)
|
|
264
|
-
}
|
|
265
|
-
.background(Color.clear)
|
|
266
|
-
.frame(height: 52)
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
public protocol ComposeApi {
|
|
4
|
-
func request(funcName: String, params: Any, _ completion: ((String) -> Void)?)
|
|
5
|
-
func request(funcName: String, params: Any) -> String
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
// Default implementation
|
|
9
|
-
public class ComposeApiImpl: ComposeApi {
|
|
10
|
-
public static let shared = ComposeApiImpl()
|
|
11
|
-
|
|
12
|
-
private init() {}
|
|
13
|
-
|
|
14
|
-
public func request(funcName: String, params: Any, _ completion: ((String) -> Void)?) {
|
|
15
|
-
// Implement your API request logic here
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
public func request(funcName: String, params: Any) -> String {
|
|
19
|
-
// Implement your API request logic here
|
|
20
|
-
return ""
|
|
21
|
-
}
|
|
22
|
-
}
|