diff --git a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/common/SignedOutConnectionActions.kt b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/common/SignedOutConnectionActions.kt index 6ccf13fa05..0beff920ff 100644 --- a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/common/SignedOutConnectionActions.kt +++ b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/common/SignedOutConnectionActions.kt @@ -14,6 +14,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics @@ -37,6 +38,11 @@ internal fun SignedOutConnectionActions( buttonHeight: Dp = 48.dp, spacing: Dp = 10.dp, fontSize: Int = 16, + // The quiet half of the pair. On a page it is a card; in the sidebar it is + // chrome, so the layer is the caller's to name rather than assumed here. + quietBackground: Color = MaterialTheme.colorScheme.surface, + quietBorder: Color = MaterialTheme.colorScheme.outlineVariant, + quietContent: Color = MaterialTheme.colorScheme.onSurface, ) { val shape = RoundedCornerShape(buttonHeight / 2) Column( @@ -49,8 +55,8 @@ internal fun SignedOutConnectionActions( .fillMaxWidth() .height(buttonHeight) .clip(shape) - .background(if (primaryScan) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface) - .border(1.dp, if (primaryScan) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outlineVariant, shape) + .background(if (primaryScan) MaterialTheme.colorScheme.primary else quietBackground) + .border(1.dp, if (primaryScan) MaterialTheme.colorScheme.primary else quietBorder, shape) .clickable(enabled = enabled, role = Role.Button, onClick = onScan) .semantics(mergeDescendants = true) { contentDescription = scanLabel @@ -61,7 +67,7 @@ internal fun SignedOutConnectionActions( scanLabel, fontSize = fontSize.sp, fontWeight = FontWeight.Bold, - color = if (primaryScan) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface, + color = if (primaryScan) MaterialTheme.colorScheme.onPrimary else quietContent, ) } } @@ -70,8 +76,8 @@ internal fun SignedOutConnectionActions( .fillMaxWidth() .height(buttonHeight) .clip(shape) - .background(if (primaryScan) MaterialTheme.colorScheme.surface else MaterialTheme.colorScheme.primary) - .border(1.dp, if (primaryScan) MaterialTheme.colorScheme.outlineVariant else MaterialTheme.colorScheme.primary, shape) + .background(if (primaryScan) quietBackground else MaterialTheme.colorScheme.primary) + .border(1.dp, if (primaryScan) quietBorder else MaterialTheme.colorScheme.primary, shape) .clickable(enabled = enabled, role = Role.Button, onClick = onOpenAccount) .semantics(mergeDescendants = true) { contentDescription = accountLabel @@ -82,7 +88,7 @@ internal fun SignedOutConnectionActions( accountLabel, fontSize = fontSize.sp, fontWeight = FontWeight.Bold, - color = if (primaryScan) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onPrimary, + color = if (primaryScan) quietContent else MaterialTheme.colorScheme.onPrimary, ) } } diff --git a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/remote/RemoteSessionListView.kt b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/remote/RemoteSessionListView.kt index 0937bdfcd6..1442db0168 100644 --- a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/remote/RemoteSessionListView.kt +++ b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/remote/RemoteSessionListView.kt @@ -433,6 +433,9 @@ private fun RemoteSessionListHeader( diameter = 38, onClick = onToggleViewSettings, modifier = Modifier.testTag(VIEW_SETTINGS_TOGGLE_TEST_TAG), + background = MaterialTheme.colorScheme.surface, + border = MaterialTheme.colorScheme.outlineVariant, + tint = MaterialTheme.colorScheme.onSurface, ) SidebarCircleButton( icon = R.drawable.ic_symbol_magnifyingglass, @@ -440,6 +443,9 @@ private fun RemoteSessionListHeader( diameter = 38, onClick = onToggleSearch, modifier = Modifier.testTag(SESSION_SEARCH_TOGGLE_TEST_TAG), + background = MaterialTheme.colorScheme.surface, + border = MaterialTheme.colorScheme.outlineVariant, + tint = MaterialTheme.colorScheme.onSurface, ) } } diff --git a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/MobileScreen.kt b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/MobileScreen.kt index b5769a5f21..37fd435122 100644 --- a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/MobileScreen.kt +++ b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/MobileScreen.kt @@ -55,6 +55,7 @@ import com.openbitfun.mobile.app.ui.remote.ConnectView import com.openbitfun.mobile.app.ui.settings.GeneralSettingsScreen import com.openbitfun.mobile.app.ui.settings.SettingsScreen import com.openbitfun.mobile.app.ui.shell.sidebar.AppSidebar +import com.openbitfun.mobile.app.ui.theme.openBitFunColors import com.openbitfun.mobile.app.viewmodel.AccountViewModel import com.openbitfun.mobile.core.feature.account.AccountIntent import com.openbitfun.mobile.core.feature.account.AccountUiState @@ -540,6 +541,10 @@ internal fun MobileScreen() { if (sidebarWidth > 0) { PermanentDrawerSheet( Modifier.width(sidebarWidth.dp).testTag(MASTER_DETAIL_TEST_TAG), + // Material fills a drawer sheet from surfaceContainerLow; + // the rail paints its own chrome, so the sheet gets out + // of the way rather than tinting a second layer under it. + drawerContainerColor = openBitFunColors.sidebar.background, ) { sidebar() } PaneSeparator( if (previewVisible) { diff --git a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/AppSidebar.kt b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/AppSidebar.kt index e18854a2e4..3350c495d6 100644 --- a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/AppSidebar.kt +++ b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/AppSidebar.kt @@ -14,9 +14,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -36,6 +38,7 @@ import com.openbitfun.mobile.app.R import com.openbitfun.mobile.app.ui.remote.SessionActionPopup import com.openbitfun.mobile.app.ui.remote.SessionActionSheet import com.openbitfun.mobile.app.ui.remote.SessionDetailsSheet +import com.openbitfun.mobile.app.ui.theme.openBitFunColors import com.openbitfun.mobile.core.feature.account.AccountDeviceUi import com.openbitfun.mobile.core.feature.connection.ConnectionPhase import com.openbitfun.mobile.core.feature.connection.RemoteControlSource @@ -107,71 +110,85 @@ internal fun AppSidebar( var remoteActionAnchor by remember { mutableStateOf(IntRect.Zero) } var remoteDetailsSessionId by rememberSaveable { mutableStateOf(null) } - Box(modifier = modifier.fillMaxSize().testTag(SIDEBAR_TEST_TAG)) { - // No spacedBy: the MiniApps row carries its own 4dp/8dp rhythm, and a - // column-level gap on top of it would push the section header away. - Column( - modifier = Modifier + // The rail is chrome, not another page: it paints the desktop client's + // `surface.chrome` itself and hands every unstyled descendant the matching + // ink, so nothing inside has to remember which layer it is on. + CompositionLocalProvider(LocalContentColor provides openBitFunColors.sidebar.ink) { + Box( + modifier = modifier .fillMaxSize() - .padding(start = 20.dp, end = 20.dp, top = 0.dp, bottom = 16.dp), + .background(openBitFunColors.sidebar.background) + .testTag(SIDEBAR_TEST_TAG), ) { - if (signedIn) { - SidebarAuthenticatedHeader(searchOpen, query, onQueryChange, onToggleSearch) - } else { - Text(stringResource(R.string.app_name), style = MaterialTheme.typography.titleLarge) - } + // No spacedBy: the MiniApps row carries its own 4dp/8dp rhythm, and a + // column-level gap on top of it would push the section header away. + Column( + modifier = Modifier + .fillMaxSize() + .padding(start = 20.dp, end = 20.dp, top = 0.dp, bottom = 16.dp), + ) { + if (signedIn) { + SidebarAuthenticatedHeader(searchOpen, query, onQueryChange, onToggleSearch) + } else { + Text( + stringResource(R.string.app_name), + style = MaterialTheme.typography.titleLarge, + color = openBitFunColors.sidebar.ink, + ) + } - com.openbitfun.mobile.app.ui.miniapps.MiniAppsButton(sidebar = true) + com.openbitfun.mobile.app.ui.miniapps.MiniAppsButton(sidebar = true) - Column(Modifier.weight(1f).verticalScroll(rememberScrollState()).padding(bottom = 142.dp)) { - SidebarRemoteWorkspaceSection( - connectionPhase = connectionPhase, - controlSource = remoteControlSource, - devices = remoteDevices, - selectedDeviceId = remoteSelectedDeviceId, - deviceName = remoteDeviceName, - remoteState = remoteState, - workspaceState = workspaceState, - workspaceDirectory = workspaceDirectory, - selectedSessionId = remoteSelectedSessionId.takeIf { remoteActive }, - onConnect = onScanDesktop, - onRetryActive = onRetryRemoteDevice, - onRefreshDevices = onRefreshRemoteDevices, - refreshingDevices = refreshingRemoteDevices, - directoryRefreshError = directoryRefreshError, - onSelectDevice = onSelectRemoteDevice, - onOpenSession = onOpenRemoteSession, - onOpenActions = { row, anchor -> - remoteActionAnchor = anchor - remoteActionSession = row - }, - onCreateInWorkspace = onCreateRemoteInWorkspace, - onOpenWorkspace = onOpenRemoteWorkspace, - onExpandWorkspace = onExpandRemoteWorkspace, - onRetryWorkspaceSessions = onRetryRemoteWorkspaceSessions, - onAddWorkspace = onAddRemoteWorkspace, - onWorkspaceTool = onWorkspaceTool, - ) - } + Column(Modifier.weight(1f).verticalScroll(rememberScrollState()).padding(bottom = 142.dp)) { + SidebarRemoteWorkspaceSection( + connectionPhase = connectionPhase, + controlSource = remoteControlSource, + devices = remoteDevices, + selectedDeviceId = remoteSelectedDeviceId, + deviceName = remoteDeviceName, + remoteState = remoteState, + workspaceState = workspaceState, + workspaceDirectory = workspaceDirectory, + selectedSessionId = remoteSelectedSessionId.takeIf { remoteActive }, + onConnect = onScanDesktop, + onRetryActive = onRetryRemoteDevice, + onRefreshDevices = onRefreshRemoteDevices, + refreshingDevices = refreshingRemoteDevices, + directoryRefreshError = directoryRefreshError, + onSelectDevice = onSelectRemoteDevice, + onOpenSession = onOpenRemoteSession, + onOpenActions = { row, anchor -> + remoteActionAnchor = anchor + remoteActionSession = row + }, + onCreateInWorkspace = onCreateRemoteInWorkspace, + onOpenWorkspace = onOpenRemoteWorkspace, + onExpandWorkspace = onExpandRemoteWorkspace, + onRetryWorkspaceSessions = onRetryRemoteWorkspaceSessions, + onAddWorkspace = onAddRemoteWorkspace, + onWorkspaceTool = onWorkspaceTool, + ) + } - } + } - // Over the list, not after it: the 84dp tail the list reserves is what - // keeps the last conversation from ending up underneath this. - Box( - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .padding(start = 20.dp, end = 20.dp, bottom = 16.dp), - ) { - if (signedIn) { - SidebarAuthenticatedFooter({ onWorkspaceTool("", null, false) }, onOpenSettings) - } else { - SidebarSignedOutFooter( - showScan = connectionPhase != ConnectionPhase.CONNECTED, - onScanDesktop = onScanDesktop, - onOpenAccount = onOpenAccount, - ) + // Over the list, not after it: the 84dp tail the list reserves is what + // keeps the last conversation from ending up underneath this. + Box( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(start = 20.dp, end = 20.dp, bottom = 16.dp), + ) { + if (signedIn) { + SidebarAuthenticatedFooter({ onWorkspaceTool("", null, false) }, onOpenSettings) + } else { + SidebarSignedOutFooter( + showScan = connectionPhase != ConnectionPhase.CONNECTED, + onScanDesktop = onScanDesktop, + onOpenAccount = onOpenAccount, + ) + } } } } diff --git a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarChrome.kt b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarChrome.kt index 7e18a6fb2a..70cb6a3172 100644 --- a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarChrome.kt +++ b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarChrome.kt @@ -27,9 +27,13 @@ import com.openbitfun.mobile.core.feature.connection.ConnectionTone * The round carded button the sidebar's header and footer are built from, * ported from the 38x38 and 46x46 stacks in `AppSidebar.ets`. * - * Carded rather than a bare `IconButton`: the drawer's own background is the - * page colour, so an icon drawn straight onto it has no edge and reads as - * decoration instead of as something to press. + * Carded rather than a bare `IconButton`: the drawer's own background is flat + * chrome, so an icon drawn straight onto it has no edge and reads as decoration + * instead of as something to press. + * + * The colours default to the sidebar's own chrome because that is where this + * shape belongs. The session-list header reuses the shape on a page surface, so + * it passes the page roles instead — same button, correct layer. */ @Composable internal fun SidebarCircleButton( @@ -38,21 +42,24 @@ internal fun SidebarCircleButton( diameter: Int, onClick: () -> Unit, modifier: Modifier, + background: Color = openBitFunColors.sidebar.raised, + border: Color = openBitFunColors.sidebar.line, + tint: Color = openBitFunColors.sidebar.ink, ) { Box( modifier = modifier .size(diameter.dp) .shadow(2.dp, CircleShape) .clip(CircleShape) - .background(MaterialTheme.colorScheme.surface) - .border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape) + .background(background) + .border(1.dp, border, CircleShape) .clickable(role = Role.Button, onClick = onClick), contentAlignment = Alignment.Center, ) { Icon( painterResource(icon), contentDescription = contentDescription, - tint = MaterialTheme.colorScheme.onSurface, + tint = tint, modifier = Modifier.size((diameter * 0.46f).dp), ) } @@ -65,9 +72,9 @@ internal fun ConnectionDot(phase: ConnectionPhase) { // Keep automatic recovery quiet; color and accessibility state express reachability. val color: Color = when (tone) { ConnectionTone.OK -> openBitFunColors.statusSuccess - ConnectionTone.BUSY -> MaterialTheme.colorScheme.onSurfaceVariant + ConnectionTone.BUSY -> openBitFunColors.sidebar.muted ConnectionTone.ERROR -> MaterialTheme.colorScheme.error - ConnectionTone.MUTED -> MaterialTheme.colorScheme.onSurfaceVariant + ConnectionTone.MUTED -> openBitFunColors.sidebar.muted } Box( Modifier diff --git a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarDeviceSelectorRow.kt b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarDeviceSelectorRow.kt index 5686e1c64d..4f053427e2 100644 --- a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarDeviceSelectorRow.kt +++ b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarDeviceSelectorRow.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -30,6 +29,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.openbitfun.mobile.app.R import com.openbitfun.mobile.app.ui.common.labelRes +import com.openbitfun.mobile.app.ui.theme.openBitFunColors import com.openbitfun.mobile.core.feature.connection.ConnectionPhase @Composable @@ -48,7 +48,7 @@ internal fun SidebarDeviceSelectorRow( .fillMaxWidth() .height(52.dp) .clip(RoundedCornerShape(10.dp)) - .background(if (selected) MaterialTheme.colorScheme.surfaceContainerHigh else MaterialTheme.colorScheme.surface.copy(alpha = 0f)) + .background(if (selected) openBitFunColors.sidebar.selection else openBitFunColors.transparent) .clickable(enabled = online || (selected && phase in listOf(ConnectionPhase.FAILED, ConnectionPhase.DISCONNECTED)), role = Role.Button, onClick = onSelect) .semantics(mergeDescendants = true) { this.selected = selected @@ -62,21 +62,21 @@ internal fun SidebarDeviceSelectorRow( Icon( painterResource(R.drawable.ic_symbol_desktop), contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface, + tint = openBitFunColors.sidebar.ink, modifier = Modifier.size(21.dp), ) Text( deviceName, fontSize = 14.sp, fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal, - color = MaterialTheme.colorScheme.onSurface, + color = openBitFunColors.sidebar.ink, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f), ) if (loading) { CircularProgressIndicator( - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = openBitFunColors.sidebar.muted, strokeWidth = 1.5.dp, modifier = Modifier.size(14.dp), ) diff --git a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarFooter.kt b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarFooter.kt index c491030452..077bb4ea55 100644 --- a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarFooter.kt +++ b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarFooter.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -30,6 +29,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.openbitfun.mobile.app.R import com.openbitfun.mobile.app.ui.common.SignedOutConnectionActions +import com.openbitfun.mobile.app.ui.theme.openBitFunColors internal const val SIDEBAR_NEW_CHAT_TEST_TAG: String = "app-sidebar-new-chat" internal const val SIDEBAR_SETTINGS_TEST_TAG: String = "app-sidebar-settings" @@ -47,8 +47,8 @@ internal fun SidebarAuthenticatedFooter(onOpenTools: () -> Unit, onOpenSettings: .widthIn(min = 104.dp) .height(48.dp) .clip(RoundedCornerShape(24.dp)) - .background(MaterialTheme.colorScheme.surface) - .border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(24.dp)) + .background(openBitFunColors.sidebar.raised) + .border(1.dp, openBitFunColors.sidebar.line, RoundedCornerShape(24.dp)) .clickable(role = Role.Button, onClick = onOpenTools) .semantics(mergeDescendants = true) { contentDescription = toolsLabel @@ -61,14 +61,14 @@ internal fun SidebarAuthenticatedFooter(onOpenTools: () -> Unit, onOpenSettings: Icon( painterResource(R.drawable.ic_symbol_wrench_and_screwdriver), contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface, + tint = openBitFunColors.sidebar.ink, modifier = Modifier.size(24.dp), ) Text( toolsLabel, fontSize = 15.sp, fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface, + color = openBitFunColors.sidebar.ink, ) } Box(Modifier.weight(1f)) @@ -98,5 +98,8 @@ internal fun SidebarSignedOutFooter( onScan = onScanDesktop, onOpenAccount = onOpenAccount, showScan = showScan, + quietBackground = openBitFunColors.sidebar.raised, + quietBorder = openBitFunColors.sidebar.line, + quietContent = openBitFunColors.sidebar.ink, ) } diff --git a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarHeader.kt b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarHeader.kt index 78f218af56..6cd5329d70 100644 --- a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarHeader.kt +++ b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarHeader.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.material3.Icon import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -29,6 +28,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.openbitfun.mobile.app.R +import com.openbitfun.mobile.app.ui.theme.openBitFunColors internal const val SIDEBAR_SEARCH_TEST_TAG: String = "app-sidebar-search" @@ -51,7 +51,7 @@ internal fun SidebarAuthenticatedHeader( stringResource(R.string.app_name), fontSize = 20.sp, fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onBackground, + color = openBitFunColors.sidebar.ink, modifier = Modifier.weight(1f), ) SidebarCircleButton( @@ -80,7 +80,7 @@ private fun SidebarSearchField(query: String, onQueryChange: (String) -> Unit) { .padding(top = 12.dp) .height(42.dp) .clip(RoundedCornerShape(8.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant) + .background(openBitFunColors.sidebar.hover) .padding(horizontal = 12.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, @@ -88,7 +88,7 @@ private fun SidebarSearchField(query: String, onQueryChange: (String) -> Unit) { Icon( painterResource(R.drawable.ic_symbol_magnifyingglass), contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, + tint = openBitFunColors.sidebar.muted, modifier = Modifier.size(18.dp), ) BasicTextField( @@ -97,15 +97,15 @@ private fun SidebarSearchField(query: String, onQueryChange: (String) -> Unit) { singleLine = true, textStyle = LocalTextStyle.current.copy( fontSize = 15.sp, - color = MaterialTheme.colorScheme.onSurface, + color = openBitFunColors.sidebar.ink, ), - cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface), + cursorBrush = SolidColor(openBitFunColors.sidebar.ink), decorationBox = { field -> if (query.isEmpty()) { Text( stringResource(R.string.sidebar_search_placeholder), fontSize = 15.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = openBitFunColors.sidebar.muted, ) } field() @@ -141,14 +141,14 @@ internal fun SidebarSignedOutHeader(onNewChat: () -> Unit) { Icon( painterResource(R.drawable.ic_symbol_square_and_pencil), contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface, + tint = openBitFunColors.sidebar.ink, modifier = Modifier.size(22.dp), ) Text( newChatLabel, fontSize = 16.sp, fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface, + color = openBitFunColors.sidebar.ink, ) } } diff --git a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarRemoteWorkspaceSection.kt b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarRemoteWorkspaceSection.kt index a3a508743e..8aa0478b7a 100644 --- a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarRemoteWorkspaceSection.kt +++ b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarRemoteWorkspaceSection.kt @@ -107,14 +107,14 @@ internal fun SidebarRemoteWorkspaceSection( stringResource(R.string.sidebar_devices), fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = openBitFunColors.sidebar.muted, ) Box(Modifier.weight(1f)) if (!connected && projectedDevices.isEmpty()) { Text( stringResource(R.string.sidebar_workspaces_offline), fontSize = 12.sp, - color = MaterialTheme.colorScheme.outline, + color = openBitFunColors.sidebar.subtle, modifier = Modifier.padding(end = 8.dp), ) } @@ -133,7 +133,7 @@ internal fun SidebarRemoteWorkspaceSection( else Icon( painterResource(R.drawable.ic_symbol_arrow_clockwise), contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, + tint = openBitFunColors.sidebar.muted, modifier = Modifier.size(17.dp), ) } @@ -274,7 +274,7 @@ private fun SidebarActiveDeviceBody( verticalAlignment = Alignment.CenterVertically, ) { Text(stringResource(R.string.sidebar_workspaces), fontSize = 14.sp, - fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.Medium, color = openBitFunColors.sidebar.muted, modifier = Modifier.weight(1f)) if (onAddWorkspace != null) { androidx.compose.material3.IconButton(onClick = onAddWorkspace, @@ -291,7 +291,7 @@ private fun SidebarActiveDeviceBody( Text( stringResource(R.string.sidebar_legacy_workspace_catalog), fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = openBitFunColors.sidebar.muted, modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp), ) } @@ -306,7 +306,7 @@ private fun SidebarActiveDeviceBody( Text( stringResource(R.string.sidebar_empty_workspaces), fontSize = 14.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = openBitFunColors.sidebar.muted, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 6.dp, bottom = 6.dp), ) } else { @@ -360,9 +360,9 @@ private fun SidebarActiveDeviceBody( painterResource(R.drawable.ic_symbol_folder), contentDescription = null, tint = if (entry.selected) { - MaterialTheme.colorScheme.onSurface + openBitFunColors.sidebar.ink } else { - MaterialTheme.colorScheme.onSurfaceVariant + openBitFunColors.sidebar.muted }, modifier = Modifier.size(21.dp), ) @@ -374,7 +374,7 @@ private fun SidebarActiveDeviceBody( } else { FontWeight.Normal }, - color = MaterialTheme.colorScheme.onSurface, + color = openBitFunColors.sidebar.ink, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f), @@ -396,7 +396,7 @@ private fun SidebarActiveDeviceBody( else R.drawable.ic_symbol_chevron_down, ), contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, + tint = openBitFunColors.sidebar.muted, modifier = Modifier.size(13.dp), ) } @@ -413,7 +413,7 @@ private fun SidebarActiveDeviceBody( Text( stringResource(R.string.sidebar_workspace_empty_sessions), fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = openBitFunColors.sidebar.muted, modifier = Modifier.fillMaxWidth() .padding(start = 26.dp, top = 6.dp, bottom = 6.dp), ) @@ -464,14 +464,14 @@ private fun DeviceLoadingRow(startPadding: Dp = 10.dp) { verticalAlignment = Alignment.CenterVertically, ) { CircularProgressIndicator( - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = openBitFunColors.sidebar.muted, strokeWidth = 1.5.dp, modifier = Modifier.size(14.dp), ) Text( stringResource(R.string.sidebar_device_loading), fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = openBitFunColors.sidebar.muted, ) } } @@ -488,13 +488,13 @@ private fun WorkspaceFailedRow(onRetry: () -> Unit) { Text( stringResource(R.string.sidebar_workspace_load_failed), fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = openBitFunColors.sidebar.muted, modifier = Modifier.weight(1f), ) Text( retryLabel, fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurface, + color = openBitFunColors.sidebar.ink, modifier = Modifier .clickable(role = Role.Button, onClick = onRetry) .semantics { contentDescription = retryLabel }, @@ -516,13 +516,13 @@ private fun DeviceFailedRow(onRetry: () -> Unit) { Text( stringResource(R.string.sidebar_device_load_failed), fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = openBitFunColors.sidebar.muted, modifier = Modifier.weight(1f), ) Text( retryLabel, fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurface, + color = openBitFunColors.sidebar.ink, modifier = Modifier .clickable(role = Role.Button, onClick = onRetry) .semantics { contentDescription = retryLabel }, @@ -550,19 +550,19 @@ private fun ConnectDesktopRow(onConnect: () -> Unit) { Icon( painterResource(R.drawable.ic_symbol_desktop), contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, + tint = openBitFunColors.sidebar.muted, modifier = Modifier.size(22.dp), ) Text( connectLabel, fontSize = 15.sp, - color = MaterialTheme.colorScheme.onSurface, + color = openBitFunColors.sidebar.ink, modifier = Modifier.weight(1f), ) Icon( painterResource(R.drawable.ic_symbol_chevron_right), contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, + tint = openBitFunColors.sidebar.muted, modifier = Modifier.size(13.dp), ) } @@ -589,7 +589,7 @@ private fun RemoteSessionRow( .fillMaxWidth() .height(44.dp) .clip(RoundedCornerShape(10.dp)) - .background(if (selected) MaterialTheme.colorScheme.surfaceVariant else openBitFunColors.transparent) + .background(if (selected) openBitFunColors.sidebar.selection else openBitFunColors.transparent) .onGloballyPositioned { coordinates -> anchorBounds = coordinates.boundsInWindow().toIntRect() } @@ -614,14 +614,14 @@ private fun RemoteSessionRow( Icon( painterResource(icon), contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, + tint = openBitFunColors.sidebar.muted, modifier = Modifier.size(19.dp), ) Text( sessionTitle, fontSize = 13.sp, fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal, - color = MaterialTheme.colorScheme.onSurface, + color = openBitFunColors.sidebar.ink, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f), @@ -634,7 +634,7 @@ private fun RemoteSessionRow( Icon( painterResource(R.drawable.ic_symbol_ellipsis), contentDescription = stringResource(R.string.session_actions), - tint = MaterialTheme.colorScheme.onSurfaceVariant, + tint = openBitFunColors.sidebar.muted, modifier = Modifier.size(18.dp), ) } @@ -680,7 +680,7 @@ private fun MoreRow( Text( moreLabel, fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = openBitFunColors.sidebar.muted, ) } } diff --git a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarSessionList.kt b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarSessionList.kt index 858c4b68e6..c49940b49e 100644 --- a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarSessionList.kt +++ b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/shell/sidebar/SidebarSessionList.kt @@ -16,7 +16,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -105,7 +104,7 @@ internal fun SidebarSessionList( ), fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = openBitFunColors.sidebar.muted, modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), ) } @@ -158,7 +157,7 @@ private fun SectionLabel(text: String, modifier: Modifier) { text, fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = openBitFunColors.sidebar.muted, modifier = modifier.fillMaxWidth().padding(top = 16.dp, bottom = 6.dp), ) } @@ -186,7 +185,7 @@ private fun SessionRow( .fillMaxWidth() .height(44.dp) .clip(RoundedCornerShape(10.dp)) - .background(if (selected) MaterialTheme.colorScheme.surfaceVariant else openBitFunColors.transparent) + .background(if (selected) openBitFunColors.sidebar.selection else openBitFunColors.transparent) .onGloballyPositioned { coordinates -> anchorBounds = coordinates.boundsInWindow().toIntRect() } @@ -204,14 +203,14 @@ private fun SessionRow( Icon( painterResource(R.drawable.ic_symbol_checkmark_circle), contentDescription = stringResource(R.string.sidebar_pinned), - tint = MaterialTheme.colorScheme.onSurfaceVariant, + tint = openBitFunColors.sidebar.muted, modifier = Modifier.size(18.dp), ) } Text( title, fontSize = 15.sp, - color = MaterialTheme.colorScheme.onSurface, + color = openBitFunColors.sidebar.ink, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f), @@ -228,7 +227,7 @@ private fun SessionRow( Icon( painterResource(R.drawable.ic_symbol_ellipsis), contentDescription = stringResource(R.string.session_actions), - tint = MaterialTheme.colorScheme.onSurfaceVariant, + tint = openBitFunColors.sidebar.muted, modifier = Modifier.size(18.dp), ) } @@ -252,7 +251,7 @@ private fun ArchivedDisclosureRow(count: Int, expanded: Boolean, onToggle: () -> .padding(top = 8.dp) .height(46.dp) .clip(RoundedCornerShape(10.dp)) - .background(if (expanded) MaterialTheme.colorScheme.surfaceVariant else openBitFunColors.transparent) + .background(if (expanded) openBitFunColors.sidebar.selection else openBitFunColors.transparent) .clickable(role = Role.Button, onClick = onToggle) .semantics(mergeDescendants = true) { contentDescription = archivedLabel @@ -265,14 +264,14 @@ private fun ArchivedDisclosureRow(count: Int, expanded: Boolean, onToggle: () -> Icon( painterResource(R.drawable.ic_symbol_archivebox), contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, + tint = openBitFunColors.sidebar.muted, modifier = Modifier.size(20.dp), ) Text( archivedLabel, fontSize = 14.sp, fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface, + color = openBitFunColors.sidebar.ink, modifier = Modifier.weight(1f), ) Box( @@ -280,14 +279,14 @@ private fun ArchivedDisclosureRow(count: Int, expanded: Boolean, onToggle: () -> .width(24.dp) .height(22.dp) .clip(RoundedCornerShape(11.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant), + .background(openBitFunColors.sidebar.selection), contentAlignment = Alignment.Center, ) { Text( count.toString(), fontSize = 12.sp, fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = openBitFunColors.sidebar.muted, ) } Icon( @@ -299,7 +298,7 @@ private fun ArchivedDisclosureRow(count: Int, expanded: Boolean, onToggle: () -> }, ), contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, + tint = openBitFunColors.sidebar.muted, modifier = Modifier.size(16.dp), ) } diff --git a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/theme/Theme.kt b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/theme/Theme.kt index b7d1abb357..2332a81199 100644 --- a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/theme/Theme.kt +++ b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/theme/Theme.kt @@ -138,9 +138,33 @@ internal data class OpenBitFunColors( val heroSurface: Color, val heroAccent: Color, val heroSecondary: Color, + val sidebar: SidebarColors, val code: CodeSyntaxColors, ) +/** + * The navigation chrome, held apart from the page palette above. + * + * The desktop client paints its sidebar from a separate family — `surface.chrome` + * one step off the scene, hairlines and selection fills carried as alpha over it + * — so the rail reads as structure rather than as another sheet of paper. These + * are those roles, one for one, and they belong to the sidebar only: a page that + * borrows them stops looking like the desktop, not more like it. + * + * [selection], [line] and [hover] are translucent on purpose. They are meant to + * composite over [background]; flattening them loses the rail's depth. + */ +internal data class SidebarColors( + val background: Color, + val raised: Color, + val line: Color, + val hover: Color, + val selection: Color, + val ink: Color, + val muted: Color, + val subtle: Color, +) + /** * What `CodeSyntaxTokenKind` looks like, one entry per kind that is not plain * text. Straight from the `code_*` colour elements of the HarmonyOS client. @@ -179,6 +203,16 @@ private val LightExtras = OpenBitFunColors( heroSurface = LightTokens.ConnectHeroSurface, heroAccent = LightTokens.ConnectHeroAccent, heroSecondary = LightTokens.ConnectHeroSecondary, + sidebar = SidebarColors( + background = LightTokens.SidebarBg, + raised = LightTokens.SidebarRaised, + line = LightTokens.SidebarLine, + hover = LightTokens.SidebarHover, + selection = LightTokens.SidebarSelection, + ink = LightTokens.SidebarInk, + muted = LightTokens.SidebarMuted, + subtle = LightTokens.SidebarSubtle, + ), code = CodeSyntaxColors( lineNumber = LightTokens.CodeLineNumber, keyword = LightTokens.CodeKeyword, @@ -210,6 +244,16 @@ private val DarkExtras = OpenBitFunColors( heroSurface = DarkTokens.ConnectHeroSurface, heroAccent = DarkTokens.ConnectHeroAccent, heroSecondary = DarkTokens.ConnectHeroSecondary, + sidebar = SidebarColors( + background = DarkTokens.SidebarBg, + raised = DarkTokens.SidebarRaised, + line = DarkTokens.SidebarLine, + hover = DarkTokens.SidebarHover, + selection = DarkTokens.SidebarSelection, + ink = DarkTokens.SidebarInk, + muted = DarkTokens.SidebarMuted, + subtle = DarkTokens.SidebarSubtle, + ), code = CodeSyntaxColors( lineNumber = DarkTokens.CodeLineNumber, keyword = DarkTokens.CodeKeyword, diff --git a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/theme/generated/MobileDesignTokens.kt b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/theme/generated/MobileDesignTokens.kt index aba82c2ece..11823306b9 100644 --- a/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/theme/generated/MobileDesignTokens.kt +++ b/src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/theme/generated/MobileDesignTokens.kt @@ -42,6 +42,15 @@ internal object MobileDesignColors { val FloatingBorder = Color(0x18000000) val Soft = Color(0xFFF4F3F0) val FloatingPanelBg = Color(0xFFF7F7F5) + val SidebarBg = Color(0xFFF8F8F9) + val SidebarBgFade = Color(0x00F8F8F9) + val SidebarRaised = Color(0xFFFFFFFF) + val SidebarLine = Color(0x14101A27) + val SidebarHover = Color(0xFFF3F3F5) + val SidebarSelection = Color(0x14000000) + val SidebarInk = Color(0xCC000000) + val SidebarMuted = Color(0x99000000) + val SidebarSubtle = Color(0x66000000) val StatusSuccess = Color(0xFF27C46A) val StatusDanger = Color(0xFFE04F4F) val CodeLineNumber = Color(0xFFAAA69D) @@ -96,6 +105,15 @@ internal object MobileDesignColors { val FloatingBorder = Color(0x18000000) val Soft = Color(0xFF2D2C28) val FloatingPanelBg = Color(0xFF1E1E1C) + val SidebarBg = Color(0xFF0E0E10) + val SidebarBgFade = Color(0x000E0E10) + val SidebarRaised = Color(0xFF1C1C1F) + val SidebarLine = Color(0x1FFFFFFF) + val SidebarHover = Color(0x0FFFFFFF) + val SidebarSelection = Color(0x1FFFFFFF) + val SidebarInk = Color(0xFFE8E8E8) + val SidebarMuted = Color(0xFFB0B0B0) + val SidebarSubtle = Color(0xFF858585) val StatusSuccess = Color(0xFF3BD47B) val StatusDanger = Color(0xFFFF6B6B) val CodeLineNumber = Color(0xFF77756E) diff --git a/src/apps/mobile/design-system/README.md b/src/apps/mobile/design-system/README.md index 9d4d9baad9..69288d318a 100644 --- a/src/apps/mobile/design-system/README.md +++ b/src/apps/mobile/design-system/README.md @@ -57,6 +57,41 @@ Platform renderers may choose the listed size variant for available width, but must not substitute a different content purpose merely to obtain a preferred metric. +## Sidebar chrome + +The left sidebar is chrome, not another page. The desktop client paints it with +its own surface family — one step off the scene the conversation sits on — so +the mobile clients carry a dedicated `sidebar_*` token family rather than +reusing the page roles (`page_bg`, `card`, `soft`, `line`, `ink`, `muted`, +`subtle`). Those page roles stay exactly as they were for conversations, +sheets, and action surfaces; only the rail moved. + +Each token maps one-to-one onto a desktop semantic role from +`design-system/packages/theme-openbitfun`: + +| Mobile token | Desktop role | Used for | +| --- | --- | --- | +| `sidebar_bg` | `color.surface.chrome` | the rail itself, and the pane behind it | +| `sidebar_bg_fade` | same hue at zero alpha | the top stop of the footer fade | +| `sidebar_raised` | `color.surface.raised` | circle buttons, the quiet footer action | +| `sidebar_line` | `color.border.subtle` | hairlines and control borders | +| `sidebar_hover` | `color.action.quiet.hover` | the search field fill | +| `sidebar_selection` | `color.selection.surface` | the selected row and device fills | +| `sidebar_ink` | `color.content.primary` | titles, row labels, icon tint | +| `sidebar_muted` | `color.content.secondary` | supporting row text | +| `sidebar_subtle` | `color.content.caption` | placeholders and offline devices | + +`sidebar_line`, `sidebar_hover` (dark), and `sidebar_selection` are stored as +`#AARRGGBB` on purpose: desktop defines them as alpha over the chrome surface, +and keeping the alpha lets them composite the same way instead of baking a +flattened value that drifts the moment the surface changes. + +Sidebar components shared with page surfaces take their layer from the caller +rather than assuming it — Android's `SidebarCircleButton` and +`SignedOutConnectionActions` both expose background/border/content parameters, +and the sheet presented from the sidebar (the workspace picker) stays on page +roles because a sheet is not chrome. + ## Simulator captures The native galleries can be launched without changing the normal app path: diff --git a/src/apps/mobile/design-system/components/mobile-components.json b/src/apps/mobile/design-system/components/mobile-components.json index e5b953b5c4..166c1731bc 100644 --- a/src/apps/mobile/design-system/components/mobile-components.json +++ b/src/apps/mobile/design-system/components/mobile-components.json @@ -8,18 +8,25 @@ "tokens": ["conversation_header_height"], "platformNotes": "Both signed-in header rows use conversationHeaderHeight from the same safe-area origin, without a sidebar-only top inset. Preserve this height when a subtitle appears or disappears. Anchor drawer content scaling vertically at half the header height so reveal animation cannot shift its control centerline." }, + "sidebar_chrome": { + "purpose": "Give the workspace sidebar the desktop client's structural chrome instead of the conversation page surface.", + "anatomy": ["chrome_surface", "header_row", "scroll_fade", "raised_control", "hairline"], + "states": ["light", "dark", "signed_out", "signed_in"], + "tokens": ["sidebar_bg", "sidebar_bg_fade", "sidebar_raised", "sidebar_line", "sidebar_hover", "sidebar_selection", "sidebar_ink", "sidebar_muted", "sidebar_subtle"], + "platformNotes": "The family mirrors the desktop semantic theme one role at a time: sidebar_bg is surface.chrome, sidebar_raised is surface.raised, sidebar_line is border.subtle, sidebar_hover is action.quiet.hover, sidebar_selection is selection.surface, and sidebar_ink / sidebar_muted / sidebar_subtle are content.primary / content.secondary / content.caption. The sidebar therefore sits one step away from the conversation surface in both appearance modes, as the desktop navigation panel does beside the scene. Alpha roles composite over sidebar_bg and must not be flattened into opaque literals. Conversation, sheet and action surfaces keep the paper-and-ink page tokens; only sidebar chrome uses this family." + }, "sidebar_device_selector": { "purpose": "Select one remote device and show its workspace panel below the device list, matching mobile web.", "anatomy": ["device_icon", "device_name", "connection_indicator", "selected_workspace_panel"], "states": ["selected", "online", "offline", "loading"], - "tokens": ["soft", "ink", "muted", "status_success"], - "platformNotes": "Device rows have no disclosure chevron. Re-selecting the current device does not collapse its workspaces or restart a healthy connection. Use a soft selected surface and normal ink for device identity; only the separate status indicator expresses reachability. Do not add a connection status text row or recovery banner below the device. Automatic reconnect stays quiet; tapping the selected failed or disconnected device retries the connection. Workspace disclosure remains independent and survives refresh and device switching. Compact and wide layouts use the same selection semantics." + "tokens": ["sidebar_selection", "sidebar_ink", "sidebar_muted", "status_success"], + "platformNotes": "Device rows have no disclosure chevron. Re-selecting the current device does not collapse its workspaces or restart a healthy connection. Use the sidebar selection surface and normal ink for device identity; only the separate status indicator expresses reachability. Do not add a connection status text row or recovery banner below the device. Automatic reconnect stays quiet; tapping the selected failed or disconnected device retries the connection. Workspace disclosure remains independent and survives refresh and device switching. Compact and wide layouts use the same selection semantics." }, "sidebar_tools_footer": { "purpose": "Open controlled-device tools without creating a chat; keep settings separately reachable.", "anatomy": ["tools_icon_and_label", "flexible_gap", "settings_button"], "states": ["enabled", "disabled", "pressed"], - "tokens": ["card", "line", "ink"], + "tokens": ["sidebar_raised", "sidebar_line", "sidebar_ink"], "platformNotes": "Use Tools / 工具 for both the sidebar entry and destination title. Render a native wrench-and-screwdriver symbol (or the existing platform vector) in a 24-unit slot, 8-unit label gap, 15-unit medium text, 14-unit horizontal padding and a minimum 104 by 48 pill. Keep the 48-unit settings action separate. Use a quiet surface and outline without raised shadows. Compact and wide sidebars reuse a pure footer component with state and callbacks; create-session validation must not affect tools appearance." }, "circle_control": { diff --git a/src/apps/mobile/design-system/preview/generated/mobile-design-data.js b/src/apps/mobile/design-system/preview/generated/mobile-design-data.js index fff122e026..24534fa11f 100644 --- a/src/apps/mobile/design-system/preview/generated/mobile-design-data.js +++ b/src/apps/mobile/design-system/preview/generated/mobile-design-data.js @@ -138,6 +138,42 @@ export const mobileTokens = { "light": "#F7F7F5", "dark": "#1E1E1C" }, + "sidebar_bg": { + "light": "#F8F8F9", + "dark": "#0E0E10" + }, + "sidebar_bg_fade": { + "light": "#00F8F8F9", + "dark": "#000E0E10" + }, + "sidebar_raised": { + "light": "#FFFFFF", + "dark": "#1C1C1F" + }, + "sidebar_line": { + "light": "#14101A27", + "dark": "#1FFFFFFF" + }, + "sidebar_hover": { + "light": "#F3F3F5", + "dark": "#0FFFFFFF" + }, + "sidebar_selection": { + "light": "#14000000", + "dark": "#1FFFFFFF" + }, + "sidebar_ink": { + "light": "#CC000000", + "dark": "#E8E8E8" + }, + "sidebar_muted": { + "light": "#99000000", + "dark": "#B0B0B0" + }, + "sidebar_subtle": { + "light": "#66000000", + "dark": "#858585" + }, "status_success": { "light": "#27C46A", "dark": "#3BD47B" @@ -414,6 +450,34 @@ export const mobileComponents = { ], "platformNotes": "Both signed-in header rows use conversationHeaderHeight from the same safe-area origin, without a sidebar-only top inset. Preserve this height when a subtitle appears or disappears. Anchor drawer content scaling vertically at half the header height so reveal animation cannot shift its control centerline." }, + "sidebar_chrome": { + "purpose": "Give the workspace sidebar the desktop client's structural chrome instead of the conversation page surface.", + "anatomy": [ + "chrome_surface", + "header_row", + "scroll_fade", + "raised_control", + "hairline" + ], + "states": [ + "light", + "dark", + "signed_out", + "signed_in" + ], + "tokens": [ + "sidebar_bg", + "sidebar_bg_fade", + "sidebar_raised", + "sidebar_line", + "sidebar_hover", + "sidebar_selection", + "sidebar_ink", + "sidebar_muted", + "sidebar_subtle" + ], + "platformNotes": "The family mirrors the desktop semantic theme one role at a time: sidebar_bg is surface.chrome, sidebar_raised is surface.raised, sidebar_line is border.subtle, sidebar_hover is action.quiet.hover, sidebar_selection is selection.surface, and sidebar_ink / sidebar_muted / sidebar_subtle are content.primary / content.secondary / content.caption. The sidebar therefore sits one step away from the conversation surface in both appearance modes, as the desktop navigation panel does beside the scene. Alpha roles composite over sidebar_bg and must not be flattened into opaque literals. Conversation, sheet and action surfaces keep the paper-and-ink page tokens; only sidebar chrome uses this family." + }, "sidebar_device_selector": { "purpose": "Select one remote device and show its workspace panel below the device list, matching mobile web.", "anatomy": [ @@ -429,12 +493,12 @@ export const mobileComponents = { "loading" ], "tokens": [ - "soft", - "ink", - "muted", + "sidebar_selection", + "sidebar_ink", + "sidebar_muted", "status_success" ], - "platformNotes": "Device rows have no disclosure chevron. Re-selecting the current device does not collapse its workspaces or restart a healthy connection. Use a soft selected surface and normal ink for device identity; only the separate status indicator expresses reachability. Do not add a connection status text row or recovery banner below the device. Automatic reconnect stays quiet; tapping the selected failed or disconnected device retries the connection. Workspace disclosure remains independent and survives refresh and device switching. Compact and wide layouts use the same selection semantics." + "platformNotes": "Device rows have no disclosure chevron. Re-selecting the current device does not collapse its workspaces or restart a healthy connection. Use the sidebar selection surface and normal ink for device identity; only the separate status indicator expresses reachability. Do not add a connection status text row or recovery banner below the device. Automatic reconnect stays quiet; tapping the selected failed or disconnected device retries the connection. Workspace disclosure remains independent and survives refresh and device switching. Compact and wide layouts use the same selection semantics." }, "sidebar_tools_footer": { "purpose": "Open controlled-device tools without creating a chat; keep settings separately reachable.", @@ -449,9 +513,9 @@ export const mobileComponents = { "pressed" ], "tokens": [ - "card", - "line", - "ink" + "sidebar_raised", + "sidebar_line", + "sidebar_ink" ], "platformNotes": "Use Tools / 工具 for both the sidebar entry and destination title. Render a native wrench-and-screwdriver symbol (or the existing platform vector) in a 24-unit slot, 8-unit label gap, 15-unit medium text, 14-unit horizontal padding and a minimum 104 by 48 pill. Keep the 48-unit settings action separate. Use a quiet surface and outline without raised shadows. Compact and wide sidebars reuse a pure footer component with state and callbacks; create-session validation must not affect tools appearance." }, diff --git a/src/apps/mobile/design-system/tokens/mobile-tokens.json b/src/apps/mobile/design-system/tokens/mobile-tokens.json index 46d6916e04..f34770c638 100644 --- a/src/apps/mobile/design-system/tokens/mobile-tokens.json +++ b/src/apps/mobile/design-system/tokens/mobile-tokens.json @@ -137,6 +137,42 @@ "light": "#F7F7F5", "dark": "#1E1E1C" }, + "sidebar_bg": { + "light": "#F8F8F9", + "dark": "#0E0E10" + }, + "sidebar_bg_fade": { + "light": "#00F8F8F9", + "dark": "#000E0E10" + }, + "sidebar_raised": { + "light": "#FFFFFF", + "dark": "#1C1C1F" + }, + "sidebar_line": { + "light": "#14101A27", + "dark": "#1FFFFFFF" + }, + "sidebar_hover": { + "light": "#F3F3F5", + "dark": "#0FFFFFFF" + }, + "sidebar_selection": { + "light": "#14000000", + "dark": "#1FFFFFFF" + }, + "sidebar_ink": { + "light": "#CC000000", + "dark": "#E8E8E8" + }, + "sidebar_muted": { + "light": "#99000000", + "dark": "#B0B0B0" + }, + "sidebar_subtle": { + "light": "#66000000", + "dark": "#858585" + }, "status_success": { "light": "#27C46A", "dark": "#3BD47B" diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/generated/MobileDesignTokens.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/generated/MobileDesignTokens.ets index fd3a78070e..96075ed014 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/generated/MobileDesignTokens.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/generated/MobileDesignTokens.ets @@ -44,6 +44,15 @@ export class MobileDesignColors { static readonly floatingBorder: MobileColorPair = new MobileColorPair("#18000000", "#18000000"); static readonly soft: MobileColorPair = new MobileColorPair("#F4F3F0", "#2D2C28"); static readonly floatingPanelBg: MobileColorPair = new MobileColorPair("#F7F7F5", "#1E1E1C"); + static readonly sidebarBg: MobileColorPair = new MobileColorPair("#F8F8F9", "#0E0E10"); + static readonly sidebarBgFade: MobileColorPair = new MobileColorPair("#00F8F8F9", "#000E0E10"); + static readonly sidebarRaised: MobileColorPair = new MobileColorPair("#FFFFFF", "#1C1C1F"); + static readonly sidebarLine: MobileColorPair = new MobileColorPair("#14101A27", "#1FFFFFFF"); + static readonly sidebarHover: MobileColorPair = new MobileColorPair("#F3F3F5", "#0FFFFFFF"); + static readonly sidebarSelection: MobileColorPair = new MobileColorPair("#14000000", "#1FFFFFFF"); + static readonly sidebarInk: MobileColorPair = new MobileColorPair("#CC000000", "#E8E8E8"); + static readonly sidebarMuted: MobileColorPair = new MobileColorPair("#99000000", "#B0B0B0"); + static readonly sidebarSubtle: MobileColorPair = new MobileColorPair("#66000000", "#858585"); static readonly statusSuccess: MobileColorPair = new MobileColorPair("#27C46A", "#3BD47B"); static readonly statusDanger: MobileColorPair = new MobileColorPair("#E04F4F", "#FF6B6B"); static readonly codeLineNumber: MobileColorPair = new MobileColorPair("#AAA69D", "#77756E"); diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/AppSidebar.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/AppSidebar.ets index 2ff40fd2c4..ef88080b86 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/AppSidebar.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/AppSidebar.ets @@ -3,7 +3,19 @@ import { SavedRuntimeConnection } from '../../model/RemoteModels'; import { WorkspaceToolsIntent } from '../state/WorkspaceToolsState'; import { MobileDesignGeometry, MobileDesignTypography } from '../../generated/MobileDesignTokens'; import { RemoteI18n } from '../../i18n/RemoteI18n'; -import { CARD, INK, LINE, PAGE_BG, PAGE_BG_FADE, SHADOW_FAINT, SOFT, SUBTLE, TRANSPARENT } from './Theme'; +import { + CARD, + INK, + SHADOW_FAINT, + SIDEBAR_BG, + SIDEBAR_BG_FADE, + SIDEBAR_HOVER, + SIDEBAR_INK, + SIDEBAR_LINE, + SIDEBAR_RAISED, + SIDEBAR_SUBTLE, + TRANSPARENT, +} from './Theme'; import { SidebarToggleButton } from './SidebarToggleButton'; import { SidebarGlyph } from './SidebarGlyphs'; import { SidebarConnectionActionPolicy } from '../policy/SidebarConnectionActionPolicy'; @@ -69,7 +81,7 @@ export struct AppSidebar { Text(RemoteI18n.t('miniapp.title')) .fontSize(MobileDesignTypography.bodyLarge.size) .fontWeight(FontWeight.Medium) - .fontColor(INK) + .fontColor(SIDEBAR_INK) Blank() }.width('100%').alignItems(VerticalAlign.Center) } @@ -102,7 +114,7 @@ export struct AppSidebar { .hitTestBehavior(HitTestMode.None) .linearGradient({ angle: 180, - colors: [[PAGE_BG_FADE, 0.0], [PAGE_BG, 0.62]] + colors: [[SIDEBAR_BG_FADE, 0.0], [SIDEBAR_BG, 0.62]] }) } @@ -118,7 +130,7 @@ export struct AppSidebar { .width('100%') .height('100%') .padding({ left: 20, right: 20, top: 0, bottom: 16 }) - .backgroundColor(PAGE_BG) + .backgroundColor(SIDEBAR_BG) } @Builder @@ -131,7 +143,7 @@ export struct AppSidebar { Text('OpenBitFun') .fontSize(MobileDesignTypography.headlineMedium.size) .fontWeight(FontWeight.Medium) - .fontColor(INK) + .fontColor(SIDEBAR_INK) Blank() // Right-aligned so the search and collapse controls keep the same // position whether or not the source-specific view-settings entry shows. @@ -158,8 +170,8 @@ export struct AppSidebar { .width(44) .height(44) .padding(0) - .backgroundColor(CARD) - .border({ width: 0.5, color: LINE }) + .backgroundColor(SIDEBAR_RAISED) + .border({ width: 0.5, color: SIDEBAR_LINE }) .borderRadius(22) .shadow({ radius: 18, color: SHADOW_FAINT, offsetY: 5 }) .stateEffect(true) @@ -188,10 +200,10 @@ export struct AppSidebar { .width('100%') .height(42) .fontSize(MobileDesignTypography.bodyMedium.size) - .fontColor(INK) - .placeholderColor(SUBTLE) + .fontColor(SIDEBAR_INK) + .placeholderColor(SIDEBAR_SUBTLE) .padding(0) - .backgroundColor(SOFT) + .backgroundColor(SIDEBAR_HOVER) .borderRadius(8) .margin({ top: 12 }) .onChange((value: string) => { @@ -211,13 +223,13 @@ export struct AppSidebar { Row({ space: 14 }) { SymbolGlyph($r('sys.symbol.desktop')) .fontSize(23) - .fontColor([INK]) + .fontColor([SIDEBAR_INK]) .width(24) .height(24) Text(RemoteI18n.t('sidebar.devices')) .fontSize(MobileDesignTypography.bodyLarge.size) .fontWeight(FontWeight.Medium) - .fontColor(INK) + .fontColor(SIDEBAR_INK) } .layoutWeight(1) .height(50) @@ -255,15 +267,15 @@ export struct AppSidebar { Text(RemoteI18n.t('sidebar.scanToConnect')) .fontSize(MobileDesignTypography.bodyMedium.size) .fontWeight(FontWeight.Medium) - .fontColor(INK) + .fontColor(SIDEBAR_INK) .maxLines(1) .textOverflow({ overflow: TextOverflow.Clip }) } .width('100%') .height(48) .padding(0) - .backgroundColor(CARD) - .border({ width: 1, color: LINE }) + .backgroundColor(SIDEBAR_RAISED) + .border({ width: 1, color: SIDEBAR_LINE }) .borderRadius(16) .onClick(() => { this.onScanDesktop(); diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/SidebarDeviceGroup.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/SidebarDeviceGroup.ets index a53f565999..31961aace7 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/SidebarDeviceGroup.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/SidebarDeviceGroup.ets @@ -17,10 +17,9 @@ import { ConversationSessionFilterPolicy } from '../policy/ConversationSessionFi import { SidebarDirectoryPreviewPolicy } from '../policy/SidebarDirectoryPreviewPolicy'; import { HarnessProfileMenu } from './HarnessProfileMenu'; import { - INK, - PAGE_BG, - MUTED, - SOFT, + SIDEBAR_INK, + SIDEBAR_MUTED, + SIDEBAR_SELECTION, STATUS_SUCCESS, TRANSPARENT, } from './Theme'; @@ -94,7 +93,7 @@ struct SidebarWorkspaceGroup { SymbolGlyph(this.expanded ? $r('sys.symbol.chevron_down') : $r('sys.symbol.chevron_right')) .fontSize(13) - .fontColor([MUTED]) + .fontColor([SIDEBAR_MUTED]) .hitTestBehavior(HitTestMode.None) } .width(24) @@ -108,14 +107,14 @@ struct SidebarWorkspaceGroup { Row({ space: 10 }) { SymbolGlyph($r('sys.symbol.folder')) .fontSize(19) - .fontColor([this.currentWorkspace ? INK : MUTED]) + .fontColor([this.currentWorkspace ? SIDEBAR_INK : SIDEBAR_MUTED]) .width(21) .height(21) .hitTestBehavior(HitTestMode.None) Text(this.name || this.basename(this.path)) .fontSize(MobileDesignTypography.titleSmall.size) .fontWeight(this.currentWorkspace ? FontWeight.Medium : FontWeight.Regular) - .fontColor(INK) + .fontColor(SIDEBAR_INK) .layoutWeight(1) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) @@ -135,7 +134,7 @@ struct SidebarWorkspaceGroup { Stack({ alignContent: Alignment.Center }) { SymbolGlyph($r('sys.symbol.plus')) .fontSize(17) - .fontColor([INK]) + .fontColor([SIDEBAR_INK]) .hitTestBehavior(HitTestMode.None) } .width(30) @@ -182,10 +181,10 @@ struct SidebarWorkspaceGroup { LoadingProgress() .width(13) .height(13) - .color(MUTED) + .color(SIDEBAR_MUTED) Text(RemoteI18n.t('common.loading')) .fontSize(MobileDesignTypography.bodySmall.size) - .fontColor(MUTED) + .fontColor(SIDEBAR_MUTED) } .width('100%') .height(40) @@ -198,11 +197,11 @@ struct SidebarWorkspaceGroup { Row({ space: 8 }) { Text(RemoteI18n.t('sidebar.deviceLoadFailed')) .fontSize(MobileDesignTypography.bodySmall.size) - .fontColor(MUTED) + .fontColor(SIDEBAR_MUTED) .layoutWeight(1) Text(RemoteI18n.t('common.retry')) .fontSize(MobileDesignTypography.bodySmall.size) - .fontColor(INK) + .fontColor(SIDEBAR_INK) } .width('100%') .height(40) @@ -219,7 +218,7 @@ struct SidebarWorkspaceGroup { Stack({ alignContent: Alignment.BottomStart }) { SymbolGlyph(this.sessionGlyph(item)) .fontSize(17) - .fontColor([MUTED]) + .fontColor([SIDEBAR_MUTED]) .width(19) .height(19) if (this.activeDevice && item.id === this.runningSessionId) { @@ -235,7 +234,7 @@ struct SidebarWorkspaceGroup { Text(item.title || RemoteI18n.t('sidebar.untitled')) .fontSize(MobileDesignTypography.bodySmall.size) .fontWeight(item.id === this.selectedSessionId ? FontWeight.Bold : FontWeight.Regular) - .fontColor(INK) + .fontColor(SIDEBAR_INK) .layoutWeight(1) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) @@ -245,7 +244,7 @@ struct SidebarWorkspaceGroup { .height(44) .padding({ left: this.bodyIndent + 34, right: 10 }) .alignItems(VerticalAlign.Center) - .backgroundColor(item.id === this.selectedSessionId ? SOFT : TRANSPARENT) + .backgroundColor(item.id === this.selectedSessionId ? SIDEBAR_SELECTION : TRANSPARENT) .borderRadius(10) .onClick(() => { this.onOpenSession(item); @@ -260,10 +259,10 @@ struct SidebarWorkspaceGroup { Row({ space: 8 }) { Text('···') .fontSize(MobileDesignTypography.bodySmall.size) - .fontColor(MUTED) + .fontColor(SIDEBAR_MUTED) Text(RemoteI18n.f('sidebar.moreSessions', String(this.hiddenSessionCount()))) .fontSize(MobileDesignTypography.bodySmall.size) - .fontColor(MUTED) + .fontColor(SIDEBAR_MUTED) } .width('100%') .height(40) @@ -423,10 +422,10 @@ export struct SidebarDeviceGroup { LoadingProgress() .width(14) .height(14) - .color(MUTED) + .color(SIDEBAR_MUTED) Text(RemoteI18n.t('common.loading')) .fontSize(MobileDesignTypography.bodySmall.size) - .fontColor(MUTED) + .fontColor(SIDEBAR_MUTED) } .width('100%') .height(40) @@ -440,11 +439,11 @@ export struct SidebarDeviceGroup { Text(this.device.errorText.length > 0 ? this.device.errorText : RemoteI18n.t('sidebar.deviceLoadFailed')) .fontSize(MobileDesignTypography.bodySmall.size) - .fontColor(MUTED) + .fontColor(SIDEBAR_MUTED) .layoutWeight(1) Text(RemoteI18n.t('common.retry')) .fontSize(MobileDesignTypography.bodySmall.size) - .fontColor(INK) + .fontColor(SIDEBAR_INK) .onClick(() => { this.onRetry(); }) @@ -459,7 +458,7 @@ export struct SidebarDeviceGroup { private EmptyWorkspaces() { Text(RemoteI18n.t('sidebar.emptyWorkspaces')) .fontSize(MobileDesignTypography.bodyMedium.size) - .fontColor(MUTED) + .fontColor(SIDEBAR_MUTED) .width('100%') .padding({ left: 10, top: 6, bottom: 6 }) } @@ -469,10 +468,10 @@ export struct SidebarDeviceGroup { Row({ space: 8 }) { Text('···') .fontSize(MobileDesignTypography.bodySmall.size) - .fontColor(MUTED) + .fontColor(SIDEBAR_MUTED) Text(RemoteI18n.f('sidebar.moreWorkspaces', String(this.hiddenWorkspaceCount()))) .fontSize(MobileDesignTypography.bodySmall.size) - .fontColor(MUTED) + .fontColor(SIDEBAR_MUTED) } .width('100%') .height(40) diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/SidebarGlyphs.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/SidebarGlyphs.ets index 1b841d41c1..fc1b3b1ae5 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/SidebarGlyphs.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/SidebarGlyphs.ets @@ -1,11 +1,11 @@ -import { CARD, STATUS_SUCCESS, INK, MUTED } from './Theme'; +import { SIDEBAR_INK, SIDEBAR_MUTED, SIDEBAR_RAISED, STATUS_SUCCESS } from './Theme'; import { TemplateIcon } from './TemplateIcon'; @ComponentV2 export struct SidebarGlyph { @Param kind: string = ''; @Param connectionState: string = ''; - @Param tint: ResourceColor = INK; + @Param tint: ResourceColor = SIDEBAR_INK; build() { if (this.kind === 'session_more') { @@ -34,9 +34,9 @@ export struct SidebarGlyph { @Builder private MoreDots() { Row({ space: 3 }) { - Text('').width(3.5).height(3.5).backgroundColor(MUTED).borderRadius(2) - Text('').width(3.5).height(3.5).backgroundColor(MUTED).borderRadius(2) - Text('').width(3.5).height(3.5).backgroundColor(MUTED).borderRadius(2) + Text('').width(3.5).height(3.5).backgroundColor(SIDEBAR_MUTED).borderRadius(2) + Text('').width(3.5).height(3.5).backgroundColor(SIDEBAR_MUTED).borderRadius(2) + Text('').width(3.5).height(3.5).backgroundColor(SIDEBAR_MUTED).borderRadius(2) } .height(8) .alignItems(VerticalAlign.Center) @@ -50,7 +50,7 @@ export struct SidebarGlyph { Text('').width(8).height(8).backgroundColor(STATUS_SUCCESS).borderRadius(4) .position({ x: 24, y: 22 }) } else { - TemplateIcon({ src: $r('app.media.remote_logo'), iconWidth: 34, iconHeight: 34, tint: MUTED }) + TemplateIcon({ src: $r('app.media.remote_logo'), iconWidth: 34, iconHeight: 34, tint: SIDEBAR_MUTED }) } } .width(35).height(34) @@ -59,17 +59,17 @@ export struct SidebarGlyph { @Builder private Search() { SymbolGlyph($r('sys.symbol.magnifyingglass')) - .fontSize(22).fontColor([INK]).width(24).height(24) + .fontSize(22).fontColor([SIDEBAR_INK]).width(24).height(24) } @Builder private Notebook() { Stack() { - Text('').width(22).height(24).borderRadius(5).border({ width: 1.5, color: INK }) + Text('').width(22).height(24).borderRadius(5).border({ width: 1.5, color: SIDEBAR_INK }) .position({ x: 8, y: 5 }) - Text('').width(4).height(4).borderRadius(2).backgroundColor(INK) + Text('').width(4).height(4).borderRadius(2).backgroundColor(SIDEBAR_INK) .position({ x: 5, y: 11 }) - Text('').width(4).height(4).borderRadius(2).backgroundColor(INK) + Text('').width(4).height(4).borderRadius(2).backgroundColor(SIDEBAR_INK) .position({ x: 5, y: 20 }) } .width(34).height(34) @@ -78,11 +78,11 @@ export struct SidebarGlyph { @Builder private Clock() { Stack() { - Text('').width(26).height(26).borderRadius(13).border({ width: 1.5, color: INK }) + Text('').width(26).height(26).borderRadius(13).border({ width: 1.5, color: SIDEBAR_INK }) .position({ x: 4, y: 4 }) - Text('').width(1.5).height(9).backgroundColor(INK).borderRadius(2) + Text('').width(1.5).height(9).backgroundColor(SIDEBAR_INK).borderRadius(2) .position({ x: 18, y: 10 }) - Text('').width(9).height(1.5).backgroundColor(INK).borderRadius(2) + Text('').width(9).height(1.5).backgroundColor(SIDEBAR_INK).borderRadius(2) .position({ x: 18, y: 20 }) } .width(34).height(34) @@ -101,15 +101,15 @@ export struct SidebarGlyph { @Builder private CodeFlower() { Stack() { - Text('').width(16).height(16).borderRadius(8).border({ width: 1.5, color: INK }) - .backgroundColor(CARD).position({ x: 9, y: 1 }) - Text('').width(16).height(16).borderRadius(8).border({ width: 1.5, color: INK }) - .backgroundColor(CARD).position({ x: 17, y: 9 }) - Text('').width(16).height(16).borderRadius(8).border({ width: 1.5, color: INK }) - .backgroundColor(CARD).position({ x: 9, y: 17 }) - Text('').width(16).height(16).borderRadius(8).border({ width: 1.5, color: INK }) - .backgroundColor(CARD).position({ x: 1, y: 9 }) - Text('').width(14).height(14).borderRadius(7).backgroundColor(CARD) + Text('').width(16).height(16).borderRadius(8).border({ width: 1.5, color: SIDEBAR_INK }) + .backgroundColor(SIDEBAR_RAISED).position({ x: 9, y: 1 }) + Text('').width(16).height(16).borderRadius(8).border({ width: 1.5, color: SIDEBAR_INK }) + .backgroundColor(SIDEBAR_RAISED).position({ x: 17, y: 9 }) + Text('').width(16).height(16).borderRadius(8).border({ width: 1.5, color: SIDEBAR_INK }) + .backgroundColor(SIDEBAR_RAISED).position({ x: 9, y: 17 }) + Text('').width(16).height(16).borderRadius(8).border({ width: 1.5, color: SIDEBAR_INK }) + .backgroundColor(SIDEBAR_RAISED).position({ x: 1, y: 9 }) + Text('').width(14).height(14).borderRadius(7).backgroundColor(SIDEBAR_RAISED) .position({ x: 10, y: 10 }) } .width(34).height(34) @@ -125,7 +125,7 @@ export struct SidebarGlyph { @Builder private Dot() { - Text('').width(5).height(5).borderRadius(3).backgroundColor(INK) + Text('').width(5).height(5).borderRadius(3).backgroundColor(SIDEBAR_INK) } @Builder diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/SidebarNavigationIcons.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/SidebarNavigationIcons.ets index 29d960975e..726de9139d 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/SidebarNavigationIcons.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/SidebarNavigationIcons.ets @@ -1,4 +1,4 @@ -import { INK, MUTED } from './Theme'; +import { SIDEBAR_INK, SIDEBAR_MUTED } from './Theme'; import { TemplateIcon } from './TemplateIcon'; /** Desktop-provided device artwork normalized as a theme-tinted template icon. */ @@ -6,7 +6,7 @@ import { TemplateIcon } from './TemplateIcon'; export struct SidebarDeviceIcon { @Param deviceName: string = ''; @Param iconSize: number = 24; - @Param tint: ResourceColor = INK; + @Param tint: ResourceColor = SIDEBAR_INK; build() { Stack({ alignContent: Alignment.Center }) { @@ -74,7 +74,7 @@ export struct SidebarDeviceIcon { export struct SidebarChevronIcon { @Param expanded: boolean = false; @Param iconSize: number = 14; - @Param tint: ResourceColor = MUTED; + @Param tint: ResourceColor = SIDEBAR_MUTED; build() { SymbolGlyph(this.expanded ? diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/SidebarToolsFooter.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/SidebarToolsFooter.ets index 07c665a404..d8b1db08d0 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/SidebarToolsFooter.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/SidebarToolsFooter.ets @@ -1,6 +1,6 @@ import { MobileDesignTypography } from '../../generated/MobileDesignTokens'; import { RemoteI18n } from '../../i18n/RemoteI18n'; -import { CARD, INK, LINE } from './Theme'; +import { SIDEBAR_INK, SIDEBAR_LINE, SIDEBAR_RAISED } from './Theme'; /** Pure navigation controls reused in compact and wide sidebars. */ @ComponentV2 @@ -16,20 +16,20 @@ export struct SidebarToolsFooter { Stack() { SymbolGlyph($r('sys.symbol.wrench_and_screwdriver')) .fontSize(20) - .fontColor([INK]) + .fontColor([SIDEBAR_INK]) }.width(24).height(24) Text(RemoteI18n.t('workspaceTools.title')) .fontSize(MobileDesignTypography.labelLarge.size) .fontWeight(FontWeight.Medium) - .fontColor(INK) + .fontColor(SIDEBAR_INK) .maxLines(1) } } .constraintSize({ minWidth: 104 }) .height(48) .padding({ left: 14, right: 14 }) - .backgroundColor(CARD) - .border({ width: 0.5, color: LINE }) + .backgroundColor(SIDEBAR_RAISED) + .border({ width: 0.5, color: SIDEBAR_LINE }) .borderRadius(24) .enabled(this.toolsEnabled) .opacity(this.toolsEnabled ? 1 : 0.45) @@ -40,12 +40,12 @@ export struct SidebarToolsFooter { Button() { SymbolGlyph($r('sys.symbol.gearshape')) .fontSize(20) - .fontColor([INK]) + .fontColor([SIDEBAR_INK]) } .width(48) .height(48) .padding(0) - .backgroundColor(CARD) + .backgroundColor(SIDEBAR_RAISED) .borderRadius(24) .accessibilityText(RemoteI18n.t('settings.title')) .stateEffect(true) diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/SidebarWorkspaceSection.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/SidebarWorkspaceSection.ets index a38e044f2b..b6a01689ec 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/SidebarWorkspaceSection.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/SidebarWorkspaceSection.ets @@ -16,7 +16,15 @@ import { SettingsSheetKind } from '../policy/SettingsPlacementPolicy'; import { AdaptiveSheetOptions } from './AdaptiveSheetOptions'; -import { STATUS_SUCCESS, STATUS_DANGER, INK, MUTED, SUBTLE, SOFT, TRANSPARENT } from './Theme'; +import { + SIDEBAR_INK, + SIDEBAR_MUTED, + SIDEBAR_SELECTION, + SIDEBAR_SUBTLE, + STATUS_DANGER, + STATUS_SUCCESS, + TRANSPARENT, +} from './Theme'; import { SidebarDeviceGroup } from './SidebarDeviceGroup'; import { SidebarDeviceIcon } from './SidebarNavigationIcons'; import { SidebarWorkspacePicker } from './SidebarWorkspacePicker'; @@ -124,19 +132,19 @@ export struct SidebarWorkspaceSection { .id('sidebar-workspace-section') .fontSize(MobileDesignTypography.bodyMedium.size) .fontWeight(FontWeight.Medium) - .fontColor(MUTED) + .fontColor(SIDEBAR_MUTED) Blank() Stack({ alignContent: Alignment.Center }) { if (this.directoryState.refreshing) { LoadingProgress() .width(18) .height(18) - .color(MUTED) + .color(SIDEBAR_MUTED) } else { SymbolGlyph($r('sys.symbol.arrow_clockwise')) .fontSize(20) .fontWeight(FontWeight.Medium) - .fontColor([MUTED]) + .fontColor([SIDEBAR_MUTED]) } } .width(34) @@ -159,13 +167,13 @@ export struct SidebarWorkspaceSection { SidebarDeviceIcon({ deviceName: entry.deviceName, iconSize: 24, - tint: INK + tint: SIDEBAR_INK }) .hitTestBehavior(HitTestMode.None) Text(entry.deviceName) .fontSize(MobileDesignTypography.bodyMedium.size) .fontWeight(this.isSelected(entry) ? FontWeight.Bold : FontWeight.Regular) - .fontColor(INK) + .fontColor(SIDEBAR_INK) .layoutWeight(1) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) @@ -175,7 +183,7 @@ export struct SidebarWorkspaceSection { LoadingProgress() .width(13) .height(13) - .color(MUTED) + .color(SIDEBAR_MUTED) .hitTestBehavior(HitTestMode.None) } else { Text('') @@ -190,7 +198,7 @@ export struct SidebarWorkspaceSection { .height(52) .padding({ left: 8, right: 4 }) .alignItems(VerticalAlign.Center) - .backgroundColor(this.isSelected(entry) ? SOFT : TRANSPARENT) + .backgroundColor(this.isSelected(entry) ? SIDEBAR_SELECTION : TRANSPARENT) .borderRadius(10) .accessibilityText(`${entry.deviceName}, ${this.isActiveDevice(entry.deviceId) ? ConnectionStatusPresenter.label(this.connectionState) : RemoteI18n.t(entry.online ? 'sidebar.deviceOnline' : 'sidebar.deviceOffline')}`) @@ -210,10 +218,10 @@ export struct SidebarWorkspaceSection { } private deviceHealthColor(entry: DeviceDirectoryEntry): ResourceColor { - if (!this.isActiveDevice(entry.deviceId)) return entry.online ? STATUS_SUCCESS : SUBTLE; + if (!this.isActiveDevice(entry.deviceId)) return entry.online ? STATUS_SUCCESS : SIDEBAR_SUBTLE; if (this.connectionState === 'connected') return STATUS_SUCCESS; if (this.connectionState === 'failed' || this.connectionState === 'disconnected') return STATUS_DANGER; - return MUTED; + return SIDEBAR_MUTED; } @Builder @@ -223,12 +231,12 @@ export struct SidebarWorkspaceSection { Text(RemoteI18n.t('sidebar.workspaces')) .fontSize(MobileDesignTypography.bodyMedium.size) .fontWeight(FontWeight.Medium) - .fontColor(MUTED) + .fontColor(SIDEBAR_MUTED) Blank() Stack({ alignContent: Alignment.Center }) { SymbolGlyph($r('sys.symbol.plus')) .fontSize(18) - .fontColor([this.canUseDevice(entry) ? INK : SUBTLE]) + .fontColor([this.canUseDevice(entry) ? SIDEBAR_INK : SIDEBAR_SUBTLE]) } .width(34) .height(34) @@ -248,7 +256,7 @@ export struct SidebarWorkspaceSection { if (entry.catalogSource === 'recent') { Text(RemoteI18n.t('sidebar.legacyWorkspaceCatalog')) .fontSize(MobileDesignTypography.bodySmall.size) - .fontColor(MUTED) + .fontColor(SIDEBAR_MUTED) .padding({ left: 10, right: 10, bottom: 4 }) } SidebarDeviceGroup({ @@ -299,11 +307,11 @@ export struct SidebarWorkspaceSection { Row({ space: 8 }) { Text('···') .fontSize(MobileDesignTypography.bodySmall.size) - .fontColor(MUTED) + .fontColor(SIDEBAR_MUTED) Text(RemoteI18n.f('sidebar.moreDevices', String(this.deviceEntries().length - this.visibleDeviceCount))) .fontSize(MobileDesignTypography.bodySmall.size) - .fontColor(MUTED) + .fontColor(SIDEBAR_MUTED) } .width('100%') .height(40) diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/Theme.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/Theme.ets index 5163a24c93..81033e77fc 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/Theme.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/Theme.ets @@ -38,6 +38,33 @@ export const SHADOW_STRONG: ResourceColor = $r('app.color.shadow_strong'); export const FLOATING_BORDER: ResourceColor = $r('app.color.floating_border'); export const SOFT: ResourceColor = $r('app.color.soft'); export const FLOATING_PANEL_BG: ResourceColor = $r('app.color.floating_panel_bg'); +/** + * Sidebar chrome, mirroring the desktop client's structural navigation surface. + * + * The conversation keeps the paper-and-ink page tokens; the sidebar is a + * different structural layer there and here, so it reads one step away from the + * content surface instead of sharing PAGE_BG with it. Each constant below is + * one desktop semantic role, and the alpha ones are meant to composite over + * SIDEBAR_BG rather than be flattened. + */ +/** Desktop `color.surface.chrome`. */ +export const SIDEBAR_BG: ResourceColor = $r('app.color.sidebar_bg'); +/** SIDEBAR_BG at zero alpha, per theme — the transparent end of a fade-out. */ +export const SIDEBAR_BG_FADE: ResourceColor = $r('app.color.sidebar_bg_fade'); +/** Desktop `color.surface.raised` — a control lifted off the chrome. */ +export const SIDEBAR_RAISED: ResourceColor = $r('app.color.sidebar_raised'); +/** Desktop `color.border.subtle`. */ +export const SIDEBAR_LINE: ResourceColor = $r('app.color.sidebar_line'); +/** Desktop `color.action.quiet.hover` — quiet fills such as the search field. */ +export const SIDEBAR_HOVER: ResourceColor = $r('app.color.sidebar_hover'); +/** Desktop `color.selection.surface` — the selected device, workspace or session. */ +export const SIDEBAR_SELECTION: ResourceColor = $r('app.color.sidebar_selection'); +/** Desktop `color.content.primary`. */ +export const SIDEBAR_INK: ResourceColor = $r('app.color.sidebar_ink'); +/** Desktop `color.content.secondary`. */ +export const SIDEBAR_MUTED: ResourceColor = $r('app.color.sidebar_muted'); +/** Desktop `color.content.caption`. */ +export const SIDEBAR_SUBTLE: ResourceColor = $r('app.color.sidebar_subtle'); export const STATUS_SUCCESS: ResourceColor = $r('app.color.status_success'); export const STATUS_DANGER: ResourceColor = $r('app.color.status_danger'); export const STATUS_WARNING: ResourceColor = $r('app.color.status_warning'); diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/WideConversationHost.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/WideConversationHost.ets index ff7aecab26..9f47925b93 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/WideConversationHost.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/WideConversationHost.ets @@ -28,10 +28,10 @@ import { import { SidebarToggleButton } from './SidebarToggleButton'; import { SidebarWorkspaceSection } from './SidebarWorkspaceSection'; import { - CARD, LINE, PAGE_BG, SHADOW_SUBTLE, + SIDEBAR_BG, } from './Theme'; const WIDE_DETAIL_CONTENT_MAX_WIDTH: number = 920; @@ -155,7 +155,7 @@ export struct WideConversationHost { onOpenAccount: this.actions.onSidebar.openAccount, }) } - .width('100%').height('100%').backgroundColor(CARD) + .width('100%').height('100%').backgroundColor(SIDEBAR_BG) .borderRadius(18).clip(true) .shadow({ radius: 24, color: SHADOW_SUBTLE, offsetX: 4, offsetY: 8 }) } diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/remote/RemoteSurfaceHost.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/remote/RemoteSurfaceHost.ets index b004038f5d..70a8e131c2 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/remote/RemoteSurfaceHost.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/pages/components/remote/RemoteSurfaceHost.ets @@ -22,7 +22,7 @@ import { SettingsPlacementPolicy, SettingsSheetKind } from '../../policy/SettingsPlacementPolicy'; -import { CARD, STATUS_SUCCESS, INK, LINE, MUTED, PAGE_BG, PRIMARY_ACTION, CONTENT_ON_ACTION, STATUS_DANGER } from '../Theme'; +import { CARD, STATUS_SUCCESS, INK, MUTED, PAGE_BG, PRIMARY_ACTION, CONTENT_ON_ACTION, STATUS_DANGER } from '../Theme'; export enum RemoteSurfaceMode { Master = 'master', @@ -266,8 +266,11 @@ export struct RemoteSurfaceHost { .layoutWeight(1).alignItems(HorizontalAlign.Center) Blank().width(48).height(48) } + // No hairline under the welcome header. The compact header does not draw + // one and neither does the desktop client; the wide surface is the same + // page, not a sheet, so it should not grow a divider just because it is + // wider. .width('100%').height(76).padding({ left: 16, right: 16, top: 14, bottom: 12 }) - .border({ width: { bottom: 1 }, color: LINE }) this.CompactEmptyState() diff --git a/src/apps/mobile/harmonyos/entry/src/main/resources/base/element/color.json b/src/apps/mobile/harmonyos/entry/src/main/resources/base/element/color.json index d63161fa23..94428cf3ce 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/resources/base/element/color.json +++ b/src/apps/mobile/harmonyos/entry/src/main/resources/base/element/color.json @@ -132,6 +132,42 @@ "name": "floating_panel_bg", "value": "#F7F7F5" }, + { + "name": "sidebar_bg", + "value": "#F8F8F9" + }, + { + "name": "sidebar_bg_fade", + "value": "#00F8F8F9" + }, + { + "name": "sidebar_raised", + "value": "#FFFFFF" + }, + { + "name": "sidebar_line", + "value": "#14101A27" + }, + { + "name": "sidebar_hover", + "value": "#F3F3F5" + }, + { + "name": "sidebar_selection", + "value": "#14000000" + }, + { + "name": "sidebar_ink", + "value": "#CC000000" + }, + { + "name": "sidebar_muted", + "value": "#99000000" + }, + { + "name": "sidebar_subtle", + "value": "#66000000" + }, { "name": "status_success", "value": "#27C46A" diff --git a/src/apps/mobile/harmonyos/entry/src/main/resources/dark/element/color.json b/src/apps/mobile/harmonyos/entry/src/main/resources/dark/element/color.json index d7597f1000..918fb093dd 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/resources/dark/element/color.json +++ b/src/apps/mobile/harmonyos/entry/src/main/resources/dark/element/color.json @@ -132,6 +132,42 @@ "name": "floating_panel_bg", "value": "#1E1E1C" }, + { + "name": "sidebar_bg", + "value": "#0E0E10" + }, + { + "name": "sidebar_bg_fade", + "value": "#000E0E10" + }, + { + "name": "sidebar_raised", + "value": "#1C1C1F" + }, + { + "name": "sidebar_line", + "value": "#1FFFFFFF" + }, + { + "name": "sidebar_hover", + "value": "#0FFFFFFF" + }, + { + "name": "sidebar_selection", + "value": "#1FFFFFFF" + }, + { + "name": "sidebar_ink", + "value": "#E8E8E8" + }, + { + "name": "sidebar_muted", + "value": "#B0B0B0" + }, + { + "name": "sidebar_subtle", + "value": "#858585" + }, { "name": "status_success", "value": "#3BD47B" diff --git a/src/apps/mobile/ios/OpenBitFun/Features/DesignSystem/GeneratedMobileDesignTokens.swift b/src/apps/mobile/ios/OpenBitFun/Features/DesignSystem/GeneratedMobileDesignTokens.swift index 12e20d1803..f6024069e8 100644 --- a/src/apps/mobile/ios/OpenBitFun/Features/DesignSystem/GeneratedMobileDesignTokens.swift +++ b/src/apps/mobile/ios/OpenBitFun/Features/DesignSystem/GeneratedMobileDesignTokens.swift @@ -55,6 +55,15 @@ enum MobileDesignColors { static let floatingBorder = dynamic(light: 0x18000000, dark: 0x18000000) static let soft = dynamic(light: 0xFFF4F3F0, dark: 0xFF2D2C28) static let floatingPanelBg = dynamic(light: 0xFFF7F7F5, dark: 0xFF1E1E1C) + static let sidebarBg = dynamic(light: 0xFFF8F8F9, dark: 0xFF0E0E10) + static let sidebarBgFade = dynamic(light: 0x00F8F8F9, dark: 0x000E0E10) + static let sidebarRaised = dynamic(light: 0xFFFFFFFF, dark: 0xFF1C1C1F) + static let sidebarLine = dynamic(light: 0x14101A27, dark: 0x1FFFFFFF) + static let sidebarHover = dynamic(light: 0xFFF3F3F5, dark: 0x0FFFFFFF) + static let sidebarSelection = dynamic(light: 0x14000000, dark: 0x1FFFFFFF) + static let sidebarInk = dynamic(light: 0xCC000000, dark: 0xFFE8E8E8) + static let sidebarMuted = dynamic(light: 0x99000000, dark: 0xFFB0B0B0) + static let sidebarSubtle = dynamic(light: 0x66000000, dark: 0xFF858585) static let statusSuccess = dynamic(light: 0xFF27C46A, dark: 0xFF3BD47B) static let statusDanger = dynamic(light: 0xFFE04F4F, dark: 0xFFFF6B6B) static let codeLineNumber = dynamic(light: 0xFFAAA69D, dark: 0xFF77756E) diff --git a/src/apps/mobile/ios/OpenBitFun/Features/Shell/OpenBitFunTheme.swift b/src/apps/mobile/ios/OpenBitFun/Features/Shell/OpenBitFunTheme.swift index aafea6fc61..763cc09728 100644 --- a/src/apps/mobile/ios/OpenBitFun/Features/Shell/OpenBitFunTheme.swift +++ b/src/apps/mobile/ios/OpenBitFun/Features/Shell/OpenBitFunTheme.swift @@ -23,6 +23,23 @@ enum OpenBitFunTheme { static let floatingBorder = MobileDesignColors.floatingBorder static let statusSuccess = MobileDesignColors.statusSuccess static let statusDanger = MobileDesignColors.statusDanger + + // The navigation rail, held apart from the page palette above. + // + // The desktop client paints its sidebar from `surface.chrome` rather than + // from the scene, and carries hairlines, quiet fills and selection as alpha + // over it. These are those roles one for one; they are the sidebar's, and a + // page that borrows them stops matching the desktop instead of matching it + // more closely. `sidebarLine`, `sidebarHover` and `sidebarSelection` are + // translucent on purpose — they composite over `sidebarBg`. + static let sidebarBg = MobileDesignColors.sidebarBg + static let sidebarRaised = MobileDesignColors.sidebarRaised + static let sidebarLine = MobileDesignColors.sidebarLine + static let sidebarHover = MobileDesignColors.sidebarHover + static let sidebarSelection = MobileDesignColors.sidebarSelection + static let sidebarInk = MobileDesignColors.sidebarInk + static let sidebarMuted = MobileDesignColors.sidebarMuted + static let sidebarSubtle = MobileDesignColors.sidebarSubtle } struct CircleControl: View { diff --git a/src/apps/mobile/ios/OpenBitFun/Features/Shell/SidebarDeviceRow.swift b/src/apps/mobile/ios/OpenBitFun/Features/Shell/SidebarDeviceRow.swift index 3037ad0389..f4af81b886 100644 --- a/src/apps/mobile/ios/OpenBitFun/Features/Shell/SidebarDeviceRow.swift +++ b/src/apps/mobile/ios/OpenBitFun/Features/Shell/SidebarDeviceRow.swift @@ -30,11 +30,11 @@ struct SidebarDeviceRow: View { } // Explicit styling prevents disabled/default button tint from implying // that a collapsed or unselected device has disconnected. - .foregroundStyle(OpenBitFunTheme.ink) + .foregroundStyle(OpenBitFunTheme.sidebarInk) .padding(.leading, 8) .padding(.trailing, 4) .frame(height: 52) - .background(current ? OpenBitFunTheme.soft : OpenBitFunTheme.transparent, in: RoundedRectangle(cornerRadius: 10)) + .background(current ? OpenBitFunTheme.sidebarSelection : OpenBitFunTheme.transparent, in: RoundedRectangle(cornerRadius: 10)) .contentShape(Rectangle()) } .buttonStyle(.plain) diff --git a/src/apps/mobile/ios/OpenBitFun/Features/Shell/SidebarToolsFooter.swift b/src/apps/mobile/ios/OpenBitFun/Features/Shell/SidebarToolsFooter.swift index 997a13570c..b485427ad2 100644 --- a/src/apps/mobile/ios/OpenBitFun/Features/Shell/SidebarToolsFooter.swift +++ b/src/apps/mobile/ios/OpenBitFun/Features/Shell/SidebarToolsFooter.swift @@ -15,12 +15,12 @@ struct SidebarToolsFooter: View { .labelStyle(SidebarToolsLabelStyle()) .padding(.horizontal, 14) .frame(minWidth: 104, minHeight: 48) - .background(OpenBitFunTheme.card, in: Capsule()) - .overlay(Capsule().stroke(OpenBitFunTheme.line, lineWidth: 0.5)) + .background(OpenBitFunTheme.sidebarRaised, in: Capsule()) + .overlay(Capsule().stroke(OpenBitFunTheme.sidebarLine, lineWidth: 0.5)) .contentShape(Capsule()) } .buttonStyle(.plain) - .foregroundStyle(OpenBitFunTheme.ink) + .foregroundStyle(OpenBitFunTheme.sidebarInk) .disabled(!toolsEnabled) .opacity(toolsEnabled ? 1 : 0.45) .accessibilityIdentifier("sidebar.deviceTools") @@ -29,10 +29,10 @@ struct SidebarToolsFooter: View { Image(systemName: "gearshape") .font(.system(size: 20)) .frame(width: 48, height: 48) - .background(OpenBitFunTheme.card, in: Circle()) + .background(OpenBitFunTheme.sidebarRaised, in: Circle()) } .buttonStyle(.plain) - .foregroundStyle(OpenBitFunTheme.ink) + .foregroundStyle(OpenBitFunTheme.sidebarInk) .accessibilityLabel(settingsTitle) } .frame(minHeight: 56) diff --git a/src/apps/mobile/ios/OpenBitFun/Features/Shell/SidebarView.swift b/src/apps/mobile/ios/OpenBitFun/Features/Shell/SidebarView.swift index 8b911ea9f8..e49e41b2c2 100644 --- a/src/apps/mobile/ios/OpenBitFun/Features/Shell/SidebarView.swift +++ b/src/apps/mobile/ios/OpenBitFun/Features/Shell/SidebarView.swift @@ -80,7 +80,7 @@ struct SidebarView: View { .layoutPriority(-1) .clipped() .accessibilityIdentifier("sidebar.workspaces") - footer.background(OpenBitFunTheme.page) + footer.background(OpenBitFunTheme.sidebarBg) } .padding(.horizontal, 20) .padding(.top, 0) @@ -90,7 +90,7 @@ struct SidebarView: View { height: proxy.size.height, alignment: .topLeading ) - .background(OpenBitFunTheme.page) + .background(OpenBitFunTheme.sidebarBg) } .sheet(item: $compactActionSession) { session in let detentHeight: CGFloat = model.surface == .local ? 330 : 230 @@ -187,16 +187,16 @@ struct SidebarView: View { HStack(spacing: 6) { Text(verbatim: "OpenBitFun") .font(.system(size: 20, weight: .medium)) - .foregroundStyle(OpenBitFunTheme.ink) + .foregroundStyle(OpenBitFunTheme.sidebarInk) Spacer(minLength: 0) if let onCollapse { Button(action: onCollapse) { Image(systemName: "sidebar.left") .font(.system(size: 17, weight: .medium)) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) .frame(width: 38, height: 38) - .background(OpenBitFunTheme.card) - .overlay(Circle().stroke(OpenBitFunTheme.line, lineWidth: 1)) + .background(OpenBitFunTheme.sidebarRaised) + .overlay(Circle().stroke(OpenBitFunTheme.sidebarLine, lineWidth: 1)) .clipShape(Circle()) } .buttonStyle(.plain) @@ -210,8 +210,8 @@ struct SidebarView: View { .font(.system(size: 18, weight: .regular)) .frame(width: 22, height: 22) .frame(width: 44, height: 44) - .background(OpenBitFunTheme.card) - .overlay(Circle().stroke(OpenBitFunTheme.line, lineWidth: 0.5)) + .background(OpenBitFunTheme.sidebarRaised) + .overlay(Circle().stroke(OpenBitFunTheme.sidebarLine, lineWidth: 0.5)) .clipShape(Circle()) .shadow(color: MobileDesignColors.shadowFaint, radius: 9, y: 3) } @@ -230,7 +230,7 @@ struct SidebarView: View { Text(model.localized("连接桌面端")) .font(.system(size: 15, weight: .medium)) } - .foregroundStyle(OpenBitFunTheme.ink) + .foregroundStyle(OpenBitFunTheme.sidebarInk) .frame(height: 42) } .buttonStyle(.plain) @@ -239,10 +239,10 @@ struct SidebarView: View { Button(action: onCollapse) { Image(systemName: "sidebar.left") .font(.system(size: 17, weight: .medium)) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) .frame(width: 38, height: 38) - .background(OpenBitFunTheme.card) - .overlay(Circle().stroke(OpenBitFunTheme.line, lineWidth: 1)) + .background(OpenBitFunTheme.sidebarRaised) + .overlay(Circle().stroke(OpenBitFunTheme.sidebarLine, lineWidth: 1)) .clipShape(Circle()) } .buttonStyle(.plain) @@ -255,10 +255,10 @@ struct SidebarView: View { private var searchField: some View { TextField(model.localized("搜索对话"), text: $search) .font(.system(size: 14)) - .foregroundStyle(OpenBitFunTheme.ink) + .foregroundStyle(OpenBitFunTheme.sidebarInk) .padding(.horizontal, 14) .frame(height: 42) - .background(OpenBitFunTheme.soft) + .background(OpenBitFunTheme.sidebarHover) .clipShape(RoundedRectangle(cornerRadius: 8)) .padding(.top, 12) .onChange(of: search) { value in @@ -278,7 +278,7 @@ struct SidebarView: View { HStack { Text(model.localized("设备")) .font(.system(size: 14, weight: .medium)) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) Spacer() if model.accountUser != nil { Button { model.refreshRemoteDevices() } label: { @@ -287,7 +287,7 @@ struct SidebarView: View { } else { Image(systemName: "arrow.clockwise") .font(.system(size: 14, weight: .medium)) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) } } .buttonStyle(.plain) @@ -314,7 +314,7 @@ struct SidebarView: View { if directoryEntries.isEmpty { Text(model.localized("尚未连接桌面设备")) .font(.system(size: 13)) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) .padding(.horizontal, 10) .frame(height: 42, alignment: .leading) } @@ -328,7 +328,7 @@ struct SidebarView: View { HStack { Text(model.localized("工作区")) .font(.system(size: 14, weight: .medium)) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) Spacer(minLength: 0) Button { workspacePickerDevice = selectedDirectoryEntry @@ -336,7 +336,7 @@ struct SidebarView: View { } label: { Image(systemName: "plus") .font(.system(size: 18, weight: .regular)) - .foregroundStyle(selectedDirectoryEntry.online ? OpenBitFunTheme.ink : OpenBitFunTheme.muted) + .foregroundStyle(selectedDirectoryEntry.online ? OpenBitFunTheme.sidebarInk : OpenBitFunTheme.sidebarSubtle) .frame(width: 34, height: 34) } .buttonStyle(.plain) @@ -360,7 +360,7 @@ struct SidebarView: View { enabled: device.online || (current && model.connectionPhase == .disconnected), loading: device.status == "LOADING", statusColor: current ? activeConnectionColor : - (device.online ? OpenBitFunTheme.statusSuccess : OpenBitFunTheme.muted), + (device.online ? OpenBitFunTheme.statusSuccess : OpenBitFunTheme.sidebarMuted), statusLabel: current ? activeConnectionLabel : (device.online ? model.localized("在线") : model.localized("离线")), accessibilityID: "sidebar.device.\(device.id)", @@ -383,7 +383,7 @@ struct SidebarView: View { private var activeConnectionColor: Color { switch model.connectionPhase { case .connected: return OpenBitFunTheme.statusSuccess - case .reconnecting: return OpenBitFunTheme.muted + case .reconnecting: return OpenBitFunTheme.sidebarMuted case .disconnected: return OpenBitFunTheme.statusDanger } } @@ -405,11 +405,11 @@ struct SidebarView: View { if device.catalogSource == "RECENT" { Text(model.localized("旧版设备:显示最近访问的工作区")) .font(.system(size: 12)) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) .padding(.horizontal, 10) } if device.status == "LOADING" && device.workspaces.isEmpty && device.sessions.isEmpty { - HStack(spacing: 8) { ProgressView().controlSize(.small); Text(model.localized("正在加载工作区")).font(.system(size: 13)).foregroundStyle(OpenBitFunTheme.muted) } + HStack(spacing: 8) { ProgressView().controlSize(.small); Text(model.localized("正在加载工作区")).font(.system(size: 13)).foregroundStyle(OpenBitFunTheme.sidebarMuted) } .padding(.horizontal, 18).frame(height: 42) } else if device.status == "FAILED" { Button { model.retryDeviceDirectory(device) } label: { @@ -419,7 +419,7 @@ struct SidebarView: View { } else if device.status == "READY" && device.online && device.workspaces.isEmpty && device.sessions.isEmpty { Text(model.localized("这台电脑还没有工作区")) .font(.system(size: 13)) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) .padding(.horizontal, 18) .frame(maxWidth: .infinity, minHeight: 42, alignment: .leading) .accessibilityIdentifier("sidebar.emptyWorkspaces") @@ -482,7 +482,7 @@ struct SidebarView: View { "还有 %lld 个工作区", Int64(device.workspaces.count - visibleWorkspaceCount) )) - .font(.system(size: 13)).foregroundStyle(OpenBitFunTheme.muted).padding(.leading, 42).frame(height: 36, alignment: .leading) + .font(.system(size: 13)).foregroundStyle(OpenBitFunTheme.sidebarMuted).padding(.leading, 42).frame(height: 36, alignment: .leading) }.buttonStyle(.plain) } } @@ -494,7 +494,7 @@ struct SidebarView: View { ProgressView().controlSize(.small) Text(model.localized("正在加载工作区")) .font(.system(size: 13)) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) } .padding(.horizontal, 10) .frame(height: 42) @@ -515,7 +515,7 @@ struct SidebarView: View { if model.remoteHasMore { Button { model.loadMoreRemoteSessions() } label: { Text(model.localized(model.busy ? "正在加载" : "加载更多会话")) - .font(.system(size: 13)).foregroundStyle(OpenBitFunTheme.muted) + .font(.system(size: 13)).foregroundStyle(OpenBitFunTheme.sidebarMuted) .frame(maxWidth: .infinity, minHeight: 42) } .buttonStyle(.plain).disabled(model.busy) @@ -531,7 +531,7 @@ struct SidebarView: View { if visibleSessions.isEmpty && !model.workspaceLoading { Text(model.localized(hasActiveRemoteViewFilter ? "没有匹配的会话" : "暂无远程会话")) .font(.system(size: 13)) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) .padding(.horizontal, 10) .frame(height: 42, alignment: .leading) } else { @@ -611,7 +611,7 @@ struct SidebarView: View { VStack(alignment: .leading, spacing: 0) { Text(model.localized(bucket.title)) .font(.system(size: 14, weight: .medium)) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) .padding(.top, 12) .padding(.bottom, 4) ForEach(bucket.sessions) { session in @@ -641,13 +641,13 @@ struct SidebarView: View { HStack(spacing: 8) { Text(model.localized("连接桌面端")) .font(.system(size: 14, weight: .medium)) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) Text(verbatim: "\(sessions.count)") .font(.system(size: 12)) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) Image(systemName: remoteChatsCollapsed ? "chevron.right" : "chevron.down") .font(.system(size: 12, weight: .medium)) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) } } .buttonStyle(.plain) @@ -655,7 +655,7 @@ struct SidebarView: View { Button { model.createRemoteAssistantSession() } label: { Image(systemName: "folder.badge.gearshape") .font(.system(size: 16, weight: .medium)) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) .frame(width: 40, height: 40) } .buttonStyle(.plain) @@ -773,7 +773,7 @@ struct SidebarView: View { .frame(width: 22, height: 18) Text(name) .font(.system(size: 15)) - .foregroundStyle(OpenBitFunTheme.ink) + .foregroundStyle(OpenBitFunTheme.sidebarInk) .lineLimit(1) Spacer(minLength: 0) Circle().fill(OpenBitFunTheme.statusSuccess).frame(width: 7, height: 7) @@ -820,7 +820,7 @@ struct SidebarView: View { } label: { Image(systemName: "plus") .font(.system(size: 17, weight: .regular)) - .foregroundStyle(OpenBitFunTheme.ink) + .foregroundStyle(OpenBitFunTheme.sidebarInk) .frame(width: 30, height: 40) }) } @@ -845,7 +845,7 @@ struct SidebarView: View { Button(action: action) { Text(verbatim: title) .font(.system(size: 15, weight: .medium)) - .foregroundStyle(OpenBitFunTheme.ink) + .foregroundStyle(OpenBitFunTheme.sidebarInk) .frame(maxWidth: .infinity, minHeight: MobileDesignGeometry.compactPopoverActionHeight, alignment: .leading) .padding(.horizontal, 18) .contentShape(Rectangle()) @@ -900,12 +900,12 @@ private struct SidebarRecentRow: View { VStack(alignment: .leading, spacing: 2) { Text(session.title) .font(.system(size: 13, weight: selected ? .bold : .regular)) - .foregroundStyle(OpenBitFunTheme.ink) + .foregroundStyle(OpenBitFunTheme.sidebarInk) .lineLimit(1) if let metadata, !metadata.isEmpty { Text(metadata) .font(MobileDesignTypography.labelSmall.font) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) .lineLimit(1) } } @@ -919,9 +919,9 @@ private struct SidebarRecentRow: View { onActions() } label: { HStack(spacing: 3) { - Circle().fill(OpenBitFunTheme.muted).frame(width: 3.5, height: 3.5) - Circle().fill(OpenBitFunTheme.muted).frame(width: 3.5, height: 3.5) - Circle().fill(OpenBitFunTheme.muted).frame(width: 3.5, height: 3.5) + Circle().fill(OpenBitFunTheme.sidebarMuted).frame(width: 3.5, height: 3.5) + Circle().fill(OpenBitFunTheme.sidebarMuted).frame(width: 3.5, height: 3.5) + Circle().fill(OpenBitFunTheme.sidebarMuted).frame(width: 3.5, height: 3.5) } .frame(width: 34, height: 40) .opacity(0.62) @@ -937,7 +937,7 @@ private struct SidebarRecentRow: View { .padding(.leading, 12) .padding(.trailing, 4) .frame(minHeight: metadata == nil ? 44 : 56) - .background(selected ? OpenBitFunTheme.soft : OpenBitFunTheme.transparent) + .background(selected ? OpenBitFunTheme.sidebarSelection : OpenBitFunTheme.transparent) .clipShape(RoundedRectangle(cornerRadius: 10)) } } @@ -990,7 +990,7 @@ private struct SidebarWorkspaceRow: View { .frame(width: 24, height: 20) Text(workspace.name) .font(.system(size: 15, weight: workspace.selected ? .medium : .regular)) - .foregroundStyle(OpenBitFunTheme.ink) + .foregroundStyle(OpenBitFunTheme.sidebarInk) .lineLimit(1) } .contentShape(Rectangle()) @@ -1006,7 +1006,7 @@ private struct SidebarWorkspaceRow: View { Button(action: onToggleCreate) { Image(systemName: "plus") .font(.system(size: 17, weight: .regular)) - .foregroundStyle(OpenBitFunTheme.ink) + .foregroundStyle(OpenBitFunTheme.sidebarInk) .frame(width: 30, height: 40) } } @@ -1031,7 +1031,7 @@ private struct SidebarWorkspaceRow: View { ProgressView().controlSize(.small) Text(MobileLocalization.text("正在加载")) .font(.system(size: 13)) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) } .padding(.leading, 42) .frame(height: 40, alignment: .leading) @@ -1039,10 +1039,10 @@ private struct SidebarWorkspaceRow: View { Button(action: { onRetryDirectoryLoad?() }) { HStack(spacing: 8) { Text(MobileLocalization.text("这台电脑暂时无法读取")) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) Spacer(minLength: 0) Text(MobileLocalization.text("重试")) - .foregroundStyle(OpenBitFunTheme.ink) + .foregroundStyle(OpenBitFunTheme.sidebarInk) } .font(.system(size: 13)) .padding(.leading, 42) @@ -1053,7 +1053,7 @@ private struct SidebarWorkspaceRow: View { } else if workspace.sessions.isEmpty && directoryLoadStatus == "READY" { Text(MobileLocalization.text("此工作区暂无会话")) .font(.system(size: 13)) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) .padding(.leading, 42) .frame(height: 38, alignment: .leading) } @@ -1063,7 +1063,7 @@ private struct SidebarWorkspaceRow: View { HStack(spacing: 10) { Image(systemName: "doc") .font(.system(size: 17, weight: .regular)) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) .frame(width: 19, height: 19) .overlay(alignment: .bottomLeading) { if ["running", "active", "in_progress"].contains(session.status.lowercased()) { @@ -1076,12 +1076,12 @@ private struct SidebarWorkspaceRow: View { size: 13, weight: isSelected(session) ? .bold : .regular )) - .foregroundStyle(OpenBitFunTheme.ink) + .foregroundStyle(OpenBitFunTheme.sidebarInk) .lineLimit(1) if let detail = metadata(session), !detail.isEmpty { Text(detail) .font(MobileDesignTypography.labelSmall.font) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) .lineLimit(1) } } @@ -1094,7 +1094,7 @@ private struct SidebarWorkspaceRow: View { .accessibilityAddTraits(isSelected(session) ? .isSelected : []) Button { onActions(session) } label: { Image(systemName: "ellipsis") - .font(.system(size: 13, weight: .medium)).foregroundStyle(OpenBitFunTheme.muted) + .font(.system(size: 13, weight: .medium)).foregroundStyle(OpenBitFunTheme.sidebarMuted) .frame(width: 36, height: 40) } .buttonStyle(.plain) @@ -1108,7 +1108,7 @@ private struct SidebarWorkspaceRow: View { .padding(.leading, 44) .padding(.trailing, 4) .frame(minHeight: metadata(session) == nil ? 44 : 56) - .background(isSelected(session) ? OpenBitFunTheme.soft : OpenBitFunTheme.transparent) + .background(isSelected(session) ? OpenBitFunTheme.sidebarSelection : OpenBitFunTheme.transparent) .clipShape(RoundedRectangle(cornerRadius: 9)) } if workspace.sessions.count > visibleSessionCount { @@ -1123,7 +1123,7 @@ private struct SidebarWorkspaceRow: View { ) ) .font(.system(size: 13)) - .foregroundStyle(OpenBitFunTheme.muted) + .foregroundStyle(OpenBitFunTheme.sidebarMuted) .padding(.leading, 42) .frame(height: 36, alignment: .leading) }