From 2de57a9e810373f31b62079a62cf9a210a9d419d Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 4 May 2026 16:10:06 -0500 Subject: [PATCH] feat: align theme with Design Standards v1.3, remove contrast setting (#5355) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../kotlin/org/meshtastic/app/MainActivity.kt | 4 +- .../settings/SetContrastLevelUseCase.kt | 27 -- .../meshtastic/core/prefs/ui/UiPrefsImpl.kt | 8 - .../core/repository/AppPreferences.kt | 4 - .../core/testing/FakeAppPreferences.kt | 6 - .../org/meshtastic/core/ui/theme/Color.kt | 277 +++++------------- .../meshtastic/core/ui/theme/ContrastLevel.kt | 45 --- .../meshtastic/core/ui/theme/CustomColors.kt | 118 +++++++- .../org/meshtastic/core/ui/theme/Theme.kt | 185 +----------- .../core/ui/viewmodel/UIViewModel.kt | 1 - .../kotlin/org/meshtastic/desktop/Main.kt | 20 +- .../messaging/component/MessageItem.kt | 69 +---- .../feature/settings/SettingsScreen.kt | 10 - .../settings/component/AppearanceSection.kt | 19 +- .../feature/settings/SettingsViewModel.kt | 6 - .../component/ContrastPickerDialog.kt | 58 ---- .../feature/settings/SettingsViewModelTest.kt | 3 - .../feature/settings/DesktopSettingsScreen.kt | 18 -- 18 files changed, 203 insertions(+), 675 deletions(-) delete mode 100644 core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetContrastLevelUseCase.kt delete mode 100644 core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/ContrastLevel.kt delete mode 100644 feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/ContrastPickerDialog.kt diff --git a/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt b/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt index 726e51bfe..78e8ce559 100644 --- a/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt +++ b/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt @@ -128,8 +128,6 @@ class MainActivity : AppCompatActivity() { setSingletonImageLoaderFactory { get() } val theme by model.theme.collectAsStateWithLifecycle() - val contrastLevelValue by model.contrastLevel.collectAsStateWithLifecycle() - val contrastLevel = org.meshtastic.core.ui.theme.ContrastLevel.fromValue(contrastLevelValue) val dynamic = theme == MODE_DYNAMIC val dark = when (theme) { @@ -147,7 +145,7 @@ class MainActivity : AppCompatActivity() { } AppCompositionLocals { - AppTheme(dynamicColor = dynamic, darkTheme = dark, contrastLevel = contrastLevel) { + AppTheme(dynamicColor = dynamic, darkTheme = dark) { val appIntroCompleted by model.appIntroCompleted.collectAsStateWithLifecycle() // Signal to the system that the initial UI is "fully drawn" diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetContrastLevelUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetContrastLevelUseCase.kt deleted file mode 100644 index 3b7ad0ba9..000000000 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetContrastLevelUseCase.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.domain.usecase.settings - -import org.koin.core.annotation.Single -import org.meshtastic.core.repository.UiPrefs - -@Single -open class SetContrastLevelUseCase constructor(private val uiPrefs: UiPrefs) { - operator fun invoke(value: Int) { - uiPrefs.setContrastLevel(value) - } -} diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt index 8f96ac7bc..ec4dc0b20 100644 --- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt +++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt @@ -62,13 +62,6 @@ class UiPrefsImpl( scope.launch { dataStore.edit { it[KEY_THEME] = value } } } - override val contrastLevel: StateFlow = - dataStore.data.map { it[KEY_CONTRAST_LEVEL] ?: 0 }.stateIn(scope, SharingStarted.Lazily, 0) - - override fun setContrastLevel(value: Int) { - scope.launch { dataStore.edit { it[KEY_CONTRAST_LEVEL] = value } } - } - override val locale: StateFlow = dataStore.data.map { it[KEY_LOCALE] ?: "" }.stateIn(scope, SharingStarted.Eagerly, "") @@ -194,7 +187,6 @@ class UiPrefsImpl( val KEY_APP_INTRO_COMPLETED = booleanPreferencesKey("app_intro_completed") val KEY_THEME = intPreferencesKey("theme") - val KEY_CONTRAST_LEVEL = intPreferencesKey("contrast-level") val KEY_LOCALE = stringPreferencesKey("locale") val KEY_NODE_SORT = intPreferencesKey("node-sort-option") val KEY_INCLUDE_UNKNOWN = booleanPreferencesKey("include-unknown") diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt index c421cb34d..bcbaad63b 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt @@ -80,10 +80,6 @@ interface UiPrefs { fun setTheme(value: Int) - val contrastLevel: StateFlow - - fun setContrastLevel(value: Int) - val locale: StateFlow fun setLocale(languageTag: String) diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeAppPreferences.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeAppPreferences.kt index c55bf8a28..5cb6f5ce1 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeAppPreferences.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeAppPreferences.kt @@ -84,12 +84,6 @@ class FakeUiPrefs : UiPrefs { theme.value = value } - override val contrastLevel = MutableStateFlow(0) - - override fun setContrastLevel(value: Int) { - contrastLevel.value = value - } - override val locale = MutableStateFlow("en") override fun setLocale(languageTag: String) { diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Color.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Color.kt index ed097c768..d74f71942 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Color.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Color.kt @@ -18,218 +18,81 @@ package org.meshtastic.core.ui.theme import androidx.compose.ui.graphics.Color -val primaryLight = Color(0xFF306A42) +// ─── Meshtastic Design Standards v1.3 ─── +// Primary: Green 700 #2D8F52 | Secondary: Neutral 600 #555668 +// Tertiary: Blue 700 #2855A8 | Neutral: #2C2D3C | Neutral Variant: #303245 +// See: standards/meshtastic_design_standards_v1_3.md §8 + +// ─── Light Scheme (§8.2) ─── +val primaryLight = Color(0xFF2D8F52) // Green 700 val onPrimaryLight = Color(0xFFFFFFFF) -val primaryContainerLight = Color(0xFFB3F1BF) -val onPrimaryContainerLight = Color(0xFF00210D) -val secondaryLight = Color(0xFF506353) +val primaryContainerLight = Color(0xFFB5F5CE) // Green 300 +val onPrimaryContainerLight = Color(0xFF002E13) // Green 950 +val secondaryLight = Color(0xFF555668) // Neutral 600 val onSecondaryLight = Color(0xFFFFFFFF) -val secondaryContainerLight = Color(0xFFD2E8D3) -val onSecondaryContainerLight = Color(0xFF0D1F12) -val tertiaryLight = Color(0xFF3A656E) +val secondaryContainerLight = Color(0xFFD5D6E0) // Neutral 200 +val onSecondaryContainerLight = Color(0xFF2C2D3C) // Neutral 800 +val tertiaryLight = Color(0xFF2855A8) // Blue 700 val onTertiaryLight = Color(0xFFFFFFFF) -val tertiaryContainerLight = Color(0xFFBEEAF6) -val onTertiaryContainerLight = Color(0xFF001F25) -val errorLight = Color(0xFFBA1A1A) +val tertiaryContainerLight = Color(0xFFE8EAF6) // Blue 50 +val onTertiaryContainerLight = Color(0xFF001849) // Blue 950 +val errorLight = Color(0xFFBA1A1A) // Error 600 (WCAG-safe on white) val onErrorLight = Color(0xFFFFFFFF) -val errorContainerLight = Color(0xFFFFDAD6) -val onErrorContainerLight = Color(0xFF410002) -val backgroundLight = Color(0xFFF6FBF3) -val onBackgroundLight = Color(0xFF181D18) -val surfaceLight = Color(0xFFF6FBF3) -val onSurfaceLight = Color(0xFF181D18) -val surfaceVariantLight = Color(0xFFDDE5DA) -val onSurfaceVariantLight = Color(0xFF414941) -val outlineLight = Color(0xFF717971) -val outlineVariantLight = Color(0xFFC1C9BF) +val errorContainerLight = Color(0xFFFDEAEA) // Error 100 +val onErrorContainerLight = Color(0xFF410002) // Error 900 +val backgroundLight = Color(0xFFF5F6FA) // Neutral 50 +val onBackgroundLight = Color(0xFF2C2D3C) // Neutral 800 +val surfaceLight = Color(0xFFF5F6FA) // Neutral 50 +val onSurfaceLight = Color(0xFF2C2D3C) // Neutral 800 +val surfaceVariantLight = Color(0xFFDADBE7) // NV 200 +val onSurfaceVariantLight = Color(0xFF5C5E78) // NV 600 +val outlineLight = Color(0xFF767892) // NV 500 +val outlineVariantLight = Color(0xFFBDBFCF) // NV 300 val scrimLight = Color(0xFF000000) -val inverseSurfaceLight = Color(0xFF2D322D) -val inverseOnSurfaceLight = Color(0xFFEEF2EA) -val inversePrimaryLight = Color(0xFF97D5A5) -val surfaceDimLight = Color(0xFFD7DBD4) -val surfaceBrightLight = Color(0xFFF6FBF3) +val inverseSurfaceLight = Color(0xFF3D3E50) // Neutral 700 +val inverseOnSurfaceLight = Color(0xFFECEDF3) // Neutral 100 +val inversePrimaryLight = Color(0xFF67EA94) // Green 500 +val surfaceDimLight = Color(0xFFD5D6E0) // Neutral 200 +val surfaceBrightLight = Color(0xFFF5F6FA) // Neutral 50 val surfaceContainerLowestLight = Color(0xFFFFFFFF) -val surfaceContainerLowLight = Color(0xFFF0F5ED) -val surfaceContainerLight = Color(0xFFEBEFE7) -val surfaceContainerHighLight = Color(0xFFE5EAE2) -val surfaceContainerHighestLight = Color(0xFFDFE4DC) +val surfaceContainerLowLight = Color(0xFFF5F6FA) // Neutral 50 +val surfaceContainerLight = Color(0xFFECEDF3) // Neutral 100 +val surfaceContainerHighLight = Color(0xFFE0E1EB) // Interpolated 100↔200 +val surfaceContainerHighestLight = Color(0xFFD5D6E0) // Neutral 200 -val primaryLightMediumContrast = Color(0xFF0F4D29) -val onPrimaryLightMediumContrast = Color(0xFFFFFFFF) -val primaryContainerLightMediumContrast = Color(0xFF478157) -val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF) -val secondaryLightMediumContrast = Color(0xFF344738) -val onSecondaryLightMediumContrast = Color(0xFFFFFFFF) -val secondaryContainerLightMediumContrast = Color(0xFF657A68) -val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF) -val tertiaryLightMediumContrast = Color(0xFF1C4952) -val onTertiaryLightMediumContrast = Color(0xFFFFFFFF) -val tertiaryContainerLightMediumContrast = Color(0xFF517B85) -val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF) -val errorLightMediumContrast = Color(0xFF8C0009) -val onErrorLightMediumContrast = Color(0xFFFFFFFF) -val errorContainerLightMediumContrast = Color(0xFFDA342E) -val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF) -val backgroundLightMediumContrast = Color(0xFFF6FBF3) -val onBackgroundLightMediumContrast = Color(0xFF181D18) -val surfaceLightMediumContrast = Color(0xFFF6FBF3) -val onSurfaceLightMediumContrast = Color(0xFF181D18) -val surfaceVariantLightMediumContrast = Color(0xFFDDE5DA) -val onSurfaceVariantLightMediumContrast = Color(0xFF3D453D) -val outlineLightMediumContrast = Color(0xFF596159) -val outlineVariantLightMediumContrast = Color(0xFF757D74) -val scrimLightMediumContrast = Color(0xFF000000) -val inverseSurfaceLightMediumContrast = Color(0xFF2D322D) -val inverseOnSurfaceLightMediumContrast = Color(0xFFEEF2EA) -val inversePrimaryLightMediumContrast = Color(0xFF97D5A5) -val surfaceDimLightMediumContrast = Color(0xFFD7DBD4) -val surfaceBrightLightMediumContrast = Color(0xFFF6FBF3) -val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF) -val surfaceContainerLowLightMediumContrast = Color(0xFFF0F5ED) -val surfaceContainerLightMediumContrast = Color(0xFFEBEFE7) -val surfaceContainerHighLightMediumContrast = Color(0xFFE5EAE2) -val surfaceContainerHighestLightMediumContrast = Color(0xFFDFE4DC) - -val primaryLightHighContrast = Color(0xFF002911) -val onPrimaryLightHighContrast = Color(0xFFFFFFFF) -val primaryContainerLightHighContrast = Color(0xFF0F4D29) -val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF) -val secondaryLightHighContrast = Color(0xFF142619) -val onSecondaryLightHighContrast = Color(0xFFFFFFFF) -val secondaryContainerLightHighContrast = Color(0xFF344738) -val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF) -val tertiaryLightHighContrast = Color(0xFF00262E) -val onTertiaryLightHighContrast = Color(0xFFFFFFFF) -val tertiaryContainerLightHighContrast = Color(0xFF1C4952) -val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF) -val errorLightHighContrast = Color(0xFF4E0002) -val onErrorLightHighContrast = Color(0xFFFFFFFF) -val errorContainerLightHighContrast = Color(0xFF8C0009) -val onErrorContainerLightHighContrast = Color(0xFFFFFFFF) -val backgroundLightHighContrast = Color(0xFFF6FBF3) -val onBackgroundLightHighContrast = Color(0xFF181D18) -val surfaceLightHighContrast = Color(0xFFF6FBF3) -val onSurfaceLightHighContrast = Color(0xFF000000) -val surfaceVariantLightHighContrast = Color(0xFFDDE5DA) -val onSurfaceVariantLightHighContrast = Color(0xFF1E261F) -val outlineLightHighContrast = Color(0xFF3D453D) -val outlineVariantLightHighContrast = Color(0xFF3D453D) -val scrimLightHighContrast = Color(0xFF000000) -val inverseSurfaceLightHighContrast = Color(0xFF2D322D) -val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF) -val inversePrimaryLightHighContrast = Color(0xFFBCFBC8) -val surfaceDimLightHighContrast = Color(0xFFD7DBD4) -val surfaceBrightLightHighContrast = Color(0xFFF6FBF3) -val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF) -val surfaceContainerLowLightHighContrast = Color(0xFFF0F5ED) -val surfaceContainerLightHighContrast = Color(0xFFEBEFE7) -val surfaceContainerHighLightHighContrast = Color(0xFFE5EAE2) -val surfaceContainerHighestLightHighContrast = Color(0xFFDFE4DC) - -val primaryDark = Color(0xFF97D5A5) -val onPrimaryDark = Color(0xFF00391A) -val primaryContainerDark = Color(0xFF15512C) -val onPrimaryContainerDark = Color(0xFFB3F1BF) -val secondaryDark = Color(0xFFB6CCB8) -val onSecondaryDark = Color(0xFF223526) -val secondaryContainerDark = Color(0xFF384B3C) -val onSecondaryContainerDark = Color(0xFFD2E8D3) -val tertiaryDark = Color(0xFFA2CED9) -val onTertiaryDark = Color(0xFF01363F) -val tertiaryContainerDark = Color(0xFF204D56) -val onTertiaryContainerDark = Color(0xFFBEEAF6) -val errorDark = Color(0xFFFFB4AB) -val onErrorDark = Color(0xFF690005) -val errorContainerDark = Color(0xFF93000A) -val onErrorContainerDark = Color(0xFFFFDAD6) -val backgroundDark = Color(0xFF101510) -val onBackgroundDark = Color(0xFFDFE4DC) -val surfaceDark = Color(0xFF101510) -val onSurfaceDark = Color(0xFFDFE4DC) -val surfaceVariantDark = Color(0xFF414941) -val onSurfaceVariantDark = Color(0xFFC1C9BF) -val outlineDark = Color(0xFF8B938A) -val outlineVariantDark = Color(0xFF414941) +// ─── Dark Scheme (§8.3) ─── +val primaryDark = Color(0xFF67EA94) // Green 500 +val onPrimaryDark = Color(0xFF0F1017) // Neutral 950 +val primaryContainerDark = Color(0xFF2D8F52) // Green 700 +val onPrimaryContainerDark = Color(0xFFB5F5CE) // Green 300 +val secondaryDark = Color(0xFFB8BAC8) // Neutral 300 +val onSecondaryDark = Color(0xFF1A1B26) // Neutral 900 +val secondaryContainerDark = Color(0xFF3D3E50) // Neutral 700 +val onSecondaryContainerDark = Color(0xFFD5D6E0) // Neutral 200 +val tertiaryDark = Color(0xFFB0BFF0) // Blue 300 +val onTertiaryDark = Color(0xFF001849) // Blue 950 +val tertiaryContainerDark = Color(0xFF2855A8) // Blue 700 +val onTertiaryContainerDark = Color(0xFFE8EAF6) // Blue 50 +val errorDark = Color(0xFFFFB4AB) // Error 300 +val onErrorDark = Color(0xFF690005) // Error 800 +val errorContainerDark = Color(0xFF93000A) // Error 700 +val onErrorContainerDark = Color(0xFFFDEAEA) // Error 100 +val backgroundDark = Color(0xFF1A1B26) // Neutral 900 +val onBackgroundDark = Color(0xFFECEDF3) // Neutral 100 +val surfaceDark = Color(0xFF1A1B26) // Neutral 900 +val onSurfaceDark = Color(0xFFECEDF3) // Neutral 100 +val surfaceVariantDark = Color(0xFF444660) // NV 700 +val onSurfaceVariantDark = Color(0xFFBDBFCF) // NV 300 +val outlineDark = Color(0xFF767892) // NV 500 +val outlineVariantDark = Color(0xFF444660) // NV 700 val scrimDark = Color(0xFF000000) -val inverseSurfaceDark = Color(0xFFDFE4DC) -val inverseOnSurfaceDark = Color(0xFF2D322D) -val inversePrimaryDark = Color(0xFF306A42) -val surfaceDimDark = Color(0xFF101510) -val surfaceBrightDark = Color(0xFF353A35) -val surfaceContainerLowestDark = Color(0xFF0A0F0B) -val surfaceContainerLowDark = Color(0xFF181D18) -val surfaceContainerDark = Color(0xFF1C211C) -val surfaceContainerHighDark = Color(0xFF262B26) -val surfaceContainerHighestDark = Color(0xFF313631) - -val primaryDarkMediumContrast = Color(0xFF9BD9A9) -val onPrimaryDarkMediumContrast = Color(0xFF001B09) -val primaryContainerDarkMediumContrast = Color(0xFF639D72) -val onPrimaryContainerDarkMediumContrast = Color(0xFF000000) -val secondaryDarkMediumContrast = Color(0xFFBBD0BC) -val onSecondaryDarkMediumContrast = Color(0xFF081A0D) -val secondaryContainerDarkMediumContrast = Color(0xFF819683) -val onSecondaryContainerDarkMediumContrast = Color(0xFF000000) -val tertiaryDarkMediumContrast = Color(0xFFA6D2DD) -val onTertiaryDarkMediumContrast = Color(0xFF00191F) -val tertiaryContainerDarkMediumContrast = Color(0xFF6D97A2) -val onTertiaryContainerDarkMediumContrast = Color(0xFF000000) -val errorDarkMediumContrast = Color(0xFFFFBAB1) -val onErrorDarkMediumContrast = Color(0xFF370001) -val errorContainerDarkMediumContrast = Color(0xFFFF5449) -val onErrorContainerDarkMediumContrast = Color(0xFF000000) -val backgroundDarkMediumContrast = Color(0xFF101510) -val onBackgroundDarkMediumContrast = Color(0xFFDFE4DC) -val surfaceDarkMediumContrast = Color(0xFF101510) -val onSurfaceDarkMediumContrast = Color(0xFFF8FCF4) -val surfaceVariantDarkMediumContrast = Color(0xFF414941) -val onSurfaceVariantDarkMediumContrast = Color(0xFFC5CDC3) -val outlineDarkMediumContrast = Color(0xFF9DA59C) -val outlineVariantDarkMediumContrast = Color(0xFF7D857D) -val scrimDarkMediumContrast = Color(0xFF000000) -val inverseSurfaceDarkMediumContrast = Color(0xFFDFE4DC) -val inverseOnSurfaceDarkMediumContrast = Color(0xFF262B26) -val inversePrimaryDarkMediumContrast = Color(0xFF16522E) -val surfaceDimDarkMediumContrast = Color(0xFF101510) -val surfaceBrightDarkMediumContrast = Color(0xFF353A35) -val surfaceContainerLowestDarkMediumContrast = Color(0xFF0A0F0B) -val surfaceContainerLowDarkMediumContrast = Color(0xFF181D18) -val surfaceContainerDarkMediumContrast = Color(0xFF1C211C) -val surfaceContainerHighDarkMediumContrast = Color(0xFF262B26) -val surfaceContainerHighestDarkMediumContrast = Color(0xFF313631) - -val primaryDarkHighContrast = Color(0xFFEFFFEE) -val onPrimaryDarkHighContrast = Color(0xFF000000) -val primaryContainerDarkHighContrast = Color(0xFF9BD9A9) -val onPrimaryContainerDarkHighContrast = Color(0xFF000000) -val secondaryDarkHighContrast = Color(0xFFEFFFEE) -val onSecondaryDarkHighContrast = Color(0xFF000000) -val secondaryContainerDarkHighContrast = Color(0xFFBBD0BC) -val onSecondaryContainerDarkHighContrast = Color(0xFF000000) -val tertiaryDarkHighContrast = Color(0xFFF3FCFF) -val onTertiaryDarkHighContrast = Color(0xFF000000) -val tertiaryContainerDarkHighContrast = Color(0xFFA6D2DD) -val onTertiaryContainerDarkHighContrast = Color(0xFF000000) -val errorDarkHighContrast = Color(0xFFFFF9F9) -val onErrorDarkHighContrast = Color(0xFF000000) -val errorContainerDarkHighContrast = Color(0xFFFFBAB1) -val onErrorContainerDarkHighContrast = Color(0xFF000000) -val backgroundDarkHighContrast = Color(0xFF101510) -val onBackgroundDarkHighContrast = Color(0xFFDFE4DC) -val surfaceDarkHighContrast = Color(0xFF101510) -val onSurfaceDarkHighContrast = Color(0xFFFFFFFF) -val surfaceVariantDarkHighContrast = Color(0xFF414941) -val onSurfaceVariantDarkHighContrast = Color(0xFFF5FDF2) -val outlineDarkHighContrast = Color(0xFFC5CDC3) -val outlineVariantDarkHighContrast = Color(0xFFC5CDC3) -val scrimDarkHighContrast = Color(0xFF000000) -val inverseSurfaceDarkHighContrast = Color(0xFFDFE4DC) -val inverseOnSurfaceDarkHighContrast = Color(0xFF000000) -val inversePrimaryDarkHighContrast = Color(0xFF003216) -val surfaceDimDarkHighContrast = Color(0xFF101510) -val surfaceBrightDarkHighContrast = Color(0xFF353A35) -val surfaceContainerLowestDarkHighContrast = Color(0xFF0A0F0B) -val surfaceContainerLowDarkHighContrast = Color(0xFF181D18) -val surfaceContainerDarkHighContrast = Color(0xFF1C211C) -val surfaceContainerHighDarkHighContrast = Color(0xFF262B26) -val surfaceContainerHighestDarkHighContrast = Color(0xFF313631) +val inverseSurfaceDark = Color(0xFFECEDF3) // Neutral 100 +val inverseOnSurfaceDark = Color(0xFF2C2D3C) // Neutral 800 +val inversePrimaryDark = Color(0xFF2D8F52) // Green 700 +val surfaceDimDark = Color(0xFF0F1017) // Neutral 950 +val surfaceBrightDark = Color(0xFF3D3E50) // Neutral 700 +val surfaceContainerLowestDark = Color(0xFF0F1017) // Neutral 950 +val surfaceContainerLowDark = Color(0xFF1A1B26) // Neutral 900 +val surfaceContainerDark = Color(0xFF242533) // Interpolated 900↔800 +val surfaceContainerHighDark = Color(0xFF2C2D3C) // Neutral 800 +val surfaceContainerHighestDark = Color(0xFF3D3E50) // Neutral 700 diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/ContrastLevel.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/ContrastLevel.kt deleted file mode 100644 index 9abb6719a..000000000 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/ContrastLevel.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.ui.theme - -import androidx.compose.runtime.staticCompositionLocalOf - -/** - * Application-wide contrast level for accessibility. - * - * [STANDARD] keeps the default Material 3 color scheme. [MEDIUM] uses Material 3 medium-contrast color tokens and - * increases message bubble opacity. [HIGH] uses Material 3 high-contrast color tokens, forces `onSurface` text in - * message bubbles, and replaces translucent node-color fills with opaque theme surfaces plus accent borders. - */ -enum class ContrastLevel(val value: Int) { - STANDARD(0), - MEDIUM(1), - HIGH(2), - ; - - companion object { - fun fromValue(value: Int): ContrastLevel = entries.firstOrNull { it.value == value } ?: MEDIUM - } -} - -/** - * Composition local providing the current [ContrastLevel]. - * - * Read by components that need to adapt their rendering for accessibility (e.g. message bubbles, signal indicators). - */ -@Suppress("CompositionLocalAllowlist") -val LocalContrastLevel = staticCompositionLocalOf { ContrastLevel.MEDIUM } diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/CustomColors.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/CustomColors.kt index 7f5cd7d3d..9304d5e2b 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/CustomColors.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/CustomColors.kt @@ -21,10 +21,98 @@ import androidx.compose.material3.ColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color -val MeshtasticGreen = Color(0xFF67EA94) -val MeshtasticAlt = Color(0xFF2C2D3C) -val HyperlinkBlue = Color(0xFF43C3B0) -val AnnotationColor = Color(0xFF039BE5) +// ─── Brand Colors (Design Standards v1.3) ─── +val MeshtasticGreen = Color(0xFF67EA94) // Green 500 — Brand Accent +val MeshtasticAlt = Color(0xFF2C2D3C) // Neutral 800 — Brand Primary + +// ─── Neutral Scale ─── +object NeutralPalette { + val N950 = Color(0xFF0F1017) + val N900 = Color(0xFF1A1B26) + val N800 = Color(0xFF2C2D3C) + val N700 = Color(0xFF3D3E50) + val N600 = Color(0xFF555668) + val N500 = Color(0xFF6E7082) + val N400 = Color(0xFF9496A6) + val N300 = Color(0xFFB8BAC8) + val N200 = Color(0xFFD5D6E0) + val N100 = Color(0xFFECEDF3) + val N50 = Color(0xFFF5F6FA) +} + +// ─── Neutral Variant Scale (§7.3) ─── +object NeutralVariantPalette { + val NV900 = Color(0xFF1D1E2B) + val NV800 = Color(0xFF303245) + val NV700 = Color(0xFF444660) + val NV600 = Color(0xFF5C5E78) + val NV500 = Color(0xFF767892) + val NV400 = Color(0xFF9698B0) + val NV300 = Color(0xFFBDBFCF) + val NV200 = Color(0xFFDADBE7) + val NV100 = Color(0xFFEDEEF6) + val NV50 = Color(0xFFF6F7FC) +} + +// ─── Green Scale (§7.4) ─── +object GreenPalette { + val G950 = Color(0xFF002E13) + val G900 = Color(0xFF003D1A) + val G800 = Color(0xFF005C2E) + val G700 = Color(0xFF2D8F52) + val G600 = Color(0xFF3FB86D) + val G500 = Color(0xFF67EA94) + val G400 = Color(0xFF8FF0B2) + val G300 = Color(0xFFB5F5CE) + val G200 = Color(0xFFCCFADD) + val G100 = Color(0xFFE5FCEE) + val G50 = Color(0xFFF0FEF5) +} + +// ─── Accent Blue Scale (§7.5) ─── +object BluePalette { + val B950 = Color(0xFF001849) + val B900 = Color(0xFF002366) + val B800 = Color(0xFF1A3F8C) + val B700 = Color(0xFF2855A8) + val B600 = Color(0xFF5C6BC0) + val B500 = Color(0xFF7B8AD0) + val B400 = Color(0xFF9BA8E0) + val B300 = Color(0xFFB0BFF0) + val B200 = Color(0xFFD0D8F5) + val B100 = Color(0xFFE0E3F8) + val B50 = Color(0xFFE8EAF6) +} + +// ─── Error Scale (§7.6) ─── +object ErrorPalette { + val E900 = Color(0xFF410002) + val E800 = Color(0xFF690005) + val E700 = Color(0xFF93000A) + val E600 = Color(0xFFBA1A1A) + val E500 = Color(0xFFE05252) + val E400 = Color(0xFFFF897D) + val E300 = Color(0xFFFFB4AB) + val E200 = Color(0xFFFFDAD6) + val E100 = Color(0xFFFDEAEA) +} + +// ─── Semantic Colors (§7.7) ─── +object SemanticColors { + val Accent = Color(0xFF2855A8) // Blue 700 + val AccentLight = Color(0xFFE0E3F8) // Blue 100 + val Info = Color(0xFF5C6BC0) // Blue 600 + val InfoLight = Color(0xFFE8EAF6) // Blue 50 + val Warning = Color(0xFFE8A33E) + val WarningLight = Color(0xFFFFF3E0) + val Error = Color(0xFFE05252) // Error 500 — non-text indicators only + val ErrorLight = Color(0xFFFDEAEA) // Error 100 + val Success = Color(0xFF3FB86D) // Green 600 + val SuccessLight = Color(0xFFE5FCEE) // Green 100 +} + +val HyperlinkBlue = Color(0xFF5C6BC0) // Blue 600 (Info) +val AnnotationColor = Color(0xFF2855A8) // Blue 700 (Accent) object TracerouteColors { // High-contrast pair that stays legible on light/dark tiles and for most color-blind users. @@ -69,20 +157,20 @@ object GraphColors { object StatusColors { val ColorScheme.StatusGreen: Color @Composable - get() = // If it might change based on theme + get() = if (isSystemInDarkTheme()) { - Color(0xFF28A03B) // Example dark green + Color(0xFF3FB86D) // Green 600 } else { - Color(0xFF30C047) + Color(0xFF3FB86D) // Green 600 (Success) } val ColorScheme.StatusYellow: Color @Composable get() = if (isSystemInDarkTheme()) { - Color(0xFFFFC107) + Color(0xFFE8A33E) // Warning } else { - Color(0xFFFFD54F) + Color(0xFFE8A33E) // Warning } val ColorScheme.StatusOrange: Color @@ -96,20 +184,20 @@ object StatusColors { val ColorScheme.StatusRed: Color @Composable - get() = // If it might change based on theme + get() = if (isSystemInDarkTheme()) { - Color(0xFFB00020) + Color(0xFFE05252) // Error } else { - Color(0xFFF44336) + Color(0xFFE05252) // Error } val ColorScheme.StatusBlue: Color @Composable - get() = // If it might change based on theme + get() = if (isSystemInDarkTheme()) { - Color(0xFF2196F3) + Color(0xFF5C6BC0) // Info } else { - Color(0xFF42A5F5) + Color(0xFF5C6BC0) // Info } } diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Theme.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Theme.kt index 633fd42aa..5b3cffcfd 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Theme.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Theme.kt @@ -25,7 +25,6 @@ import androidx.compose.material3.MotionScheme.Companion.expressive import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Color @@ -107,162 +106,6 @@ private val darkScheme = surfaceContainerHighest = surfaceContainerHighestDark, ) -private val mediumContrastLightColorScheme = - lightColorScheme( - primary = primaryLightMediumContrast, - onPrimary = onPrimaryLightMediumContrast, - primaryContainer = primaryContainerLightMediumContrast, - onPrimaryContainer = onPrimaryContainerLightMediumContrast, - secondary = secondaryLightMediumContrast, - onSecondary = onSecondaryLightMediumContrast, - secondaryContainer = secondaryContainerLightMediumContrast, - onSecondaryContainer = onSecondaryContainerLightMediumContrast, - tertiary = tertiaryLightMediumContrast, - onTertiary = onTertiaryLightMediumContrast, - tertiaryContainer = tertiaryContainerLightMediumContrast, - onTertiaryContainer = onTertiaryContainerLightMediumContrast, - error = errorLightMediumContrast, - onError = onErrorLightMediumContrast, - errorContainer = errorContainerLightMediumContrast, - onErrorContainer = onErrorContainerLightMediumContrast, - background = backgroundLightMediumContrast, - onBackground = onBackgroundLightMediumContrast, - surface = surfaceLightMediumContrast, - onSurface = onSurfaceLightMediumContrast, - surfaceVariant = surfaceVariantLightMediumContrast, - onSurfaceVariant = onSurfaceVariantLightMediumContrast, - outline = outlineLightMediumContrast, - outlineVariant = outlineVariantLightMediumContrast, - scrim = scrimLightMediumContrast, - inverseSurface = inverseSurfaceLightMediumContrast, - inverseOnSurface = inverseOnSurfaceLightMediumContrast, - inversePrimary = inversePrimaryLightMediumContrast, - surfaceDim = surfaceDimLightMediumContrast, - surfaceBright = surfaceBrightLightMediumContrast, - surfaceContainerLowest = surfaceContainerLowestLightMediumContrast, - surfaceContainerLow = surfaceContainerLowLightMediumContrast, - surfaceContainer = surfaceContainerLightMediumContrast, - surfaceContainerHigh = surfaceContainerHighLightMediumContrast, - surfaceContainerHighest = surfaceContainerHighestLightMediumContrast, - ) - -private val highContrastLightColorScheme = - lightColorScheme( - primary = primaryLightHighContrast, - onPrimary = onPrimaryLightHighContrast, - primaryContainer = primaryContainerLightHighContrast, - onPrimaryContainer = onPrimaryContainerLightHighContrast, - secondary = secondaryLightHighContrast, - onSecondary = onSecondaryLightHighContrast, - secondaryContainer = secondaryContainerLightHighContrast, - onSecondaryContainer = onSecondaryContainerLightHighContrast, - tertiary = tertiaryLightHighContrast, - onTertiary = onTertiaryLightHighContrast, - tertiaryContainer = tertiaryContainerLightHighContrast, - onTertiaryContainer = onTertiaryContainerLightHighContrast, - error = errorLightHighContrast, - onError = onErrorLightHighContrast, - errorContainer = errorContainerLightHighContrast, - onErrorContainer = onErrorContainerLightHighContrast, - background = backgroundLightHighContrast, - onBackground = onBackgroundLightHighContrast, - surface = surfaceLightHighContrast, - onSurface = onSurfaceLightHighContrast, - surfaceVariant = surfaceVariantLightHighContrast, - onSurfaceVariant = onSurfaceVariantLightHighContrast, - outline = outlineLightHighContrast, - outlineVariant = outlineVariantLightHighContrast, - scrim = scrimLightHighContrast, - inverseSurface = inverseSurfaceLightHighContrast, - inverseOnSurface = inverseOnSurfaceLightHighContrast, - inversePrimary = inversePrimaryLightHighContrast, - surfaceDim = surfaceDimLightHighContrast, - surfaceBright = surfaceBrightLightHighContrast, - surfaceContainerLowest = surfaceContainerLowestLightHighContrast, - surfaceContainerLow = surfaceContainerLowLightHighContrast, - surfaceContainer = surfaceContainerLightHighContrast, - surfaceContainerHigh = surfaceContainerHighLightHighContrast, - surfaceContainerHighest = surfaceContainerHighestLightHighContrast, - ) - -private val mediumContrastDarkColorScheme = - darkColorScheme( - primary = primaryDarkMediumContrast, - onPrimary = onPrimaryDarkMediumContrast, - primaryContainer = primaryContainerDarkMediumContrast, - onPrimaryContainer = onPrimaryContainerDarkMediumContrast, - secondary = secondaryDarkMediumContrast, - onSecondary = onSecondaryDarkMediumContrast, - secondaryContainer = secondaryContainerDarkMediumContrast, - onSecondaryContainer = onSecondaryContainerDarkMediumContrast, - tertiary = tertiaryDarkMediumContrast, - onTertiary = onTertiaryDarkMediumContrast, - tertiaryContainer = tertiaryContainerDarkMediumContrast, - onTertiaryContainer = onTertiaryContainerDarkMediumContrast, - error = errorDarkMediumContrast, - onError = onErrorDarkMediumContrast, - errorContainer = errorContainerDarkMediumContrast, - onErrorContainer = onErrorContainerDarkMediumContrast, - background = backgroundDarkMediumContrast, - onBackground = onBackgroundDarkMediumContrast, - surface = surfaceDarkMediumContrast, - onSurface = onSurfaceDarkMediumContrast, - surfaceVariant = surfaceVariantDarkMediumContrast, - onSurfaceVariant = onSurfaceVariantDarkMediumContrast, - outline = outlineDarkMediumContrast, - outlineVariant = outlineVariantDarkMediumContrast, - scrim = scrimDarkMediumContrast, - inverseSurface = inverseSurfaceDarkMediumContrast, - inverseOnSurface = inverseOnSurfaceDarkMediumContrast, - inversePrimary = inversePrimaryDarkMediumContrast, - surfaceDim = surfaceDimDarkMediumContrast, - surfaceBright = surfaceBrightDarkMediumContrast, - surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast, - surfaceContainerLow = surfaceContainerLowDarkMediumContrast, - surfaceContainer = surfaceContainerDarkMediumContrast, - surfaceContainerHigh = surfaceContainerHighDarkMediumContrast, - surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast, - ) - -private val highContrastDarkColorScheme = - darkColorScheme( - primary = primaryDarkHighContrast, - onPrimary = onPrimaryDarkHighContrast, - primaryContainer = primaryContainerDarkHighContrast, - onPrimaryContainer = onPrimaryContainerDarkHighContrast, - secondary = secondaryDarkHighContrast, - onSecondary = onSecondaryDarkHighContrast, - secondaryContainer = secondaryContainerDarkHighContrast, - onSecondaryContainer = onSecondaryContainerDarkHighContrast, - tertiary = tertiaryDarkHighContrast, - onTertiary = onTertiaryDarkHighContrast, - tertiaryContainer = tertiaryContainerDarkHighContrast, - onTertiaryContainer = onTertiaryContainerDarkHighContrast, - error = errorDarkHighContrast, - onError = onErrorDarkHighContrast, - errorContainer = errorContainerDarkHighContrast, - onErrorContainer = onErrorContainerDarkHighContrast, - background = backgroundDarkHighContrast, - onBackground = onBackgroundDarkHighContrast, - surface = surfaceDarkHighContrast, - onSurface = onSurfaceDarkHighContrast, - surfaceVariant = surfaceVariantDarkHighContrast, - onSurfaceVariant = onSurfaceVariantDarkHighContrast, - outline = outlineDarkHighContrast, - outlineVariant = outlineVariantDarkHighContrast, - scrim = scrimDarkHighContrast, - inverseSurface = inverseSurfaceDarkHighContrast, - inverseOnSurface = inverseOnSurfaceDarkHighContrast, - inversePrimary = inversePrimaryDarkHighContrast, - surfaceDim = surfaceDimDarkHighContrast, - surfaceBright = surfaceBrightDarkHighContrast, - surfaceContainerLowest = surfaceContainerLowestDarkHighContrast, - surfaceContainerLow = surfaceContainerLowDarkHighContrast, - surfaceContainer = surfaceContainerDarkHighContrast, - surfaceContainerHigh = surfaceContainerHighDarkHighContrast, - surfaceContainerHighest = surfaceContainerHighestDarkHighContrast, - ) - @Immutable data class ColorFamily(val color: Color, val onColor: Color, val colorContainer: Color, val onColorContainer: Color) @@ -273,33 +116,23 @@ val unspecified_scheme = ColorFamily(Color.Unspecified, Color.Unspecified, Color fun AppTheme( darkTheme: Boolean = isSystemInDarkTheme(), dynamicColor: Boolean = true, - contrastLevel: ContrastLevel = ContrastLevel.STANDARD, content: @Composable() () -> Unit, ) { - val dynamicScheme = - if (dynamicColor && contrastLevel == ContrastLevel.STANDARD) { + val colorScheme = + if (dynamicColor) { dynamicColorScheme(darkTheme) } else { null - } - val colorScheme = - dynamicScheme - ?: when (contrastLevel) { - ContrastLevel.MEDIUM -> if (darkTheme) mediumContrastDarkColorScheme else mediumContrastLightColorScheme - ContrastLevel.HIGH -> if (darkTheme) highContrastDarkColorScheme else highContrastLightColorScheme - else -> if (darkTheme) darkScheme else lightScheme - } + } ?: if (darkTheme) darkScheme else lightScheme - CompositionLocalProvider(LocalContrastLevel provides contrastLevel) { - MaterialExpressiveTheme( - colorScheme = colorScheme, - typography = AppTypography, - motionScheme = expressive(), - content = content, - ) - } + MaterialExpressiveTheme( + colorScheme = colorScheme, + typography = AppTypography, + motionScheme = expressive(), + content = content, + ) } const val MODE_DYNAMIC = 6969420 diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt index edb647903..e0d895226 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt @@ -120,7 +120,6 @@ class UIViewModel( } val theme: StateFlow = uiPrefs.theme - val contrastLevel: StateFlow = uiPrefs.contrastLevel val firmwareEdition = meshLogRepository.getMyNodeInfo().map { nodeInfo -> nodeInfo?.firmware_edition } diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt index 0aeac718c..1d0e0190e 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt @@ -170,6 +170,7 @@ private fun MeshServiceLifecycle() { // ----- Theme, locale, and application shell ----- /** Resolves the user's theme/locale preferences and renders the full application UI. */ +@Suppress("ViewModelForwarding") @Composable @OptIn(ExperimentalCoilApi::class) private fun ApplicationScope.ThemeAndLocaleProvider(uiViewModel: UIViewModel) { @@ -177,8 +178,6 @@ private fun ApplicationScope.ThemeAndLocaleProvider(uiViewModel: UIViewModel) { val uiPrefs = koinInject() val themePref by uiPrefs.theme.collectAsState(initial = -1) val localePref by uiPrefs.locale.collectAsState(initial = "") - val contrastLevelValue by uiPrefs.contrastLevel.collectAsState(initial = 0) - val contrastLevel = org.meshtastic.core.ui.theme.ContrastLevel.fromValue(contrastLevelValue) Locale.setDefault(localePref.takeIf { it.isNotEmpty() }?.let(Locale::forLanguageTag) ?: systemLocale) val isDarkTheme = @@ -188,19 +187,16 @@ private fun ApplicationScope.ThemeAndLocaleProvider(uiViewModel: UIViewModel) { else -> isSystemInDarkTheme() } - MeshtasticDesktopApp(uiViewModel, isDarkTheme, contrastLevel) + MeshtasticDesktopApp(uiViewModel, isDarkTheme) } // ----- Application chrome (tray, window, navigation) ----- /** Composes the system tray, window, and Coil image loader. */ +@Suppress("ViewModelForwarding") @Composable @OptIn(ExperimentalCoilApi::class) -private fun ApplicationScope.MeshtasticDesktopApp( - uiViewModel: UIViewModel, - isDarkTheme: Boolean, - contrastLevel: org.meshtastic.core.ui.theme.ContrastLevel, -) { +private fun ApplicationScope.MeshtasticDesktopApp(uiViewModel: UIViewModel, isDarkTheme: Boolean) { var isAppVisible by remember { mutableStateOf(true) } var isWindowReady by remember { mutableStateOf(false) } val trayState = rememberTrayState() @@ -232,7 +228,7 @@ private fun ApplicationScope.MeshtasticDesktopApp( ) if (isWindowReady && isAppVisible) { - MeshtasticWindow(uiViewModel, isDarkTheme, contrastLevel, appIcon, windowState) { isAppVisible = false } + MeshtasticWindow(uiViewModel, isDarkTheme, appIcon, windowState) { isAppVisible = false } } } @@ -275,12 +271,12 @@ private fun WindowBoundsManager( // ----- Main window with keyboard shortcuts and Coil ----- /** Renders the main application window with keyboard shortcuts, Coil image loading, and the Compose UI tree. */ +@Suppress("ViewModelForwarding") @Composable @OptIn(ExperimentalCoilApi::class) private fun ApplicationScope.MeshtasticWindow( uiViewModel: UIViewModel, isDarkTheme: Boolean, - contrastLevel: org.meshtastic.core.ui.theme.ContrastLevel, appIcon: Painter, windowState: WindowState, onCloseRequest: () -> Unit, @@ -306,9 +302,7 @@ private fun ApplicationScope.MeshtasticWindow( CoilImageLoaderSetup() CompositionLocalProvider(LocalEventBranding provides eventEdition) { - AppTheme(darkTheme = isDarkTheme, contrastLevel = contrastLevel) { - DesktopMainScreen(uiViewModel, multiBackstack) - } + AppTheme(darkTheme = isDarkTheme) { DesktopMainScreen(uiViewModel, multiBackstack) } } } } diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt index 840479bad..ccda3a56b 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt @@ -16,6 +16,7 @@ */ package org.meshtastic.feature.messaging.component +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable @@ -73,8 +74,6 @@ import org.meshtastic.core.ui.emoji.EmojiPickerDialog import org.meshtastic.core.ui.icon.FormatQuote import org.meshtastic.core.ui.icon.HopCount import org.meshtastic.core.ui.icon.MeshtasticIcons -import org.meshtastic.core.ui.theme.ContrastLevel -import org.meshtastic.core.ui.theme.LocalContrastLevel import org.meshtastic.core.ui.theme.MessageItemColors import org.meshtastic.core.ui.util.createClipEntry @@ -178,7 +177,6 @@ fun MessageItem( } val containsBel = message.text.contains('\u0007') - val contrastLevel = LocalContrastLevel.current val nodeColor = Color(if (message.fromLocal) ourNode.colors.second else node.colors.second) val alpha = @@ -190,33 +188,9 @@ fun MessageItem( NORMAL_ALPHA } - val containerColor = - when (contrastLevel) { - ContrastLevel.HIGH -> - when { - message.filtered -> MaterialTheme.colorScheme.surfaceContainerLow - inSelectionMode && selected -> MaterialTheme.colorScheme.surfaceContainerHighest - inSelectionMode && !selected -> MaterialTheme.colorScheme.surfaceContainerLow - else -> MaterialTheme.colorScheme.surfaceContainerHigh - } - - ContrastLevel.MEDIUM -> nodeColor.copy(alpha = (alpha + 0.2f).coerceAtMost(1f)) - - ContrastLevel.STANDARD -> nodeColor.copy(alpha = alpha) - } - val contentColor = - when (contrastLevel) { - ContrastLevel.HIGH, - ContrastLevel.MEDIUM, - -> MaterialTheme.colorScheme.onSurface - - ContrastLevel.STANDARD -> Color(if (message.fromLocal) ourNode.colors.first else node.colors.first) - } - val metadataStyle = - when (contrastLevel) { - ContrastLevel.HIGH -> MaterialTheme.typography.bodySmall - else -> MaterialTheme.typography.labelSmall - } + val containerColor = nodeColor.copy(alpha = alpha) + val contentColor = MaterialTheme.colorScheme.onSurface + val metadataStyle = MaterialTheme.typography.labelSmall val messageShape = getMessageBubbleShape( cornerRadius = 8.dp, @@ -230,14 +204,7 @@ fun MessageItem( if (containsBel) { Modifier.border(2.dp, color = MessageItemColors.Red, shape = messageShape) } else { - when (contrastLevel) { - ContrastLevel.HIGH -> Modifier.border(2.dp, color = nodeColor, shape = messageShape) - - ContrastLevel.MEDIUM -> - Modifier.border(1.dp, color = nodeColor.copy(alpha = 0.6f), shape = messageShape) - - ContrastLevel.STANDARD -> Modifier - } + Modifier }, ) val senderName = if (message.fromLocal) ourNode.user.long_name else node.user.long_name @@ -282,6 +249,7 @@ fun MessageItem( color = containerColor, contentColor = contentColor, shape = messageShape, + border = BorderStroke(0.5.dp, nodeColor), ) { Column(modifier = Modifier.width(IntrinsicSize.Max)) { OriginalMessageSnippet( @@ -292,7 +260,7 @@ fun MessageItem( ) Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)) { - AutoLinkText(text = message.text, style = MaterialTheme.typography.bodyMedium, color = contentColor) + AutoLinkText(text = message.text, style = MaterialTheme.typography.bodyLarge, color = contentColor) Row(modifier = Modifier, verticalAlignment = Alignment.CenterVertically) { if (!message.fromLocal) { @@ -337,12 +305,7 @@ fun MessageItem( Text( text = stringResource(Res.string.filter_message_label), style = metadataStyle, - color = - if (contrastLevel == ContrastLevel.HIGH) { - MaterialTheme.colorScheme.onSurface - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 8.dp, end = 4.dp), ) } @@ -393,20 +356,8 @@ private fun OriginalMessageSnippet( val originalMessage = message.originalMessage if (originalMessage != null && originalMessage.packetId != 0) { val originalMessageNode = if (originalMessage.fromLocal) ourNode else originalMessage.node - val contrastLevel = LocalContrastLevel.current - val replyContainerColor = - when (contrastLevel) { - ContrastLevel.HIGH -> MaterialTheme.colorScheme.surfaceContainer - else -> Color(originalMessageNode.colors.second).copy(alpha = 0.8f) - } - val replyContentColor = - when (contrastLevel) { - ContrastLevel.HIGH, - ContrastLevel.MEDIUM, - -> MaterialTheme.colorScheme.onSurface - - ContrastLevel.STANDARD -> Color(originalMessageNode.colors.first) - } + val replyContainerColor = Color(originalMessageNode.colors.second).copy(alpha = 0.8f) + val replyContentColor = MaterialTheme.colorScheme.onSurface // Rectangle shape — the outer message bubble's Surface clips to its // rounded corners, so the reply header inherits the correct top radii // automatically and stays square on the bottom where body text follows. diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt index 24a98cae1..99e86e905 100644 --- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt @@ -59,7 +59,6 @@ import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Wifi import org.meshtastic.feature.settings.component.AppInfoSection import org.meshtastic.feature.settings.component.AppearanceSection -import org.meshtastic.feature.settings.component.ContrastPickerDialog import org.meshtastic.feature.settings.component.ExpressiveSection import org.meshtastic.feature.settings.component.PersistenceSection import org.meshtastic.feature.settings.component.PrivacySection @@ -163,14 +162,6 @@ fun SettingsScreen( ) } - var showContrastPickerDialog by remember { mutableStateOf(false) } - if (showContrastPickerDialog) { - ContrastPickerDialog( - onClickContrast = { settingsViewModel.setContrastLevel(it) }, - onDismiss = { showContrastPickerDialog = false }, - ) - } - Scaffold( topBar = { // Show back arrow when remotely administering (caller supplies onBack and we're not on the local node). @@ -245,7 +236,6 @@ fun SettingsScreen( AppearanceSection( onShowLanguagePicker = { showLanguagePickerDialog = true }, onShowThemePicker = { showThemePickerDialog = true }, - onShowContrastPicker = { showContrastPickerDialog = true }, ) ExpressiveSection(title = stringResource(Res.string.wifi_devices)) { diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppearanceSection.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppearanceSection.kt index 6c2cce942..586c4fbc8 100644 --- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppearanceSection.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppearanceSection.kt @@ -28,7 +28,6 @@ import androidx.core.net.toUri import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.app_settings -import org.meshtastic.core.resources.contrast import org.meshtastic.core.resources.preferences_language import org.meshtastic.core.resources.theme import org.meshtastic.core.ui.component.ListItem @@ -38,13 +37,9 @@ import org.meshtastic.core.ui.icon.Language import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.theme.AppTheme -/** Section for app appearance settings like language, theme, and contrast. */ +/** Section for app appearance settings like language and theme. */ @Composable -fun AppearanceSection( - onShowLanguagePicker: () -> Unit, - onShowThemePicker: () -> Unit, - onShowContrastPicker: () -> Unit, -) { +fun AppearanceSection(onShowLanguagePicker: () -> Unit, onShowThemePicker: () -> Unit) { val context = LocalContext.current val settingsLauncher = rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {} @@ -79,19 +74,11 @@ fun AppearanceSection( ) { onShowThemePicker() } - - ListItem( - text = stringResource(Res.string.contrast), - leadingIcon = MeshtasticIcons.FormatPaint, - trailingIcon = null, - ) { - onShowContrastPicker() - } } } @Preview(showBackground = true) @Composable private fun AppearanceSectionPreview() { - AppTheme { AppearanceSection(onShowLanguagePicker = {}, onShowThemePicker = {}, onShowContrastPicker = {}) } + AppTheme { AppearanceSection(onShowLanguagePicker = {}, onShowThemePicker = {}) } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt index ca5de3850..be5ca8c79 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt @@ -33,7 +33,6 @@ import org.meshtastic.core.domain.usecase.settings.ExportDataUseCase import org.meshtastic.core.domain.usecase.settings.IsOtaCapableUseCase import org.meshtastic.core.domain.usecase.settings.MeshLocationUseCase import org.meshtastic.core.domain.usecase.settings.SetAppIntroCompletedUseCase -import org.meshtastic.core.domain.usecase.settings.SetContrastLevelUseCase import org.meshtastic.core.domain.usecase.settings.SetDatabaseCacheLimitUseCase import org.meshtastic.core.domain.usecase.settings.SetLocaleUseCase import org.meshtastic.core.domain.usecase.settings.SetMeshLogSettingsUseCase @@ -66,7 +65,6 @@ class SettingsViewModel( private val meshLogPrefs: MeshLogPrefs, private val notificationPrefs: NotificationPrefs, private val setThemeUseCase: SetThemeUseCase, - private val setContrastLevelUseCase: SetContrastLevelUseCase, private val setLocaleUseCase: SetLocaleUseCase, private val setAppIntroCompletedUseCase: SetAppIntroCompletedUseCase, private val setProvideLocationUseCase: SetProvideLocationUseCase, @@ -164,10 +162,6 @@ class SettingsViewModel( setThemeUseCase(theme) } - fun setContrastLevel(level: Int) { - setContrastLevelUseCase(level) - } - /** Set the application locale. Empty string means system default. */ fun setLocale(languageTag: String) { setLocaleUseCase(languageTag) diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/ContrastPickerDialog.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/ContrastPickerDialog.kt deleted file mode 100644 index 5e2697800..000000000 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/ContrastPickerDialog.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -@file:Suppress("MatchingDeclarationName") - -package org.meshtastic.feature.settings.component - -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.Composable -import org.jetbrains.compose.resources.StringResource -import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.choose_contrast -import org.meshtastic.core.resources.contrast_high -import org.meshtastic.core.resources.contrast_medium -import org.meshtastic.core.resources.contrast_standard -import org.meshtastic.core.ui.component.ListItem -import org.meshtastic.core.ui.component.MeshtasticDialog -import org.meshtastic.core.ui.theme.ContrastLevel - -/** Contrast level options matching [ContrastLevel] ordinal values. */ -enum class ContrastOption(val label: StringResource, val level: ContrastLevel) { - STANDARD(label = Res.string.contrast_standard, level = ContrastLevel.STANDARD), - MEDIUM(label = Res.string.contrast_medium, level = ContrastLevel.MEDIUM), - HIGH(label = Res.string.contrast_high, level = ContrastLevel.HIGH), -} - -/** Shared dialog for picking a contrast level. Used by both Android and Desktop settings screens. */ -@Composable -fun ContrastPickerDialog(onClickContrast: (Int) -> Unit, onDismiss: () -> Unit) { - MeshtasticDialog( - title = stringResource(Res.string.choose_contrast), - onDismiss = onDismiss, - text = { - Column { - ContrastOption.entries.forEach { option -> - ListItem(text = stringResource(option.label), trailingIcon = null) { - onClickContrast(option.level.value) - onDismiss() - } - } - } - }, - ) -} diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt index a01d1a823..95e02f05b 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt @@ -40,7 +40,6 @@ import org.meshtastic.core.domain.usecase.settings.ExportDataUseCase import org.meshtastic.core.domain.usecase.settings.IsOtaCapableUseCase import org.meshtastic.core.domain.usecase.settings.MeshLocationUseCase import org.meshtastic.core.domain.usecase.settings.SetAppIntroCompletedUseCase -import org.meshtastic.core.domain.usecase.settings.SetContrastLevelUseCase import org.meshtastic.core.domain.usecase.settings.SetDatabaseCacheLimitUseCase import org.meshtastic.core.domain.usecase.settings.SetLocaleUseCase import org.meshtastic.core.domain.usecase.settings.SetMeshLogSettingsUseCase @@ -97,7 +96,6 @@ class SettingsViewModelTest { val uiPrefs = appPreferences.ui val setThemeUseCase = SetThemeUseCase(uiPrefs) - val setContrastLevelUseCase = SetContrastLevelUseCase(uiPrefs) val setLocaleUseCase = SetLocaleUseCase(uiPrefs) val setAppIntroCompletedUseCase = SetAppIntroCompletedUseCase(uiPrefs) val setProvideLocationUseCase = SetProvideLocationUseCase(uiPrefs) @@ -118,7 +116,6 @@ class SettingsViewModelTest { meshLogPrefs = appPreferences.meshLog, notificationPrefs = notificationPrefs, setThemeUseCase = setThemeUseCase, - setContrastLevelUseCase = setContrastLevelUseCase, setLocaleUseCase = setLocaleUseCase, setAppIntroCompletedUseCase = setAppIntroCompletedUseCase, setProvideLocationUseCase = setProvideLocationUseCase, diff --git a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/DesktopSettingsScreen.kt b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/DesktopSettingsScreen.kt index 0ce90631f..06ad3df7a 100644 --- a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/DesktopSettingsScreen.kt +++ b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/DesktopSettingsScreen.kt @@ -46,7 +46,6 @@ import org.meshtastic.core.resources.acknowledgements import org.meshtastic.core.resources.app_settings import org.meshtastic.core.resources.app_version import org.meshtastic.core.resources.bottom_nav_settings -import org.meshtastic.core.resources.contrast import org.meshtastic.core.resources.device_db_cache_limit import org.meshtastic.core.resources.device_db_cache_limit_summary import org.meshtastic.core.resources.info @@ -68,7 +67,6 @@ import org.meshtastic.core.ui.icon.Memory import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Wifi import org.meshtastic.core.ui.util.rememberShowToastResource -import org.meshtastic.feature.settings.component.ContrastPickerDialog import org.meshtastic.feature.settings.component.ExpressiveSection import org.meshtastic.feature.settings.component.HomoglyphSetting import org.meshtastic.feature.settings.component.NotificationSection @@ -103,7 +101,6 @@ fun DesktopSettingsScreen( var showThemePickerDialog by remember { mutableStateOf(false) } var showLanguagePickerDialog by remember { mutableStateOf(false) } - var showContrastPickerDialog by remember { mutableStateOf(false) } if (showThemePickerDialog) { ThemePickerDialog( onClickTheme = { settingsViewModel.setTheme(it) }, @@ -111,13 +108,6 @@ fun DesktopSettingsScreen( ) } - if (showContrastPickerDialog) { - ContrastPickerDialog( - onClickContrast = { settingsViewModel.setContrastLevel(it) }, - onDismiss = { showContrastPickerDialog = false }, - ) - } - if (showLanguagePickerDialog) { LanguagePickerDialog( onSelectLanguage = { tag -> settingsViewModel.setLocale(tag) }, @@ -182,14 +172,6 @@ fun DesktopSettingsScreen( showThemePickerDialog = true } - ListItem( - text = stringResource(Res.string.contrast), - leadingIcon = MeshtasticIcons.FormatPaint, - trailingIcon = null, - ) { - showContrastPickerDialog = true - } - ListItem( text = stringResource(Res.string.preferences_language), leadingIcon = MeshtasticIcons.Language,