mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 19:35:28 -06:00
refactor(android): collapse the chat's second color palette into ClawTheme (#125020)
* refactor(android): consolidate chat palette * refactor(android): fold the font family into ClawTheme
This commit is contained in:
committed by
GitHub
parent
f7a8638282
commit
d095113698
@@ -289,8 +289,8 @@ internal object AndroidScreenshotFixture {
|
||||
add(
|
||||
chatMessage(
|
||||
"assistant",
|
||||
"The main thread asks for a regression test around session restore, and the second one wants the new " +
|
||||
"config key documented before merge. Both are small; I can draft patches for each if you want.",
|
||||
"The release check is ready:\n\n```kotlin\nval ready = lint && tests\n```\n\n" +
|
||||
"Review https://openclaw.ai before tagging.",
|
||||
1_783_555_200_000,
|
||||
),
|
||||
)
|
||||
@@ -343,6 +343,13 @@ internal object AndroidScreenshotFixture {
|
||||
put("contextTokens", JsonPrimitive(200_000))
|
||||
},
|
||||
)
|
||||
put(
|
||||
"inFlightRun",
|
||||
buildJsonObject {
|
||||
put("runId", JsonPrimitive("android-screenshot-active-run"))
|
||||
put("text", JsonPrimitive(""))
|
||||
},
|
||||
)
|
||||
}.toString()
|
||||
|
||||
private fun chatMessage(
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
package ai.openclaw.app.ui
|
||||
|
||||
import ai.openclaw.app.R
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
internal data class MobileColors(
|
||||
val surface: Color,
|
||||
val surfaceStrong: Color,
|
||||
val cardSurface: Color,
|
||||
val border: Color,
|
||||
val borderStrong: Color,
|
||||
val text: Color,
|
||||
val textSecondary: Color,
|
||||
val textTertiary: Color,
|
||||
val accent: Color,
|
||||
val accentSoft: Color,
|
||||
val accentBorderStrong: Color,
|
||||
val success: Color,
|
||||
val successSoft: Color,
|
||||
val warning: Color,
|
||||
val warningSoft: Color,
|
||||
val danger: Color,
|
||||
val dangerSoft: Color,
|
||||
val codeBg: Color,
|
||||
val codeText: Color,
|
||||
val codeBorder: Color,
|
||||
val codeAccent: Color,
|
||||
// Syntax token colors; every value must keep >= 4.5:1 contrast against codeBg in its theme.
|
||||
val codeKeyword: Color,
|
||||
val codeString: Color,
|
||||
val codeComment: Color,
|
||||
val codeNumber: Color,
|
||||
val chipBorderConnected: Color,
|
||||
val chipBorderConnecting: Color,
|
||||
val chipBorderWarning: Color,
|
||||
val chipBorderError: Color,
|
||||
)
|
||||
|
||||
internal fun lightMobileColors() =
|
||||
MobileColors(
|
||||
surface = Color(0xFFFAFBFC),
|
||||
surfaceStrong = Color(0xFFEFF3F8),
|
||||
cardSurface = Color(0xFFFFFFFF),
|
||||
border = Color(0xFFDDE3EC),
|
||||
borderStrong = Color(0xFFC7D0DC),
|
||||
text = Color(0xFF16181D),
|
||||
textSecondary = Color(0xFF505B6A),
|
||||
textTertiary = Color(0xFF8E98A7),
|
||||
accent = Color(0xFF1B5ACB),
|
||||
accentSoft = Color(0xFFEAF2FF),
|
||||
accentBorderStrong = Color(0xFF174CA9),
|
||||
success = Color(0xFF287F52),
|
||||
successSoft = Color(0xFFEAF7F0),
|
||||
warning = Color(0xFFAF7418),
|
||||
warningSoft = Color(0xFFFFF4DF),
|
||||
danger = Color(0xFFC94343),
|
||||
dangerSoft = Color(0xFFFFECEC),
|
||||
codeBg = Color(0xFFEFF3F8),
|
||||
codeText = Color(0xFF172033),
|
||||
codeBorder = Color(0xFFD7DDE7),
|
||||
codeAccent = Color(0xFF287F52),
|
||||
codeKeyword = Color(0xFF1B5ACB),
|
||||
codeString = Color(0xFF1F6B45),
|
||||
codeComment = Color(0xFF505B6A),
|
||||
codeNumber = Color(0xFF8F5D10),
|
||||
chipBorderConnected = Color(0xFFCFEBD8),
|
||||
chipBorderConnecting = Color(0xFFD5E2FA),
|
||||
chipBorderWarning = Color(0xFFEED8B8),
|
||||
chipBorderError = Color(0xFFF3C8C8),
|
||||
)
|
||||
|
||||
internal fun darkMobileColors() =
|
||||
MobileColors(
|
||||
surface = Color(0xFF1A1C20),
|
||||
surfaceStrong = Color(0xFF24262B),
|
||||
cardSurface = Color(0xFF1E2024),
|
||||
border = Color(0xFF2E3038),
|
||||
borderStrong = Color(0xFF3A3D46),
|
||||
text = Color(0xFFE4E5EA),
|
||||
textSecondary = Color(0xFFA0A6B4),
|
||||
textTertiary = Color(0xFF6B7280),
|
||||
accent = Color(0xFF6EA8FF),
|
||||
accentSoft = Color(0xFF1A2A44),
|
||||
accentBorderStrong = Color(0xFF5B93E8),
|
||||
success = Color(0xFF5FBB85),
|
||||
successSoft = Color(0xFF152E22),
|
||||
warning = Color(0xFFE8A844),
|
||||
warningSoft = Color(0xFF2E2212),
|
||||
danger = Color(0xFFE87070),
|
||||
dangerSoft = Color(0xFF2E1616),
|
||||
codeBg = Color(0xFF111317),
|
||||
codeText = Color(0xFFE8EAEE),
|
||||
codeBorder = Color(0xFF2B2E35),
|
||||
codeAccent = Color(0xFF3FC97A),
|
||||
codeKeyword = Color(0xFF6EA8FF),
|
||||
codeString = Color(0xFF3FC97A),
|
||||
codeComment = Color(0xFFA0A6B4),
|
||||
codeNumber = Color(0xFFE8A844),
|
||||
chipBorderConnected = Color(0xFF1E4A30),
|
||||
chipBorderConnecting = Color(0xFF1E3358),
|
||||
chipBorderWarning = Color(0xFF3E3018),
|
||||
chipBorderError = Color(0xFF3E1E1E),
|
||||
)
|
||||
|
||||
// Defaulting to light tokens keeps previews/tests usable when a screen forgets to
|
||||
// provide the app theme; production roots override this composition local.
|
||||
internal val LocalMobileColors = staticCompositionLocalOf { lightMobileColors() }
|
||||
|
||||
internal object MobileColorsAccessor {
|
||||
val current: MobileColors
|
||||
@Composable get() = LocalMobileColors.current
|
||||
}
|
||||
|
||||
// Keep these accessors while screens migrate to `MobileColorsAccessor.current`.
|
||||
// Each getter must stay composable so callers always read the active theme.
|
||||
internal val mobileSurface: Color @Composable get() = LocalMobileColors.current.surface
|
||||
internal val mobileCardSurface: Color @Composable get() = LocalMobileColors.current.cardSurface
|
||||
internal val mobileBorder: Color @Composable get() = LocalMobileColors.current.border
|
||||
internal val mobileBorderStrong: Color @Composable get() = LocalMobileColors.current.borderStrong
|
||||
internal val mobileText: Color @Composable get() = LocalMobileColors.current.text
|
||||
internal val mobileTextSecondary: Color @Composable get() = LocalMobileColors.current.textSecondary
|
||||
internal val mobileTextTertiary: Color @Composable get() = LocalMobileColors.current.textTertiary
|
||||
internal val mobileAccent: Color @Composable get() = LocalMobileColors.current.accent
|
||||
internal val mobileAccentSoft: Color @Composable get() = LocalMobileColors.current.accentSoft
|
||||
internal val mobileAccentBorderStrong: Color @Composable get() = LocalMobileColors.current.accentBorderStrong
|
||||
internal val mobileWarning: Color @Composable get() = LocalMobileColors.current.warning
|
||||
internal val mobileWarningSoft: Color @Composable get() = LocalMobileColors.current.warningSoft
|
||||
internal val mobileDanger: Color @Composable get() = LocalMobileColors.current.danger
|
||||
internal val mobileDangerSoft: Color @Composable get() = LocalMobileColors.current.dangerSoft
|
||||
internal val mobileCodeBg: Color @Composable get() = LocalMobileColors.current.codeBg
|
||||
internal val mobileCodeText: Color @Composable get() = LocalMobileColors.current.codeText
|
||||
internal val mobileCodeBorder: Color @Composable get() = LocalMobileColors.current.codeBorder
|
||||
|
||||
// Build the page backdrop from semantic surfaces so light/dark palettes keep
|
||||
// their contrast relationship without duplicating raw color stops.
|
||||
internal val mobileBackgroundGradient: Brush
|
||||
@Composable get() {
|
||||
val colors = LocalMobileColors.current
|
||||
return Brush.verticalGradient(
|
||||
listOf(
|
||||
colors.surface,
|
||||
colors.surfaceStrong,
|
||||
colors.surfaceStrong,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal val mobileFontFamily =
|
||||
FontFamily(
|
||||
Font(resId = R.font.manrope_400_regular, weight = FontWeight.Normal),
|
||||
Font(resId = R.font.manrope_500_medium, weight = FontWeight.Medium),
|
||||
Font(resId = R.font.manrope_600_semibold, weight = FontWeight.SemiBold),
|
||||
Font(resId = R.font.manrope_700_bold, weight = FontWeight.Bold),
|
||||
)
|
||||
|
||||
internal val mobileHeadline =
|
||||
TextStyle(
|
||||
fontFamily = mobileFontFamily,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 22.sp,
|
||||
letterSpacing = (-0.1).sp,
|
||||
)
|
||||
|
||||
internal val mobileBody =
|
||||
TextStyle(
|
||||
fontFamily = mobileFontFamily,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 15.sp,
|
||||
lineHeight = 22.sp,
|
||||
)
|
||||
|
||||
internal val mobileCallout =
|
||||
TextStyle(
|
||||
fontFamily = mobileFontFamily,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
)
|
||||
|
||||
internal val mobileCaption1 =
|
||||
TextStyle(
|
||||
fontFamily = mobileFontFamily,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.2.sp,
|
||||
)
|
||||
|
||||
internal val mobileCaption2 =
|
||||
TextStyle(
|
||||
fontFamily = mobileFontFamily,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 14.sp,
|
||||
letterSpacing = 0.4.sp,
|
||||
)
|
||||
@@ -17,7 +17,7 @@ import androidx.core.view.WindowCompat
|
||||
internal val LocalResolvedAppearanceIsDark = staticCompositionLocalOf { false }
|
||||
|
||||
/**
|
||||
* App theme wrapper that installs dynamic Material colors and legacy mobile color tokens.
|
||||
* App theme wrapper that resolves the requested appearance for system surfaces and child themes.
|
||||
*/
|
||||
@Composable
|
||||
fun OpenClawTheme(
|
||||
@@ -27,12 +27,10 @@ fun OpenClawTheme(
|
||||
val context = LocalContext.current
|
||||
val isDark = themeMode.isDark(systemDark = isSystemInDarkTheme())
|
||||
val colorScheme = if (isDark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
val mobileColors = if (isDark) darkMobileColors() else lightMobileColors()
|
||||
|
||||
OpenClawSystemBarAppearance(lightAppearance = !isDark)
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalMobileColors provides mobileColors,
|
||||
LocalResolvedAppearanceIsDark provides isDark,
|
||||
) {
|
||||
MaterialTheme(colorScheme = colorScheme, content = content)
|
||||
|
||||
@@ -3,12 +3,6 @@ package ai.openclaw.app.ui.chat
|
||||
import ai.openclaw.app.chat.CHAT_IMAGE_MAX_BASE64_CHARS
|
||||
import ai.openclaw.app.i18n.nativeString
|
||||
import ai.openclaw.app.ui.design.ClawTheme
|
||||
import ai.openclaw.app.ui.mobileAccent
|
||||
import ai.openclaw.app.ui.mobileCallout
|
||||
import ai.openclaw.app.ui.mobileCaption1
|
||||
import ai.openclaw.app.ui.mobileCodeBg
|
||||
import ai.openclaw.app.ui.mobileCodeText
|
||||
import ai.openclaw.app.ui.mobileTextSecondary
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
@@ -120,7 +114,12 @@ fun ChatMarkdown(
|
||||
) {
|
||||
val blocks = remember(text, isStreaming) { segmentChatMarkdown(text, isStreaming) }
|
||||
val inlineStyles =
|
||||
InlineStyles(inlineCodeBg = mobileCodeBg, inlineCodeColor = mobileCodeText, linkColor = mobileAccent, baseCallout = mobileCallout)
|
||||
InlineStyles(
|
||||
inlineCodeBg = ClawTheme.colors.codeBg,
|
||||
inlineCodeColor = ClawTheme.colors.codeText,
|
||||
linkColor = ClawTheme.colors.accent,
|
||||
baseCallout = ClawTheme.type.body,
|
||||
)
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
for (block in blocks) {
|
||||
@@ -225,7 +224,7 @@ private fun RenderCommonMarkBlock(
|
||||
Modifier
|
||||
.width(2.dp)
|
||||
.fillMaxHeight()
|
||||
.background(mobileTextSecondary.copy(alpha = 0.35f)),
|
||||
.background(ClawTheme.colors.textMuted.copy(alpha = 0.35f)),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
@@ -272,7 +271,7 @@ private fun RenderCommonMarkBlock(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(1.dp)
|
||||
.background(mobileTextSecondary.copy(alpha = 0.25f)),
|
||||
.background(ClawTheme.colors.textMuted.copy(alpha = 0.25f)),
|
||||
)
|
||||
}
|
||||
is HtmlBlock -> {
|
||||
@@ -290,7 +289,7 @@ private fun RenderLiteralHtml(
|
||||
if (literal.isNotEmpty()) {
|
||||
Text(
|
||||
text = literal,
|
||||
style = mobileCallout.copy(fontFamily = FontFamily.Monospace),
|
||||
style = ClawTheme.type.body.copy(fontFamily = FontFamily.Monospace),
|
||||
color = textColor,
|
||||
)
|
||||
}
|
||||
@@ -326,11 +325,11 @@ private fun RenderMarkdownDisclosure(
|
||||
) {
|
||||
Text(
|
||||
text = if (isExpanded) "▾" else "▸",
|
||||
style = mobileCallout.copy(fontWeight = FontWeight.SemiBold),
|
||||
style = ClawTheme.type.body.copy(fontWeight = FontWeight.SemiBold),
|
||||
)
|
||||
Text(
|
||||
text = summary,
|
||||
style = mobileCallout.copy(fontWeight = FontWeight.SemiBold),
|
||||
style = ClawTheme.type.body.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = textColor,
|
||||
)
|
||||
}
|
||||
@@ -499,7 +498,7 @@ private fun RenderTableBlock(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalScroll(scrollState)
|
||||
.border(1.dp, mobileTextSecondary.copy(alpha = 0.25f)),
|
||||
.border(1.dp, ClawTheme.colors.textMuted.copy(alpha = 0.25f)),
|
||||
) {
|
||||
for (row in rows) {
|
||||
Row(
|
||||
@@ -509,11 +508,11 @@ private fun RenderTableBlock(
|
||||
val cell = row.cells.getOrNull(index) ?: AnnotatedString("")
|
||||
Text(
|
||||
text = cell,
|
||||
style = if (row.isHeader) mobileCaption1.copy(fontWeight = FontWeight.SemiBold) else inlineStyles.baseCallout,
|
||||
style = if (row.isHeader) ClawTheme.type.caption.copy(fontWeight = FontWeight.SemiBold) else inlineStyles.baseCallout,
|
||||
color = textColor,
|
||||
modifier =
|
||||
Modifier
|
||||
.border(1.dp, mobileTextSecondary.copy(alpha = 0.22f))
|
||||
.border(1.dp, ClawTheme.colors.textMuted.copy(alpha = 0.22f))
|
||||
.padding(horizontal = 8.dp, vertical = 6.dp)
|
||||
.width(160.dp),
|
||||
)
|
||||
@@ -1213,8 +1212,8 @@ private fun InlineBase64Image(
|
||||
Text(
|
||||
text = nativeString("Image unavailable"),
|
||||
modifier = Modifier.padding(vertical = 2.dp),
|
||||
style = mobileCaption1,
|
||||
color = mobileTextSecondary,
|
||||
style = ClawTheme.type.caption,
|
||||
color = ClawTheme.colors.textMuted,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,26 +9,9 @@ import ai.openclaw.app.chat.normalizeVisibleChatMessageRole
|
||||
import ai.openclaw.app.gateway.GatewayLoadedImage
|
||||
import ai.openclaw.app.i18n.nativeString
|
||||
import ai.openclaw.app.i18n.nativeStringResource
|
||||
import ai.openclaw.app.ui.MobileColorsAccessor
|
||||
import ai.openclaw.app.ui.design.ClawTheme
|
||||
import ai.openclaw.app.ui.image.RemoteImageResult
|
||||
import ai.openclaw.app.ui.image.safeRemoteImageStore
|
||||
import ai.openclaw.app.ui.mobileAccent
|
||||
import ai.openclaw.app.ui.mobileAccentBorderStrong
|
||||
import ai.openclaw.app.ui.mobileAccentSoft
|
||||
import ai.openclaw.app.ui.mobileBorder
|
||||
import ai.openclaw.app.ui.mobileBorderStrong
|
||||
import ai.openclaw.app.ui.mobileCallout
|
||||
import ai.openclaw.app.ui.mobileCaption1
|
||||
import ai.openclaw.app.ui.mobileCaption2
|
||||
import ai.openclaw.app.ui.mobileCardSurface
|
||||
import ai.openclaw.app.ui.mobileCodeBg
|
||||
import ai.openclaw.app.ui.mobileCodeBorder
|
||||
import ai.openclaw.app.ui.mobileCodeText
|
||||
import ai.openclaw.app.ui.mobileDanger
|
||||
import ai.openclaw.app.ui.mobileText
|
||||
import ai.openclaw.app.ui.mobileTextSecondary
|
||||
import ai.openclaw.app.ui.mobileWarning
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
@@ -94,8 +77,8 @@ private fun ChatBubbleContainer(
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
border = BorderStroke(1.dp, borderColor ?: if (user) mobileAccentBorderStrong else mobileBorderStrong),
|
||||
color = if (user) mobileAccentSoft else mobileCardSurface,
|
||||
border = BorderStroke(1.dp, borderColor ?: if (user) ClawTheme.colors.accentBorder else ClawTheme.colors.borderStrong),
|
||||
color = if (user) ClawTheme.colors.accentSoft else ClawTheme.colors.surfaceRaised,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
modifier =
|
||||
@@ -147,8 +130,8 @@ private fun ChatLinkPreview(
|
||||
Surface(
|
||||
onClick = { expanded = true },
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = mobileCardSurface,
|
||||
border = BorderStroke(1.dp, mobileBorder),
|
||||
color = ClawTheme.colors.surfaceRaised,
|
||||
border = BorderStroke(1.dp, ClawTheme.colors.border),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||
@@ -157,8 +140,8 @@ private fun ChatLinkPreview(
|
||||
) {
|
||||
Text(
|
||||
text = nativeString("Preview · \$domain", domain),
|
||||
style = mobileCaption1.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = mobileTextSecondary,
|
||||
style = ClawTheme.type.caption.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = ClawTheme.colors.textMuted,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
@@ -166,7 +149,7 @@ private fun ChatLinkPreview(
|
||||
androidx.compose.material3.Icon(
|
||||
imageVector = Icons.Default.ExpandMore,
|
||||
contentDescription = nativeString("Expand link preview"),
|
||||
tint = mobileTextSecondary,
|
||||
tint = ClawTheme.colors.textMuted,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -190,8 +173,8 @@ private fun ChatLinkPreview(
|
||||
Surface(
|
||||
onClick = { uriHandler.openUri(url) },
|
||||
shape = cardShape,
|
||||
color = mobileCardSurface,
|
||||
border = BorderStroke(1.dp, mobileBorder),
|
||||
color = ClawTheme.colors.surfaceRaised,
|
||||
border = BorderStroke(1.dp, ClawTheme.colors.border),
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
previewImage?.let { image ->
|
||||
@@ -206,16 +189,16 @@ private fun ChatLinkPreview(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(3.dp),
|
||||
) {
|
||||
Text(domain, style = mobileCaption2, color = mobileTextSecondary, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(domain, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
when (val preview = result) {
|
||||
null -> Text(nativeString("Loading preview…"), style = mobileCaption1, color = mobileTextSecondary)
|
||||
LinkPreviewResult.Failed -> Text(nativeString("No preview available"), style = mobileCallout, color = mobileTextSecondary)
|
||||
null -> Text(nativeString("Loading preview…"), style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted)
|
||||
LinkPreviewResult.Failed -> Text(nativeString("No preview available"), style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
|
||||
is LinkPreviewResult.Loaded -> {
|
||||
preview.metadata.title?.let { title ->
|
||||
Text(
|
||||
text = title,
|
||||
style = mobileCallout.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = mobileText,
|
||||
style = ClawTheme.type.body.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = ClawTheme.colors.text,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
@@ -223,8 +206,8 @@ private fun ChatLinkPreview(
|
||||
preview.metadata.description?.let { description ->
|
||||
Text(
|
||||
text = description,
|
||||
style = mobileCaption1,
|
||||
color = mobileTextSecondary,
|
||||
style = ClawTheme.type.caption,
|
||||
color = ClawTheme.colors.textMuted,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
@@ -262,17 +245,17 @@ fun ChatTypingIndicatorBubble(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
WorkingClawIcon(runKey = runKey, color = mobileAccent)
|
||||
WorkingClawIcon(runKey = runKey, color = ClawTheme.colors.accent)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
Text(formatLocalizedChatDurationCompact(elapsedMs), style = mobileCallout, color = mobileTextSecondary)
|
||||
Text(formatLocalizedChatDurationCompact(elapsedMs), style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
|
||||
tokens?.let {
|
||||
Text(nativeStringResource("·"), style = mobileCallout, color = mobileTextSecondary)
|
||||
Text(it, style = mobileCallout, color = mobileTextSecondary)
|
||||
Text(nativeStringResource("·"), style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
|
||||
Text(it, style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
|
||||
}
|
||||
phrase?.let { Text(nativeStringResource("· \$phrase", it), style = mobileCallout, color = mobileTextSecondary) }
|
||||
phrase?.let { Text(nativeStringResource("· \$phrase", it), style = ClawTheme.type.body, color = ClawTheme.colors.textMuted) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,7 +270,7 @@ fun ChatOutboxBubble(
|
||||
onDelete: () -> Unit,
|
||||
) {
|
||||
val failed = item.status == ChatOutboxStatus.Failed
|
||||
val statusColor = if (failed) mobileDanger else mobileWarning
|
||||
val statusColor = if (failed) ClawTheme.colors.danger else ClawTheme.colors.warning
|
||||
val statusLabel =
|
||||
when (item.status) {
|
||||
ChatOutboxStatus.Queued -> nativeString("Queued — sends when reconnected")
|
||||
@@ -314,13 +297,13 @@ fun ChatOutboxBubble(
|
||||
borderColor = statusColor.copy(alpha = 0.6f),
|
||||
) {
|
||||
if (item.text.isNotBlank()) {
|
||||
ChatMarkdown(text = item.text, textColor = mobileText)
|
||||
ChatMarkdown(text = item.text, textColor = ClawTheme.colors.text)
|
||||
}
|
||||
item.attachments.forEach { attachment ->
|
||||
Text(
|
||||
text = nativeString("📎 \${attachment.fileName}", attachment.fileName),
|
||||
style = mobileCaption1,
|
||||
color = mobileTextSecondary,
|
||||
style = ClawTheme.type.caption,
|
||||
color = ClawTheme.colors.textMuted,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
@@ -329,17 +312,17 @@ fun ChatOutboxBubble(
|
||||
) {
|
||||
Text(
|
||||
text = statusLabel,
|
||||
style = mobileCaption1,
|
||||
style = ClawTheme.type.caption,
|
||||
color = statusColor,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
if (failed && retryEnabled) {
|
||||
ChatOutboxAction(label = nativeString("Retry"), color = mobileAccent, onClick = onRetry)
|
||||
ChatOutboxAction(label = nativeString("Retry"), color = ClawTheme.colors.accent, onClick = onRetry)
|
||||
}
|
||||
// Sending rows are mid-dispatch and accepted rows may already be delivered; both stay
|
||||
// action-free until reconciliation resolves them, so a delete can never race a send.
|
||||
if (item.status == ChatOutboxStatus.Queued || failed) {
|
||||
ChatOutboxAction(label = nativeString("Delete"), color = mobileTextSecondary, onClick = onDelete)
|
||||
ChatOutboxAction(label = nativeString("Delete"), color = ClawTheme.colors.textMuted, onClick = onDelete)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -360,7 +343,7 @@ private fun ChatOutboxAction(
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = mobileCaption1.copy(fontWeight = FontWeight.SemiBold),
|
||||
style = ClawTheme.type.caption.copy(fontWeight = FontWeight.SemiBold),
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp),
|
||||
)
|
||||
}
|
||||
@@ -377,7 +360,7 @@ internal fun ChatBase64Image(
|
||||
if (image != null) {
|
||||
ChatImagePreview(image = image, description = mimeType ?: nativeString("Attachment"), stateKey = base64)
|
||||
} else if (imageState.failed) {
|
||||
Text(nativeString("Unsupported attachment"), style = mobileCaption1, color = mobileTextSecondary)
|
||||
Text(nativeString("Unsupported attachment"), style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,23 +397,23 @@ internal fun ChatManagedImage(
|
||||
Surface(
|
||||
onClick = { retryGeneration += 1 },
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
border = BorderStroke(1.dp, mobileBorder),
|
||||
color = mobileCardSurface,
|
||||
border = BorderStroke(1.dp, ClawTheme.colors.border),
|
||||
color = ClawTheme.colors.surfaceRaised,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
nativeString("Image unavailable · Tap to retry"),
|
||||
modifier = Modifier.padding(12.dp),
|
||||
style = mobileCaption1,
|
||||
color = mobileTextSecondary,
|
||||
style = ClawTheme.type.caption,
|
||||
color = ClawTheme.colors.textMuted,
|
||||
)
|
||||
}
|
||||
else ->
|
||||
Text(
|
||||
nativeString("Loading image…"),
|
||||
modifier = Modifier.padding(12.dp),
|
||||
style = mobileCaption1,
|
||||
color = mobileTextSecondary,
|
||||
style = ClawTheme.type.caption,
|
||||
color = ClawTheme.colors.textMuted,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -445,8 +428,8 @@ private fun ChatImagePreview(
|
||||
Surface(
|
||||
onClick = { previewVisible = true },
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
border = BorderStroke(1.dp, mobileBorder),
|
||||
color = mobileCardSurface,
|
||||
border = BorderStroke(1.dp, ClawTheme.colors.border),
|
||||
color = ClawTheme.colors.surfaceRaised,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Box {
|
||||
@@ -515,14 +498,14 @@ fun ChatCodeBlock(
|
||||
isComplete: Boolean = true,
|
||||
) {
|
||||
val display = code.trimEnd()
|
||||
// Token colors come from the theme's code palette so light/dark both keep readable contrast.
|
||||
val palette = MobileColorsAccessor.current
|
||||
// Syntax roles reuse semantic colors that keep at least 4.5:1 contrast against codeBg;
|
||||
// changing these mappings can make highlighted code less readable than plain code.
|
||||
val tokenColors =
|
||||
CodeTokenColors(
|
||||
keyword = palette.codeKeyword,
|
||||
string = palette.codeString,
|
||||
comment = palette.codeComment,
|
||||
number = palette.codeNumber,
|
||||
keyword = ClawTheme.colors.accent,
|
||||
string = ClawTheme.colors.success,
|
||||
comment = ClawTheme.colors.textMuted,
|
||||
number = ClawTheme.colors.danger,
|
||||
)
|
||||
// Keyed on content: streaming re-renders of unchanged blocks reuse the tokenized result,
|
||||
// and still-open fences stay plain until the closing fence arrives.
|
||||
@@ -532,23 +515,23 @@ fun ChatCodeBlock(
|
||||
}
|
||||
Surface(
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = mobileCodeBg,
|
||||
border = BorderStroke(1.dp, mobileCodeBorder),
|
||||
color = ClawTheme.colors.codeBg,
|
||||
border = BorderStroke(1.dp, ClawTheme.colors.codeBorder),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
if (!language.isNullOrBlank()) {
|
||||
Text(
|
||||
text = language.uppercase(Locale.US),
|
||||
style = mobileCaption2.copy(letterSpacing = 0.4.sp),
|
||||
color = mobileTextSecondary,
|
||||
style = ClawTheme.type.caption.copy(letterSpacing = 0.4.sp),
|
||||
color = ClawTheme.colors.textMuted,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = highlighted,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
style = mobileCallout,
|
||||
color = mobileCodeText,
|
||||
style = ClawTheme.type.body,
|
||||
color = ClawTheme.colors.codeText,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,6 @@ import ai.openclaw.app.ui.design.agentAvatarSource
|
||||
import ai.openclaw.app.ui.gatewayDiagnosticsEndpoint
|
||||
import ai.openclaw.app.ui.gatewayStatusForDisplay
|
||||
import ai.openclaw.app.ui.localizedUppercase
|
||||
import ai.openclaw.app.ui.mobileCallout
|
||||
import ai.openclaw.app.ui.relativeSessionTime
|
||||
import ai.openclaw.app.ui.rememberSystemAnimationsEnabled
|
||||
import android.os.SystemClock
|
||||
@@ -1922,7 +1921,7 @@ private fun ChatUserMessageText(
|
||||
if (preview != null && !expanded) {
|
||||
Text(
|
||||
text = preview,
|
||||
style = mobileCallout,
|
||||
style = ClawTheme.type.body,
|
||||
color = ClawTheme.colors.text,
|
||||
)
|
||||
} else {
|
||||
@@ -1944,7 +1943,7 @@ private fun ChatUserMessageText(
|
||||
) {
|
||||
Text(
|
||||
text = toggleLabel,
|
||||
style = mobileCallout.copy(fontSize = 12.sp, lineHeight = 16.sp, fontWeight = FontWeight.SemiBold),
|
||||
style = ClawTheme.type.body.copy(fontSize = 12.sp, lineHeight = 16.sp, fontWeight = FontWeight.SemiBold),
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package ai.openclaw.app.ui.design
|
||||
|
||||
import ai.openclaw.app.ui.LocalMobileColors
|
||||
import ai.openclaw.app.ui.darkMobileColors
|
||||
import ai.openclaw.app.ui.lightMobileColors
|
||||
import ai.openclaw.app.ui.mobileFontFamily
|
||||
import ai.openclaw.app.R
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Shapes
|
||||
import androidx.compose.material3.Typography
|
||||
@@ -16,12 +13,21 @@ import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
internal val clawFontFamily =
|
||||
FontFamily(
|
||||
Font(resId = R.font.manrope_400_regular, weight = FontWeight.Normal),
|
||||
Font(resId = R.font.manrope_500_medium, weight = FontWeight.Medium),
|
||||
Font(resId = R.font.manrope_600_semibold, weight = FontWeight.SemiBold),
|
||||
Font(resId = R.font.manrope_700_bold, weight = FontWeight.Bold),
|
||||
)
|
||||
|
||||
/**
|
||||
* App color tokens consumed by ClawTheme and bridged into Material components.
|
||||
*/
|
||||
@@ -31,6 +37,7 @@ internal data class ClawColors(
|
||||
val surface: Color,
|
||||
val surfaceRaised: Color,
|
||||
val surfacePressed: Color,
|
||||
val accent: Color,
|
||||
val accentSoft: Color,
|
||||
val accentBorder: Color,
|
||||
val border: Color,
|
||||
@@ -46,6 +53,9 @@ internal data class ClawColors(
|
||||
val warningSoft: Color,
|
||||
val danger: Color,
|
||||
val dangerSoft: Color,
|
||||
val codeBg: Color,
|
||||
val codeText: Color,
|
||||
val codeBorder: Color,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -97,6 +107,7 @@ private val ClawDarkColors =
|
||||
surface = Color(0xFF0A0A0A),
|
||||
surfaceRaised = Color(0xFF111111),
|
||||
surfacePressed = Color(0xFF1A1A1A),
|
||||
accent = Color(0xFF6EA8FF),
|
||||
accentSoft = Color(0xFF1A2A44),
|
||||
accentBorder = Color(0xFF5B93E8),
|
||||
border = Color(0xFF242424),
|
||||
@@ -112,6 +123,9 @@ private val ClawDarkColors =
|
||||
warningSoft = Color(0xFF2B2412),
|
||||
danger = Color(0xFFFF6B6B),
|
||||
dangerSoft = Color(0xFF2C1414),
|
||||
codeBg = Color(0xFF111317),
|
||||
codeText = Color(0xFFE8EAEE),
|
||||
codeBorder = Color(0xFF2B2E35),
|
||||
)
|
||||
|
||||
private val ClawLightColors =
|
||||
@@ -120,6 +134,7 @@ private val ClawLightColors =
|
||||
surface = Color(0xFFFFFEFB),
|
||||
surfaceRaised = Color(0xFFFFFFFF),
|
||||
surfacePressed = Color(0xFFE9EDF3),
|
||||
accent = Color(0xFF1B5ACB),
|
||||
accentSoft = Color(0xFFEAF2FF),
|
||||
accentBorder = Color(0xFF174CA9),
|
||||
border = Color(0xFFDDE3EC),
|
||||
@@ -135,12 +150,15 @@ private val ClawLightColors =
|
||||
warningSoft = Color(0xFFFFF3DC),
|
||||
danger = Color(0xFFB82929),
|
||||
dangerSoft = Color(0xFFFFE9E9),
|
||||
codeBg = Color(0xFFEFF3F8),
|
||||
codeText = Color(0xFF172033),
|
||||
codeBorder = Color(0xFFD7DDE7),
|
||||
)
|
||||
|
||||
private val LocalClawColors = staticCompositionLocalOf { ClawDarkColors }
|
||||
private val LocalClawSpacing = staticCompositionLocalOf { ClawSpacing() }
|
||||
private val LocalClawRadii = staticCompositionLocalOf { ClawRadii() }
|
||||
private val LocalClawTypography = staticCompositionLocalOf { clawTypography(mobileFontFamily) }
|
||||
private val LocalClawTypography = staticCompositionLocalOf { clawTypography(clawFontFamily) }
|
||||
|
||||
/**
|
||||
* Composition-local access point for OpenClaw Android design tokens.
|
||||
@@ -176,12 +194,10 @@ internal fun ClawDesignTheme(
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val colors = if (dark) ClawDarkColors else ClawLightColors
|
||||
val mobileColors = if (dark) darkMobileColors() else lightMobileColors()
|
||||
val typography = clawTypography(mobileFontFamily)
|
||||
val typography = clawTypography(clawFontFamily)
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalClawColors provides colors,
|
||||
LocalMobileColors provides mobileColors,
|
||||
LocalClawSpacing provides ClawSpacing(),
|
||||
LocalClawRadii provides ClawRadii(),
|
||||
LocalClawTypography provides typography,
|
||||
|
||||
@@ -99,12 +99,11 @@ class AndroidScreenshotFixtureTest {
|
||||
|
||||
@Test
|
||||
fun providesDeterministicChatHistory() {
|
||||
val messages =
|
||||
val history =
|
||||
json
|
||||
.parseToJsonElement(AndroidScreenshotFixture.request("chat.history", null))
|
||||
.jsonObject["messages"]
|
||||
?.jsonArray
|
||||
.orEmpty()
|
||||
.jsonObject
|
||||
val messages = history["messages"]?.jsonArray.orEmpty()
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
@@ -120,8 +119,8 @@ class AndroidScreenshotFixtureTest {
|
||||
listOf("user", "Summarize the open review feedback for me.", "1783555140000"),
|
||||
listOf(
|
||||
"assistant",
|
||||
"The main thread asks for a regression test around session restore, and the second one wants the new " +
|
||||
"config key documented before merge. Both are small; I can draft patches for each if you want.",
|
||||
"The release check is ready:\n\n```kotlin\nval ready = lint && tests\n```\n\n" +
|
||||
"Review https://openclaw.ai before tagging.",
|
||||
"1783555200000",
|
||||
),
|
||||
listOf("system", "Compaction", "1783555220000"),
|
||||
@@ -157,6 +156,9 @@ class AndroidScreenshotFixtureTest {
|
||||
val reset = messages[7].jsonObject["__openclaw"]?.jsonObject
|
||||
assertEquals("reset", reset?.get("kind")?.jsonPrimitive?.content)
|
||||
assertEquals("android-screenshot-reset", reset?.get("id")?.jsonPrimitive?.content)
|
||||
val inFlightRun = history["inFlightRun"]?.jsonObject
|
||||
assertEquals("android-screenshot-active-run", inFlightRun?.get("runId")?.jsonPrimitive?.content)
|
||||
assertEquals("", inFlightRun?.get("text")?.jsonPrimitive?.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user