diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index c49cc980..04dbe7e8 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -98,6 +98,7 @@ dependencies { implementation(libs.compose.ui) implementation(libs.compose.ui.graphics) implementation(libs.compose.material3) + implementation(libs.compose.ui.text.google.fonts) debugImplementation(libs.compose.ui.tooling) implementation(libs.compose.ui.tooling.preview) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/MainActivity.kt b/android/app/src/main/java/com/fabledsword/minstrel/MainActivity.kt index 97e9824d..59a8cd00 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/MainActivity.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/MainActivity.kt @@ -10,6 +10,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.fabledsword.minstrel.theme.MinstrelTheme import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -17,7 +18,9 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() - setContent { App() } + setContent { + MinstrelTheme { App() } + } } } diff --git a/android/app/src/main/java/com/fabledsword/minstrel/theme/LocalActionColors.kt b/android/app/src/main/java/com/fabledsword/minstrel/theme/LocalActionColors.kt new file mode 100644 index 00000000..34d8109d --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/theme/LocalActionColors.kt @@ -0,0 +1,29 @@ +package com.fabledsword.minstrel.theme + +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color + +/** + * Semantic action colors. The FabledSword design system reserves Moss/Bronze/ + * Oxblood for action buttons; the forest-teal accent (`FabledSwordTokens.accent`) + * is for surfaces / highlights / non-action emphasis only and must NEVER be + * used as a button color. + * + * Composables that render action buttons should read from `LocalActionColors.current` + * rather than `MaterialTheme.colorScheme.primary`. + */ +data class ActionColors( + val primary: Color, // Moss — confirm, save, play + val secondary: Color, // Bronze — neutral, browse, accent action + val destructive: Color, // Oxblood — delete, unflag-as-destructive + val onAction: Color, // foreground / text on any of the above +) + +val LocalActionColors = staticCompositionLocalOf { + ActionColors( + primary = FabledSwordTokens.moss, + secondary = FabledSwordTokens.bronze, + destructive = FabledSwordTokens.oxblood, + onAction = FabledSwordTokens.onAction, + ) +} diff --git a/android/app/src/main/java/com/fabledsword/minstrel/theme/MinstrelTheme.kt b/android/app/src/main/java/com/fabledsword/minstrel/theme/MinstrelTheme.kt new file mode 100644 index 00000000..1161418e --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/theme/MinstrelTheme.kt @@ -0,0 +1,51 @@ +package com.fabledsword.minstrel.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider + +/** + * Dark Material 3 color scheme assembled from FabledSword tokens. The accent + * (forest-teal) becomes the M3 `primary`; surfaces/backgrounds come from the + * dark token cohort. Action colors do NOT come from this scheme — see + * `LocalActionColors`. + * + * Light theme is deferred to post-v1 (mirrors Flutter primary mode). + */ +private val MinstrelDarkColorScheme = darkColorScheme( + primary = FabledSwordTokens.accent, + onPrimary = FabledSwordTokens.onAction, + primaryContainer = FabledSwordTokens.accent.copy(alpha = 0.25f), + onPrimaryContainer = FabledSwordTokens.parchment, + secondary = FabledSwordTokens.bronze, + onSecondary = FabledSwordTokens.onAction, + background = FabledSwordTokens.obsidian, + onBackground = FabledSwordTokens.parchment, + surface = FabledSwordTokens.iron, + onSurface = FabledSwordTokens.parchment, + surfaceVariant = FabledSwordTokens.slate, + onSurfaceVariant = FabledSwordTokens.ash, + error = FabledSwordTokens.error, + onError = FabledSwordTokens.onAction, + outline = FabledSwordTokens.pewter, + outlineVariant = FabledSwordTokens.slate, +) + +@Composable +fun MinstrelTheme(content: @Composable () -> Unit) { + MaterialTheme( + colorScheme = MinstrelDarkColorScheme, + typography = MinstrelTypography, + ) { + CompositionLocalProvider( + LocalActionColors provides ActionColors( + primary = FabledSwordTokens.moss, + secondary = FabledSwordTokens.bronze, + destructive = FabledSwordTokens.oxblood, + onAction = FabledSwordTokens.onAction, + ), + content = content, + ) + } +} diff --git a/android/app/src/main/java/com/fabledsword/minstrel/theme/Tokens.kt b/android/app/src/main/java/com/fabledsword/minstrel/theme/Tokens.kt new file mode 100644 index 00000000..a30bc57f --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/theme/Tokens.kt @@ -0,0 +1,42 @@ +package com.fabledsword.minstrel.theme + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +/** + * FabledSword design-system tokens. Pure data — no theme wiring. + * + * Source of truth: `flutter_client/shared/fabledsword.tokens.json`. When that + * file changes, mirror the new hex values here. (Native v1 doesn't run the + * Flutter `tool/gen_tokens.dart` generator — manual sync until a cross-language + * codegen pipeline lands.) + * + * Mirrors flutter_client/lib/theme/tokens.dart, dark surfaces + flat. Light + * theme support deferred to post-v1; the user's primary mode is dark. + */ +object FabledSwordTokens { + // Dark surface tokens. + val obsidian = Color(0xFF14171A) + val iron = Color(0xFF1E2228) + val slate = Color(0xFF2C313A) + val pewter = Color(0xFF3F4651) + val parchment = Color(0xFFE8E4D8) + val vellum = Color(0xFFC2BFB4) + val ash = Color(0xFF9C9A92) + + // Flat (theme-mode-independent) tokens. + val moss = Color(0xFF4A5D3F) // primary action + val bronze = Color(0xFF8B7355) // secondary action + val oxblood = Color(0xFF6B2118) // destructive action + val warning = Color(0xFF8B6F1E) + val error = Color(0xFFC04A1F) + val info = Color(0xFF3D5A6E) + val accent = Color(0xFF4A6B5C) // forest-teal — NEVER for actions + val onAction = Color(0xFFE8E4D8) + + // Corner radii. + val radiusSm = 4.dp + val radiusMd = 8.dp + val radiusLg = 12.dp + val radiusXl = 16.dp +} diff --git a/android/app/src/main/java/com/fabledsword/minstrel/theme/Typography.kt b/android/app/src/main/java/com/fabledsword/minstrel/theme/Typography.kt new file mode 100644 index 00000000..f48ab4b3 --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/theme/Typography.kt @@ -0,0 +1,71 @@ +package com.fabledsword.minstrel.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.googlefonts.Font +import androidx.compose.ui.text.googlefonts.GoogleFont +import androidx.compose.ui.unit.sp +import com.fabledsword.minstrel.R + +/** + * Google Fonts provider — fetches font files via Play Services Fonts at + * runtime, caches them across launches. Matches the Flutter client's + * `google_fonts` package behaviour (no bundled .ttf files in either tree). + * + * Per FabledSword design system: + * - Fraunces — display + headline (mythic serif) + * - Inter — body + label (clean sans for UI text) + * - JetBrains Mono — technical / monospace + * Weights are restricted to 400 (regular) and 500 (medium) only. + */ +private val GoogleFontProvider = GoogleFont.Provider( + providerAuthority = "com.google.android.gms.fonts", + providerPackage = "com.google.android.gms", + certificates = R.array.com_google_android_gms_fonts_certs, +) + +private val FrauncesFont = GoogleFont("Fraunces") +private val InterFont = GoogleFont("Inter") +private val JetBrainsMonoFont = GoogleFont("JetBrains Mono") + +private val Fraunces = FontFamily( + Font(googleFont = FrauncesFont, fontProvider = GoogleFontProvider, weight = FontWeight.W400, style = FontStyle.Normal), + Font(googleFont = FrauncesFont, fontProvider = GoogleFontProvider, weight = FontWeight.W500, style = FontStyle.Normal), +) + +private val Inter = FontFamily( + Font(googleFont = InterFont, fontProvider = GoogleFontProvider, weight = FontWeight.W400, style = FontStyle.Normal), + Font(googleFont = InterFont, fontProvider = GoogleFontProvider, weight = FontWeight.W500, style = FontStyle.Normal), +) + +private val JetBrainsMono = FontFamily( + Font(googleFont = JetBrainsMonoFont, fontProvider = GoogleFontProvider, weight = FontWeight.W400, style = FontStyle.Normal), +) + +/** + * FabledSword Material 3 Typography mapping. Display/headline slots use + * Fraunces; title/body/label use Inter; the smallest label slots step down + * to JetBrains Mono for technical/code surfaces. Sizes follow Material 3 + * defaults; the project's "Fraunces ≥18px" rule is satisfied because every + * Fraunces-bearing slot is 18sp or larger. + */ +val MinstrelTypography = Typography( + displayLarge = TextStyle(fontFamily = Fraunces, fontWeight = FontWeight.W500, fontSize = 32.sp), + displayMedium = TextStyle(fontFamily = Fraunces, fontWeight = FontWeight.W500, fontSize = 28.sp), + displaySmall = TextStyle(fontFamily = Fraunces, fontWeight = FontWeight.W500, fontSize = 24.sp), + headlineLarge = TextStyle(fontFamily = Fraunces, fontWeight = FontWeight.W500, fontSize = 22.sp), + headlineMedium = TextStyle(fontFamily = Fraunces, fontWeight = FontWeight.W500, fontSize = 20.sp), + headlineSmall = TextStyle(fontFamily = Fraunces, fontWeight = FontWeight.W500, fontSize = 18.sp), + titleLarge = TextStyle(fontFamily = Inter, fontWeight = FontWeight.W500, fontSize = 18.sp), + titleMedium = TextStyle(fontFamily = Inter, fontWeight = FontWeight.W500, fontSize = 16.sp), + titleSmall = TextStyle(fontFamily = Inter, fontWeight = FontWeight.W500, fontSize = 14.sp), + bodyLarge = TextStyle(fontFamily = Inter, fontWeight = FontWeight.W400, fontSize = 16.sp), + bodyMedium = TextStyle(fontFamily = Inter, fontWeight = FontWeight.W400, fontSize = 14.sp), + bodySmall = TextStyle(fontFamily = Inter, fontWeight = FontWeight.W400, fontSize = 12.sp), + labelLarge = TextStyle(fontFamily = Inter, fontWeight = FontWeight.W500, fontSize = 14.sp), + labelMedium = TextStyle(fontFamily = JetBrainsMono, fontWeight = FontWeight.W400, fontSize = 12.sp), + labelSmall = TextStyle(fontFamily = JetBrainsMono, fontWeight = FontWeight.W400, fontSize = 11.sp), +) diff --git a/android/app/src/main/res/values/font_certs.xml b/android/app/src/main/res/values/font_certs.xml new file mode 100644 index 00000000..66434243 --- /dev/null +++ b/android/app/src/main/res/values/font_certs.xml @@ -0,0 +1,16 @@ + + + + + @array/com_google_android_gms_fonts_certs_dev + @array/com_google_android_gms_fonts_certs_prod + + + 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 + + diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 76ebf116..e154bf75 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -41,6 +41,7 @@ compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } compose-material3 = { module = "androidx.compose.material3:material3" } +compose-ui-text-google-fonts = { module = "androidx.compose.ui:ui-text-google-fonts", version = "1.7.5" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }