Skip to content

Commit ae86c12

Browse files
eriedclaude
andcommitted
Sources sheet: visually separate Compare tab from the 3 source tabs
Three changes to make Compare read as an action rather than a fourth peer: - Spatial gap: source chips are clustered tight (6dp); a 10dp gap + thin vertical divider + another 10dp gap sit before the Compare chip, so the tab bar reads as "(Phone | Wheel | External) | Compare". - Different shape: Compare chip is outlined (border-only, no fill) when unselected, vs the source chips' filled surfaceVariant background. When selected it fills lightly like the source chips do, so the active state still reads obviously. - Icon prefix: Compare gets a CompareArrows icon where source tabs have their live/offline dot, reinforcing the visual difference. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b66d607 commit ae86c12

1 file changed

Lines changed: 139 additions & 58 deletions

File tree

app/src/main/java/com/eried/eucplanet/ui/dashboard/sources/DataSourcesSheet.kt

Lines changed: 139 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ import androidx.compose.foundation.layout.width
2222
import androidx.compose.foundation.layout.wrapContentSize
2323
import androidx.compose.foundation.shape.CircleShape
2424
import androidx.compose.foundation.shape.RoundedCornerShape
25+
import androidx.compose.material.icons.Icons
26+
import androidx.compose.material.icons.filled.CompareArrows
2527
import androidx.compose.material3.ExperimentalMaterial3Api
28+
import androidx.compose.material3.Icon
2629
import androidx.compose.material3.MaterialTheme
2730
import androidx.compose.material3.ModalBottomSheet
2831
import androidx.compose.material3.Text
@@ -144,70 +147,148 @@ private fun TabBar(
144147
selected: TabKind,
145148
onSelect: (TabKind) -> Unit
146149
) {
147-
data class Entry(
148-
val tab: TabKind,
149-
val label: String,
150-
val color: Color,
151-
val source: DataSource?
152-
)
153-
val entries = listOf(
154-
Entry(TabKind.PHONE, DataSource.PHONE.displayName, DataSource.PHONE.color, DataSource.PHONE),
155-
Entry(TabKind.WHEEL, DataSource.WHEEL.displayName, DataSource.WHEEL.color, DataSource.WHEEL),
156-
Entry(TabKind.RACEBOX, DataSource.RACEBOX.displayName, DataSource.RACEBOX.color, DataSource.RACEBOX),
157-
Entry(TabKind.COMPARE, "Compare", MaterialTheme.colorScheme.onSurface, null)
150+
val sourceEntries = listOf(
151+
Triple(TabKind.PHONE, DataSource.PHONE, DataSource.PHONE.displayName),
152+
Triple(TabKind.WHEEL, DataSource.WHEEL, DataSource.WHEEL.displayName),
153+
Triple(TabKind.RACEBOX, DataSource.RACEBOX, DataSource.RACEBOX.displayName)
158154
)
159155
Row(
160156
modifier = Modifier.fillMaxWidth(),
161-
horizontalArrangement = Arrangement.spacedBy(6.dp)
157+
verticalAlignment = Alignment.CenterVertically
162158
) {
163-
entries.forEach { e ->
164-
val isSel = e.tab == selected
165-
val live = e.source?.let { snapshots[it]?.isLive == true }
166-
Box(
167-
modifier = Modifier
168-
.weight(1f)
169-
.clip(RoundedCornerShape(10.dp))
170-
.background(
171-
if (isSel) e.color.copy(alpha = 0.18f)
172-
else MaterialTheme.colorScheme.surfaceVariant
173-
)
174-
.clickable { onSelect(e.tab) }
175-
.padding(vertical = 8.dp, horizontal = 4.dp),
176-
contentAlignment = Alignment.Center
177-
) {
178-
Row(
179-
verticalAlignment = Alignment.CenterVertically,
180-
horizontalArrangement = Arrangement.Center
181-
) {
182-
// Live/offline dot — filled colour for live, hollow grey
183-
// ring for offline. Skipped on the Compare tab.
184-
if (live != null) {
185-
if (live) {
186-
Box(
187-
modifier = Modifier
188-
.size(7.dp)
189-
.clip(CircleShape)
190-
.background(e.color)
191-
)
192-
} else {
193-
Box(
194-
modifier = Modifier
195-
.size(7.dp)
196-
.clip(CircleShape)
197-
.border(1.dp, Color(0xFF707070), CircleShape)
198-
)
199-
}
200-
Spacer(Modifier.width(5.dp))
201-
}
202-
Text(
203-
text = e.label,
204-
fontSize = 12.sp,
205-
fontWeight = if (isSel) FontWeight.Bold else FontWeight.Medium,
206-
color = if (isSel) e.color else MaterialTheme.colorScheme.onSurfaceVariant
207-
)
208-
}
159+
// Three source tabs in a tight cluster — they're peers, all the
160+
// same chip style with a live/offline dot prefix.
161+
Row(
162+
modifier = Modifier.weight(3f),
163+
horizontalArrangement = Arrangement.spacedBy(6.dp)
164+
) {
165+
sourceEntries.forEach { (tab, source, label) ->
166+
SourceTabChip(
167+
label = label,
168+
color = source.color,
169+
isSelected = tab == selected,
170+
isLive = snapshots[source]?.isLive == true,
171+
onClick = { onSelect(tab) },
172+
modifier = Modifier.weight(1f)
173+
)
209174
}
210175
}
176+
// Visual break before Compare: 10dp gap + a thin vertical divider
177+
// so the tab bar reads as "(3 sources) | (action)" instead of four
178+
// peers. Without this the Compare chip blends in with the sources.
179+
Spacer(Modifier.width(10.dp))
180+
Box(
181+
modifier = Modifier
182+
.width(1.dp)
183+
.height(28.dp)
184+
.background(MaterialTheme.colorScheme.surfaceVariant)
185+
)
186+
Spacer(Modifier.width(10.dp))
187+
// Compare tab — outlined chip (no filled background) when unselected
188+
// so it reads visually distinct from the three source chips, plus a
189+
// compare-arrows icon prefix that source tabs don't have.
190+
CompareTabChip(
191+
isSelected = TabKind.COMPARE == selected,
192+
onClick = { onSelect(TabKind.COMPARE) },
193+
modifier = Modifier.weight(1.1f)
194+
)
195+
}
196+
}
197+
198+
@Composable
199+
private fun SourceTabChip(
200+
label: String,
201+
color: Color,
202+
isSelected: Boolean,
203+
isLive: Boolean,
204+
onClick: () -> Unit,
205+
modifier: Modifier = Modifier
206+
) {
207+
Box(
208+
modifier = modifier
209+
.clip(RoundedCornerShape(10.dp))
210+
.background(
211+
if (isSelected) color.copy(alpha = 0.18f)
212+
else MaterialTheme.colorScheme.surfaceVariant
213+
)
214+
.clickable { onClick() }
215+
.padding(vertical = 8.dp, horizontal = 4.dp),
216+
contentAlignment = Alignment.Center
217+
) {
218+
Row(
219+
verticalAlignment = Alignment.CenterVertically,
220+
horizontalArrangement = Arrangement.Center
221+
) {
222+
if (isLive) {
223+
Box(
224+
modifier = Modifier
225+
.size(7.dp)
226+
.clip(CircleShape)
227+
.background(color)
228+
)
229+
} else {
230+
Box(
231+
modifier = Modifier
232+
.size(7.dp)
233+
.clip(CircleShape)
234+
.border(1.dp, Color(0xFF707070), CircleShape)
235+
)
236+
}
237+
Spacer(Modifier.width(5.dp))
238+
Text(
239+
text = label,
240+
fontSize = 12.sp,
241+
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium,
242+
color = if (isSelected) color else MaterialTheme.colorScheme.onSurfaceVariant
243+
)
244+
}
245+
}
246+
}
247+
248+
@Composable
249+
private fun CompareTabChip(
250+
isSelected: Boolean,
251+
onClick: () -> Unit,
252+
modifier: Modifier = Modifier
253+
) {
254+
val accent = MaterialTheme.colorScheme.onSurface
255+
Box(
256+
modifier = modifier
257+
.clip(RoundedCornerShape(10.dp))
258+
// Outlined when unselected — no filled background, just a border.
259+
// When selected it fills like the source chips so the active
260+
// state still reads obviously.
261+
.then(
262+
if (isSelected) Modifier.background(accent.copy(alpha = 0.12f))
263+
else Modifier
264+
)
265+
.border(
266+
width = 1.dp,
267+
color = if (isSelected) accent.copy(alpha = 0.5f) else accent.copy(alpha = 0.30f),
268+
shape = RoundedCornerShape(10.dp)
269+
)
270+
.clickable { onClick() }
271+
.padding(vertical = 8.dp, horizontal = 4.dp),
272+
contentAlignment = Alignment.Center
273+
) {
274+
Row(
275+
verticalAlignment = Alignment.CenterVertically,
276+
horizontalArrangement = Arrangement.Center
277+
) {
278+
Icon(
279+
imageVector = Icons.Default.CompareArrows,
280+
contentDescription = null,
281+
tint = if (isSelected) accent else accent.copy(alpha = 0.6f),
282+
modifier = Modifier.size(14.dp)
283+
)
284+
Spacer(Modifier.width(4.dp))
285+
Text(
286+
text = "Compare",
287+
fontSize = 12.sp,
288+
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium,
289+
color = if (isSelected) accent else accent.copy(alpha = 0.7f)
290+
)
291+
}
211292
}
212293
}
213294

0 commit comments

Comments
 (0)