@momo-kits/native-kits 0.168.1-beta.2-debug → 0.168.1-beta.24-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/CLAUDE.md +10 -6
- package/compose/build.gradle.kts +1 -1
- package/compose/compose.podspec +1 -1
- package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +4 -3
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +62 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +25 -16
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +3 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +18 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +25 -13
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +206 -45
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +14 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +53 -30
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +2 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +124 -59
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +3 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +10 -3
- package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +3 -1
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +9 -8
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +54 -10
- package/gradle.properties +1 -1
- package/ios/Application/ApplicationEnvironment.swift +7 -6
- package/ios/Application/DesignSystemKits.swift +163 -0
- package/ios/Application/DesignSystemKitsPayload.swift +68 -0
- package/ios/Badge/BadgeRibbon.swift +8 -1
- package/ios/Extensions/Color++.swift +2 -4
- package/ios/Input/Input.swift +13 -3
- package/ios/Input/InputPhoneNumber.swift +1 -3
- package/ios/Input/InputSearch.swift +1 -2
- package/ios/Input/InputTextArea.swift +5 -3
- package/ios/InputMoney/InputMoney.swift +6 -7
- package/ios/Layout/ScreenLayout.swift +65 -10
- package/ios/Navigation/BottomSheet.swift +153 -23
- package/ios/Navigation/BottomTab/DuoBarCompression.swift +2 -3
- package/ios/Navigation/BottomTab/DuoTabBar.swift +94 -69
- package/ios/Navigation/Component/HeaderBackground.swift +22 -1
- package/ios/Navigation/Component/SnackBar.swift +4 -0
- package/ios/Navigation/ModalScreen.swift +7 -3
- package/ios/Navigation/StackScreen.swift +3 -7
- package/ios/Popup/PopupDisplay.swift +2 -2
- package/ios/Popup/PopupInput.swift +2 -2
- package/ios/Popup/PopupLayout.swift +15 -10
- package/ios/Popup/PopupNotify.swift +2 -2
- package/ios/Stepper/Stepper.swift +1 -2
- package/ios/Template/TrustBanner/TrustBanner.swift +4 -2
- package/ios/Typography/FontScale.swift +29 -15
- package/ios/Typography/Text.swift +86 -12
- package/ios/Typography/Typography.swift +2 -2
- package/ios/native-kits.podspec +1 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/local.properties +8 -0
- package/package.json +1 -1
- package/.claude/settings.local.json +0 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
- package/ios/Typography/FontScaleStore.swift +0 -58
- package/ios/Typography/KitDesignSystem.swift +0 -17
|
@@ -6,7 +6,9 @@ import androidx.compose.animation.core.tween
|
|
|
6
6
|
import androidx.compose.foundation.background
|
|
7
7
|
import androidx.compose.foundation.border
|
|
8
8
|
import androidx.compose.foundation.gestures.detectDragGestures
|
|
9
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
9
10
|
import androidx.compose.foundation.layout.Box
|
|
11
|
+
import androidx.compose.foundation.layout.BoxScope
|
|
10
12
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
11
13
|
import androidx.compose.foundation.layout.Column
|
|
12
14
|
import androidx.compose.foundation.layout.Row
|
|
@@ -17,6 +19,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|
|
17
19
|
import androidx.compose.foundation.layout.height
|
|
18
20
|
import androidx.compose.foundation.layout.heightIn
|
|
19
21
|
import androidx.compose.foundation.layout.offset
|
|
22
|
+
import androidx.compose.foundation.layout.PaddingValues
|
|
20
23
|
import androidx.compose.foundation.layout.padding
|
|
21
24
|
import androidx.compose.foundation.layout.size
|
|
22
25
|
import androidx.compose.foundation.layout.width
|
|
@@ -25,15 +28,22 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|
|
25
28
|
import androidx.compose.runtime.Composable
|
|
26
29
|
import androidx.compose.runtime.DisposableEffect
|
|
27
30
|
import androidx.compose.runtime.LaunchedEffect
|
|
31
|
+
import androidx.compose.runtime.getValue
|
|
32
|
+
import androidx.compose.runtime.mutableStateOf
|
|
28
33
|
import androidx.compose.runtime.remember
|
|
34
|
+
import androidx.compose.runtime.setValue
|
|
29
35
|
import androidx.compose.runtime.rememberCoroutineScope
|
|
36
|
+
import androidx.compose.ui.AbsoluteAlignment
|
|
30
37
|
import androidx.compose.ui.Alignment
|
|
31
38
|
import androidx.compose.ui.Modifier
|
|
32
39
|
import androidx.compose.ui.graphics.Brush
|
|
33
40
|
import androidx.compose.ui.graphics.Color
|
|
34
41
|
import androidx.compose.ui.input.pointer.pointerInput
|
|
42
|
+
import androidx.compose.ui.layout.layout
|
|
35
43
|
import androidx.compose.ui.platform.LocalDensity
|
|
44
|
+
import androidx.compose.ui.unit.Dp
|
|
36
45
|
import androidx.compose.ui.unit.IntOffset
|
|
46
|
+
import androidx.compose.ui.unit.constrainHeight
|
|
37
47
|
import androidx.compose.ui.unit.dp
|
|
38
48
|
import kotlinx.coroutines.launch
|
|
39
49
|
import vn.momo.kits.components.Divider
|
|
@@ -52,11 +62,24 @@ import vn.momo.kits.modifier.conditional
|
|
|
52
62
|
import vn.momo.kits.modifier.noFeedbackClickable
|
|
53
63
|
import vn.momo.kits.platform.BackHandler
|
|
54
64
|
import vn.momo.kits.platform.getScreenHeight
|
|
65
|
+
import vn.momo.kits.layout.DuoMetrics
|
|
66
|
+
import vn.momo.kits.layout.SideBarEdge
|
|
67
|
+
import vn.momo.kits.layout.SideBarLayout
|
|
55
68
|
import vn.momo.kits.layout.getScreenLayout
|
|
69
|
+
import vn.momo.kits.modifier.setAutomationId
|
|
70
|
+
import vn.momo.kits.navigation.component.HeaderType
|
|
71
|
+
import vn.momo.kits.navigation.component.SideBarButton
|
|
72
|
+
import vn.momo.kits.navigation.component.getHeaderColor
|
|
56
73
|
|
|
57
74
|
// iPhone Duo inner display: center the sheet instead of spanning the width (RN BottomSheet).
|
|
58
75
|
private val DUO_REGULAR_MIN_WIDTH = 600.dp
|
|
59
76
|
|
|
77
|
+
private val INDICATOR_HEIGHT = 16.dp
|
|
78
|
+
private val HEADER_ROW_HEIGHT = 56.dp
|
|
79
|
+
|
|
80
|
+
// Side column controls line up with the header row while the sheet is short.
|
|
81
|
+
private val SIDE_COLUMN_OFFSET = INDICATOR_HEIGHT + (HEADER_ROW_HEIGHT - DuoMetrics.controlSize) / 2
|
|
82
|
+
|
|
60
83
|
@Composable
|
|
61
84
|
internal fun BottomSheet(
|
|
62
85
|
content: @Composable () -> Unit,
|
|
@@ -70,7 +93,21 @@ internal fun BottomSheet(
|
|
|
70
93
|
) {
|
|
71
94
|
val layout = getScreenLayout()
|
|
72
95
|
val isDuo = layout.isIPhoneDuoDevice
|
|
73
|
-
val isDuoCentered = isDuo && layout.width >= DUO_REGULAR_MIN_WIDTH
|
|
96
|
+
val isDuoCentered = isDuo && minOf(layout.width, layout.height) >= DUO_REGULAR_MIN_WIDTH
|
|
97
|
+
val isDuoFloating = isDuo && !isDuoCentered
|
|
98
|
+
val title = header as? BottomHeader.Title
|
|
99
|
+
val sideColumn = layout.sideBar?.takeIf {
|
|
100
|
+
isDuoFloating && title != null && it.width - Spacing.M >= DuoMetrics.controlSize + Spacing.M
|
|
101
|
+
}
|
|
102
|
+
// the floating sheet spans the side safe area, its content stays clear of it
|
|
103
|
+
val sidePadding = if (isDuoFloating) {
|
|
104
|
+
PaddingValues.Absolute(
|
|
105
|
+
left = (layout.insetLeft - Spacing.M).coerceAtLeast(0.dp),
|
|
106
|
+
right = (layout.insetRight - Spacing.M).coerceAtLeast(0.dp),
|
|
107
|
+
)
|
|
108
|
+
} else {
|
|
109
|
+
PaddingValues()
|
|
110
|
+
}
|
|
74
111
|
// the screen is the outer display on iPhone Duo, even when unfolded
|
|
75
112
|
val screenHeightDp = if (isDuo) layout.height else getScreenHeight()
|
|
76
113
|
val screenHeightPx = with(LocalDensity.current) { screenHeightDp.toPx() }
|
|
@@ -96,6 +133,11 @@ internal fun BottomSheet(
|
|
|
96
133
|
onDispose { }
|
|
97
134
|
}
|
|
98
135
|
|
|
136
|
+
// a closing sheet keeps going: a second close, the open slide or a drag would cancel its animation
|
|
137
|
+
var isClosing by remember { mutableStateOf(false) }
|
|
138
|
+
// runs once the sheet has closed itself, after its onDismiss
|
|
139
|
+
var afterDismiss by remember { mutableStateOf<(() -> Unit)?>(null) }
|
|
140
|
+
|
|
99
141
|
suspend fun openEvent(){
|
|
100
142
|
backgroundAlpha.animateTo(
|
|
101
143
|
targetValue = 0.3f,
|
|
@@ -104,6 +146,7 @@ internal fun BottomSheet(
|
|
|
104
146
|
easing = CubicBezierEasing(0.05f, 0.7f, 0.1f, 1f)
|
|
105
147
|
)
|
|
106
148
|
)
|
|
149
|
+
if (isClosing) return
|
|
107
150
|
sheetOffset.animateTo(
|
|
108
151
|
targetValue = 0f,
|
|
109
152
|
animationSpec = tween(
|
|
@@ -113,7 +156,9 @@ internal fun BottomSheet(
|
|
|
113
156
|
)
|
|
114
157
|
}
|
|
115
158
|
|
|
116
|
-
fun closeEvent(){
|
|
159
|
+
fun closeEvent(then: (() -> Unit)? = null){
|
|
160
|
+
if (isClosing) return
|
|
161
|
+
isClosing = true
|
|
117
162
|
coroutineScope.launch {
|
|
118
163
|
sheetOffset.animateTo(
|
|
119
164
|
targetValue = screenHeightPx,
|
|
@@ -129,10 +174,15 @@ internal fun BottomSheet(
|
|
|
129
174
|
easing = CubicBezierEasing(0.3f, 0.0f, 0.8f, 0.15f)
|
|
130
175
|
)
|
|
131
176
|
)
|
|
177
|
+
// not reached when a newer overlay replaced this one, so its back is dropped
|
|
178
|
+
afterDismiss = then
|
|
132
179
|
OverplayComponentRegistry.hardClearAfterDismiss()
|
|
133
180
|
}
|
|
134
181
|
}
|
|
135
182
|
|
|
183
|
+
// RN `leftOptions.onPressIconLeft`: the sheet closes first, then the back runs
|
|
184
|
+
val onBack = title?.onBack?.let { back -> { closeEvent(then = back) } }
|
|
185
|
+
|
|
136
186
|
LaunchedEffect(Unit) {
|
|
137
187
|
openEvent()
|
|
138
188
|
}
|
|
@@ -141,6 +191,7 @@ internal fun BottomSheet(
|
|
|
141
191
|
OverplayComponentRegistry.bindClose { closeEvent() }
|
|
142
192
|
onDispose {
|
|
143
193
|
onDismiss?.invoke()
|
|
194
|
+
afterDismiss?.invoke()
|
|
144
195
|
}
|
|
145
196
|
}
|
|
146
197
|
|
|
@@ -162,30 +213,29 @@ internal fun BottomSheet(
|
|
|
162
213
|
} else {
|
|
163
214
|
parentHeight - 90.dp
|
|
164
215
|
}
|
|
165
|
-
|
|
166
|
-
val sideInset = isDuo && !isDuoCentered && layout.width > layout.height
|
|
216
|
+
val radius = if (isDuo) Radius.XL else Radius.M
|
|
167
217
|
|
|
168
218
|
Box(
|
|
169
219
|
modifier = Modifier
|
|
170
220
|
.align(Alignment.BottomCenter)
|
|
171
221
|
.offset { IntOffset(x = 0, y = sheetOffset.value.toInt()) }
|
|
172
222
|
.then(
|
|
173
|
-
|
|
223
|
+
when {
|
|
174
224
|
// keeps the portrait width in landscape, like system sheets
|
|
175
|
-
Modifier.width(
|
|
225
|
+
isDuoCentered -> Modifier.width(
|
|
176
226
|
minOf(
|
|
177
227
|
minOf(layout.width, layout.height) - Spacing.L * 2,
|
|
178
228
|
layout.width - maxOf(layout.insetLeft, layout.insetRight, Spacing.L) * 2,
|
|
179
229
|
)
|
|
180
230
|
)
|
|
181
|
-
|
|
182
|
-
Modifier.fillMaxWidth()
|
|
231
|
+
isDuoFloating -> Modifier.fillMaxWidth().padding(horizontal = Spacing.M)
|
|
232
|
+
else -> Modifier.fillMaxWidth()
|
|
183
233
|
}
|
|
184
234
|
)
|
|
185
235
|
.heightIn(max = maxHeight)
|
|
186
236
|
.background(
|
|
187
237
|
color = if (isSurface) AppTheme.current.colors.background.surface else AppTheme.current.colors.background.default,
|
|
188
|
-
shape = RoundedCornerShape(topStart =
|
|
238
|
+
shape = RoundedCornerShape(topStart = radius, topEnd = radius)
|
|
189
239
|
)
|
|
190
240
|
.conditional(IsShowBaseLineDebug) {
|
|
191
241
|
border(1.dp, Colors.blue_03)
|
|
@@ -196,28 +246,24 @@ internal fun BottomSheet(
|
|
|
196
246
|
val bottomInset = if (useBottomInset) {
|
|
197
247
|
(AppNavigationBar.current - keyboardSizeState().value).coerceAtLeast(0.dp)
|
|
198
248
|
} else 0.dp
|
|
199
|
-
Column(
|
|
200
|
-
Modifier
|
|
201
|
-
.padding(bottom = bottomInset)
|
|
202
|
-
.conditional(sideInset) {
|
|
203
|
-
absolutePadding(left = layout.insetLeft, right = layout.insetRight)
|
|
204
|
-
}
|
|
205
|
-
) {
|
|
249
|
+
Column(Modifier.padding(bottom = bottomInset)) {
|
|
206
250
|
Box(
|
|
207
251
|
modifier = Modifier
|
|
208
|
-
.height(
|
|
252
|
+
.height(INDICATOR_HEIGHT + HEADER_ROW_HEIGHT)
|
|
209
253
|
.fillMaxWidth()
|
|
210
254
|
.conditional(draggable) {
|
|
211
255
|
pointerInput(Unit) {
|
|
212
256
|
detectDragGestures(
|
|
213
257
|
onDrag = { change, dragAmount ->
|
|
214
258
|
change.consume()
|
|
259
|
+
if (isClosing) return@detectDragGestures
|
|
215
260
|
coroutineScope.launch {
|
|
216
261
|
val newOffset = (sheetOffset.value + dragAmount.y).coerceAtLeast(0f)
|
|
217
262
|
sheetOffset.snapTo(newOffset)
|
|
218
263
|
}
|
|
219
264
|
},
|
|
220
265
|
onDragEnd = {
|
|
266
|
+
if (isClosing) return@detectDragGestures
|
|
221
267
|
coroutineScope.launch {
|
|
222
268
|
if (sheetOffset.value > sheetCloseOffset) {
|
|
223
269
|
closeEvent()
|
|
@@ -233,6 +279,7 @@ internal fun BottomSheet(
|
|
|
233
279
|
Column(
|
|
234
280
|
modifier = Modifier
|
|
235
281
|
.fillMaxSize()
|
|
282
|
+
.padding(sidePadding)
|
|
236
283
|
.padding(horizontal = Spacing.M),
|
|
237
284
|
horizontalAlignment = Alignment.CenterHorizontally
|
|
238
285
|
) {
|
|
@@ -247,29 +294,16 @@ internal fun BottomSheet(
|
|
|
247
294
|
)
|
|
248
295
|
)
|
|
249
296
|
Spacer(Modifier.height(4.dp))
|
|
250
|
-
Box(Modifier.fillMaxWidth().height(
|
|
297
|
+
Box(Modifier.fillMaxWidth().height(HEADER_ROW_HEIGHT), contentAlignment = Alignment.Center){
|
|
251
298
|
when (header) {
|
|
252
|
-
is BottomHeader.Title ->
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
)
|
|
261
|
-
}
|
|
262
|
-
Box(Modifier
|
|
263
|
-
.size(24.dp)
|
|
264
|
-
.noFeedbackClickable {
|
|
265
|
-
closeEvent()
|
|
266
|
-
},
|
|
267
|
-
contentAlignment = Alignment.Center
|
|
268
|
-
){
|
|
269
|
-
Icon(source = "navigation_close")
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
299
|
+
is BottomHeader.Title -> BottomSheetTitle(
|
|
300
|
+
text = header.data,
|
|
301
|
+
backIcon = header.backIcon,
|
|
302
|
+
// the side column holds back and close, the row keeps only the title
|
|
303
|
+
showButtons = sideColumn == null,
|
|
304
|
+
onBack = onBack,
|
|
305
|
+
onClose = { closeEvent() },
|
|
306
|
+
)
|
|
273
307
|
is BottomHeader.Custom -> {
|
|
274
308
|
header.content()
|
|
275
309
|
}
|
|
@@ -277,27 +311,141 @@ internal fun BottomSheet(
|
|
|
277
311
|
}
|
|
278
312
|
}
|
|
279
313
|
}
|
|
280
|
-
|
|
314
|
+
Box(Modifier.padding(sidePadding)) {
|
|
315
|
+
Divider()
|
|
316
|
+
}
|
|
281
317
|
if (footerComponent != null) {
|
|
282
|
-
Box(Modifier.weight(1f, fill = false)) {
|
|
318
|
+
Box(Modifier.weight(1f, fill = false).padding(sidePadding)) {
|
|
283
319
|
content()
|
|
284
320
|
}
|
|
285
|
-
BottomSheetFooter(footerComponent)
|
|
321
|
+
BottomSheetFooter(footerComponent, sidePadding)
|
|
286
322
|
} else if (isDuo) {
|
|
287
323
|
// content shrinks so a tall sheet keeps its header
|
|
288
|
-
Box(Modifier.weight(1f, fill = false)) {
|
|
324
|
+
Box(Modifier.weight(1f, fill = false).padding(sidePadding)) {
|
|
289
325
|
content()
|
|
290
326
|
}
|
|
291
327
|
} else {
|
|
292
328
|
content()
|
|
293
329
|
}
|
|
294
330
|
}
|
|
331
|
+
if (sideColumn != null && title != null) {
|
|
332
|
+
BottomSheetSideColumn(
|
|
333
|
+
sideBar = sideColumn,
|
|
334
|
+
parentHeight = parentHeight,
|
|
335
|
+
backIcon = title.backIcon,
|
|
336
|
+
onBack = onBack,
|
|
337
|
+
onClose = { closeEvent() },
|
|
338
|
+
)
|
|
339
|
+
}
|
|
295
340
|
}
|
|
296
341
|
}
|
|
297
342
|
}
|
|
298
343
|
|
|
299
344
|
@Composable
|
|
300
|
-
private fun
|
|
345
|
+
private fun BottomSheetTitle(
|
|
346
|
+
text: String,
|
|
347
|
+
backIcon: String,
|
|
348
|
+
showButtons: Boolean,
|
|
349
|
+
onBack: (() -> Unit)?,
|
|
350
|
+
onClose: () -> Unit,
|
|
351
|
+
) {
|
|
352
|
+
Row(modifier = Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) {
|
|
353
|
+
if (showButtons) {
|
|
354
|
+
if (onBack != null) {
|
|
355
|
+
BottomSheetHeaderButton(icon = backIcon, automationId = "btn_bottom_sheet_back", onClick = onBack)
|
|
356
|
+
} else {
|
|
357
|
+
Box(Modifier.size(24.dp))
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
Box(
|
|
361
|
+
Modifier
|
|
362
|
+
.fillMaxHeight()
|
|
363
|
+
.weight(1f)
|
|
364
|
+
.conditional(!showButtons) { padding(horizontal = Spacing.M) },
|
|
365
|
+
contentAlignment = Alignment.Center
|
|
366
|
+
) {
|
|
367
|
+
Text(
|
|
368
|
+
text = text,
|
|
369
|
+
color = Colors.black_17,
|
|
370
|
+
style = Typography.headerDefaultBold
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
if (showButtons) {
|
|
374
|
+
BottomSheetHeaderButton(icon = "navigation_close", automationId = "btn_bottom_sheet_close", onClick = onClose)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
@Composable
|
|
380
|
+
private fun BottomSheetHeaderButton(icon: String, automationId: String, onClick: () -> Unit) {
|
|
381
|
+
Box(
|
|
382
|
+
Modifier
|
|
383
|
+
.size(24.dp)
|
|
384
|
+
.noFeedbackClickable(onClick = onClick)
|
|
385
|
+
.setAutomationId(automationId),
|
|
386
|
+
contentAlignment = Alignment.Center
|
|
387
|
+
) {
|
|
388
|
+
Icon(source = icon)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// iPhone Duo outer display: back and close sit on the part of the sheet under the side safe area,
|
|
393
|
+
// 12pt from the strip's inner edge like the screen side bar. They follow the header row while the
|
|
394
|
+
// sheet is short and stay below the status bar cluster while it is tall.
|
|
395
|
+
@Composable
|
|
396
|
+
private fun BoxScope.BottomSheetSideColumn(
|
|
397
|
+
sideBar: SideBarLayout,
|
|
398
|
+
parentHeight: Dp,
|
|
399
|
+
backIcon: String,
|
|
400
|
+
onBack: (() -> Unit)?,
|
|
401
|
+
onClose: () -> Unit,
|
|
402
|
+
) {
|
|
403
|
+
val isRight = sideBar.edge == SideBarEdge.RIGHT
|
|
404
|
+
val headerColor = getHeaderColor(HeaderType.None, 1f, null, AppTheme.current.colors.text.default)
|
|
405
|
+
|
|
406
|
+
Box(Modifier.matchParentSize()) {
|
|
407
|
+
Column(
|
|
408
|
+
modifier = Modifier
|
|
409
|
+
.align(if (isRight) AbsoluteAlignment.TopRight else AbsoluteAlignment.TopLeft)
|
|
410
|
+
.width(sideBar.width - Spacing.M)
|
|
411
|
+
.layout { measurable, constraints ->
|
|
412
|
+
val placeable = measurable.measure(constraints.copy(minHeight = 0))
|
|
413
|
+
val sheetHeight = constraints.maxHeight
|
|
414
|
+
// the sheet rests on the bottom of the overlay, which starts at the window top
|
|
415
|
+
val sheetTop = parentHeight.roundToPx() - sheetHeight
|
|
416
|
+
// clear of a camera at the bottom end of the strip when it can
|
|
417
|
+
val lowest = sheetHeight - sideBar.bottom.roundToPx() - placeable.height
|
|
418
|
+
val y = maxOf(sideBar.top.roundToPx() - sheetTop, SIDE_COLUMN_OFFSET.roundToPx())
|
|
419
|
+
.coerceAtMost(lowest)
|
|
420
|
+
.coerceAtLeast(0)
|
|
421
|
+
layout(placeable.width, constraints.constrainHeight(y + placeable.height)) {
|
|
422
|
+
placeable.place(0, y)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
.absolutePadding(left = if (isRight) Spacing.M else 0.dp, right = if (isRight) 0.dp else Spacing.M),
|
|
426
|
+
horizontalAlignment = if (isRight) AbsoluteAlignment.Left else AbsoluteAlignment.Right,
|
|
427
|
+
verticalArrangement = Arrangement.spacedBy(Spacing.M),
|
|
428
|
+
) {
|
|
429
|
+
if (onBack != null) {
|
|
430
|
+
SideBarButton(
|
|
431
|
+
icon = backIcon,
|
|
432
|
+
headerColor = headerColor,
|
|
433
|
+
automationId = "btn_bottom_sheet_back",
|
|
434
|
+
onClick = onBack,
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
SideBarButton(
|
|
438
|
+
icon = "navigation_close",
|
|
439
|
+
headerColor = headerColor,
|
|
440
|
+
automationId = "btn_bottom_sheet_close",
|
|
441
|
+
onClick = onClose,
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
@Composable
|
|
448
|
+
private fun BottomSheetFooter(footerComponent: @Composable () -> Unit, sidePadding: PaddingValues) {
|
|
301
449
|
val shadowBrush = remember {
|
|
302
450
|
Brush.verticalGradient(
|
|
303
451
|
colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.05f))
|
|
@@ -312,6 +460,7 @@ private fun BottomSheetFooter(footerComponent: @Composable () -> Unit) {
|
|
|
312
460
|
.conditional(IsShowBaseLineDebug) {
|
|
313
461
|
border(1.dp, Colors.blue_03)
|
|
314
462
|
}
|
|
463
|
+
.padding(sidePadding)
|
|
315
464
|
.padding(vertical = Spacing.S)
|
|
316
465
|
) {
|
|
317
466
|
footerComponent()
|
|
@@ -330,7 +479,19 @@ private fun BottomSheetFooter(footerComponent: @Composable () -> Unit) {
|
|
|
330
479
|
sealed class BottomHeader {
|
|
331
480
|
data class Title(
|
|
332
481
|
val data: String = "Bottom Sheet Title",
|
|
333
|
-
) : BottomHeader()
|
|
482
|
+
) : BottomHeader() {
|
|
483
|
+
// Outside the primary constructor so code built against older kits keeps Title() and copy().
|
|
484
|
+
// onBack shows a back button: the sheet closes (onDismiss fires), then it runs.
|
|
485
|
+
var onBack: (() -> Unit)? = null
|
|
486
|
+
private set
|
|
487
|
+
var backIcon: String = "arrow-back"
|
|
488
|
+
private set
|
|
489
|
+
|
|
490
|
+
constructor(data: String, onBack: (() -> Unit)?, backIcon: String = "arrow-back") : this(data) {
|
|
491
|
+
this.onBack = onBack
|
|
492
|
+
this.backIcon = backIcon
|
|
493
|
+
}
|
|
494
|
+
}
|
|
334
495
|
|
|
335
496
|
data class Custom(
|
|
336
497
|
val content: @Composable () -> Unit
|
|
@@ -8,8 +8,9 @@ import androidx.compose.foundation.border
|
|
|
8
8
|
import androidx.compose.foundation.clickable
|
|
9
9
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
10
10
|
import androidx.compose.foundation.layout.Box
|
|
11
|
+
import androidx.compose.foundation.layout.absolutePadding
|
|
11
12
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
12
|
-
import androidx.compose.foundation.layout.
|
|
13
|
+
import androidx.compose.foundation.layout.sizeIn
|
|
13
14
|
import androidx.compose.foundation.layout.wrapContentSize
|
|
14
15
|
import androidx.compose.runtime.*
|
|
15
16
|
import androidx.compose.ui.Alignment
|
|
@@ -26,6 +27,8 @@ import vn.momo.kits.modifier.conditional
|
|
|
26
27
|
import vn.momo.kits.modifier.noFeedbackClickable
|
|
27
28
|
import vn.momo.kits.navigation.tracking.ScreenTracker
|
|
28
29
|
import vn.momo.kits.platform.BackHandler
|
|
30
|
+
import vn.momo.kits.layout.duoModalMaxHeight
|
|
31
|
+
import vn.momo.kits.layout.duoModalMaxWidth
|
|
29
32
|
import vn.momo.kits.layout.getScreenLayout
|
|
30
33
|
|
|
31
34
|
@Composable
|
|
@@ -111,9 +114,14 @@ internal fun ModalScreen(
|
|
|
111
114
|
.noFeedbackClickable {
|
|
112
115
|
if(barrierDismissible) closeEvent()
|
|
113
116
|
}
|
|
114
|
-
// center
|
|
117
|
+
// center in the safe area, clear of the side toolbar strip and the home indicator
|
|
115
118
|
.conditional(layout.isIPhoneDuoDevice) {
|
|
116
|
-
|
|
119
|
+
absolutePadding(
|
|
120
|
+
left = layout.insetLeft,
|
|
121
|
+
top = layout.insetTop,
|
|
122
|
+
right = layout.insetRight,
|
|
123
|
+
bottom = layout.insetBottom,
|
|
124
|
+
)
|
|
117
125
|
},
|
|
118
126
|
contentAlignment = Alignment.Center
|
|
119
127
|
) {
|
|
@@ -121,6 +129,9 @@ internal fun ModalScreen(
|
|
|
121
129
|
modifier = Modifier
|
|
122
130
|
.scale(scale.value)
|
|
123
131
|
.alpha(alpha.value)
|
|
132
|
+
.conditional(layout.isIPhoneDuoDevice) {
|
|
133
|
+
sizeIn(maxWidth = layout.duoModalMaxWidth, maxHeight = layout.duoModalMaxHeight)
|
|
134
|
+
}
|
|
124
135
|
.wrapContentSize()
|
|
125
136
|
.conditional(IsShowBaseLineDebug) {
|
|
126
137
|
border(1.dp, Colors.blue_03)
|
|
@@ -20,7 +20,7 @@ import androidx.navigation.toRoute
|
|
|
20
20
|
import kotlinx.coroutines.delay
|
|
21
21
|
import kotlinx.coroutines.flow.collectLatest
|
|
22
22
|
import vn.momo.kits.application.DesignSystemConfig
|
|
23
|
-
import vn.momo.kits.application.
|
|
23
|
+
import vn.momo.kits.application.DesignSystemKits
|
|
24
24
|
import vn.momo.kits.application.LocalContext
|
|
25
25
|
import vn.momo.kits.application.LocalDesignSystemConfig
|
|
26
26
|
import vn.momo.kits.application.LocalLocalize
|
|
@@ -66,7 +66,7 @@ fun NavigationContainer(
|
|
|
66
66
|
val stackTransition = remember { StackTransitionCoordinator() }
|
|
67
67
|
val theme = remember { mutableStateOf(initialTheme) }
|
|
68
68
|
val resolvedDesignSystemConfig =
|
|
69
|
-
designSystemConfig ?: LocalDesignSystemConfig.current ?:
|
|
69
|
+
designSystemConfig ?: LocalDesignSystemConfig.current ?: DesignSystemKits.getConfigs().designSystemConfig
|
|
70
70
|
val headerBar = resolvedDesignSystemConfig?.headerBar
|
|
71
71
|
// RN parity: the theme's own header image wins; config headerBar is only the fallback.
|
|
72
72
|
val resolvedTheme = remember(theme.value, headerBar) {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
package vn.momo.kits.navigation
|
|
2
2
|
|
|
3
3
|
import androidx.compose.foundation.layout.Box
|
|
4
|
+
import androidx.compose.foundation.layout.absolutePadding
|
|
4
5
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
5
6
|
import androidx.compose.runtime.Composable
|
|
6
7
|
import androidx.compose.runtime.MutableState
|
|
8
|
+
import androidx.compose.runtime.key
|
|
7
9
|
import androidx.compose.runtime.mutableStateOf
|
|
8
10
|
import androidx.compose.runtime.staticCompositionLocalOf
|
|
9
11
|
import androidx.compose.ui.Alignment
|
|
@@ -19,6 +21,8 @@ import kotlinx.coroutines.launch
|
|
|
19
21
|
import kotlinx.serialization.Serializable
|
|
20
22
|
import vn.momo.kits.components.PopupAction
|
|
21
23
|
import vn.momo.kits.components.PopupNotify
|
|
24
|
+
import vn.momo.kits.layout.getScreenLayout
|
|
25
|
+
import vn.momo.kits.modifier.conditional
|
|
22
26
|
import vn.momo.kits.navigation.BottomHeader.*
|
|
23
27
|
import vn.momo.kits.navigation.component.SnackBar
|
|
24
28
|
import vn.momo.maxapi.IMaxApi
|
|
@@ -113,9 +117,10 @@ class Navigator(
|
|
|
113
117
|
}
|
|
114
118
|
}
|
|
115
119
|
private suspend fun dismissOverplay(isDelay: Boolean = true) {
|
|
120
|
+
val dismissing = OverplayComponentRegistry.current()
|
|
116
121
|
OverplayComponentRegistry.clear()
|
|
117
122
|
if (isDelay) delay(300L.milliseconds)
|
|
118
|
-
OverplayComponentRegistry.hardClearAfterDismiss()
|
|
123
|
+
OverplayComponentRegistry.hardClearAfterDismiss(dismissing)
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
private suspend fun dismissScreen() {
|
|
@@ -182,9 +187,10 @@ class Navigator(
|
|
|
182
187
|
|
|
183
188
|
fun hideSnackBar() {
|
|
184
189
|
scope.launch {
|
|
190
|
+
val dismissing = OverplayComponentRegistry.current()
|
|
185
191
|
OverplayComponentRegistry.clear()
|
|
186
192
|
delay(250L.milliseconds)
|
|
187
|
-
OverplayComponentRegistry.hardClearAfterDismiss()
|
|
193
|
+
OverplayComponentRegistry.hardClearAfterDismiss(dismissing)
|
|
188
194
|
}
|
|
189
195
|
}
|
|
190
196
|
|
|
@@ -358,42 +364,59 @@ object OverplayComponentRegistry {
|
|
|
358
364
|
requestClose = null
|
|
359
365
|
}
|
|
360
366
|
|
|
367
|
+
internal fun current(): OverplayComponent? = currentOverlayComponent.value
|
|
368
|
+
|
|
369
|
+
// An overlay shown from the dismissed one's onDismiss, before the delayed clear, stays.
|
|
370
|
+
internal fun hardClearAfterDismiss(dismissing: OverplayComponent?) {
|
|
371
|
+
if (currentOverlayComponent.value === dismissing) hardClearAfterDismiss()
|
|
372
|
+
}
|
|
373
|
+
|
|
361
374
|
@Composable
|
|
362
375
|
internal fun OverlayComponent(){
|
|
363
376
|
val overplay = currentOverlayComponent.value ?: return
|
|
364
377
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
+
// every registration is its own composition (params are new instances), as SwiftUI's `.id(item.id)`:
|
|
379
|
+
// a sheet shown from another one's close must not inherit its finished animation and effects
|
|
380
|
+
key(overplay) {
|
|
381
|
+
when (val params = overplay.params) {
|
|
382
|
+
is OverplayComponentParams.BottomSheet -> {
|
|
383
|
+
BottomSheet(
|
|
384
|
+
content = overplay.content,
|
|
385
|
+
header = params.bottomSheetHeader ?: Title(),
|
|
386
|
+
isSurface = params.isSurface,
|
|
387
|
+
barrierDismissible = params.barrierDismissible,
|
|
388
|
+
draggable = params.draggable,
|
|
389
|
+
useBottomInset = params.useBottomInset,
|
|
390
|
+
footerComponent = params.footerComponent,
|
|
391
|
+
onDismiss = params.onDismiss
|
|
392
|
+
)
|
|
393
|
+
}
|
|
378
394
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
395
|
+
is OverplayComponentParams.Modal -> {
|
|
396
|
+
ModalScreen(
|
|
397
|
+
content = overplay.content,
|
|
398
|
+
barrierDismissible = params.barrierDismissible,
|
|
399
|
+
onDismiss = params.onDismiss
|
|
400
|
+
)
|
|
401
|
+
}
|
|
386
402
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
403
|
+
is OverplayComponentParams.SnackBar -> {
|
|
404
|
+
val layout = getScreenLayout()
|
|
405
|
+
Box(
|
|
406
|
+
modifier = Modifier
|
|
407
|
+
.fillMaxSize()
|
|
408
|
+
// iPhone Duo: clear of the side toolbar strip, like the screen content
|
|
409
|
+
.conditional(layout.isIPhoneDuo) {
|
|
410
|
+
absolutePadding(left = layout.insetLeft, right = layout.insetRight)
|
|
411
|
+
},
|
|
412
|
+
contentAlignment = Alignment.BottomCenter
|
|
413
|
+
) {
|
|
414
|
+
overplay.content()
|
|
415
|
+
}
|
|
393
416
|
}
|
|
394
|
-
}
|
|
395
417
|
|
|
396
|
-
|
|
418
|
+
null -> {}
|
|
419
|
+
}
|
|
397
420
|
}
|
|
398
421
|
}
|
|
399
422
|
}
|
package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt
CHANGED
|
@@ -39,8 +39,6 @@ internal val DUO_GROUP_GAP = Spacing.M
|
|
|
39
39
|
private val DEFAULT_TOOLBAR_HEIGHT = DuoMetrics.controlSize + Spacing.M + DuoMetrics.itemPitch * 2
|
|
40
40
|
// Back, then the single button the toolbar folds into
|
|
41
41
|
private val COMPRESSED_TOOLBAR_HEIGHT = DuoMetrics.controlSize * 2 + Spacing.M
|
|
42
|
-
// Back alone, while a rail opened from the compressed state takes the toolbar slot
|
|
43
|
-
private val BACK_ONLY_HEIGHT = DuoMetrics.controlSize
|
|
44
42
|
|
|
45
43
|
// Port of RN DuoBarCompression (HIG, Designing for iPhone Duo: Vertical controls):
|
|
46
44
|
// - both fit: both stay whole
|
|
@@ -78,7 +76,8 @@ internal fun rememberDuoBarCompression(
|
|
|
78
76
|
expandTabBar = { expanded = DuoBar.TAB_BAR },
|
|
79
77
|
setToolbarHeight = setToolbarHeight,
|
|
80
78
|
)
|
|
81
|
-
opened
|
|
79
|
+
// an opened rail always shows, even when only the group gap is short: it closes once a tab is picked
|
|
80
|
+
opened == DuoBar.TAB_BAR -> DuoBarCompression(
|
|
82
81
|
toolbarCompressed = true,
|
|
83
82
|
toolbarHidden = true,
|
|
84
83
|
expandToolbar = { opened = DuoBar.TOOLBAR },
|