@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.
@@ -40,7 +40,7 @@ kotlin {
40
40
  }
41
41
 
42
42
  cocoapods {
43
- version = "0.156.6-beta.22-debug"
43
+ version = "0.156.6-beta.23-debug"
44
44
  summary = "IOS Shared module"
45
45
  homepage = "https://momo.vn"
46
46
  ios.deploymentTarget = "15.0"
@@ -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
- Box(
67
- modifier = Modifier
68
- .height(scaleSize.dp)
69
- .widthIn(min = scaleSize.dp)
70
- .background(color = badgeColor, shape = RoundedCornerShape(Radius.M))
71
- .border(width = 1.dp, shape = RoundedCornerShape(Radius.M), color = Colors.black_01)
72
- .conditional(IsShowBaseLineDebug) {
73
- border(1.dp, Colors.blue_03)
74
- }
75
- .padding(horizontal = Spacing.XS), contentAlignment = Alignment.Center
76
- ) {
77
- Text(
78
- text = formatTitle(label),
79
- color = Colors.black_01,
80
- style = Typography.actionXxsBold
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() || text.text == "0") {
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 = 56
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.XXS)
173
+ .padding(horizontal = Spacing.S, vertical = Spacing.S)
160
174
  .noFeedbackClickable {
161
175
  onClick()
162
176
  },
163
177
  horizontalAlignment = Alignment.CenterHorizontally,
164
- verticalArrangement = Arrangement.Bottom
178
+ verticalArrangement = Arrangement.Center
165
179
  ) {
166
180
  Icon(
167
181
  source = item.icon,
168
- modifier = Modifier.weight(1f),
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(item.badgeLabel != null){
179
- Box(modifier = Modifier
180
- .offset(x = 44.dp, y = (-32).dp)
181
- ){
182
- Badge(item.badgeLabel)
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
@@ -18,7 +18,7 @@ kotlin.apple.xcodeCompatibility.nowarn=true
18
18
  name="ComposeKits"
19
19
  group=vn.momo.kits
20
20
  artifact.id=kits
21
- version=0.156.6-beta.16
21
+ version=0.156.6-beta.23
22
22
 
23
23
  repo=GitLab
24
24
  url=https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven
@@ -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)
@@ -31,7 +31,11 @@ public struct ImageView: View {
31
31
  .placeholder {
32
32
  VStack(alignment: .center, content: {
33
33
  if error {
34
- Image(systemName: "photo.badge.exclamationmark").frame(width: 24, height: 24)
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
- Text(title)
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
- Text(description)
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
- Text(description)
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
- Text((errorCodeLabels[language] ?? "Mã lỗi: ") + errorCode)
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
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/native-kits",
3
- "version": "0.156.6-beta.22-debug",
3
+ "version": "0.156.6-beta.23-debug",
4
4
  "private": false,
5
5
  "dependencies": {},
6
6
  "devDependencies": {},
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
  }