@momo-kits/native-kits 0.112.1-beta.1 → 0.112.1-beta.10

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.
@@ -178,6 +178,7 @@ fun Header(
178
178
  val statusBarHeight = AppStatusBar.current ?: getStatusBarHeight()
179
179
  val tintIconColor = AppTheme.current.colors.text.default
180
180
  val backgroundButton = Colors.black_01.copy(alpha = 0.6f)
181
+ val miniAppContext = ApplicationContext.current
181
182
 
182
183
  var colorFraction by remember { mutableStateOf(0f) }
183
184
 
@@ -242,10 +243,12 @@ fun Header(
242
243
  }
243
244
  }
244
245
 
245
- Box(Modifier.graphicsLayer {
246
- alpha = if (animatedHeader !== null) opacity ?: 1f else 1f
247
- }) {
248
- headerRight?.invoke()
246
+ if (inputSearchProps == null && miniAppContext != null) {
247
+ Box(Modifier.graphicsLayer {
248
+ alpha = if (animatedHeader !== null) opacity ?: 1f else 1f
249
+ }) {
250
+ headerRight?.invoke()
251
+ }
249
252
  }
250
253
  }
251
254
 
@@ -1,4 +1,4 @@
1
- package vn.momo.kits.components
1
+ package vn.momo.kits.application
2
2
 
3
3
  import androidx.compose.foundation.background
4
4
  import androidx.compose.foundation.border
@@ -23,32 +23,45 @@ import androidx.compose.ui.Modifier
23
23
  import androidx.compose.ui.draw.clip
24
24
  import androidx.compose.ui.graphics.Color
25
25
  import androidx.compose.ui.unit.dp
26
- import vn.momo.kits.application.ComposeApi
27
- import vn.momo.kits.application.PlatformApi
26
+ import vn.momo.kits.components.BadgeDot
27
+ import vn.momo.kits.components.DotSize
28
+ import vn.momo.kits.components.Icon
28
29
  import vn.momo.kits.const.Colors
29
30
 
30
31
  data class HeaderRightData(
31
- val useOnBoarding: Boolean = false,
32
- val buttonOnBoarding: String? = null,
33
- val onPress: (() -> Unit)? = null,
34
- val tintColor: Color = Colors.black_20,
35
- val preventClose: Any? = null,
36
32
  val useShortcut: Boolean = false,
37
33
  val useMore: Boolean = false,
38
34
  val tools: List<ToolGroup> = emptyList(),
39
- val appId: String = ""
35
+ val toolCallback: ((String) -> Unit)? = { _ -> }
40
36
  )
41
37
 
42
38
  data class Tool(
43
39
  val key: String,
44
40
  val icon: String,
45
- val onPress: () -> Unit,
46
- val showBadge: Boolean = false
47
- )
41
+ val showBadge: Boolean = false,
42
+ val name: Map<String, String> = emptyMap()
43
+ ) {
44
+ fun toMap(): Map<String, Any> {
45
+ return mapOf(
46
+ "key" to key,
47
+ "icon" to icon,
48
+ "showBadge" to showBadge,
49
+ "name" to name
50
+ )
51
+ }
52
+ }
48
53
 
49
54
  data class ToolGroup(
55
+ val title: Map<String, String> = emptyMap(),
50
56
  val items: List<Tool>
51
- )
57
+ ) {
58
+ fun toMap(): Map<String, Any> {
59
+ return mapOf(
60
+ "title" to title,
61
+ "items" to items.map { it.toMap() }
62
+ )
63
+ }
64
+ }
52
65
 
53
66
  data class NavigationButtonConfig(
54
67
  val icon: String,
@@ -77,18 +90,46 @@ fun ToolkitHeaderRight(headerRight: HeaderRightData? = null) {
77
90
  var isFavorite by remember { mutableStateOf(false) }
78
91
  val isLoading by remember { mutableStateOf(false) }
79
92
  val api = PlatformApi.current as? ComposeApi
93
+ val context = ApplicationContext.current
80
94
 
81
95
  fun onPressShortcut() {
82
- isFavorite = !isFavorite
96
+ api?.request("onToolAction", mapOf("item" to mapOf("key" to "onFavorite"))) {
97
+ isFavorite = !isFavorite
98
+ }
83
99
  }
84
- fun onPressHelpCenter(){
85
- api?.request("showHelpCenter", "")
100
+
101
+ fun onPressHelpCenter() {
102
+ val paramMap = mapOf(
103
+ "appId" to context?.appId,
104
+ "code" to context?.appCode,
105
+ "name" to context?.appName,
106
+ "icon" to context?.appIcon,
107
+ "description" to context?.description
108
+ )
109
+ api?.request("showHelpCenter", paramMap) {
110
+
111
+ }
86
112
  }
87
- fun onPressClose(){
113
+
114
+ fun onPressClose() {
88
115
  api?.request("dismiss", "")
89
116
  }
117
+
90
118
  fun onPressMore() {
91
- api?.request("showTools", "")
119
+ api?.request("showTools",
120
+ mapOf(
121
+ "tools" to headerRight?.tools?.map { it.toMap() },
122
+ "context" to mapOf(
123
+ "appId" to context?.appId,
124
+ "code" to context?.appCode,
125
+ "name" to context?.appName,
126
+ "icon" to context?.appIcon,
127
+ "description" to context?.description
128
+ )
129
+ )
130
+ ) {
131
+
132
+ }
92
133
  }
93
134
 
94
135
  fun getNavigationButtonConfig(
@@ -109,7 +150,7 @@ fun ToolkitHeaderRight(headerRight: HeaderRightData? = null) {
109
150
  if (singleTool != null) {
110
151
  icon = singleTool.icon
111
152
  onClickHandler = {
112
- singleTool.onPress()
153
+ headerRight?.toolCallback?.invoke(singleTool.key)
113
154
  }
114
155
  }
115
156
  }
@@ -22,40 +22,20 @@ import vn.momo.kits.const.defaultTheme
22
22
  import vn.momo.kits.platform.disableOverscroll
23
23
  import vn.momo.kits.platform.getStatusBarHeight
24
24
 
25
- val PlatformApi = staticCompositionLocalOf<Any?> { null }
26
-
27
25
  interface ComposeApi {
28
- fun request(funcName: String, params: String?): String
29
- fun request(funcName: String, params: String?, onResponse: ((String) -> Unit)?): String
30
- fun requestCallback(funcName: String, params: String?, onResponse: ((String) -> Unit)?)
26
+ fun request(funcName: String, params: Any?): String
27
+ fun request(funcName: String, params: Any?, onResponse: ((String) -> Unit)?): String
28
+ fun requestCallback(funcName: String, params: Any?, onResponse: ((String) -> Unit)?)
31
29
  fun removeCallback(id: String)
32
30
  }
33
31
 
34
- val AppNavigator = staticCompositionLocalOf<Navigator> {
35
- error("no navigator provided!")
36
- }
37
-
38
32
  class Navigator {
39
- fun push(content: @Composable () -> Unit) {
40
- }
41
-
42
- fun present(content: @Composable () -> Unit) {
43
-
44
- }
45
-
46
- fun reset(content: @Composable () -> Unit) {
47
-
48
- }
49
-
50
- fun pop() {
51
- }
52
-
53
- fun replace(content: @Composable () -> Unit) {
54
- }
55
-
56
- fun popToTop() {
57
- }
58
-
33
+ fun push(content: @Composable () -> Unit) {}
34
+ fun present(content: @Composable () -> Unit) {}
35
+ fun reset(content: @Composable () -> Unit) {}
36
+ fun pop() {}
37
+ fun replace(content: @Composable () -> Unit) {}
38
+ fun popToTop() {}
59
39
  fun showModal(
60
40
  onClose: () -> Unit = {},
61
41
  canBackgroundClose: Boolean = true,
@@ -63,8 +43,25 @@ class Navigator {
63
43
  ) {
64
44
  }
65
45
 
66
- fun showBottomSheet(content: @Composable () -> Unit) {
67
- }
46
+ fun showBottomSheet(content: @Composable () -> Unit) {}
47
+ }
48
+
49
+ data class MiniAppContext(
50
+ val appIcon: String = "",
51
+ val appName: Any? = null,
52
+ val appId: String = "",
53
+ val appCode: String = "",
54
+ val description: Any? = null,
55
+ )
56
+
57
+ val PlatformApi = staticCompositionLocalOf<Any?> { null }
58
+
59
+ val AppNavigator = staticCompositionLocalOf<Navigator> {
60
+ error("no navigator provided!")
61
+ }
62
+
63
+ val ApplicationContext = staticCompositionLocalOf<MiniAppContext?> {
64
+ error("no context provided!")
68
65
  }
69
66
 
70
67
  @Composable
@@ -72,7 +69,8 @@ fun ApplicationContainer(
72
69
  theme: Theme = defaultTheme,
73
70
  composeApi: ComposeApi? = null,
74
71
  statusBarHeight: Dp? = AppStatusBar.current ?: getStatusBarHeight(),
75
- content: @Composable () -> Unit
72
+ applicationContext: MiniAppContext? = null,
73
+ content: @Composable () -> Unit,
76
74
  ) {
77
75
  var appTheme by remember { mutableStateOf(theme) }
78
76
  val coroutineScope = rememberCoroutineScope()
@@ -97,8 +95,7 @@ fun ApplicationContainer(
97
95
  }
98
96
  }
99
97
  }
100
- }
101
- catch (e: Exception) {
98
+ } catch (e: Exception) {
102
99
  print("@@ == NavigationContainer fetch config error $e")
103
100
  }
104
101
  }
@@ -109,6 +106,7 @@ fun ApplicationContainer(
109
106
  PlatformApi provides composeApi,
110
107
  AppNavigator provides Navigator(),
111
108
  AppStatusBar provides statusBarHeight,
109
+ ApplicationContext provides applicationContext,
112
110
  ) {
113
111
  content()
114
112
  }
@@ -40,7 +40,6 @@ import vn.momo.kits.const.Spacing
40
40
  import vn.momo.kits.modifier.conditional
41
41
  import vn.momo.kits.modifier.shadow
42
42
  import vn.momo.kits.platform.getStatusBarHeight
43
- import vn.momo.kits.components.HeaderRight
44
43
 
45
44
  enum class HeaderType {
46
45
  DEFAULT,
@@ -22,10 +22,10 @@ fun Icon(
22
22
  color: Color? = AppTheme.current.colors.text.default,
23
23
  modifier: Modifier = Modifier,
24
24
  ) {
25
- val icon = when {
26
- source.startsWith("data:") -> source
27
- source.contains("https") -> source
28
- else -> Icons[source] ?: ""
25
+ val icon = if (source.contains("https")) {
26
+ source
27
+ } else {
28
+ Icons[source] ?: ""
29
29
  }
30
30
  Box(modifier = modifier.size(size).semantics {contentDescription = "img|$icon"}) {
31
31
  AsyncImage(
@@ -39,15 +39,15 @@ fun getFont(font: String): FontFamily {
39
39
  fun getFontFamily(fontFamily: String, fontWeight: FontWeight? = FontWeight.Normal): FontFamily {
40
40
  val key = "$fontFamily-${fontWeight?.weight}"
41
41
  val fontMap = mapOf(
42
- "SFProText-100" to "sfprotext_thin.ttf",
43
- "SFProText-200" to "sfprotext_ultralight.ttf",
42
+ "SFProText-100" to "sfprotext_thin.otf",
43
+ "SFProText-200" to "sfprotext_ultralight.otf",
44
44
  "SFProText-300" to "sfprotext_light.ttf",
45
45
  "SFProText-400" to "sfprotext_regular.ttf",
46
46
  "SFProText-500" to "sfprotext_medium.ttf",
47
- "SFProText-600" to "sfprotext_medium.ttf",
48
- "SFProText-700" to "sfprotext_semibold.ttf",
49
- "SFProText-800" to "sfprotext_bold.ttf",
50
- "SFProText-900" to "sfprotext_heavy.ttf",
47
+ "SFProText-600" to "sfprotext_semibold.ttf",
48
+ "SFProText-700" to "sfprotext_bold.ttf",
49
+ "SFProText-800" to "sfprotext_heavy.ttf",
50
+ "SFProText-900" to "sfprotext_black.otf",
51
51
 
52
52
  "MoMoSignature-100" to "momosignature.otf",
53
53
  "MoMoSignature-200" to "momosignature.otf",
@@ -0,0 +1,223 @@
1
+ //
2
+ // Components.swift
3
+ // Pods
4
+ //
5
+ // Created by sophia on 24/1/25.
6
+ //
7
+
8
+ import SwiftUI
9
+
10
+ public enum TitlePosition {
11
+ case left, center
12
+ }
13
+
14
+ public enum HeaderType {
15
+ case `default`, extended, none
16
+ }
17
+
18
+
19
+ public struct AnimatedHeader {
20
+ public var type: HeaderType = .default
21
+ public var composable: (_ scrollState: Int) -> AnyView = { _ in AnyView(EmptyView()) }
22
+
23
+ public init(
24
+ type: HeaderType = .default,
25
+ composable: @escaping (_ scrollState: Int) -> AnyView = { _ in AnyView(EmptyView()) }
26
+ ) {
27
+ self.type = type
28
+ self.composable = composable
29
+ }
30
+ }
31
+
32
+ func safeAreaTopInset() -> CGFloat {
33
+ if #available(iOS 15.0, *) {
34
+ return UIApplication.shared.connectedScenes
35
+ .compactMap { ($0 as? UIWindowScene)?.keyWindow?.safeAreaInsets.top }
36
+ .first ?? 0
37
+ } else {
38
+ return UIApplication.shared.connectedScenes
39
+ .compactMap { ($0 as? UIWindowScene)?.windows.first?.safeAreaInsets.top }
40
+ .first ?? 0
41
+ }
42
+ }
43
+
44
+
45
+ public struct HeaderBackground: View {
46
+ var headerType: HeaderType = .default
47
+ var scrollState: CGFloat
48
+
49
+ public var body: some View {
50
+
51
+ let statusBarHeight = safeAreaTopInset()
52
+
53
+ let opacity = 1 - CGFloat(scrollState) / 200
54
+ let backgroundColor = scrollState == 0 ? Color.clear : Colors.black01
55
+
56
+ Group {
57
+ if headerType == .default {
58
+ let height = statusBarHeight + 52
59
+ ZStack {
60
+
61
+ Rectangle()
62
+ .fill(backgroundColor)
63
+ .frame(height: height)
64
+ .shadow(color: Colors.black20.opacity(0.2), radius: 10, x: 0, y: -2)
65
+ .overlay(Rectangle()
66
+ .fill(LinearGradient(
67
+ gradient: Gradient(colors: [
68
+ Color(red: 1, green: 0.8, blue: 0.87),
69
+ Color(red: 1, green: 0.8, blue: 0.87).opacity(0.5)
70
+ ]),
71
+ startPoint: .top,
72
+ endPoint: .bottom))
73
+ .opacity(opacity)
74
+ .frame(height: height))
75
+ .overlay(Rectangle()
76
+ .fill(Colors.black04)
77
+ .frame(height: 1)
78
+ .frame(maxWidth: .infinity)
79
+ .alignmentGuide(.bottom) { _ in 0 }, alignment: .bottom
80
+ )
81
+ }
82
+ .frame(maxWidth: .infinity)
83
+
84
+ } else if headerType == .extended {
85
+ ZStack {
86
+ Rectangle()
87
+ .fill(LinearGradient(
88
+ gradient: Gradient(colors: [
89
+ Color(red: 1, green: 0.8, blue: 0.87),
90
+ Color(red: 1, green: 0.8, blue: 0.87).opacity(0)
91
+ ]),
92
+ startPoint: .top,
93
+ endPoint: .bottom))
94
+ .opacity(opacity)
95
+ .frame(height: 154)
96
+
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ }
103
+
104
+ public struct Header: View {
105
+ var headerType: HeaderType = .default
106
+ var titlePosition: TitlePosition
107
+ var title: String
108
+ var headerRight: (() -> AnyView)?
109
+ var goBack: (() -> Void)?
110
+ var opacity: CGFloat? = nil
111
+ var animatedHeader: AnimatedHeader? = nil
112
+ var scrollState: CGFloat = 0
113
+ var inputSearchProps: InputSearchProps? = nil
114
+
115
+ @State private var colorFraction: CGFloat = 0
116
+
117
+ public init(
118
+ headerType: HeaderType = .default,
119
+ titlePosition: TitlePosition,
120
+ title: String,
121
+ headerRight: (() -> AnyView)? = nil,
122
+ goBack: (() -> Void)? = nil,
123
+ opacity: CGFloat? = nil,
124
+ animatedHeader: AnimatedHeader? = nil,
125
+ scrollState: CGFloat = 0,
126
+ inputSearchProps: InputSearchProps? = nil
127
+ ) {
128
+ self.headerType = headerType
129
+ self.titlePosition = titlePosition
130
+ self.title = title
131
+ self.headerRight = headerRight
132
+ self.goBack = goBack
133
+ self.opacity = opacity
134
+ self.animatedHeader = animatedHeader
135
+ self.scrollState = scrollState
136
+ self.inputSearchProps = inputSearchProps
137
+ }
138
+
139
+ public var body: some View {
140
+
141
+ let statusBarHeight = safeAreaTopInset()
142
+ let tintIconColor = Colors.black17
143
+ let backgroundButton = Colors.black01.opacity(0.6)
144
+
145
+ if headerType != .none {
146
+ VStack(spacing: 0) {
147
+ ZStack {
148
+ HStack {
149
+ if let goBack = goBack {
150
+ SwiftUI.Button(action: goBack) {
151
+ Circle()
152
+ .stroke(Color.black.opacity(0.2), lineWidth: 0.2)
153
+ .background(Circle().fill(backgroundButton))
154
+ .frame(width: 28, height: 28)
155
+ .overlay(
156
+ Image(systemName: "arrow.backward")
157
+ .foregroundColor(.black)
158
+ )
159
+ }
160
+
161
+ }
162
+
163
+ Spacer() // Pushes headerRight to the right
164
+
165
+ if let headerRight = headerRight {
166
+ headerRight()
167
+ }
168
+ }
169
+ .frame(height: 52)
170
+ .padding(.horizontal)
171
+
172
+
173
+
174
+ HStack {
175
+ if titlePosition == .left, goBack != nil {
176
+ Spacer().frame(width: 40)
177
+ }
178
+
179
+ if inputSearchProps != nil {
180
+ if let inputProps = inputSearchProps {
181
+ InputSearch(
182
+ text: inputProps.$text,
183
+ buttonText: inputProps.buttonText,
184
+ showButtonText: inputProps.showButtonText,
185
+ showBorder: inputProps.showBorder,
186
+ placeholder: inputProps.placeholder,
187
+ onChangeText: inputProps.onChangeText,
188
+ onPressButtonText: inputProps.onPressButtonText,
189
+ error: inputProps.error,
190
+ disabled: inputProps.disabled,
191
+ icon: inputProps.icon,
192
+ iconColor: inputProps.iconColor,
193
+ onRightIconPressed: inputProps.onRightIconPressed,
194
+ onFocus: inputProps.onFocus,
195
+ onBlur: inputProps.onBlur,
196
+ loading: inputProps.loading,
197
+ fontWeight: inputProps.fontWeight,
198
+ keyboardType: inputProps.keyboardType
199
+ )
200
+ }
201
+ } else {
202
+ Text(title)
203
+ .font(.headline)
204
+ .foregroundColor(tintIconColor)
205
+ .frame(maxWidth: titlePosition == .center ? .infinity : nil, alignment: titlePosition == .center ? .center : .leading)
206
+ }
207
+
208
+ Spacer()
209
+ }
210
+ .frame(height: 52)
211
+ .padding(.horizontal)
212
+
213
+ }
214
+ .background(Color.clear)
215
+ }
216
+ }
217
+ }
218
+
219
+ }
220
+
221
+
222
+
223
+
@@ -0,0 +1,148 @@
1
+ //
2
+ // FloatingButton.swift
3
+ // Pods
4
+ //
5
+ // Created by sophia on 6/2/25.
6
+ //
7
+
8
+ import SwiftUI
9
+ import Combine
10
+
11
+ public enum FABSize {
12
+ case small
13
+ case `default`
14
+ }
15
+
16
+ public enum FABPosition {
17
+ case end
18
+ case center
19
+ }
20
+
21
+ public struct FabProps {
22
+ let icon: String
23
+ let iconColor: Color?
24
+ let label: String?
25
+ let onClick: () -> Void
26
+ let size: FABSize
27
+ let bottom: CGFloat?
28
+ let scrollOffset: CGFloat?
29
+ let position: FABPosition?
30
+
31
+ public init(
32
+ icon: String,
33
+ iconColor: Color? = nil,
34
+ label: String? = nil,
35
+ onClick: @escaping () -> Void,
36
+ size: FABSize = .default,
37
+ bottom: CGFloat? = nil,
38
+ scrollOffset: CGFloat? = nil,
39
+ position: FABPosition? = .end
40
+ ) {
41
+ self.icon = icon
42
+ self.iconColor = iconColor
43
+ self.label = label
44
+ self.onClick = onClick
45
+ self.size = size
46
+ self.bottom = bottom
47
+ self.scrollOffset = scrollOffset
48
+ self.position = position
49
+ }
50
+ }
51
+
52
+ public struct FloatingButton: View {
53
+ @State private var lastScrollOffset: CGFloat = 0
54
+ @State private var isExpanded: Bool = false
55
+ let scrollOffset: CGFloat
56
+ let bottomPadding: CGFloat
57
+ let onClick: () -> Void
58
+ let containerColor: Color
59
+ let contentColor: Color
60
+ let icon: String
61
+ let iconColor: Color?
62
+ let text: String?
63
+ let size: FABSize
64
+ let position: FABPosition
65
+ let keyboardOffset: CGFloat
66
+
67
+ public init(
68
+ scrollOffset: CGFloat = 0,
69
+ bottomPadding: CGFloat = 0,
70
+ onClick: @escaping () -> Void,
71
+ containerColor: Color = .blue,
72
+ contentColor: Color = .white,
73
+ icon: String,
74
+ iconColor: Color? = nil,
75
+ text: String? = nil,
76
+ size: FABSize = .default,
77
+ position: FABPosition = .end,
78
+ keyboardOffset: CGFloat = 0
79
+ ) {
80
+ self.scrollOffset = scrollOffset
81
+ self.bottomPadding = bottomPadding
82
+ self.onClick = onClick
83
+ self.containerColor = containerColor
84
+ self.contentColor = contentColor
85
+ self.icon = icon
86
+ self.iconColor = iconColor
87
+ self.text = text
88
+ self.size = size
89
+ self.position = position
90
+ self.keyboardOffset = keyboardOffset
91
+ }
92
+
93
+ public var body: some View {
94
+ VStack {
95
+ Spacer()
96
+ HStack {
97
+ if position == .center {
98
+ Spacer()
99
+ }
100
+ FloatingButtonContent()
101
+ .padding(.trailing, position == .end ? 12 : 0)
102
+ .padding(.bottom, bottomPadding)
103
+ .offset(y: -keyboardOffset)
104
+ if position == .end {
105
+ Spacer()
106
+ }
107
+ }
108
+ }
109
+ .onReceive(Just(scrollOffset)) { newValue in
110
+ withAnimation(.easeInOut(duration: 0.3)) {
111
+ isExpanded = newValue > lastScrollOffset
112
+ lastScrollOffset = newValue
113
+ }
114
+ }
115
+ }
116
+
117
+ @ViewBuilder
118
+ private func FloatingButtonContent() -> some View {
119
+ SwiftUI.Button(action: onClick) {
120
+ HStack {
121
+ Image(systemName: icon)
122
+ .foregroundColor(iconColor ?? .white)
123
+ .font(size == .small ? .system(size: 12) : .system(size: 24))
124
+
125
+ if isExpanded, let text = text {
126
+ Text(text)
127
+ .foregroundColor(.white)
128
+ .transition(.asymmetric(insertion: expandAnimation, removal: collapseAnimation))
129
+ }
130
+ }
131
+ .padding(.horizontal, isExpanded ? 12 : 0)
132
+ .frame(width: isExpanded ? 80 : (size == .small ? 36 : 48), height: size == .small ? 36 : 48)
133
+ .background(containerColor)
134
+ .cornerRadius(24)
135
+ .shadow(radius: 4)
136
+ }
137
+ }
138
+
139
+ private var expandAnimation: AnyTransition {
140
+ AnyTransition.opacity.animation(.easeIn(duration: 0.2))
141
+ .combined(with: .move(edge: .leading))
142
+ }
143
+
144
+ private var collapseAnimation: AnyTransition {
145
+ AnyTransition.opacity.animation(.easeOut(duration: 0.5))
146
+ .combined(with: .move(edge: .trailing))
147
+ }
148
+ }
@@ -0,0 +1,231 @@
1
+ import SwiftUI
2
+
3
+ public struct HeaderRightData {
4
+ var useShortcut: Bool
5
+ var useMore: Bool
6
+ var tools: [ToolGroup]
7
+ var toolCallback: ((String) -> Void)?
8
+
9
+ public init(
10
+ useShortcut: Bool = false,
11
+ useMore: Bool = false,
12
+ tools: [ToolGroup] = [],
13
+ toolCallback: ((String) -> Void)? = nil
14
+ ) {
15
+ self.useShortcut = useShortcut
16
+ self.useMore = useMore
17
+ self.tools = tools
18
+ self.toolCallback = toolCallback
19
+ }
20
+ }
21
+
22
+ public struct Tool {
23
+ var key: String
24
+ var icon: String
25
+ var showBadge: Bool
26
+ var name: [String: String]
27
+
28
+ public init(
29
+ key: String,
30
+ icon: String,
31
+ showBadge: Bool = false,
32
+ name: [String: String] = [:]
33
+ ) {
34
+ self.key = key
35
+ self.icon = icon
36
+ self.showBadge = showBadge
37
+ self.name = name
38
+ }
39
+
40
+ func toMap() -> [String: Any] {
41
+ return [
42
+ "key": key,
43
+ "icon": icon,
44
+ "showBadge": showBadge,
45
+ "name": name
46
+ ]
47
+ }
48
+ }
49
+
50
+ public struct ToolGroup {
51
+ var title: [String: String]
52
+ var items: [Tool]
53
+
54
+ public init(
55
+ title: [String: String] = [:],
56
+ items: [Tool]
57
+ ) {
58
+ self.title = title
59
+ self.items = items
60
+ }
61
+
62
+ func toMap() -> [String: Any] {
63
+ return [
64
+ "title": title,
65
+ "items": items.map { $0.toMap() }
66
+ ]
67
+ }
68
+ }
69
+
70
+ struct NavigationButtonConfig {
71
+ let icon: String
72
+ let onPress: () -> Void
73
+ }
74
+
75
+ // MARK: - Components
76
+ struct HeaderRight: View {
77
+ var headerRight: HeaderRightData?
78
+
79
+ var body: some View {
80
+ HStack(alignment: .center) {
81
+ ToolkitHeaderRight(headerRight: headerRight)
82
+ }
83
+ }
84
+ }
85
+
86
+ struct ToolkitHeaderRight: View {
87
+ var headerRight: HeaderRightData?
88
+ @State private var isFavorite: Bool = false
89
+ @State private var isLoading: Bool = false
90
+
91
+ // MARK: - Actions
92
+ private func onPressShortcut() {
93
+ ComposeApi.shared.request(
94
+ funcName: "onToolAction",
95
+ params: ["item": ["key": "onFavorite"]]
96
+ ) { _ in
97
+ isFavorite.toggle()
98
+ }
99
+ }
100
+
101
+ private func onPressHelpCenter() {
102
+ let context = ApplicationContext.shared
103
+ let paramMap: [String: Any] = [
104
+ "appId": context.appId,
105
+ "code": context.appCode,
106
+ "name": context.appName,
107
+ "icon": context.appIcon,
108
+ "description": context.description
109
+ ]
110
+ ComposeApi.shared.request(
111
+ funcName: "showHelpCenter",
112
+ params: paramMap
113
+ ) { _ in }
114
+ }
115
+
116
+ private func onPressClose() {
117
+ ComposeApi.shared.request(funcName: "dismiss", params: "")
118
+ }
119
+
120
+ private func onPressMore() {
121
+ let context = ApplicationContext.shared
122
+ let params: [String: Any] = [
123
+ "tools": headerRight?.tools.map { $0.toMap() } ?? [],
124
+ "context": [
125
+ "appId": context.appId,
126
+ "code": context.appCode,
127
+ "name": context.appName,
128
+ "icon": context.appIcon,
129
+ "description": context.description
130
+ ]
131
+ ]
132
+ ComposeApi.shared.request(
133
+ funcName: "showTools",
134
+ params: params
135
+ ) { _ in }
136
+ }
137
+
138
+ private func getNavigationButtonConfig() -> NavigationButtonConfig {
139
+ let totalTools = headerRight?.tools.reduce(0) { $0 + $1.items.count } ?? 0
140
+ var icon = isFavorite ? "pin_star_checked" : "pin_star"
141
+ var onClickHandler: () -> Void = onPressShortcut
142
+
143
+ if totalTools > 1 || headerRight?.useMore == true {
144
+ icon = "navigation_more_icon"
145
+ onClickHandler = onPressMore
146
+ } else if totalTools == 1, let singleTool = headerRight?.tools.first?.items.first {
147
+ icon = singleTool.icon
148
+ onClickHandler = {
149
+ headerRight?.toolCallback?(singleTool.key)
150
+ }
151
+ }
152
+
153
+ return NavigationButtonConfig(icon: icon, onPress: onClickHandler)
154
+ }
155
+
156
+ var body: some View {
157
+ let navButtonConfig = getNavigationButtonConfig()
158
+ let showBadge = headerRight?.tools.contains { group in
159
+ group.items.contains { $0.showBadge }
160
+ } ?? false
161
+
162
+ HStack(alignment: .center) {
163
+ if headerRight?.useShortcut == true {
164
+ NavigationButton(
165
+ disabled: isLoading,
166
+ icon: navButtonConfig.icon,
167
+ showBadge: showBadge,
168
+ onClick: navButtonConfig.onPress
169
+ )
170
+ }
171
+
172
+ HStack(alignment: .center, spacing: 0) {
173
+ Icon(source: "help_center", size: 20)
174
+ .padding(4)
175
+ .onTapGesture {
176
+ onPressHelpCenter()
177
+ }
178
+
179
+ Rectangle()
180
+ .fill(Colors.black20)
181
+ .frame(width: 0.5, height: 12)
182
+
183
+ Icon(source: "16_navigation_close_circle", size: 20)
184
+ .padding(4)
185
+ .onTapGesture {
186
+ onPressClose()
187
+ }
188
+ }
189
+ .frame(width: 65, height: 28)
190
+ .background(Color.white.opacity(0.6))
191
+ .cornerRadius(14)
192
+ .overlay(
193
+ RoundedRectangle(cornerRadius: 14)
194
+ .stroke(Color.black.opacity(0.2), lineWidth: 0.2)
195
+ )
196
+ .padding(.leading, 8)
197
+ }
198
+ }
199
+ }
200
+
201
+ struct NavigationButton: View {
202
+ var disabled: Bool
203
+ var icon: String
204
+ var showBadge: Bool
205
+ var onClick: () -> Void
206
+
207
+ var body: some View {
208
+ ZStack {
209
+ Circle()
210
+ .fill(Color.white.opacity(0.6))
211
+ .overlay(
212
+ Circle()
213
+ .stroke(Color.black.opacity(0.2), lineWidth: 0.2)
214
+ )
215
+
216
+ Icon(source: icon, size: 16)
217
+
218
+ if showBadge {
219
+ BadgeDot(size: .small)
220
+ .offset(x: -2, y: -2)
221
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
222
+ }
223
+ }
224
+ .frame(width: 28, height: 28)
225
+ .onTapGesture {
226
+ if !disabled {
227
+ onClick()
228
+ }
229
+ }
230
+ }
231
+ }
@@ -0,0 +1,228 @@
1
+ //
2
+ // Screen.swift
3
+ // Pods
4
+ //
5
+ // Created by sophia on 5/2/25.
6
+ //
7
+
8
+ import SwiftUI
9
+
10
+ public struct Screen<Content: View>: View {
11
+ var backgroundColor: Color?
12
+ var isBack: Bool
13
+ var headerType: HeaderType
14
+ var verticalAlignment: VerticalAlignment
15
+ var horizontalAlignment: HorizontalAlignment
16
+ var title: String
17
+ var titlePosition: TitlePosition
18
+ var goBack: (() -> Void)?
19
+ var scrollable: Bool
20
+ var onContentLayout: ((CGRect) -> Void)?
21
+ var useAvoidKeyboard: Bool
22
+ var footer: (() -> AnyView)?
23
+ var headerRight: HeaderRightData?
24
+ var animatedHeader: AnimatedHeader? = nil
25
+ var layoutOffset: CGFloat
26
+ var inputSearchProps: InputSearchProps? = nil
27
+ var fabProps: FabProps? = nil
28
+ var content: () -> Content
29
+
30
+ public init(
31
+ backgroundColor: Color? = nil,
32
+ isBack: Bool = true,
33
+ headerType: HeaderType = .default,
34
+ verticalAlignment: VerticalAlignment = .top,
35
+ horizontalAlignment: HorizontalAlignment = .leading,
36
+ title: String = "Stack",
37
+ titlePosition: TitlePosition = .center,
38
+ goBack: (() -> Void)? = nil,
39
+ scrollable: Bool = true,
40
+ onContentLayout: ((CGRect) -> Void)? = nil,
41
+ useAvoidKeyboard: Bool = true,
42
+ footer: (() -> AnyView)? = nil,
43
+ headerRight: HeaderRightData? = nil,
44
+ animatedHeader: AnimatedHeader? = nil,
45
+ layoutOffset: CGFloat = 56,
46
+ inputSearchProps: InputSearchProps? = nil,
47
+ fabProps: FabProps? = nil,
48
+ @ViewBuilder content: @escaping () -> Content
49
+ ) {
50
+ self.backgroundColor = backgroundColor
51
+ self.isBack = isBack
52
+ self.headerType = headerType
53
+ self.verticalAlignment = verticalAlignment
54
+ self.horizontalAlignment = horizontalAlignment
55
+ self.title = title
56
+ self.titlePosition = titlePosition
57
+ self.goBack = goBack
58
+ self.scrollable = scrollable
59
+ self.onContentLayout = onContentLayout
60
+ self.useAvoidKeyboard = useAvoidKeyboard
61
+ self.footer = footer
62
+ self.headerRight = headerRight
63
+ self.animatedHeader = animatedHeader
64
+ self.layoutOffset = layoutOffset
65
+ self.inputSearchProps = inputSearchProps
66
+ self.fabProps = fabProps
67
+ self.content = content
68
+ }
69
+
70
+ @State private var scrollOffset: CGFloat = 0
71
+
72
+ public var body: some View {
73
+ GeometryReader { geometry in
74
+ let safeAreaInsets = geometry.safeAreaInsets
75
+ let statusBarHeight = safeAreaInsets.top
76
+ let keyboardHeight: CGFloat = 0
77
+ let keyboardSize: CGFloat = useAvoidKeyboard ? max(keyboardHeight, 0) : 0
78
+
79
+ ZStack {
80
+ (backgroundColor ?? Color.white).edgesIgnoringSafeArea(.all)
81
+
82
+ VStack(spacing: 0) {
83
+ ZStack {
84
+ if animatedHeader == nil {
85
+ HeaderBackground(headerType: headerType, scrollState: scrollOffset)
86
+ } else {
87
+ Color.white.opacity(Double(min(scrollOffset / 114, 1)))
88
+ .overlay(HeaderBackground(headerType: headerType, scrollState: scrollOffset))
89
+
90
+ }
91
+
92
+ Header(
93
+ headerType: headerType,
94
+ titlePosition: titlePosition,
95
+ title: title,
96
+ headerRight: headerRight,
97
+ goBack: goBack,
98
+ opacity: min(scrollOffset / 114, 1),
99
+ animatedHeader: animatedHeader,
100
+ scrollState: scrollOffset,
101
+ inputSearchProps: inputSearchProps
102
+ ).padding(.bottom, headerType == .extended ? 0 : -safeAreaTopInset())
103
+ }.padding(.top, -safeAreaTopInset())
104
+
105
+
106
+ VStack(alignment: horizontalAlignment, spacing: 0) {
107
+ if animatedHeader != nil {
108
+ Spacer().frame(height: statusBarHeight + 52 + layoutOffset)
109
+ }
110
+
111
+ if scrollable {
112
+ ScrollView(.vertical, showsIndicators: false) {
113
+ VStack {
114
+ content()
115
+ }
116
+ .background(GeometryReader { geo -> Color in
117
+ DispatchQueue.main.async {
118
+ self.scrollOffset = -geo.frame(in: .global).origin.y
119
+ }
120
+ return Color.clear
121
+ })
122
+ }
123
+ } else {
124
+ content()
125
+ }
126
+ }
127
+ .padding(.bottom, keyboardSize)
128
+
129
+
130
+ if let footerView = footer?() {
131
+ ZStack(alignment: .top) {
132
+ Rectangle()
133
+ .fill(Colors.black20)
134
+ .frame(height: 4)
135
+ .shadow(color: Colors.black20.opacity(0.2), radius: 10, x: 0, y: -2)
136
+
137
+ Footer(footerView: footerView, keyboardSize: keyboardSize)
138
+ }
139
+ }
140
+
141
+ if let fabProps = fabProps {
142
+ FloatingButton(
143
+ scrollOffset: scrollOffset,
144
+ bottomPadding: fabProps.bottom ?? 16,
145
+ onClick: fabProps.onClick,
146
+ containerColor: .blue,
147
+ contentColor: .white,
148
+ icon: fabProps.icon,
149
+ iconColor: fabProps.iconColor,
150
+ text: fabProps.label,
151
+ size: fabProps.size,
152
+ position: fabProps.position ?? .end,
153
+ keyboardOffset: keyboardSize
154
+ )
155
+ }
156
+ }
157
+
158
+ }
159
+ }
160
+ }
161
+ }
162
+
163
+ struct Footer: View {
164
+ var footerView: AnyView
165
+ var keyboardSize: CGFloat
166
+
167
+ var body: some View {
168
+ VStack {
169
+ footerView
170
+ }
171
+ .padding(.bottom, keyboardSize)
172
+ .background(Colors.black01)
173
+ }
174
+ }
175
+
176
+ public struct Header: View {
177
+ var headerType: HeaderType = .default
178
+ var titlePosition: TitlePosition
179
+ var title: String
180
+ var headerRight: HeaderRightData?
181
+ var goBack: (() -> Void)?
182
+ var opacity: CGFloat? = nil
183
+ var animatedHeader: AnimatedHeader? = nil
184
+ var scrollState: CGFloat = 0
185
+ var inputSearchProps: InputSearchProps? = nil
186
+
187
+ @State private var colorFraction: CGFloat = 0
188
+
189
+ public init(
190
+ headerType: HeaderType = .default,
191
+ titlePosition: TitlePosition,
192
+ title: String,
193
+ headerRight: HeaderRightData? = nil,
194
+ goBack: (() -> Void)? = nil,
195
+ opacity: CGFloat? = nil,
196
+ animatedHeader: AnimatedHeader? = nil,
197
+ scrollState: CGFloat = 0,
198
+ inputSearchProps: InputSearchProps? = nil
199
+ ) {
200
+ self.headerType = headerType
201
+ self.titlePosition = titlePosition
202
+ self.title = title
203
+ self.headerRight = headerRight
204
+ self.goBack = goBack
205
+ self.opacity = opacity
206
+ self.animatedHeader = animatedHeader
207
+ self.scrollState = scrollState
208
+ self.inputSearchProps = inputSearchProps
209
+ }
210
+
211
+ public var body: some View {
212
+ HStack {
213
+ if let goBack = goBack {
214
+ Button(action: goBack) {
215
+ Text("Back")
216
+ }
217
+ }
218
+
219
+ Spacer()
220
+
221
+ if headerRight != nil {
222
+ HeaderRight(headerRight: headerRight)
223
+ .opacity(animatedHeader != nil ? opacity ?? 1 : 1)
224
+ }
225
+ }
226
+ }
227
+ }
228
+
@@ -37,6 +37,62 @@ struct RenderRightIconSearch: View {
37
37
  }
38
38
  }
39
39
 
40
+ public struct InputSearchProps {
41
+ @Binding var text: String
42
+ var buttonText: String = ""
43
+ var showButtonText: Bool = false
44
+ var showBorder: Bool = true
45
+ var placeholder: String = ""
46
+ var onChangeText: (String) -> Void = { _ in }
47
+ var onPressButtonText: () -> Void = {}
48
+ var error: String = ""
49
+ var disabled: Bool = false
50
+ var icon: String = ""
51
+ var iconColor: Color = Colors.text
52
+ var onRightIconPressed: () -> Void = {}
53
+ var onFocus: () -> Void = {}
54
+ var onBlur: () -> Void = {}
55
+ var loading: Bool = false
56
+ var fontWeight: Font.Weight = .regular
57
+ var keyboardType: UIKeyboardType = .default
58
+
59
+ public init(text: Binding<String>,
60
+ buttonText: String = "",
61
+ showButtonText: Bool = false,
62
+ showBorder: Bool = true,
63
+ placeholder: String = "",
64
+ onChangeText: @escaping (String) -> Void = { _ in },
65
+ onPressButtonText: @escaping () -> Void = {},
66
+ error: String = "",
67
+ disabled: Bool = false,
68
+ icon: String = "",
69
+ iconColor: Color = Colors.text,
70
+ onRightIconPressed: @escaping () -> Void = {},
71
+ onFocus: @escaping () -> Void = {},
72
+ onBlur: @escaping () -> Void = {},
73
+ loading: Bool = false,
74
+ fontWeight: Font.Weight = .regular,
75
+ keyboardType: UIKeyboardType = .default) {
76
+ self._text = text
77
+ self.buttonText = buttonText
78
+ self.showButtonText = showButtonText
79
+ self.showBorder = showBorder
80
+ self.placeholder = placeholder
81
+ self.onChangeText = onChangeText
82
+ self.onPressButtonText = onPressButtonText
83
+ self.error = error
84
+ self.disabled = disabled
85
+ self.icon = icon
86
+ self.iconColor = iconColor
87
+ self.onRightIconPressed = onRightIconPressed
88
+ self.onFocus = onFocus
89
+ self.onBlur = onBlur
90
+ self.loading = loading
91
+ self.fontWeight = fontWeight
92
+ self.keyboardType = keyboardType
93
+ }
94
+ }
95
+
40
96
  // MARK: - InputSearch
41
97
 
42
98
  public struct InputSearch: View {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/native-kits",
3
- "version": "0.112.1-beta.1",
3
+ "version": "0.112.1-beta.10",
4
4
  "private": false,
5
5
  "dependencies": {},
6
6
  "devDependencies": {},