create-cmp-cli 0.2.0

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 (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +232 -0
  3. package/bin/create-cmp.mjs +91 -0
  4. package/options.schema.json +110 -0
  5. package/package.json +52 -0
  6. package/src/bootstrap/checks.mjs +283 -0
  7. package/src/bootstrap/exec.mjs +90 -0
  8. package/src/commands/clean.mjs +195 -0
  9. package/src/commands/create.mjs +223 -0
  10. package/src/commands/doctor.mjs +256 -0
  11. package/src/commands/upgrade.mjs +196 -0
  12. package/src/commands/verify.mjs +94 -0
  13. package/src/doctor.mjs +109 -0
  14. package/src/lib/args.mjs +38 -0
  15. package/src/lib/clean.mjs +72 -0
  16. package/src/lib/fsutil.mjs +90 -0
  17. package/src/lib/log.mjs +40 -0
  18. package/src/lib/project-doctor.mjs +329 -0
  19. package/src/lib/registry.mjs +102 -0
  20. package/src/lib/rename.mjs +116 -0
  21. package/src/lib/schema.mjs +123 -0
  22. package/src/lib/toggle.mjs +130 -0
  23. package/src/lib/tokens.mjs +96 -0
  24. package/src/lib/toml.mjs +137 -0
  25. package/src/lib/upgrade.mjs +159 -0
  26. package/src/lib/verify.mjs +97 -0
  27. package/src/scaffold.mjs +347 -0
  28. package/src/verify.mjs +4 -0
  29. package/src/versions/registry.json +42 -0
  30. package/template/.github/workflows/verify.yml +70 -0
  31. package/template/build.gradle.kts +17 -0
  32. package/template/composeApp/build.gradle.kts +257 -0
  33. package/template/composeApp/google-services.json +29 -0
  34. package/template/composeApp/proguard-rules.pro +34 -0
  35. package/template/composeApp/src/androidDebug/AndroidManifest.xml +13 -0
  36. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/ComposeRootRegistry.kt +40 -0
  37. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorCatalog.kt +80 -0
  38. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorHttpServer.kt +322 -0
  39. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorInit.kt +16 -0
  40. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/LiveSemanticsJson.kt +102 -0
  41. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/RemoteControlPage.kt +106 -0
  42. package/template/composeApp/src/androidDebug/res/xml/debug_network_security_config.xml +9 -0
  43. package/template/composeApp/src/androidMain/AndroidManifest.xml +28 -0
  44. package/template/composeApp/src/androidMain/kotlin/com/example/app/AppApplication.kt +72 -0
  45. package/template/composeApp/src/androidMain/kotlin/com/example/app/MainActivity.kt +24 -0
  46. package/template/composeApp/src/androidMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt +42 -0
  47. package/template/composeApp/src/androidMain/kotlin/com/example/app/data/local/DatabaseBuilder.android.kt +13 -0
  48. package/template/composeApp/src/androidMain/kotlin/com/example/app/di/AndroidModule.kt +9 -0
  49. package/template/composeApp/src/androidMain/kotlin/com/example/app/presentation/components/TestTagAutomation.android.kt +11 -0
  50. package/template/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml +9 -0
  51. package/template/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
  52. package/template/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
  53. package/template/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png +0 -0
  54. package/template/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.png +0 -0
  55. package/template/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  56. package/template/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png +0 -0
  57. package/template/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.png +0 -0
  58. package/template/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  59. package/template/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png +0 -0
  60. package/template/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
  61. package/template/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  62. package/template/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  63. package/template/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
  64. package/template/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  65. package/template/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  66. package/template/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
  67. package/template/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  68. package/template/composeApp/src/androidMain/res/values/colors.xml +6 -0
  69. package/template/composeApp/src/androidMain/res/values/themes.xml +9 -0
  70. package/template/composeApp/src/androidMain/res/values-v31/themes.xml +8 -0
  71. package/template/composeApp/src/androidRelease/kotlin/com/example/app/inspector/InspectorInit.kt +13 -0
  72. package/template/composeApp/src/commonMain/composeResources/font/DMSans_Bold.ttf +0 -0
  73. package/template/composeApp/src/commonMain/composeResources/font/DMSans_Medium.ttf +0 -0
  74. package/template/composeApp/src/commonMain/composeResources/font/DMSans_Regular.ttf +0 -0
  75. package/template/composeApp/src/commonMain/composeResources/font/DMSans_SemiBold.ttf +0 -0
  76. package/template/composeApp/src/commonMain/kotlin/com/example/app/App.kt +7 -0
  77. package/template/composeApp/src/commonMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt +7 -0
  78. package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/AppDatabase.kt +28 -0
  79. package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/DatabaseBuilder.kt +14 -0
  80. package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/ItemDao.kt +18 -0
  81. package/template/composeApp/src/commonMain/kotlin/com/example/app/data/remote/FirebaseConfig.kt +5 -0
  82. package/template/composeApp/src/commonMain/kotlin/com/example/app/data/remote/ItemRepositoryImpl.kt +22 -0
  83. package/template/composeApp/src/commonMain/kotlin/com/example/app/di/AppModule.kt +23 -0
  84. package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/model/Item.kt +8 -0
  85. package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/repository/ItemRepository.kt +8 -0
  86. package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/usecase/GetItemsUseCase.kt +12 -0
  87. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/App.kt +13 -0
  88. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/BaseScreen.kt +69 -0
  89. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/TestTagAutomation.kt +18 -0
  90. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/DetailScreen.kt +39 -0
  91. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeScreen.kt +87 -0
  92. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeViewModel.kt +35 -0
  93. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppNavHost.kt +43 -0
  94. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppShell.kt +144 -0
  95. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppTab.kt +25 -0
  96. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/Screen.kt +15 -0
  97. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/profile/ProfileScreen.kt +43 -0
  98. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/DesignToken.kt +25 -0
  99. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Shape.kt +12 -0
  100. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Theme.kt +64 -0
  101. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Tokens.kt +19 -0
  102. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Typography.kt +36 -0
  103. package/template/composeApp/src/desktopMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.desktop.kt +9 -0
  104. package/template/composeApp/src/desktopMain/kotlin/com/example/app/data/local/DatabaseBuilder.desktop.kt +13 -0
  105. package/template/composeApp/src/desktopMain/kotlin/com/example/app/di/DesktopModule.kt +32 -0
  106. package/template/composeApp/src/desktopMain/kotlin/com/example/app/main.kt +29 -0
  107. package/template/composeApp/src/desktopMain/kotlin/com/example/app/presentation/components/TestTagAutomation.desktop.kt +6 -0
  108. package/template/composeApp/src/iosMain/kotlin/com/example/app/KoinHelper.kt +50 -0
  109. package/template/composeApp/src/iosMain/kotlin/com/example/app/MainViewController.kt +10 -0
  110. package/template/composeApp/src/iosMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt +32 -0
  111. package/template/composeApp/src/iosMain/kotlin/com/example/app/data/local/DatabaseBuilder.ios.kt +10 -0
  112. package/template/composeApp/src/iosMain/kotlin/com/example/app/presentation/components/TestTagAutomation.ios.kt +11 -0
  113. package/template/docs/dev-client.md +52 -0
  114. package/template/gitignore +21 -0
  115. package/template/gradle/libs.versions.toml +100 -0
  116. package/template/gradle/wrapper/gradle-wrapper.jar +0 -0
  117. package/template/gradle/wrapper/gradle-wrapper.properties +7 -0
  118. package/template/gradle.properties +9 -0
  119. package/template/gradlew +152 -0
  120. package/template/gradlew.bat +94 -0
  121. package/template/iosApp/Podfile +27 -0
  122. package/template/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png +0 -0
  123. package/template/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
  124. package/template/iosApp/iosApp/Assets.xcassets/Contents.json +6 -0
  125. package/template/iosApp/iosApp/ContentView.swift +17 -0
  126. package/template/iosApp/iosApp/GoogleService-Info.plist +34 -0
  127. package/template/iosApp/iosApp/Info.plist +43 -0
  128. package/template/iosApp/iosApp/PrivacyInfo.xcprivacy +42 -0
  129. package/template/iosApp/iosApp/iOSApp.swift +33 -0
  130. package/template/iosApp/project.yml +56 -0
  131. package/template/local.properties.example +2 -0
  132. package/template/manifest.json +125 -0
  133. package/template/qa/appium/README.md +23 -0
  134. package/template/qa/appium/lib/appium-client.mjs +225 -0
  135. package/template/qa/appium/package.json +8 -0
  136. package/template/qa/appium/run-android-smoke.mjs +39 -0
  137. package/template/settings.gradle.kts +34 -0
  138. package/template/tests/appium/cmp/conftest.py +96 -0
  139. package/template/tests/appium/cmp/test_smoke.py +17 -0
@@ -0,0 +1,24 @@
1
+ package __PACKAGE__
2
+
3
+ import android.graphics.Color
4
+ import android.os.Bundle
5
+ import androidx.activity.ComponentActivity
6
+ import androidx.activity.SystemBarStyle
7
+ import androidx.activity.compose.setContent
8
+ import androidx.activity.enableEdgeToEdge
9
+
10
+ class MainActivity : ComponentActivity() {
11
+
12
+ override fun onCreate(savedInstanceState: Bundle?) {
13
+ super.onCreate(savedInstanceState)
14
+ // Edge-to-edge: the status and navigation bars are transparent and content draws
15
+ // beneath them. Screens own their insets in shared code via BaseScreen's Scaffold
16
+ // (statusBarsPadding / navigationBarsPadding), which also maps to iOS safe areas
17
+ // under Compose Multiplatform. Never set the deprecated window.statusBarColor on API 35.
18
+ enableEdgeToEdge(
19
+ statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT),
20
+ navigationBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT),
21
+ )
22
+ setContent { App() }
23
+ }
24
+ }
@@ -0,0 +1,42 @@
1
+ package __PACKAGE__.core.connectivity
2
+
3
+ import android.content.Context
4
+ import android.net.ConnectivityManager
5
+ import android.net.Network
6
+ import android.net.NetworkCapabilities
7
+ import android.net.NetworkRequest
8
+ import kotlinx.coroutines.flow.MutableStateFlow
9
+ import kotlinx.coroutines.flow.StateFlow
10
+ import kotlinx.coroutines.flow.asStateFlow
11
+
12
+ actual class NetworkMonitor actual constructor(context: Any?) {
13
+
14
+ private val cm = (context as Context)
15
+ .getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
16
+
17
+ private val _isOnline = MutableStateFlow(
18
+ cm.activeNetwork
19
+ ?.let { cm.getNetworkCapabilities(it) }
20
+ ?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true
21
+ )
22
+ actual val isOnline: StateFlow<Boolean> = _isOnline.asStateFlow()
23
+
24
+ init {
25
+ val request = NetworkRequest.Builder()
26
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
27
+ .build()
28
+
29
+ val cb = object : ConnectivityManager.NetworkCallback() {
30
+ override fun onAvailable(network: Network) {
31
+ _isOnline.value = true
32
+ }
33
+ override fun onLost(network: Network) {
34
+ _isOnline.value = cm.activeNetwork != null
35
+ }
36
+ override fun onUnavailable() {
37
+ _isOnline.value = false
38
+ }
39
+ }
40
+ cm.registerNetworkCallback(request, cb)
41
+ }
42
+ }
@@ -0,0 +1,13 @@
1
+ package __PACKAGE__.data.local
2
+
3
+ import android.content.Context
4
+ import androidx.room.Room
5
+ import androidx.room.RoomDatabase
6
+
7
+ lateinit var appContext: Context
8
+
9
+ actual fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> =
10
+ Room.databaseBuilder(
11
+ context = appContext,
12
+ name = appContext.getDatabasePath("app.db").absolutePath
13
+ )
@@ -0,0 +1,9 @@
1
+ package __PACKAGE__.di
2
+
3
+ import org.koin.core.module.Module
4
+ import org.koin.dsl.module
5
+
6
+ // Android-only DI bindings (platform services that need a Context, etc.).
7
+ // Empty by default — add your platform-specific singletons here.
8
+ val androidModule: Module = module {
9
+ }
@@ -0,0 +1,11 @@
1
+ package __PACKAGE__.presentation.components
2
+
3
+ import androidx.compose.ui.ExperimentalComposeUiApi
4
+ import androidx.compose.ui.Modifier
5
+ import androidx.compose.ui.semantics.semantics
6
+ import androidx.compose.ui.semantics.testTagsAsResourceId
7
+
8
+ /** Android: surface testTags as `resource-id` entries for uiautomator/Appium id-selectors. */
9
+ @OptIn(ExperimentalComposeUiApi::class)
10
+ actual fun Modifier.exposeTestTagsForAutomation(): Modifier =
11
+ semantics { testTagsAsResourceId = true }
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Adaptive-icon background: a brand gradient tile. Replace with your own brand colours. -->
3
+ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
4
+ <gradient
5
+ android:type="linear"
6
+ android:angle="270"
7
+ android:startColor="#0E2E50"
8
+ android:endColor="#0A2540" />
9
+ </shape>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <background android:drawable="@drawable/ic_launcher_background" />
4
+ <foreground android:drawable="@mipmap/ic_launcher_foreground" />
5
+ <monochrome android:drawable="@mipmap/ic_launcher_foreground" />
6
+ </adaptive-icon>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <background android:drawable="@drawable/ic_launcher_background" />
4
+ <foreground android:drawable="@mipmap/ic_launcher_foreground" />
5
+ <monochrome android:drawable="@mipmap/ic_launcher_foreground" />
6
+ </adaptive-icon>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ <!-- Brand primary, used as the window background so there is no white flash before
4
+ Compose draws its first frame. Keep in sync with __THEME_PREFIX__Colors.Primary. -->
5
+ <color name="app_window_background">#0A2540</color>
6
+ </resources>
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ <!-- NoActionBar + a brand-coloured window background so the very first frames
4
+ (system launch window, any pre-Compose frame) match the app. Status/nav bar
5
+ styling is set in MainActivity (edge-to-edge), not here. -->
6
+ <style name="Theme.App" parent="@android:style/Theme.Material.Light.NoActionBar">
7
+ <item name="android:windowBackground">@color/app_window_background</item>
8
+ </style>
9
+ </resources>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ <!-- API 31+ platform splash: brand background so the splash window matches frame 0. -->
4
+ <style name="Theme.App" parent="@android:style/Theme.Material.Light.NoActionBar">
5
+ <item name="android:windowBackground">@color/app_window_background</item>
6
+ <item name="android:windowSplashScreenBackground">@color/app_window_background</item>
7
+ </style>
8
+ </resources>
@@ -0,0 +1,13 @@
1
+ package __PACKAGE__.inspector
2
+
3
+ import android.app.Application
4
+
5
+ /**
6
+ * RELEASE variant: no-op twin of the androidDebug `startInspector()`. Release builds compile
7
+ * this body instead — no server class, no registry, no inspector endpoint strings. The
8
+ * inspector is structurally absent from release, not merely disabled.
9
+ */
10
+ @Suppress("UnusedReceiverParameter")
11
+ fun Application.startInspector() {
12
+ // Intentionally empty.
13
+ }
@@ -0,0 +1,7 @@
1
+ package __PACKAGE__
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import __PACKAGE__.presentation.App as PresentationApp
5
+
6
+ @Composable
7
+ fun App() = PresentationApp()
@@ -0,0 +1,7 @@
1
+ package __PACKAGE__.core.connectivity
2
+
3
+ import kotlinx.coroutines.flow.StateFlow
4
+
5
+ expect class NetworkMonitor(context: Any?) {
6
+ val isOnline: StateFlow<Boolean>
7
+ }
@@ -0,0 +1,28 @@
1
+ package __PACKAGE__.data.local
2
+
3
+ import androidx.room.ConstructedBy
4
+ import androidx.room.Database
5
+ import androidx.room.Entity
6
+ import androidx.room.PrimaryKey
7
+ import androidx.room.RoomDatabase
8
+ import androidx.room.RoomDatabaseConstructor
9
+
10
+ // ── Example Room entity for the `home` feature's offline cache ───────────────────────
11
+ @Entity(tableName = "items")
12
+ data class ItemEntity(
13
+ @PrimaryKey val id: String,
14
+ val title: String,
15
+ val subtitle: String,
16
+ )
17
+
18
+ @Database(entities = [ItemEntity::class], version = 1, exportSchema = true)
19
+ @ConstructedBy(AppDatabaseConstructor::class)
20
+ abstract class AppDatabase : RoomDatabase() {
21
+ abstract fun itemDao(): ItemDao
22
+ }
23
+
24
+ // The actual implementation is generated by Room's KSP processor per platform.
25
+ @Suppress("NO_ACTUAL_FOR_EXPECT", "KotlinNoActualForExpect")
26
+ expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> {
27
+ override fun initialize(): AppDatabase
28
+ }
@@ -0,0 +1,14 @@
1
+ package __PACKAGE__.data.local
2
+
3
+ import androidx.room.RoomDatabase
4
+ import androidx.sqlite.driver.bundled.BundledSQLiteDriver
5
+
6
+ expect fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase>
7
+
8
+ fun buildDatabase(): AppDatabase =
9
+ getDatabaseBuilder()
10
+ // BundledSQLiteDriver is required for Room KMP on Kotlin/Native (iOS).
11
+ .setDriver(BundledSQLiteDriver())
12
+ .fallbackToDestructiveMigration(dropAllTables = true)
13
+ .fallbackToDestructiveMigrationOnDowngrade(dropAllTables = true)
14
+ .build()
@@ -0,0 +1,18 @@
1
+ package __PACKAGE__.data.local
2
+
3
+ import androidx.room.Dao
4
+ import androidx.room.Insert
5
+ import androidx.room.OnConflictStrategy
6
+ import androidx.room.Query
7
+
8
+ @Dao
9
+ interface ItemDao {
10
+ @Query("SELECT * FROM items")
11
+ suspend fun getAll(): List<ItemEntity>
12
+
13
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
14
+ suspend fun upsertAll(items: List<ItemEntity>)
15
+
16
+ @Query("DELETE FROM items")
17
+ suspend fun clear()
18
+ }
@@ -0,0 +1,5 @@
1
+ package __PACKAGE__.data.remote
2
+
3
+ // Region for Cloud Functions / callables. Keep schedulers, callables, Firestore in the
4
+ // SAME region — cross-region 2nd-gen wiring fails.
5
+ const val FIREBASE_FUNCTIONS_REGION = "__REGION__"
@@ -0,0 +1,22 @@
1
+ package __PACKAGE__.data.remote
2
+
3
+ import __PACKAGE__.domain.model.Item
4
+ import __PACKAGE__.domain.repository.ItemRepository
5
+ import kotlinx.coroutines.delay
6
+
7
+ // Example data source for the `home` feature. This is intentionally dependency-light
8
+ // (no Firebase / no Room coupling) so the scaffold builds in every feature combination.
9
+ //
10
+ // Real apps swap this for a Firestore/Ktor source and add a Room cache (see data/local).
11
+ // The Clean Architecture seam is the ItemRepository interface in the domain layer.
12
+ class ItemRepositoryImpl : ItemRepository {
13
+ override suspend fun getItems(): List<Item> {
14
+ delay(300) // simulate I/O
15
+ return listOf(
16
+ Item("1", "Welcome to __APP_NAME__", "Your Compose Multiplatform app is wired end-to-end."),
17
+ Item("2", "Clean Architecture", "presentation → domain → data, with Koin DI."),
18
+ Item("3", "Edge-to-edge, pre-solved", "BaseScreen owns the window insets for you."),
19
+ Item("4", "Android + iOS", "One codebase, two green builds."),
20
+ )
21
+ }
22
+ }
@@ -0,0 +1,23 @@
1
+ package __PACKAGE__.di
2
+
3
+ import __PACKAGE__.data.remote.ItemRepositoryImpl
4
+ import __PACKAGE__.domain.repository.ItemRepository
5
+ import __PACKAGE__.domain.usecase.GetItemsUseCase
6
+ import __PACKAGE__.presentation.home.HomeViewModel
7
+ import org.koin.core.module.dsl.viewModelOf
8
+ import org.koin.dsl.module
9
+
10
+ val repositoryModule = module {
11
+ single<ItemRepository> { ItemRepositoryImpl() }
12
+ }
13
+
14
+ val useCaseModule = module {
15
+ factory { GetItemsUseCase(get()) }
16
+ }
17
+
18
+ val viewModelModule = module {
19
+ viewModelOf(::HomeViewModel)
20
+ }
21
+
22
+ // Aggregated common modules, started from AppApplication (Android) and KoinHelper (iOS).
23
+ val appModules = listOf(repositoryModule, useCaseModule, viewModelModule)
@@ -0,0 +1,8 @@
1
+ package __PACKAGE__.domain.model
2
+
3
+ // Generic domain model for the example `home` feature. Replace with your own entities.
4
+ data class Item(
5
+ val id: String,
6
+ val title: String,
7
+ val subtitle: String,
8
+ )
@@ -0,0 +1,8 @@
1
+ package __PACKAGE__.domain.repository
2
+
3
+ import __PACKAGE__.domain.model.Item
4
+
5
+ // Domain-facing contract. Presentation depends on THIS, never on a concrete data source.
6
+ interface ItemRepository {
7
+ suspend fun getItems(): List<Item>
8
+ }
@@ -0,0 +1,12 @@
1
+ package __PACKAGE__.domain.usecase
2
+
3
+ import __PACKAGE__.domain.model.Item
4
+ import __PACKAGE__.domain.repository.ItemRepository
5
+
6
+ // A use case is a single business action. ViewModels depend on use cases, not repositories
7
+ // directly, so business rules stay testable and out of the presentation layer.
8
+ class GetItemsUseCase(
9
+ private val repository: ItemRepository,
10
+ ) {
11
+ suspend operator fun invoke(): List<Item> = repository.getItems()
12
+ }
@@ -0,0 +1,13 @@
1
+ package __PACKAGE__.presentation
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import __PACKAGE__.presentation.navigation.AppNavHost
5
+ import __PACKAGE__.presentation.theme.__THEME_PREFIX__Theme
6
+
7
+ // Root composable. Wraps the whole app in the theme and hosts navigation.
8
+ @Composable
9
+ fun App() {
10
+ __THEME_PREFIX__Theme {
11
+ AppNavHost()
12
+ }
13
+ }
@@ -0,0 +1,69 @@
1
+ package __PACKAGE__.presentation.components
2
+
3
+ import androidx.compose.foundation.layout.Box
4
+ import androidx.compose.foundation.layout.PaddingValues
5
+ import androidx.compose.foundation.layout.WindowInsets
6
+ import androidx.compose.foundation.layout.consumeWindowInsets
7
+ import androidx.compose.foundation.layout.fillMaxSize
8
+ import androidx.compose.foundation.layout.navigationBarsPadding
9
+ import androidx.compose.foundation.layout.padding
10
+ import androidx.compose.foundation.layout.statusBarsPadding
11
+ import androidx.compose.material3.Scaffold
12
+ import androidx.compose.runtime.Composable
13
+ import androidx.compose.ui.Modifier
14
+ import androidx.compose.ui.graphics.Color
15
+ import __PACKAGE__.presentation.theme.designToken
16
+
17
+ /**
18
+ * The insets moat, pre-solved. Every screen wraps its content in [BaseScreen] instead of
19
+ * re-deriving edge-to-edge padding. The Activity is edge-to-edge (transparent system bars);
20
+ * this Scaffold owns the status-bar / navigation-bar insets in shared code, which also maps
21
+ * to iOS safe areas under Compose Multiplatform.
22
+ *
23
+ * - [topBar] / [bottomBar] draw edge-to-edge (e.g. a nav bar that bleeds behind the gesture
24
+ * pill) and are responsible for their own inset padding.
25
+ * - The content lambda receives padding already accounting for any bars; by default the body
26
+ * gets status + navigation bar padding so plain screens are safe with zero ceremony.
27
+ */
28
+ @Composable
29
+ fun BaseScreen(
30
+ modifier: Modifier = Modifier,
31
+ containerColor: Color = Color.Unspecified,
32
+ applyStatusBarPadding: Boolean = true,
33
+ applyNavBarPadding: Boolean = true,
34
+ topBar: @Composable () -> Unit = {},
35
+ bottomBar: @Composable () -> Unit = {},
36
+ content: @Composable (PaddingValues) -> Unit,
37
+ ) {
38
+ Scaffold(
39
+ modifier = modifier,
40
+ // Screens manage their own insets via the padding modifiers below, so the Scaffold
41
+ // itself consumes nothing — this avoids the classic doubled-padding bug.
42
+ contentWindowInsets = WindowInsets(0, 0, 0, 0),
43
+ containerColor = if (containerColor == Color.Unspecified)
44
+ androidx.compose.material3.MaterialTheme.colorScheme.background else containerColor,
45
+ topBar = topBar,
46
+ bottomBar = bottomBar,
47
+ ) { innerPadding ->
48
+ Box(
49
+ modifier = Modifier
50
+ .fillMaxSize()
51
+ .padding(innerPadding)
52
+ .consumeWindowInsets(innerPadding)
53
+ .then(if (applyStatusBarPadding) Modifier.statusBarsPadding() else Modifier)
54
+ .then(if (applyNavBarPadding) Modifier.navigationBarsPadding() else Modifier)
55
+ // Inspector: self-report the inset facts this Box actually applies. No dimen
56
+ // token is used here — the padding comes from Scaffold insets, not the design
57
+ // system — so the tokens list is empty and only the resolved facts are emitted.
58
+ .designToken(
59
+ tokens = emptyList(),
60
+ resolved = mapOf(
61
+ "statusBarPadding" to applyStatusBarPadding.toString(),
62
+ "navBarPadding" to applyNavBarPadding.toString(),
63
+ ),
64
+ )
65
+ ) {
66
+ content(innerPadding)
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,18 @@
1
+ package __PACKAGE__.presentation.components
2
+
3
+ import androidx.compose.ui.Modifier
4
+
5
+ /**
6
+ * Exposes every `Modifier.testTag` in the subtree to the platform's UI-automation layer,
7
+ * so Appium/uiautomator id-selectors (and the cmp-test-generated suites) find them on a
8
+ * stock build with no extra flags.
9
+ *
10
+ * - Android actual: `semantics { testTagsAsResourceId = true }` → tags surface as
11
+ * `resource-id` in the uiautomator/Appium tree.
12
+ * - iOS actual: same semantics flag → tags surface as `accessibilityIdentifier` (XCUITest).
13
+ * - Desktop actual: no-op — the skiko `ui` artifact has no `testTagsAsResourceId`
14
+ * (there is no resource-id concept on desktop; tests use the Compose test APIs directly).
15
+ *
16
+ * Apply once at the root (AppShell does); it covers the whole subtree.
17
+ */
18
+ expect fun Modifier.exposeTestTagsForAutomation(): Modifier
@@ -0,0 +1,39 @@
1
+ package __PACKAGE__.presentation.home
2
+
3
+ import androidx.compose.foundation.layout.Column
4
+ import androidx.compose.foundation.layout.fillMaxSize
5
+ import androidx.compose.foundation.layout.padding
6
+ import androidx.compose.material3.MaterialTheme
7
+ import androidx.compose.material3.Text
8
+ import androidx.compose.material3.TextButton
9
+ import androidx.compose.runtime.Composable
10
+ import androidx.compose.ui.Modifier
11
+ import __PACKAGE__.presentation.components.BaseScreen
12
+ import __PACKAGE__.presentation.theme.__THEME_PREFIX__Tokens
13
+ import __PACKAGE__.presentation.theme.designToken
14
+
15
+ @Composable
16
+ fun DetailScreen(
17
+ itemId: String,
18
+ onBack: () -> Unit,
19
+ ) {
20
+ BaseScreen {
21
+ Column(
22
+ Modifier
23
+ .fillMaxSize()
24
+ .designToken(
25
+ tokens = listOf("PaddingPage"),
26
+ resolved = mapOf("padding" to "16dp"),
27
+ )
28
+ .padding(__THEME_PREFIX__Tokens.PaddingPage),
29
+ ) {
30
+ TextButton(onClick = onBack) { Text("← Back") }
31
+ Text("Detail", style = MaterialTheme.typography.headlineMedium)
32
+ Text(
33
+ "Item id: $itemId",
34
+ style = MaterialTheme.typography.bodyLarge,
35
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
36
+ )
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,87 @@
1
+ package __PACKAGE__.presentation.home
2
+
3
+ import androidx.compose.foundation.clickable
4
+ import androidx.compose.foundation.layout.Arrangement
5
+ import androidx.compose.foundation.layout.Box
6
+ import androidx.compose.foundation.layout.Column
7
+ import androidx.compose.foundation.layout.fillMaxSize
8
+ import androidx.compose.foundation.layout.fillMaxWidth
9
+ import androidx.compose.foundation.layout.padding
10
+ import androidx.compose.foundation.lazy.LazyColumn
11
+ import androidx.compose.foundation.lazy.items
12
+ import androidx.compose.material3.CircularProgressIndicator
13
+ import androidx.compose.material3.MaterialTheme
14
+ import androidx.compose.material3.Surface
15
+ import androidx.compose.material3.Text
16
+ import androidx.compose.runtime.Composable
17
+ import androidx.compose.runtime.getValue
18
+ import androidx.compose.ui.Alignment
19
+ import androidx.compose.ui.Modifier
20
+ import androidx.compose.ui.semantics.semantics
21
+ import androidx.compose.ui.semantics.testTag
22
+ import androidx.compose.ui.unit.dp
23
+ import androidx.lifecycle.compose.collectAsStateWithLifecycle
24
+ import __PACKAGE__.presentation.theme.__THEME_PREFIX__Tokens
25
+ import __PACKAGE__.presentation.theme.designToken
26
+ import org.koin.compose.viewmodel.koinViewModel
27
+
28
+ @Composable
29
+ fun HomeScreen(
30
+ onItemClick: (String) -> Unit,
31
+ viewModel: HomeViewModel = koinViewModel(),
32
+ ) {
33
+ val state by viewModel.state.collectAsStateWithLifecycle()
34
+
35
+ Column(
36
+ Modifier
37
+ .fillMaxSize()
38
+ .designToken(
39
+ tokens = listOf("PaddingPage"),
40
+ resolved = mapOf("padding" to "16dp"),
41
+ )
42
+ .padding(__THEME_PREFIX__Tokens.PaddingPage),
43
+ ) {
44
+ Text(
45
+ text = "Home",
46
+ style = MaterialTheme.typography.headlineMedium,
47
+ modifier = Modifier.semantics { testTag = "home_title" }.padding(bottom = 12.dp),
48
+ )
49
+
50
+ if (state.isLoading) {
51
+ Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
52
+ CircularProgressIndicator()
53
+ }
54
+ } else {
55
+ LazyColumn(verticalArrangement = Arrangement.spacedBy(__THEME_PREFIX__Tokens.GapCard)) {
56
+ items(state.items, key = { it.id }) { item ->
57
+ Surface(
58
+ color = MaterialTheme.colorScheme.surface,
59
+ shape = MaterialTheme.shapes.medium,
60
+ tonalElevation = __THEME_PREFIX__Tokens.ElevationCard,
61
+ modifier = Modifier
62
+ .fillMaxWidth()
63
+ .designToken(
64
+ tokens = listOf("RadiusCard", "ElevationCard", "PaddingCard"),
65
+ resolved = mapOf(
66
+ "radius" to "16dp",
67
+ "elevation" to "2dp",
68
+ "padding" to "16dp",
69
+ "color" to "#FFFFFFFF",
70
+ ),
71
+ )
72
+ .clickable { onItemClick(item.id) },
73
+ ) {
74
+ Column(Modifier.padding(__THEME_PREFIX__Tokens.PaddingCard)) {
75
+ Text(item.title, style = MaterialTheme.typography.titleMedium)
76
+ Text(
77
+ item.subtitle,
78
+ style = MaterialTheme.typography.bodyMedium,
79
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
80
+ )
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }