@momo-kits/native-kits 0.113.1-beta.6 → 0.113.1-beta.8

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.
@@ -0,0 +1,196 @@
1
+ package vn.momo.kits.application
2
+
3
+ import androidx.compose.animation.animateColorAsState
4
+ import androidx.compose.foundation.background
5
+ import androidx.compose.foundation.border
6
+ import androidx.compose.foundation.clickable
7
+ import androidx.compose.foundation.layout.Arrangement
8
+ import androidx.compose.foundation.layout.Box
9
+ import androidx.compose.foundation.layout.BoxWithConstraints
10
+ import androidx.compose.foundation.layout.Row
11
+ import androidx.compose.foundation.layout.Spacer
12
+ import androidx.compose.foundation.layout.fillMaxWidth
13
+ import androidx.compose.foundation.layout.height
14
+ import androidx.compose.foundation.layout.padding
15
+ import androidx.compose.foundation.layout.size
16
+ import androidx.compose.foundation.layout.width
17
+ import androidx.compose.foundation.shape.RoundedCornerShape
18
+ import androidx.compose.runtime.Composable
19
+ import androidx.compose.runtime.LaunchedEffect
20
+ import androidx.compose.runtime.getValue
21
+ import androidx.compose.runtime.mutableStateOf
22
+ import androidx.compose.runtime.remember
23
+ import androidx.compose.runtime.setValue
24
+ import androidx.compose.runtime.snapshotFlow
25
+ import androidx.compose.ui.Alignment
26
+ import androidx.compose.ui.Modifier
27
+ import androidx.compose.ui.graphics.Color
28
+ import androidx.compose.ui.graphics.graphicsLayer
29
+ import androidx.compose.ui.semantics.semantics
30
+ import androidx.compose.ui.semantics.testTag
31
+ import androidx.compose.ui.unit.dp
32
+ import androidx.compose.ui.zIndex
33
+ import kotlinx.coroutines.flow.distinctUntilChanged
34
+ import kotlinx.coroutines.flow.map
35
+ import vn.momo.kits.components.Icon
36
+ import vn.momo.kits.components.InputSearch
37
+ import vn.momo.kits.components.InputSearchProps
38
+ import vn.momo.kits.const.AppStatusBar
39
+ import vn.momo.kits.const.AppTheme
40
+ import vn.momo.kits.const.Colors
41
+ import vn.momo.kits.const.Spacing
42
+ import vn.momo.kits.modifier.setAutomationId
43
+ import vn.momo.kits.platform.getStatusBarHeight
44
+
45
+ enum class TitlePosition {
46
+ LEFT, CENTER,
47
+ }
48
+
49
+ data class AnimatedHeader(
50
+ val type: HeaderType = HeaderType.DEFAULT,
51
+ val composable: @Composable (scrollState: Int) -> Unit = {}
52
+ )
53
+
54
+ @Composable
55
+ fun Header(
56
+ headerType: HeaderType = HeaderType.DEFAULT,
57
+ titlePosition: TitlePosition,
58
+ title: String,
59
+ headerRight: @Composable (() -> Unit)? = null,
60
+ goBack: (() -> Unit)? = null,
61
+ opacity: Float? = null,
62
+ animatedHeader: AnimatedHeader? = null,
63
+ scrollState: Int = 0,
64
+ inputSearchProps: InputSearchProps? = null,
65
+ useAnimationSearch: Boolean = false
66
+ ) {
67
+ val statusBarHeight = AppStatusBar.current ?: getStatusBarHeight()
68
+ val tintIconColor = AppTheme.current.colors.text.default
69
+ val backgroundButton = Colors.black_01.copy(alpha = 0.6f)
70
+ val miniAppContext = ApplicationContext.current
71
+
72
+ var colorFraction by remember { mutableStateOf(0f) }
73
+
74
+ LaunchedEffect(scrollState) {
75
+ snapshotFlow { scrollState }
76
+ .map { (it / 50f).coerceIn(0f, 1f) }
77
+ .distinctUntilChanged()
78
+ .collect { fraction -> colorFraction = fraction }
79
+ }
80
+
81
+ val backgroundSearch by animateColorAsState(
82
+ targetValue = animateColor(
83
+ Colors.black_01,
84
+ AppTheme.current.colors.background.default,
85
+ colorFraction
86
+ )
87
+ )
88
+
89
+ fun getHeaderRightOpacity(): Float {
90
+ if (useAnimationSearch) {
91
+ return 1f - (opacity ?: 1f)
92
+ }
93
+ return if (animatedHeader !== null) opacity ?: 1f else 1f
94
+ }
95
+
96
+ if (headerType != HeaderType.NONE) {
97
+ BoxWithConstraints(
98
+ Modifier.height(statusBarHeight + 52.dp)
99
+ .fillMaxWidth()
100
+ .zIndex(10f),
101
+ contentAlignment = Alignment.BottomCenter
102
+ ) {
103
+ Row(
104
+ modifier = Modifier.height(52.dp)
105
+ .fillMaxWidth()
106
+ .padding(horizontal = Spacing.M),
107
+ verticalAlignment = Alignment.CenterVertically,
108
+ horizontalArrangement = Arrangement.SpaceBetween
109
+ ) {
110
+ Box {
111
+ if (goBack != null) {
112
+ Box(
113
+ modifier = Modifier
114
+ .size(28.dp)
115
+ .then(
116
+ Modifier.border(
117
+ 0.2.dp,
118
+ Colors.black_20.copy(alpha = 0.2f),
119
+ RoundedCornerShape(100)
120
+ )
121
+ )
122
+ .background(
123
+ backgroundButton,
124
+ RoundedCornerShape(100)
125
+ )
126
+ .clickable(
127
+ onClick = goBack
128
+ )
129
+ .padding(Spacing.XS)
130
+ .setAutomationId("btn_navigation_back"),
131
+ contentAlignment = Alignment.Center
132
+ ) {
133
+ Icon(
134
+ source = "arrow-back",
135
+ color = Colors.black_20,
136
+ size = 20.dp,
137
+ )
138
+ }
139
+ }
140
+ }
141
+
142
+ if (((useAnimationSearch && inputSearchProps != null) || inputSearchProps == null) && miniAppContext != null) {
143
+ Box(Modifier.graphicsLayer {
144
+ alpha = getHeaderRightOpacity()
145
+ }) {
146
+ headerRight?.invoke()
147
+ }
148
+ }
149
+ }
150
+
151
+ // Title
152
+ Row(
153
+ modifier = Modifier.height(52.dp)
154
+ .fillMaxWidth()
155
+ .padding(horizontal = Spacing.M),
156
+ verticalAlignment = Alignment.CenterVertically,
157
+ ) {
158
+ if (goBack != null && titlePosition == TitlePosition.LEFT) {
159
+ Spacer(Modifier.width(40.dp))
160
+ }
161
+ if (inputSearchProps != null && !useAnimationSearch) {
162
+ InputSearch(inputSearchProps = inputSearchProps.copy(backgroundColor = backgroundSearch))
163
+ } else {
164
+ Box(
165
+ Modifier.weight(1f).graphicsLayer {
166
+ alpha = if (useAnimationSearch && inputSearchProps != null) {
167
+ 1f - (opacity ?: 1f)
168
+ } else {
169
+ if (animatedHeader !== null) opacity ?: 1f else 1f
170
+ }
171
+ },
172
+ contentAlignment = if (titlePosition == TitlePosition.LEFT) Alignment.TopStart else Alignment.Center
173
+ ) {
174
+ HeaderTitle(
175
+ title = title,
176
+ color = tintIconColor,
177
+ modifier = Modifier.semantics {
178
+ testTag = "title_navigation_header"
179
+ }
180
+ )
181
+ }
182
+ }
183
+ }
184
+ }
185
+ }
186
+ }
187
+
188
+
189
+ fun animateColor(start: Color, stop: Color, fraction: Float): Color {
190
+ return Color(
191
+ red = start.red + fraction * (stop.red - start.red),
192
+ green = start.green + fraction * (stop.green - start.green),
193
+ blue = start.blue + fraction * (stop.blue - start.blue),
194
+ alpha = start.alpha + fraction * (stop.alpha - start.alpha)
195
+ )
196
+ }
@@ -0,0 +1,70 @@
1
+ package vn.momo.kits.application
2
+
3
+ import androidx.compose.foundation.layout.Box
4
+ import androidx.compose.foundation.layout.fillMaxWidth
5
+ import androidx.compose.runtime.Composable
6
+ import androidx.compose.ui.Modifier
7
+ import androidx.compose.ui.graphics.Color
8
+ import androidx.compose.ui.unit.Dp
9
+ import androidx.compose.ui.unit.dp
10
+ import vn.momo.kits.components.InputSearchProps
11
+ import vn.momo.kits.const.AppStatusBar
12
+ import vn.momo.kits.const.AppTheme
13
+ import vn.momo.kits.platform.getScreenDimensions
14
+ import vn.momo.kits.platform.getStatusBarHeight
15
+
16
+ private const val SCREEN_PADDING = 12
17
+ private const val BACK_WIDTH = 28
18
+
19
+ @Composable
20
+ fun HeaderBackground(
21
+ isBack: Boolean = true,
22
+ headerType: HeaderType = HeaderType.DEFAULT,
23
+ scrollState: Int,
24
+ inputSearchProps: InputSearchProps? = null,
25
+ headerRightWidth: Dp = 0.dp,
26
+ useAnimationSearch: Boolean = false
27
+ ) {
28
+ val statusBarHeight = AppStatusBar.current ?: getStatusBarHeight()
29
+ val screenWidth = getScreenDimensions().width
30
+ val leftPosition = if (isBack) (BACK_WIDTH + 20).dp else 20.dp
31
+ val searchWidth = screenWidth - (SCREEN_PADDING * 2)
32
+
33
+ val animations = useHeaderSearchAnimation(
34
+ scrollState = scrollState,
35
+ statusBarHeight = statusBarHeight,
36
+ leftPosition = leftPosition,
37
+ searchWidth = searchWidth,
38
+ headerRightWidth = headerRightWidth
39
+ )
40
+
41
+ val backgroundColor = if (scrollState == 0) {
42
+ Color.Transparent
43
+ } else {
44
+ AppTheme.current.colors.background.surface
45
+ }
46
+
47
+ when (headerType) {
48
+ HeaderType.DEFAULT -> {
49
+ val height = statusBarHeight + 52.dp
50
+ HeaderDefault(
51
+ height = height,
52
+ animations = animations,
53
+ backgroundColor = backgroundColor
54
+ )
55
+ }
56
+
57
+ HeaderType.EXTENDED -> {
58
+ HeaderExtended(
59
+ animations = animations,
60
+ inputSearchProps = inputSearchProps,
61
+ useAnimationSearch = useAnimationSearch,
62
+ backgroundColor = backgroundColor
63
+ )
64
+ }
65
+
66
+ else -> {
67
+ Box(modifier = Modifier.fillMaxWidth())
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,63 @@
1
+ package vn.momo.kits.application
2
+
3
+ import androidx.compose.foundation.background
4
+ import androidx.compose.foundation.layout.Box
5
+ import androidx.compose.foundation.layout.fillMaxWidth
6
+ import androidx.compose.foundation.layout.height
7
+ import androidx.compose.runtime.Composable
8
+ import androidx.compose.ui.Modifier
9
+ import androidx.compose.ui.draw.alpha
10
+ import androidx.compose.ui.geometry.Offset
11
+ import androidx.compose.ui.graphics.Brush
12
+ import androidx.compose.ui.graphics.Color
13
+ import androidx.compose.ui.layout.ContentScale
14
+ import androidx.compose.ui.platform.LocalDensity
15
+ import androidx.compose.ui.unit.Dp
16
+ import androidx.compose.ui.unit.dp
17
+ import vn.momo.kits.components.Image
18
+ import vn.momo.kits.components.Options
19
+ import vn.momo.kits.const.AppTheme
20
+ import vn.momo.kits.utils.bottomBorder
21
+
22
+ @Composable
23
+ fun HeaderDefault(
24
+ height: Dp,
25
+ backgroundColor: Color,
26
+ animations: HeaderAnimations
27
+ ) {
28
+ Box(
29
+ modifier = Modifier.fillMaxWidth().height(height)
30
+ .background(backgroundColor)
31
+ .bottomBorder(
32
+ strokeWidth = 1.dp,
33
+ color = AppTheme.current.colors.border.default
34
+ )
35
+ ) {
36
+ Box(
37
+ modifier = Modifier
38
+ .height(height)
39
+ .fillMaxWidth()
40
+ .alpha(animations.opacity)
41
+ .background(
42
+ Brush.linearGradient(
43
+ colors = listOf(
44
+ Color(0xFFFDCADE),
45
+ Color(0x00FDCADE)
46
+ ),
47
+ start = Offset(0f, 0f),
48
+ end = Offset(0f, with(LocalDensity.current) { 154.dp.toPx() })
49
+ )
50
+ )
51
+ )
52
+ if (AppTheme.current.assets.headerBackground != null) {
53
+ Image(
54
+ loading = false,
55
+ source = AppTheme.current.assets.headerBackground!!,
56
+ modifier = Modifier.fillMaxWidth().height(height),
57
+ options = Options(
58
+ contentScale = ContentScale.FillWidth
59
+ )
60
+ )
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,102 @@
1
+ package vn.momo.kits.application
2
+
3
+ import androidx.compose.foundation.background
4
+ import androidx.compose.foundation.layout.Box
5
+ import androidx.compose.foundation.layout.fillMaxWidth
6
+ import androidx.compose.foundation.layout.height
7
+ import androidx.compose.foundation.layout.offset
8
+ import androidx.compose.foundation.layout.padding
9
+ import androidx.compose.foundation.layout.width
10
+ import androidx.compose.foundation.shape.RoundedCornerShape
11
+ import androidx.compose.runtime.Composable
12
+ import androidx.compose.ui.Alignment
13
+ import androidx.compose.ui.Modifier
14
+ import androidx.compose.ui.draw.alpha
15
+ import androidx.compose.ui.geometry.Offset
16
+ import androidx.compose.ui.graphics.Brush
17
+ import androidx.compose.ui.graphics.Color
18
+ import androidx.compose.ui.layout.ContentScale
19
+ import androidx.compose.ui.platform.LocalDensity
20
+ import androidx.compose.ui.unit.dp
21
+ import vn.momo.kits.components.Image
22
+ import vn.momo.kits.components.InputSearch
23
+ import vn.momo.kits.components.InputSearchProps
24
+ import vn.momo.kits.components.Options
25
+ import vn.momo.kits.const.AppTheme
26
+ import vn.momo.kits.const.Radius
27
+ import vn.momo.kits.const.Spacing
28
+
29
+ @Composable
30
+ fun HeaderExtended(
31
+ animations: HeaderAnimations,
32
+ inputSearchProps: InputSearchProps? = null,
33
+ useAnimationSearch: Boolean = false,
34
+ backgroundColor: Color = Color.Transparent
35
+ ){
36
+ Box(
37
+ modifier = Modifier.fillMaxWidth().height(animations.height.dp)
38
+ ) {
39
+ Box(
40
+ modifier = Modifier
41
+ .fillMaxWidth()
42
+ .height(154.dp)
43
+ .alpha(animations.opacity)
44
+ .background(
45
+ Brush.linearGradient(
46
+ colors = listOf(
47
+ Color(0xFFFDCADE),
48
+ Color(0x00FDCADE)
49
+ ),
50
+ start = Offset(0f, 0f),
51
+ end = Offset(0f, with(LocalDensity.current) { 154.dp.toPx() })
52
+ )
53
+ )
54
+ ) {
55
+ if (AppTheme.current.assets.headerBackground != null) {
56
+ Box(
57
+ modifier = Modifier
58
+ .fillMaxWidth()
59
+ .background(backgroundColor)
60
+ ) {
61
+ Image(
62
+ loading = false,
63
+ source = AppTheme.current.assets.headerBackground!!,
64
+ modifier = Modifier.fillMaxWidth().height(154.dp),
65
+ options = Options(
66
+ contentScale = ContentScale.FillWidth
67
+ )
68
+ )
69
+ }
70
+ }
71
+ }
72
+
73
+ if (inputSearchProps != null && useAnimationSearch) {
74
+ Box(
75
+ modifier = Modifier
76
+ .height(52.dp)
77
+ .offset(
78
+ x = animations.translateX.dp
79
+ )
80
+ .padding(vertical = Spacing.S)
81
+ .align(Alignment.BottomStart)
82
+ ) {
83
+ Box(
84
+ modifier = Modifier
85
+ .width(animations.width.dp)
86
+ .background(
87
+ color = animations.backgroundSearch,
88
+ shape = RoundedCornerShape(Radius.XL)
89
+ )
90
+ ) {
91
+ InputSearch(
92
+ inputSearchProps = inputSearchProps.copy(
93
+ showButtonText = false,
94
+ backgroundColor = Color.Transparent
95
+ )
96
+ )
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
102
+
@@ -79,12 +79,12 @@ object Spacing {
79
79
 
80
80
  @Composable
81
81
  fun HeaderRight(
82
- headerRight: HeaderRightData? = null,
82
+ data: HeaderRightData? = null,
83
83
  ) {
84
84
  Row(
85
85
  verticalAlignment = Alignment.CenterVertically,
86
86
  ) {
87
- ToolkitHeaderRight(headerRight = headerRight)
87
+ ToolkitHeaderRight(headerRight = data)
88
88
  }
89
89
  }
90
90
 
@@ -0,0 +1,27 @@
1
+ package vn.momo.kits.application
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import androidx.compose.ui.Modifier
5
+ import androidx.compose.ui.graphics.Color
6
+ import androidx.compose.ui.text.style.TextAlign
7
+ import androidx.compose.ui.zIndex
8
+ import vn.momo.kits.components.Text
9
+ import vn.momo.kits.const.Typography
10
+
11
+
12
+ @Composable
13
+ fun HeaderTitle(
14
+ title: String = "",
15
+ modifier: Modifier = Modifier,
16
+ textAlign: TextAlign = TextAlign.Start,
17
+ color: Color? = null,
18
+ ) {
19
+ Text(
20
+ modifier = Modifier.then(modifier).zIndex(1f),
21
+ text = title,
22
+ textAlign = textAlign,
23
+ style = Typography.actionS,
24
+ color = color,
25
+ maxLines = 1
26
+ )
27
+ }
@@ -84,7 +84,7 @@ fun Screen(
84
84
  val bottomPadding = min(indicator, 21.dp)
85
85
 
86
86
  val opacity by animateFloatAsState(
87
- targetValue = ((scrollState.value / 52f)).coerceIn(0f, 1f),
87
+ targetValue = ((scrollState.value / 114f)).coerceIn(0f, 1f)
88
88
  )
89
89
 
90
90
  val headerAnimated =
@@ -0,0 +1,61 @@
1
+ package vn.momo.kits.application
2
+
3
+ import androidx.compose.animation.animateColorAsState
4
+ import androidx.compose.animation.core.animateFloatAsState
5
+ import androidx.compose.runtime.Composable
6
+ import androidx.compose.ui.graphics.Color
7
+ import androidx.compose.ui.unit.Dp
8
+ import vn.momo.kits.const.AppTheme
9
+ import vn.momo.kits.const.Colors
10
+ import androidx.compose.runtime.getValue
11
+
12
+ data class HeaderAnimations(
13
+ val opacity: Float,
14
+ val backgroundSearch: Color,
15
+ val translateX: Float,
16
+ val width: Float,
17
+ val height: Float
18
+ )
19
+
20
+ @Composable
21
+ fun useHeaderSearchAnimation(
22
+ scrollState: Int,
23
+ statusBarHeight: Dp,
24
+ leftPosition: Dp,
25
+ searchWidth: Int,
26
+ headerRightWidth: Dp
27
+ ): HeaderAnimations {
28
+ val opacityAni by animateFloatAsState(
29
+ targetValue = (1 - (scrollState / 52f)).coerceIn(0f, 1f)
30
+ )
31
+
32
+ val backgroundSearch by animateColorAsState(
33
+ targetValue = animateColor(
34
+ AppTheme.current.colors.background.default,
35
+ Colors.black_01,
36
+ opacityAni
37
+ )
38
+ )
39
+
40
+ val animatedTranslateX by animateFloatAsState(
41
+ targetValue = (scrollState / 52f).coerceIn(0f, 1f) * (leftPosition.value - 12) + 12
42
+ )
43
+
44
+ val animatedWidth by animateFloatAsState(
45
+ targetValue = (scrollState / 52f).coerceIn(0f, 1f) *
46
+ ((searchWidth - leftPosition.value - headerRightWidth.value + 12) - searchWidth) + searchWidth
47
+ )
48
+
49
+ val animatedHeight by animateFloatAsState(
50
+ targetValue = (1 - (scrollState / 52f).coerceIn(0f, 1f)) *
51
+ (statusBarHeight.value + 94 - (statusBarHeight.value + 52)) + (statusBarHeight.value + 52)
52
+ )
53
+
54
+ return HeaderAnimations(
55
+ opacity = opacityAni,
56
+ backgroundSearch = backgroundSearch,
57
+ translateX = animatedTranslateX,
58
+ width = animatedWidth,
59
+ height = animatedHeight
60
+ )
61
+ }
@@ -115,7 +115,7 @@ fun PopupNotify(
115
115
  )
116
116
 
117
117
  Column(modifier = Modifier.padding(Spacing.XL)) {
118
- Text(style = Typography.titleXs, maxLines = 2, text = title)
118
+ Text(style = Typography.titleXs, maxLines = 1, text = title)
119
119
  content(Modifier.padding(top = Spacing.S))
120
120
  if (error.isNotEmpty()) {
121
121
  Text(
@@ -23,12 +23,22 @@ public class MiniAppContext {
23
23
  }
24
24
  }
25
25
 
26
+ public struct Config: Decodable {
27
+ public let trustBanner: TrustBanner?
28
+
29
+ enum CodingKeys: String, CodingKey {
30
+ case trustBanner = "trust_banner"
31
+ }
32
+ }
33
+
26
34
  public class ApplicationEnvironment: ObservableObject {
27
35
  let applicationContext: MiniAppContext?
28
36
  let composeApi: KitComposeApi
37
+ @Published public var config: Config?
29
38
 
30
- public init(applicationContext: MiniAppContext?, composeApi: KitComposeApi) {
39
+ public init(applicationContext: MiniAppContext?, composeApi: KitComposeApi, config: Config? = nil) {
31
40
  self.applicationContext = applicationContext
32
41
  self.composeApi = composeApi
42
+ self.config = config
33
43
  }
34
44
  }
@@ -98,8 +98,9 @@ private extension View {
98
98
  case .secondary:
99
99
  return AnyView(self
100
100
  .background(Colors.card)
101
- .foregroundColor(Colors.primary)
101
+ .foregroundColor(Colors.text)
102
102
  .cornerRadius(8)
103
+ .overlay(RoundedRectangle(cornerRadius: Radius.S).stroke(Colors.black06, lineWidth: 1))
103
104
  .padding(2)
104
105
  )
105
106
  case .outline:
@@ -43,7 +43,6 @@ public struct PopupDisplay: View {
43
43
  Image(systemName: "xmark.circle.fill")
44
44
  .resizable()
45
45
  .frame(width: 16, height: 16)
46
- .overlay(RoundedRectangle(cornerRadius: Radius.S).stroke(Colors.black01, lineWidth: 1))
47
46
  }.foregroundColor(Colors.black20)
48
47
  .background(Colors.black01.cornerRadius(8))
49
48
  .offset(x: 6, y: -6)
@@ -53,17 +52,15 @@ public struct PopupDisplay: View {
53
52
  WebImage(url: URL(string: url), isAnimating: .constant(true))
54
53
  .resizable()
55
54
  .scaledToFill()
56
- .frame(maxWidth: .infinity)
55
+ .frame(maxWidth: .infinity, maxHeight: 164)
57
56
  .clipShape(RoundedCorner(radius: 8, corners: [.topLeft, .topRight]))
58
- .aspectRatio(2, contentMode: .fit)
59
57
 
60
58
  VStack(alignment: .leading) {
61
59
  Text(title)
62
60
  .foregroundColor(.black)
63
- .font(.title3)
64
- .padding(.top, 16)
65
- .padding(.bottom, 8)
66
- .lineLimit(2)
61
+ .font(.headerText1)
62
+ .padding(.top, 12)
63
+ .padding(.bottom, 24)
67
64
 
68
65
  Text(description)
69
66
  .foregroundColor(.black)
@@ -90,8 +87,10 @@ public struct PopupDisplay: View {
90
87
  }.clipped()
91
88
  }
92
89
  }
93
- .background(Color.white.cornerRadius(15))
94
- .padding(.horizontal, 24)
90
+ .background(Color.white.cornerRadius(8))
91
+ .shadow(color: .black.opacity(0.08), radius: 2, x: 0, y: 0)
92
+ .shadow(color: .black.opacity(0.16), radius: 24, x: 0, y: 0)
93
+ .padding(.horizontal, 40)
95
94
  }
96
95
 
97
96
  // MARK: Internal
@@ -107,7 +106,7 @@ public struct PopupDisplay: View {
107
106
  var onClose: () -> Void
108
107
  var actionButtonTitle: String
109
108
  var closeButtonTitle: String
110
-
109
+
111
110
  var RowTextButtons: some View {
112
111
  HStack {
113
112
  SwiftUI.Button(action: onPressCloseButton) {
@@ -154,10 +153,7 @@ public struct PopupDisplay: View {
154
153
 
155
154
  var ColumnButtons: some View {
156
155
  VStack(alignment: .trailing) {
157
- Button(
158
- title: actionButtonTitle,
159
- action: onPressAction
160
- )
156
+ Button(title: actionButtonTitle, action: onPressAction)
161
157
  Button(title: closeButtonTitle, action: onPressCloseButton, type: .secondary)
162
158
  }
163
159
  .frame(maxWidth: .infinity, alignment: .trailing)
@@ -9,14 +9,9 @@ import Foundation
9
9
  import SwiftUI
10
10
  import SDWebImageSwiftUI
11
11
 
12
- let jsonUrl = "https://static.momocdn.net/app/json/component-kits/design_system.json"
13
-
14
- struct TrustBannerData: Decodable {
15
- let trustBanner: TrustBanner
16
- }
17
-
18
- struct TrustBanner: Decodable {
12
+ public struct TrustBanner: Decodable {
19
13
  let content: Dictionary<String, String>
14
+ let subContent: Dictionary<String, String>
20
15
  let pciImage: String
21
16
  let sslImage: String
22
17
  let urlConfig: String
@@ -25,18 +20,20 @@ struct TrustBanner: Decodable {
25
20
  let momoImage: String
26
21
 
27
22
  enum CodingKeys: String, CodingKey {
28
- case content, pciImage, sslImage, icons, momoImage
23
+ case content, subContent, pciImage, sslImage, icons, momoImage
29
24
  case urlConfig = "url_config"
30
25
  case featureCode = "feature_code"
31
26
  }
32
27
  }
33
28
 
34
29
  let defaultBanner = TrustBanner(
35
- content: ["vi": "Bảo mật thông tin & An toàn tài sản của bạn là ưu tiên hàng đầu của MoMo.",
36
- "en": "Your data security and money safety are MoMo's top priorities."],
37
- pciImage: "https://static.momocdn.net/app/img/kits/trustBanner/pci.png",
30
+ content: ["vi": "An toàn tài sản & Bảo mật thông tin của bạn là ưu tiên hàng đầu của MoMo.",
31
+ "en": "Ensuring financial security and data privacy is MoMo's highest priority."],
32
+ subContent: ["vi": "Tìm hiểu thêm",
33
+ "en": "Learn more"],
34
+ pciImage: "https://static.momocdn.net/app/img/kits/trustBanner/pci_dss.png",
38
35
  sslImage: "https://static.momocdn.net/app/img/kits/trustBanner/ssl.png",
39
- urlConfig: "https://momo.vn/an-toan-bao-mat",
36
+ urlConfig: "login_and_security",
40
37
  featureCode: "login_and_security",
41
38
  icons: [
42
39
  "https://static.momocdn.net/app/img/kits/trustBanner/ic_viettinbank.png",
@@ -44,58 +41,23 @@ let defaultBanner = TrustBanner(
44
41
  "https://static.momocdn.net/app/img/kits/trustBanner/ic_vietcombank.png",
45
42
  "https://static.momocdn.net/app/img/kits/trustBanner/ic_bidv.png"
46
43
  ],
47
- momoImage: "https://static.momocdn.net/app/img/kits/trustBanner/ic_momo_secu.png"
44
+ momoImage: "https://static.momocdn.net/app/img/kits/trustBanner/ic_secu.png"
48
45
  )
49
46
 
50
- let defaultData = TrustBannerData(trustBanner: defaultBanner)
51
-
52
- class TrustBannerViewModel: ObservableObject {
53
- @Published var trustBanner: TrustBannerData?
54
-
55
- init() {
56
- fetchData(from: jsonUrl)
57
- }
58
-
59
- func fetchData(from urlString: String) {
60
- guard let url = URL(string: urlString) else { return }
61
-
62
- URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
63
- guard let self = self else { return }
64
-
65
- if let error = error {
66
- print("Data fetching error: \(error)")
67
- return
68
- }
69
-
70
- guard let data = data else {
71
- print("No data received")
72
- return
73
- }
74
- do {
75
- let decodedData = try JSONDecoder().decode(TrustBannerData.self, from: data)
76
- DispatchQueue.main.async {
77
- self.trustBanner = decodedData
78
- }
79
- } catch {
80
- print("Decoding error: \(error)")
81
- }
82
- }.resume()
83
- }
84
- }
85
-
86
47
  public struct TrustBannerView: View {
87
48
  var serviceName: String? = ""
88
49
  var screenName: String? = ""
89
50
  var onPress: ((_ params: NSDictionary) -> Void)?
90
51
  var trackEvent: ((_ eventName: String, _ params: NSDictionary) -> Void)?
91
52
  var language: String?
92
-
93
- @ObservedObject private var viewModel = TrustBannerViewModel()
53
+
54
+ @EnvironmentObject private var environment: ApplicationEnvironment
94
55
 
95
56
  public init(serviceName: String?,
96
57
  screenName: String?,
97
58
  onPress: ((_ params: NSDictionary) -> Void)?,
98
- trackEvent: ((_ eventName: String,_ params: NSDictionary) -> Void)?, language: String? = "vi") {
59
+ trackEvent: ((_ eventName: String,_ params: NSDictionary) -> Void)?,
60
+ language: String? = "vi") {
99
61
  if let trackEvent = trackEvent {
100
62
  trackEvent("service_component_displayed", ["service_name": serviceName ?? "", "screen_name": screenName ?? "", "component_name": "logo_trust"])
101
63
  }
@@ -107,9 +69,15 @@ public struct TrustBannerView: View {
107
69
  }
108
70
 
109
71
  public var body: some View {
110
- if let trustBanner = viewModel.trustBanner {
111
- TrustBannerContentView(trustBanner: trustBanner.trustBanner, onPress: onPress, trackEvent: trackEvent, serviceName: serviceName, screenName: screenName, language: language)
112
- }
72
+ let trustBanner = environment.config?.trustBanner ?? defaultBanner
73
+ TrustBannerContentView(
74
+ trustBanner: trustBanner,
75
+ onPress: onPress,
76
+ trackEvent: trackEvent,
77
+ serviceName: serviceName,
78
+ screenName: screenName,
79
+ language: language
80
+ )
113
81
  }
114
82
  }
115
83
 
@@ -128,52 +96,47 @@ struct TrustBannerContentView: View {
128
96
  .resizable()
129
97
  .scaledToFit()
130
98
  .frame(width: 64, height: 64)
131
- VStack(alignment: .leading, spacing: 8){
99
+ VStack(alignment: .leading, spacing: 8) {
132
100
  Text(trustBanner.content[language ?? "vi"] ?? "")
133
101
  .foregroundColor(Color(hex: 0xFF484848))
134
102
  .lineLimit(2)
135
- .font(.system(size: 12, weight: Font.Weight.regular))
136
- .frame(width: geo.size.width - 24 - 64 - 8, height: 40,alignment: .leading)
137
- HStack{
138
- HStack{
139
- WebImage(url: URL(string: trustBanner.pciImage))
140
- .resizable()
141
- .scaledToFit()
142
- .frame(width: 52, height: 20)
143
- WebImage(url: URL(string: trustBanner.sslImage))
103
+ .font(.system(size: 12))
104
+ .frame(maxWidth: .infinity, alignment: .leading)
105
+
106
+ HStack {
107
+ HStack(spacing: 2) {
108
+ Text(trustBanner.subContent[language ?? "vi"] ?? "")
109
+ .foregroundColor(Color(hex: 0xFFEB2F96))
110
+ .font(.system(size: 12))
111
+
112
+ WebImage(url: URL(string: "https://img.mservice.io/momo_app_v2/new_version/img/appx_icon/16_arrow_chevron_right_small.png"))
144
113
  .resizable()
145
- .scaledToFit()
146
- .frame(width: 52, height: 20)
114
+ .renderingMode(.template)
115
+ .foregroundColor(Color(hex: 0xFFEB2F96))
116
+ .frame(width: 16, height: 16)
147
117
  }
118
+
148
119
  Spacer()
149
- HStack(spacing: -6){
150
- ForEach(trustBanner.icons, id: \.self) { iconURL in
151
- WebImage(url: URL(string: iconURL))
120
+
121
+ HStack(spacing: 4) {
122
+ if let pciUrl = URL(string: trustBanner.pciImage) {
123
+ WebImage(url: pciUrl)
152
124
  .resizable()
153
125
  .scaledToFit()
154
- .frame(width:24, height: 24)
155
- .background(Color.white)
156
- .cornerRadius(12)
157
- .overlay(
158
- Circle().stroke(Color(hex: 0xFFE8E8E8), lineWidth: 1)
159
- )
126
+ .frame(width: 27, height: 20)
160
127
  }
161
- Text("36+")
162
- .font(.system(size: 10, weight: Font.Weight.regular))
163
- .frame(width: 24, height: 24, alignment: .center)
164
- .foregroundColor(Color(hex: 0xFFEB2F96))
165
- .background(Color(hex: 0xFFFDEAF4))
166
- .cornerRadius(12)
167
- .overlay(
168
- Circle()
169
- .stroke(Color(hex: 0xFFE8E8E8), lineWidth: 1)
170
- )
171
- }.frame(width: 96, height: 24)
128
+
129
+ if let sslUrl = URL(string: trustBanner.sslImage) {
130
+ WebImage(url: sslUrl)
131
+ .resizable()
132
+ .scaledToFit()
133
+ .frame(width: 52, height: 20)
134
+ }
135
+ }
172
136
  }
173
137
  }
174
138
  }
175
- .frame(width: geo.size.width - 24)
176
- .padding(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12))
139
+ .padding(12)
177
140
  .background(Color(hex: 0xFFF2F8FF))
178
141
  .cornerRadius(12)
179
142
  .onTapGesture {
@@ -181,7 +144,7 @@ struct TrustBannerContentView: View {
181
144
  trackEvent("service_component_clicked", ["service_name": self.serviceName ?? "", "screen_name": self.screenName ?? "", "component_name": "logo_trust", "url_config": trustBanner.urlConfig])
182
145
  }
183
146
  if let onPress = onPress {
184
- onPress(["service_name": self.serviceName ?? "", "screen_name": self.screenName ?? "", "component_name": "logo_trust", "url_config": trustBanner.urlConfig, "feature_code": trustBanner.featureCode])
147
+ onPress(["service_name": self.serviceName ?? "", "screen_name": self.screenName ?? "", "url_config": trustBanner.urlConfig])
185
148
  }
186
149
  }
187
150
  }
package/local.properties CHANGED
@@ -4,5 +4,5 @@
4
4
  # Location of the SDK. This is only used by Gradle.
5
5
  # For customization when using a Version Control System, please read the
6
6
  # header note.
7
- #Thu Jan 09 10:43:10 ICT 2025
8
- sdk.dir=/Users/sophia/Library/Android/sdk
7
+ #Tue Apr 23 16:40:15 ICT 2024
8
+ sdk.dir=/Users/hung.dao1/Library/Android/sdk
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/native-kits",
3
- "version": "0.113.1-beta.6",
3
+ "version": "0.113.1-beta.8",
4
4
  "private": false,
5
5
  "dependencies": {},
6
6
  "devDependencies": {},
@@ -1,379 +0,0 @@
1
- package vn.momo.kits.application
2
-
3
- import androidx.compose.animation.animateColorAsState
4
- import androidx.compose.animation.core.animateFloatAsState
5
- import androidx.compose.foundation.background
6
- import androidx.compose.foundation.border
7
- import androidx.compose.foundation.clickable
8
- import androidx.compose.foundation.layout.Arrangement
9
- import androidx.compose.foundation.layout.Box
10
- import androidx.compose.foundation.layout.BoxWithConstraints
11
- import androidx.compose.foundation.layout.Row
12
- import androidx.compose.foundation.layout.Spacer
13
- import androidx.compose.foundation.layout.fillMaxWidth
14
- import androidx.compose.foundation.layout.height
15
- import androidx.compose.foundation.layout.offset
16
- import androidx.compose.foundation.layout.padding
17
- import androidx.compose.foundation.layout.size
18
- import androidx.compose.foundation.layout.width
19
- import androidx.compose.foundation.shape.RoundedCornerShape
20
- import androidx.compose.runtime.Composable
21
- import androidx.compose.runtime.LaunchedEffect
22
- import androidx.compose.runtime.getValue
23
- import androidx.compose.runtime.mutableStateOf
24
- import androidx.compose.runtime.remember
25
- import androidx.compose.runtime.setValue
26
- import androidx.compose.runtime.snapshotFlow
27
- import androidx.compose.ui.Alignment
28
- import androidx.compose.ui.Modifier
29
- import androidx.compose.ui.draw.alpha
30
- import androidx.compose.ui.geometry.Offset
31
- import androidx.compose.ui.graphics.Brush
32
- import androidx.compose.ui.graphics.Color
33
- import androidx.compose.ui.graphics.graphicsLayer
34
- import androidx.compose.ui.layout.ContentScale
35
- import androidx.compose.ui.platform.LocalDensity
36
- import androidx.compose.ui.semantics.semantics
37
- import androidx.compose.ui.semantics.testTag
38
- import androidx.compose.ui.text.style.TextAlign
39
- import androidx.compose.ui.unit.Dp
40
- import androidx.compose.ui.unit.dp
41
- import androidx.compose.ui.zIndex
42
- import kotlinx.coroutines.flow.distinctUntilChanged
43
- import kotlinx.coroutines.flow.map
44
- import vn.momo.kits.components.Icon
45
- import vn.momo.kits.components.Image
46
- import vn.momo.kits.components.InputSearch
47
- import vn.momo.kits.components.InputSearchProps
48
- import vn.momo.kits.components.Options
49
- import vn.momo.kits.components.Text
50
- import vn.momo.kits.const.AppStatusBar
51
- import vn.momo.kits.const.AppTheme
52
- import vn.momo.kits.const.Colors
53
- import vn.momo.kits.const.Radius
54
- import vn.momo.kits.const.Spacing
55
- import vn.momo.kits.const.Typography
56
- import vn.momo.kits.modifier.setAutomationId
57
- import vn.momo.kits.platform.getScreenDimensions
58
- import vn.momo.kits.platform.getStatusBarHeight
59
- import vn.momo.kits.utils.bottomBorder
60
-
61
- enum class TitlePosition {
62
- LEFT, CENTER,
63
- }
64
-
65
- private const val SCREEN_PADDING = 12
66
- private const val BACK_WIDTH = 28
67
-
68
- data class AnimatedHeader(
69
- val type: HeaderType = HeaderType.DEFAULT,
70
- val composable: @Composable (scrollState: Int) -> Unit = {}
71
- )
72
-
73
- @Composable
74
- fun HeaderBackground(
75
- isBack: Boolean = true,
76
- headerType: HeaderType = HeaderType.DEFAULT,
77
- scrollState: Int,
78
- inputSearchProps: InputSearchProps? = null,
79
- headerRightWidth: Dp = 0.dp,
80
- useAnimationSearch: Boolean = false
81
- ) {
82
- val statusBarHeight = AppStatusBar.current ?: getStatusBarHeight()
83
- val screenWidth = getScreenDimensions().width
84
- val leftPosition = if (isBack) (BACK_WIDTH + 20).dp else 20.dp
85
- val searchWidth = screenWidth - (SCREEN_PADDING * 2)
86
-
87
- val opacityAni by animateFloatAsState(
88
- targetValue = (1 - (scrollState / 52f)).coerceIn(0f, 1f),
89
- )
90
-
91
- val backgroundColor by animateColorAsState(
92
- targetValue = animateColor(
93
- Color.Transparent,
94
- AppTheme.current.colors.background.surface,
95
- opacityAni
96
- ),
97
- )
98
-
99
- val backgroundSearch by animateColorAsState(
100
- targetValue = animateColor(
101
- AppTheme.current.colors.background.default,
102
- Colors.black_01,
103
- opacityAni
104
- ),
105
- )
106
-
107
- val animatedTranslateX by animateFloatAsState(
108
- targetValue = (scrollState / 52f).coerceIn(0f, 1f) * (leftPosition.value - 12) + 12,
109
- )
110
-
111
- val animatedWidth by animateFloatAsState(
112
- targetValue = (scrollState / 52f).coerceIn(
113
- 0f,
114
- 1f
115
- ) * ((searchWidth - leftPosition.value - headerRightWidth.value + 12) - searchWidth) + searchWidth,
116
- )
117
-
118
- val animatedHeight by animateFloatAsState(
119
- targetValue = (1 - (scrollState / 52f).coerceIn(
120
- 0f,
121
- 1f
122
- )) * (statusBarHeight.value + 94 - (statusBarHeight.value + 52)) + (statusBarHeight.value + 52),
123
- )
124
-
125
- when (headerType) {
126
- HeaderType.DEFAULT -> {
127
- val height = statusBarHeight + 52.dp
128
- Box(
129
- modifier = Modifier.fillMaxWidth().height(height)
130
- .background(backgroundColor)
131
- .bottomBorder(
132
- strokeWidth = 1.dp,
133
- color = AppTheme.current.colors.border.default
134
- )
135
- ) {
136
- Box(
137
- modifier = Modifier
138
- .height(height)
139
- .fillMaxWidth()
140
- .alpha(opacityAni)
141
- .background(
142
- Brush.linearGradient(
143
- colors = listOf(
144
- Color(0xFFFDCADE),
145
- Color(0x00FDCADE)
146
- ),
147
- start = Offset(0f, 0f),
148
- end = Offset(0f, with(LocalDensity.current) { 154.dp.toPx() })
149
- )
150
- )
151
- )
152
- if (AppTheme.current.assets.headerBackground != null) {
153
- Image(
154
- loading = false,
155
- source = AppTheme.current.assets.headerBackground!!,
156
- modifier = Modifier.fillMaxWidth().height(height),
157
- options = Options(
158
- contentScale = ContentScale.FillWidth
159
- )
160
- )
161
- }
162
- }
163
- }
164
-
165
- HeaderType.EXTENDED -> {
166
- Box(
167
- modifier = Modifier.fillMaxWidth().height(animatedHeight.dp)
168
- ) {
169
- Box(
170
- modifier = Modifier
171
- .fillMaxWidth()
172
- .height(154.dp)
173
- .alpha(opacityAni)
174
- .background(
175
- Brush.linearGradient(
176
- colors = listOf(
177
- Color(0xFFFDCADE),
178
- Color(0x00FDCADE)
179
- ),
180
- start = Offset(0f, 0f),
181
- end = Offset(0f, with(LocalDensity.current) { 154.dp.toPx() })
182
- )
183
- )
184
- ) {
185
- if (AppTheme.current.assets.headerBackground != null) {
186
- Box(
187
- modifier = Modifier
188
- .fillMaxWidth()
189
- .background(backgroundColor)
190
- ) {
191
- Image(
192
- loading = false,
193
- source = AppTheme.current.assets.headerBackground!!,
194
- modifier = Modifier.fillMaxWidth().height(154.dp),
195
- options = Options(
196
- contentScale = ContentScale.FillWidth
197
- )
198
- )
199
- }
200
- }
201
- }
202
-
203
- if (inputSearchProps != null && useAnimationSearch) {
204
- Box(
205
- modifier = Modifier
206
- .height(52.dp)
207
- .offset(
208
- x = animatedTranslateX.dp
209
- )
210
- .padding(vertical = Spacing.S)
211
- .align(Alignment.BottomStart)
212
- ) {
213
- Box(
214
- modifier = Modifier
215
- .width(animatedWidth.dp)
216
- .background(
217
- color = backgroundSearch,
218
- shape = RoundedCornerShape(Radius.XL)
219
- )
220
- ) {
221
- InputSearch(
222
- inputSearchProps = inputSearchProps.copy(
223
- showButtonText = false,
224
- backgroundColor = Color.Transparent
225
- )
226
- )
227
- }
228
- }
229
- }
230
- }
231
- }
232
-
233
- else -> {
234
- Box(modifier = Modifier.fillMaxWidth())
235
- }
236
- }
237
- }
238
-
239
- @Composable
240
- fun Header(
241
- headerType: HeaderType = HeaderType.DEFAULT,
242
- titlePosition: TitlePosition,
243
- title: String,
244
- headerRight: @Composable (() -> Unit)? = null,
245
- goBack: (() -> Unit)? = null,
246
- opacity: Float? = null,
247
- animatedHeader: AnimatedHeader? = null,
248
- scrollState: Int = 0,
249
- inputSearchProps: InputSearchProps? = null,
250
- useAnimationSearch: Boolean = false
251
- ) {
252
- val statusBarHeight = AppStatusBar.current ?: getStatusBarHeight()
253
- val tintIconColor = AppTheme.current.colors.text.default
254
- val backgroundButton = Colors.black_01.copy(alpha = 0.6f)
255
- val miniAppContext = ApplicationContext.current
256
-
257
- var colorFraction by remember { mutableStateOf(0f) }
258
-
259
- LaunchedEffect(scrollState) {
260
- snapshotFlow { scrollState }
261
- .map { (it / 50f).coerceIn(0f, 1f) }
262
- .distinctUntilChanged()
263
- .collect { fraction -> colorFraction = fraction }
264
- }
265
-
266
- if (headerType != HeaderType.NONE) {
267
- BoxWithConstraints(
268
- Modifier.height(statusBarHeight + 52.dp)
269
- .fillMaxWidth()
270
- .zIndex(10f),
271
- contentAlignment = Alignment.BottomCenter
272
- ) {
273
- Row(
274
- modifier = Modifier.height(52.dp)
275
- .fillMaxWidth()
276
- .padding(horizontal = Spacing.M),
277
- verticalAlignment = Alignment.CenterVertically,
278
- horizontalArrangement = Arrangement.SpaceBetween
279
- ) {
280
- Box {
281
- if (goBack != null) {
282
- Box(
283
- modifier = Modifier
284
- .size(28.dp)
285
- .then(
286
- Modifier.border(
287
- 0.2.dp,
288
- Colors.black_20.copy(alpha = 0.2f),
289
- RoundedCornerShape(100)
290
- )
291
- )
292
- .background(
293
- backgroundButton,
294
- RoundedCornerShape(100)
295
- )
296
- .clickable(
297
- onClick = goBack
298
- )
299
- .padding(Spacing.XS)
300
- .setAutomationId("btn_navigation_back"),
301
- contentAlignment = Alignment.Center
302
- ) {
303
- Icon(
304
- source = "arrow-back",
305
- color = Colors.black_20,
306
- size = 20.dp,
307
- )
308
- }
309
- }
310
- }
311
-
312
- if (inputSearchProps == null && miniAppContext != null) {
313
- Box(Modifier.graphicsLayer {
314
- alpha = if (animatedHeader !== null) opacity ?: 1f else 1f
315
- }) {
316
- headerRight?.invoke()
317
- }
318
- }
319
- }
320
-
321
- // Title
322
- Row(
323
- modifier = Modifier.height(52.dp)
324
- .fillMaxWidth()
325
- .padding(horizontal = Spacing.M),
326
- verticalAlignment = Alignment.CenterVertically,
327
- ) {
328
- if (goBack != null && titlePosition == TitlePosition.LEFT) {
329
- Spacer(Modifier.width(40.dp))
330
- } else {
331
- Box(
332
- Modifier.weight(1f).graphicsLayer {
333
- alpha = if (useAnimationSearch && inputSearchProps != null) {
334
- 1f - (opacity ?: 1f)
335
- } else {
336
- if (animatedHeader !== null) opacity ?: 1f else 1f
337
- }
338
- },
339
- contentAlignment = if (titlePosition == TitlePosition.LEFT) Alignment.TopStart else Alignment.Center
340
- ) {
341
- HeaderTitle(
342
- title = title,
343
- color = tintIconColor,
344
- modifier = Modifier.semantics {
345
- testTag = "title_navigation_header"
346
- }
347
- )
348
- }
349
- }
350
- }
351
- }
352
- }
353
- }
354
-
355
- @Composable
356
- fun HeaderTitle(
357
- title: String = "",
358
- modifier: Modifier = Modifier,
359
- textAlign: TextAlign = TextAlign.Start,
360
- color: Color? = null,
361
- ) {
362
- Text(
363
- modifier = Modifier.then(modifier).zIndex(1f),
364
- text = title,
365
- textAlign = textAlign,
366
- style = Typography.actionS,
367
- color = color,
368
- maxLines = 1
369
- )
370
- }
371
-
372
- fun animateColor(start: Color, stop: Color, fraction: Float): Color {
373
- return Color(
374
- red = start.red + fraction * (stop.red - start.red),
375
- green = start.green + fraction * (stop.green - start.green),
376
- blue = start.blue + fraction * (stop.blue - start.blue),
377
- alpha = start.alpha + fraction * (stop.alpha - start.alpha)
378
- )
379
- }