From 9c910415186a675c2d9d0eb51814c6d117ec61a0 Mon Sep 17 00:00:00 2001 From: "a.emogurov" Date: Wed, 1 Apr 2026 15:45:35 +0400 Subject: [PATCH] feature: add separate ui-kit module & m3 --- .../main/kotlin/convention.compose.gradle.kts | 1 + .../convention.debug.panel.plugin.gradle.kts | 1 + panel-core/build.gradle.kts | 2 + .../core/ui/debugpanel/DebugPanelActivity.kt | 7 +- panel-ui-kit/build.gradle.kts | 45 +++++ panel-ui-kit/consumer-rules.pro | 0 panel-ui-kit/proguard-rules.pro | 1 + panel-ui-kit/src/main/AndroidManifest.xml | 2 + .../debug/uikit/theme/AnimatedColors.kt | 81 ++++++++ .../debug/uikit/theme/BaseColors.kt | 187 ++++++++++++++++++ .../uikit/theme/DebugPanelColorPresets.kt | 46 +++++ .../debug/uikit/theme/DebugPanelColors.kt | 66 +++++++ .../redmadrobot/debug/uikit/theme/Theme.kt | 154 +++++++++++++++ .../debug/uikit/theme/Typography.kt | 102 ++++++++++ settings.gradle.kts | 3 +- 15 files changed, 695 insertions(+), 3 deletions(-) create mode 100644 panel-ui-kit/build.gradle.kts create mode 100644 panel-ui-kit/consumer-rules.pro create mode 100644 panel-ui-kit/proguard-rules.pro create mode 100644 panel-ui-kit/src/main/AndroidManifest.xml create mode 100644 panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/AnimatedColors.kt create mode 100644 panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/BaseColors.kt create mode 100644 panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/DebugPanelColorPresets.kt create mode 100644 panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/DebugPanelColors.kt create mode 100644 panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/Theme.kt create mode 100644 panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/Typography.kt diff --git a/buildSrc/src/main/kotlin/convention.compose.gradle.kts b/buildSrc/src/main/kotlin/convention.compose.gradle.kts index 19b84202..bad48b00 100644 --- a/buildSrc/src/main/kotlin/convention.compose.gradle.kts +++ b/buildSrc/src/main/kotlin/convention.compose.gradle.kts @@ -7,6 +7,7 @@ plugins { dependencies { "implementation"(androidx.compose.foundation) "implementation"(androidx.compose.material) + "implementation"(androidx.compose.material3) "implementation"(androidx.compose.ui) "implementation"(androidx.compose.runtime) "implementation"(androidx.lifecycle.viewmodel.compose) diff --git a/buildSrc/src/main/kotlin/convention.debug.panel.plugin.gradle.kts b/buildSrc/src/main/kotlin/convention.debug.panel.plugin.gradle.kts index 785292aa..167109f4 100644 --- a/buildSrc/src/main/kotlin/convention.debug.panel.plugin.gradle.kts +++ b/buildSrc/src/main/kotlin/convention.debug.panel.plugin.gradle.kts @@ -34,4 +34,5 @@ kotlin { dependencies { implementation(project(":panel-core")) + implementation(project(":panel-ui-kit")) } diff --git a/panel-core/build.gradle.kts b/panel-core/build.gradle.kts index 67851357..8bbb4de4 100644 --- a/panel-core/build.gradle.kts +++ b/panel-core/build.gradle.kts @@ -43,6 +43,8 @@ kotlin { } dependencies { + implementation(project(":panel-ui-kit")) + implementation(androidx.activity.compose) implementation(androidx.navigation.compose) implementation(androidx.core) diff --git a/panel-core/src/main/kotlin/com/redmadrobot/debug/core/ui/debugpanel/DebugPanelActivity.kt b/panel-core/src/main/kotlin/com/redmadrobot/debug/core/ui/debugpanel/DebugPanelActivity.kt index 197d24bb..66e35ffa 100644 --- a/panel-core/src/main/kotlin/com/redmadrobot/debug/core/ui/debugpanel/DebugPanelActivity.kt +++ b/panel-core/src/main/kotlin/com/redmadrobot/debug/core/ui/debugpanel/DebugPanelActivity.kt @@ -3,14 +3,17 @@ package com.redmadrobot.debug.core.ui.debugpanel import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.remember import com.redmadrobot.debug.core.inapp.compose.DebugPanelScreen +import com.redmadrobot.debug.uikit.theme.DebugPanelTheme +import com.redmadrobot.debug.uikit.theme.ThemeState internal class DebugPanelActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - MaterialTheme { + val themeState = remember { ThemeState() } + DebugPanelTheme(themeState = themeState) { DebugPanelScreen(onClose = { finish() }) } } diff --git a/panel-ui-kit/build.gradle.kts b/panel-ui-kit/build.gradle.kts new file mode 100644 index 00000000..25c65ef8 --- /dev/null +++ b/panel-ui-kit/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + id("com.android.library") + id("convention.compose") + id("convention-publish") + id("convention.detekt") +} + +description = "Debug panel UI kit: theme, design tokens, shared components" + +android { + compileSdk = Project.COMPILE_SDK + lint.targetSdk = Project.TARGET_SDK + + defaultConfig { + minSdk = Project.MIN_SDK + + consumerProguardFile("consumer-rules.pro") + } + + buildTypes { + getByName(Project.BuildTypes.release) { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile(Project.Proguard.androidOptimizedRules), + Project.Proguard.projectRules + ) + } + } + + kotlin { + jvmToolchain(17) + } + + namespace = "com.redmadrobot.debug.uikit" +} + +kotlin { + explicitApi() +} + +dependencies { + implementation(androidx.compose.material3) + implementation(androidx.compose.ui.tooling) + implementation(androidx.compose.ui.tooling.preview) +} diff --git a/panel-ui-kit/consumer-rules.pro b/panel-ui-kit/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/panel-ui-kit/proguard-rules.pro b/panel-ui-kit/proguard-rules.pro new file mode 100644 index 00000000..fb164d66 --- /dev/null +++ b/panel-ui-kit/proguard-rules.pro @@ -0,0 +1 @@ +# Add project specific ProGuard rules here. diff --git a/panel-ui-kit/src/main/AndroidManifest.xml b/panel-ui-kit/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8072ee00 --- /dev/null +++ b/panel-ui-kit/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/AnimatedColors.kt b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/AnimatedColors.kt new file mode 100644 index 00000000..1a1ad24f --- /dev/null +++ b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/AnimatedColors.kt @@ -0,0 +1,81 @@ +package com.redmadrobot.debug.uikit.theme + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.Color + +private val ColorAnimSpec = spring(stiffness = 600f) + +@Composable +internal fun DebugPanelColors.animated(): DebugPanelColors = DebugPanelColors( + background = background.animated(), + button = button.animated(), + content = content.animated(), + stroke = stroke.animated(), + surface = surface.animated(), + source = source.animated(), +) + +@Composable +private fun animateColor(target: Color): Color { + val animated by animateColorAsState( + targetValue = target, + animationSpec = ColorAnimSpec, + label = "", + ) + return animated +} + +@Composable +private fun BackgroundColors.animated(): BackgroundColors = BackgroundColors( + primary = animateColor(primary), + secondary = animateColor(secondary), + tertiary = animateColor(tertiary), +) + +@Composable +private fun ButtonColors.animated(): ButtonColors = ButtonColors( + primary = animateColor(primary), + onPrimary = animateColor(onPrimary), + secondary = animateColor(secondary), + onSecondary = animateColor(onSecondary), + error = animateColor(error), + onError = animateColor(onError), +) + +@Composable +private fun ContentColors.animated(): ContentColors = ContentColors( + primary = animateColor(primary), + secondary = animateColor(secondary), + tertiary = animateColor(tertiary), + accent = animateColor(accent), + error = animateColor(error), + teal = animateColor(teal), +) + +@Composable +private fun StrokeColors.animated(): StrokeColors = StrokeColors( + primary = animateColor(primary), + secondary = animateColor(secondary), +) + +@Composable +private fun SurfaceColors.animated(): SurfaceColors = SurfaceColors( + primary = animateColor(primary), + secondary = animateColor(secondary), + tertiary = animateColor(tertiary), + dialog = animateColor(dialog), + selected = animateColor(selected), +) + +@Composable +private fun SourceColors.animated(): SourceColors = SourceColors( + defaultText = animateColor(defaultText), + defaultBackground = animateColor(defaultBackground), + debugText = animateColor(debugText), + debugBackground = animateColor(debugBackground), + remoteText = animateColor(remoteText), + remoteBackground = animateColor(remoteBackground), +) diff --git a/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/BaseColors.kt b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/BaseColors.kt new file mode 100644 index 00000000..3baf438b --- /dev/null +++ b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/BaseColors.kt @@ -0,0 +1,187 @@ +@file:Suppress("MagicNumber") + +package com.redmadrobot.debug.uikit.theme + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +internal object BaseColors { + // Purple + val Purple10 = Color(0xFF21005D) + val Purple20 = Color(0xFF381E72) + val Purple30 = Color(0xFF4A4458) + val Purple40 = Color(0xFF6750A4) + val Purple80 = Color(0xFFD0BCFF) + val Purple90 = Color(0xFFE8DEF8) + val Purple95 = Color(0xFFF3EDF7) + val Purple99 = Color(0xFFFEF7FF) + + // Neutral + val Neutral10 = Color(0xFF1C1B1F) + val Neutral20 = Color(0xFF313033) + val Neutral30 = Color(0xFF49454F) + val Neutral40 = Color(0xFF605D66) + val Neutral50 = Color(0xFF79747E) + val Neutral60 = Color(0xFF938F99) + val Neutral80 = Color(0xFFCAC4D0) + val Neutral87 = Color(0xFFE6E1E5) + val Neutral90 = Color(0xFFE6E0E9) + val Neutral92 = Color(0xFFECE6F0) + val Neutral94 = Color(0xFFF4EFF4) + val Neutral95 = Color(0xFFE7E0EC) + val Neutral99 = Color(0xFFFFFBFE) + + // Neutral variant (dark surfaces) + val NeutralVariant20 = Color(0xFF2B2930) + val NeutralVariant30 = Color(0xFF36343B) + val NeutralVariant40 = Color(0xFF414046) + + // Secondary + val Secondary40 = Color(0xFF625B71) + val Secondary80 = Color(0xFFCCC2DC) + val Secondary90 = Color(0xFFE8DEF8) + + // Tertiary + val Tertiary20 = Color(0xFF492532) + val Tertiary30 = Color(0xFF633B48) + val Tertiary40 = Color(0xFF7D5260) + val Tertiary80 = Color(0xFFEFB8C8) + val Tertiary90 = Color(0xFFFFD8E4) + + // Error + val Error20 = Color(0xFF601410) + val Error30 = Color(0xFF8C1D18) + val Error40 = Color(0xFFB3261E) + val Error80 = Color(0xFFF2B8B5) + val Error90 = Color(0xFFF9DEDC) + + // Status / accent + val Teal = Color(0xFF03DAC6) + val Green = Color(0xFF1B6E2D) + val GreenLight = Color(0xFFD6F5E0) + val GreenDark = Color(0xFF1B3D2A) + val GreenDarkText = Color(0xFF7DD99E) + val Orange = Color(0xFFE65100) + val OrangeLight = Color(0xFFFFF3E0) + val OrangeDark = Color(0xFF3D2E10) + val OrangeDarkText = Color(0xFFFFB74D) + + val White = Color(0xFFFFFFFF) + val Black = Color(0xFF000000) +} + +@Suppress("LongMethod") +@OptIn(ExperimentalLayoutApi::class) +@Composable +@Preview(showBackground = true) +private fun Preview() { + DebugPanelTheme { + FlowRow( + modifier = Modifier + .fillMaxSize() + .verticalScroll(state = rememberScrollState()) + .padding(horizontal = 8.dp), + horizontalArrangement = Arrangement.spacedBy(space = 4.dp), + verticalArrangement = Arrangement.spacedBy(space = 4.dp), + ) { + // Purple + ColorElement(BaseColors.Purple10, "Purple10") + ColorElement(BaseColors.Purple20, "Purple20") + ColorElement(BaseColors.Purple30, "Purple30") + ColorElement(BaseColors.Purple40, "Purple40") + ColorElement(BaseColors.Purple80, "Purple80") + ColorElement(BaseColors.Purple90, "Purple90") + ColorElement(BaseColors.Purple95, "Purple95") + ColorElement(BaseColors.Purple99, "Purple99") + + // Neutral + ColorElement(BaseColors.Neutral10, "Neutral10") + ColorElement(BaseColors.Neutral20, "Neutral20") + ColorElement(BaseColors.Neutral30, "Neutral30") + ColorElement(BaseColors.Neutral40, "Neutral40") + ColorElement(BaseColors.Neutral50, "Neutral50") + ColorElement(BaseColors.Neutral60, "Neutral60") + ColorElement(BaseColors.Neutral80, "Neutral80") + ColorElement(BaseColors.Neutral87, "Neutral87") + ColorElement(BaseColors.Neutral90, "Neutral90") + ColorElement(BaseColors.Neutral92, "Neutral92") + ColorElement(BaseColors.Neutral94, "Neutral94") + ColorElement(BaseColors.Neutral95, "Neutral95") + ColorElement(BaseColors.Neutral99, "Neutral99") + + // Neutral variant + ColorElement(BaseColors.NeutralVariant20, "NV20") + ColorElement(BaseColors.NeutralVariant30, "NV30") + ColorElement(BaseColors.NeutralVariant40, "NV40") + + // Secondary + ColorElement(BaseColors.Secondary40, "Secondary40") + ColorElement(BaseColors.Secondary80, "Secondary80") + ColorElement(BaseColors.Secondary90, "Secondary90") + + // Tertiary + ColorElement(BaseColors.Tertiary20, "Tertiary20") + ColorElement(BaseColors.Tertiary30, "Tertiary30") + ColorElement(BaseColors.Tertiary40, "Tertiary40") + ColorElement(BaseColors.Tertiary80, "Tertiary80") + ColorElement(BaseColors.Tertiary90, "Tertiary90") + + // Error + ColorElement(BaseColors.Error20, "Error20") + ColorElement(BaseColors.Error30, "Error30") + ColorElement(BaseColors.Error40, "Error40") + ColorElement(BaseColors.Error80, "Error80") + ColorElement(BaseColors.Error90, "Error90") + + // Status / accent + ColorElement(BaseColors.Teal, "Teal") + ColorElement(BaseColors.Green, "Green") + ColorElement(BaseColors.GreenLight, "GreenLight") + ColorElement(BaseColors.GreenDark, "GreenDark") + ColorElement(BaseColors.GreenDarkText, "GreenDkTxt") + ColorElement(BaseColors.Orange, "Orange") + ColorElement(BaseColors.OrangeLight, "OrangeLight") + ColorElement(BaseColors.OrangeDark, "OrangeDark") + ColorElement(BaseColors.OrangeDarkText, "OrangeDkTxt") + + // Black & White + ColorElement(BaseColors.White, "White") + ColorElement(BaseColors.Black, "Black") + } + } +} + +@Composable +private fun ColorElement(color: Color, label: String, modifier: Modifier = Modifier) { + val textColor = if (color.luminance() > 0.5f) Color.Black else Color.White + + Box( + modifier = modifier + .size(size = 100.dp) + .background(color = color), + contentAlignment = Alignment.Center, + ) { + Text( + text = label, + color = textColor, + fontSize = 10.sp, + ) + } +} diff --git a/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/DebugPanelColorPresets.kt b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/DebugPanelColorPresets.kt new file mode 100644 index 00000000..c3d10e98 --- /dev/null +++ b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/DebugPanelColorPresets.kt @@ -0,0 +1,46 @@ +package com.redmadrobot.debug.uikit.theme + +internal val LightDebugPanelColors = DebugPanelColors() + +internal val DarkDebugPanelColors = DebugPanelColors( + background = BackgroundColors( + primary = BaseColors.Neutral10, + secondary = BaseColors.NeutralVariant20, + tertiary = BaseColors.Purple30, + ), + button = ButtonColors( + primary = BaseColors.Purple80, + onPrimary = BaseColors.Purple20, + secondary = BaseColors.Purple30, + onSecondary = BaseColors.Purple80, + error = BaseColors.Error80, + onError = BaseColors.Error20, + ), + content = ContentColors( + primary = BaseColors.Neutral87, + secondary = BaseColors.Neutral80, + tertiary = BaseColors.Neutral50, + accent = BaseColors.Purple80, + error = BaseColors.Error80, + teal = BaseColors.Teal, + ), + stroke = StrokeColors( + primary = BaseColors.Neutral30, + secondary = BaseColors.Neutral40, + ), + surface = SurfaceColors( + primary = BaseColors.Neutral10, + secondary = BaseColors.NeutralVariant20, + tertiary = BaseColors.Purple30, + dialog = BaseColors.NeutralVariant20, + selected = BaseColors.Purple30, + ), + source = SourceColors( + defaultText = BaseColors.Neutral80, + defaultBackground = BaseColors.Neutral30, + debugText = BaseColors.GreenDarkText, + debugBackground = BaseColors.GreenDark, + remoteText = BaseColors.OrangeDarkText, + remoteBackground = BaseColors.OrangeDark, + ), +) diff --git a/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/DebugPanelColors.kt b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/DebugPanelColors.kt new file mode 100644 index 00000000..bfd21cc7 --- /dev/null +++ b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/DebugPanelColors.kt @@ -0,0 +1,66 @@ +package com.redmadrobot.debug.uikit.theme + +import androidx.compose.runtime.Stable +import androidx.compose.ui.graphics.Color + +@Stable +public data class BackgroundColors( + val primary: Color = BaseColors.Purple99, + val secondary: Color = BaseColors.Purple95, + val tertiary: Color = BaseColors.Purple90, +) + +@Stable +public data class ButtonColors( + val primary: Color = BaseColors.Purple40, + val onPrimary: Color = BaseColors.White, + val secondary: Color = BaseColors.Purple90, + val onSecondary: Color = BaseColors.Purple40, + val error: Color = BaseColors.Error40, + val onError: Color = BaseColors.White, +) + +@Stable +public data class ContentColors( + val primary: Color = BaseColors.Neutral10, + val secondary: Color = BaseColors.Neutral30, + val tertiary: Color = BaseColors.Neutral50, + val accent: Color = BaseColors.Purple40, + val error: Color = BaseColors.Error40, + val teal: Color = BaseColors.Teal, +) + +@Stable +public data class StrokeColors( + val primary: Color = BaseColors.Neutral95, + val secondary: Color = BaseColors.Neutral80, +) + +@Stable +public data class SurfaceColors( + val primary: Color = BaseColors.Purple99, + val secondary: Color = BaseColors.Purple95, + val tertiary: Color = BaseColors.Purple90, + val dialog: Color = BaseColors.Neutral99, + val selected: Color = BaseColors.Purple90, +) + +@Stable +public data class SourceColors( + val defaultText: Color = BaseColors.Neutral50, + val defaultBackground: Color = BaseColors.Neutral95, + val debugText: Color = BaseColors.Green, + val debugBackground: Color = BaseColors.GreenLight, + val remoteText: Color = BaseColors.Orange, + val remoteBackground: Color = BaseColors.OrangeLight, +) + +@Stable +public data class DebugPanelColors( + val background: BackgroundColors = BackgroundColors(), + val button: ButtonColors = ButtonColors(), + val content: ContentColors = ContentColors(), + val stroke: StrokeColors = StrokeColors(), + val surface: SurfaceColors = SurfaceColors(), + val source: SourceColors = SourceColors(), +) diff --git a/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/Theme.kt b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/Theme.kt new file mode 100644 index 00000000..47cde525 --- /dev/null +++ b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/Theme.kt @@ -0,0 +1,154 @@ +package com.redmadrobot.debug.uikit.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext + +/** + * Theme mode selectable at runtime from debug panel settings. + */ +public enum class ThemeMode { + /** Follow system dark/light setting. */ + System, + + /** Always light. */ + Light, + + /** Always dark. */ + Dark, +} + +/** + * Holds the currently selected [ThemeMode]. + * + * Mutate [themeMode] from settings UI — the composable tree will recompose automatically. + */ +@Stable +public class ThemeState(initialMode: ThemeMode = ThemeMode.System) { + public var themeMode: ThemeMode by mutableStateOf(initialMode) +} + +internal val LocalColors = compositionLocalOf { LightDebugPanelColors } +internal val LocalTypography = compositionLocalOf { DebugPanelTypographyTokens() } +internal val LocalThemeState = compositionLocalOf { ThemeState() } + +/** + * Entry point for accessing Debug Panel design tokens. + * + * Usage: + * ``` + * color = DebugPanelTheme.colors.background.primary + * style = DebugPanelTheme.typography.bodySmall + * mode = DebugPanelTheme.themeState.themeMode + * ``` + */ +public object DebugPanelTheme { + public val colors: DebugPanelColors + @Composable + @ReadOnlyComposable + get() = LocalColors.current + + public val typography: DebugPanelTypographyTokens + @Composable + @ReadOnlyComposable + get() = LocalTypography.current + + public val themeState: ThemeState + @Composable + @ReadOnlyComposable + get() = LocalThemeState.current +} + +/** + * Debug Panel theme. + * + * Provides [DebugPanelColors] and [DebugPanelTypographyTokens] via [CompositionLocal], + * and configures Material 3 [ColorScheme] for standard M3 components. + * + * When [themeState]`.themeMode` changes at runtime (e.g. from settings), + * the entire UI recomposes with animated color transitions. + * + * @param themeState holds the runtime-mutable [ThemeMode]. Shared across the panel so that + * settings can write to it and all screens react. + * @param dynamicColor whether to use Material You dynamic colors on supported devices. + * @param content the composable content to be themed. + */ +@Composable +public fun DebugPanelTheme( + themeState: ThemeState = ThemeState(), + dynamicColor: Boolean = true, + content: @Composable () -> Unit, +) { + val systemDark = isSystemInDarkTheme() + val darkTheme = when (themeState.themeMode) { + ThemeMode.System -> systemDark + ThemeMode.Light -> false + ThemeMode.Dark -> true + } + + val targetColors = if (darkTheme) DarkDebugPanelColors else LightDebugPanelColors + val panelColors = targetColors.animated() + val panelTypography = if (darkTheme) DarkDebugPanelTypographyTokens else DebugPanelTypographyTokens() + + val materialColorScheme = if (dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } else { + panelColors.toMaterialColorScheme() + } + + CompositionLocalProvider( + LocalColors provides panelColors, + LocalTypography provides panelTypography, + LocalThemeState provides themeState, + ) { + MaterialTheme( + colorScheme = materialColorScheme, + typography = panelTypography.toMaterialTypography(), + content = content, + ) + } +} + +internal fun DebugPanelColors.toMaterialColorScheme(): ColorScheme = lightColorScheme( + primary = button.primary, + onPrimary = button.onPrimary, + primaryContainer = surface.tertiary, + onPrimaryContainer = content.primary, + secondary = content.secondary, + onSecondary = button.onPrimary, + secondaryContainer = button.secondary, + onSecondaryContainer = content.primary, + tertiary = content.tertiary, + onTertiary = button.onPrimary, + error = content.error, + onError = button.onError, + errorContainer = BaseColors.Error90, + onErrorContainer = BaseColors.Error20, + background = background.primary, + onBackground = content.primary, + surface = surface.primary, + onSurface = content.primary, + surfaceVariant = stroke.primary, + onSurfaceVariant = content.secondary, + outline = content.tertiary, + outlineVariant = stroke.secondary, + surfaceContainer = surface.secondary, + surfaceContainerHigh = surface.tertiary, + inverseSurface = content.primary, + inverseOnSurface = background.primary, + inversePrimary = content.accent, +) diff --git a/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/Typography.kt b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/Typography.kt new file mode 100644 index 00000000..2b574250 --- /dev/null +++ b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/Typography.kt @@ -0,0 +1,102 @@ +package com.redmadrobot.debug.uikit.theme + +import androidx.compose.material3.Typography +import androidx.compose.runtime.Stable +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +/** Monospace font family for technical values (URLs, config keys, code). */ +public val MonoFontFamily: FontFamily = FontFamily.Monospace + +@Stable +public data class DebugPanelTypographyTokens( + /** TopAppBar title. */ + val titleLarge: TextStyle = TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.sp, + ), + /** Section headers, group names. */ + val titleMedium: TextStyle = TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), + /** Card titles, server names. */ + val titleSmall: TextStyle = TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), + /** Primary body text. */ + val bodyLarge: TextStyle = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp, + ), + /** Secondary body text, config values. */ + val bodyMedium: TextStyle = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 13.sp, + lineHeight = 18.sp, + letterSpacing = 0.25.sp, + ), + /** Technical values (URLs, keys) — compact monospace. */ + val bodySmall: TextStyle = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp, + fontFamily = MonoFontFamily, + ), + /** Tab labels, chip text. */ + val labelLarge: TextStyle = TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.1.sp, + ), + /** Badges, source indicators. */ + val labelMedium: TextStyle = TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + ), + /** Section headers (uppercase), overline text. */ + val labelSmall: TextStyle = TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 10.sp, + lineHeight = 14.sp, + letterSpacing = 1.sp, + ), +) + +internal val DarkDebugPanelTypographyTokens = DebugPanelTypographyTokens( + bodySmall = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp, + fontFamily = MonoFontFamily, + color = BaseColors.Neutral80, + ), +) + +internal fun DebugPanelTypographyTokens.toMaterialTypography(): Typography = Typography( + titleLarge = titleLarge, + titleMedium = titleMedium, + titleSmall = titleSmall, + bodyLarge = bodyLarge, + bodyMedium = bodyMedium, + bodySmall = bodySmall, + labelLarge = labelLarge, + labelMedium = labelMedium, + labelSmall = labelSmall, +) diff --git a/settings.gradle.kts b/settings.gradle.kts index e6cd6d55..25282f3c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -47,7 +47,8 @@ dependencyResolutionManagement { // Base modules include( ":panel-no-op", - ":panel-core" + ":panel-core", + ":panel-ui-kit" ) // Plugins