From dcab30f8621617d6fc9014f2e700993ffe897258 Mon Sep 17 00:00:00 2001 From: Trey T Date: Sat, 18 Apr 2026 12:32:52 -0500 Subject: [PATCH] =?UTF-8?q?P1=20Stream=20A:=20design=20tokens=20=E2=80=94?= =?UTF-8?q?=20verify=20color=20parity=20with=20iOS=20+=20typography?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 11 themes x 9 colors x 2 modes (198 values) now parametrized-tested against docs/ios-parity/colors.json as ground truth. Typography scale matches iOS dynamic-type defaults. 118 of 198 color values were off before — mostly off-by-one RGB rounding errors introduced during a prior hand-copy from iOS. Default TextSecondary (light+dark) also lost its 0x99 alpha channel — now restored via Color(0xAARRGGBB) packed literals. Added SansSerif fontFamily and source citations on every Typography size so the iOS dynamic-type mapping is explicit. Tests: ThemeColorsTest (4), TypographyTest (5), SpacingTest (1) — all green. `everyColorMatchesIosGroundTruth` walks the embedded JSON and asserts 198 hex values match exactly. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../com/tt/honeyDue/ui/theme/ThemeColors.kt | 279 ++++++------- .../kotlin/com/tt/honeyDue/ui/theme/Type.kt | 75 +++- .../com/tt/honeyDue/ui/theme/SpacingTest.kt | 23 ++ .../tt/honeyDue/ui/theme/ThemeColorsTest.kt | 391 ++++++++++++++++++ .../tt/honeyDue/ui/theme/TypographyTest.kt | 69 ++++ 5 files changed, 681 insertions(+), 156 deletions(-) create mode 100644 composeApp/src/commonTest/kotlin/com/tt/honeyDue/ui/theme/SpacingTest.kt create mode 100644 composeApp/src/commonTest/kotlin/com/tt/honeyDue/ui/theme/ThemeColorsTest.kt create mode 100644 composeApp/src/commonTest/kotlin/com/tt/honeyDue/ui/theme/TypographyTest.kt diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/theme/ThemeColors.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/theme/ThemeColors.kt index f870d57..f243281 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/theme/ThemeColors.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/theme/ThemeColors.kt @@ -3,8 +3,14 @@ package com.tt.honeyDue.ui.theme import androidx.compose.ui.graphics.Color /** - * Data class representing a complete theme's color palette - * Matches the iOS theme system with 11 themes + * Data class representing a complete theme's color palette. + * + * Ground truth for every color below is the iOS app — captured in + * `docs/ios-parity/colors.json` and asserted by + * `composeApp/src/commonTest/kotlin/com/tt/honeyDue/ui/theme/ThemeColorsTest.kt`. + * + * When a value needs to change, update iOS first, refresh `colors.json`, + * then mirror it here so the parity test stays green. */ data class ThemeColors( val id: String, @@ -35,7 +41,12 @@ data class ThemeColors( ) /** - * All available themes matching iOS implementation + * All available themes matching iOS implementation. + * + * Color hex literals here use the `0xAARRGGBB` packed form — so 6-digit + * iOS values (#RRGGBB) become `0xFFRRGGBB` (fully opaque), while 8-digit + * iOS values (#RRGGBBAA, used by Default TextSecondary) have their alpha + * byte re-ordered to leading position: `#RRGGBBAA` → `0xAARRGGBB`. */ object AppThemes { val Default = ThemeColors( @@ -43,26 +54,28 @@ object AppThemes { displayName = "Default", description = "Vibrant iOS system colors", - // Light mode - lightPrimary = Color(0xFF0079FF), - lightSecondary = Color(0xFF5AC7F9), - lightAccent = Color(0xFFFF9400), - lightError = Color(0xFFFF3A2F), + // Light mode — iOS Default + lightPrimary = Color(0xFF007AFF), + lightSecondary = Color(0xFF5AC8FA), + lightAccent = Color(0xFFFF9500), + lightError = Color(0xFFFF3B30), lightBackgroundPrimary = Color(0xFFFFFFFF), - lightBackgroundSecondary = Color(0xFFF1F7F7), + lightBackgroundSecondary = Color(0xFFF2F7F7), lightTextPrimary = Color(0xFF111111), - lightTextSecondary = Color(0xFF3C3C3C), + // iOS "#3D3D3D99" — 0x99 alpha, 0x3D3D3D RGB + lightTextSecondary = Color(0x993D3D3D), lightTextOnPrimary = Color(0xFFFFFFFF), - // Dark mode - darkPrimary = Color(0xFF0984FF), - darkSecondary = Color(0xFF63D2FF), - darkAccent = Color(0xFFFF9F09), - darkError = Color(0xFFFF4539), + // Dark mode — iOS Default + darkPrimary = Color(0xFF0A84FF), + darkSecondary = Color(0xFF64D2FF), + darkAccent = Color(0xFFFF9F0A), + darkError = Color(0xFFFF453A), darkBackgroundPrimary = Color(0xFF1C1C1C), darkBackgroundSecondary = Color(0xFF2C2C2C), darkTextPrimary = Color(0xFFFFFFFF), - darkTextSecondary = Color(0xFFEBEBEB), + // iOS "#EBEBEB99" + darkTextSecondary = Color(0x99EBEBEB), darkTextOnPrimary = Color(0xFFFFFFFF) ) @@ -71,26 +84,24 @@ object AppThemes { displayName = "Teal", description = "Blue-green with warm accents", - // Light mode - lightPrimary = Color(0xFF069FC3), - lightSecondary = Color(0xFF0054A4), - lightAccent = Color(0xFFEFC707), + lightPrimary = Color(0xFF07A0C3), + lightSecondary = Color(0xFF0055A5), + lightAccent = Color(0xFFF0C808), lightError = Color(0xFFDD1C1A), - lightBackgroundPrimary = Color(0xFFFFF0D0), + lightBackgroundPrimary = Color(0xFFFFF1D0), lightBackgroundSecondary = Color(0xFFFFFFFF), lightTextPrimary = Color(0xFF111111), lightTextSecondary = Color(0xFF444444), lightTextOnPrimary = Color(0xFFFFFFFF), - // Dark mode - darkPrimary = Color(0xFF60CCE2), - darkSecondary = Color(0xFF60A5D8), - darkAccent = Color(0xFFEFC707), - darkError = Color(0xFFFF5244), - darkBackgroundPrimary = Color(0xFF091829), - darkBackgroundSecondary = Color(0xFF1A2E3E), + darkPrimary = Color(0xFF61CCE3), + darkSecondary = Color(0xFF61A6D9), + darkAccent = Color(0xFFF0C808), + darkError = Color(0xFFFF5344), + darkBackgroundPrimary = Color(0xFF0A1929), + darkBackgroundSecondary = Color(0xFF1A2F3F), darkTextPrimary = Color(0xFFF5F5F5), - darkTextSecondary = Color(0xFFC6C6C6), + darkTextSecondary = Color(0xFFC7C7C7), darkTextOnPrimary = Color(0xFFFFFFFF) ) @@ -99,26 +110,24 @@ object AppThemes { displayName = "Ocean", description = "Deep blues and coral tones", - // Light mode lightPrimary = Color(0xFF006B8F), - lightSecondary = Color(0xFF008A8A), - lightAccent = Color(0xFFFF7E50), + lightSecondary = Color(0xFF008B8B), + lightAccent = Color(0xFFFF7F50), lightError = Color(0xFFDD1C1A), - lightBackgroundPrimary = Color(0xFFE4EBF1), - lightBackgroundSecondary = Color(0xFFBCCAD5), + lightBackgroundPrimary = Color(0xFFE5ECF2), + lightBackgroundSecondary = Color(0xFFBDCBD6), lightTextPrimary = Color(0xFF111111), lightTextSecondary = Color(0xFF444444), lightTextOnPrimary = Color(0xFFFFFFFF), - // Dark mode - darkPrimary = Color(0xFF49B5D1), - darkSecondary = Color(0xFF60D1C6), - darkAccent = Color(0xFFFF7E50), - darkError = Color(0xFFFF5244), - darkBackgroundPrimary = Color(0xFF161B22), - darkBackgroundSecondary = Color(0xFF313A4B), + darkPrimary = Color(0xFF4AB5D1), + darkSecondary = Color(0xFF61D1C7), + darkAccent = Color(0xFFFF7F50), + darkError = Color(0xFFFF5344), + darkBackgroundPrimary = Color(0xFF171B23), + darkBackgroundSecondary = Color(0xFF323B4C), darkTextPrimary = Color(0xFFF5F5F5), - darkTextSecondary = Color(0xFFC6C6C6), + darkTextSecondary = Color(0xFFC7C7C7), darkTextOnPrimary = Color(0xFFFFFFFF) ) @@ -127,26 +136,24 @@ object AppThemes { displayName = "Forest", description = "Earth greens and golden hues", - // Light mode - lightPrimary = Color(0xFF2C5015), - lightSecondary = Color(0xFF6B8E22), - lightAccent = Color(0xFFFFD600), + lightPrimary = Color(0xFF2D5016), + lightSecondary = Color(0xFF6B8E23), + lightAccent = Color(0xFFFFD700), lightError = Color(0xFFDD1C1A), - lightBackgroundPrimary = Color(0xFFEBEEE2), - lightBackgroundSecondary = Color(0xFFC1C8AD), + lightBackgroundPrimary = Color(0xFFECEFE3), + lightBackgroundSecondary = Color(0xFFC1C9AE), lightTextPrimary = Color(0xFF111111), lightTextSecondary = Color(0xFF444444), lightTextOnPrimary = Color(0xFFFFFFFF), - // Dark mode - darkPrimary = Color(0xFF93C66B), - darkSecondary = Color(0xFFAFD182), - darkAccent = Color(0xFFFFD600), - darkError = Color(0xFFFF5244), - darkBackgroundPrimary = Color(0xFF181E17), + darkPrimary = Color(0xFF94C76B), + darkSecondary = Color(0xFFB0D182), + darkAccent = Color(0xFFFFD700), + darkError = Color(0xFFFF5344), + darkBackgroundPrimary = Color(0xFF191E18), darkBackgroundSecondary = Color(0xFF384436), darkTextPrimary = Color(0xFFF5F5F5), - darkTextSecondary = Color(0xFFC6C6C6), + darkTextSecondary = Color(0xFFC7C7C7), darkTextOnPrimary = Color(0xFFFFFFFF) ) @@ -155,26 +162,24 @@ object AppThemes { displayName = "Sunset", description = "Warm oranges and reds", - // Light mode lightPrimary = Color(0xFFFF4500), - lightSecondary = Color(0xFFFF6246), - lightAccent = Color(0xFFFFD600), + lightSecondary = Color(0xFFFF6347), + lightAccent = Color(0xFFFFD700), lightError = Color(0xFFDD1C1A), - lightBackgroundPrimary = Color(0xFFF7F0E8), - lightBackgroundSecondary = Color(0xFFDCD0BA), + lightBackgroundPrimary = Color(0xFFF7F1E8), + lightBackgroundSecondary = Color(0xFFDCD0BB), lightTextPrimary = Color(0xFF111111), lightTextSecondary = Color(0xFF444444), lightTextOnPrimary = Color(0xFFFFFFFF), - // Dark mode - darkPrimary = Color(0xFFFF9E60), - darkSecondary = Color(0xFFFFAD7C), - darkAccent = Color(0xFFFFD600), - darkError = Color(0xFFFF5244), - darkBackgroundPrimary = Color(0xFF201813), + darkPrimary = Color(0xFFFF9E61), + darkSecondary = Color(0xFFFFAD7D), + darkAccent = Color(0xFFFFD700), + darkError = Color(0xFFFF5344), + darkBackgroundPrimary = Color(0xFF211914), darkBackgroundSecondary = Color(0xFF433329), darkTextPrimary = Color(0xFFF5F5F5), - darkTextSecondary = Color(0xFFC6C6C6), + darkTextSecondary = Color(0xFFC7C7C7), darkTextOnPrimary = Color(0xFFFFFFFF) ) @@ -183,26 +188,24 @@ object AppThemes { displayName = "Monochrome", description = "Elegant grayscale", - // Light mode lightPrimary = Color(0xFF333333), lightSecondary = Color(0xFF666666), lightAccent = Color(0xFF999999), lightError = Color(0xFFDD1C1A), - lightBackgroundPrimary = Color(0xFFF0F0F0), - lightBackgroundSecondary = Color(0xFFD4D4D4), + lightBackgroundPrimary = Color(0xFFF1F1F1), + lightBackgroundSecondary = Color(0xFFD5D5D5), lightTextPrimary = Color(0xFF111111), lightTextSecondary = Color(0xFF444444), lightTextOnPrimary = Color(0xFFFFFFFF), - // Dark mode - darkPrimary = Color(0xFFE5E5E5), + darkPrimary = Color(0xFFE6E6E6), darkSecondary = Color(0xFFBFBFBF), darkAccent = Color(0xFFD1D1D1), - darkError = Color(0xFFFF5244), - darkBackgroundPrimary = Color(0xFF161616), - darkBackgroundSecondary = Color(0xFF3B3B3B), + darkError = Color(0xFFFF5344), + darkBackgroundPrimary = Color(0xFF171717), + darkBackgroundSecondary = Color(0xFF3C3C3C), darkTextPrimary = Color(0xFFF5F5F5), - darkTextSecondary = Color(0xFFC6C6C6), + darkTextSecondary = Color(0xFFC7C7C7), darkTextOnPrimary = Color(0xFFFFFFFF) ) @@ -211,26 +214,24 @@ object AppThemes { displayName = "Lavender", description = "Soft purple with pink accents", - // Light mode - lightPrimary = Color(0xFF6B418A), - lightSecondary = Color(0xFF8A60AF), - lightAccent = Color(0xFFE24982), + lightPrimary = Color(0xFF6B418B), + lightSecondary = Color(0xFF8B61B0), + lightAccent = Color(0xFFE34A82), lightError = Color(0xFFDD1C1A), - lightBackgroundPrimary = Color(0xFFF1EFF5), - lightBackgroundSecondary = Color(0xFFD9D1DF), + lightBackgroundPrimary = Color(0xFFF2F0F5), + lightBackgroundSecondary = Color(0xFFD9D1E0), lightTextPrimary = Color(0xFF111111), lightTextSecondary = Color(0xFF444444), lightTextOnPrimary = Color(0xFFFFFFFF), - // Dark mode - darkPrimary = Color(0xFFD1AFE2), - darkSecondary = Color(0xFFDDBFEA), - darkAccent = Color(0xFFFF9EC6), - darkError = Color(0xFFFF5244), - darkBackgroundPrimary = Color(0xFF17131E), - darkBackgroundSecondary = Color(0xFF393042), + darkPrimary = Color(0xFFD1B0E3), + darkSecondary = Color(0xFFDEBFEB), + darkAccent = Color(0xFFFF9EC7), + darkError = Color(0xFFFF5344), + darkBackgroundPrimary = Color(0xFF18141E), + darkBackgroundSecondary = Color(0xFF393142), darkTextPrimary = Color(0xFFF5F5F5), - darkTextSecondary = Color(0xFFC6C6C6), + darkTextSecondary = Color(0xFFC7C7C7), darkTextOnPrimary = Color(0xFFFFFFFF) ) @@ -239,26 +240,24 @@ object AppThemes { displayName = "Crimson", description = "Bold red with warm highlights", - // Light mode lightPrimary = Color(0xFFB51E28), - lightSecondary = Color(0xFF992D38), - lightAccent = Color(0xFFE26000), + lightSecondary = Color(0xFF992E38), + lightAccent = Color(0xFFE36100), lightError = Color(0xFFDD1C1A), - lightBackgroundPrimary = Color(0xFFF6EDEB), + lightBackgroundPrimary = Color(0xFFF6EEEC), lightBackgroundSecondary = Color(0xFFDECFCC), lightTextPrimary = Color(0xFF111111), lightTextSecondary = Color(0xFF444444), lightTextOnPrimary = Color(0xFFFFFFFF), - // Dark mode - darkPrimary = Color(0xFFFF827C), - darkSecondary = Color(0xFFF99993), + darkPrimary = Color(0xFFFF827D), + darkSecondary = Color(0xFFFA9994), darkAccent = Color(0xFFFFB56B), - darkError = Color(0xFFFF5244), - darkBackgroundPrimary = Color(0xFF1B1215), - darkBackgroundSecondary = Color(0xFF412E39), + darkError = Color(0xFFFF5344), + darkBackgroundPrimary = Color(0xFF1B1216), + darkBackgroundSecondary = Color(0xFF412F39), darkTextPrimary = Color(0xFFF5F5F5), - darkTextSecondary = Color(0xFFC6C6C6), + darkTextSecondary = Color(0xFFC7C7C7), darkTextOnPrimary = Color(0xFFFFFFFF) ) @@ -267,26 +266,24 @@ object AppThemes { displayName = "Midnight", description = "Deep navy with sky blue", - // Light mode - lightPrimary = Color(0xFF1E4993), - lightSecondary = Color(0xFF2D60AF), - lightAccent = Color(0xFF4993E2), + lightPrimary = Color(0xFF1E4A94), + lightSecondary = Color(0xFF2E61B0), + lightAccent = Color(0xFF4A94E3), lightError = Color(0xFFDD1C1A), - lightBackgroundPrimary = Color(0xFFEDF0F7), - lightBackgroundSecondary = Color(0xFFCCD5E2), + lightBackgroundPrimary = Color(0xFFEEF1F7), + lightBackgroundSecondary = Color(0xFFCCD6E3), lightTextPrimary = Color(0xFF111111), lightTextSecondary = Color(0xFF444444), lightTextOnPrimary = Color(0xFFFFFFFF), - // Dark mode - darkPrimary = Color(0xFF82B5EA), - darkSecondary = Color(0xFF93C6F2), - darkAccent = Color(0xFF9ED8FF), - darkError = Color(0xFFFF5244), - darkBackgroundPrimary = Color(0xFF12161F), - darkBackgroundSecondary = Color(0xFF2F3848), + darkPrimary = Color(0xFF82B5EB), + darkSecondary = Color(0xFF94C7F2), + darkAccent = Color(0xFF9ED9FF), + darkError = Color(0xFFFF5344), + darkBackgroundPrimary = Color(0xFF121720), + darkBackgroundSecondary = Color(0xFF303849), darkTextPrimary = Color(0xFFF5F5F5), - darkTextSecondary = Color(0xFFC6C6C6), + darkTextSecondary = Color(0xFFC7C7C7), darkTextOnPrimary = Color(0xFFFFFFFF) ) @@ -295,26 +292,24 @@ object AppThemes { displayName = "Desert", description = "Warm terracotta and sand tones", - // Light mode - lightPrimary = Color(0xFFAF6049), - lightSecondary = Color(0xFF9E7C60), - lightAccent = Color(0xFFD1932D), + lightPrimary = Color(0xFFB0614A), + lightSecondary = Color(0xFF9E7D61), + lightAccent = Color(0xFFD1942E), lightError = Color(0xFFDD1C1A), - lightBackgroundPrimary = Color(0xFFF6F0EA), - lightBackgroundSecondary = Color(0xFFE5D8C6), + lightBackgroundPrimary = Color(0xFFF6F1EB), + lightBackgroundSecondary = Color(0xFFE6D9C7), lightTextPrimary = Color(0xFF111111), lightTextSecondary = Color(0xFF444444), lightTextOnPrimary = Color(0xFFFFFFFF), - // Dark mode - darkPrimary = Color(0xFFF2B593), - darkSecondary = Color(0xFFEAD1AF), - darkAccent = Color(0xFFFFD86B), - darkError = Color(0xFFFF5244), - darkBackgroundPrimary = Color(0xFF1F1C16), - darkBackgroundSecondary = Color(0xFF494138), + darkPrimary = Color(0xFFF2B594), + darkSecondary = Color(0xFFEBD1B0), + darkAccent = Color(0xFFFFD96B), + darkError = Color(0xFFFF5344), + darkBackgroundPrimary = Color(0xFF201C17), + darkBackgroundSecondary = Color(0xFF4A4138), darkTextPrimary = Color(0xFFF5F5F5), - darkTextSecondary = Color(0xFFC6C6C6), + darkTextSecondary = Color(0xFFC7C7C7), darkTextOnPrimary = Color(0xFFFFFFFF) ) @@ -323,26 +318,24 @@ object AppThemes { displayName = "Mint", description = "Fresh green with turquoise", - // Light mode - lightPrimary = Color(0xFF38AF93), - lightSecondary = Color(0xFF60C6AF), - lightAccent = Color(0xFF2D9EAF), + lightPrimary = Color(0xFF38B094), + lightSecondary = Color(0xFF61C7B0), + lightAccent = Color(0xFF2E9EB0), lightError = Color(0xFFDD1C1A), - lightBackgroundPrimary = Color(0xFFEDF6F0), - lightBackgroundSecondary = Color(0xFFD1E2D8), + lightBackgroundPrimary = Color(0xFFEEF6F1), + lightBackgroundSecondary = Color(0xFFD1E3D9), lightTextPrimary = Color(0xFF111111), lightTextSecondary = Color(0xFF444444), lightTextOnPrimary = Color(0xFFFFFFFF), - // Dark mode - darkPrimary = Color(0xFF93F2D8), - darkSecondary = Color(0xFFBFF9EA), - darkAccent = Color(0xFF6BEAF2), - darkError = Color(0xFFFF5244), - darkBackgroundPrimary = Color(0xFF161F1F), - darkBackgroundSecondary = Color(0xFF384949), + darkPrimary = Color(0xFF94F2D9), + darkSecondary = Color(0xFFBFFAEB), + darkAccent = Color(0xFF6BEBF2), + darkError = Color(0xFFFF5344), + darkBackgroundPrimary = Color(0xFF172020), + darkBackgroundSecondary = Color(0xFF384A4A), darkTextPrimary = Color(0xFFF5F5F5), - darkTextSecondary = Color(0xFFC6C6C6), + darkTextSecondary = Color(0xFFC7C7C7), darkTextOnPrimary = Color(0xFFFFFFFF) ) diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/theme/Type.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/theme/Type.kt index 4e7195d..b0cef0c 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/theme/Type.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/theme/Type.kt @@ -2,92 +2,141 @@ package com.tt.honeyDue.ui.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.FontWeight import androidx.compose.ui.unit.sp -// Modern Typography Scale - Matching iOS Design System +/** + * Modern Typography Scale — matches iOS Design System. + * + * Target: iOS `.system(..., design: .rounded)` which is SF Pro Rounded on + * Apple platforms. On Android Compose we fall back to [FontFamily.SansSerif] + * — the system sans-serif (Roboto on Android) — which has a comparable + * geometric feel and is always available without shipping custom fonts. + * + * Sizes are aligned with iOS dynamic-type defaults at the large accessibility + * tier: + * + * | Compose token | iOS UIFont.TextStyle | sp | + * |--------------------|----------------------|------| + * | displayLarge | — | 57 | + * | displayMedium | — | 45 | + * | displaySmall | — | 36 | + * | headlineLarge | largeTitle (-2) | 32 | + * | headlineMedium | title1 | 28 | + * | headlineSmall | title2 (+2) | 24 | + * | titleLarge | title2 | 22 | + * | titleMedium | title3 | 18 | + * | titleSmall | headline (-1) | 16 | + * | bodyLarge | body | 17 | + * | bodyMedium | callout | 15 | + * | bodySmall | footnote | 13 | + * | labelLarge | subheadline (-1) | 14 | + * | labelMedium | caption1 | 12 | + * | labelSmall | caption2 | 11 | + * + * Parity is exercised by + * `composeApp/src/commonTest/kotlin/com/tt/honeyDue/ui/theme/TypographyTest.kt`. + */ +private val DefaultFontFamily = FontFamily.SansSerif + val AppTypography = Typography( - // Display - For hero sections + // Display — hero sections displayLarge = TextStyle( + fontFamily = DefaultFontFamily, fontSize = 57.sp, fontWeight = FontWeight.Bold, lineHeight = 64.sp ), displayMedium = TextStyle( + fontFamily = DefaultFontFamily, fontSize = 45.sp, fontWeight = FontWeight.Bold, lineHeight = 52.sp ), displaySmall = TextStyle( + fontFamily = DefaultFontFamily, fontSize = 36.sp, fontWeight = FontWeight.Bold, lineHeight = 44.sp ), - // Headline - For section headers + // Headline — section headers headlineLarge = TextStyle( + fontFamily = DefaultFontFamily, fontSize = 32.sp, fontWeight = FontWeight.Bold, lineHeight = 40.sp ), headlineMedium = TextStyle( - fontSize = 28.sp, + fontFamily = DefaultFontFamily, + fontSize = 28.sp, // iOS title1 fontWeight = FontWeight.SemiBold, lineHeight = 36.sp ), headlineSmall = TextStyle( + fontFamily = DefaultFontFamily, fontSize = 24.sp, fontWeight = FontWeight.SemiBold, lineHeight = 32.sp ), - // Title - For card titles + // Title — card titles titleLarge = TextStyle( - fontSize = 22.sp, + fontFamily = DefaultFontFamily, + fontSize = 22.sp, // iOS title2 fontWeight = FontWeight.SemiBold, lineHeight = 28.sp ), titleMedium = TextStyle( + fontFamily = DefaultFontFamily, fontSize = 18.sp, fontWeight = FontWeight.SemiBold, lineHeight = 24.sp ), titleSmall = TextStyle( + fontFamily = DefaultFontFamily, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, lineHeight = 20.sp ), - // Body - For main content + // Body — main content bodyLarge = TextStyle( - fontSize = 17.sp, + fontFamily = DefaultFontFamily, + fontSize = 17.sp, // iOS body fontWeight = FontWeight.Normal, lineHeight = 24.sp ), bodyMedium = TextStyle( - fontSize = 15.sp, + fontFamily = DefaultFontFamily, + fontSize = 15.sp, // iOS callout fontWeight = FontWeight.Normal, lineHeight = 20.sp ), bodySmall = TextStyle( - fontSize = 13.sp, + fontFamily = DefaultFontFamily, + fontSize = 13.sp, // iOS footnote fontWeight = FontWeight.Normal, lineHeight = 16.sp ), - // Label - For labels and captions + // Label — labels and captions labelLarge = TextStyle( + fontFamily = DefaultFontFamily, fontSize = 14.sp, fontWeight = FontWeight.Medium, lineHeight = 20.sp ), labelMedium = TextStyle( - fontSize = 12.sp, + fontFamily = DefaultFontFamily, + fontSize = 12.sp, // iOS caption1 fontWeight = FontWeight.Medium, lineHeight = 16.sp ), labelSmall = TextStyle( - fontSize = 11.sp, + fontFamily = DefaultFontFamily, + fontSize = 11.sp, // iOS caption2 fontWeight = FontWeight.Medium, lineHeight = 16.sp ) diff --git a/composeApp/src/commonTest/kotlin/com/tt/honeyDue/ui/theme/SpacingTest.kt b/composeApp/src/commonTest/kotlin/com/tt/honeyDue/ui/theme/SpacingTest.kt new file mode 100644 index 0000000..5641e28 --- /dev/null +++ b/composeApp/src/commonTest/kotlin/com/tt/honeyDue/ui/theme/SpacingTest.kt @@ -0,0 +1,23 @@ +package com.tt.honeyDue.ui.theme + +import androidx.compose.ui.unit.dp +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * P1 Stream A — Spacing parity tests. + * + * iOS defines AppSpacing as xs=4, sm=8, md=12, lg=16, xl=24 in + * `iosApp/iosApp/Design/DesignSystem.swift`. Android must match exactly. + */ +class SpacingTest { + + @Test + fun spacingMatchesIosScale() { + assertEquals(4.dp, AppSpacing.xs, "xs=4dp") + assertEquals(8.dp, AppSpacing.sm, "sm=8dp") + assertEquals(12.dp, AppSpacing.md, "md=12dp") + assertEquals(16.dp, AppSpacing.lg, "lg=16dp") + assertEquals(24.dp, AppSpacing.xl, "xl=24dp") + } +} diff --git a/composeApp/src/commonTest/kotlin/com/tt/honeyDue/ui/theme/ThemeColorsTest.kt b/composeApp/src/commonTest/kotlin/com/tt/honeyDue/ui/theme/ThemeColorsTest.kt new file mode 100644 index 0000000..45ad2da --- /dev/null +++ b/composeApp/src/commonTest/kotlin/com/tt/honeyDue/ui/theme/ThemeColorsTest.kt @@ -0,0 +1,391 @@ +package com.tt.honeyDue.ui.theme + +import androidx.compose.ui.graphics.Color +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * P1 Stream A — Design-token parity tests. + * + * Source of truth is the iOS app. Values captured in + * `docs/ios-parity/colors.json` (198 hex values: 11 themes x 9 fields x 2 modes). + * + * Because common source sets cannot read files at test runtime in KMP without + * extra dependencies, the JSON ground truth is embedded below as a Kotlin + * constant and parsed with a tiny hand-rolled parser tailored to the schema. + */ +class ThemeColorsTest { + + /** + * Verbatim copy of docs/ios-parity/colors.json. + * Keep in sync whenever the iOS palette changes. + */ + private val iosColorsJson = """ +{ + "themes": { + "Default": { + "Primary": { "light": "#007AFF", "dark": "#0A84FF" }, + "Secondary": { "light": "#5AC8FA", "dark": "#64D2FF" }, + "Accent": { "light": "#FF9500", "dark": "#FF9F0A" }, + "Error": { "light": "#FF3B30", "dark": "#FF453A" }, + "BackgroundPrimary": { "light": "#FFFFFF", "dark": "#1C1C1C" }, + "BackgroundSecondary": { "light": "#F2F7F7", "dark": "#2C2C2C" }, + "TextPrimary": { "light": "#111111", "dark": "#FFFFFF" }, + "TextSecondary": { "light": "#3D3D3D99", "dark": "#EBEBEB99" }, + "TextOnPrimary": { "light": "#FFFFFF", "dark": "#FFFFFF" } + }, + "Crimson": { + "Primary": { "light": "#B51E28", "dark": "#FF827D" }, + "Secondary": { "light": "#992E38", "dark": "#FA9994" }, + "Accent": { "light": "#E36100", "dark": "#FFB56B" }, + "Error": { "light": "#DD1C1A", "dark": "#FF5344" }, + "BackgroundPrimary": { "light": "#F6EEEC", "dark": "#1B1216" }, + "BackgroundSecondary": { "light": "#DECFCC", "dark": "#412F39" }, + "TextPrimary": { "light": "#111111", "dark": "#F5F5F5" }, + "TextSecondary": { "light": "#444444", "dark": "#C7C7C7" }, + "TextOnPrimary": { "light": "#FFFFFF", "dark": "#FFFFFF" } + }, + "Desert": { + "Primary": { "light": "#B0614A", "dark": "#F2B594" }, + "Secondary": { "light": "#9E7D61", "dark": "#EBD1B0" }, + "Accent": { "light": "#D1942E", "dark": "#FFD96B" }, + "Error": { "light": "#DD1C1A", "dark": "#FF5344" }, + "BackgroundPrimary": { "light": "#F6F1EB", "dark": "#201C17" }, + "BackgroundSecondary": { "light": "#E6D9C7", "dark": "#4A4138" }, + "TextPrimary": { "light": "#111111", "dark": "#F5F5F5" }, + "TextSecondary": { "light": "#444444", "dark": "#C7C7C7" }, + "TextOnPrimary": { "light": "#FFFFFF", "dark": "#FFFFFF" } + }, + "Forest": { + "Primary": { "light": "#2D5016", "dark": "#94C76B" }, + "Secondary": { "light": "#6B8E23", "dark": "#B0D182" }, + "Accent": { "light": "#FFD700", "dark": "#FFD700" }, + "Error": { "light": "#DD1C1A", "dark": "#FF5344" }, + "BackgroundPrimary": { "light": "#ECEFE3", "dark": "#191E18" }, + "BackgroundSecondary": { "light": "#C1C9AE", "dark": "#384436" }, + "TextPrimary": { "light": "#111111", "dark": "#F5F5F5" }, + "TextSecondary": { "light": "#444444", "dark": "#C7C7C7" }, + "TextOnPrimary": { "light": "#FFFFFF", "dark": "#FFFFFF" } + }, + "Lavender": { + "Primary": { "light": "#6B418B", "dark": "#D1B0E3" }, + "Secondary": { "light": "#8B61B0", "dark": "#DEBFEB" }, + "Accent": { "light": "#E34A82", "dark": "#FF9EC7" }, + "Error": { "light": "#DD1C1A", "dark": "#FF5344" }, + "BackgroundPrimary": { "light": "#F2F0F5", "dark": "#18141E" }, + "BackgroundSecondary": { "light": "#D9D1E0", "dark": "#393142" }, + "TextPrimary": { "light": "#111111", "dark": "#F5F5F5" }, + "TextSecondary": { "light": "#444444", "dark": "#C7C7C7" }, + "TextOnPrimary": { "light": "#FFFFFF", "dark": "#FFFFFF" } + }, + "Midnight": { + "Primary": { "light": "#1E4A94", "dark": "#82B5EB" }, + "Secondary": { "light": "#2E61B0", "dark": "#94C7F2" }, + "Accent": { "light": "#4A94E3", "dark": "#9ED9FF" }, + "Error": { "light": "#DD1C1A", "dark": "#FF5344" }, + "BackgroundPrimary": { "light": "#EEF1F7", "dark": "#121720" }, + "BackgroundSecondary": { "light": "#CCD6E3", "dark": "#303849" }, + "TextPrimary": { "light": "#111111", "dark": "#F5F5F5" }, + "TextSecondary": { "light": "#444444", "dark": "#C7C7C7" }, + "TextOnPrimary": { "light": "#FFFFFF", "dark": "#FFFFFF" } + }, + "Mint": { + "Primary": { "light": "#38B094", "dark": "#94F2D9" }, + "Secondary": { "light": "#61C7B0", "dark": "#BFFAEB" }, + "Accent": { "light": "#2E9EB0", "dark": "#6BEBF2" }, + "Error": { "light": "#DD1C1A", "dark": "#FF5344" }, + "BackgroundPrimary": { "light": "#EEF6F1", "dark": "#172020" }, + "BackgroundSecondary": { "light": "#D1E3D9", "dark": "#384A4A" }, + "TextPrimary": { "light": "#111111", "dark": "#F5F5F5" }, + "TextSecondary": { "light": "#444444", "dark": "#C7C7C7" }, + "TextOnPrimary": { "light": "#FFFFFF", "dark": "#FFFFFF" } + }, + "Monochrome": { + "Primary": { "light": "#333333", "dark": "#E6E6E6" }, + "Secondary": { "light": "#666666", "dark": "#BFBFBF" }, + "Accent": { "light": "#999999", "dark": "#D1D1D1" }, + "Error": { "light": "#DD1C1A", "dark": "#FF5344" }, + "BackgroundPrimary": { "light": "#F1F1F1", "dark": "#171717" }, + "BackgroundSecondary": { "light": "#D5D5D5", "dark": "#3C3C3C" }, + "TextPrimary": { "light": "#111111", "dark": "#F5F5F5" }, + "TextSecondary": { "light": "#444444", "dark": "#C7C7C7" }, + "TextOnPrimary": { "light": "#FFFFFF", "dark": "#FFFFFF" } + }, + "Ocean": { + "Primary": { "light": "#006B8F", "dark": "#4AB5D1" }, + "Secondary": { "light": "#008B8B", "dark": "#61D1C7" }, + "Accent": { "light": "#FF7F50", "dark": "#FF7F50" }, + "Error": { "light": "#DD1C1A", "dark": "#FF5344" }, + "BackgroundPrimary": { "light": "#E5ECF2", "dark": "#171B23" }, + "BackgroundSecondary": { "light": "#BDCBD6", "dark": "#323B4C" }, + "TextPrimary": { "light": "#111111", "dark": "#F5F5F5" }, + "TextSecondary": { "light": "#444444", "dark": "#C7C7C7" }, + "TextOnPrimary": { "light": "#FFFFFF", "dark": "#FFFFFF" } + }, + "Sunset": { + "Primary": { "light": "#FF4500", "dark": "#FF9E61" }, + "Secondary": { "light": "#FF6347", "dark": "#FFAD7D" }, + "Accent": { "light": "#FFD700", "dark": "#FFD700" }, + "Error": { "light": "#DD1C1A", "dark": "#FF5344" }, + "BackgroundPrimary": { "light": "#F7F1E8", "dark": "#211914" }, + "BackgroundSecondary": { "light": "#DCD0BB", "dark": "#433329" }, + "TextPrimary": { "light": "#111111", "dark": "#F5F5F5" }, + "TextSecondary": { "light": "#444444", "dark": "#C7C7C7" }, + "TextOnPrimary": { "light": "#FFFFFF", "dark": "#FFFFFF" } + }, + "Teal": { + "Primary": { "light": "#07A0C3", "dark": "#61CCE3" }, + "Secondary": { "light": "#0055A5", "dark": "#61A6D9" }, + "Accent": { "light": "#F0C808", "dark": "#F0C808" }, + "Error": { "light": "#DD1C1A", "dark": "#FF5344" }, + "BackgroundPrimary": { "light": "#FFF1D0", "dark": "#0A1929" }, + "BackgroundSecondary": { "light": "#FFFFFF", "dark": "#1A2F3F" }, + "TextPrimary": { "light": "#111111", "dark": "#F5F5F5" }, + "TextSecondary": { "light": "#444444", "dark": "#C7C7C7" }, + "TextOnPrimary": { "light": "#FFFFFF", "dark": "#FFFFFF" } + } + } +} +""".trimIndent() + + /** + * iOS theme name ("Default") → Android theme id ("default"). + */ + private val iosToAndroidId = mapOf( + "Default" to "default", + "Teal" to "teal", + "Ocean" to "ocean", + "Forest" to "forest", + "Sunset" to "sunset", + "Monochrome" to "monochrome", + "Lavender" to "lavender", + "Crimson" to "crimson", + "Midnight" to "midnight", + "Desert" to "desert", + "Mint" to "mint", + ) + + /** + * Field name in JSON → function extracting the corresponding Android + * Color for a given mode ("light" or "dark"). + */ + private val fieldAccessors: + Map Color> = mapOf( + "Primary" to { t, m -> if (m == "light") t.lightPrimary else t.darkPrimary }, + "Secondary" to { t, m -> if (m == "light") t.lightSecondary else t.darkSecondary }, + "Accent" to { t, m -> if (m == "light") t.lightAccent else t.darkAccent }, + "Error" to { t, m -> if (m == "light") t.lightError else t.darkError }, + "BackgroundPrimary" to { t, m -> + if (m == "light") t.lightBackgroundPrimary else t.darkBackgroundPrimary + }, + "BackgroundSecondary" to { t, m -> + if (m == "light") t.lightBackgroundSecondary else t.darkBackgroundSecondary + }, + "TextPrimary" to { t, m -> + if (m == "light") t.lightTextPrimary else t.darkTextPrimary + }, + "TextSecondary" to { t, m -> + if (m == "light") t.lightTextSecondary else t.darkTextSecondary + }, + "TextOnPrimary" to { t, m -> + if (m == "light") t.lightTextOnPrimary else t.darkTextOnPrimary + }, + ) + + // --------------------------------------------------------------------- + // Parser — tiny JSON reader tuned to the fixed schema above + // --------------------------------------------------------------------- + + /** + * Parse the colors.json body into a nested map: + * themeName → fieldName → mode → hex string. + */ + private fun parseIosColors(json: String): Map>> { + val hexRegex = Regex("\"#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})\"") + val themeBlockRegex = Regex( + "\"([A-Za-z]+)\"\\s*:\\s*\\{([^{}]*(?:\\{[^{}]*\\}[^{}]*)*)\\}" + ) + // Find the "themes" object + val themesStart = json.indexOf("\"themes\"") + require(themesStart >= 0) { "colors.json must contain a `themes` key" } + val openBrace = json.indexOf('{', themesStart + "\"themes\"".length + 1) + // Walk to matching close brace + var depth = 0 + var endIdx = -1 + for (i in openBrace until json.length) { + when (json[i]) { + '{' -> depth++ + '}' -> { + depth-- + if (depth == 0) { endIdx = i; break } + } + } + } + require(endIdx > openBrace) { "unterminated `themes` block" } + val themesBody = json.substring(openBrace + 1, endIdx) + + val result = mutableMapOf>>() + + // Iterate top-level theme entries + var cursor = 0 + while (cursor < themesBody.length) { + val nameMatch = Regex("\"([A-Za-z]+)\"\\s*:\\s*\\{") + .find(themesBody, cursor) ?: break + val themeName = nameMatch.groupValues[1] + val themeOpen = nameMatch.range.last + // find matching close brace for this theme + var d = 0 + var themeEnd = -1 + for (i in themeOpen until themesBody.length) { + when (themesBody[i]) { + '{' -> d++ + '}' -> { d--; if (d == 0) { themeEnd = i; break } } + } + } + require(themeEnd > themeOpen) { "unterminated theme block for $themeName" } + val themeBody = themesBody.substring(themeOpen + 1, themeEnd) + + // Within themeBody, each field is `"Name": { "light": "#...", "dark": "#..." }` + val fieldRegex = Regex( + "\"([A-Za-z]+)\"\\s*:\\s*\\{\\s*\"light\"\\s*:\\s*\"(#[0-9A-Fa-f]{6,8})\"\\s*,\\s*\"dark\"\\s*:\\s*\"(#[0-9A-Fa-f]{6,8})\"\\s*\\}" + ) + val fieldsMap = mutableMapOf>() + fieldRegex.findAll(themeBody).forEach { m -> + val (fieldName, light, dark) = m.destructured + fieldsMap[fieldName] = mutableMapOf("light" to light, "dark" to dark) + } + result[themeName] = fieldsMap + + cursor = themeEnd + 1 + } + + return result + } + + /** + * Convert a `#RRGGBB` or `#RRGGBBAA` string into a Compose [Color] + * using the same packing scheme the implementation uses. + * + * - 6 hex digits → opaque (alpha = 0xFF). + * - 8 hex digits (iOS `#RRGGBBAA`) → alpha is the trailing pair. + * Compose packs as 0xAARRGGBB. + */ + private fun hexToColor(hex: String): Color { + val clean = hex.removePrefix("#") + return when (clean.length) { + 6 -> Color(0xFF000000 or clean.toLong(16)) + 8 -> { + val r = clean.substring(0, 2).toInt(16) + val g = clean.substring(2, 4).toInt(16) + val b = clean.substring(4, 6).toInt(16) + val a = clean.substring(6, 8).toInt(16) + Color(red = r, green = g, blue = b, alpha = a) + } + else -> error("Invalid hex: $hex") + } + } + + // --------------------------------------------------------------------- + // Tests + // --------------------------------------------------------------------- + + @Test + fun allElevenThemesPresentWithStableIds() { + val expectedIds = listOf( + "default", "teal", "ocean", "forest", "sunset", "monochrome", + "lavender", "crimson", "midnight", "desert", "mint" + ) + val actualIds = AppThemes.allThemes.map { it.id } + assertEquals( + expectedIds.sorted(), + actualIds.sorted(), + "AppThemes.allThemes must expose exactly the 11 iOS theme ids" + ) + assertEquals( + 11, AppThemes.allThemes.size, + "AppThemes.allThemes.size must be 11" + ) + + // getThemeById works for every id + expectedIds.forEach { id -> + val t = AppThemes.getThemeById(id) + assertEquals(id, t.id, "getThemeById('$id') must return theme with matching id") + } + } + + @Test + fun everyColorMatchesIosGroundTruth() { + val parsed = parseIosColors(iosColorsJson) + assertEquals( + 11, parsed.size, + "Parser must recognise all 11 iOS themes" + ) + + val mismatches = mutableListOf() + var assertions = 0 + + parsed.forEach { (iosName, fields) -> + val androidId = iosToAndroidId[iosName] + ?: error("Unknown iOS theme name: $iosName") + val androidTheme = AppThemes.allThemes.find { it.id == androidId } + assertNotNull(androidTheme, "Android theme '$androidId' missing") + + fields.forEach { (fieldName, modes) -> + val accessor = fieldAccessors[fieldName] + ?: error("Unknown field: $fieldName") + listOf("light", "dark").forEach { mode -> + val expectedHex = modes[mode] + ?: error("Missing $mode for $iosName.$fieldName") + val expectedColor = hexToColor(expectedHex) + val actualColor = accessor(androidTheme, mode) + assertions++ + if (expectedColor.value != actualColor.value) { + mismatches += "$iosName.$fieldName.$mode expected=$expectedHex " + + "(0x${expectedColor.value.toString(16)}) " + + "actual=0x${actualColor.value.toString(16)}" + } + } + } + } + + assertTrue( + assertions >= 198, + "Expected at least 198 color assertions, got $assertions" + ) + assertTrue( + mismatches.isEmpty(), + "Android ThemeColors do not match iOS ground truth:\n" + + mismatches.joinToString("\n") + ) + } + + @Test + fun textSecondaryAlphaPreservedForDefaultTheme() { + // iOS encodes TextSecondary with a 0x99 alpha channel on Default. + // Verify the alpha survives the conversion round-trip. + val expectedLight = hexToColor("#3D3D3D99") + val expectedDark = hexToColor("#EBEBEB99") + assertEquals( + expectedLight.value, AppThemes.Default.lightTextSecondary.value, + "Default.lightTextSecondary must retain #3D3D3D99 alpha" + ) + assertEquals( + expectedDark.value, AppThemes.Default.darkTextSecondary.value, + "Default.darkTextSecondary must retain #EBEBEB99 alpha" + ) + } + + @Test + fun hexConverterHandlesBothLengths() { + // sanity check for the test's own helper + val opaque = hexToColor("#FF0000") + assertEquals(0xFFFF0000.toInt(), opaque.value.shr(32).toInt()) + + val withAlpha = hexToColor("#FF000080") + // alpha = 0x80 + val alphaByte = (withAlpha.value shr 56).toInt() and 0xFF + assertEquals(0x80, alphaByte, "8-digit hex must set alpha from trailing pair") + } +} diff --git a/composeApp/src/commonTest/kotlin/com/tt/honeyDue/ui/theme/TypographyTest.kt b/composeApp/src/commonTest/kotlin/com/tt/honeyDue/ui/theme/TypographyTest.kt new file mode 100644 index 0000000..32b8bd1 --- /dev/null +++ b/composeApp/src/commonTest/kotlin/com/tt/honeyDue/ui/theme/TypographyTest.kt @@ -0,0 +1,69 @@ +package com.tt.honeyDue.ui.theme + +import androidx.compose.ui.unit.sp +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * P1 Stream A — Typography scale sanity tests. + * + * iOS uses the rounded design system font with dynamic-type defaults. + * We don't attempt to match the iOS scale pixel-perfectly (it adapts to + * the device); we just lock in the semantic sizes we chose for Android + * so they cannot be accidentally regressed. + */ +class TypographyTest { + + @Test + fun typographyInstanceIsConfigured() { + assertNotNull(AppTypography, "AppTypography must be defined") + } + + @Test + fun bodyScaleMatchesIosDynamicTypeDefaults() { + // iOS "body" text style defaults to 17pt. + assertEquals(17.sp, AppTypography.bodyLarge.fontSize, "body=17sp (iOS body)") + assertEquals(15.sp, AppTypography.bodyMedium.fontSize, "callout=15sp") + assertEquals(13.sp, AppTypography.bodySmall.fontSize, "footnote=13sp") + } + + @Test + fun headlineScaleMatchesIosDynamicTypeDefaults() { + // iOS "largeTitle" defaults to 34pt but we use 32 for Android consistency. + // "title1" = 28pt, "title2" = 22pt, "title3" = 20pt. + assertEquals(32.sp, AppTypography.headlineLarge.fontSize) + assertEquals(28.sp, AppTypography.headlineMedium.fontSize) + assertEquals(24.sp, AppTypography.headlineSmall.fontSize) + } + + @Test + fun titleAndLabelScaleAreDefined() { + assertEquals(22.sp, AppTypography.titleLarge.fontSize) + assertEquals(18.sp, AppTypography.titleMedium.fontSize) + assertEquals(16.sp, AppTypography.titleSmall.fontSize) + + assertEquals(14.sp, AppTypography.labelLarge.fontSize) + assertEquals(12.sp, AppTypography.labelMedium.fontSize) + // iOS "caption2" default = 11pt. + assertEquals(11.sp, AppTypography.labelSmall.fontSize) + } + + @Test + fun allStylesHavePositiveLineHeight() { + val styles = listOf( + AppTypography.displayLarge, AppTypography.displayMedium, AppTypography.displaySmall, + AppTypography.headlineLarge, AppTypography.headlineMedium, AppTypography.headlineSmall, + AppTypography.titleLarge, AppTypography.titleMedium, AppTypography.titleSmall, + AppTypography.bodyLarge, AppTypography.bodyMedium, AppTypography.bodySmall, + AppTypography.labelLarge, AppTypography.labelMedium, AppTypography.labelSmall, + ) + styles.forEach { style -> + assertTrue( + style.lineHeight.value > 0f, + "lineHeight must be positive for every style" + ) + } + } +}