@momo-kits/native-kits 0.152.4-maxapi → 0.152.4-scale.3
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/compose/build.gradle.kts +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +2 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +246 -78
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +3 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +21 -19
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +12 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +22 -11
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +6 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +19 -24
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +24 -19
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +25 -18
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +3 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +0 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +42 -7
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +106 -36
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScaleSizeScope.kt +8 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +34 -10
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +11 -10
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +125 -0
- package/ios/Badge/Badge.swift +85 -0
- package/ios/Badge/BadgeRibbon.swift +242 -0
- package/ios/Button/Button.swift +7 -7
- package/local.properties +8 -0
- package/package.json +1 -1
|
@@ -115,11 +115,11 @@ internal fun StackScreen(
|
|
|
115
115
|
HeaderBackground()
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
Box(Modifier.zIndex(
|
|
118
|
+
Box(Modifier.zIndex(5f)) {
|
|
119
119
|
Header()
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
Column (Modifier.zIndex(
|
|
122
|
+
Column (Modifier.zIndex(6f)) {
|
|
123
123
|
SearchAnimated(isScrollInProgress = scrollInProcess)
|
|
124
124
|
}
|
|
125
125
|
|
|
@@ -127,14 +127,15 @@ internal fun StackScreen(
|
|
|
127
127
|
MainContent(content = content)
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
Box(Modifier.zIndex(
|
|
131
|
-
|
|
130
|
+
Box(Modifier.zIndex(4f).align(Alignment.BottomCenter)) {
|
|
131
|
+
FooterContent()
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
Box(Modifier.zIndex(
|
|
135
|
-
|
|
136
|
-
OverplayComponentRegistry.OverlayComponent()
|
|
134
|
+
Box(Modifier.zIndex(7f)){
|
|
135
|
+
FloatingContent()
|
|
137
136
|
}
|
|
137
|
+
|
|
138
|
+
OverplayView(bottomTabIndex = bottomTabIndex, id = id)
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
}
|
|
@@ -191,9 +192,9 @@ fun ColumnScope.MainContent(content: @Composable ()-> Unit){
|
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
if (options.footerComponent != null){
|
|
194
|
-
val
|
|
195
|
-
val
|
|
196
|
-
|
|
195
|
+
val footerHeight = LocalFooterHeightPx.current
|
|
196
|
+
val footerHeightDp = with(density) { footerHeight.value.toDp() }
|
|
197
|
+
Spacer(Modifier.height(footerHeightDp))
|
|
197
198
|
}
|
|
198
199
|
}
|
|
199
200
|
|
|
@@ -217,6 +218,29 @@ fun ScreenContent(content: @Composable () -> Unit){
|
|
|
217
218
|
}
|
|
218
219
|
}
|
|
219
220
|
|
|
221
|
+
@Composable
|
|
222
|
+
fun FooterContent(){
|
|
223
|
+
val options = LocalOptions.current
|
|
224
|
+
if (options.footerComponent != null){
|
|
225
|
+
val isKeyboardVisible = isKeyboardVisible()
|
|
226
|
+
val bottomPadding = min(AppNavigationBar.current, if (isKeyboardVisible) 0.dp else 21.dp)
|
|
227
|
+
Footer(footerComponent = options.footerComponent, bottomPadding = bottomPadding)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@Composable
|
|
232
|
+
fun OverplayView(bottomTabIndex: Int, id: Int){
|
|
233
|
+
val overplayType = OverplayComponentRegistry.getOverplayType()
|
|
234
|
+
|
|
235
|
+
if (overplayType != null) {
|
|
236
|
+
Box(Modifier.zIndex(if (overplayType == OverplayComponentType.SNACK_BAR) 3f else 8f).fillMaxSize()){
|
|
237
|
+
if (bottomTabIndex != -1) return@Box
|
|
238
|
+
if (OverplayComponentRegistry.currentRootId() != id) return@Box
|
|
239
|
+
OverplayComponentRegistry.OverlayComponent()
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
220
244
|
@Composable
|
|
221
245
|
fun Footer(footerComponent: @Composable (() -> Unit)?, bottomPadding: Dp) {
|
|
222
246
|
if (footerComponent == null) return
|
|
@@ -4,6 +4,7 @@ import androidx.compose.animation.core.FastOutSlowInEasing
|
|
|
4
4
|
import androidx.compose.animation.core.animateDpAsState
|
|
5
5
|
import androidx.compose.animation.core.tween
|
|
6
6
|
import androidx.compose.foundation.background
|
|
7
|
+
import androidx.compose.foundation.border
|
|
7
8
|
import androidx.compose.foundation.layout.Arrangement
|
|
8
9
|
import androidx.compose.foundation.layout.Box
|
|
9
10
|
import androidx.compose.foundation.layout.Column
|
|
@@ -39,6 +40,7 @@ import vn.momo.kits.const.Colors
|
|
|
39
40
|
import vn.momo.kits.const.Radius
|
|
40
41
|
import vn.momo.kits.const.Spacing
|
|
41
42
|
import vn.momo.kits.const.Typography
|
|
43
|
+
import vn.momo.kits.const.scaleSize
|
|
42
44
|
import vn.momo.kits.modifier.noFeedbackClickable
|
|
43
45
|
import vn.momo.kits.platform.getScreenDimensions
|
|
44
46
|
|
|
@@ -71,6 +73,7 @@ fun BottomTabBar(
|
|
|
71
73
|
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing),
|
|
72
74
|
label = "IndicatorX"
|
|
73
75
|
)
|
|
76
|
+
val bottomTabBarHeight = scaleSize(BOTTOM_TAB_BAR_HEIGHT.toFloat())
|
|
74
77
|
|
|
75
78
|
Box(
|
|
76
79
|
contentAlignment = Alignment.BottomCenter
|
|
@@ -156,17 +159,15 @@ fun TabBarItem(item: BottomTabItem, selected: Boolean, onClick: () -> Unit) {
|
|
|
156
159
|
) {
|
|
157
160
|
Icon(
|
|
158
161
|
source = item.icon,
|
|
162
|
+
modifier = Modifier.weight(1f),
|
|
159
163
|
color = if (selected) AppTheme.current.colors.primary else AppTheme.current.colors.text.hint)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
overflow = TextOverflow.Ellipsis
|
|
168
|
-
)
|
|
169
|
-
}
|
|
164
|
+
Text(
|
|
165
|
+
text = item.label,
|
|
166
|
+
color = if (selected) AppTheme.current.colors.primary else AppTheme.current.colors.text.hint,
|
|
167
|
+
style = Typography.labelXsMedium,
|
|
168
|
+
maxLines = 1,
|
|
169
|
+
overflow = TextOverflow.Ellipsis
|
|
170
|
+
)
|
|
170
171
|
}
|
|
171
172
|
if(item.badgeLabel != null){
|
|
172
173
|
Box(modifier = Modifier
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
package vn.momo.kits.navigation.component
|
|
2
|
+
|
|
3
|
+
import androidx.compose.animation.core.Animatable
|
|
4
|
+
import androidx.compose.animation.core.tween
|
|
5
|
+
import androidx.compose.foundation.layout.Box
|
|
6
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
7
|
+
import androidx.compose.foundation.layout.offset
|
|
8
|
+
import androidx.compose.runtime.Composable
|
|
9
|
+
import androidx.compose.runtime.DisposableEffect
|
|
10
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
11
|
+
import androidx.compose.runtime.getValue
|
|
12
|
+
import androidx.compose.runtime.mutableStateOf
|
|
13
|
+
import androidx.compose.runtime.remember
|
|
14
|
+
import androidx.compose.runtime.rememberCoroutineScope
|
|
15
|
+
import androidx.compose.runtime.setValue
|
|
16
|
+
import androidx.compose.ui.Modifier
|
|
17
|
+
import androidx.compose.ui.layout.onGloballyPositioned
|
|
18
|
+
import androidx.compose.ui.platform.LocalDensity
|
|
19
|
+
import androidx.compose.ui.unit.IntOffset
|
|
20
|
+
import androidx.compose.ui.unit.dp
|
|
21
|
+
import androidx.compose.ui.unit.min
|
|
22
|
+
import kotlinx.coroutines.delay
|
|
23
|
+
import kotlinx.coroutines.launch
|
|
24
|
+
import vn.momo.kits.const.AppNavigationBar
|
|
25
|
+
import vn.momo.kits.navigation.LocalFooterHeightPx
|
|
26
|
+
import vn.momo.kits.navigation.LocalNavigator
|
|
27
|
+
import vn.momo.kits.navigation.LocalOptions
|
|
28
|
+
import vn.momo.kits.navigation.OverplayComponentRegistry
|
|
29
|
+
|
|
30
|
+
sealed class SnackBar(open val duration: Long? = null) {
|
|
31
|
+
data class Custom(
|
|
32
|
+
val content: @Composable () -> Unit,
|
|
33
|
+
override val duration: Long? = 3000L
|
|
34
|
+
) : SnackBar(duration)
|
|
35
|
+
|
|
36
|
+
data class Toast(
|
|
37
|
+
override val duration: Long? = 3000L
|
|
38
|
+
) : SnackBar(duration)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Composable
|
|
42
|
+
fun SnackBar(
|
|
43
|
+
data: SnackBar,
|
|
44
|
+
onDismiss: (() -> Unit)?
|
|
45
|
+
) {
|
|
46
|
+
val navigator = LocalNavigator.current
|
|
47
|
+
val options = LocalOptions.current
|
|
48
|
+
val density = LocalDensity.current
|
|
49
|
+
val footerHeightPxState = LocalFooterHeightPx.current
|
|
50
|
+
val navigationBar = AppNavigationBar.current
|
|
51
|
+
|
|
52
|
+
val footerHeight = if (options.footerComponent != null) {
|
|
53
|
+
footerHeightPxState.value
|
|
54
|
+
} else {
|
|
55
|
+
with(density){ min(navigationBar, 21.dp).toPx() }
|
|
56
|
+
}.toInt()
|
|
57
|
+
|
|
58
|
+
var startPosition by remember { mutableStateOf(Float.MAX_VALUE) }
|
|
59
|
+
val targetPosition = 0f
|
|
60
|
+
|
|
61
|
+
var offsetY by remember { mutableStateOf(Animatable(startPosition)) }
|
|
62
|
+
val coroutineScope = rememberCoroutineScope()
|
|
63
|
+
|
|
64
|
+
suspend fun openEvent() {
|
|
65
|
+
offsetY.snapTo(startPosition)
|
|
66
|
+
offsetY.animateTo(
|
|
67
|
+
targetValue = targetPosition,
|
|
68
|
+
animationSpec = tween(
|
|
69
|
+
durationMillis = 350,
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fun closeEvent() {
|
|
75
|
+
coroutineScope.launch {
|
|
76
|
+
offsetY.animateTo(
|
|
77
|
+
targetValue = startPosition,
|
|
78
|
+
animationSpec = tween(
|
|
79
|
+
durationMillis = 200,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
navigator.hideSnackBar()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
LaunchedEffect(startPosition) {
|
|
87
|
+
if (startPosition != Float.MAX_VALUE){
|
|
88
|
+
openEvent()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
LaunchedEffect(data.duration) {
|
|
93
|
+
val duration = data.duration
|
|
94
|
+
if (duration != null) {
|
|
95
|
+
delay(duration)
|
|
96
|
+
navigator.hideSnackBar()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
DisposableEffect(Unit) {
|
|
101
|
+
OverplayComponentRegistry.bindClose {
|
|
102
|
+
closeEvent()
|
|
103
|
+
}
|
|
104
|
+
onDispose {
|
|
105
|
+
onDismiss?.invoke()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
Box(
|
|
110
|
+
modifier = Modifier
|
|
111
|
+
.offset { IntOffset(0, offsetY.value.toInt() - footerHeight) }
|
|
112
|
+
.onGloballyPositioned {
|
|
113
|
+
if (startPosition != it.size.height.toFloat()) {
|
|
114
|
+
startPosition = it.size.height.toFloat()
|
|
115
|
+
offsetY = Animatable(startPosition)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
.fillMaxWidth()
|
|
119
|
+
) {
|
|
120
|
+
when (data) {
|
|
121
|
+
is SnackBar.Custom -> data.content()
|
|
122
|
+
is SnackBar.Toast -> {}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
public struct Badge: View {
|
|
4
|
+
let label: String
|
|
5
|
+
let backgroundColor: Color?
|
|
6
|
+
|
|
7
|
+
private var primaryColors: [Color] {
|
|
8
|
+
[
|
|
9
|
+
Colors.black03,
|
|
10
|
+
Colors.pink03,
|
|
11
|
+
Colors.violet03,
|
|
12
|
+
Colors.indigo03,
|
|
13
|
+
Colors.blue03,
|
|
14
|
+
Colors.mint03,
|
|
15
|
+
Colors.green03,
|
|
16
|
+
Colors.lime03,
|
|
17
|
+
Colors.yellow03,
|
|
18
|
+
Colors.gold03,
|
|
19
|
+
Colors.orange03,
|
|
20
|
+
Colors.red03
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public init(label: String = "Label", backgroundColor: Color? = nil) {
|
|
25
|
+
self.label = label
|
|
26
|
+
self.backgroundColor = backgroundColor
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private func isNumber(_ text: String) -> Bool {
|
|
30
|
+
return text.range(of: "^\\d+$", options: .regularExpression) != nil
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private func formatTitle(_ text: String) -> String {
|
|
34
|
+
if isNumber(text), let number = Int(text), number > 99 {
|
|
35
|
+
return "99+"
|
|
36
|
+
}
|
|
37
|
+
return text
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private var badgeColor: Color {
|
|
41
|
+
// If a custom background color is provided, use it
|
|
42
|
+
if let bgColor = backgroundColor {
|
|
43
|
+
return bgColor
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Otherwise, use default colors based on label type
|
|
47
|
+
if isNumber(label) {
|
|
48
|
+
return Colors.red03 // error.primary
|
|
49
|
+
} else {
|
|
50
|
+
return Colors.orange03 // warning.primary
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public var body: some View {
|
|
55
|
+
Text(formatTitle(label))
|
|
56
|
+
.font(.system(size: scaleSize(10), weight: .bold))
|
|
57
|
+
.foregroundColor(Colors.black01)
|
|
58
|
+
.padding(.horizontal, Spacing.XS)
|
|
59
|
+
.frame(minWidth: scaleSize(16), minHeight: scaleSize(16))
|
|
60
|
+
.background(badgeColor)
|
|
61
|
+
.cornerRadius(Radius.M)
|
|
62
|
+
.overlay(
|
|
63
|
+
RoundedRectangle(cornerRadius: Radius.M)
|
|
64
|
+
.stroke(Colors.black01, lineWidth: 1)
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// MARK: - Preview
|
|
70
|
+
#if DEBUG
|
|
71
|
+
struct Badge_Previews: PreviewProvider {
|
|
72
|
+
static var previews: some View {
|
|
73
|
+
VStack(spacing: 20) {
|
|
74
|
+
Badge(label: "5")
|
|
75
|
+
Badge(label: "99")
|
|
76
|
+
Badge(label: "100")
|
|
77
|
+
Badge(label: "New")
|
|
78
|
+
Badge(label: "Hot", backgroundColor: Colors.pink03)
|
|
79
|
+
}
|
|
80
|
+
.padding()
|
|
81
|
+
.previewLayout(.sizeThatFits)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
#endif
|
|
85
|
+
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
// MARK: - Custom Shape for Rounded Corners
|
|
4
|
+
struct RoundedCorners: Shape {
|
|
5
|
+
var topLeft: CGFloat = 0.0
|
|
6
|
+
var topRight: CGFloat = 0.0
|
|
7
|
+
var bottomLeft: CGFloat = 0.0
|
|
8
|
+
var bottomRight: CGFloat = 0.0
|
|
9
|
+
|
|
10
|
+
func path(in rect: CGRect) -> Path {
|
|
11
|
+
var path = Path()
|
|
12
|
+
|
|
13
|
+
let width = rect.size.width
|
|
14
|
+
let height = rect.size.height
|
|
15
|
+
|
|
16
|
+
// Start from top left
|
|
17
|
+
path.move(to: CGPoint(x: topLeft, y: 0))
|
|
18
|
+
|
|
19
|
+
// Top edge and top right corner
|
|
20
|
+
path.addLine(to: CGPoint(x: width - topRight, y: 0))
|
|
21
|
+
if topRight > 0 {
|
|
22
|
+
path.addArc(
|
|
23
|
+
center: CGPoint(x: width - topRight, y: topRight),
|
|
24
|
+
radius: topRight,
|
|
25
|
+
startAngle: Angle(degrees: -90),
|
|
26
|
+
endAngle: Angle(degrees: 0),
|
|
27
|
+
clockwise: false
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Right edge and bottom right corner
|
|
32
|
+
path.addLine(to: CGPoint(x: width, y: height - bottomRight))
|
|
33
|
+
if bottomRight > 0 {
|
|
34
|
+
path.addArc(
|
|
35
|
+
center: CGPoint(x: width - bottomRight, y: height - bottomRight),
|
|
36
|
+
radius: bottomRight,
|
|
37
|
+
startAngle: Angle(degrees: 0),
|
|
38
|
+
endAngle: Angle(degrees: 90),
|
|
39
|
+
clockwise: false
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Bottom edge and bottom left corner
|
|
44
|
+
path.addLine(to: CGPoint(x: bottomLeft, y: height))
|
|
45
|
+
if bottomLeft > 0 {
|
|
46
|
+
path.addArc(
|
|
47
|
+
center: CGPoint(x: bottomLeft, y: height - bottomLeft),
|
|
48
|
+
radius: bottomLeft,
|
|
49
|
+
startAngle: Angle(degrees: 90),
|
|
50
|
+
endAngle: Angle(degrees: 180),
|
|
51
|
+
clockwise: false
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Left edge and top left corner
|
|
56
|
+
path.addLine(to: CGPoint(x: 0, y: topLeft))
|
|
57
|
+
if topLeft > 0 {
|
|
58
|
+
path.addArc(
|
|
59
|
+
center: CGPoint(x: topLeft, y: topLeft),
|
|
60
|
+
radius: topLeft,
|
|
61
|
+
startAngle: Angle(degrees: 180),
|
|
62
|
+
endAngle: Angle(degrees: 270),
|
|
63
|
+
clockwise: false
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
path.closeSubpath()
|
|
68
|
+
return path
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public enum RibbonPosition {
|
|
73
|
+
case topLeft
|
|
74
|
+
case topRight
|
|
75
|
+
case bottomLeft
|
|
76
|
+
case bottomRight
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public struct BadgeRibbon: View {
|
|
80
|
+
let position: RibbonPosition
|
|
81
|
+
let label: String
|
|
82
|
+
let isRound: Bool
|
|
83
|
+
let backgroundColor: Color
|
|
84
|
+
|
|
85
|
+
public init(
|
|
86
|
+
position: RibbonPosition = .topRight,
|
|
87
|
+
label: String = "Label",
|
|
88
|
+
isRound: Bool = false,
|
|
89
|
+
backgroundColor: Color? = nil
|
|
90
|
+
) {
|
|
91
|
+
self.position = position
|
|
92
|
+
self.label = label
|
|
93
|
+
self.isRound = isRound
|
|
94
|
+
self.backgroundColor = backgroundColor ?? Colors.orange03
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private var rotation: Angle {
|
|
98
|
+
switch position {
|
|
99
|
+
case .topRight, .bottomRight:
|
|
100
|
+
return .degrees(180)
|
|
101
|
+
case .topLeft, .bottomLeft:
|
|
102
|
+
return .zero
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private var useUpTail: Bool {
|
|
107
|
+
position == .bottomLeft || position == .topRight
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private var verticalAlignment: VerticalAlignment {
|
|
111
|
+
switch position {
|
|
112
|
+
case .topLeft, .bottomRight:
|
|
113
|
+
return .top
|
|
114
|
+
case .bottomLeft, .topRight:
|
|
115
|
+
return .bottom
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public var body: some View {
|
|
120
|
+
HStack(alignment: verticalAlignment, spacing: 0) {
|
|
121
|
+
if useUpTail {
|
|
122
|
+
upTail
|
|
123
|
+
} else {
|
|
124
|
+
downTail
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if isRound {
|
|
128
|
+
roundContent
|
|
129
|
+
} else {
|
|
130
|
+
skewContent
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
.frame(height: RibbonConstants.ribbonHeight)
|
|
134
|
+
.rotationEffect(rotation)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private var roundContent: some View {
|
|
138
|
+
HStack(spacing: 0) {
|
|
139
|
+
Text(label)
|
|
140
|
+
.font(.system(size: 12, weight: .medium))
|
|
141
|
+
.foregroundColor(Colors.black01)
|
|
142
|
+
.lineLimit(1)
|
|
143
|
+
.rotationEffect(rotation)
|
|
144
|
+
}
|
|
145
|
+
.frame(height: RibbonConstants.roundHeight)
|
|
146
|
+
.padding(.trailing, RibbonConstants.roundPaddingEnd)
|
|
147
|
+
.background(
|
|
148
|
+
RoundedCorners(
|
|
149
|
+
topLeft: 0,
|
|
150
|
+
topRight: RibbonConstants.roundRightRadius,
|
|
151
|
+
bottomLeft: 0,
|
|
152
|
+
bottomRight: RibbonConstants.roundRightRadius
|
|
153
|
+
)
|
|
154
|
+
.fill(backgroundColor)
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private var skewContent: some View {
|
|
159
|
+
HStack(spacing: 0) {
|
|
160
|
+
Text(label)
|
|
161
|
+
.font(.system(size: 12, weight: .medium))
|
|
162
|
+
.foregroundColor(Colors.black01)
|
|
163
|
+
.lineLimit(1)
|
|
164
|
+
.rotationEffect(rotation)
|
|
165
|
+
.frame(height: RibbonConstants.skewBodyHeight)
|
|
166
|
+
.padding(.horizontal, 8)
|
|
167
|
+
.background(backgroundColor)
|
|
168
|
+
|
|
169
|
+
rightTail
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private var upTail: some View {
|
|
174
|
+
ImageView(
|
|
175
|
+
"https://static.momocdn.net/app/img/kits/utils/Head_down_4x.png",
|
|
176
|
+
placeholder: AnyView(Color.clear)
|
|
177
|
+
)
|
|
178
|
+
.frame(width: RibbonConstants.headTailWidth, height: RibbonConstants.headTailHeight)
|
|
179
|
+
.rotationEffect(.degrees(180))
|
|
180
|
+
.offset(x: 1)
|
|
181
|
+
.clipped()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private var downTail: some View {
|
|
185
|
+
ImageView(
|
|
186
|
+
"https://static.momocdn.net/app/img/kits/utils/Head_4x.png",
|
|
187
|
+
placeholder: AnyView(Color.clear)
|
|
188
|
+
)
|
|
189
|
+
.frame(width: RibbonConstants.headTailWidth, height: RibbonConstants.headTailHeight)
|
|
190
|
+
.offset(x: 1)
|
|
191
|
+
.clipped()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private var rightTail: some View {
|
|
195
|
+
ImageView(
|
|
196
|
+
"https://static.momocdn.net/app/img/kits/utils/Tail_4x.png",
|
|
197
|
+
placeholder: AnyView(Color.clear)
|
|
198
|
+
)
|
|
199
|
+
.frame(width: RibbonConstants.skewTailWidth, height: RibbonConstants.skewTailHeight)
|
|
200
|
+
.offset(x: -2)
|
|
201
|
+
.clipped()
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// MARK: - Constants
|
|
206
|
+
private struct RibbonConstants {
|
|
207
|
+
static let ribbonHeight: CGFloat = 20
|
|
208
|
+
static let roundHeight: CGFloat = 16
|
|
209
|
+
static let skewBodyHeight: CGFloat = 16
|
|
210
|
+
static let roundRightRadius: CGFloat = 8
|
|
211
|
+
static let roundPaddingEnd: CGFloat = 6
|
|
212
|
+
static let skewTailWidth: CGFloat = 8
|
|
213
|
+
static let skewTailHeight: CGFloat = 16
|
|
214
|
+
static let headTailWidth: CGFloat = 5
|
|
215
|
+
static let headTailHeight: CGFloat = 20
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// MARK: - String Extension for Text Size
|
|
219
|
+
extension String {
|
|
220
|
+
func size(withAttributes attributes: [NSAttributedString.Key: Any]?) -> CGSize {
|
|
221
|
+
let size = (self as NSString).size(withAttributes: attributes)
|
|
222
|
+
return size
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// MARK: - Preview
|
|
227
|
+
#if DEBUG
|
|
228
|
+
struct BadgeRibbon_Previews: PreviewProvider {
|
|
229
|
+
static var previews: some View {
|
|
230
|
+
VStack(spacing: 30) {
|
|
231
|
+
// Basic ribbon examples
|
|
232
|
+
BadgeRibbon(position: .topRight, label: "New")
|
|
233
|
+
BadgeRibbon(position: .topLeft, label: "Hot", isRound: true)
|
|
234
|
+
BadgeRibbon(position: .bottomLeft, label: "Sale")
|
|
235
|
+
BadgeRibbon(position: .bottomRight, label: "50% OFF", isRound: true)
|
|
236
|
+
}
|
|
237
|
+
.padding()
|
|
238
|
+
.previewLayout(.sizeThatFits)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
#endif
|
|
242
|
+
|
package/ios/Button/Button.swift
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
-
import SwiftUI
|
|
3
2
|
import Lottie
|
|
3
|
+
import SwiftUI
|
|
4
4
|
|
|
5
5
|
// MARK: - ButtonType
|
|
6
6
|
|
|
@@ -33,7 +33,7 @@ public extension ButtonType {
|
|
|
33
33
|
func backgroundColor(loading: Bool) -> Color {
|
|
34
34
|
switch self {
|
|
35
35
|
case .disabled: return Colors.black05.opacity(loading ? 0.75 : 1)
|
|
36
|
-
case .primary:
|
|
36
|
+
case .primary: return Colors.primary.opacity(loading ? 0.75 : 1)
|
|
37
37
|
case .secondary: return Colors.card.opacity(loading ? 0.75 : 1)
|
|
38
38
|
case .outline: return Colors.card.opacity(loading ? 0.75 : 1)
|
|
39
39
|
case .tonal: return Colors.primaryLight.opacity(loading ? 0.75 : 1)
|
|
@@ -116,13 +116,13 @@ extension ButtonSize {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
public struct Button: View {
|
|
119
|
-
|
|
120
119
|
var title: String
|
|
121
120
|
var action: () -> Void
|
|
122
121
|
var type: ButtonType
|
|
123
122
|
var size: ButtonSize
|
|
124
123
|
var iconLeft: AnyView?
|
|
125
124
|
var iconRight: AnyView?
|
|
125
|
+
var isFull: Bool
|
|
126
126
|
var loading: Bool
|
|
127
127
|
|
|
128
128
|
public init(
|
|
@@ -132,6 +132,7 @@ public struct Button: View {
|
|
|
132
132
|
size: ButtonSize = .large,
|
|
133
133
|
iconLeft: AnyView? = nil,
|
|
134
134
|
iconRight: AnyView? = nil,
|
|
135
|
+
isFull: Bool = true,
|
|
135
136
|
loading: Bool = false
|
|
136
137
|
) {
|
|
137
138
|
self.title = title
|
|
@@ -140,6 +141,7 @@ public struct Button: View {
|
|
|
140
141
|
self.size = size
|
|
141
142
|
self.iconLeft = iconLeft
|
|
142
143
|
self.iconRight = iconRight
|
|
144
|
+
self.isFull = isFull
|
|
143
145
|
self.loading = loading
|
|
144
146
|
}
|
|
145
147
|
|
|
@@ -156,7 +158,6 @@ public struct Button: View {
|
|
|
156
158
|
}
|
|
157
159
|
|
|
158
160
|
public var body: some View {
|
|
159
|
-
|
|
160
161
|
let loadingOnLeft = shouldLoadingOnLeft(iconLeft, iconRight)
|
|
161
162
|
|
|
162
163
|
let bg = type.backgroundColor(loading: loading)
|
|
@@ -165,12 +166,11 @@ public struct Button: View {
|
|
|
165
166
|
let borderWidth = type.borderWidth
|
|
166
167
|
|
|
167
168
|
SwiftUI.Button(action: {
|
|
168
|
-
if !loading
|
|
169
|
+
if !loading, type != .disabled {
|
|
169
170
|
action()
|
|
170
171
|
}
|
|
171
172
|
}) {
|
|
172
173
|
HStack(spacing: size.iconSpacing) {
|
|
173
|
-
|
|
174
174
|
if loading && loadingOnLeft {
|
|
175
175
|
LottieView(name: "lottie_circle_loader", loopMode: .loop)
|
|
176
176
|
.frame(width: size.iconSize, height: size.iconSize)
|
|
@@ -195,7 +195,7 @@ public struct Button: View {
|
|
|
195
195
|
iconRight.frame(width: size.iconSize, height: size.iconSize)
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
|
-
.frame(maxWidth: .infinity)
|
|
198
|
+
.frame(maxWidth: isFull ? .infinity : nil)
|
|
199
199
|
.padding(.horizontal, size.padding)
|
|
200
200
|
.frame(height: size.height)
|
|
201
201
|
.background(bg)
|
package/local.properties
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
## This file must *NOT* be checked into Version Control Systems,
|
|
2
|
+
# as it contains information specific to your local configuration.
|
|
3
|
+
#
|
|
4
|
+
# Location of the SDK. This is only used by Gradle.
|
|
5
|
+
# For customization when using a Version Control System, please read the
|
|
6
|
+
# header note.
|
|
7
|
+
#Wed Sep 10 18:28:25 ICT 2025
|
|
8
|
+
sdk.dir=/Users/phuc/Library/Android/sdk
|