feat(android): FabledSword theme — Material 3 + Google Fonts
M8 phase 1.4. Mirrors flutter_client/lib/theme/. Source of truth for hex values is flutter_client/shared/fabledsword.tokens.json (manual sync until cross-language codegen lands; ports the dark-surface + flat cohort). Material 3 ColorScheme takes accent as primary; action colors (Moss/Bronze/Oxblood) live in LocalActionColors as semantic roles per the project_design_system rule "NEVER use accent for action buttons". Typography uses androidx.compose.ui.text.googlefonts to fetch Fraunces / Inter / JetBrains Mono at runtime via Play Services Fonts — matches the Flutter client's `google_fonts` package (no bundled .ttf files in either tree). Weights restricted to 400/500. Fraunces is reserved for ≥18sp display/headline slots per the design-system rule. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Google Fonts provider certificate hashes for downloadable fonts via
|
||||
androidx.compose.ui.text.googlefonts.GoogleFont.Provider. Standard
|
||||
values published by Google; copied verbatim from the AndroidX docs. -->
|
||||
<resources>
|
||||
<array name="com_google_android_gms_fonts_certs">
|
||||
<item>@array/com_google_android_gms_fonts_certs_dev</item>
|
||||
<item>@array/com_google_android_gms_fonts_certs_prod</item>
|
||||
</array>
|
||||
<string-array name="com_google_android_gms_fonts_certs_dev">
|
||||
<item>MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpoyLcfobBPv6yyz8x1IxWWmF9c1IGN3vSL6BLNJEUyMEPzC2WZdwT4ZG2cuJTtzeETl6jWFKx68ETtZxNVHe9Iy9NMxEljDqVZ4y6+FlHaiYJqq3LcJpJVuKYz4kvOcyf3M0nDA8mUlVdfsOlw/H4uoNQ7VrAQUKB4kAyfxsKp/RZmnZSJ7+8Ag9aTC+oguTd1iFNuMqDUlpePo6CGuh73iKuq8mYvtdQQ0Yz+mF4j2YWB7Gj0R1k2cCAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=</item>
|
||||
</string-array>
|
||||
<string-array name="com_google_android_gms_fonts_certs_prod">
|
||||
<item>MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAwEAAaOB1zCB1DAdBgNVHQ4EFgQUhzkS9E6G+x8U7eIYZVgWyN4j2u4wgaQGA1UdIwSBnDCBmYAUhzkS9E6G+x8U7eIYZVgWyN4j2u6heKR2MHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZIIJAMLgh0ZkSjCNMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABywqUAtNkXf2EVQuRGiI3pnNvIYx7N5xj4LMtloEdEqMpEcMa6Qe87qDx2hsArOR1nzQAFGsT/8YIIfX0fAJjQuP1lAcExSxVKbFICEvFBaWuhGgOOZ7CYzfHB6tEzJFLR2DQHQrXLT2HKDDhxhe9hKzqIRDSc5Hjr3jY5MMzfYM5lFvKK9pLqEsP6/Ad9SDhupcVoOWVrSCNKfRb6jpJbZuxJhCnq8tmlV4iy5tEW0a3VBYzpRoBdAaORWqHQTUlt+iL3aH7C5OxhgN/JuxvxXBL/3kkc0wK1ZNuk+sb4lNXmHnVqQYTcyowQHRPCRsPzCCl4ANULRpZjxAd0xUgg=</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user