Skip to content
This repository was archived by the owner on Aug 25, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,34 @@ messages, UI strings, or script output. Plain text only. Also no em-dashes; writ
plain human voice (commit subjects are the user-facing changelog). Use words like `PASS`/
`FAIL`, not pictographs.

## The verification bar: a UI change is "verified" ONLY when every box below is checked

Phases passing + host gates green is NOT the bar - twice in one session (2026-07-23) a change
was declared verified with boxes missing, and the user caught both. Before writing "verified":

1. **Coverage phases - ONLY the phases the change touches, never the full tour.** The full
multi-hour matrix for a scoped change is a waste of device time and delays the verdict;
`PHASES="..."` exists precisely so a settings change re-runs ~5 min/leg, not ~40. Same
scoping on the OTHER axes: both flavors only when the change can differ between them, the
`SOFTKEYS=off` touch leg only when the change touches a surface the soft-key declutter
diverges (search bar, FABs, place-sheet buttons, Options menus) - a spoke-only settings
change does not need it. Run the touched phases at all 4 geometries; skip axes the change
provably cannot vary across, and SAY which were skipped and why.
2. **Dynamic focus walk** of any changed/added settings spoke
(`AUDIT_SECTIONS=<Spoke> tests/dpad/audit_dynamic.sh`): every row takes focus in ORDER with
the orange ring on each stop. Reusing an enforced component does not waive this.
3. **Ring evidence** for new focusable controls - a walk frame or ring_walk assertion, not an
inference from the component used.
4. **Enumerate EVERY surface the change touches** and verify each one. A theme touches every
screen; a palette role touches every consumer; a shared component touches every caller. If
no phase reaches a touched surface, ADD ONE (same PR) - that is the fix-the-harness rule.
5. **Eyeball the frames yourself**, show them in chat, and put grids in the PR body.
6. **A/B against main** whenever a check fails - regression and pre-existing look identical
until you run the same check on main's build.

State the boxes explicitly when declaring verification; if one is genuinely unmet, name it and
why instead of letting "verified" slide.

## Visual verification (HARD RULE, NO EXCEPTIONS)

**Anything a user can see MUST be verified VISUALLY, by looking at an actual on-device
Expand Down Expand Up @@ -1052,7 +1080,7 @@ state - upstream's own 13ac02e8 already made the layers panel a VelaMenu):
in-app theme with the composable **`isAppInDarkTheme()`** - never call
`isSystemInDarkTheme()` directly in app UI (it ignores the user's Light/Dark/
System choice in Settings → Appearance). FOUR modes since 2026-07-23: SYSTEM /
LIGHT / DARK / **AMOLED** (the dark scheme on true-black surfaces, `AmoledColors`
LIGHT / DARK / **Black** (the dark scheme on true-black surfaces - the AMOLED mode, `AmoledColors`
in `Theme.kt`; `isAppInDarkTheme()` is true for it, so the map and every dark
branch follow automatically). Light mode uses soft teal-cast off-whites, never
pure white (user feedback - harsh). `AppTheme.mode` is a process-wide
Expand Down
6 changes: 6 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,12 @@ dependencies {
// stays UI-agnostic.
implementation(libs.maplibre.android)
implementation(libs.androidx.car.app) // Android Auto (projection): templates + car surface
// The PROJECTED HOST CONNECTOR - without it Android Auto never lists the app: the host
// discovers a car app through the CarAppMetadataHolderService this artifact merges into the
// manifest, not through our own service declaration alone (official car-app docs: app-projected
// is the Android Auto artifact). The first-cut port shipped only the base :app template
// library, which is why Vela has never appeared in an AA launcher (device report, OnePlus 12).
implementation(libs.androidx.car.app.projected)

debugImplementation(libs.androidx.compose.ui.tooling)
}
14 changes: 12 additions & 2 deletions app/src/main/java/app/vela/ui/SheetPalette.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,24 @@ object SheetPalette {
val DimLight = Color(0xFF5F6368)
val RowDark = Color(0xFF202124) // inset row / chip background
val RowLight = Color(0xFFF1F3F4)
// AMOLED: the sheets go TRUE BLACK (they are the app's largest chrome surfaces - a grey sheet
// on a black theme defeats the mode's whole point), with the inset rows stepped to a near-black
// so chips/rows still read as layers. Ink/Dim stay the dark values (contrast is higher on black).
val Amoled = Color(0xFF000000)
val RowAmoled = Color(0xFF000000) // rows too - black is black; hairlines separate

/** True while the app-wide theme is the AMOLED mode - a state read, so composables that call
* the helpers below recompose when the mode flips (same mechanism as isAppInDarkTheme). */
private val amoled: Boolean
get() = app.vela.ui.theme.AppTheme.mode.value == app.vela.ui.theme.ThemeMode.AMOLED

// Shared traffic-coded colours (route ETAs, the route line, the steps header).
val TrafficGreen = Color(0xFF1E8E3E)
val TrafficAmber = Color(0xFFE8923D)
val TrafficRed = Color(0xFFD93838)

fun bg(dark: Boolean) = if (dark) Dark else Light
fun bg(dark: Boolean) = if (dark) { if (amoled) Amoled else Dark } else Light
fun ink(dark: Boolean) = if (dark) InkDark else InkLight
fun dim(dark: Boolean) = if (dark) DimDark else DimLight
fun row(dark: Boolean) = if (dark) RowDark else RowLight
fun row(dark: Boolean) = if (dark) { if (amoled) RowAmoled else RowDark } else RowLight
}
15 changes: 8 additions & 7 deletions app/src/main/java/app/vela/ui/place/PlaceSheet.kt
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,9 @@ import java.util.Locale
// like Google Maps, instead of a washed-out dynamic tone.
// The sheet palette is shared app-wide (see ui/SheetPalette) so the place sheet,
// directions panel, route chooser and steps list all match.
private val SheetDark = SheetPalette.Dark
private val SheetLight = SheetPalette.Light
// Resolved through SheetPalette.bg() at every use site (NOT captured as constants): bg() is
// AMOLED-aware - a captured val froze the grey and the place sheet stayed #1F1F1F on the
// true-black theme while every other sheet went black (code-audit find, 2026-07-23).
private val InkDark = SheetPalette.InkDark
private val InkLight = SheetPalette.InkLight
private val DimDark = SheetPalette.DimDark
Expand Down Expand Up @@ -380,7 +381,7 @@ fun PlaceSheet(
Card(
modifier.fillMaxWidth().heightIn(max = maxSheetHeight),
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
colors = CardDefaults.cardColors(containerColor = if (dark) SheetDark else SheetLight),
colors = CardDefaults.cardColors(containerColor = SheetPalette.bg(dark)),
) {
// Card background fills to the screen bottom; pad the content up off the nav bar.
Column(Modifier.navigationBarsPadding()) {
Expand Down Expand Up @@ -1083,7 +1084,7 @@ fun DirectionsPanel(
Card(
modifier.fillMaxWidth(),
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
colors = CardDefaults.cardColors(containerColor = if (dark) SheetDark else SheetLight),
colors = CardDefaults.cardColors(containerColor = SheetPalette.bg(dark)),
) {
Column(Modifier.navigationBarsPadding().padding(start = 20.dp, end = 8.dp, top = 8.dp, bottom = 16.dp)) {
// Drag handle - swipe down to minimise the chooser (peek the route on the
Expand Down Expand Up @@ -1531,7 +1532,7 @@ fun TransitNavSheet(
val dim = if (dark) DimDark else DimLight
val itin = nav.itinerary
val step = itin.steps.getOrNull(nav.stepIndex)
Surface(Modifier.fillMaxSize(), color = if (dark) SheetDark else SheetLight) {
Surface(Modifier.fillMaxSize(), color = SheetPalette.bg(dark)) {
Column(Modifier.fillMaxSize().statusBarsPadding().navigationBarsPadding().padding(16.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
Expand Down Expand Up @@ -2042,7 +2043,7 @@ fun RouteDetailSheet(
step?.alightStop?.let { a -> if (mid.none { it.name == a.name } && step.boardStop?.name != a.name) add(a) }
}
}
Surface(Modifier.fillMaxSize(), color = if (dark) SheetDark else SheetLight) {
Surface(Modifier.fillMaxSize(), color = SheetPalette.bg(dark)) {
Column(Modifier.fillMaxSize().statusBarsPadding().navigationBarsPadding()) {
Row(
Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 6.dp),
Expand Down Expand Up @@ -2597,7 +2598,7 @@ private fun FullScreenReviews(featureId: String, place: Place, ink: Color, dim:
// to the WebView cleanly once the page loads. No-op under touch.
val reviewsBackFocus = rememberDpadAutoFocus()
Dialog(onDismissRequest = onClose, properties = DialogProperties(usePlatformDefaultWidth = false)) {
Surface(Modifier.fillMaxSize(), color = if (dark) SheetDark else SheetLight, contentColor = ink) {
Surface(Modifier.fillMaxSize(), color = SheetPalette.bg(dark), contentColor = ink) {
Column(Modifier.fillMaxSize().statusBarsPadding()) {
Row(
verticalAlignment = Alignment.CenterVertically,
Expand Down
14 changes: 9 additions & 5 deletions app/src/main/java/app/vela/ui/softkey/VelaSoftkeys.kt
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,13 @@ object VelaSoftkeys {
// visible flash on each screen change. So rebuild ONLY when the theme actually flipped; a plain
// label change just re-set()s in place (no churn, no flash).
val dark = app.vela.ui.theme.isAppInDarkTheme()
val themeMode = app.vela.ui.theme.AppTheme.mode.value
val modal = modalDepth.intValue > 0 // a VelaDialog is up -> hide the bar (keys go to it)
val lastDark = remember { mutableStateOf<Boolean?>(null) }
LaunchedEffect(left?.label, right?.label, dark, modal) {
val lastTheme = remember { mutableStateOf<app.vela.ui.theme.ThemeMode?>(null) }
LaunchedEffect(left?.label, right?.label, dark, themeMode, modal) {
val ctl = Softkeys.of(activity)
val themeFlipped = lastDark.value?.let { it != dark } == true
lastDark.value = dark
val themeFlipped = lastTheme.value?.let { it != themeMode } == true
lastTheme.value = themeMode
applyThemeColors(dark)
if (modal || (leftNow == null && rightNow == null)) {
ctl.clear()
Expand All @@ -139,9 +140,12 @@ object VelaSoftkeys {
/** Paint the bar for the in-app theme. A dark toolbar in dark, a light one in light - both
* single-ink, matching Vela's map chrome. Applied before a rebuild (see [MapSoftkeys]). */
private fun applyThemeColors(dark: Boolean) {
// AMOLED: the bar goes true black with a slightly stronger divider so it still separates
// from a black map bezel; every other colour keeps the dark set (contrast only improves).
val amoled = app.vela.ui.theme.AppTheme.mode.value == app.vela.ui.theme.ThemeMode.AMOLED
Yapchik.style.apply {
if (dark) {
backgroundColor = 0xFF14343A.toInt()
backgroundColor = if (amoled) 0xFF000000.toInt() else 0xFF14343A.toInt()
textColor = 0xFFECECEC.toInt()
pressedTextColor = 0xFF4DD0C4.toInt()
dividerColor = 0x33FFFFFF
Expand Down
15 changes: 8 additions & 7 deletions app/src/main/java/app/vela/ui/theme/Theme.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,19 @@ private val DarkColors = darkColorScheme(
tertiary = VelaAmber,
)

// AMOLED: the dark scheme on TRUE BLACK surfaces (every lit pixel costs battery on OLED, and
// pure black is its own look). Container roles step up in near-blacks so cards and the title bar
// still read as layers; the thin borders on Settings cards carry the structure.
// Black: the dark scheme on TRUE BLACK - every surface AND container role is #000000 ("black
// should be black", user 2026-07-23; the earlier near-black stepping read as grey areas). The
// hairline outlines and the Settings cards' thin borders carry ALL of the structure; nothing
// separates by fill any more.
private val AmoledColors = DarkColors.copy(
background = androidx.compose.ui.graphics.Color(0xFF000000),
surface = androidx.compose.ui.graphics.Color(0xFF000000),
surfaceDim = androidx.compose.ui.graphics.Color(0xFF000000),
surfaceContainerLowest = androidx.compose.ui.graphics.Color(0xFF000000),
surfaceContainerLow = androidx.compose.ui.graphics.Color(0xFF060809),
surfaceContainer = androidx.compose.ui.graphics.Color(0xFF0B0E0F),
surfaceContainerHigh = androidx.compose.ui.graphics.Color(0xFF121617),
surfaceContainerHighest = androidx.compose.ui.graphics.Color(0xFF191E1F),
surfaceContainerLow = androidx.compose.ui.graphics.Color(0xFF000000),
surfaceContainer = androidx.compose.ui.graphics.Color(0xFF000000),
surfaceContainerHigh = androidx.compose.ui.graphics.Color(0xFF000000),
surfaceContainerHighest = androidx.compose.ui.graphics.Color(0xFF000000),
)

/**
Expand Down
8 changes: 5 additions & 3 deletions app/src/main/java/app/vela/web/ReviewsPanel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -473,9 +473,11 @@ private fun buildPanelWebView(
* Maintenance passes keep re-applying for a while (the SPA re-attaches chrome on interaction).
*/
private fun carveScript(dark: Boolean, fullScreen: Boolean): String {
// Vela's own sheet colour (SheetPalette Dark/Light) - the panel matches it EXACTLY so there's
// no seam with the surrounding place sheet.
val bg = if (dark) "#1f1f1f" else "#ffffff"
// Vela's own sheet colour - the panel matches SheetPalette.bg() EXACTLY so there's no seam
// with the surrounding place sheet (incl. the AMOLED true-black variant).
val bg = if (dark) {
if (app.vela.ui.theme.AppTheme.mode.value == app.vela.ui.theme.ThemeMode.AMOLED) "#000000" else "#1f1f1f"
} else "#ffffff"
// Dark = a scoped invert on the PANEL CONTENT ONLY (main), NOT its background. The Vela colour
// lives on <body> (which the filter doesn't touch - it's on main), and main + every ancestor
// are made transparent so that colour is the panel's backdrop; only Google's content inverts.
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<string name="settings_follow_system">System folgen</string>
<string name="settings_theme_light">Hell</string>
<string name="settings_theme_dark">Dunkel</string>
<string name="settings_theme_amoled">AMOLED-Schwarz</string>
<string name="settings_theme_amoled">Schwarz</string>
<string name="settings_appearance_hint">Hell/Dunkel gilt nur für Vela, das System-Design Ihres Telefons wird nicht verändert.</string>
<string name="settings_map_style">Kartenstil</string>
<string name="settings_map_style_hint">OpenFreeMap (Standard) ist schluessellos und detailliert.</string>
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<string name="settings_follow_system">Seguir el sistema</string>
<string name="settings_theme_light">Claro</string>
<string name="settings_theme_dark">Oscuro</string>
<string name="settings_theme_amoled">Negro AMOLED</string>
<string name="settings_theme_amoled">Negro</string>
<string name="settings_appearance_hint">Claro/Oscuro se aplica solo a Vela; no cambia el tema del sistema de tu teléfono.</string>
<string name="settings_map_style">Estilo del mapa</string>
<string name="settings_map_style_hint">OpenFreeMap (predeterminado) no necesita clave y es detallado.</string>
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<string name="settings_follow_system">Suivre le système</string>
<string name="settings_theme_light">Clair</string>
<string name="settings_theme_dark">Sombre</string>
<string name="settings_theme_amoled">Noir AMOLED</string>
<string name="settings_theme_amoled">Noir</string>
<string name="settings_appearance_hint">Le mode clair/sombre s\'applique uniquement à Vela - il ne modifie pas le thème système de votre téléphone.</string>
<string name="settings_map_style">Style de carte</string>
<string name="settings_map_style_hint">OpenFreeMap (par defaut) est sans cle et detaille.</string>
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values-it/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<string name="settings_follow_system">Come il sistema</string>
<string name="settings_theme_light">Chiaro</string>
<string name="settings_theme_dark">Scuro</string>
<string name="settings_theme_amoled">Nero AMOLED</string>
<string name="settings_theme_amoled">Nero</string>
<string name="settings_appearance_hint">Chiaro/Scuro si applica solo a Vela, non modifica il tema di sistema del telefono.</string>
<string name="settings_map_style">Stile mappa</string>
<string name="settings_map_style_hint">OpenFreeMap (predefinito) e senza chiave e dettagliato.</string>
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values-iw/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<string name="settings_follow_system">לפי הגדרת המערכת</string>
<string name="settings_theme_light">בהיר</string>
<string name="settings_theme_dark">כהה</string>
<string name="settings_theme_amoled">שחור AMOLED</string>
<string name="settings_theme_amoled">שחור</string>
<string name="settings_appearance_hint">חל על Vela בלבד.</string>
<string name="settings_map_style">סגנון מפה</string>
<string name="settings_map_style_hint">OpenFreeMap (ברירת המחדל) מפורט ואינו דורש מפתח.</string>
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values-ja/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<string name="settings_follow_system">システムに合わせる</string>
<string name="settings_theme_light">ライト</string>
<string name="settings_theme_dark">ダーク</string>
<string name="settings_theme_amoled">AMOLEDブラック</string>
<string name="settings_theme_amoled">ブラック</string>
<string name="settings_appearance_hint">Vela にのみ適用されます。</string>
<string name="settings_map_style">地図のスタイル</string>
<string name="settings_map_style_hint">OpenFreeMap(デフォルト)はキー不要で詳細です。</string>
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values-nl/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<string name="settings_follow_system">Systeem volgen</string>
<string name="settings_theme_light">Licht</string>
<string name="settings_theme_dark">Donker</string>
<string name="settings_theme_amoled">AMOLED-zwart</string>
<string name="settings_theme_amoled">Zwart</string>
<string name="settings_appearance_hint">Licht/donker geldt alleen voor Vela - het thema van uw telefoon wordt niet gewijzigd.</string>
<string name="settings_map_style">Kaartstijl</string>
<string name="settings_map_style_hint">OpenFreeMap (standaard) is sleutelloos en gedetailleerd.</string>
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values-pl/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<string name="settings_follow_system">Jak w systemie</string>
<string name="settings_theme_light">Jasny</string>
<string name="settings_theme_dark">Ciemny</string>
<string name="settings_theme_amoled">Czern AMOLED</string>
<string name="settings_theme_amoled">Czarny</string>
<string name="settings_appearance_hint">Tryb jasny/ciemny dotyczy tylko Vela - nie zmienia motywu systemowego telefonu.</string>
<string name="settings_map_style">Styl mapy</string>
<string name="settings_map_style_hint">OpenFreeMap (domyslny) jest szczegolowy i nie wymaga klucza.</string>
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values-pt/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<string name="settings_follow_system">Seguir o sistema</string>
<string name="settings_theme_light">Claro</string>
<string name="settings_theme_dark">Escuro</string>
<string name="settings_theme_amoled">Preto AMOLED</string>
<string name="settings_theme_amoled">Preto</string>
<string name="settings_appearance_hint">Claro/Escuro aplica-se apenas ao Vela - não altera o tema do sistema do seu telemóvel.</string>
<string name="settings_map_style">Estilo do mapa</string>
<string name="settings_map_style_hint">OpenFreeMap (padrao) dispensa chave e e detalhado.</string>
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values-ru/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<string name="settings_follow_system">Как в системе</string>
<string name="settings_theme_light">Светлая</string>
<string name="settings_theme_dark">Тёмная</string>
<string name="settings_theme_amoled">AMOLED-чёрный</string>
<string name="settings_theme_amoled">Чёрный</string>
<string name="settings_appearance_hint">Светлая или тёмная тема применяется только к Vela - системная тема телефона не меняется.</string>
<string name="settings_map_style">Стиль карты</string>
<string name="settings_map_style_hint">OpenFreeMap (по умолчанию) детальный и не требует ключа.</string>
Expand Down
Loading
Loading