@momo-kits/native-kits 0.167.1-duo-search.3-debug → 0.167.1-duo-search.22-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 +12 -9
- package/README.md +2 -2
- package/compose/build.gradle.kts +1 -1
- package/compose/compose.podspec +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +228 -73
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +23 -13
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +4 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +10 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +3 -17
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +95 -18
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +56 -37
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +28 -19
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +62 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +17 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +39 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +4 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +33 -26
- package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +38 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +11 -3
- package/gradle.properties +1 -1
- package/ios/Application/MaxApi.swift +30 -3
- package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
- package/ios/Input/InputSearch.swift +5 -2
- package/ios/Layout/GridContext.swift +67 -3
- package/ios/Layout/ScreenLayout.swift +101 -33
- package/ios/Navigation/BottomSheet.swift +83 -36
- package/ios/Navigation/BottomTab/BottomTab.swift +165 -20
- package/ios/Navigation/BottomTab/BottomTabBar.swift +3 -0
- package/ios/Navigation/Component/FloatingButton.swift +29 -51
- package/ios/Navigation/Component/Header.swift +70 -18
- package/ios/Navigation/Component/HeaderBackground.swift +0 -1
- package/ios/Navigation/Component/HeaderRight.swift +107 -68
- package/ios/Navigation/Component/SideBar.swift +15 -8
- package/ios/Navigation/Component/SnackBar.swift +3 -3
- package/ios/Navigation/HostBackGestureBridge.swift +27 -6
- package/ios/Navigation/ModalScreen.swift +2 -2
- package/ios/Navigation/Navigation.swift +14 -0
- package/ios/Navigation/NavigationContainer.swift +29 -58
- package/ios/Navigation/Navigator.swift +132 -181
- package/ios/Navigation/StackScreen.swift +217 -65
- package/ios/Navigation/StackTransition.swift +400 -0
- package/ios/Navigation/SwipeBackHaptic.swift +33 -0
- package/ios/native-kits.podspec +1 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/package.json +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
- package/ios/Application/ComposeApi.swift +0 -23
- package/ios/Application/LegacyHeader.swift +0 -212
- package/ios/Application/Screen.swift +0 -251
|
@@ -67,7 +67,7 @@ data class Theme(
|
|
|
67
67
|
return !assets.headerBackground.isNullOrEmpty()
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
-
val
|
|
70
|
+
private val lightBaseTheme = Theme(
|
|
71
71
|
dark = false,
|
|
72
72
|
colors = ColorScheme(
|
|
73
73
|
primary = Colors.pink_03,
|
|
@@ -117,14 +117,14 @@ val defaultTheme = Theme(
|
|
|
117
117
|
container = Colors.blue_08
|
|
118
118
|
)
|
|
119
119
|
),
|
|
120
|
-
font = "
|
|
120
|
+
font = "SFProText",
|
|
121
121
|
assets = ThemeAssets(
|
|
122
122
|
headerBackground = null
|
|
123
123
|
)
|
|
124
124
|
)
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
val
|
|
127
|
+
private val darkBaseTheme = Theme(
|
|
128
128
|
dark = true,
|
|
129
129
|
colors = ColorScheme(
|
|
130
130
|
primary = Colors.pink_04,
|
|
@@ -174,13 +174,19 @@ val darkTheme = Theme(
|
|
|
174
174
|
container = Colors.blue_08
|
|
175
175
|
)
|
|
176
176
|
),
|
|
177
|
-
font = "
|
|
177
|
+
font = "SFProText",
|
|
178
178
|
assets = ThemeAssets(
|
|
179
179
|
headerBackground = null
|
|
180
180
|
)
|
|
181
181
|
)
|
|
182
182
|
|
|
183
183
|
|
|
184
|
+
val defaultTheme: Theme
|
|
185
|
+
get() = lightBaseTheme.copy(font = defaultFontFamily)
|
|
186
|
+
|
|
187
|
+
val darkTheme: Theme
|
|
188
|
+
get() = darkBaseTheme.copy(font = defaultFontFamily)
|
|
189
|
+
|
|
184
190
|
val AppTheme = staticCompositionLocalOf { defaultTheme }
|
|
185
191
|
|
|
186
192
|
val AppStatusBar = staticCompositionLocalOf { 0.dp }
|
|
@@ -121,31 +121,17 @@ private val hostFontFiles: Map<String, String> = mapOf(
|
|
|
121
121
|
)
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
/**
|
|
125
|
-
* Font mặc định mới, chỉ áp cho user nằm trong whitelist design system ([KitDesignSystem]).
|
|
126
|
-
* Ngoài whitelist thì quay về [LEGACY_DEFAULT_FONT_FAMILY] như trước.
|
|
127
|
-
*/
|
|
128
124
|
private const val NEW_DEFAULT_FONT_FAMILY = "MomoTrustSans"
|
|
129
125
|
private const val LEGACY_DEFAULT_FONT_FAMILY = "SFProText"
|
|
130
126
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
* (commit header title 5bfde6251); truoc do header lay font theo theme, nen ngoai
|
|
134
|
-
* whitelist ca hai deu ha ve [LEGACY_DEFAULT_FONT_FAMILY].
|
|
135
|
-
*/
|
|
136
|
-
private val newFontFamilies = setOf(NEW_DEFAULT_FONT_FAMILY, "MoMoTrustDisplay")
|
|
127
|
+
internal val defaultFontFamily: String
|
|
128
|
+
get() = if (KitDesignSystem.isWhiteList) NEW_DEFAULT_FONT_FAMILY else LEGACY_DEFAULT_FONT_FAMILY
|
|
137
129
|
|
|
138
130
|
@Composable
|
|
139
131
|
fun getFontFamily(fontFamily: String, fontWeight: FontWeight? = FontWeight.Normal): FontFamily {
|
|
140
132
|
val isWhiteList = KitDesignSystem.isWhiteList
|
|
141
|
-
val resolvedFamily =
|
|
142
|
-
if (fontFamily in newFontFamilies && !isWhiteList) {
|
|
143
|
-
LEGACY_DEFAULT_FONT_FAMILY
|
|
144
|
-
} else {
|
|
145
|
-
fontFamily
|
|
146
|
-
}
|
|
147
133
|
val defaultFile = if (isWhiteList) NEW_DEFAULT_FONT_FILE else LEGACY_DEFAULT_FONT_FILE
|
|
148
|
-
val key = "$
|
|
134
|
+
val key = "$fontFamily-${fontWeight?.weight}"
|
|
149
135
|
val fileName = hostFontFiles[key] ?: defaultFile
|
|
150
136
|
return hostFontFamily(fileName)
|
|
151
137
|
?: hostFontFamily(defaultFile)
|
|
@@ -32,6 +32,10 @@ data class SideBarLayout(
|
|
|
32
32
|
val bottom: Dp,
|
|
33
33
|
)
|
|
34
34
|
|
|
35
|
+
// Room the header row keeps from each side for the status bar clock and battery clusters.
|
|
36
|
+
@Immutable
|
|
37
|
+
data class StatusBarClearance(val left: Dp, val right: Dp)
|
|
38
|
+
|
|
35
39
|
@Immutable
|
|
36
40
|
data class ScreenLayout(
|
|
37
41
|
val width: Dp,
|
|
@@ -53,6 +57,8 @@ data class ScreenLayout(
|
|
|
53
57
|
// Portrait iPhone Duo puts the status bar in the side strip, about twice as tall as the strip is wide.
|
|
54
58
|
// Stored so it survives consumeHorizontalInsets().
|
|
55
59
|
val duoTopInset: Dp = insetTop,
|
|
60
|
+
// iPhone Duo inner display in portrait: the header row shares the status bar band, between its clusters.
|
|
61
|
+
val statusBarClearance: StatusBarClearance? = null,
|
|
56
62
|
) {
|
|
57
63
|
val headerBarHeight: Dp get() = headerHeight - headerStatusBarHeight
|
|
58
64
|
}
|
|
@@ -76,6 +82,10 @@ internal object DuoMetrics {
|
|
|
76
82
|
// Outer display in landscape has no status bar in the strip: controls keep this far from
|
|
77
83
|
// both ends unless a reserved region (the camera at either end) asks for more.
|
|
78
84
|
val edgeInset = 24.dp
|
|
85
|
+
// Inner display in portrait: the clock and connectivity cluster sits at the top right corner
|
|
86
|
+
// (x 550–643 of 669, clock digits centered on y 47); its reserved region is taller than the glyphs.
|
|
87
|
+
val innerStatusBarCenterY = 47.dp
|
|
88
|
+
val innerStatusBarWidth = 120.dp
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
// Widest short side of any single-display iPhone.
|
|
@@ -97,9 +107,19 @@ fun getScreenLayout(): ScreenLayout =
|
|
|
97
107
|
internal fun calculateScreenLayout(topInset: Dp, parent: ScreenLayout? = null): ScreenLayout {
|
|
98
108
|
val isPhone = isPhoneIdiom()
|
|
99
109
|
if (parent != null) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
110
|
+
// the parent's row center reproduces its own centered geometry
|
|
111
|
+
val titleCenterY = parent.headerStatusBarHeight + parent.headerBarHeight / 2
|
|
112
|
+
val statusBarClearance = parent.statusBarClearance?.takeIf { topInset >= STATUS_BAR_MIN_INSET }
|
|
113
|
+
val (headerHeight, headerStatusBarHeight) = if (statusBarClearance != null) {
|
|
114
|
+
statusBarRowGeometry()
|
|
115
|
+
} else {
|
|
116
|
+
headerGeometry(topInset, isPhone, parent.width, parent.height, parent.sideBar != null, titleCenterY)
|
|
117
|
+
}
|
|
118
|
+
return parent.copy(
|
|
119
|
+
headerHeight = headerHeight,
|
|
120
|
+
headerStatusBarHeight = headerStatusBarHeight,
|
|
121
|
+
statusBarClearance = statusBarClearance,
|
|
122
|
+
)
|
|
103
123
|
}
|
|
104
124
|
|
|
105
125
|
val density = LocalDensity.current
|
|
@@ -139,32 +159,72 @@ internal fun calculateScreenLayout(topInset: Dp, parent: ScreenLayout? = null):
|
|
|
139
159
|
internal fun ScreenLayout.consumeHorizontalInsets(): ScreenLayout =
|
|
140
160
|
copy(width = contentWidth, insetLeft = 0.dp, insetRight = 0.dp, sideBar = null)
|
|
141
161
|
|
|
142
|
-
|
|
162
|
+
// titleCenterY: window y the title row centers on for the outer display in landscape.
|
|
163
|
+
private fun headerGeometry(
|
|
164
|
+
topInset: Dp,
|
|
165
|
+
isPhone: Boolean,
|
|
166
|
+
width: Dp,
|
|
167
|
+
height: Dp,
|
|
168
|
+
hasSideBar: Boolean,
|
|
169
|
+
titleCenterY: Dp,
|
|
170
|
+
): Pair<Dp, Dp> {
|
|
143
171
|
val headerHeight = topInset + HEADER_HEIGHT.dp
|
|
144
172
|
if (!isPhone) return headerHeight to topInset
|
|
145
|
-
if (hasSideBar && topInset < STATUS_BAR_MIN_INSET
|
|
146
|
-
|
|
147
|
-
|
|
173
|
+
if (hasSideBar && topInset < STATUS_BAR_MIN_INSET) {
|
|
174
|
+
if (height > width && width < DuoMetrics.innerMinHeight) {
|
|
175
|
+
// outer display: the row starts below the top so the title centers on the camera
|
|
176
|
+
return DuoMetrics.outerHeaderHeight to DuoMetrics.outerCameraCenterY * 2 - DuoMetrics.outerHeaderHeight
|
|
177
|
+
}
|
|
178
|
+
if (width > height && height < DuoMetrics.innerMinHeight) {
|
|
179
|
+
// reaches 8pt below a side bar control centered on the row
|
|
180
|
+
val centered = maxOf(IPHONE_HEADER_MIN_HEIGHT, titleCenterY + DuoMetrics.controlSize / 2 + Spacing.S)
|
|
181
|
+
return centered to maxOf(0.dp, titleCenterY * 2 - centered)
|
|
182
|
+
}
|
|
148
183
|
}
|
|
149
184
|
return maxOf(headerHeight, IPHONE_HEADER_MIN_HEIGHT) to if (topInset < STATUS_BAR_MIN_INSET) 0.dp else topInset
|
|
150
185
|
}
|
|
151
186
|
|
|
152
|
-
|
|
187
|
+
// Centered on the clock digits; the cluster's region center sits above them.
|
|
188
|
+
private fun statusBarRowGeometry(): Pair<Dp, Dp> = DuoMetrics.innerStatusBarCenterY * 2 to 0.dp
|
|
189
|
+
|
|
190
|
+
private data class StripOcclusion(val top: Dp, val topCenter: Dp, val bottom: Dp)
|
|
153
191
|
|
|
154
192
|
// How far system regions reach into a side strip from its top and from its bottom end
|
|
155
|
-
// (e.g. the camera, which sits at either end depending on the rotation)
|
|
193
|
+
// (e.g. the camera, which sits at either end depending on the rotation), and the center y
|
|
194
|
+
// of the region reaching furthest from the top.
|
|
156
195
|
private fun stripOcclusion(regions: List<ReservedRegion>, stripLeft: Dp, stripRight: Dp, windowHeight: Dp): StripOcclusion {
|
|
157
196
|
var top = 0.dp
|
|
197
|
+
var topCenter = 0.dp
|
|
158
198
|
var bottom = 0.dp
|
|
159
199
|
regions.forEach { region ->
|
|
160
200
|
if (region.x >= stripRight || region.x + region.width <= stripLeft) return@forEach
|
|
161
|
-
|
|
162
|
-
|
|
201
|
+
val centerY = region.y + region.height / 2
|
|
202
|
+
if (centerY < windowHeight / 2) {
|
|
203
|
+
if (region.y + region.height > top) {
|
|
204
|
+
top = region.y + region.height
|
|
205
|
+
topCenter = centerY
|
|
206
|
+
}
|
|
163
207
|
} else {
|
|
164
208
|
bottom = maxOf(bottom, windowHeight - region.y)
|
|
165
209
|
}
|
|
166
210
|
}
|
|
167
|
-
return StripOcclusion(top, bottom)
|
|
211
|
+
return StripOcclusion(top, topCenter, bottom)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// How far the clock and battery clusters of a status bar on the top edge reach in from each side;
|
|
215
|
+
// null without reserved regions there.
|
|
216
|
+
private fun statusBarClusters(regions: List<ReservedRegion>, windowWidth: Dp, topInset: Dp): StatusBarClearance? {
|
|
217
|
+
val midX = windowWidth / 2
|
|
218
|
+
val clusters = regions.filter {
|
|
219
|
+
it.y < topInset && it.y + it.height <= topInset && (it.x + it.width <= midX || it.x >= midX)
|
|
220
|
+
}
|
|
221
|
+
if (clusters.isEmpty()) return null
|
|
222
|
+
var left = 0.dp
|
|
223
|
+
var right = 0.dp
|
|
224
|
+
clusters.forEach {
|
|
225
|
+
if (it.x < midX) left = maxOf(left, it.x + it.width) else right = maxOf(right, windowWidth - it.x)
|
|
226
|
+
}
|
|
227
|
+
return StatusBarClearance(left, right)
|
|
168
228
|
}
|
|
169
229
|
|
|
170
230
|
// Slab iPhones report equal side insets in landscape; iPhone Duo moves the status bar to one side.
|
|
@@ -187,24 +247,36 @@ private fun screenLayoutOf(
|
|
|
187
247
|
reservedRegions: List<ReservedRegion>,
|
|
188
248
|
): ScreenLayout {
|
|
189
249
|
val isIPhoneDuo = isPhone && isIPhoneDuoWindow(width, height, asymmetricInsets)
|
|
250
|
+
val isInnerPortrait = height > width && width >= DuoMetrics.innerMinHeight
|
|
251
|
+
val statusBarClearance = if (isIPhoneDuo && isInnerPortrait && topInset >= STATUS_BAR_MIN_INSET) {
|
|
252
|
+
statusBarClusters(reservedRegions, width, topInset)
|
|
253
|
+
?: StatusBarClearance(left = 0.dp, right = DuoMetrics.innerStatusBarWidth + Spacing.S)
|
|
254
|
+
} else {
|
|
255
|
+
null
|
|
256
|
+
}
|
|
190
257
|
|
|
191
258
|
var sideBar: SideBarLayout? = null
|
|
259
|
+
var titleCenterY = 0.dp
|
|
192
260
|
val sideInset = maxOf(insetLeft, insetRight)
|
|
193
261
|
// narrower strips cannot host a control 12pt from the inner edge
|
|
194
262
|
if (isIPhoneDuo && sideInset >= DuoMetrics.controlSize + Spacing.M) {
|
|
263
|
+
val edge = if (insetRight >= insetLeft) SideBarEdge.RIGHT else SideBarEdge.LEFT
|
|
264
|
+
val stripLeft = if (edge == SideBarEdge.RIGHT) width - sideInset else 0.dp
|
|
265
|
+
// clear of the camera and of the status bar cluster, which grows for calls and Live
|
|
266
|
+
// Activities; region frames already include the system's margin
|
|
267
|
+
val occlusion = stripOcclusion(reservedRegions, stripLeft, stripLeft + sideInset, height)
|
|
195
268
|
// UIKit reports no usable status bar frame here (a 2pt top strip), so the spot is inferred like RN
|
|
196
269
|
val top = when {
|
|
270
|
+
statusBarClearance != null -> statusBarRowGeometry().first + Spacing.M
|
|
197
271
|
topInset >= STATUS_BAR_MIN_INSET -> topInset + HEADER_HEIGHT.dp + Spacing.M
|
|
198
272
|
width <= height -> DuoMetrics.firstItemTop
|
|
199
273
|
height >= DuoMetrics.innerMinHeight -> DuoMetrics.firstItemTopLandscape
|
|
200
274
|
// outer display in landscape: no status bar in the strip
|
|
201
275
|
else -> DuoMetrics.edgeInset
|
|
202
276
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
// Activities; region frames already include the system's margin
|
|
207
|
-
val occlusion = stripOcclusion(reservedRegions, stripLeft, stripLeft + sideInset, height)
|
|
277
|
+
// outer display in landscape: the title row centers on the camera when it sits at the
|
|
278
|
+
// top end, otherwise on the back button
|
|
279
|
+
titleCenterY = if (occlusion.top > top) occlusion.topCenter else top + DuoMetrics.controlSize / 2
|
|
208
280
|
sideBar = SideBarLayout(
|
|
209
281
|
edge = edge,
|
|
210
282
|
width = sideInset,
|
|
@@ -212,7 +284,11 @@ private fun screenLayoutOf(
|
|
|
212
284
|
bottom = maxOf(DuoMetrics.edgeInset, occlusion.bottom),
|
|
213
285
|
)
|
|
214
286
|
}
|
|
215
|
-
val (headerHeight, headerStatusBarHeight) =
|
|
287
|
+
val (headerHeight, headerStatusBarHeight) = if (statusBarClearance != null) {
|
|
288
|
+
statusBarRowGeometry()
|
|
289
|
+
} else {
|
|
290
|
+
headerGeometry(topInset, isPhone, width, height, sideBar != null, titleCenterY)
|
|
291
|
+
}
|
|
216
292
|
|
|
217
293
|
return ScreenLayout(
|
|
218
294
|
width = width,
|
|
@@ -228,5 +304,6 @@ private fun screenLayoutOf(
|
|
|
228
304
|
sideBar = sideBar,
|
|
229
305
|
isIPhoneDuoDevice = isDuoDevice,
|
|
230
306
|
duoTopInset = maxOf(topInset, if (height > width) sideInset * 2 else 0.dp),
|
|
307
|
+
statusBarClearance = statusBarClearance,
|
|
231
308
|
)
|
|
232
309
|
}
|
|
@@ -30,6 +30,9 @@ class Navigation(
|
|
|
30
30
|
headerType: HeaderType? = null,
|
|
31
31
|
scrollData: ScrollData? = null,
|
|
32
32
|
backgroundColor: Color? = null,
|
|
33
|
+
// AI-GENERATED START: custom screen background
|
|
34
|
+
backgroundComponent: (@Composable () -> Unit)? = null,
|
|
35
|
+
// AI-GENERATED END: custom screen background
|
|
33
36
|
tintColor: Color? = null,
|
|
34
37
|
footerComponent: (@Composable () -> Unit)? = null,
|
|
35
38
|
footerTransparent: Boolean? = null,
|
|
@@ -48,6 +51,9 @@ class Navigation(
|
|
|
48
51
|
headerType = headerType ?: options.headerType,
|
|
49
52
|
scrollData = scrollData ?: options.scrollData,
|
|
50
53
|
backgroundColor = backgroundColor ?: options.backgroundColor,
|
|
54
|
+
// AI-GENERATED START: custom screen background
|
|
55
|
+
backgroundComponent = backgroundComponent ?: options.backgroundComponent,
|
|
56
|
+
// AI-GENERATED END: custom screen background
|
|
51
57
|
tintColor = tintColor ?: options.tintColor,
|
|
52
58
|
footerComponent = footerComponent ?: options.footerComponent,
|
|
53
59
|
footerTransparent = footerTransparent ?: options.footerTransparent,
|
|
@@ -85,6 +91,10 @@ data class NavigationOptions(
|
|
|
85
91
|
val headerType: HeaderType = HeaderType.Default(),
|
|
86
92
|
val scrollData: ScrollData = ScrollData(),
|
|
87
93
|
val backgroundColor: Color? = null,
|
|
94
|
+
// AI-GENERATED START: custom screen background (SwiftUI `NavigationOptions.background`)
|
|
95
|
+
/** Drawn instead of `backgroundColor` over the whole screen: under the header, outside the keyboard padding. */
|
|
96
|
+
val backgroundComponent: @Composable (() -> Unit)? = null,
|
|
97
|
+
// AI-GENERATED END: custom screen background (SwiftUI `NavigationOptions.background`)
|
|
88
98
|
val tintColor: Color? = null,
|
|
89
99
|
val footerComponent: @Composable (() -> Unit)? = null,
|
|
90
100
|
val footerTransparent: Boolean = false,
|
|
@@ -55,6 +55,12 @@ class Navigator(
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
fun onBackSafe(callBack: (() -> Unit)? = null) {
|
|
58
|
+
// An open modal or bottom sheet takes the back first, as its BackHandler does for gestures.
|
|
59
|
+
val overplayType = OverplayComponentRegistry.getOverplayType()
|
|
60
|
+
if (overplayType != null && overplayType != OverplayComponentType.SNACK_BAR) {
|
|
61
|
+
pop(1, callBack)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
58
64
|
tabBackHandler?.let {
|
|
59
65
|
it()
|
|
60
66
|
return
|
|
@@ -83,6 +83,7 @@ import vn.momo.kits.utils.horizontalSafeAreaPadding
|
|
|
83
83
|
import vn.momo.kits.layout.LocalScreenLayout
|
|
84
84
|
import vn.momo.kits.layout.ScreenLayout
|
|
85
85
|
import vn.momo.kits.layout.consumeHorizontalInsets
|
|
86
|
+
import vn.momo.kits.layout.DuoMetrics
|
|
86
87
|
import vn.momo.kits.layout.getScreenLayout
|
|
87
88
|
import kotlin.time.Clock
|
|
88
89
|
import kotlin.time.Duration.Companion.milliseconds
|
|
@@ -213,48 +214,62 @@ internal fun StackScreen(
|
|
|
213
214
|
LocalFooterHeightPx provides footerHeightPx,
|
|
214
215
|
LocalHeaderRightWidthPx provides headerRightWidthPx
|
|
215
216
|
) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
.
|
|
220
|
-
|
|
221
|
-
|
|
217
|
+
// AI-GENERATED START: custom screen background, full window and outside the keyboard padding
|
|
218
|
+
Box(Modifier.fillMaxSize()) {
|
|
219
|
+
options.backgroundComponent?.let { background ->
|
|
220
|
+
Box(Modifier.fillMaxSize()) { background() }
|
|
221
|
+
}
|
|
222
|
+
// AI-GENERATED END: custom screen background, full window and outside the keyboard padding
|
|
223
|
+
Box(
|
|
224
|
+
Modifier
|
|
225
|
+
.fillMaxSize()
|
|
226
|
+
// AI-GENERATED START: custom background replaces the colour
|
|
227
|
+
.background(
|
|
228
|
+
if (options.backgroundComponent != null) Color.Transparent
|
|
229
|
+
else options.backgroundColor ?: AppTheme.current.colors.background.default
|
|
230
|
+
)
|
|
231
|
+
// AI-GENERATED END: custom background replaces the colour
|
|
232
|
+
.conditional(options.keyboardOptions.keyboardShouldPersistTaps) {
|
|
233
|
+
hideKeyboardOnTap()
|
|
234
|
+
}
|
|
235
|
+
.conditional(options.keyboardOptions.useAvoidKeyboard && supportsImePadding()) {
|
|
236
|
+
imePadding()
|
|
237
|
+
}
|
|
238
|
+
) {
|
|
239
|
+
Box(Modifier.zIndex(1f)) {
|
|
240
|
+
HeaderBackground()
|
|
222
241
|
}
|
|
223
|
-
|
|
224
|
-
|
|
242
|
+
|
|
243
|
+
Box(Modifier.zIndex(4f).graphicsLayer { translationY = -headerHide.value }) {
|
|
244
|
+
Header(onBackHandler)
|
|
225
245
|
}
|
|
226
|
-
) {
|
|
227
|
-
Box(Modifier.zIndex(1f)) {
|
|
228
|
-
HeaderBackground()
|
|
229
|
-
}
|
|
230
246
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
247
|
+
HeaderSideBar(
|
|
248
|
+
bottomInset = maxOf(
|
|
249
|
+
bottomBarsHeight(isBottomTabChild) + Spacing.XL,
|
|
250
|
+
if (isBottomTabChild) LocalDuoRailReserve.current else 0.dp,
|
|
251
|
+
),
|
|
252
|
+
modifier = Modifier.zIndex(4f),
|
|
253
|
+
)
|
|
234
254
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (isBottomTabChild) LocalDuoRailReserve.current else 0.dp,
|
|
239
|
-
),
|
|
240
|
-
modifier = Modifier.zIndex(4f),
|
|
241
|
-
)
|
|
255
|
+
Column(Modifier.zIndex(5f).horizontalSafeAreaPadding()) {
|
|
256
|
+
SearchAnimated(snapState = snapState)
|
|
257
|
+
}
|
|
242
258
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
259
|
+
Column(Modifier.zIndex(2f).fillMaxSize()) {
|
|
260
|
+
MainContent(content = content, isBottomTabRoot = isBottomTabRoot, isBottomTabChild = isBottomTabChild)
|
|
261
|
+
FooterContent()
|
|
262
|
+
}
|
|
246
263
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
264
|
+
Box(Modifier.zIndex(6f).horizontalSafeAreaPadding()) {
|
|
265
|
+
FloatingContent(isBottomTabChild = isBottomTabChild)
|
|
266
|
+
}
|
|
251
267
|
|
|
252
|
-
|
|
253
|
-
FloatingContent(isBottomTabChild = isBottomTabChild)
|
|
268
|
+
OverplayView(isBottomTabChild = isBottomTabChild, id = id)
|
|
254
269
|
}
|
|
255
|
-
|
|
256
|
-
OverplayView(isBottomTabChild = isBottomTabChild, id = id)
|
|
270
|
+
// AI-GENERATED START: custom screen background, full window and outside the keyboard padding
|
|
257
271
|
}
|
|
272
|
+
// AI-GENERATED END: custom screen background, full window and outside the keyboard padding
|
|
258
273
|
}
|
|
259
274
|
}
|
|
260
275
|
|
|
@@ -431,9 +446,10 @@ fun Footer(
|
|
|
431
446
|
|
|
432
447
|
val backgroundColor = if (transparent) Color.Transparent else AppTheme.current.colors.background.surface
|
|
433
448
|
|
|
449
|
+
// inset as a whole, not padded inside, so the surface and its shadow stay off the iPhone Duo side bar
|
|
434
450
|
Box(Modifier.onGloballyPositioned {
|
|
435
451
|
if (footerHeightPx.intValue != it.size.height) footerHeightPx.intValue = it.size.height
|
|
436
|
-
}) {
|
|
452
|
+
}.horizontalSafeAreaPadding()) {
|
|
437
453
|
Box(
|
|
438
454
|
Modifier
|
|
439
455
|
.fillMaxWidth()
|
|
@@ -441,7 +457,6 @@ fun Footer(
|
|
|
441
457
|
.conditional(IsShowBaseLineDebug) {
|
|
442
458
|
border(1.dp, Colors.blue_03)
|
|
443
459
|
}
|
|
444
|
-
.horizontalSafeAreaPadding()
|
|
445
460
|
.padding(
|
|
446
461
|
top = if (useMargin) Spacing.S else 0.dp,
|
|
447
462
|
start = if (useMargin) Spacing.M else 0.dp,
|
|
@@ -494,10 +509,14 @@ internal fun SearchAnimated(
|
|
|
494
509
|
// collapsed, the search is centered in the bar row
|
|
495
510
|
val minTopPadding = screenLayout.headerStatusBarHeight + (screenLayout.headerBarHeight - HEADER_HEIGHT.dp) / 2
|
|
496
511
|
val maxTopPadding = screenLayout.headerHeight
|
|
512
|
+
// collapsed on the iPhone Duo status bar row, the search stays between its clusters (48pt back there)
|
|
513
|
+
val clearance = screenLayout.statusBarClearance
|
|
514
|
+
val backWidth = if (clearance != null && !useSideBar) DuoMetrics.controlSize else 28.dp
|
|
497
515
|
val minStartPadding = Spacing.M
|
|
498
|
-
val maxStartPadding =
|
|
516
|
+
val maxStartPadding = (clearance?.left ?: 0.dp) +
|
|
517
|
+
if (options.hiddenBack || useSideBar) Spacing.M else backWidth + Spacing.M + Spacing.S
|
|
499
518
|
val minEndPadding = Spacing.M
|
|
500
|
-
val maxEndPadding = when {
|
|
519
|
+
val maxEndPadding = (clearance?.right ?: 0.dp) + when {
|
|
501
520
|
options.headerRight is HeaderRight.None -> Spacing.M
|
|
502
521
|
useSideBar && options.headerRight is HeaderRight.Toolkit -> Spacing.M
|
|
503
522
|
else -> headerRightWidthDp + Spacing.M * 2
|
|
@@ -28,6 +28,7 @@ import vn.momo.kits.navigation.LocalNavigator
|
|
|
28
28
|
import vn.momo.kits.navigation.NavigationOptions
|
|
29
29
|
import vn.momo.kits.navigation.StackScreen
|
|
30
30
|
import vn.momo.kits.navigation.component.HeaderType
|
|
31
|
+
import vn.momo.kits.platform.BackHandler
|
|
31
32
|
import vn.momo.kits.platform.reportFloatingArrowBack
|
|
32
33
|
import vn.momo.kits.layout.getScreenLayout
|
|
33
34
|
|
|
@@ -50,10 +51,16 @@ fun BottomTab(
|
|
|
50
51
|
// iPhone Duo: when the side bar can't hold both bars, which one starts compressed;
|
|
51
52
|
// tapping the compressed one swaps them
|
|
52
53
|
compressionBehavior: DuoBarCompressionBehavior = DuoBarCompressionBehavior.TOOLBAR,
|
|
54
|
+
initialRouteName: String? = null,
|
|
55
|
+
onTabPress: ((index: Int, switchTab: () -> Unit) -> Unit)? = null,
|
|
56
|
+
backToInitialTab: Boolean = true,
|
|
53
57
|
) {
|
|
54
58
|
val navigation = LocalNavigation.current
|
|
55
59
|
val navigator = LocalNavigator.current
|
|
56
60
|
val navController = rememberNavController()
|
|
61
|
+
val startRoute = remember {
|
|
62
|
+
"option${items.indexOfFirst { it.name == initialRouteName }.coerceAtLeast(0)}"
|
|
63
|
+
}
|
|
57
64
|
|
|
58
65
|
// Only on new items: this recomposes on every tab switch and would drop options a tab set at runtime.
|
|
59
66
|
remember(items) {
|
|
@@ -68,15 +75,14 @@ fun BottomTab(
|
|
|
68
75
|
)
|
|
69
76
|
}
|
|
70
77
|
|
|
71
|
-
// Tab 0 keeps the seeked transition; other tabs go to the host's floating arrow. Matches RN.
|
|
72
78
|
val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
|
|
73
|
-
val
|
|
79
|
+
val onStartTab = currentRoute == null || currentRoute == startRoute
|
|
80
|
+
val isFirstTab = onStartTab || !backToInitialTab
|
|
81
|
+
val backToStartTab = { navController.popBackStack(startRoute, inclusive = false) }
|
|
74
82
|
|
|
75
83
|
DisposableEffect(isFirstTab) {
|
|
76
84
|
reportFloatingArrowBack(!isFirstTab)
|
|
77
|
-
navigator.tabBackHandler = if (isFirstTab) null else ({
|
|
78
|
-
navController.popBackStack(navController.graph.startDestinationRoute!!, false)
|
|
79
|
-
})
|
|
85
|
+
navigator.tabBackHandler = if (isFirstTab) null else ({ backToStartTab() })
|
|
80
86
|
// both live outside this composition
|
|
81
87
|
onDispose {
|
|
82
88
|
reportFloatingArrowBack(false)
|
|
@@ -90,16 +96,19 @@ fun BottomTab(
|
|
|
90
96
|
val railReserve = layout.sideBar?.takeIf { layout.isIPhoneDuo }?.let {
|
|
91
97
|
it.bottom + duoRailHeight(items.size, hasQr, compressed = compression.tabBarCompressed) + DUO_GROUP_GAP
|
|
92
98
|
} ?: 0.dp
|
|
93
|
-
val onTabSelected: (Int) -> Unit = {
|
|
94
|
-
val
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
val onTabSelected: (Int) -> Unit = { index ->
|
|
100
|
+
val switchTab = {
|
|
101
|
+
val targetRoute = "option$index"
|
|
102
|
+
if (navController.currentBackStackEntry?.destination?.route != targetRoute) {
|
|
103
|
+
// one entry per tab above the start tab, so switching never grows the back stack
|
|
104
|
+
navController.navigate(targetRoute) {
|
|
105
|
+
popUpTo(startRoute) { saveState = true }
|
|
106
|
+
launchSingleTop = true
|
|
107
|
+
restoreState = true
|
|
108
|
+
}
|
|
101
109
|
}
|
|
102
110
|
}
|
|
111
|
+
onTabPress?.invoke(index, switchTab) ?: switchTab()
|
|
103
112
|
}
|
|
104
113
|
|
|
105
114
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
|
|
@@ -112,7 +121,7 @@ fun BottomTab(
|
|
|
112
121
|
) {
|
|
113
122
|
NavHost(
|
|
114
123
|
navController = navController,
|
|
115
|
-
startDestination =
|
|
124
|
+
startDestination = startRoute
|
|
116
125
|
) {
|
|
117
126
|
items.forEachIndexed { index, item ->
|
|
118
127
|
composable(
|
|
@@ -153,11 +162,8 @@ fun BottomTab(
|
|
|
153
162
|
navigationOptions = option,
|
|
154
163
|
bottomTabIndex = index,
|
|
155
164
|
onBackHandler = {
|
|
156
|
-
if (index !=
|
|
157
|
-
|
|
158
|
-
navController.graph.startDestinationRoute!!,
|
|
159
|
-
inclusive = false
|
|
160
|
-
)
|
|
165
|
+
if (backToInitialTab && "option$index" != startRoute) {
|
|
166
|
+
backToStartTab()
|
|
161
167
|
} else {
|
|
162
168
|
navigator.pop()
|
|
163
169
|
}
|
|
@@ -168,6 +174,9 @@ fun BottomTab(
|
|
|
168
174
|
}
|
|
169
175
|
}
|
|
170
176
|
}
|
|
177
|
+
BackHandler(enabled = !onStartTab) {
|
|
178
|
+
if (backToInitialTab) backToStartTab() else navigator.pop()
|
|
179
|
+
}
|
|
171
180
|
if (layout.isIPhoneDuo) {
|
|
172
181
|
DuoTabBar(
|
|
173
182
|
layout = layout,
|