@momo-kits/native-kits 0.157.5-debug → 0.157.6-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/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +7 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +123 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +224 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +338 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +348 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +256 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +494 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +131 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +215 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +531 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +192 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +3 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +5 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +2 -11
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +5 -1
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +12 -0
- package/gradle.properties +1 -1
- package/ios/Popup/PopupPromotion.swift +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
package vn.momo.kits.components
|
|
2
|
+
|
|
3
|
+
import androidx.compose.animation.core.animateDpAsState
|
|
4
|
+
import androidx.compose.animation.core.tween
|
|
5
|
+
import androidx.compose.foundation.ScrollState
|
|
6
|
+
import androidx.compose.foundation.background
|
|
7
|
+
import androidx.compose.foundation.border
|
|
8
|
+
import androidx.compose.foundation.horizontalScroll
|
|
9
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
10
|
+
import androidx.compose.foundation.layout.Box
|
|
11
|
+
import androidx.compose.foundation.layout.Column
|
|
12
|
+
import androidx.compose.foundation.layout.Row
|
|
13
|
+
import androidx.compose.foundation.layout.Spacer
|
|
14
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
15
|
+
import androidx.compose.foundation.layout.height
|
|
16
|
+
import androidx.compose.foundation.layout.offset
|
|
17
|
+
import androidx.compose.foundation.layout.padding
|
|
18
|
+
import androidx.compose.foundation.layout.width
|
|
19
|
+
import androidx.compose.foundation.layout.wrapContentWidth
|
|
20
|
+
import androidx.compose.foundation.pager.HorizontalPager
|
|
21
|
+
import androidx.compose.foundation.pager.rememberPagerState
|
|
22
|
+
import androidx.compose.foundation.rememberScrollState
|
|
23
|
+
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
24
|
+
import androidx.compose.runtime.Composable
|
|
25
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
26
|
+
import androidx.compose.runtime.getValue
|
|
27
|
+
import androidx.compose.runtime.mutableStateListOf
|
|
28
|
+
import androidx.compose.runtime.mutableStateOf
|
|
29
|
+
import androidx.compose.runtime.remember
|
|
30
|
+
import androidx.compose.runtime.rememberCoroutineScope
|
|
31
|
+
import androidx.compose.runtime.setValue
|
|
32
|
+
import androidx.compose.runtime.snapshotFlow
|
|
33
|
+
import androidx.compose.ui.Alignment
|
|
34
|
+
import androidx.compose.ui.Modifier
|
|
35
|
+
import androidx.compose.ui.graphics.Color
|
|
36
|
+
import androidx.compose.ui.layout.onGloballyPositioned
|
|
37
|
+
import androidx.compose.ui.layout.positionInParent
|
|
38
|
+
import androidx.compose.ui.platform.LocalDensity
|
|
39
|
+
import androidx.compose.ui.text.style.TextOverflow
|
|
40
|
+
import androidx.compose.ui.unit.Dp
|
|
41
|
+
import androidx.compose.ui.unit.dp
|
|
42
|
+
import kotlinx.coroutines.launch
|
|
43
|
+
import vn.momo.kits.application.IsShowBaseLineDebug
|
|
44
|
+
import vn.momo.kits.const.AppTheme
|
|
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.modifier.activeOpacityClickable
|
|
50
|
+
import vn.momo.kits.modifier.conditional
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Data model
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
data class TabViewItem(
|
|
57
|
+
val title: String,
|
|
58
|
+
val icon: String? = null,
|
|
59
|
+
val renderIcon: (@Composable (active: Boolean) -> Unit)? = null,
|
|
60
|
+
val showDot: Boolean = false,
|
|
61
|
+
val dotSize: DotSize = DotSize.Small,
|
|
62
|
+
val badgeValue: String? = null,
|
|
63
|
+
val accessibilityLabel: String? = null,
|
|
64
|
+
val content: @Composable () -> Unit = {},
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
enum class TabViewType { DEFAULT, CARD }
|
|
68
|
+
|
|
69
|
+
enum class TabViewDirection { ROW, COLUMN }
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Public composable
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
@Composable
|
|
76
|
+
fun TabView(
|
|
77
|
+
modifier: Modifier = Modifier,
|
|
78
|
+
tabs: List<TabViewItem>,
|
|
79
|
+
initialPage: Int = 0,
|
|
80
|
+
type: TabViewType = TabViewType.DEFAULT,
|
|
81
|
+
scrollable: Boolean = false,
|
|
82
|
+
fitContent: Boolean = false,
|
|
83
|
+
selectedColor: Color? = null,
|
|
84
|
+
unselectedColor: Color? = null,
|
|
85
|
+
direction: TabViewDirection = TabViewDirection.ROW,
|
|
86
|
+
userScrollEnabled: Boolean = true,
|
|
87
|
+
onPressTabItem: (Int) -> Unit = {},
|
|
88
|
+
onPageSelected: (Int) -> Unit = {},
|
|
89
|
+
) {
|
|
90
|
+
if (tabs.isEmpty()) return
|
|
91
|
+
|
|
92
|
+
val theme = AppTheme.current
|
|
93
|
+
val resolvedSelectedColor = selectedColor ?: theme.colors.primary
|
|
94
|
+
val resolvedUnselectedColor = unselectedColor ?: theme.colors.text.default
|
|
95
|
+
|
|
96
|
+
val startPage = initialPage.coerceIn(0, tabs.lastIndex)
|
|
97
|
+
var selectedIndex by remember { mutableStateOf(startPage) }
|
|
98
|
+
|
|
99
|
+
// Track which pages have already been visited for lazy rendering
|
|
100
|
+
val lazyRendered = remember { mutableStateListOf(startPage) }
|
|
101
|
+
|
|
102
|
+
val pagerState = if (!fitContent) {
|
|
103
|
+
rememberPagerState(initialPage = startPage) { tabs.size }
|
|
104
|
+
} else null
|
|
105
|
+
|
|
106
|
+
val coroutineScope = rememberCoroutineScope()
|
|
107
|
+
|
|
108
|
+
// Keep selectedIndex in sync when user swipes the pager
|
|
109
|
+
if (pagerState != null) {
|
|
110
|
+
LaunchedEffect(pagerState) {
|
|
111
|
+
snapshotFlow { pagerState.currentPage }.collect { page ->
|
|
112
|
+
if (!lazyRendered.contains(page)) lazyRendered.add(page)
|
|
113
|
+
selectedIndex = page
|
|
114
|
+
onPageSelected(page)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
val onTabPressed: (Int) -> Unit = { index ->
|
|
120
|
+
onPressTabItem(index)
|
|
121
|
+
if (!lazyRendered.contains(index)) lazyRendered.add(index)
|
|
122
|
+
selectedIndex = index
|
|
123
|
+
if (pagerState != null) {
|
|
124
|
+
coroutineScope.launch { pagerState.animateScrollToPage(index) }
|
|
125
|
+
}
|
|
126
|
+
if (fitContent) {
|
|
127
|
+
onPageSelected(index)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
Column(
|
|
132
|
+
modifier = modifier
|
|
133
|
+
.fillMaxWidth()
|
|
134
|
+
.conditional(IsShowBaseLineDebug) { border(1.dp, Colors.blue_03) },
|
|
135
|
+
) {
|
|
136
|
+
// Tab bar
|
|
137
|
+
when {
|
|
138
|
+
type == TabViewType.CARD -> CardTabBar(
|
|
139
|
+
tabs = tabs,
|
|
140
|
+
selectedIndex = selectedIndex,
|
|
141
|
+
selectedColor = resolvedSelectedColor,
|
|
142
|
+
unselectedColor = resolvedUnselectedColor,
|
|
143
|
+
direction = direction,
|
|
144
|
+
onPressTabItem = onTabPressed,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
scrollable -> ScrollableTabBar(
|
|
148
|
+
tabs = tabs,
|
|
149
|
+
selectedIndex = selectedIndex,
|
|
150
|
+
selectedColor = resolvedSelectedColor,
|
|
151
|
+
unselectedColor = resolvedUnselectedColor,
|
|
152
|
+
direction = direction,
|
|
153
|
+
onPressTabItem = onTabPressed,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
else -> DefaultTabBar(
|
|
157
|
+
tabs = tabs,
|
|
158
|
+
selectedIndex = selectedIndex,
|
|
159
|
+
selectedColor = resolvedSelectedColor,
|
|
160
|
+
unselectedColor = resolvedUnselectedColor,
|
|
161
|
+
direction = direction,
|
|
162
|
+
onPressTabItem = onTabPressed,
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Content area
|
|
167
|
+
if (fitContent) {
|
|
168
|
+
Box(modifier = Modifier.fillMaxWidth()) {
|
|
169
|
+
tabs.getOrNull(selectedIndex)?.content?.invoke()
|
|
170
|
+
}
|
|
171
|
+
} else if (pagerState != null) {
|
|
172
|
+
HorizontalPager(
|
|
173
|
+
state = pagerState,
|
|
174
|
+
modifier = Modifier
|
|
175
|
+
.fillMaxWidth()
|
|
176
|
+
.weight(1f),
|
|
177
|
+
userScrollEnabled = userScrollEnabled,
|
|
178
|
+
) { page ->
|
|
179
|
+
Box(modifier = Modifier.fillMaxWidth()) {
|
|
180
|
+
if (lazyRendered.contains(page)) {
|
|
181
|
+
tabs[page].content()
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Default fixed-width tab bar
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
@Composable
|
|
194
|
+
private fun DefaultTabBar(
|
|
195
|
+
tabs: List<TabViewItem>,
|
|
196
|
+
selectedIndex: Int,
|
|
197
|
+
selectedColor: Color,
|
|
198
|
+
unselectedColor: Color,
|
|
199
|
+
direction: TabViewDirection,
|
|
200
|
+
onPressTabItem: (Int) -> Unit,
|
|
201
|
+
) {
|
|
202
|
+
val theme = AppTheme.current
|
|
203
|
+
val density = LocalDensity.current
|
|
204
|
+
var totalWidthPx by remember { mutableStateOf(0) }
|
|
205
|
+
|
|
206
|
+
val itemWidth: Dp = if (totalWidthPx > 0 && tabs.isNotEmpty()) {
|
|
207
|
+
with(density) { (totalWidthPx / tabs.size).toDp() }
|
|
208
|
+
} else 0.dp
|
|
209
|
+
|
|
210
|
+
val indicatorOffsetX: Dp by animateDpAsState(
|
|
211
|
+
targetValue = itemWidth * selectedIndex + Spacing.XS,
|
|
212
|
+
animationSpec = tween(durationMillis = 200),
|
|
213
|
+
label = "DefaultIndicatorX",
|
|
214
|
+
)
|
|
215
|
+
val indicatorWidth: Dp = (itemWidth - Spacing.XS * 2).coerceAtLeast(0.dp)
|
|
216
|
+
|
|
217
|
+
Box(
|
|
218
|
+
modifier = Modifier
|
|
219
|
+
.fillMaxWidth()
|
|
220
|
+
.onGloballyPositioned { totalWidthPx = it.size.width }
|
|
221
|
+
.background(theme.colors.background.surface)
|
|
222
|
+
.border(
|
|
223
|
+
width = 0.5.dp,
|
|
224
|
+
color = theme.colors.border.default,
|
|
225
|
+
shape = RoundedCornerShape(0.dp),
|
|
226
|
+
),
|
|
227
|
+
) {
|
|
228
|
+
Row(modifier = Modifier.fillMaxWidth()) {
|
|
229
|
+
tabs.forEachIndexed { index, tab ->
|
|
230
|
+
TabItemView(
|
|
231
|
+
modifier = Modifier.weight(1f),
|
|
232
|
+
tab = tab,
|
|
233
|
+
active = selectedIndex == index,
|
|
234
|
+
selectedColor = selectedColor,
|
|
235
|
+
unselectedColor = unselectedColor,
|
|
236
|
+
direction = direction,
|
|
237
|
+
onPress = { onPressTabItem(index) },
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Animated underline indicator
|
|
243
|
+
Box(
|
|
244
|
+
modifier = Modifier
|
|
245
|
+
.align(Alignment.BottomStart)
|
|
246
|
+
.offset(x = indicatorOffsetX)
|
|
247
|
+
.width(indicatorWidth)
|
|
248
|
+
.height(2.dp)
|
|
249
|
+
.background(
|
|
250
|
+
color = selectedColor,
|
|
251
|
+
shape = RoundedCornerShape(topStart = Radius.XXS, topEnd = Radius.XXS),
|
|
252
|
+
),
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
// Scrollable tab bar
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
@Composable
|
|
262
|
+
private fun ScrollableTabBar(
|
|
263
|
+
tabs: List<TabViewItem>,
|
|
264
|
+
selectedIndex: Int,
|
|
265
|
+
selectedColor: Color,
|
|
266
|
+
unselectedColor: Color,
|
|
267
|
+
direction: TabViewDirection,
|
|
268
|
+
onPressTabItem: (Int) -> Unit,
|
|
269
|
+
) {
|
|
270
|
+
val theme = AppTheme.current
|
|
271
|
+
val density = LocalDensity.current
|
|
272
|
+
val scrollState: ScrollState = rememberScrollState()
|
|
273
|
+
val coroutineScope = rememberCoroutineScope()
|
|
274
|
+
|
|
275
|
+
// Per-item (widthPx, xPx) measured relative to the scrollable row
|
|
276
|
+
val itemMeasures = remember { mutableStateListOf<Pair<Float, Float>>() }
|
|
277
|
+
|
|
278
|
+
val rawWidth = itemMeasures.getOrNull(selectedIndex)?.first ?: 0f
|
|
279
|
+
val rawX = itemMeasures.getOrNull(selectedIndex)?.second ?: 0f
|
|
280
|
+
|
|
281
|
+
val animatedWidth: Dp by animateDpAsState(
|
|
282
|
+
targetValue = with(density) { rawWidth.toDp() },
|
|
283
|
+
animationSpec = tween(250),
|
|
284
|
+
label = "ScrollableIndicatorWidth",
|
|
285
|
+
)
|
|
286
|
+
val animatedX: Dp by animateDpAsState(
|
|
287
|
+
targetValue = with(density) { rawX.toDp() },
|
|
288
|
+
animationSpec = tween(250),
|
|
289
|
+
label = "ScrollableIndicatorX",
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
// Auto-scroll bar to keep selected item visible
|
|
293
|
+
LaunchedEffect(selectedIndex, itemMeasures.size) {
|
|
294
|
+
val x = itemMeasures.getOrNull(selectedIndex)?.second ?: return@LaunchedEffect
|
|
295
|
+
val target = (x - with(density) { Spacing.S.toPx() }).coerceAtLeast(0f).toInt()
|
|
296
|
+
coroutineScope.launch { scrollState.animateScrollTo(target) }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
val scrollOffsetDp = with(density) { scrollState.value.toDp() }
|
|
300
|
+
|
|
301
|
+
Box(
|
|
302
|
+
modifier = Modifier
|
|
303
|
+
.fillMaxWidth()
|
|
304
|
+
.background(theme.colors.background.surface)
|
|
305
|
+
.border(
|
|
306
|
+
width = 0.5.dp,
|
|
307
|
+
color = theme.colors.border.default,
|
|
308
|
+
shape = RoundedCornerShape(0.dp),
|
|
309
|
+
),
|
|
310
|
+
) {
|
|
311
|
+
Row(
|
|
312
|
+
modifier = Modifier
|
|
313
|
+
.fillMaxWidth()
|
|
314
|
+
.horizontalScroll(scrollState),
|
|
315
|
+
) {
|
|
316
|
+
tabs.forEachIndexed { index, tab ->
|
|
317
|
+
Box(
|
|
318
|
+
modifier = Modifier
|
|
319
|
+
.wrapContentWidth()
|
|
320
|
+
.onGloballyPositioned { coords ->
|
|
321
|
+
val w = coords.size.width.toFloat()
|
|
322
|
+
val x = coords.positionInParent().x
|
|
323
|
+
while (itemMeasures.size <= index) itemMeasures.add(0f to 0f)
|
|
324
|
+
itemMeasures[index] = w to x
|
|
325
|
+
},
|
|
326
|
+
) {
|
|
327
|
+
TabItemView(
|
|
328
|
+
tab = tab,
|
|
329
|
+
active = selectedIndex == index,
|
|
330
|
+
selectedColor = selectedColor,
|
|
331
|
+
unselectedColor = unselectedColor,
|
|
332
|
+
direction = direction,
|
|
333
|
+
scrollable = true,
|
|
334
|
+
onPress = { onPressTabItem(index) },
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Animated underline indicator aligned to bottom of the bar
|
|
341
|
+
Box(
|
|
342
|
+
modifier = Modifier
|
|
343
|
+
.align(Alignment.BottomStart)
|
|
344
|
+
.offset(x = animatedX - scrollOffsetDp + Spacing.XS)
|
|
345
|
+
.width((animatedWidth - Spacing.XS * 2).coerceAtLeast(0.dp))
|
|
346
|
+
.height(2.dp)
|
|
347
|
+
.background(
|
|
348
|
+
color = selectedColor,
|
|
349
|
+
shape = RoundedCornerShape(topStart = Radius.XXS, topEnd = Radius.XXS),
|
|
350
|
+
),
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
// Card tab bar
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
@Composable
|
|
360
|
+
private fun CardTabBar(
|
|
361
|
+
tabs: List<TabViewItem>,
|
|
362
|
+
selectedIndex: Int,
|
|
363
|
+
selectedColor: Color,
|
|
364
|
+
unselectedColor: Color,
|
|
365
|
+
direction: TabViewDirection,
|
|
366
|
+
onPressTabItem: (Int) -> Unit,
|
|
367
|
+
) {
|
|
368
|
+
val theme = AppTheme.current
|
|
369
|
+
|
|
370
|
+
Row(
|
|
371
|
+
modifier = Modifier
|
|
372
|
+
.fillMaxWidth()
|
|
373
|
+
.padding(top = Spacing.XS),
|
|
374
|
+
verticalAlignment = Alignment.Bottom,
|
|
375
|
+
) {
|
|
376
|
+
tabs.forEachIndexed { index, tab ->
|
|
377
|
+
val isActive = selectedIndex == index
|
|
378
|
+
val iconColor = if (isActive) selectedColor else unselectedColor
|
|
379
|
+
|
|
380
|
+
Box(
|
|
381
|
+
modifier = Modifier
|
|
382
|
+
.weight(1f)
|
|
383
|
+
.height(if (isActive) 44.dp else 40.dp)
|
|
384
|
+
.background(
|
|
385
|
+
color = if (isActive) theme.colors.background.surface
|
|
386
|
+
else theme.colors.background.tonal,
|
|
387
|
+
shape = RoundedCornerShape(
|
|
388
|
+
topStart = Radius.M,
|
|
389
|
+
topEnd = Radius.M,
|
|
390
|
+
),
|
|
391
|
+
)
|
|
392
|
+
.activeOpacityClickable { onPressTabItem(index) }
|
|
393
|
+
.padding(horizontal = Spacing.S),
|
|
394
|
+
contentAlignment = Alignment.Center,
|
|
395
|
+
) {
|
|
396
|
+
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
397
|
+
if (tab.renderIcon != null) {
|
|
398
|
+
tab.renderIcon.invoke(isActive)
|
|
399
|
+
Spacer(modifier = Modifier.width(Spacing.XS))
|
|
400
|
+
} else if (tab.icon != null) {
|
|
401
|
+
Icon(
|
|
402
|
+
source = tab.icon,
|
|
403
|
+
size = 18.dp,
|
|
404
|
+
color = iconColor,
|
|
405
|
+
)
|
|
406
|
+
Spacer(modifier = Modifier.width(Spacing.XS))
|
|
407
|
+
}
|
|
408
|
+
Text(
|
|
409
|
+
text = tab.title,
|
|
410
|
+
style = if (isActive) Typography.headerSSemibold else Typography.bodyDefaultRegular,
|
|
411
|
+
color = if (isActive) selectedColor else unselectedColor,
|
|
412
|
+
maxLines = 1,
|
|
413
|
+
overflow = TextOverflow.Ellipsis,
|
|
414
|
+
modifier = Modifier.weight(1f, fill = false),
|
|
415
|
+
)
|
|
416
|
+
if (tab.showDot) {
|
|
417
|
+
Spacer(modifier = Modifier.width(Spacing.XS))
|
|
418
|
+
BadgeDot(size = tab.dotSize)
|
|
419
|
+
} else if (!tab.badgeValue.isNullOrEmpty()) {
|
|
420
|
+
Spacer(modifier = Modifier.width(Spacing.XS))
|
|
421
|
+
Badge(label = tab.badgeValue, modifier = Modifier)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
// Shared tab item used by DefaultTabBar and ScrollableTabBar
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
|
|
433
|
+
@Composable
|
|
434
|
+
private fun TabItemView(
|
|
435
|
+
modifier: Modifier = Modifier,
|
|
436
|
+
tab: TabViewItem,
|
|
437
|
+
active: Boolean,
|
|
438
|
+
selectedColor: Color,
|
|
439
|
+
unselectedColor: Color,
|
|
440
|
+
direction: TabViewDirection,
|
|
441
|
+
scrollable: Boolean = false,
|
|
442
|
+
onPress: () -> Unit,
|
|
443
|
+
) {
|
|
444
|
+
val textColor = if (active) selectedColor else unselectedColor
|
|
445
|
+
val textStyle = if (active) Typography.headerSSemibold else Typography.bodyDefaultRegular
|
|
446
|
+
|
|
447
|
+
if (direction == TabViewDirection.COLUMN) {
|
|
448
|
+
Column(
|
|
449
|
+
modifier = modifier
|
|
450
|
+
.height(68.dp)
|
|
451
|
+
.activeOpacityClickable { onPress() }
|
|
452
|
+
.padding(horizontal = Spacing.S),
|
|
453
|
+
horizontalAlignment = Alignment.CenterHorizontally,
|
|
454
|
+
verticalArrangement = Arrangement.Center,
|
|
455
|
+
) {
|
|
456
|
+
// Icon area with badge/dot overlay
|
|
457
|
+
Box(contentAlignment = Alignment.TopEnd) {
|
|
458
|
+
if (tab.renderIcon != null) {
|
|
459
|
+
tab.renderIcon.invoke(active)
|
|
460
|
+
} else if (tab.icon != null) {
|
|
461
|
+
Icon(
|
|
462
|
+
source = tab.icon,
|
|
463
|
+
size = 24.dp,
|
|
464
|
+
color = textColor,
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
if (tab.showDot) {
|
|
468
|
+
BadgeDot(
|
|
469
|
+
size = tab.dotSize,
|
|
470
|
+
modifier = Modifier
|
|
471
|
+
.align(Alignment.TopEnd)
|
|
472
|
+
.offset(x = 4.dp, y = (-4).dp),
|
|
473
|
+
)
|
|
474
|
+
} else if (!tab.badgeValue.isNullOrEmpty()) {
|
|
475
|
+
Badge(
|
|
476
|
+
label = tab.badgeValue,
|
|
477
|
+
modifier = Modifier
|
|
478
|
+
.align(Alignment.TopEnd)
|
|
479
|
+
.offset(x = 4.dp, y = (-4).dp),
|
|
480
|
+
)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (tab.icon != null || tab.renderIcon != null) {
|
|
484
|
+
Spacer(modifier = Modifier.height(Spacing.XXS))
|
|
485
|
+
}
|
|
486
|
+
Text(
|
|
487
|
+
text = tab.title,
|
|
488
|
+
style = textStyle,
|
|
489
|
+
color = textColor,
|
|
490
|
+
maxLines = 1,
|
|
491
|
+
overflow = TextOverflow.Ellipsis,
|
|
492
|
+
)
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
Row(
|
|
496
|
+
modifier = modifier
|
|
497
|
+
.height(48.dp)
|
|
498
|
+
.activeOpacityClickable { onPress() }
|
|
499
|
+
.padding(horizontal = Spacing.M),
|
|
500
|
+
horizontalArrangement = Arrangement.Center,
|
|
501
|
+
verticalAlignment = Alignment.CenterVertically,
|
|
502
|
+
) {
|
|
503
|
+
if (tab.renderIcon != null) {
|
|
504
|
+
tab.renderIcon.invoke(active)
|
|
505
|
+
Spacer(modifier = Modifier.width(Spacing.XS))
|
|
506
|
+
} else if (tab.icon != null) {
|
|
507
|
+
Icon(
|
|
508
|
+
source = tab.icon,
|
|
509
|
+
size = 18.dp,
|
|
510
|
+
color = textColor,
|
|
511
|
+
)
|
|
512
|
+
Spacer(modifier = Modifier.width(Spacing.XS))
|
|
513
|
+
}
|
|
514
|
+
Text(
|
|
515
|
+
text = tab.title,
|
|
516
|
+
style = textStyle,
|
|
517
|
+
color = textColor,
|
|
518
|
+
maxLines = 1,
|
|
519
|
+
overflow = TextOverflow.Ellipsis,
|
|
520
|
+
modifier = if (scrollable) Modifier else Modifier.weight(1f, fill = false),
|
|
521
|
+
)
|
|
522
|
+
if (tab.showDot) {
|
|
523
|
+
Spacer(modifier = Modifier.width(Spacing.XS))
|
|
524
|
+
BadgeDot(size = tab.dotSize)
|
|
525
|
+
} else if (!tab.badgeValue.isNullOrEmpty()) {
|
|
526
|
+
Spacer(modifier = Modifier.width(Spacing.XS))
|
|
527
|
+
Badge(label = tab.badgeValue, modifier = Modifier)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|