catalyst-core 0.1.0-canary.2 → 0.1.0-canary.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/changelog.md +9 -0
  2. package/dist/native/androidProject/app/build.gradle.kts +14 -4
  3. package/dist/native/androidProject/app/proguard-rules.pro +25 -0
  4. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +10 -0
  5. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +204 -0
  6. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/design/DesignTokens.kt +307 -0
  7. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/EmulatorDetector.kt +203 -0
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/FridaDetector.kt +187 -0
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityAlertHandler.kt +170 -0
  10. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityAlertUI.kt +366 -0
  11. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityBottomSheet.kt +138 -0
  12. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityCheckManager.kt +511 -0
  13. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityCheckScheduler.kt +68 -0
  14. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +12 -1
  15. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/DeviceInfoUtils.kt +12 -0
  16. package/dist/native/androidProject/app/src/main/res/values/themes.xml +12 -0
  17. package/dist/native/androidProject/gradle/libs.versions.toml +5 -1
  18. package/dist/native/bridge/constants/NativeInterfaces.js +6 -3
  19. package/dist/native/bridge/errors.js +1 -1
  20. package/dist/native/bridge/hooks.js +6 -2
  21. package/dist/native/bridge/utils/NativeBridge.js +5 -1
  22. package/dist/native/buildAppIos.js +11 -8
  23. package/dist/native/iosnativeWebView/Package.swift +5 -2
  24. package/dist/native/iosnativeWebView/Sources/CatalystNotifications/NotificationManager.swift +1 -1
  25. package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +3 -0
  26. package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +7 -0
  27. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +264 -0
  28. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +18 -0
  29. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +2 -0
  30. package/dist/native/iosnativeWebView/iosnativeWebView/AppDelegate.swift +13 -0
  31. package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebView.entitlements +4 -0
  32. package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebViewApp.swift +10 -0
  33. package/package.json +1 -1
  34. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/LegacyGoogleSignInPlugin.kt +0 -176
  35. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/README.md +0 -97
package/changelog.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.0-canary.4] - 2026-02-12
4
+
5
+ - Added Google Sign-In support for both Android and iOS in Catalyst, enabling a unified native authentication experience for apps built on the framework.
6
+ - Improved release/build reliability with related bridge and CI updates, making integration smoother and more consistent across platforms.
7
+
8
+ ## [0.1.0-canary.3] - 2026-02-04
9
+
10
+ - Android device security: added root, emulator, and Frida detection with launch-time checks and a Material Design 3 bottom sheet threat alert UI
11
+
3
12
  ## [0.1.0-canary.2] - 2026-01-28
4
13
 
5
14
  - Adds mono-repository support for catalyst
@@ -114,12 +114,12 @@ android {
114
114
  }
115
115
 
116
116
  compileOptions {
117
- sourceCompatibility = JavaVersion.VERSION_1_8
118
- targetCompatibility = JavaVersion.VERSION_1_8
117
+ sourceCompatibility = JavaVersion.VERSION_11
118
+ targetCompatibility = JavaVersion.VERSION_11
119
119
  }
120
120
 
121
121
  kotlinOptions {
122
- jvmTarget = "1.8"
122
+ jvmTarget = "11"
123
123
  }
124
124
 
125
125
  buildFeatures {
@@ -136,6 +136,10 @@ android {
136
136
  excludes.add("META-INF/INDEX.LIST")
137
137
  excludes.add("META-INF/io.netty.versions.properties")
138
138
  }
139
+ jniLibs {
140
+ // Enable 16KB page size alignment for all native libraries
141
+ useLegacyPackaging = false
142
+ }
139
143
  }
140
144
 
141
145
  // Configure lint options
@@ -167,14 +171,20 @@ dependencies {
167
171
  androidTestImplementation(libs.androidx.junit)
168
172
  androidTestImplementation(libs.androidx.espresso.core)
169
173
  implementation(libs.androidx.webkit)
174
+ implementation(libs.androidx.credentials)
175
+ implementation(libs.androidx.credentials.play.services.auth)
176
+ implementation(libs.googleid)
170
177
  implementation("org.json:json:20231013")
171
-
172
178
  // Ktor Server dependencies for FrameworkServer (~200KB total)
173
179
  implementation("io.ktor:ktor-server-core:3.0.3")
174
180
  implementation("io.ktor:ktor-server-netty:3.0.3")
175
181
  implementation("io.ktor:ktor-server-content-negotiation:3.0.3")
176
182
  implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
177
183
 
184
+ // Security detection dependencies
185
+ implementation("com.scottyab:rootbeer-lib:0.1.1") // Root detection
186
+ implementation("com.google.android.play:integrity:1.3.0") // Play Integrity API
187
+
178
188
  // SLF4J simple logger for Ktor (optional, can be excluded if needed)
179
189
  implementation("org.slf4j:slf4j-simple:2.0.9")
180
190
 
@@ -59,6 +59,31 @@
59
59
  -keep class io.yourname.androidproject.WebCacheManager$CacheEntry { *; }
60
60
  -keep class io.yourname.androidproject.WebCacheManager$CacheMetadata { *; }
61
61
 
62
+ # Keep RootBeer library for root detection
63
+ -keep class com.scottyab.rootbeer.** { *; }
64
+ -dontwarn com.scottyab.rootbeer.**
65
+
66
+ # Security: keep only the public API surface that MainActivity wires up.
67
+ # Detection internals (EmulatorDetector, FridaDetector) and UI classes
68
+ # (SecurityBottomSheet, SecurityAlertUI) are intentionally obfuscated.
69
+ -keep class io.yourname.androidproject.security.SecurityCheckScheduler {
70
+ public static void initialize(android.content.Context, kotlinx.coroutines.CoroutineScope, io.yourname.androidproject.security.SecurityCheckScheduler$SecurityCheckCallback);
71
+ }
72
+ -keep interface io.yourname.androidproject.security.SecurityCheckScheduler$SecurityCheckCallback {
73
+ public void onSecurityCheckComplete(org.json.JSONObject);
74
+ }
75
+ -keep class io.yourname.androidproject.security.SecurityCheckManager {
76
+ public static org.json.JSONObject getLatestSecurityResults(android.content.Context);
77
+ public static java.lang.String getSecurityMode(android.content.Context);
78
+ }
79
+ -keep class io.yourname.androidproject.security.SecurityAlertHandler {
80
+ public static void handleSecurityCheckResults(android.app.Activity, org.json.JSONObject);
81
+ }
82
+
83
+ # Keep Play Integrity API
84
+ -keep class com.google.android.play.core.integrity.** { *; }
85
+ -dontwarn com.google.android.play.core.integrity.**
86
+
62
87
  # Remove debug logging in release builds
63
88
  -assumenosideeffects class android.util.Log {
64
89
  public static *** d(...);
@@ -20,6 +20,7 @@ import io.yourname.androidproject.utils.NetworkUtils
20
20
  import io.yourname.androidproject.utils.NotificationConstants
21
21
  import io.yourname.androidproject.utils.SafeAreaInsets
22
22
  import io.yourname.androidproject.utils.SafeAreaUtils
23
+ import io.yourname.androidproject.security.SecurityCheckScheduler
23
24
  import kotlinx.coroutines.CoroutineScope
24
25
  import kotlinx.coroutines.MainScope
25
26
  import kotlinx.coroutines.cancelChildren
@@ -253,6 +254,15 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
253
254
 
254
255
  configureEdgeToEdge()
255
256
 
257
+ // Initialize security checks (release builds only)
258
+ if (!BuildConfig.DEBUG) {
259
+ SecurityCheckScheduler.initialize(this, this, object : SecurityCheckScheduler.SecurityCheckCallback {
260
+ override fun onSecurityCheckComplete(results: JSONObject) {
261
+ io.yourname.androidproject.security.SecurityAlertHandler.handleSecurityCheckResults(this@MainActivity, results)
262
+ }
263
+ })
264
+ }
265
+
256
266
  // Initialize MetricsMonitor
257
267
  metricsMonitor = MetricsMonitor.getInstance(this)
258
268
 
@@ -10,12 +10,19 @@ import android.webkit.WebView
10
10
  import androidx.activity.result.ActivityResultLauncher
11
11
  import androidx.activity.result.contract.ActivityResultContracts
12
12
  import androidx.core.app.NotificationCompat
13
+ import androidx.credentials.CredentialManager
14
+ import androidx.credentials.GetCredentialRequest
15
+ import androidx.credentials.exceptions.GetCredentialCancellationException
16
+ import androidx.credentials.exceptions.GetCredentialException
17
+ import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
18
+ import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
13
19
  import io.yourname.androidproject.MainActivity
14
20
  import io.yourname.androidproject.utils.*
15
21
  import kotlinx.coroutines.*
16
22
  import org.json.JSONArray
17
23
  import org.json.JSONObject
18
24
  import java.util.Properties
25
+ import java.util.concurrent.atomic.AtomicBoolean
19
26
 
20
27
  private data class FilePickerOptions(
21
28
  val mimeType: String = "*/*",
@@ -86,6 +93,38 @@ private fun JSONObject.optIntNullable(key: String): Int? =
86
93
  private fun JSONObject.optLongNullable(key: String): Long? =
87
94
  if (has(key) && !isNull(key)) getLong(key) else null
88
95
 
96
+ private data class GoogleSignInOptions(
97
+ val clientId: String,
98
+ val nonce: String? = null,
99
+ val autoSelect: Boolean = false,
100
+ val filterByAuthorizedAccounts: Boolean = false
101
+ ) {
102
+ companion object {
103
+ fun fromRaw(raw: String?): GoogleSignInOptions {
104
+ if (raw.isNullOrBlank()) {
105
+ // Allow fallback to config-supplied clientId
106
+ return GoogleSignInOptions(clientId = "")
107
+ }
108
+
109
+ return try {
110
+ val json = JSONObject(raw)
111
+ val resolvedClientId = json.optString("clientId")
112
+ .ifBlank { json.optString("webClientId") }
113
+
114
+ GoogleSignInOptions(
115
+ clientId = resolvedClientId,
116
+ nonce = json.optString("nonce").takeIf { it.isNotBlank() },
117
+ autoSelect = json.optBoolean("autoSelect", false),
118
+ filterByAuthorizedAccounts = json.optBoolean("filterByAuthorizedAccounts", false)
119
+ )
120
+ } catch (e: Exception) {
121
+ BridgeUtils.logDebug("NativeBridge", "Failed to parse options JSON: ${e.message}")
122
+ GoogleSignInOptions(clientId = raw.trim())
123
+ }
124
+ }
125
+ }
126
+ }
127
+
89
128
  class NativeBridge(
90
129
  private val mainActivity: MainActivity,
91
130
  private val webView: WebView,
@@ -104,6 +143,12 @@ class NativeBridge(
104
143
  private lateinit var permissionLauncher: ActivityResultLauncher<String>
105
144
  private lateinit var filePickerLauncher: ActivityResultLauncher<Intent>
106
145
  private var currentFilePickerOptions: FilePickerOptions = FilePickerOptions()
146
+ private val isGoogleSignInInProgress = AtomicBoolean(false)
147
+ private val credentialManager: CredentialManager by lazy { CredentialManager.create(mainActivity) }
148
+ private val isGoogleSignInEnabled: Boolean by lazy {
149
+ properties.getProperty("googleSignIn.enabled", "false").toBoolean()
150
+ }
151
+
107
152
  private var networkMonitor: NetworkMonitor? = null
108
153
 
109
154
  // Unified notification manager
@@ -111,6 +156,7 @@ class NativeBridge(
111
156
 
112
157
  companion object {
113
158
  private const val TAG = "NativeBridge"
159
+ private const val GOOGLE_SIGN_IN_SAFE_EXECUTE_ACTION = "start Google Sign-In"
114
160
 
115
161
  /**
116
162
  * Parse and validate bridge message
@@ -237,6 +283,55 @@ class NativeBridge(
237
283
  }
238
284
  }
239
285
 
286
+ @JavascriptInterface
287
+ fun googleSignIn(optionsRaw: String?) {
288
+ BridgeUtils.safeExecute(
289
+ webView,
290
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_ERROR,
291
+ GOOGLE_SIGN_IN_SAFE_EXECUTE_ACTION
292
+ ) {
293
+ if (!isGoogleSignInEnabled) {
294
+ BridgeUtils.notifyWebError(
295
+ webView,
296
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_ERROR,
297
+ "Google Sign-In is disabled in config"
298
+ )
299
+ return@safeExecute
300
+ }
301
+
302
+ val options = GoogleSignInOptions.fromRaw(optionsRaw)
303
+
304
+ val resolvedClientId = when {
305
+ options.clientId.isNotBlank() -> options.clientId
306
+ !properties.getProperty("googleSignIn.clientId", "").isNullOrBlank() ->
307
+ properties.getProperty("googleSignIn.clientId", "")
308
+ else -> ""
309
+ }
310
+
311
+ if (resolvedClientId.isBlank()) {
312
+ BridgeUtils.notifyWebError(
313
+ webView,
314
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_ERROR,
315
+ "clientId is required for Google Sign-In"
316
+ )
317
+ return@safeExecute
318
+ }
319
+
320
+ if (!isGoogleSignInInProgress.compareAndSet(false, true)) {
321
+ BridgeUtils.notifyWebError(
322
+ webView,
323
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_ERROR,
324
+ "A Google Sign-In request is already in progress"
325
+ )
326
+ return@safeExecute
327
+ }
328
+
329
+ BridgeUtils.logDebug(TAG, "Google Sign-In using clientId prefix: ${resolvedClientId.take(12)}...")
330
+
331
+ launchGoogleSignIn(options.copy(clientId = resolvedClientId))
332
+ }
333
+ }
334
+
240
335
  @JavascriptInterface
241
336
  fun openFileWithIntent(params: String?) {
242
337
  BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_INTENT_ERROR, "open file with intent") {
@@ -626,6 +721,115 @@ class NativeBridge(
626
721
  }
627
722
  }
628
723
  }
724
+
725
+ private fun launchGoogleSignIn(options: GoogleSignInOptions) {
726
+ launch {
727
+ try {
728
+ val googleOptionBuilder = GetSignInWithGoogleOption.Builder(options.clientId)
729
+ options.nonce?.let { googleOptionBuilder.setNonce(it) }
730
+ val googleOption = googleOptionBuilder.build()
731
+
732
+ val request = GetCredentialRequest.Builder()
733
+ .addCredentialOption(googleOption)
734
+ .build()
735
+
736
+ BridgeUtils.logDebug(
737
+ TAG,
738
+ "Starting Google Sign-In via CredentialManager (hasNonce=${options.nonce != null})"
739
+ )
740
+
741
+ val response = credentialManager.getCredential(mainActivity, request)
742
+ handleCredentialResponse(response)
743
+ } catch (e: GetCredentialCancellationException) {
744
+ isGoogleSignInInProgress.set(false)
745
+ BridgeUtils.logDebug(
746
+ TAG,
747
+ "Google Sign-In cancelled: ${e.message} :: ${e.javaClass.simpleName}"
748
+ )
749
+ BridgeUtils.notifyWeb(
750
+ webView,
751
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_CANCELLED,
752
+ "User cancelled Google sign-in"
753
+ )
754
+ } catch (e: GetCredentialException) {
755
+ isGoogleSignInInProgress.set(false)
756
+ val message = e.errorMessage?.toString() ?: e.message ?: "Credential request failed"
757
+ BridgeUtils.logError(
758
+ TAG,
759
+ "Google Sign-In credential error: $message :: ${e.javaClass.simpleName}",
760
+ e
761
+ )
762
+ BridgeUtils.notifyWebError(
763
+ webView,
764
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_ERROR,
765
+ message
766
+ )
767
+ } catch (e: Exception) {
768
+ isGoogleSignInInProgress.set(false)
769
+ BridgeUtils.logError(
770
+ TAG,
771
+ "Google Sign-In unexpected error: ${e.message} :: ${e.javaClass.simpleName}",
772
+ e
773
+ )
774
+ BridgeUtils.notifyWebError(
775
+ webView,
776
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_ERROR,
777
+ "Error preparing Google sign-in: ${e.message}"
778
+ )
779
+ }
780
+ }
781
+ }
782
+
783
+ private fun handleCredentialResponse(response: androidx.credentials.GetCredentialResponse) {
784
+ try {
785
+ val credential = response.credential
786
+ if (credential.type != GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
787
+ BridgeUtils.logError(TAG, "Unexpected credential type: ${credential.type}")
788
+ isGoogleSignInInProgress.set(false)
789
+ BridgeUtils.notifyWebError(
790
+ webView,
791
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_ERROR,
792
+ "Unsupported credential type returned"
793
+ )
794
+ return
795
+ }
796
+
797
+ val googleCredential = GoogleIdTokenCredential.createFrom(credential.data)
798
+ val idToken = googleCredential.idToken
799
+
800
+ if (idToken.isNullOrBlank()) {
801
+ isGoogleSignInInProgress.set(false)
802
+ BridgeUtils.notifyWebError(
803
+ webView,
804
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_ERROR,
805
+ "No ID token returned from Google sign-in"
806
+ )
807
+ return
808
+ }
809
+
810
+ val payload = JSONObject().apply {
811
+ put("idToken", idToken)
812
+ googleCredential.id?.let { put("email", it) }
813
+ googleCredential.displayName?.let { put("name", it) }
814
+ googleCredential.profilePictureUri?.toString()?.let { put("photoUrl", it) }
815
+ put("provider", "google")
816
+ }
817
+
818
+ isGoogleSignInInProgress.set(false)
819
+ BridgeUtils.notifyWebJson(
820
+ webView,
821
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_SUCCESS,
822
+ payload
823
+ )
824
+ } catch (e: Exception) {
825
+ isGoogleSignInInProgress.set(false)
826
+ BridgeUtils.notifyWebError(
827
+ webView,
828
+ BridgeUtils.WebEvents.ON_GOOGLE_SIGN_IN_ERROR,
829
+ "Error handling Google credential: ${e.message}"
830
+ )
831
+ }
832
+ }
629
833
 
630
834
  private fun processSelectedFiles(uris: List<Uri>, options: FilePickerOptions) {
631
835
  launch {
@@ -0,0 +1,307 @@
1
+ package io.yourname.androidproject.design
2
+
3
+ import android.content.Context
4
+ import android.content.res.Configuration
5
+ import androidx.annotation.ColorInt
6
+
7
+ /**
8
+ * Global design tokens for the application.
9
+ * Follows Material Design 3 principles with support for light/dark themes.
10
+ *
11
+ * All spacing follows 8dp grid system.
12
+ * Color system supports dynamic theming based on system configuration.
13
+ */
14
+ object DesignTokens {
15
+
16
+ // ============================================================
17
+ // COLORS - Light Theme
18
+ // ============================================================
19
+ object LightColors {
20
+ // Surface colors
21
+ @ColorInt const val SURFACE = 0xFFFFFFFF.toInt() // Pure white
22
+ @ColorInt const val SURFACE_VARIANT = 0xFFF5F5F5.toInt() // Light gray background
23
+
24
+ // Error colors (for alerts, warnings)
25
+ @ColorInt const val ERROR = 0xFFB3261E.toInt() // Material error red
26
+ @ColorInt const val ERROR_CONTAINER = 0xFFF9DEDC.toInt() // Light red background
27
+ @ColorInt const val ON_ERROR = 0xFFFFFFFF.toInt() // White text on error
28
+ @ColorInt const val ON_ERROR_CONTAINER = 0xFF410E0B.toInt() // Dark red text
29
+
30
+ // Text colors
31
+ @ColorInt const val ON_SURFACE = 0xFF1C1B1F.toInt() // Primary text (near black)
32
+ @ColorInt const val ON_SURFACE_VARIANT = 0xFF49454F.toInt() // Secondary text (gray)
33
+ @ColorInt const val OUTLINE = 0xFF79747E.toInt() // Dividers, borders
34
+
35
+ // Accent colors
36
+ @ColorInt const val PRIMARY = 0xFF6750A4.toInt() // Material purple
37
+ @ColorInt const val ON_PRIMARY = 0xFFFFFFFF.toInt() // White text on primary
38
+ @ColorInt const val PRIMARY_CONTAINER = 0xFFEADDFF.toInt() // Light purple background
39
+ @ColorInt const val ON_PRIMARY_CONTAINER = 0xFF21005D.toInt() // Dark purple text
40
+
41
+ // Secondary colors
42
+ @ColorInt const val SECONDARY = 0xFF625B71.toInt() // Material gray-purple
43
+ @ColorInt const val ON_SECONDARY = 0xFFFFFFFF.toInt() // White text on secondary
44
+ @ColorInt const val SECONDARY_CONTAINER = 0xFFE8DEF8.toInt() // Light secondary background
45
+ @ColorInt const val ON_SECONDARY_CONTAINER = 0xFF1D192B.toInt() // Dark text
46
+ }
47
+
48
+ // ============================================================
49
+ // COLORS - Dark Theme
50
+ // ============================================================
51
+ object DarkColors {
52
+ // Surface colors
53
+ @ColorInt const val SURFACE = 0xFF1C1B1F.toInt() // Dark surface
54
+ @ColorInt const val SURFACE_VARIANT = 0xFF2B2930.toInt() // Darker variant
55
+
56
+ // Error colors (for alerts, warnings)
57
+ @ColorInt const val ERROR = 0xFFF2B8B5.toInt() // Light red for dark mode
58
+ @ColorInt const val ERROR_CONTAINER = 0xFF8C1D18.toInt() // Dark red background
59
+ @ColorInt const val ON_ERROR = 0xFF601410.toInt() // Dark text on light error
60
+ @ColorInt const val ON_ERROR_CONTAINER = 0xFFF2B8B5.toInt() // Light text on dark error
61
+
62
+ // Text colors
63
+ @ColorInt const val ON_SURFACE = 0xFFE6E1E5.toInt() // Light text
64
+ @ColorInt const val ON_SURFACE_VARIANT = 0xFFCAC4D0.toInt() // Secondary light text
65
+ @ColorInt const val OUTLINE = 0xFF938F99.toInt() // Light dividers, borders
66
+
67
+ // Accent colors
68
+ @ColorInt const val PRIMARY = 0xFFD0BCFF.toInt() // Light purple for dark mode
69
+ @ColorInt const val ON_PRIMARY = 0xFF381E72.toInt() // Dark text on light primary
70
+ @ColorInt const val PRIMARY_CONTAINER = 0xFF4F378B.toInt() // Dark purple background
71
+ @ColorInt const val ON_PRIMARY_CONTAINER = 0xFFEADDFF.toInt() // Light text
72
+
73
+ // Secondary colors
74
+ @ColorInt const val SECONDARY = 0xFFCCC2DC.toInt() // Light gray-purple for dark
75
+ @ColorInt const val ON_SECONDARY = 0xFF332D41.toInt() // Dark text on light secondary
76
+ @ColorInt const val SECONDARY_CONTAINER = 0xFF4A4458.toInt() // Dark secondary background
77
+ @ColorInt const val ON_SECONDARY_CONTAINER = 0xFFE8DEF8.toInt() // Light text
78
+ }
79
+
80
+ // ============================================================
81
+ // SPACING - Following 8dp Grid System
82
+ // ============================================================
83
+ object Spacing {
84
+ const val SPACE_XXS = 4 // 4dp - Minimal spacing
85
+ const val SPACE_XS = 8 // 8dp - Tight spacing
86
+ const val SPACE_SM = 12 // 12dp - Small spacing
87
+ const val SPACE_MD = 16 // 16dp - Medium spacing (default)
88
+ const val SPACE_LG = 24 // 24dp - Large spacing
89
+ const val SPACE_XL = 32 // 32dp - Extra large spacing
90
+ const val SPACE_XXL = 48 // 48dp - Maximum spacing
91
+ }
92
+
93
+ // ============================================================
94
+ // DIMENSIONS
95
+ // ============================================================
96
+ object Dimensions {
97
+ // Corner radius
98
+ const val CORNER_RADIUS_SM = 8 // Small corners (buttons, chips)
99
+ const val CORNER_RADIUS_MD = 16 // Medium corners (cards)
100
+ const val CORNER_RADIUS_LG = 24 // Large corners (bottom sheet top)
101
+ const val CORNER_RADIUS_XL = 28 // Extra large (full rounded)
102
+
103
+ // Icon sizes
104
+ const val ICON_SIZE_SM = 24 // Small icons
105
+ const val ICON_SIZE_MD = 48 // Medium icons
106
+ const val ICON_SIZE_LG = 64 // Large icons (hero icon)
107
+ const val ICON_SIZE_XL = 96 // Extra large icons
108
+
109
+ // Minimum touch target (Material Design)
110
+ const val MIN_TOUCH_TARGET = 48 // 48dp minimum for accessibility
111
+
112
+ // Elevation (for shadows/depth)
113
+ const val ELEVATION_NONE = 0f
114
+ const val ELEVATION_SM = 2f // Small elevation
115
+ const val ELEVATION_MD = 4f // Medium elevation
116
+ const val ELEVATION_LG = 8f // Large elevation
117
+ }
118
+
119
+ // ============================================================
120
+ // TYPOGRAPHY
121
+ // ============================================================
122
+ object Typography {
123
+ // Text sizes (SP - scalable pixels)
124
+ const val TEXT_SIZE_DISPLAY = 32f // Large headlines
125
+ const val TEXT_SIZE_HEADLINE = 24f // Section headlines
126
+ const val TEXT_SIZE_TITLE = 20f // Card titles
127
+ const val TEXT_SIZE_BODY_LG = 16f // Large body text
128
+ const val TEXT_SIZE_BODY = 14f // Default body text
129
+ const val TEXT_SIZE_LABEL = 12f // Small labels, captions
130
+
131
+ // Line height multipliers
132
+ const val LINE_HEIGHT_TIGHT = 1.2f // Tight line height
133
+ const val LINE_HEIGHT_NORMAL = 1.5f // Normal line height
134
+ const val LINE_HEIGHT_RELAXED = 1.7f // Relaxed line height
135
+
136
+ // Font weights (Android Typeface)
137
+ const val FONT_WEIGHT_NORMAL = android.graphics.Typeface.NORMAL
138
+ const val FONT_WEIGHT_BOLD = android.graphics.Typeface.BOLD
139
+ }
140
+
141
+ // ============================================================
142
+ // ANIMATION DURATIONS (milliseconds)
143
+ // ============================================================
144
+ object Animation {
145
+ const val DURATION_SHORT = 200L // Quick transitions
146
+ const val DURATION_MEDIUM = 300L // Standard transitions
147
+ const val DURATION_LONG = 500L // Emphasized transitions
148
+ const val DURATION_EXTRA_LONG = 700L // Hero transitions
149
+ }
150
+
151
+ // ============================================================
152
+ // OPACITY/ALPHA VALUES
153
+ // ============================================================
154
+ object Opacity {
155
+ const val DISABLED = 0.38f // Disabled state
156
+ const val MEDIUM = 0.60f // Medium emphasis
157
+ const val HIGH = 0.87f // High emphasis
158
+ const val FULL = 1.0f // Full opacity
159
+ }
160
+
161
+ // ============================================================
162
+ // HELPER FUNCTIONS
163
+ // ============================================================
164
+
165
+ /**
166
+ * Check if the device is in dark mode.
167
+ *
168
+ * @param context Android context
169
+ * @return true if dark mode is enabled
170
+ */
171
+ fun isDarkMode(context: Context): Boolean {
172
+ val nightModeFlags = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
173
+ return nightModeFlags == Configuration.UI_MODE_NIGHT_YES
174
+ }
175
+
176
+ /**
177
+ * Get surface color based on current theme.
178
+ */
179
+ @ColorInt
180
+ fun getSurfaceColor(context: Context): Int {
181
+ return if (isDarkMode(context)) DarkColors.SURFACE else LightColors.SURFACE
182
+ }
183
+
184
+ /**
185
+ * Get surface variant color based on current theme.
186
+ */
187
+ @ColorInt
188
+ fun getSurfaceVariantColor(context: Context): Int {
189
+ return if (isDarkMode(context)) DarkColors.SURFACE_VARIANT else LightColors.SURFACE_VARIANT
190
+ }
191
+
192
+ /**
193
+ * Get error color based on current theme.
194
+ */
195
+ @ColorInt
196
+ fun getErrorColor(context: Context): Int {
197
+ return if (isDarkMode(context)) DarkColors.ERROR else LightColors.ERROR
198
+ }
199
+
200
+ /**
201
+ * Get error container color based on current theme.
202
+ */
203
+ @ColorInt
204
+ fun getErrorContainerColor(context: Context): Int {
205
+ return if (isDarkMode(context)) DarkColors.ERROR_CONTAINER else LightColors.ERROR_CONTAINER
206
+ }
207
+
208
+ /**
209
+ * Get on-error color (text on error background) based on current theme.
210
+ */
211
+ @ColorInt
212
+ fun getOnErrorColor(context: Context): Int {
213
+ return if (isDarkMode(context)) DarkColors.ON_ERROR else LightColors.ON_ERROR
214
+ }
215
+
216
+ /**
217
+ * Get on-error-container color based on current theme.
218
+ */
219
+ @ColorInt
220
+ fun getOnErrorContainerColor(context: Context): Int {
221
+ return if (isDarkMode(context)) DarkColors.ON_ERROR_CONTAINER else LightColors.ON_ERROR_CONTAINER
222
+ }
223
+
224
+ /**
225
+ * Get primary text color based on current theme.
226
+ */
227
+ @ColorInt
228
+ fun getOnSurfaceColor(context: Context): Int {
229
+ return if (isDarkMode(context)) DarkColors.ON_SURFACE else LightColors.ON_SURFACE
230
+ }
231
+
232
+ /**
233
+ * Get secondary text color based on current theme.
234
+ */
235
+ @ColorInt
236
+ fun getOnSurfaceVariantColor(context: Context): Int {
237
+ return if (isDarkMode(context)) DarkColors.ON_SURFACE_VARIANT else LightColors.ON_SURFACE_VARIANT
238
+ }
239
+
240
+ /**
241
+ * Get outline color (dividers, borders) based on current theme.
242
+ */
243
+ @ColorInt
244
+ fun getOutlineColor(context: Context): Int {
245
+ return if (isDarkMode(context)) DarkColors.OUTLINE else LightColors.OUTLINE
246
+ }
247
+
248
+ /**
249
+ * Get primary color based on current theme.
250
+ */
251
+ @ColorInt
252
+ fun getPrimaryColor(context: Context): Int {
253
+ return if (isDarkMode(context)) DarkColors.PRIMARY else LightColors.PRIMARY
254
+ }
255
+
256
+ /**
257
+ * Get on-primary color (text on primary background) based on current theme.
258
+ */
259
+ @ColorInt
260
+ fun getOnPrimaryColor(context: Context): Int {
261
+ return if (isDarkMode(context)) DarkColors.ON_PRIMARY else LightColors.ON_PRIMARY
262
+ }
263
+
264
+ /**
265
+ * Get primary container color based on current theme.
266
+ */
267
+ @ColorInt
268
+ fun getPrimaryContainerColor(context: Context): Int {
269
+ return if (isDarkMode(context)) DarkColors.PRIMARY_CONTAINER else LightColors.PRIMARY_CONTAINER
270
+ }
271
+
272
+ /**
273
+ * Get on-primary-container color based on current theme.
274
+ */
275
+ @ColorInt
276
+ fun getOnPrimaryContainerColor(context: Context): Int {
277
+ return if (isDarkMode(context)) DarkColors.ON_PRIMARY_CONTAINER else LightColors.ON_PRIMARY_CONTAINER
278
+ }
279
+
280
+ /**
281
+ * Get secondary color based on current theme.
282
+ */
283
+ @ColorInt
284
+ fun getSecondaryColor(context: Context): Int {
285
+ return if (isDarkMode(context)) DarkColors.SECONDARY else LightColors.SECONDARY
286
+ }
287
+
288
+ /**
289
+ * Get on-secondary color based on current theme.
290
+ */
291
+ @ColorInt
292
+ fun getOnSecondaryColor(context: Context): Int {
293
+ return if (isDarkMode(context)) DarkColors.ON_SECONDARY else LightColors.ON_SECONDARY
294
+ }
295
+
296
+ /**
297
+ * Convert DP to pixels.
298
+ *
299
+ * @param context Android context
300
+ * @param dp Value in density-independent pixels
301
+ * @return Value in pixels
302
+ */
303
+ fun dpToPx(context: Context, dp: Int): Int {
304
+ return (dp * context.resources.displayMetrics.density).toInt()
305
+ }
306
+
307
+ }