redesign NodeCard with cleaner Material 3 styling#420
Conversation
Drop the random ColorMap avatar tint and hardcoded colors in favor of colorScheme tokens. Split into list mode (flat row, leading 3dp primary bar as the sole selection signal) and card mode (ElevatedCard for HomeScreen). Unify icons to outlined family, tighten typography and spacing, and use a softer inset divider. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request refactors the NodeCard component to support both standalone card and list modes, extracts NodeCardContent and DelayChip composables, updates icons to outlined variants, and refines dividers in ConfigScreen. Feedback is provided to optimize performance by only running infinite animations when active, and to improve accessibility in dark theme by dynamically adjusting the hardcoded colors in DelayChip for better contrast.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if (onTest != null) { | ||
| val infiniteTransition = rememberInfiniteTransition(label = "pulse") | ||
| val scale by infiniteTransition.animateFloat( | ||
| initialValue = 1f, | ||
| targetValue = if (testing) 1.2f else 1f, | ||
| animationSpec = infiniteRepeatable( | ||
| animation = tween(800, easing = LinearEasing), | ||
| repeatMode = androidx.compose.animation.core.RepeatMode.Reverse | ||
| ), | ||
| label = "scale" | ||
| ) | ||
| val alpha by infiniteTransition.animateFloat( | ||
| initialValue = 1f, | ||
| targetValue = if (testing) 0.4f else 1f, | ||
| animationSpec = infiniteRepeatable( | ||
| animation = tween(800, easing = LinearEasing), | ||
| repeatMode = androidx.compose.animation.core.RepeatMode.Reverse | ||
| ), | ||
| label = "alpha" | ||
| ) | ||
| IconButton( | ||
| onClick = onTest, | ||
| enabled = enableTest, | ||
| modifier = Modifier.size(36.dp) | ||
| ) { | ||
| Icon( | ||
| imageVector = Icons.Outlined.Speed, | ||
| contentDescription = stringResource(R.string.test_url), | ||
| modifier = Modifier | ||
| .size(20.dp) | ||
| .scale(scale), | ||
| tint = if (enableTest) | ||
| MaterialTheme.colorScheme.primary.copy(alpha = alpha) | ||
| else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f) | ||
| ) | ||
|
|
||
| IconButton(onClick = onTest, enabled = enableTest) { | ||
| Icon( | ||
| imageVector = Icons.Outlined.Speed, | ||
| contentDescription = stringResource(R.string.test_url), | ||
| modifier = Modifier.scale(scale), | ||
| tint = if (enableTest) { | ||
| MaterialTheme.colorScheme.primary.copy(alpha = alpha) | ||
| } else Color.Gray | ||
| ) | ||
| } | ||
| } | ||
| if (onShare != null) { | ||
| IconButton(onClick = onShare) { | ||
| Icon(Icons.Default.Share, contentDescription = stringResource(R.string.clipboard_export)) | ||
| } | ||
| } |
There was a problem hiding this comment.
Performance Optimization: Avoid Infinite Animations when Idle
Currently, infiniteTransition is created and run continuously even when testing is false. Because infiniteRepeatable animations run on every frame, this will cause the composable to constantly redraw/recompose at 60fps or 120fps, leading to high CPU usage and rapid battery drain, especially when multiple cards are displayed in a list.
By moving the infinite transition and its animations inside the if (testing) block, we ensure that the animation only runs when active, and is completely removed from the composition when idle.
if (onTest != null) {
IconButton(
onClick = onTest,
enabled = enableTest,
modifier = Modifier.size(36.dp)
) {
if (testing) {
val infiniteTransition = rememberInfiniteTransition(label = "pulse")
val scale by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 1.2f,
animationSpec = infiniteRepeatable(
animation = tween(800, easing = LinearEasing),
repeatMode = androidx.compose.animation.core.RepeatMode.Reverse
),
label = "scale"
)
val alpha by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 0.4f,
animationSpec = infiniteRepeatable(
animation = tween(800, easing = LinearEasing),
repeatMode = androidx.compose.animation.core.RepeatMode.Reverse
),
label = "alpha"
)
Icon(
imageVector = Icons.Outlined.Speed,
contentDescription = stringResource(R.string.test_url),
modifier = Modifier
.size(20.dp)
.scale(scale),
tint = MaterialTheme.colorScheme.primary.copy(alpha = alpha)
)
} else {
Icon(
imageVector = Icons.Outlined.Speed,
contentDescription = stringResource(R.string.test_url),
modifier = Modifier.size(20.dp),
tint = if (enableTest)
MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)
)
}
}
}| private fun DelayChip(delayMs: Long) { | ||
| // Delay semantic colors: keep universal green/orange/red semantics, but tone down to Material-friendly saturation | ||
| val delayColor = when { | ||
| delayMs == -2L -> MaterialTheme.colorScheme.error | ||
| delayMs < 300 -> Color(0xFF2E7D32) // green 800 | ||
| delayMs < 900 -> Color(0xFFE65100) // orange 900 | ||
| else -> MaterialTheme.colorScheme.error | ||
| } |
There was a problem hiding this comment.
Accessibility: Improve Contrast Ratio in Dark Theme
Using hardcoded dark colors like Color(0xFF2E7D32) (Green 800) and Color(0xFFE65100) (Orange 900) on a dark background in dark theme results in extremely low contrast, making the text nearly unreadable and violating WCAG accessibility guidelines.
We should dynamically adjust these colors based on the system theme using isSystemInDarkTheme(), using lighter/softer colors in dark theme and darker/more saturated colors in light theme.
private fun DelayChip(delayMs: Long) {
// Delay semantic colors: keep universal green/orange/red semantics, but tone down to Material-friendly saturation
val isDark = androidx.compose.foundation.isSystemInDarkTheme()
val delayColor = when {
delayMs == -2L -> MaterialTheme.colorScheme.error
delayMs < 300 -> if (isDark) Color(0xFF81C784) else Color(0xFF2E7D32) // green 300 in dark, green 800 in light
delayMs < 900 -> if (isDark) Color(0xFFFFB74D) else Color(0xFFE65100) // orange 300 in dark, orange 900 in light
else -> MaterialTheme.colorScheme.error
}
Drop the random ColorMap avatar tint and hardcoded colors in favor of colorScheme tokens. Split into list mode (flat row, leading 3dp primary bar as the sole selection signal) and card mode (ElevatedCard for HomeScreen). Unify icons to outlined family, tighten typography and spacing, and use a softer inset divider.