@momo-kits/native-kits 0.167.1-duo.17-debug → 0.167.1-duo.18-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 +1 -0
- package/compose/build.gradle.kts +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +52 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +7 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +54 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +17 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +25 -18
- package/ios/Layout/ScreenLayout.swift +49 -3
- package/ios/Navigation/Component/Header.swift +48 -11
- package/ios/Navigation/Component/HeaderRight.swift +34 -28
- package/ios/Navigation/Component/SideBar.swift +15 -8
- package/ios/Navigation/StackScreen.swift +13 -12
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -69,6 +69,7 @@ Publishing is automated (`publish.sh` + GitLab CI on `[ci build]` / `main`). Do
|
|
|
69
69
|
- `layout/ScreenLayout.kt`: `getScreenLayout()` returns window size, raw top/bottom + side insets, `contentWidth`, header geometry, the iPhone Duo `sideBar` and `duoTopInset`. `NavigationContainer` computes it once and provides `LocalScreenLayout`; reactive to fold/unfold and rotation (window size + `WindowInsets`). Metrics (`DuoMetrics`, 72pt iPhone header floor, 20pt status-bar threshold) mirror RN `Layout/utils.ts` — change both together.
|
|
70
70
|
- iPhone-only: everything gates on `isPhoneIdiom()` (`platform/`), never on "iOS", so iPad windows keep the regular layout. Two detectors, as in RN: `isIPhoneDuo` (window aspect < 1.6 or asymmetric side insets) drives layout; `isIPhoneDuoDevice` (short side of screen or window > 440, RN `device.ts`) drives popups, modal and bottom sheet.
|
|
71
71
|
- Header height is `headerHeight` (not `AppStatusBar + HEADER_HEIGHT`); the bar row is `headerBarHeight` and starts `headerStatusBarHeight` below the window top. When the status bar is off the top edge (iPhone landscape, iPhone Duo) `headerStatusBarHeight` is 0 so the title stays centered — except on the Duo outer display: in portrait the header is 76pt and the row starts at 14pt so the title centers on the camera punchout; in landscape the row centers on the camera when it sits at the top end of the strip (else on the back button) and the header reaches 8pt below the back button.
|
|
72
|
+
- iPhone Duo inner display in portrait (status bar on top): the header row shares the status bar band — `headerHeight` 94pt (2 × the clock digits' y 47), `headerStatusBarHeight` 0 — and `ScreenLayout.statusBarClearance` keeps it between the clock / battery clusters (from reserved regions, else 128pt on the right). Back and toolkit use the 48pt side bar controls laid out horizontally (`ToolkitLayout.SideBarRow`); the collapsed search and a centered title stay clear of the clusters. Port of momo-kits `90af33ba2`.
|
|
72
73
|
- iPhone Duo with a side strip ≥ 60pt: back + toolkit leave the bar for `HeaderSideBar` (`navigation/component/SideBar.kt`, 48pt glass controls); custom header-right stays in the bar. `sideBar.top` is 170pt in portrait, 120pt on the inner display in landscape (height ≥ 600), 24pt on the outer display in landscape, or below the header when the status bar is on top; UIKit's `statusBarFrame` is useless on Duo. Unlike RN the column is clamped above footer (24pt gap) / rail (12pt gap).
|
|
73
74
|
- Reserved regions (iOS 27.1 `reservedRegionsOfKind:`, reached through the ObjC runtime so older SDKs still build): a probe `UIView` (`platform/ReservedRegions.ios.kt` on the Compose root view; `ReservedRegionsProbe` inside SwiftUI `momoScreenLayout()`) re-reads them in `layoutSubviews`, which UIKit re-runs when they change. They push `sideBar.top` below the status cluster as it grows for calls / Live Activities, and `sideBar.bottom` (≥ 24pt) above a camera at the bottom end of the strip.
|
|
74
75
|
- Tab bar on Duo is `bottomtab/DuoTabBar.kt`: a rail in the side strip (`sideBar.bottom` above the window bottom, QR below it) or, without a side bar, a floating bar spanning 6/8 grid columns. Both overlay the content; `duoFloatingTabBarInset` pads tab content, footer and FAB.
|
package/compose/build.gradle.kts
CHANGED
|
@@ -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.
|
|
@@ -99,9 +109,17 @@ internal fun calculateScreenLayout(topInset: Dp, parent: ScreenLayout? = null):
|
|
|
99
109
|
if (parent != null) {
|
|
100
110
|
// the parent's row center reproduces its own centered geometry
|
|
101
111
|
val titleCenterY = parent.headerStatusBarHeight + parent.headerBarHeight / 2
|
|
102
|
-
val
|
|
112
|
+
val statusBarClearance = parent.statusBarClearance?.takeIf { topInset >= STATUS_BAR_MIN_INSET }
|
|
113
|
+
val (headerHeight, headerStatusBarHeight) = if (statusBarClearance != null) {
|
|
114
|
+
statusBarRowGeometry()
|
|
115
|
+
} else {
|
|
103
116
|
headerGeometry(topInset, isPhone, parent.width, parent.height, parent.sideBar != null, titleCenterY)
|
|
104
|
-
|
|
117
|
+
}
|
|
118
|
+
return parent.copy(
|
|
119
|
+
headerHeight = headerHeight,
|
|
120
|
+
headerStatusBarHeight = headerStatusBarHeight,
|
|
121
|
+
statusBarClearance = statusBarClearance,
|
|
122
|
+
)
|
|
105
123
|
}
|
|
106
124
|
|
|
107
125
|
val density = LocalDensity.current
|
|
@@ -166,6 +184,9 @@ private fun headerGeometry(
|
|
|
166
184
|
return maxOf(headerHeight, IPHONE_HEADER_MIN_HEIGHT) to if (topInset < STATUS_BAR_MIN_INSET) 0.dp else topInset
|
|
167
185
|
}
|
|
168
186
|
|
|
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
|
+
|
|
169
190
|
private data class StripOcclusion(val top: Dp, val topCenter: Dp, val bottom: Dp)
|
|
170
191
|
|
|
171
192
|
// How far system regions reach into a side strip from its top and from its bottom end
|
|
@@ -190,6 +211,22 @@ private fun stripOcclusion(regions: List<ReservedRegion>, stripLeft: Dp, stripRi
|
|
|
190
211
|
return StripOcclusion(top, topCenter, bottom)
|
|
191
212
|
}
|
|
192
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)
|
|
228
|
+
}
|
|
229
|
+
|
|
193
230
|
// Slab iPhones report equal side insets in landscape; iPhone Duo moves the status bar to one side.
|
|
194
231
|
private fun isIPhoneDuoWindow(width: Dp, height: Dp, asymmetricInsets: Boolean): Boolean {
|
|
195
232
|
val shortSide = minOf(width, height)
|
|
@@ -210,6 +247,13 @@ private fun screenLayoutOf(
|
|
|
210
247
|
reservedRegions: List<ReservedRegion>,
|
|
211
248
|
): ScreenLayout {
|
|
212
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
|
+
}
|
|
213
257
|
|
|
214
258
|
var sideBar: SideBarLayout? = null
|
|
215
259
|
var titleCenterY = 0.dp
|
|
@@ -223,6 +267,7 @@ private fun screenLayoutOf(
|
|
|
223
267
|
val occlusion = stripOcclusion(reservedRegions, stripLeft, stripLeft + sideInset, height)
|
|
224
268
|
// UIKit reports no usable status bar frame here (a 2pt top strip), so the spot is inferred like RN
|
|
225
269
|
val top = when {
|
|
270
|
+
statusBarClearance != null -> statusBarRowGeometry().first + Spacing.M
|
|
226
271
|
topInset >= STATUS_BAR_MIN_INSET -> topInset + HEADER_HEIGHT.dp + Spacing.M
|
|
227
272
|
width <= height -> DuoMetrics.firstItemTop
|
|
228
273
|
height >= DuoMetrics.innerMinHeight -> DuoMetrics.firstItemTopLandscape
|
|
@@ -239,8 +284,11 @@ private fun screenLayoutOf(
|
|
|
239
284
|
bottom = maxOf(DuoMetrics.edgeInset, occlusion.bottom),
|
|
240
285
|
)
|
|
241
286
|
}
|
|
242
|
-
val (headerHeight, headerStatusBarHeight) =
|
|
287
|
+
val (headerHeight, headerStatusBarHeight) = if (statusBarClearance != null) {
|
|
288
|
+
statusBarRowGeometry()
|
|
289
|
+
} else {
|
|
243
290
|
headerGeometry(topInset, isPhone, width, height, sideBar != null, titleCenterY)
|
|
291
|
+
}
|
|
244
292
|
|
|
245
293
|
return ScreenLayout(
|
|
246
294
|
width = width,
|
|
@@ -256,5 +304,6 @@ private fun screenLayoutOf(
|
|
|
256
304
|
sideBar = sideBar,
|
|
257
305
|
isIPhoneDuoDevice = isDuoDevice,
|
|
258
306
|
duoTopInset = maxOf(topInset, if (height > width) sideInset * 2 else 0.dp),
|
|
307
|
+
statusBarClearance = statusBarClearance,
|
|
259
308
|
)
|
|
260
309
|
}
|
|
@@ -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
|
|
@@ -494,10 +495,14 @@ internal fun SearchAnimated(
|
|
|
494
495
|
// collapsed, the search is centered in the bar row
|
|
495
496
|
val minTopPadding = screenLayout.headerStatusBarHeight + (screenLayout.headerBarHeight - HEADER_HEIGHT.dp) / 2
|
|
496
497
|
val maxTopPadding = screenLayout.headerHeight
|
|
498
|
+
// collapsed on the iPhone Duo status bar row, the search stays between its clusters (48pt back there)
|
|
499
|
+
val clearance = screenLayout.statusBarClearance
|
|
500
|
+
val backWidth = if (clearance != null && !useSideBar) DuoMetrics.controlSize else 28.dp
|
|
497
501
|
val minStartPadding = Spacing.M
|
|
498
|
-
val maxStartPadding =
|
|
502
|
+
val maxStartPadding = (clearance?.left ?: 0.dp) +
|
|
503
|
+
if (options.hiddenBack || useSideBar) Spacing.M else backWidth + Spacing.M + Spacing.S
|
|
499
504
|
val minEndPadding = Spacing.M
|
|
500
|
-
val maxEndPadding = when {
|
|
505
|
+
val maxEndPadding = (clearance?.right ?: 0.dp) + when {
|
|
501
506
|
options.headerRight is HeaderRight.None -> Spacing.M
|
|
502
507
|
useSideBar && options.headerRight is HeaderRight.Toolkit -> Spacing.M
|
|
503
508
|
else -> headerRightWidthDp + Spacing.M * 2
|
|
@@ -40,6 +40,7 @@ import vn.momo.kits.const.Colors
|
|
|
40
40
|
import vn.momo.kits.components.PopupNotifyProps
|
|
41
41
|
import vn.momo.kits.application.IsShowBaseLineDebug
|
|
42
42
|
import vn.momo.kits.const.Spacing
|
|
43
|
+
import vn.momo.kits.layout.DuoMetrics
|
|
43
44
|
import vn.momo.kits.layout.getScreenLayout
|
|
44
45
|
import vn.momo.kits.modifier.activeOpacityClickable
|
|
45
46
|
import vn.momo.kits.modifier.conditional
|
|
@@ -59,6 +60,9 @@ enum class TitlePosition { LEFT, CENTER }
|
|
|
59
60
|
|
|
60
61
|
private val HEADER_ACTIONS_WIDTH = 172.dp
|
|
61
62
|
|
|
63
|
+
// Extra width the 48pt back and toolkit take on the iPhone Duo status bar row.
|
|
64
|
+
private val STATUS_BAR_ROW_ACTIONS_EXTRA = 55.dp
|
|
65
|
+
|
|
62
66
|
@Suppress("FrequentlyChangingValue")
|
|
63
67
|
@Composable
|
|
64
68
|
internal fun headerOpacity(): Float {
|
|
@@ -82,8 +86,10 @@ internal fun headerOpacity(): Float {
|
|
|
82
86
|
@Composable
|
|
83
87
|
internal fun headerTitleMaxWidth(): Dp {
|
|
84
88
|
val layout = getScreenLayout()
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
val clearance = layout.statusBarClearance
|
|
90
|
+
val clearanceWidth = clearance?.let { it.left + it.right } ?: 0.dp
|
|
91
|
+
if (layout.sideBar != null) return layout.contentWidth - Spacing.M * 2 - clearanceWidth
|
|
92
|
+
return layout.width - HEADER_ACTIONS_WIDTH - (if (clearance != null) STATUS_BAR_ROW_ACTIONS_EXTRA else 0.dp) - clearanceWidth
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
// Scroll distance over which the Duo glass bar fades in.
|
|
@@ -117,6 +123,9 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
|
|
|
117
123
|
val inputSearchType = getInputSearchType(options)
|
|
118
124
|
val layout = getScreenLayout()
|
|
119
125
|
val useSideBar = layout.sideBar != null
|
|
126
|
+
// iPhone Duo inner portrait: the row sits on the status bar, between its clusters
|
|
127
|
+
val clearance = layout.statusBarClearance
|
|
128
|
+
val statusBarRow = clearance != null && !useSideBar
|
|
120
129
|
val opacity = headerOpacity()
|
|
121
130
|
|
|
122
131
|
val headerColor = getHeaderColor(options.headerType, opacity, options.tintColor, Colors.black_17)
|
|
@@ -174,19 +183,31 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
|
|
|
174
183
|
modifier = Modifier.height(layout.headerBarHeight)
|
|
175
184
|
.fillMaxWidth()
|
|
176
185
|
.horizontalSafeAreaPadding()
|
|
177
|
-
.padding(
|
|
186
|
+
.padding(
|
|
187
|
+
start = Spacing.M + (clearance?.left ?: 0.dp),
|
|
188
|
+
end = Spacing.M + (clearance?.right ?: 0.dp),
|
|
189
|
+
),
|
|
178
190
|
verticalAlignment = Alignment.CenterVertically,
|
|
179
191
|
horizontalArrangement = Arrangement.SpaceBetween
|
|
180
192
|
) {
|
|
181
193
|
if (!options.hiddenBack && !useSideBar) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
navigator.onBackSafe {
|
|
188
|
-
|
|
189
|
-
|
|
194
|
+
if (statusBarRow) {
|
|
195
|
+
SideBarButton(
|
|
196
|
+
icon = "arrow-back",
|
|
197
|
+
headerColor = headerColor,
|
|
198
|
+
automationId = "btn_navigation_back",
|
|
199
|
+
onClick = { navigator.onBackSafe { } },
|
|
200
|
+
)
|
|
201
|
+
} else {
|
|
202
|
+
BackButton(
|
|
203
|
+
borderColor = headerColor.borderColor,
|
|
204
|
+
backgroundButton = headerColor.backgroundButton,
|
|
205
|
+
tintIconColor = headerColor.tintIconColor,
|
|
206
|
+
onBackHandler = {
|
|
207
|
+
navigator.onBackSafe { }
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
}
|
|
190
211
|
Spacer(Modifier.width(Spacing.S))
|
|
191
212
|
}
|
|
192
213
|
|
|
@@ -203,23 +224,40 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
|
|
|
203
224
|
Box(Modifier.onGloballyPositioned {
|
|
204
225
|
if(headerRightWidthPx.intValue != it.size.width) headerRightWidthPx.intValue = it.size.width
|
|
205
226
|
}){
|
|
206
|
-
|
|
207
|
-
|
|
227
|
+
val headerRight = options.headerRight
|
|
228
|
+
when {
|
|
229
|
+
useSideBar && headerRight is HeaderRight.Toolkit -> Unit
|
|
230
|
+
statusBarRow && headerRight is HeaderRight.Toolkit -> Toolkit(
|
|
231
|
+
headerRight = headerRight,
|
|
232
|
+
tintColor = options.tintColor,
|
|
233
|
+
headerColor = headerColor,
|
|
234
|
+
layout = ToolkitLayout.SideBarRow,
|
|
235
|
+
)
|
|
236
|
+
else -> HeaderRight(headerRight, options.tintColor, headerColor)
|
|
208
237
|
}
|
|
209
238
|
}
|
|
210
239
|
}
|
|
211
240
|
if (centerTitle) {
|
|
241
|
+
// on the status bar row the clusters are lopsided, so both sides keep the wider one's room
|
|
242
|
+
val centerInset = if (clearance != null) {
|
|
243
|
+
val back = if (options.hiddenBack || useSideBar) 0.dp else DuoMetrics.controlSize + Spacing.S
|
|
244
|
+
val right = with(LocalDensity.current) { headerRightWidthPx.intValue.toDp() }
|
|
245
|
+
maxOf(clearance.left + back, clearance.right + right) + Spacing.S
|
|
246
|
+
} else {
|
|
247
|
+
0.dp
|
|
248
|
+
}
|
|
212
249
|
Box(
|
|
213
250
|
modifier = Modifier.height(layout.headerBarHeight)
|
|
214
251
|
.fillMaxWidth()
|
|
215
252
|
.horizontalSafeAreaPadding()
|
|
216
|
-
.padding(horizontal = Spacing.M),
|
|
253
|
+
.padding(horizontal = Spacing.M + centerInset),
|
|
217
254
|
contentAlignment = Alignment.Center
|
|
218
255
|
) {
|
|
219
256
|
HeaderContent(
|
|
220
257
|
headerTitle = options.headerTitle,
|
|
221
258
|
tintIconColor = titleColor,
|
|
222
259
|
titlePosition = titlePosition,
|
|
260
|
+
centerWidthFraction = if (clearance != null) 1f else 0.5f,
|
|
223
261
|
modifier = Modifier.fillMaxWidth()
|
|
224
262
|
)
|
|
225
263
|
}
|
|
@@ -253,6 +291,7 @@ internal fun HeaderContent(
|
|
|
253
291
|
headerTitle: HeaderTitle,
|
|
254
292
|
tintIconColor: Color,
|
|
255
293
|
titlePosition: TitlePosition = TitlePosition.LEFT,
|
|
294
|
+
centerWidthFraction: Float = 0.5f,
|
|
256
295
|
modifier: Modifier = Modifier,
|
|
257
296
|
){
|
|
258
297
|
Box(
|
|
@@ -264,7 +303,7 @@ internal fun HeaderContent(
|
|
|
264
303
|
HeaderTitle(
|
|
265
304
|
title = headerTitle.title,
|
|
266
305
|
color = tintIconColor,
|
|
267
|
-
modifier = Modifier.fillMaxWidth(fraction = if (titlePosition == TitlePosition.CENTER)
|
|
306
|
+
modifier = Modifier.fillMaxWidth(fraction = if (titlePosition == TitlePosition.CENTER) centerWidthFraction else 1f),
|
|
268
307
|
textAlign = if (titlePosition == TitlePosition.CENTER) TextAlign.Center else TextAlign.Start
|
|
269
308
|
)
|
|
270
309
|
}
|
|
@@ -79,12 +79,15 @@ internal fun toolkitColumnHeight(headerRight: HeaderRight.Toolkit): Dp {
|
|
|
79
79
|
return shortcut + if (groupCount > 1) DuoMetrics.itemPitch * groupCount else DuoMetrics.controlSize
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// SideBarColumn: the iPhone Duo side bar; SideBarRow: the Duo header row on the status bar, same 48pt controls.
|
|
83
|
+
internal enum class ToolkitLayout { Bar, SideBarColumn, SideBarRow }
|
|
84
|
+
|
|
82
85
|
@Composable
|
|
83
86
|
internal fun Toolkit(
|
|
84
87
|
headerRight: HeaderRight.Toolkit,
|
|
85
88
|
tintColor: Color? = null,
|
|
86
89
|
headerColor: HeaderColor,
|
|
87
|
-
|
|
90
|
+
layout: ToolkitLayout = ToolkitLayout.Bar,
|
|
88
91
|
) {
|
|
89
92
|
val api = LocalMaxApi.current
|
|
90
93
|
val context = LocalContext.current
|
|
@@ -163,8 +166,9 @@ internal fun Toolkit(
|
|
|
163
166
|
}
|
|
164
167
|
}
|
|
165
168
|
|
|
166
|
-
if (
|
|
167
|
-
|
|
169
|
+
if (layout != ToolkitLayout.Bar) {
|
|
170
|
+
val horizontal = layout == ToolkitLayout.SideBarRow
|
|
171
|
+
val controls = @Composable {
|
|
168
172
|
if (isShowShortcut) {
|
|
169
173
|
SideBarButton(
|
|
170
174
|
icon = icon,
|
|
@@ -174,13 +178,14 @@ internal fun Toolkit(
|
|
|
174
178
|
onClick = navButtonConfig.onPress,
|
|
175
179
|
)
|
|
176
180
|
}
|
|
177
|
-
SideBarGroup(headerColor = headerColor, itemCount = if (useHelpCenter) 2 else 1) {
|
|
181
|
+
SideBarGroup(headerColor = headerColor, itemCount = if (useHelpCenter) 2 else 1, horizontal = horizontal) { item ->
|
|
178
182
|
if (useHelpCenter) {
|
|
179
183
|
SideBarGroupItem(
|
|
180
184
|
icon = "help_center",
|
|
181
185
|
headerColor = headerColor,
|
|
182
186
|
automationId = "btn_navigation_help_center",
|
|
183
187
|
onClick = ::onPressHelpCenter,
|
|
188
|
+
modifier = item,
|
|
184
189
|
)
|
|
185
190
|
}
|
|
186
191
|
SideBarGroupItem(
|
|
@@ -188,9 +193,17 @@ internal fun Toolkit(
|
|
|
188
193
|
headerColor = headerColor,
|
|
189
194
|
automationId = "btn_navigation_close",
|
|
190
195
|
onClick = ::onPressClose,
|
|
196
|
+
modifier = item,
|
|
191
197
|
)
|
|
192
198
|
}
|
|
193
199
|
}
|
|
200
|
+
if (horizontal) {
|
|
201
|
+
Row(horizontalArrangement = Arrangement.spacedBy(Spacing.M), verticalAlignment = Alignment.CenterVertically) {
|
|
202
|
+
controls()
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
Column(verticalArrangement = Arrangement.spacedBy(Spacing.M)) { controls() }
|
|
206
|
+
}
|
|
194
207
|
return
|
|
195
208
|
}
|
|
196
209
|
|
|
@@ -6,11 +6,9 @@ import androidx.compose.foundation.layout.Arrangement
|
|
|
6
6
|
import androidx.compose.foundation.layout.Box
|
|
7
7
|
import androidx.compose.foundation.layout.BoxScope
|
|
8
8
|
import androidx.compose.foundation.layout.Column
|
|
9
|
-
import androidx.compose.foundation.layout.
|
|
9
|
+
import androidx.compose.foundation.layout.Row
|
|
10
10
|
import androidx.compose.foundation.layout.Spacer
|
|
11
11
|
import androidx.compose.foundation.layout.absolutePadding
|
|
12
|
-
import androidx.compose.foundation.layout.fillMaxWidth
|
|
13
|
-
import androidx.compose.foundation.layout.height
|
|
14
12
|
import androidx.compose.foundation.layout.offset
|
|
15
13
|
import androidx.compose.foundation.layout.size
|
|
16
14
|
import androidx.compose.foundation.layout.width
|
|
@@ -101,7 +99,12 @@ internal fun BoxScope.HeaderSideBar(bottomInset: Dp, modifier: Modifier = Modifi
|
|
|
101
99
|
automationId = "btn_navigation_toolbar",
|
|
102
100
|
onClick = compression.expandToolbar,
|
|
103
101
|
)
|
|
104
|
-
else -> Toolkit(
|
|
102
|
+
else -> Toolkit(
|
|
103
|
+
headerRight = toolkit,
|
|
104
|
+
tintColor = options.tintColor,
|
|
105
|
+
headerColor = headerColor,
|
|
106
|
+
layout = ToolkitLayout.SideBarColumn,
|
|
107
|
+
)
|
|
105
108
|
}
|
|
106
109
|
}
|
|
107
110
|
}
|
|
@@ -135,34 +138,38 @@ internal fun SideBarButton(
|
|
|
135
138
|
}
|
|
136
139
|
}
|
|
137
140
|
|
|
138
|
-
// Capsule grouping related controls, one item per 50pt.
|
|
141
|
+
// Capsule grouping related controls, one item per 50pt: vertical in the side bar, horizontal on a header row.
|
|
139
142
|
@Composable
|
|
140
143
|
internal fun SideBarGroup(
|
|
141
144
|
headerColor: HeaderColor,
|
|
142
145
|
itemCount: Int,
|
|
143
|
-
|
|
146
|
+
horizontal: Boolean = false,
|
|
147
|
+
content: @Composable (itemModifier: Modifier) -> Unit,
|
|
144
148
|
) {
|
|
145
|
-
val
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
.
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
val length = if (itemCount > 1) DuoMetrics.itemPitch * itemCount else DuoMetrics.controlSize
|
|
150
|
+
val item = length / itemCount.coerceAtLeast(1)
|
|
151
|
+
val glass = Modifier.sideBarGlass(headerColor, RoundedCornerShape(Radius.XL))
|
|
152
|
+
if (horizontal) {
|
|
153
|
+
Row(Modifier.size(width = length, height = DuoMetrics.controlSize).then(glass)) {
|
|
154
|
+
content(Modifier.size(width = item, height = DuoMetrics.controlSize))
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
Column(Modifier.size(width = DuoMetrics.controlSize, height = length).then(glass)) {
|
|
158
|
+
content(Modifier.size(width = DuoMetrics.controlSize, height = item))
|
|
159
|
+
}
|
|
160
|
+
}
|
|
153
161
|
}
|
|
154
162
|
|
|
155
163
|
@Composable
|
|
156
|
-
internal fun
|
|
164
|
+
internal fun SideBarGroupItem(
|
|
157
165
|
icon: String,
|
|
158
166
|
headerColor: HeaderColor,
|
|
159
167
|
automationId: String,
|
|
160
168
|
onClick: () -> Unit,
|
|
169
|
+
modifier: Modifier = Modifier,
|
|
161
170
|
) {
|
|
162
171
|
Box(
|
|
163
|
-
modifier =
|
|
164
|
-
.weight(1f)
|
|
165
|
-
.fillMaxWidth()
|
|
172
|
+
modifier = modifier
|
|
166
173
|
.activeOpacityClickable(onClick = onClick)
|
|
167
174
|
.setAutomationId(automationId),
|
|
168
175
|
contentAlignment = Alignment.Center,
|
|
@@ -22,6 +22,12 @@ public struct SideBarLayout: Equatable {
|
|
|
22
22
|
public let bottom: CGFloat
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/// Room the header row keeps from each side for the status bar clock and battery clusters.
|
|
26
|
+
public struct StatusBarClearance: Equatable {
|
|
27
|
+
public let left: CGFloat
|
|
28
|
+
public let right: CGFloat
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
public struct ScreenLayout: Equatable {
|
|
26
32
|
public let width: CGFloat
|
|
27
33
|
public let height: CGFloat
|
|
@@ -35,6 +41,8 @@ public struct ScreenLayout: Equatable {
|
|
|
35
41
|
public let headerStatusBarHeight: CGFloat
|
|
36
42
|
/// iPhone Duo moves back and toolkit into a column in the side safe area; nil while they stay in the bar.
|
|
37
43
|
public let sideBar: SideBarLayout?
|
|
44
|
+
/// iPhone Duo inner display in portrait: the header row shares the status bar band, between its clusters.
|
|
45
|
+
public let statusBarClearance: StatusBarClearance?
|
|
38
46
|
|
|
39
47
|
public var headerBarHeight: CGFloat { headerHeight - headerStatusBarHeight }
|
|
40
48
|
}
|
|
@@ -56,6 +64,10 @@ enum DuoMetrics {
|
|
|
56
64
|
// Outer display in landscape has no status bar in the strip: controls keep this far from
|
|
57
65
|
// both ends unless a reserved region (the camera at either end) asks for more.
|
|
58
66
|
static let edgeInset: CGFloat = 24
|
|
67
|
+
// Inner display in portrait: the clock and connectivity cluster sits at the top right corner
|
|
68
|
+
// (x 550–643 of 669, clock digits centered on y 47); its reserved region is taller than the glyphs.
|
|
69
|
+
static let innerStatusBarCenterY: CGFloat = 47
|
|
70
|
+
static let innerStatusBarWidth: CGFloat = 120
|
|
59
71
|
// Widest short side of any single-display iPhone.
|
|
60
72
|
static let iPhoneMaxShortSide: CGFloat = 440
|
|
61
73
|
}
|
|
@@ -102,10 +114,21 @@ extension ScreenLayout {
|
|
|
102
114
|
return ScreenLayout(
|
|
103
115
|
width: width, height: height, isIPhoneDuo: false, insets: insets,
|
|
104
116
|
insetLeft: 0, insetRight: 0, contentWidth: width,
|
|
105
|
-
headerHeight: headerHeight, headerStatusBarHeight: headerStatusBarHeight, sideBar: nil
|
|
117
|
+
headerHeight: headerHeight, headerStatusBarHeight: headerStatusBarHeight, sideBar: nil,
|
|
118
|
+
statusBarClearance: nil
|
|
106
119
|
)
|
|
107
120
|
}
|
|
108
121
|
|
|
122
|
+
var statusBarClearance: StatusBarClearance?
|
|
123
|
+
let isInnerPortrait = height > width && width >= DuoMetrics.innerMinHeight
|
|
124
|
+
if isInnerPortrait && insets.top >= HeaderMetrics.statusBarMinInset {
|
|
125
|
+
statusBarClearance = statusBarClusters(reservedRegions, windowWidth: width, topInset: insets.top)
|
|
126
|
+
?? StatusBarClearance(left: 0, right: DuoMetrics.innerStatusBarWidth + Spacing.S)
|
|
127
|
+
// centered on the clock digits; the cluster's region center sits above them
|
|
128
|
+
headerHeight = DuoMetrics.innerStatusBarCenterY * 2
|
|
129
|
+
headerStatusBarHeight = 0
|
|
130
|
+
}
|
|
131
|
+
|
|
109
132
|
var sideBar: SideBarLayout?
|
|
110
133
|
let sideInset = max(insets.leading, insets.trailing)
|
|
111
134
|
// narrower strips cannot host a control 12pt from the inner edge
|
|
@@ -116,7 +139,9 @@ extension ScreenLayout {
|
|
|
116
139
|
// Activities; region frames already include the system's margin
|
|
117
140
|
let occlusion = stripOcclusion(reservedRegions, stripLeft: stripLeft, stripRight: stripLeft + sideInset, windowHeight: height)
|
|
118
141
|
|
|
119
|
-
var top =
|
|
142
|
+
var top = statusBarClearance != nil
|
|
143
|
+
? headerHeight + Spacing.M
|
|
144
|
+
: insets.top + HeaderMetrics.barHeight + Spacing.M
|
|
120
145
|
if insets.top < HeaderMetrics.statusBarMinInset {
|
|
121
146
|
// status bar lives in the strip: start under its cluster, as the native bar does
|
|
122
147
|
if width <= height {
|
|
@@ -150,7 +175,8 @@ extension ScreenLayout {
|
|
|
150
175
|
width: width, height: height, isIPhoneDuo: true, insets: insets,
|
|
151
176
|
insetLeft: insets.leading, insetRight: insets.trailing,
|
|
152
177
|
contentWidth: width - insets.leading - insets.trailing,
|
|
153
|
-
headerHeight: headerHeight, headerStatusBarHeight: headerStatusBarHeight, sideBar: sideBar
|
|
178
|
+
headerHeight: headerHeight, headerStatusBarHeight: headerStatusBarHeight, sideBar: sideBar,
|
|
179
|
+
statusBarClearance: statusBarClearance
|
|
154
180
|
)
|
|
155
181
|
}
|
|
156
182
|
|
|
@@ -192,6 +218,26 @@ extension ScreenLayout {
|
|
|
192
218
|
}
|
|
193
219
|
}
|
|
194
220
|
|
|
221
|
+
/// How far the clock and battery clusters of a status bar on the top edge reach in from each
|
|
222
|
+
/// side; nil without reserved regions there.
|
|
223
|
+
private static func statusBarClusters(_ regions: [CGRect], windowWidth: CGFloat, topInset: CGFloat) -> StatusBarClearance? {
|
|
224
|
+
let midX = windowWidth / 2
|
|
225
|
+
let clusters = regions.filter {
|
|
226
|
+
$0.minY < topInset && $0.maxY <= topInset && ($0.maxX <= midX || $0.minX >= midX)
|
|
227
|
+
}
|
|
228
|
+
guard !clusters.isEmpty else { return nil }
|
|
229
|
+
var left: CGFloat = 0
|
|
230
|
+
var right: CGFloat = 0
|
|
231
|
+
for region in clusters {
|
|
232
|
+
if region.minX < midX {
|
|
233
|
+
left = max(left, region.maxX)
|
|
234
|
+
} else {
|
|
235
|
+
right = max(right, windowWidth - region.minX)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return StatusBarClearance(left: left, right: right)
|
|
239
|
+
}
|
|
240
|
+
|
|
195
241
|
// Slab iPhones report equal side insets in landscape; iPhone Duo moves the status bar to one side.
|
|
196
242
|
private static func isIPhoneDuoWindow(width: CGFloat, height: CGFloat, insets: EdgeInsets) -> Bool {
|
|
197
243
|
let shortSide = min(width, height)
|
|
@@ -13,6 +13,8 @@ public let HEADER_HEIGHT: CGFloat = 52
|
|
|
13
13
|
|
|
14
14
|
/// Room the back button and toolkit take from the bar (Compose `HEADER_ACTIONS_WIDTH`).
|
|
15
15
|
let headerActionsWidth: CGFloat = 172
|
|
16
|
+
// Extra width the 48pt back and toolkit take on the iPhone Duo status bar row.
|
|
17
|
+
private let statusBarRowActionsExtra: CGFloat = 55
|
|
16
18
|
|
|
17
19
|
public enum TitlePosition {
|
|
18
20
|
case left, center
|
|
@@ -282,7 +284,9 @@ func headerSearchProps(_ props: InputSearchProps, scroll: CGFloat, layout: Scree
|
|
|
282
284
|
|
|
283
285
|
/// Max title width: back button and toolkit share the bar unless the iPhone Duo side bar holds them.
|
|
284
286
|
func headerTitleMaxWidth(_ layout: ScreenLayout) -> CGFloat {
|
|
285
|
-
|
|
287
|
+
let clearance = layout.statusBarClearance.map { $0.left + $0.right } ?? 0
|
|
288
|
+
if layout.sideBar != nil { return layout.contentWidth - Spacing.M * 2 - clearance }
|
|
289
|
+
return layout.width - headerActionsWidth - (layout.statusBarClearance != nil ? statusBarRowActionsExtra : 0) - clearance
|
|
286
290
|
}
|
|
287
291
|
|
|
288
292
|
// MARK: - Header
|
|
@@ -333,10 +337,22 @@ public struct NavigationHeader: View {
|
|
|
333
337
|
|
|
334
338
|
@Environment(\.screenLayout) private var layout
|
|
335
339
|
@Environment(\.appTheme) private var theme
|
|
340
|
+
@State private var headerRightWidth: CGFloat = 0
|
|
336
341
|
|
|
337
342
|
/// iPhone Duo side bar holds back and the toolkit; a custom header right stays in the bar.
|
|
338
343
|
private var useSideBar: Bool { layout.sideBar != nil }
|
|
339
344
|
|
|
345
|
+
/// iPhone Duo inner portrait: the row sits on the status bar, between its clusters, with 48pt controls.
|
|
346
|
+
private var statusBarRow: Bool { layout.statusBarClearance != nil && !useSideBar }
|
|
347
|
+
|
|
348
|
+
/// Max centered title width; on the status bar row the clusters are lopsided, so both sides keep the wider one's room.
|
|
349
|
+
private var centerTitleMaxWidth: CGFloat {
|
|
350
|
+
guard let clearance = layout.statusBarClearance else { return (layout.contentWidth - Spacing.M * 2) / 2 }
|
|
351
|
+
let back = goBack == nil || useSideBar ? 0 : DuoMetrics.controlSize + Spacing.S
|
|
352
|
+
let inset = max(clearance.left + back, clearance.right + headerRightWidth) + Spacing.S
|
|
353
|
+
return max(layout.contentWidth - (Spacing.M + inset) * 2, 0)
|
|
354
|
+
}
|
|
355
|
+
|
|
340
356
|
private var headerColor: HeaderColor {
|
|
341
357
|
getHeaderColor(
|
|
342
358
|
isAnimated: animatedHeader != nil,
|
|
@@ -357,7 +373,16 @@ public struct NavigationHeader: View {
|
|
|
357
373
|
ZStack {
|
|
358
374
|
HStack(spacing: 0) {
|
|
359
375
|
if let goBack, !useSideBar {
|
|
360
|
-
|
|
376
|
+
if statusBarRow {
|
|
377
|
+
SideBarButton(
|
|
378
|
+
icon: "arrow-back",
|
|
379
|
+
headerColor: headerColor,
|
|
380
|
+
accessibilityId: "btn_navigation_back",
|
|
381
|
+
action: goBack
|
|
382
|
+
)
|
|
383
|
+
} else {
|
|
384
|
+
BackButton(color: headerColor, onBack: goBack)
|
|
385
|
+
}
|
|
361
386
|
Spacer().frame(width: Spacing.S)
|
|
362
387
|
}
|
|
363
388
|
|
|
@@ -369,24 +394,36 @@ public struct NavigationHeader: View {
|
|
|
369
394
|
}
|
|
370
395
|
|
|
371
396
|
if let headerRight, !(useSideBar && toolkitHeaderRight(headerRight) != nil) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
397
|
+
Group {
|
|
398
|
+
if statusBarRow, let toolkit = toolkitHeaderRight(headerRight) {
|
|
399
|
+
Toolkit(
|
|
400
|
+
headerRight: toolkit.data ?? HeaderRightData(),
|
|
401
|
+
tintColor: toolkit.tintColor,
|
|
402
|
+
layout: .sideBarRow
|
|
403
|
+
)
|
|
404
|
+
} else {
|
|
405
|
+
AnyView(headerRight())
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
.environment(\.headerColor, headerColor)
|
|
409
|
+
.background(
|
|
410
|
+
GeometryReader { geo in
|
|
411
|
+
Color.clear.preference(key: HeaderRightWidthKey.self, value: geo.size.width)
|
|
412
|
+
}
|
|
413
|
+
)
|
|
379
414
|
}
|
|
380
415
|
}
|
|
381
|
-
.padding(.
|
|
416
|
+
.padding(.leading, Spacing.M + (layout.statusBarClearance?.left ?? 0))
|
|
417
|
+
.padding(.trailing, Spacing.M + (layout.statusBarClearance?.right ?? 0))
|
|
382
418
|
|
|
383
419
|
if centerTitle {
|
|
384
420
|
titleArea(centered: true)
|
|
385
421
|
// Compose caps a centered title at half the bar
|
|
386
|
-
.frame(maxWidth:
|
|
422
|
+
.frame(maxWidth: centerTitleMaxWidth)
|
|
387
423
|
.padding(.horizontal, Spacing.M)
|
|
388
424
|
}
|
|
389
425
|
}
|
|
426
|
+
.onPreferenceChange(HeaderRightWidthKey.self) { headerRightWidth = $0 }
|
|
390
427
|
.frame(height: layout.headerBarHeight)
|
|
391
428
|
.overlay(alignment: .bottom) { VerticalShadow(opacity: opacity) }
|
|
392
429
|
// the row starts headerStatusBarHeight below the window top: 0 with the status bar off
|
|
@@ -158,11 +158,15 @@ private struct NavigationButtonConfig {
|
|
|
158
158
|
let onPress: () -> Void
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/// `sideBarColumn`: the iPhone Duo side bar; `sideBarRow`: the Duo header row on the status bar, same 48pt controls.
|
|
162
|
+
enum ToolkitLayout {
|
|
163
|
+
case bar, sideBarColumn, sideBarRow
|
|
164
|
+
}
|
|
165
|
+
|
|
161
166
|
struct Toolkit: View {
|
|
162
167
|
let headerRight: HeaderRightData
|
|
163
168
|
var tintColor: Color?
|
|
164
|
-
|
|
165
|
-
var vertical: Bool = false
|
|
169
|
+
var layout: ToolkitLayout = .bar
|
|
166
170
|
|
|
167
171
|
@State private var isFavorite = false
|
|
168
172
|
@State private var isLoading = false
|
|
@@ -276,44 +280,46 @@ struct Toolkit: View {
|
|
|
276
280
|
|
|
277
281
|
var body: some View {
|
|
278
282
|
Group {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
} else {
|
|
283
|
+
switch layout {
|
|
284
|
+
case .bar:
|
|
282
285
|
horizontalBody
|
|
286
|
+
case .sideBarColumn:
|
|
287
|
+
VStack(spacing: Spacing.M) { sideBarControls(horizontal: false) }
|
|
288
|
+
case .sideBarRow:
|
|
289
|
+
HStack(spacing: Spacing.M) { sideBarControls(horizontal: true) }
|
|
283
290
|
}
|
|
284
291
|
}
|
|
285
292
|
.onAppear(perform: loadFavoriteState)
|
|
286
293
|
}
|
|
287
294
|
|
|
288
|
-
|
|
295
|
+
@ViewBuilder
|
|
296
|
+
private func sideBarControls(horizontal: Bool) -> some View {
|
|
289
297
|
let config = navigationButtonConfig
|
|
290
298
|
let useHelpCenter = usesHelpCenter(context)
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if useHelpCenter {
|
|
303
|
-
SideBarGroupItem(
|
|
304
|
-
icon: "help_center",
|
|
305
|
-
headerColor: headerColor,
|
|
306
|
-
accessibilityId: "btn_navigation_help_center",
|
|
307
|
-
action: onPressHelpCenter
|
|
308
|
-
)
|
|
309
|
-
}
|
|
299
|
+
if showsShortcut(headerRight, context) {
|
|
300
|
+
SideBarButton(
|
|
301
|
+
icon: config.icon,
|
|
302
|
+
headerColor: headerColor,
|
|
303
|
+
showBadge: headerRight.showBadge,
|
|
304
|
+
disabled: isLoading,
|
|
305
|
+
action: config.onPress
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
SideBarGroup(headerColor: headerColor, itemCount: useHelpCenter ? 2 : 1, horizontal: horizontal) {
|
|
309
|
+
if useHelpCenter {
|
|
310
310
|
SideBarGroupItem(
|
|
311
|
-
icon:
|
|
311
|
+
icon: "help_center",
|
|
312
312
|
headerColor: headerColor,
|
|
313
|
-
accessibilityId: "
|
|
314
|
-
action:
|
|
313
|
+
accessibilityId: "btn_navigation_help_center",
|
|
314
|
+
action: onPressHelpCenter
|
|
315
315
|
)
|
|
316
316
|
}
|
|
317
|
+
SideBarGroupItem(
|
|
318
|
+
icon: headerRight.closeIcon,
|
|
319
|
+
headerColor: headerColor,
|
|
320
|
+
accessibilityId: "btn_navigation_close",
|
|
321
|
+
action: onPressClose
|
|
322
|
+
)
|
|
317
323
|
}
|
|
318
324
|
}
|
|
319
325
|
|
|
@@ -70,7 +70,7 @@ struct HeaderSideBar: View {
|
|
|
70
70
|
Toolkit(
|
|
71
71
|
headerRight: toolkit.data ?? HeaderRightData(),
|
|
72
72
|
tintColor: toolkit.tintColor,
|
|
73
|
-
|
|
73
|
+
layout: .sideBarColumn
|
|
74
74
|
)
|
|
75
75
|
.environment(\.headerColor, headerColor)
|
|
76
76
|
}
|
|
@@ -142,19 +142,26 @@ struct SideBarButton: View {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
/// 48pt
|
|
145
|
+
/// 48pt glass capsule grouping related controls, one item per 50pt: vertical in the side bar,
|
|
146
|
+
/// horizontal on a header row.
|
|
146
147
|
struct SideBarGroup<Content: View>: View {
|
|
147
148
|
let headerColor: HeaderColor
|
|
148
149
|
let itemCount: Int
|
|
150
|
+
var horizontal: Bool = false
|
|
149
151
|
@ViewBuilder let content: () -> Content
|
|
150
152
|
|
|
151
153
|
var body: some View {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
let length = itemCount > 1 ? DuoMetrics.itemPitch * CGFloat(itemCount) : DuoMetrics.controlSize
|
|
155
|
+
Group {
|
|
156
|
+
if horizontal {
|
|
157
|
+
HStack(spacing: 0) { content() }
|
|
158
|
+
.frame(width: length, height: DuoMetrics.controlSize)
|
|
159
|
+
} else {
|
|
160
|
+
VStack(spacing: 0) { content() }
|
|
161
|
+
.frame(width: DuoMetrics.controlSize, height: length)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
.modifier(SideBarGlass(headerColor: headerColor, shape: RoundedRectangle(cornerRadius: Radius.XL)))
|
|
158
165
|
}
|
|
159
166
|
}
|
|
160
167
|
|
|
@@ -618,24 +618,25 @@ private struct SearchAnimatedHeader: View {
|
|
|
618
618
|
let minTop = rowTop + (layout.headerBarHeight - HEADER_HEIGHT) / 2
|
|
619
619
|
let maxTop = layout.headerHeight - layout.insets.top
|
|
620
620
|
let minSide = Spacing.M
|
|
621
|
-
|
|
622
|
-
let
|
|
621
|
+
// collapsed on the iPhone Duo status bar row, the search stays between its clusters (48pt back there)
|
|
622
|
+
let clearance = layout.statusBarClearance
|
|
623
|
+
let backWidth: CGFloat = clearance != nil && !useSideBar ? DuoMetrics.controlSize : 28
|
|
624
|
+
let maxStart = (clearance?.left ?? 0) + (hiddenBack || useSideBar ? Spacing.M : backWidth + Spacing.M + Spacing.S)
|
|
625
|
+
let maxEnd = (clearance?.right ?? 0) + (useSideBar && hasToolkit
|
|
623
626
|
? Spacing.M
|
|
624
|
-
: headerRightWidth + (hasHeaderRight ? Spacing.M * 2 : Spacing.M)
|
|
627
|
+
: headerRightWidth + (hasHeaderRight ? Spacing.M * 2 : Spacing.M))
|
|
625
628
|
|
|
626
629
|
let top = min(max(maxTop - scroll, minTop), maxTop)
|
|
627
630
|
let start = min(max(minSide + scroll, minSide), maxStart)
|
|
628
631
|
let end = min(max(minSide + scroll, minSide), maxEnd)
|
|
629
632
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
}
|
|
638
|
-
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
|
633
|
+
// padding, not a Spacer: on the iPhone Duo status bar row the collapsed top is above the safe area
|
|
634
|
+
InputSearch(props: headerSearchProps(props, scroll: scroll, layout: layout, theme: theme))
|
|
635
|
+
.frame(height: HEADER_HEIGHT)
|
|
636
|
+
.padding(.leading, start)
|
|
637
|
+
.padding(.trailing, end)
|
|
638
|
+
.padding(.top, top)
|
|
639
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
|
639
640
|
}
|
|
640
641
|
}
|
|
641
642
|
|