@momo-kits/native-kits 0.156.6-beta.22-debug → 0.156.6-beta.23-debug
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/compose/build.gradle.kts +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +19 -17
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +198 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +12 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +576 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +29 -12
- package/gradle.properties +1 -1
- package/ios/Application/FloatingButton.swift +1 -1
- package/ios/Badge/BadgeRibbon.swift +2 -2
- package/ios/Image/Image.swift +5 -1
- package/ios/Popup/PopupDisplay.swift +7 -17
- package/local.properties +8 -0
- package/package.json +1 -1
- package/publish.sh +26 -1
package/compose/build.gradle.kts
CHANGED
|
@@ -23,7 +23,7 @@ import vn.momo.kits.modifier.conditional
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
@Composable
|
|
26
|
-
fun Badge(label: String = "Label", backgroundColor: Color? = null) {
|
|
26
|
+
fun Badge(label: String = "Label", backgroundColor: Color? = null, modifier: Modifier? = null) {
|
|
27
27
|
val primaryColors = listOf(
|
|
28
28
|
Color(0xFFF0F0F0),
|
|
29
29
|
Color(0xFFEB2F96),
|
|
@@ -63,21 +63,23 @@ fun Badge(label: String = "Label", backgroundColor: Color? = null) {
|
|
|
63
63
|
}
|
|
64
64
|
val scaleSize = scaleSize(16f)
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
66
|
+
if (modifier != null) {
|
|
67
|
+
Box(
|
|
68
|
+
modifier = modifier
|
|
69
|
+
.height(scaleSize.dp)
|
|
70
|
+
.widthIn(min = scaleSize.dp)
|
|
71
|
+
.background(color = badgeColor, shape = RoundedCornerShape(Radius.M))
|
|
72
|
+
.border(width = 1.dp, shape = RoundedCornerShape(Radius.M), color = Colors.black_01)
|
|
73
|
+
.conditional(IsShowBaseLineDebug) {
|
|
74
|
+
border(1.dp, Colors.blue_03)
|
|
75
|
+
}
|
|
76
|
+
.padding(horizontal = Spacing.XS), contentAlignment = Alignment.Center
|
|
77
|
+
) {
|
|
78
|
+
Text(
|
|
79
|
+
text = formatTitle(label),
|
|
80
|
+
color = Colors.black_01,
|
|
81
|
+
style = Typography.actionXxsBold
|
|
82
|
+
)
|
|
83
|
+
}
|
|
82
84
|
}
|
|
83
85
|
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
package vn.momo.kits.components
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.Canvas
|
|
4
|
+
import androidx.compose.foundation.background
|
|
5
|
+
import androidx.compose.foundation.border
|
|
6
|
+
import androidx.compose.foundation.layout.Box
|
|
7
|
+
import androidx.compose.foundation.layout.WindowInsets
|
|
8
|
+
import androidx.compose.foundation.layout.asPaddingValues
|
|
9
|
+
import androidx.compose.foundation.layout.fillMaxHeight
|
|
10
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
11
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
12
|
+
import androidx.compose.foundation.layout.height
|
|
13
|
+
import androidx.compose.foundation.layout.navigationBars
|
|
14
|
+
import androidx.compose.foundation.layout.padding
|
|
15
|
+
import androidx.compose.foundation.layout.width
|
|
16
|
+
import androidx.compose.runtime.Composable
|
|
17
|
+
import androidx.compose.ui.Alignment
|
|
18
|
+
import androidx.compose.ui.Modifier
|
|
19
|
+
import androidx.compose.ui.geometry.Offset
|
|
20
|
+
import androidx.compose.ui.graphics.Color
|
|
21
|
+
import androidx.compose.ui.graphics.PathEffect
|
|
22
|
+
import androidx.compose.ui.graphics.StrokeCap
|
|
23
|
+
import androidx.compose.ui.unit.dp
|
|
24
|
+
import io.ktor.util.Platform
|
|
25
|
+
import vn.momo.kits.application.IsShowBaseLineDebug
|
|
26
|
+
import vn.momo.kits.const.Colors
|
|
27
|
+
import vn.momo.kits.modifier.conditional
|
|
28
|
+
import vn.momo.kits.platform.getPlatformName
|
|
29
|
+
import vn.momo.kits.platform.getStatusBarHeight
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A debug overlay that draws danger/warning baseline guides on top of the screen.
|
|
33
|
+
*
|
|
34
|
+
* Highlights safe-area boundaries, header regions, and bottom navigation zones
|
|
35
|
+
* using colored solid or dotted lines and semi-transparent red zones.
|
|
36
|
+
*
|
|
37
|
+
* @param enabled When `false` the composable renders nothing. Pass `false` when
|
|
38
|
+
* your QC automation flag is active to suppress the overlay.
|
|
39
|
+
*/
|
|
40
|
+
@Composable
|
|
41
|
+
fun BaselineView(enabled: Boolean = true) {
|
|
42
|
+
if (!enabled) return
|
|
43
|
+
|
|
44
|
+
val bottomInsetHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
|
45
|
+
val topInset = if (getPlatformName() == "Android") getStatusBarHeight() - 14.dp else getStatusBarHeight()
|
|
46
|
+
val bottomInset = if (getPlatformName() == "iOS") minOf(bottomInsetHeight, 21.dp) else bottomInsetHeight
|
|
47
|
+
|
|
48
|
+
Box(modifier = Modifier.fillMaxSize()) {
|
|
49
|
+
// Danger zones
|
|
50
|
+
Box(
|
|
51
|
+
modifier = Modifier
|
|
52
|
+
.fillMaxWidth()
|
|
53
|
+
.height(topInset)
|
|
54
|
+
.background(Color.Red.copy(alpha = 0.15f))
|
|
55
|
+
)
|
|
56
|
+
Box(
|
|
57
|
+
modifier = Modifier
|
|
58
|
+
.width(12.dp)
|
|
59
|
+
.fillMaxHeight()
|
|
60
|
+
.padding(top = topInset, bottom = bottomInset)
|
|
61
|
+
.background(Color.Red.copy(alpha = 0.15f))
|
|
62
|
+
)
|
|
63
|
+
Box(
|
|
64
|
+
modifier = Modifier
|
|
65
|
+
.width(12.dp)
|
|
66
|
+
.fillMaxHeight()
|
|
67
|
+
.padding(top = topInset, bottom = bottomInset)
|
|
68
|
+
.background(Color.Red.copy(alpha = 0.15f))
|
|
69
|
+
.align(Alignment.TopEnd)
|
|
70
|
+
)
|
|
71
|
+
Box(
|
|
72
|
+
modifier = Modifier
|
|
73
|
+
.fillMaxWidth()
|
|
74
|
+
.height(bottomInset)
|
|
75
|
+
.background(Color.Red.copy(alpha = 0.15f))
|
|
76
|
+
.align(Alignment.BottomCenter)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
// Danger lines
|
|
80
|
+
BaselineDottedLine(
|
|
81
|
+
Modifier
|
|
82
|
+
.padding(top = topInset)
|
|
83
|
+
.fillMaxWidth(),
|
|
84
|
+
isDotted = false,
|
|
85
|
+
color = Color(0xFFE400FF)
|
|
86
|
+
)
|
|
87
|
+
BaselineDottedLine(
|
|
88
|
+
Modifier
|
|
89
|
+
.padding(top = topInset + 52.dp)
|
|
90
|
+
.fillMaxWidth(),
|
|
91
|
+
color = Color(0xFFE400FF)
|
|
92
|
+
)
|
|
93
|
+
BaselineDottedLine(
|
|
94
|
+
Modifier
|
|
95
|
+
.padding(top = topInset + 104.dp)
|
|
96
|
+
.fillMaxWidth(),
|
|
97
|
+
color = Color(0xFFE400FF)
|
|
98
|
+
)
|
|
99
|
+
BaselineDottedLine(
|
|
100
|
+
Modifier
|
|
101
|
+
.padding(bottom = bottomInset + 64.dp)
|
|
102
|
+
.fillMaxWidth()
|
|
103
|
+
.align(Alignment.BottomCenter),
|
|
104
|
+
color = Color(0xFFE400FF)
|
|
105
|
+
)
|
|
106
|
+
BaselineDottedLine(
|
|
107
|
+
Modifier
|
|
108
|
+
.padding(bottom = bottomInset)
|
|
109
|
+
.fillMaxWidth()
|
|
110
|
+
.align(Alignment.BottomCenter),
|
|
111
|
+
isDotted = false,
|
|
112
|
+
color = Color(0xFFE400FF)
|
|
113
|
+
)
|
|
114
|
+
BaselineDottedLine(
|
|
115
|
+
Modifier
|
|
116
|
+
.padding(start = 12.dp)
|
|
117
|
+
.fillMaxHeight(),
|
|
118
|
+
orientation = BaselineOrientation.Vertical,
|
|
119
|
+
isDotted = false,
|
|
120
|
+
color = Color(0xFFE400FF)
|
|
121
|
+
)
|
|
122
|
+
BaselineDottedLine(
|
|
123
|
+
Modifier
|
|
124
|
+
.padding(end = 12.dp)
|
|
125
|
+
.fillMaxHeight()
|
|
126
|
+
.align(Alignment.BottomEnd),
|
|
127
|
+
orientation = BaselineOrientation.Vertical,
|
|
128
|
+
isDotted = false,
|
|
129
|
+
color = Color(0xFFE400FF)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
// Warning lines
|
|
133
|
+
BaselineDottedLine(
|
|
134
|
+
Modifier
|
|
135
|
+
.padding(top = topInset + 26.dp)
|
|
136
|
+
.fillMaxWidth(),
|
|
137
|
+
color = Color(0xFFFF7A00)
|
|
138
|
+
)
|
|
139
|
+
BaselineDottedLine(
|
|
140
|
+
Modifier
|
|
141
|
+
.padding(bottom = bottomInset + 56.dp)
|
|
142
|
+
.fillMaxWidth()
|
|
143
|
+
.align(Alignment.BottomCenter),
|
|
144
|
+
color = Color(0xFFFFCC00)
|
|
145
|
+
)
|
|
146
|
+
BaselineDottedLine(
|
|
147
|
+
Modifier
|
|
148
|
+
.padding(bottom = bottomInset + 8.dp)
|
|
149
|
+
.fillMaxWidth()
|
|
150
|
+
.align(Alignment.BottomCenter),
|
|
151
|
+
color = Color(0xFFFFCC00)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
// Header background warning lines
|
|
155
|
+
BaselineDottedLine(
|
|
156
|
+
Modifier
|
|
157
|
+
.padding(start = 40.dp, top = topInset)
|
|
158
|
+
.height(52.dp),
|
|
159
|
+
color = Color(0xFF00C520),
|
|
160
|
+
orientation = BaselineOrientation.Vertical
|
|
161
|
+
)
|
|
162
|
+
BaselineDottedLine(
|
|
163
|
+
Modifier
|
|
164
|
+
.padding(start = 48.dp, top = topInset)
|
|
165
|
+
.height(52.dp),
|
|
166
|
+
color = Color(0xFF00C520),
|
|
167
|
+
orientation = BaselineOrientation.Vertical
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
enum class BaselineOrientation { Horizontal, Vertical }
|
|
173
|
+
|
|
174
|
+
@Composable
|
|
175
|
+
fun BaselineDottedLine(
|
|
176
|
+
modifier: Modifier = Modifier,
|
|
177
|
+
color: Color = Color.Red,
|
|
178
|
+
orientation: BaselineOrientation = BaselineOrientation.Horizontal,
|
|
179
|
+
isDotted: Boolean = true
|
|
180
|
+
) {
|
|
181
|
+
Canvas(modifier = modifier) {
|
|
182
|
+
val pathEffect = if (isDotted) PathEffect.dashPathEffect(floatArrayOf(8f, 8f)) else null
|
|
183
|
+
drawLine(
|
|
184
|
+
color = color,
|
|
185
|
+
start = if (orientation == BaselineOrientation.Horizontal) Offset(
|
|
186
|
+
0f,
|
|
187
|
+
size.height / 2
|
|
188
|
+
) else Offset(size.width / 2, 0f),
|
|
189
|
+
end = if (orientation == BaselineOrientation.Horizontal) Offset(
|
|
190
|
+
size.width,
|
|
191
|
+
size.height / 2
|
|
192
|
+
) else Offset(size.width / 2, size.height),
|
|
193
|
+
strokeWidth = 1.dp.toPx(),
|
|
194
|
+
pathEffect = pathEffect,
|
|
195
|
+
cap = StrokeCap.Round,
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -49,9 +49,20 @@ import vn.momo.kits.utils.formatNumberToMoney
|
|
|
49
49
|
|
|
50
50
|
class CustomConverter : VisualTransformation {
|
|
51
51
|
override fun filter(text: AnnotatedString): TransformedText {
|
|
52
|
-
if (text.text.isEmpty()
|
|
52
|
+
if (text.text.isEmpty()) {
|
|
53
|
+
return TransformedText(
|
|
54
|
+
AnnotatedString("0"),
|
|
55
|
+
object : OffsetMapping {
|
|
56
|
+
override fun originalToTransformed(offset: Int): Int = 0
|
|
57
|
+
override fun transformedToOriginal(offset: Int): Int = 0
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (text.text == "0") {
|
|
53
63
|
return TransformedText(AnnotatedString("0"), OffsetMapping.Identity)
|
|
54
64
|
}
|
|
65
|
+
|
|
55
66
|
val formattedText = formatNumberToMoney(text.text.toLong())
|
|
56
67
|
|
|
57
68
|
return TransformedText(
|
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
package vn.momo.kits.components
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.Canvas
|
|
4
|
+
import androidx.compose.foundation.background
|
|
5
|
+
import androidx.compose.foundation.border
|
|
6
|
+
import androidx.compose.foundation.clickable
|
|
7
|
+
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
8
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
9
|
+
import androidx.compose.foundation.layout.Box
|
|
10
|
+
import androidx.compose.foundation.layout.Column
|
|
11
|
+
import androidx.compose.foundation.layout.Row
|
|
12
|
+
import androidx.compose.foundation.layout.Spacer
|
|
13
|
+
import androidx.compose.foundation.layout.offset
|
|
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.layout.widthIn
|
|
18
|
+
import androidx.compose.foundation.layout.wrapContentSize
|
|
19
|
+
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
20
|
+
import androidx.compose.runtime.Composable
|
|
21
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
22
|
+
import androidx.compose.runtime.Stable
|
|
23
|
+
import androidx.compose.runtime.getValue
|
|
24
|
+
import androidx.compose.runtime.mutableStateOf
|
|
25
|
+
import androidx.compose.runtime.remember
|
|
26
|
+
import androidx.compose.runtime.setValue
|
|
27
|
+
import androidx.compose.ui.Alignment
|
|
28
|
+
import androidx.compose.ui.Modifier
|
|
29
|
+
import androidx.compose.ui.draw.clip
|
|
30
|
+
import androidx.compose.ui.graphics.Paint
|
|
31
|
+
import androidx.compose.ui.graphics.Path
|
|
32
|
+
import androidx.compose.ui.graphics.PathEffect
|
|
33
|
+
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
|
34
|
+
import androidx.compose.ui.platform.LocalDensity
|
|
35
|
+
import androidx.compose.ui.text.style.TextOverflow
|
|
36
|
+
import androidx.compose.ui.unit.IntOffset
|
|
37
|
+
import androidx.compose.ui.unit.IntRect
|
|
38
|
+
import androidx.compose.ui.unit.IntSize
|
|
39
|
+
import androidx.compose.ui.unit.LayoutDirection
|
|
40
|
+
import androidx.compose.ui.unit.dp
|
|
41
|
+
import androidx.compose.ui.window.Popup
|
|
42
|
+
import androidx.compose.ui.window.PopupPositionProvider
|
|
43
|
+
import androidx.compose.ui.window.PopupProperties
|
|
44
|
+
import vn.momo.kits.application.IsShowBaseLineDebug
|
|
45
|
+
import vn.momo.kits.const.Colors
|
|
46
|
+
import vn.momo.kits.const.Radius
|
|
47
|
+
import vn.momo.kits.const.Spacing
|
|
48
|
+
import vn.momo.kits.const.Typography
|
|
49
|
+
import vn.momo.kits.const.scaleSize
|
|
50
|
+
import vn.momo.kits.modifier.activeOpacityClickable
|
|
51
|
+
import vn.momo.kits.modifier.conditional
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
// region Types
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Tooltip placement relative to the anchor element.
|
|
58
|
+
*/
|
|
59
|
+
enum class TooltipPlacement {
|
|
60
|
+
TOP,
|
|
61
|
+
BOTTOM,
|
|
62
|
+
LEFT,
|
|
63
|
+
RIGHT,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Cross-axis alignment of the tooltip relative to the anchor.
|
|
68
|
+
*/
|
|
69
|
+
enum class TooltipAlign {
|
|
70
|
+
START,
|
|
71
|
+
CENTER,
|
|
72
|
+
END,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Describes a single action button displayed in the tooltip.
|
|
77
|
+
*
|
|
78
|
+
* @param title Text label for the button (used for text buttons).
|
|
79
|
+
* @param icon Icon source for the button (used for icon buttons).
|
|
80
|
+
* @param onPress Callback invoked when the button is pressed.
|
|
81
|
+
*/
|
|
82
|
+
data class TooltipButton(
|
|
83
|
+
val title: String? = null,
|
|
84
|
+
val icon: String? = null,
|
|
85
|
+
val onPress: (() -> Unit)? = null,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* State holder for controlling tooltip visibility imperatively.
|
|
91
|
+
* Equivalent to the React Native `useImperativeHandle` ref pattern.
|
|
92
|
+
*/
|
|
93
|
+
@Stable
|
|
94
|
+
class TooltipState {
|
|
95
|
+
var isVisible by mutableStateOf(false)
|
|
96
|
+
private set
|
|
97
|
+
|
|
98
|
+
/** Shows the tooltip. */
|
|
99
|
+
fun show() {
|
|
100
|
+
isVisible = true
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Hides the tooltip. */
|
|
104
|
+
fun hide() {
|
|
105
|
+
isVisible = false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Toggles the tooltip visibility. */
|
|
109
|
+
fun toggle() {
|
|
110
|
+
isVisible = !isVisible
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Creates and remembers a [TooltipState].
|
|
116
|
+
*/
|
|
117
|
+
@Composable
|
|
118
|
+
fun rememberTooltipState(): TooltipState {
|
|
119
|
+
return remember { TooltipState() }
|
|
120
|
+
}
|
|
121
|
+
private val TOOLTIP_OFFSET = Spacing.S
|
|
122
|
+
private val ARROW_SIZE = 6.dp
|
|
123
|
+
/**
|
|
124
|
+
* Custom [PopupPositionProvider] that positions the tooltip relative to the anchor.
|
|
125
|
+
*
|
|
126
|
+
* Mirrors the React Native `placementStyle` calculation:
|
|
127
|
+
* - TOP: tooltip bottom edge sits at `anchorTop - TOOLTIP_OFFSET`
|
|
128
|
+
* - BOTTOM: tooltip top edge sits at `anchorBottom + TOOLTIP_OFFSET`
|
|
129
|
+
* - LEFT: tooltip right edge sits at `anchorLeft - TOOLTIP_OFFSET`
|
|
130
|
+
* - RIGHT: tooltip left edge sits at `anchorRight + TOOLTIP_OFFSET`
|
|
131
|
+
*
|
|
132
|
+
* Cross-axis alignment:
|
|
133
|
+
* - start: tooltip aligns to anchor start edge
|
|
134
|
+
* - center: tooltip centers on anchor
|
|
135
|
+
* - end: tooltip aligns to anchor end edge
|
|
136
|
+
*/
|
|
137
|
+
private class TooltipPositionProvider(
|
|
138
|
+
private val placement: TooltipPlacement,
|
|
139
|
+
private val align: TooltipAlign,
|
|
140
|
+
private val offsetPx: Int,
|
|
141
|
+
) : PopupPositionProvider {
|
|
142
|
+
|
|
143
|
+
override fun calculatePosition(
|
|
144
|
+
anchorBounds: IntRect,
|
|
145
|
+
windowSize: IntSize,
|
|
146
|
+
layoutDirection: LayoutDirection,
|
|
147
|
+
popupContentSize: IntSize,
|
|
148
|
+
): IntOffset {
|
|
149
|
+
val anchorX = anchorBounds.left
|
|
150
|
+
val anchorY = anchorBounds.top
|
|
151
|
+
val anchorW = anchorBounds.width
|
|
152
|
+
val anchorH = anchorBounds.height
|
|
153
|
+
val popW = popupContentSize.width
|
|
154
|
+
val popH = popupContentSize.height
|
|
155
|
+
|
|
156
|
+
var x: Int
|
|
157
|
+
var y: Int
|
|
158
|
+
|
|
159
|
+
when (placement) {
|
|
160
|
+
TooltipPlacement.TOP -> {
|
|
161
|
+
y = anchorY - popH - offsetPx
|
|
162
|
+
x = alignHorizontal(anchorX, anchorW, popW)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
TooltipPlacement.BOTTOM -> {
|
|
166
|
+
y = anchorY + anchorH + offsetPx
|
|
167
|
+
x = alignHorizontal(anchorX, anchorW, popW)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
TooltipPlacement.LEFT -> {
|
|
171
|
+
x = anchorX - popW - offsetPx
|
|
172
|
+
y = alignVertical(anchorY, anchorH, popH)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
TooltipPlacement.RIGHT -> {
|
|
176
|
+
x = anchorX + anchorW + offsetPx
|
|
177
|
+
y = alignVertical(anchorY, anchorH, popH)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return IntOffset(x, y)
|
|
182
|
+
}
|
|
183
|
+
private fun alignHorizontal(anchorX: Int, anchorW: Int, popW: Int): Int {
|
|
184
|
+
return when (align) {
|
|
185
|
+
TooltipAlign.START -> anchorX
|
|
186
|
+
TooltipAlign.END -> anchorX + anchorW - popW
|
|
187
|
+
TooltipAlign.CENTER -> anchorX + (anchorW - popW) / 2
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
private fun alignVertical(anchorY: Int, anchorH: Int, popH: Int): Int {
|
|
191
|
+
return when (align) {
|
|
192
|
+
TooltipAlign.START -> anchorY
|
|
193
|
+
TooltipAlign.END -> anchorY + anchorH - popH
|
|
194
|
+
TooltipAlign.CENTER -> anchorY + (anchorH - popH) / 2
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// endregion
|
|
200
|
+
|
|
201
|
+
// region Tooltip Composable
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* A tooltip component that wraps [content] (the anchor) and shows a positioned
|
|
205
|
+
* tooltip popup with title, description, close button, and action buttons.
|
|
206
|
+
*
|
|
207
|
+
* Port of the React Native `Tooltip` component.
|
|
208
|
+
*
|
|
209
|
+
* @param state Controls visibility (use [rememberTooltipState]).
|
|
210
|
+
* @param title Title text displayed in the tooltip header.
|
|
211
|
+
* @param description Description text shown under the title.
|
|
212
|
+
* @param buttons Action buttons rendered at the bottom.
|
|
213
|
+
* @param placement Tooltip position relative to the anchor (default TOP).
|
|
214
|
+
* @param align Cross-axis alignment (default CENTER).
|
|
215
|
+
* @param onVisibleChange Callback when visibility changes.
|
|
216
|
+
* @param onPressClose Callback when the close button (X) is pressed.
|
|
217
|
+
* @param modifier Modifier for the anchor wrapper.
|
|
218
|
+
* @param content The anchor element that the tooltip is attached to.
|
|
219
|
+
*/
|
|
220
|
+
@Composable
|
|
221
|
+
fun Tooltip(
|
|
222
|
+
state: TooltipState,
|
|
223
|
+
title: String? = null,
|
|
224
|
+
description: String? = null,
|
|
225
|
+
buttons: List<TooltipButton> = emptyList(),
|
|
226
|
+
placement: TooltipPlacement = TooltipPlacement.TOP,
|
|
227
|
+
align: TooltipAlign = TooltipAlign.CENTER,
|
|
228
|
+
onVisibleChange: ((Boolean) -> Unit)? = null,
|
|
229
|
+
onPressClose: (() -> Unit)? = null,
|
|
230
|
+
modifier: Modifier = Modifier,
|
|
231
|
+
content: @Composable () -> Unit,
|
|
232
|
+
) {
|
|
233
|
+
val density = LocalDensity.current
|
|
234
|
+
val offsetPx = with(density) { (TOOLTIP_OFFSET + ARROW_SIZE).roundToPx() }
|
|
235
|
+
|
|
236
|
+
LaunchedEffect(state.isVisible) {
|
|
237
|
+
onVisibleChange?.invoke(state.isVisible)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
Box(modifier = modifier) {
|
|
241
|
+
content()
|
|
242
|
+
|
|
243
|
+
if (state.isVisible) {
|
|
244
|
+
val positionProvider = remember(placement, align, offsetPx) {
|
|
245
|
+
TooltipPositionProvider(
|
|
246
|
+
placement = placement,
|
|
247
|
+
align = align,
|
|
248
|
+
offsetPx = offsetPx,
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
Popup(
|
|
253
|
+
popupPositionProvider = positionProvider,
|
|
254
|
+
onDismissRequest = { state.hide() },
|
|
255
|
+
properties = PopupProperties(clippingEnabled = false),
|
|
256
|
+
) {
|
|
257
|
+
TooltipPopupContent(
|
|
258
|
+
title = title,
|
|
259
|
+
description = description,
|
|
260
|
+
buttons = buttons,
|
|
261
|
+
placement = placement,
|
|
262
|
+
align = align,
|
|
263
|
+
onPressClose = onPressClose ?: { state.hide() },
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
@Composable
|
|
270
|
+
private fun TooltipPopupContent(
|
|
271
|
+
title: String?,
|
|
272
|
+
description: String?,
|
|
273
|
+
buttons: List<TooltipButton>,
|
|
274
|
+
placement: TooltipPlacement,
|
|
275
|
+
align: TooltipAlign,
|
|
276
|
+
onPressClose: () -> Unit,
|
|
277
|
+
) {
|
|
278
|
+
val tooltipMaxWidth = 300.dp
|
|
279
|
+
val tooltipShape = remember { RoundedCornerShape(Radius.S) }
|
|
280
|
+
|
|
281
|
+
Box(modifier = Modifier.wrapContentSize()) {
|
|
282
|
+
Column(
|
|
283
|
+
modifier = Modifier
|
|
284
|
+
.widthIn(max = tooltipMaxWidth)
|
|
285
|
+
.background(Colors.black_17, tooltipShape)
|
|
286
|
+
.clip(tooltipShape)
|
|
287
|
+
.conditional(IsShowBaseLineDebug) {
|
|
288
|
+
border(1.dp, Colors.blue_03)
|
|
289
|
+
}
|
|
290
|
+
.padding(Spacing.M)
|
|
291
|
+
) {
|
|
292
|
+
Row {
|
|
293
|
+
Column(modifier = Modifier.weight(1f, fill = false)) {
|
|
294
|
+
if (!title.isNullOrEmpty()) {
|
|
295
|
+
Text(
|
|
296
|
+
text = title,
|
|
297
|
+
style = Typography.headerSSemibold,
|
|
298
|
+
color = Colors.black_01,
|
|
299
|
+
maxLines = 1,
|
|
300
|
+
modifier = Modifier.padding(bottom = Spacing.XS),
|
|
301
|
+
overflow = TextOverflow.Ellipsis,
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
if (!description.isNullOrEmpty()) {
|
|
305
|
+
Text(
|
|
306
|
+
text = description,
|
|
307
|
+
style = Typography.descriptionDefaultRegular,
|
|
308
|
+
color = Colors.black_01,
|
|
309
|
+
maxLines = 2,
|
|
310
|
+
modifier = Modifier.padding(bottom = Spacing.M),
|
|
311
|
+
overflow = TextOverflow.Ellipsis,
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
Spacer(Modifier.width(Spacing.M))
|
|
316
|
+
Box(
|
|
317
|
+
modifier = Modifier
|
|
318
|
+
.size(20.dp)
|
|
319
|
+
.activeOpacityClickable {
|
|
320
|
+
onPressClose.invoke()
|
|
321
|
+
}
|
|
322
|
+
) {
|
|
323
|
+
Icon(
|
|
324
|
+
source = "navigation_close",
|
|
325
|
+
size = 20.dp,
|
|
326
|
+
color = Colors.black_01,
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (buttons.isNotEmpty()) {
|
|
331
|
+
TooltipButtons(
|
|
332
|
+
buttons = buttons,
|
|
333
|
+
modifier = Modifier.align(Alignment.End),
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
TooltipArrow(
|
|
339
|
+
placement = placement,
|
|
340
|
+
align = align,
|
|
341
|
+
modifier = Modifier.matchParentSize(),
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
@Composable
|
|
346
|
+
private fun TooltipArrow(
|
|
347
|
+
placement: TooltipPlacement,
|
|
348
|
+
align: TooltipAlign,
|
|
349
|
+
modifier: Modifier = Modifier,
|
|
350
|
+
) {
|
|
351
|
+
val arrowWidth = ARROW_SIZE * 2
|
|
352
|
+
val arrowHeight = ARROW_SIZE
|
|
353
|
+
val arrowEdgeMargin = Spacing.M
|
|
354
|
+
|
|
355
|
+
val boxAlignment = when (placement) {
|
|
356
|
+
TooltipPlacement.TOP -> when (align) {
|
|
357
|
+
TooltipAlign.START -> Alignment.BottomStart
|
|
358
|
+
TooltipAlign.CENTER -> Alignment.BottomCenter
|
|
359
|
+
TooltipAlign.END -> Alignment.BottomEnd
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
TooltipPlacement.BOTTOM -> when (align) {
|
|
363
|
+
TooltipAlign.START -> Alignment.TopStart
|
|
364
|
+
TooltipAlign.CENTER -> Alignment.TopCenter
|
|
365
|
+
TooltipAlign.END -> Alignment.TopEnd
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
TooltipPlacement.LEFT -> when (align) {
|
|
369
|
+
TooltipAlign.START -> Alignment.TopEnd
|
|
370
|
+
TooltipAlign.CENTER -> Alignment.CenterEnd
|
|
371
|
+
TooltipAlign.END -> Alignment.BottomEnd
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
TooltipPlacement.RIGHT -> when (align) {
|
|
375
|
+
TooltipAlign.START -> Alignment.TopStart
|
|
376
|
+
TooltipAlign.CENTER -> Alignment.CenterStart
|
|
377
|
+
TooltipAlign.END -> Alignment.BottomStart
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
val canvasWidth = when (placement) {
|
|
382
|
+
TooltipPlacement.TOP, TooltipPlacement.BOTTOM -> arrowWidth
|
|
383
|
+
TooltipPlacement.LEFT, TooltipPlacement.RIGHT -> arrowHeight
|
|
384
|
+
}
|
|
385
|
+
val canvasHeight = when (placement) {
|
|
386
|
+
TooltipPlacement.TOP, TooltipPlacement.BOTTOM -> arrowHeight
|
|
387
|
+
TooltipPlacement.LEFT, TooltipPlacement.RIGHT -> arrowWidth
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
val arrowOverlap = 1.dp
|
|
391
|
+
val offsetX = when (placement) {
|
|
392
|
+
TooltipPlacement.LEFT -> arrowHeight - arrowOverlap
|
|
393
|
+
TooltipPlacement.RIGHT -> -arrowHeight + arrowOverlap
|
|
394
|
+
else -> when (align) {
|
|
395
|
+
TooltipAlign.START -> arrowEdgeMargin
|
|
396
|
+
TooltipAlign.END -> -arrowEdgeMargin
|
|
397
|
+
TooltipAlign.CENTER -> 0.dp
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
val offsetY = when (placement) {
|
|
401
|
+
TooltipPlacement.TOP -> arrowHeight - arrowOverlap
|
|
402
|
+
TooltipPlacement.BOTTOM -> -arrowHeight + arrowOverlap
|
|
403
|
+
else -> when (align) {
|
|
404
|
+
TooltipAlign.START -> arrowEdgeMargin
|
|
405
|
+
TooltipAlign.END -> -arrowEdgeMargin
|
|
406
|
+
TooltipAlign.CENTER -> 0.dp
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
val arrowColor = Colors.black_17
|
|
411
|
+
|
|
412
|
+
Box(modifier = modifier) {
|
|
413
|
+
Canvas(
|
|
414
|
+
modifier = Modifier
|
|
415
|
+
.align(boxAlignment)
|
|
416
|
+
.offset(x = offsetX, y = offsetY)
|
|
417
|
+
.size(width = canvasWidth, height = canvasHeight)
|
|
418
|
+
) {
|
|
419
|
+
val w = size.width
|
|
420
|
+
val h = size.height
|
|
421
|
+
val path = Path().apply {
|
|
422
|
+
when (placement) {
|
|
423
|
+
TooltipPlacement.TOP -> {
|
|
424
|
+
moveTo(0f, 0f)
|
|
425
|
+
lineTo(w, 0f)
|
|
426
|
+
lineTo(w / 2f, h)
|
|
427
|
+
close()
|
|
428
|
+
}
|
|
429
|
+
TooltipPlacement.BOTTOM -> {
|
|
430
|
+
moveTo(0f, h)
|
|
431
|
+
lineTo(w, h)
|
|
432
|
+
lineTo(w / 2f, 0f)
|
|
433
|
+
close()
|
|
434
|
+
}
|
|
435
|
+
TooltipPlacement.LEFT -> {
|
|
436
|
+
moveTo(0f, 0f)
|
|
437
|
+
lineTo(0f, h)
|
|
438
|
+
lineTo(w, h / 2f)
|
|
439
|
+
close()
|
|
440
|
+
}
|
|
441
|
+
TooltipPlacement.RIGHT -> {
|
|
442
|
+
moveTo(w, 0f)
|
|
443
|
+
lineTo(w, h)
|
|
444
|
+
lineTo(0f, h / 2f)
|
|
445
|
+
close()
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
drawIntoCanvas { canvas ->
|
|
450
|
+
val paint = Paint().apply {
|
|
451
|
+
color = arrowColor
|
|
452
|
+
pathEffect = PathEffect.cornerPathEffect(2.dp.toPx())
|
|
453
|
+
}
|
|
454
|
+
canvas.drawPath(path, paint)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
@Composable
|
|
460
|
+
private fun TooltipButtons(buttons: List<TooltipButton>, modifier: Modifier = Modifier) {
|
|
461
|
+
Row(
|
|
462
|
+
modifier = modifier,
|
|
463
|
+
horizontalArrangement = Arrangement.End,
|
|
464
|
+
verticalAlignment = Alignment.CenterVertically,
|
|
465
|
+
) {
|
|
466
|
+
|
|
467
|
+
if (buttons.size == 1) {
|
|
468
|
+
val btn = buttons[0]
|
|
469
|
+
TooltipSingleButton(btn)
|
|
470
|
+
} else if (buttons.size == 2) {
|
|
471
|
+
val primary = buttons[0]
|
|
472
|
+
val secondary = buttons[1]
|
|
473
|
+
val bothIcons = primary.icon != null && secondary.icon != null
|
|
474
|
+
|
|
475
|
+
if (bothIcons) {
|
|
476
|
+
TooltipIconButton(
|
|
477
|
+
icon = secondary.icon,
|
|
478
|
+
onPress = secondary.onPress ?: {},
|
|
479
|
+
)
|
|
480
|
+
Spacer(modifier = Modifier.width(Spacing.S))
|
|
481
|
+
TooltipIconButton(
|
|
482
|
+
icon = primary.icon,
|
|
483
|
+
onPress = primary.onPress ?: {},
|
|
484
|
+
)
|
|
485
|
+
} else {
|
|
486
|
+
TooltipSecondaryButton(
|
|
487
|
+
title = secondary.title ?: "",
|
|
488
|
+
onPress = secondary.onPress ?: {},
|
|
489
|
+
)
|
|
490
|
+
Spacer(modifier = Modifier.width(Spacing.S))
|
|
491
|
+
TooltipPrimaryButton(
|
|
492
|
+
title = primary.title ?: "",
|
|
493
|
+
onPress = primary.onPress ?: {},
|
|
494
|
+
)
|
|
495
|
+
}
|
|
496
|
+
} else {
|
|
497
|
+
buttons.forEachIndexed { index, btn ->
|
|
498
|
+
if (index > 0) {
|
|
499
|
+
Spacer(modifier = Modifier.width(Spacing.XXS))
|
|
500
|
+
}
|
|
501
|
+
TooltipSingleButton(btn)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
@Composable
|
|
508
|
+
private fun TooltipSingleButton(btn: TooltipButton) {
|
|
509
|
+
if (btn.icon != null) {
|
|
510
|
+
TooltipIconButton(
|
|
511
|
+
icon = btn.icon,
|
|
512
|
+
onPress = btn.onPress ?: {},
|
|
513
|
+
)
|
|
514
|
+
} else {
|
|
515
|
+
TooltipPrimaryButton(
|
|
516
|
+
title = btn.title ?: "",
|
|
517
|
+
onPress = btn.onPress ?: {},
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
@Composable
|
|
522
|
+
private fun TooltipPrimaryButton(
|
|
523
|
+
title: String,
|
|
524
|
+
onPress: () -> Unit,
|
|
525
|
+
) {
|
|
526
|
+
Button(
|
|
527
|
+
onClick = onPress,
|
|
528
|
+
title = title,
|
|
529
|
+
type = ButtonType.SECONDARY,
|
|
530
|
+
size = Size.MEDIUM,
|
|
531
|
+
isFull = false,
|
|
532
|
+
)
|
|
533
|
+
}
|
|
534
|
+
@Composable
|
|
535
|
+
private fun TooltipSecondaryButton(
|
|
536
|
+
title: String,
|
|
537
|
+
onPress: () -> Unit,
|
|
538
|
+
) {
|
|
539
|
+
Box(
|
|
540
|
+
modifier = Modifier
|
|
541
|
+
.clickable(
|
|
542
|
+
interactionSource = remember { MutableInteractionSource() },
|
|
543
|
+
indication = null,
|
|
544
|
+
onClick = onPress,
|
|
545
|
+
)
|
|
546
|
+
.padding(horizontal = Spacing.M, vertical = Spacing.S),
|
|
547
|
+
) {
|
|
548
|
+
Text(
|
|
549
|
+
text = title,
|
|
550
|
+
color = Colors.black_01,
|
|
551
|
+
style = Typography.actionSBold,
|
|
552
|
+
)
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
@Composable
|
|
556
|
+
private fun TooltipIconButton(
|
|
557
|
+
icon: String,
|
|
558
|
+
onPress: () -> Unit,
|
|
559
|
+
) {
|
|
560
|
+
val iconButtonSize = scaleSize(36f).dp
|
|
561
|
+
|
|
562
|
+
Box(
|
|
563
|
+
modifier = Modifier
|
|
564
|
+
.size(iconButtonSize)
|
|
565
|
+
.background(Colors.black_01, RoundedCornerShape(Radius.XL))
|
|
566
|
+
.clip(RoundedCornerShape(Radius.XL))
|
|
567
|
+
.clickable(
|
|
568
|
+
interactionSource = remember { MutableInteractionSource() },
|
|
569
|
+
indication = null,
|
|
570
|
+
onClick = onPress,
|
|
571
|
+
),
|
|
572
|
+
contentAlignment = Alignment.Center,
|
|
573
|
+
) {
|
|
574
|
+
Icon(source = icon)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
@@ -40,13 +40,15 @@ import vn.momo.kits.const.Colors
|
|
|
40
40
|
import vn.momo.kits.const.Radius
|
|
41
41
|
import vn.momo.kits.const.Spacing
|
|
42
42
|
import vn.momo.kits.application.IsShowBaseLineDebug
|
|
43
|
+
import vn.momo.kits.components.BadgeDot
|
|
44
|
+
import vn.momo.kits.components.DotSize
|
|
43
45
|
import vn.momo.kits.const.Typography
|
|
44
46
|
import vn.momo.kits.modifier.conditional
|
|
45
47
|
import vn.momo.kits.modifier.noFeedbackClickable
|
|
46
48
|
import vn.momo.kits.platform.getScreenDimensions
|
|
47
49
|
|
|
48
50
|
val floatingButtonWidth = 75.dp
|
|
49
|
-
const val BOTTOM_TAB_BAR_HEIGHT =
|
|
51
|
+
const val BOTTOM_TAB_BAR_HEIGHT = 64
|
|
50
52
|
|
|
51
53
|
@Composable
|
|
52
54
|
fun BottomTabBar(
|
|
@@ -142,6 +144,18 @@ fun RowScope.renderTabBarItem(
|
|
|
142
144
|
|
|
143
145
|
@Composable
|
|
144
146
|
fun TabBarItem(item: BottomTabItem, selected: Boolean, onClick: () -> Unit) {
|
|
147
|
+
fun isNumber(label: String): Boolean {
|
|
148
|
+
val numberRegex = "^\\d+$".toRegex()
|
|
149
|
+
return numberRegex.matches(label)
|
|
150
|
+
}
|
|
151
|
+
fun formatLabel(label: String): String? {
|
|
152
|
+
if (isNumber(label) && label.toInt() == 0) {
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
return label
|
|
156
|
+
}
|
|
157
|
+
val badgeLabel = item.badgeLabel?.let { formatLabel(it) }
|
|
158
|
+
|
|
145
159
|
Box(modifier = Modifier
|
|
146
160
|
.fillMaxSize()
|
|
147
161
|
.padding(horizontal = Spacing.XXS)
|
|
@@ -156,31 +170,34 @@ fun TabBarItem(item: BottomTabItem, selected: Boolean, onClick: () -> Unit) {
|
|
|
156
170
|
Column(
|
|
157
171
|
modifier = Modifier
|
|
158
172
|
.fillMaxSize()
|
|
159
|
-
.padding(horizontal = Spacing.
|
|
173
|
+
.padding(horizontal = Spacing.S, vertical = Spacing.S)
|
|
160
174
|
.noFeedbackClickable {
|
|
161
175
|
onClick()
|
|
162
176
|
},
|
|
163
177
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
164
|
-
verticalArrangement = Arrangement.
|
|
178
|
+
verticalArrangement = Arrangement.Center
|
|
165
179
|
) {
|
|
166
180
|
Icon(
|
|
167
181
|
source = item.icon,
|
|
168
|
-
|
|
182
|
+
size = 28.dp,
|
|
183
|
+
modifier = Modifier.padding(Spacing.XS),
|
|
169
184
|
color = if (selected) AppTheme.current.colors.primary else AppTheme.current.colors.text.hint)
|
|
170
185
|
Text(
|
|
171
186
|
text = item.label,
|
|
172
187
|
color = if (selected) AppTheme.current.colors.primary else AppTheme.current.colors.text.hint,
|
|
173
|
-
style = Typography.labelXsMedium,
|
|
188
|
+
style = if (selected) Typography.labelXsMedium else Typography.descriptionXsRegular,
|
|
174
189
|
maxLines = 1,
|
|
175
190
|
overflow = TextOverflow.Ellipsis
|
|
176
191
|
)
|
|
177
192
|
}
|
|
178
|
-
if(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
193
|
+
if (badgeLabel != null) {
|
|
194
|
+
if (badgeLabel.isEmpty())
|
|
195
|
+
BadgeDot(
|
|
196
|
+
size = DotSize.Small,
|
|
197
|
+
modifier = Modifier.offset(x = 50.dp, y = (-48).dp)
|
|
198
|
+
)
|
|
199
|
+
else
|
|
200
|
+
Badge(badgeLabel, modifier = Modifier.offset(x = 44.dp, y = (-42).dp))
|
|
184
201
|
}
|
|
185
202
|
}
|
|
186
203
|
}
|
|
@@ -190,7 +207,7 @@ fun FloatingButton(data: BottomTabFloatingButton) {
|
|
|
190
207
|
Column(
|
|
191
208
|
modifier = Modifier
|
|
192
209
|
.width(floatingButtonWidth)
|
|
193
|
-
.padding(horizontal = Spacing.XXS)
|
|
210
|
+
.padding(horizontal = Spacing.XXS, vertical = Spacing.S)
|
|
194
211
|
.conditional(IsShowBaseLineDebug) {
|
|
195
212
|
border(1.dp, Colors.blue_03)
|
|
196
213
|
}
|
package/gradle.properties
CHANGED
|
@@ -154,7 +154,7 @@ public struct FloatingButton: View {
|
|
|
154
154
|
Spacer().frame(width: 8)
|
|
155
155
|
Text(label)
|
|
156
156
|
.foregroundColor(.white)
|
|
157
|
-
.font(.system(size: 16, weight: .bold))
|
|
157
|
+
.font(.system(size: scaleSize(16), weight: .bold))
|
|
158
158
|
.lineLimit(1)
|
|
159
159
|
.background(
|
|
160
160
|
GeometryReader { geo in
|
|
@@ -137,7 +137,7 @@ public struct BadgeRibbon: View {
|
|
|
137
137
|
private var roundContent: some View {
|
|
138
138
|
HStack(spacing: 0) {
|
|
139
139
|
Text(label)
|
|
140
|
-
.font(.system(size: 12, weight: .medium))
|
|
140
|
+
.font(.system(size: scaleSize(12), weight: .medium))
|
|
141
141
|
.foregroundColor(Colors.black01)
|
|
142
142
|
.lineLimit(1)
|
|
143
143
|
.rotationEffect(rotation)
|
|
@@ -158,7 +158,7 @@ public struct BadgeRibbon: View {
|
|
|
158
158
|
private var skewContent: some View {
|
|
159
159
|
HStack(spacing: 0) {
|
|
160
160
|
Text(label)
|
|
161
|
-
.font(.system(size: 12, weight: .medium))
|
|
161
|
+
.font(.system(size: scaleSize(12), weight: .medium))
|
|
162
162
|
.foregroundColor(Colors.black01)
|
|
163
163
|
.lineLimit(1)
|
|
164
164
|
.rotationEffect(rotation)
|
package/ios/Image/Image.swift
CHANGED
|
@@ -31,7 +31,11 @@ public struct ImageView: View {
|
|
|
31
31
|
.placeholder {
|
|
32
32
|
VStack(alignment: .center, content: {
|
|
33
33
|
if error {
|
|
34
|
-
Image(
|
|
34
|
+
Image("media_fail")
|
|
35
|
+
.resizable()
|
|
36
|
+
.renderingMode(.template)
|
|
37
|
+
.foregroundColor(Colors.black08)
|
|
38
|
+
.frame(width: 24, height: 24)
|
|
35
39
|
} else if placeholder != nil {
|
|
36
40
|
placeholder
|
|
37
41
|
}
|
|
@@ -113,11 +113,7 @@ public struct PopupDisplay: View {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
VStack(spacing: 0) {
|
|
116
|
-
if(url.isEmpty) {
|
|
117
|
-
Icon(source: "media_fail")
|
|
118
|
-
.frame(width: .infinity, height: 184)
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
116
|
+
if(!url.isEmpty) {
|
|
121
117
|
WebImage(url: URL(string: url), isAnimating: .constant(true))
|
|
122
118
|
.resizable()
|
|
123
119
|
.placeholder {
|
|
@@ -132,9 +128,7 @@ public struct PopupDisplay: View {
|
|
|
132
128
|
.clipped()
|
|
133
129
|
}
|
|
134
130
|
VStack(alignment: .leading, spacing: 0) {
|
|
135
|
-
|
|
136
|
-
.foregroundColor(.black)
|
|
137
|
-
.font(.header_default_semibold)
|
|
131
|
+
MomoText(title, typography: .headerDefaultBold)
|
|
138
132
|
.padding(.top, 24)
|
|
139
133
|
.padding(.bottom, 8)
|
|
140
134
|
.lineLimit(2)
|
|
@@ -143,9 +137,7 @@ public struct PopupDisplay: View {
|
|
|
143
137
|
Group {
|
|
144
138
|
if isScrollable {
|
|
145
139
|
ScrollView(showsIndicators: false) {
|
|
146
|
-
|
|
147
|
-
.font(.body_default_regular)
|
|
148
|
-
.foregroundColor(Colors.black17)
|
|
140
|
+
MomoText(description, typography: .bodyDefaultRegular)
|
|
149
141
|
.multilineTextAlignment(.leading)
|
|
150
142
|
.background(GeometryReader { geo in
|
|
151
143
|
Color.clear.onAppear { textHeight = geo.size.height }
|
|
@@ -153,13 +145,12 @@ public struct PopupDisplay: View {
|
|
|
153
145
|
.measureLineHeights(font: .body_default_regular,
|
|
154
146
|
oneLine: $oneLineH,
|
|
155
147
|
twoLines: $twoLineH)
|
|
148
|
+
.accessibility(identifier: "description_popup_permission")
|
|
156
149
|
}
|
|
157
150
|
// Cap the visible height to ~8.5 lines
|
|
158
151
|
.frame(height: min(maxHeight8_5, textHeight))
|
|
159
152
|
} else {
|
|
160
|
-
|
|
161
|
-
.font(.body_default_regular)
|
|
162
|
-
.foregroundColor(Colors.black17)
|
|
153
|
+
MomoText(description, typography: .bodyDefaultRegular)
|
|
163
154
|
.multilineTextAlignment(.leading)
|
|
164
155
|
.background(GeometryReader { geo in
|
|
165
156
|
Color.clear.onAppear {
|
|
@@ -171,14 +162,13 @@ public struct PopupDisplay: View {
|
|
|
171
162
|
.measureLineHeights(font: .body_default_regular,
|
|
172
163
|
oneLine: $oneLineH,
|
|
173
164
|
twoLines: $twoLineH)
|
|
165
|
+
.accessibility(identifier: "description_popup_permission")
|
|
174
166
|
}
|
|
175
167
|
}
|
|
176
168
|
.padding(.bottom, 8)
|
|
177
169
|
|
|
178
170
|
if(!errorCode.isEmpty) {
|
|
179
|
-
|
|
180
|
-
.foregroundColor(Colors.black12)
|
|
181
|
-
.font(.description_xs_regular)
|
|
171
|
+
MomoText((errorCodeLabels[language] ?? "Mã lỗi: ") + errorCode, typography: .descriptionXsRegular, color: Colors.black12)
|
|
182
172
|
.lineLimit(1)
|
|
183
173
|
.padding(.bottom, 8)
|
|
184
174
|
}
|
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
|
+
#Mon Dec 22 10:07:29 ICT 2025
|
|
8
|
+
sdk.dir=/Users/phuc/Library/Android/sdk
|
package/package.json
CHANGED
package/publish.sh
CHANGED
|
@@ -185,7 +185,32 @@ phase_publish_maven() {
|
|
|
185
185
|
|
|
186
186
|
./gradlew :compose:publishAllPublicationsToGitLabPackagesRepository
|
|
187
187
|
|
|
188
|
-
echo "✅ Maven publishing completed successfully!"
|
|
188
|
+
echo "✅ compose Maven publishing completed successfully!"
|
|
189
|
+
|
|
190
|
+
echo "📦 Publishing sample/shared KMP artifacts to Maven (version ${VERSION})..."
|
|
191
|
+
|
|
192
|
+
# Temporarily remove composeResources and compose { resources { ... } } block
|
|
193
|
+
echo "📦 Backing up sample/shared composeResources and build.gradle.kts..."
|
|
194
|
+
if [ -d "sample/shared/src/commonMain/composeResources" ]; then
|
|
195
|
+
mv sample/shared/src/commonMain/composeResources sample/shared/src/commonMain/composeResources.backup
|
|
196
|
+
fi
|
|
197
|
+
cp sample/shared/build.gradle.kts sample/shared/build.gradle.kts.backup
|
|
198
|
+
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
199
|
+
sed -i '' '/^compose {$/,/^}$/d' sample/shared/build.gradle.kts
|
|
200
|
+
else
|
|
201
|
+
sed -i '/^compose {$/,/^}$/d' sample/shared/build.gradle.kts
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
./gradlew :sample:shared:publishAllPublicationsToGitLabPackagesRepository
|
|
205
|
+
|
|
206
|
+
# Restore composeResources and build.gradle.kts
|
|
207
|
+
echo "📦 Restoring sample/shared composeResources and build.gradle.kts..."
|
|
208
|
+
mv sample/shared/build.gradle.kts.backup sample/shared/build.gradle.kts
|
|
209
|
+
if [ -d "sample/shared/src/commonMain/composeResources.backup" ]; then
|
|
210
|
+
mv sample/shared/src/commonMain/composeResources.backup sample/shared/src/commonMain/composeResources
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
echo "✅ sample/shared Maven publishing completed successfully!"
|
|
189
214
|
echo "✅ PHASE 2 COMPLETED"
|
|
190
215
|
echo ""
|
|
191
216
|
}
|