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,35 @@
1
+ package __PACKAGE__.presentation.home
2
+
3
+ import androidx.lifecycle.ViewModel
4
+ import androidx.lifecycle.viewModelScope
5
+ import __PACKAGE__.domain.model.Item
6
+ import __PACKAGE__.domain.usecase.GetItemsUseCase
7
+ import kotlinx.coroutines.flow.MutableStateFlow
8
+ import kotlinx.coroutines.flow.StateFlow
9
+ import kotlinx.coroutines.flow.asStateFlow
10
+ import kotlinx.coroutines.launch
11
+
12
+ data class HomeUiState(
13
+ val isLoading: Boolean = true,
14
+ val items: List<Item> = emptyList(),
15
+ )
16
+
17
+ class HomeViewModel(
18
+ private val getItems: GetItemsUseCase,
19
+ ) : ViewModel() {
20
+
21
+ private val _state = MutableStateFlow(HomeUiState())
22
+ val state: StateFlow<HomeUiState> = _state.asStateFlow()
23
+
24
+ init {
25
+ load()
26
+ }
27
+
28
+ fun load() {
29
+ viewModelScope.launch {
30
+ _state.value = HomeUiState(isLoading = true)
31
+ val items = getItems()
32
+ _state.value = HomeUiState(isLoading = false, items = items)
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,43 @@
1
+ package __PACKAGE__.presentation.navigation
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import androidx.navigation.NavType
5
+ import androidx.navigation.compose.NavHost
6
+ import androidx.navigation.compose.composable
7
+ import androidx.navigation.compose.rememberNavController
8
+ import androidx.navigation.navArgument
9
+ // Nav 2.9 (multiplatform): backStackEntry.arguments is a SavedState, not an Android Bundle.
10
+ // Read it via the androidx.savedstate.read extension, NOT Bundle.getString().
11
+ import androidx.savedstate.read
12
+ import __PACKAGE__.presentation.home.HomeScreen
13
+ import __PACKAGE__.presentation.profile.ProfileScreen
14
+
15
+ @Composable
16
+ fun AppNavHost() {
17
+ val navController = rememberNavController()
18
+
19
+ NavHost(navController = navController, startDestination = Screen.Shell.route) {
20
+ composable(Screen.Shell.route) {
21
+ val tabs = appTabs(
22
+ home = {
23
+ HomeScreen(
24
+ onItemClick = { itemId -> navController.navigate(Routes.detail(itemId)) },
25
+ )
26
+ },
27
+ profile = { ProfileScreen() },
28
+ )
29
+ AppShell(tabs = tabs)
30
+ }
31
+
32
+ composable(
33
+ route = Screen.Detail.route,
34
+ arguments = listOf(navArgument("itemId") { type = NavType.StringType }),
35
+ ) { backStackEntry ->
36
+ val itemId = backStackEntry.arguments?.read { getStringOrNull("itemId") }.orEmpty()
37
+ __PACKAGE__.presentation.home.DetailScreen(
38
+ itemId = itemId,
39
+ onBack = { navController.popBackStack() },
40
+ )
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,144 @@
1
+ package __PACKAGE__.presentation.navigation
2
+
3
+ import androidx.compose.foundation.background
4
+ import androidx.compose.foundation.clickable
5
+ import androidx.compose.foundation.layout.Arrangement
6
+ import androidx.compose.foundation.layout.Box
7
+ import androidx.compose.foundation.layout.Column
8
+ import androidx.compose.foundation.layout.Row
9
+ import androidx.compose.foundation.layout.defaultMinSize
10
+ import androidx.compose.foundation.layout.fillMaxSize
11
+ import androidx.compose.foundation.layout.fillMaxWidth
12
+ import androidx.compose.foundation.layout.height
13
+ import androidx.compose.foundation.layout.navigationBarsPadding
14
+ import androidx.compose.foundation.layout.padding
15
+ import androidx.compose.foundation.layout.size
16
+ import androidx.compose.foundation.shape.RoundedCornerShape
17
+ import androidx.compose.material3.Icon
18
+ import androidx.compose.material3.Text
19
+ import androidx.compose.runtime.Composable
20
+ import androidx.compose.runtime.getValue
21
+ import androidx.compose.runtime.mutableIntStateOf
22
+ import androidx.compose.runtime.saveable.rememberSaveable
23
+ import androidx.compose.runtime.setValue
24
+ import androidx.compose.ui.Alignment
25
+ import androidx.compose.ui.Modifier
26
+ import androidx.compose.ui.draw.clip
27
+ import androidx.compose.ui.semantics.semantics
28
+ import androidx.compose.ui.semantics.testTag
29
+ import androidx.compose.ui.text.font.FontWeight
30
+ import androidx.compose.ui.unit.dp
31
+ import androidx.compose.ui.unit.sp
32
+ import __PACKAGE__.presentation.components.BaseScreen
33
+ import __PACKAGE__.presentation.components.exposeTestTagsForAutomation
34
+ import __PACKAGE__.presentation.theme.__THEME_PREFIX__Colors
35
+ import __PACKAGE__.presentation.theme.__THEME_PREFIX__Tokens
36
+ import __PACKAGE__.presentation.theme.designToken
37
+
38
+ /**
39
+ * Generic bottom-nav shell. Parameterized by a [tabs] list — NOT role-hardcoded.
40
+ * Hosts the selected tab's content inside a [BaseScreen] so each tab gets correct insets;
41
+ * the bottom bar reserves the navigation-bar inset exactly once.
42
+ */
43
+ @Composable
44
+ fun AppShell(tabs: List<AppTab>) {
45
+ var selected by rememberSaveable { mutableIntStateOf(0) }
46
+
47
+ BaseScreen(
48
+ // Expose Compose testTags to the platform automation layer (Android resource-ids /
49
+ // iOS accessibilityIdentifiers) so Appium/uiautomator id-selectors and the
50
+ // cmp-test-generated suites find them; covers the whole subtree. Desktop: no-op.
51
+ modifier = Modifier.exposeTestTagsForAutomation(),
52
+ // The bottom bar owns the navigation-bar inset; the body must not also pad it.
53
+ applyNavBarPadding = false,
54
+ bottomBar = {
55
+ AppBottomNav(
56
+ tabs = tabs,
57
+ selectedIndex = selected,
58
+ onSelect = { selected = it },
59
+ )
60
+ },
61
+ ) {
62
+ Box(Modifier.fillMaxSize()) {
63
+ tabs.getOrNull(selected)?.content?.invoke()
64
+ }
65
+ }
66
+ }
67
+
68
+ @Composable
69
+ private fun AppBottomNav(
70
+ tabs: List<AppTab>,
71
+ selectedIndex: Int,
72
+ onSelect: (Int) -> Unit,
73
+ ) {
74
+ Column(Modifier.fillMaxWidth()) {
75
+ Box(
76
+ Modifier
77
+ .fillMaxWidth()
78
+ .height(1.dp)
79
+ .background(__THEME_PREFIX__Colors.OutlineVariant)
80
+ )
81
+ Row(
82
+ modifier = Modifier
83
+ .fillMaxWidth()
84
+ .background(__THEME_PREFIX__Colors.Surface)
85
+ // Lift tabs above the gesture pill / 3-button nav (maps to iOS safe area).
86
+ .navigationBarsPadding()
87
+ .height(__THEME_PREFIX__Tokens.BottomNavHeight)
88
+ // Inspector: the bottom-nav container self-reports its height token.
89
+ .designToken(
90
+ tokens = listOf("BottomNavHeight"),
91
+ resolved = mapOf("height" to "72dp"),
92
+ )
93
+ .semantics { testTag = "app_bottom_nav" }
94
+ .padding(bottom = 8.dp),
95
+ verticalAlignment = Alignment.CenterVertically,
96
+ horizontalArrangement = Arrangement.SpaceEvenly,
97
+ ) {
98
+ tabs.forEachIndexed { index, tab ->
99
+ NavItem(
100
+ label = tab.label,
101
+ selected = selectedIndex == index,
102
+ onClick = { onSelect(index) },
103
+ ) {
104
+ Icon(
105
+ imageVector = tab.icon,
106
+ contentDescription = tab.label,
107
+ tint = if (selectedIndex == index) __THEME_PREFIX__Colors.Primary
108
+ else __THEME_PREFIX__Colors.OnSurfaceVariant.copy(alpha = 0.55f),
109
+ modifier = Modifier.size(24.dp),
110
+ )
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ @Composable
118
+ private fun NavItem(
119
+ label: String,
120
+ selected: Boolean,
121
+ onClick: () -> Unit,
122
+ icon: @Composable () -> Unit,
123
+ ) {
124
+ Column(
125
+ modifier = Modifier
126
+ .clip(RoundedCornerShape(8.dp))
127
+ .clickable(onClick = onClick)
128
+ // a11y: guarantee the 48dp minimum touch target regardless of label width
129
+ // (the inspector's audit_a11y flags anything smaller).
130
+ .defaultMinSize(minWidth = 48.dp, minHeight = 48.dp)
131
+ .padding(horizontal = 8.dp, vertical = 4.dp),
132
+ horizontalAlignment = Alignment.CenterHorizontally,
133
+ verticalArrangement = Arrangement.Center,
134
+ ) {
135
+ icon()
136
+ Text(
137
+ text = label,
138
+ fontSize = 10.sp,
139
+ fontWeight = if (selected) FontWeight.Bold else FontWeight.SemiBold,
140
+ color = if (selected) __THEME_PREFIX__Colors.Primary else __THEME_PREFIX__Colors.OnSurfaceVariant,
141
+ modifier = Modifier.padding(top = 4.dp),
142
+ )
143
+ }
144
+ }
@@ -0,0 +1,25 @@
1
+ package __PACKAGE__.presentation.navigation
2
+
3
+ import androidx.compose.material.icons.Icons
4
+ import androidx.compose.material.icons.filled.Home
5
+ import androidx.compose.material.icons.filled.Person
6
+ import androidx.compose.runtime.Composable
7
+ import androidx.compose.ui.graphics.vector.ImageVector
8
+
9
+ // A single bottom-nav tab: its label, icon, and the screen it renders.
10
+ data class AppTab(
11
+ val label: String,
12
+ val icon: ImageVector,
13
+ val content: @Composable () -> Unit,
14
+ )
15
+
16
+ // The tab list drives AppShell + AppBottomNav generically (no role-hardcoded shells).
17
+ // The scaffolder regenerates this list from the configured `tabs`.
18
+ @Composable
19
+ fun appTabs(
20
+ home: @Composable () -> Unit,
21
+ profile: @Composable () -> Unit,
22
+ ): List<AppTab> = listOf(
23
+ AppTab("Home", Icons.Filled.Home, home),
24
+ AppTab("Profile", Icons.Filled.Person, profile),
25
+ )
@@ -0,0 +1,15 @@
1
+ package __PACKAGE__.presentation.navigation
2
+
3
+ // Type-safe route registry. Add a Screen object + a Routes constant per destination.
4
+ sealed class Screen(val route: String) {
5
+ data object Shell : Screen(Routes.SHELL)
6
+ // Example detail destination reachable from a tab. Add your own below.
7
+ data object Detail : Screen(Routes.DETAIL)
8
+ }
9
+
10
+ object Routes {
11
+ const val SHELL = "shell"
12
+ const val DETAIL = "detail/{itemId}"
13
+
14
+ fun detail(itemId: String) = "detail/$itemId"
15
+ }
@@ -0,0 +1,43 @@
1
+ package __PACKAGE__.presentation.profile
2
+
3
+ import androidx.compose.foundation.layout.Arrangement
4
+ import androidx.compose.foundation.layout.Column
5
+ import androidx.compose.foundation.layout.fillMaxSize
6
+ import androidx.compose.foundation.layout.padding
7
+ import androidx.compose.material3.MaterialTheme
8
+ import androidx.compose.material3.Text
9
+ import androidx.compose.runtime.Composable
10
+ import androidx.compose.ui.Alignment
11
+ import androidx.compose.ui.Modifier
12
+ import androidx.compose.ui.semantics.semantics
13
+ import androidx.compose.ui.semantics.testTag
14
+ import androidx.compose.ui.unit.dp
15
+ import __PACKAGE__.presentation.theme.__THEME_PREFIX__Tokens
16
+ import __PACKAGE__.presentation.theme.designToken
17
+
18
+ // Feature stub. Copy the `home` feature's data→domain→presentation→DI wiring to flesh this out.
19
+ @Composable
20
+ fun ProfileScreen() {
21
+ Column(
22
+ modifier = Modifier
23
+ .fillMaxSize()
24
+ .designToken(
25
+ tokens = listOf("PaddingPage"),
26
+ resolved = mapOf("padding" to "16dp"),
27
+ )
28
+ .padding(__THEME_PREFIX__Tokens.PaddingPage),
29
+ verticalArrangement = Arrangement.spacedBy(8.dp),
30
+ horizontalAlignment = Alignment.Start,
31
+ ) {
32
+ Text(
33
+ text = "Profile",
34
+ style = MaterialTheme.typography.headlineMedium,
35
+ modifier = Modifier.semantics { testTag = "profile_title" },
36
+ )
37
+ Text(
38
+ text = "This is a stub screen. Wire it up like the Home feature.",
39
+ style = MaterialTheme.typography.bodyMedium,
40
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
41
+ )
42
+ }
43
+ }
@@ -0,0 +1,25 @@
1
+ package __PACKAGE__.presentation.theme
2
+
3
+ import androidx.compose.ui.Modifier
4
+ import androidx.compose.ui.semantics.SemanticsPropertyKey
5
+ import androidx.compose.ui.semantics.SemanticsPropertyReceiver
6
+ import androidx.compose.ui.semantics.semantics
7
+
8
+ /**
9
+ * The resolved design-token payload a component self-reports into the semantics tree.
10
+ *
11
+ * [tokens] are the declared token names (e.g. "RadiusCard"); [resolved] maps a facet
12
+ * (e.g. "radius", "color") to its concrete resolved value (e.g. "16dp", "#FFFFFFFF").
13
+ * This makes the semantics tree design-system-aware, not just geometry — which is what
14
+ * the create-cmp inspector reads to catch token drift. Only uses semantics + Modifier,
15
+ * so it adds no dependencies.
16
+ */
17
+ data class DesignTokenInfo(val tokens: List<String>, val resolved: Map<String, String>)
18
+
19
+ val DesignTokenKey = SemanticsPropertyKey<DesignTokenInfo>("DesignToken")
20
+
21
+ var SemanticsPropertyReceiver.designToken by DesignTokenKey
22
+
23
+ /** Attaches the resolved design-token payload to a node's semantics. */
24
+ fun Modifier.designToken(tokens: List<String>, resolved: Map<String, String>): Modifier =
25
+ this.semantics { designToken = DesignTokenInfo(tokens, resolved) }
@@ -0,0 +1,12 @@
1
+ package __PACKAGE__.presentation.theme
2
+
3
+ import androidx.compose.foundation.shape.RoundedCornerShape
4
+ import androidx.compose.material3.Shapes
5
+ import androidx.compose.ui.unit.dp
6
+
7
+ val __THEME_PREFIX__Shapes = Shapes(
8
+ small = RoundedCornerShape(14.dp),
9
+ medium = RoundedCornerShape(16.dp),
10
+ large = RoundedCornerShape(999.dp),
11
+ extraLarge = RoundedCornerShape(24.dp),
12
+ )
@@ -0,0 +1,64 @@
1
+ package __PACKAGE__.presentation.theme
2
+
3
+ import androidx.compose.material3.MaterialTheme
4
+ import androidx.compose.material3.lightColorScheme
5
+ import androidx.compose.runtime.Composable
6
+ import androidx.compose.ui.graphics.Color
7
+
8
+ // Seed design tokens. Replace these with your own brand palette — every screen reads
9
+ // colours from here (or from MaterialTheme.colorScheme), never as raw hex.
10
+ object __THEME_PREFIX__Colors {
11
+ val Primary = Color(0xFF0A2540)
12
+ val OnPrimary = Color(0xFFFFFFFF)
13
+ val Accent = Color(0xFF00E676)
14
+ val OnAccent = Color(0xFF0A2540)
15
+ val Secondary = Color(0xFF00B96B)
16
+ val Error = Color(0xFFDC2626)
17
+ val Success = Color(0xFF16A34A)
18
+ val Warning = Color(0xFFF59E0B)
19
+ val Info = Color(0xFF2563EB)
20
+ val Background = Color(0xFFF7F9FC)
21
+ val Surface = Color(0xFFFFFFFF)
22
+ val SurfaceVariant = Color(0xFFE8EDF3)
23
+ val OnSurface = Color(0xFF1A1A1A)
24
+ val OnSurfaceVariant = Color(0xFF6B7280)
25
+ val Outline = Color(0xFF9CA3AF)
26
+ val OutlineVariant = Color(0xFFE5E7EB)
27
+ val Divider = Color(0xFFE5E7EB)
28
+ }
29
+
30
+ private val __THEME_PREFIX__ColorScheme = lightColorScheme(
31
+ primary = __THEME_PREFIX__Colors.Primary,
32
+ onPrimary = __THEME_PREFIX__Colors.OnPrimary,
33
+ secondary = __THEME_PREFIX__Colors.Secondary,
34
+ onSecondary = __THEME_PREFIX__Colors.OnPrimary,
35
+ tertiary = __THEME_PREFIX__Colors.Accent,
36
+ onTertiary = __THEME_PREFIX__Colors.OnAccent,
37
+ error = __THEME_PREFIX__Colors.Error,
38
+ background = __THEME_PREFIX__Colors.Background,
39
+ surface = __THEME_PREFIX__Colors.Surface,
40
+ surfaceVariant = __THEME_PREFIX__Colors.SurfaceVariant,
41
+ onBackground = __THEME_PREFIX__Colors.OnSurface,
42
+ onSurface = __THEME_PREFIX__Colors.OnSurface,
43
+ onSurfaceVariant = __THEME_PREFIX__Colors.OnSurfaceVariant,
44
+ outline = __THEME_PREFIX__Colors.Outline,
45
+ outlineVariant = __THEME_PREFIX__Colors.OutlineVariant,
46
+ inverseSurface = __THEME_PREFIX__Colors.Primary,
47
+ // Kill M3's tonal overlay so dialogs/menus/sheets stay on the design-system surface.
48
+ surfaceTint = Color.Transparent,
49
+ surfaceContainerLowest = __THEME_PREFIX__Colors.Surface,
50
+ surfaceContainerLow = __THEME_PREFIX__Colors.Surface,
51
+ surfaceContainer = __THEME_PREFIX__Colors.Surface,
52
+ surfaceContainerHigh = __THEME_PREFIX__Colors.Surface,
53
+ surfaceContainerHighest = __THEME_PREFIX__Colors.SurfaceVariant,
54
+ )
55
+
56
+ @Composable
57
+ fun __THEME_PREFIX__Theme(content: @Composable () -> Unit) {
58
+ MaterialTheme(
59
+ colorScheme = __THEME_PREFIX__ColorScheme,
60
+ typography = remember__THEME_PREFIX__Typography(),
61
+ shapes = __THEME_PREFIX__Shapes,
62
+ content = content,
63
+ )
64
+ }
@@ -0,0 +1,19 @@
1
+ package __PACKAGE__.presentation.theme
2
+
3
+ import androidx.compose.ui.unit.dp
4
+
5
+ object __THEME_PREFIX__Tokens {
6
+ val ElevationCard = 2.dp
7
+ val ElevationModal = 8.dp
8
+
9
+ val PaddingPage = 16.dp
10
+ val PaddingCard = 16.dp
11
+ val GapCard = 12.dp
12
+
13
+ val BottomNavHeight = 72.dp
14
+
15
+ val RadiusCard = 16.dp
16
+ val RadiusPill = 999.dp
17
+ val RadiusModal = 24.dp
18
+ val RadiusInput = 14.dp
19
+ }
@@ -0,0 +1,36 @@
1
+ package __PACKAGE__.presentation.theme
2
+
3
+ import androidx.compose.material3.Typography
4
+ import androidx.compose.runtime.Composable
5
+ import androidx.compose.ui.text.TextStyle
6
+ import androidx.compose.ui.text.font.FontFamily
7
+ import androidx.compose.ui.text.font.FontWeight
8
+ import androidx.compose.ui.unit.sp
9
+ import org.jetbrains.compose.resources.Font
10
+ import __PACKAGE__.generated.resources.DMSans_Bold
11
+ import __PACKAGE__.generated.resources.DMSans_Medium
12
+ import __PACKAGE__.generated.resources.DMSans_Regular
13
+ import __PACKAGE__.generated.resources.DMSans_SemiBold
14
+ import __PACKAGE__.generated.resources.Res
15
+
16
+ // DM Sans font family — weights 400/500/600/700 bundled in composeResources/font/.
17
+ val DmSansFontFamily: FontFamily
18
+ @Composable get() = FontFamily(
19
+ Font(Res.font.DMSans_Regular, weight = FontWeight.Normal),
20
+ Font(Res.font.DMSans_Medium, weight = FontWeight.Medium),
21
+ Font(Res.font.DMSans_SemiBold, weight = FontWeight.SemiBold),
22
+ Font(Res.font.DMSans_Bold, weight = FontWeight.Bold),
23
+ )
24
+
25
+ @Composable
26
+ fun remember__THEME_PREFIX__Typography(): Typography {
27
+ val dmSans = DmSansFontFamily
28
+ return Typography(
29
+ displayLarge = TextStyle(fontFamily = dmSans, fontWeight = FontWeight.Bold, fontSize = 32.sp),
30
+ headlineMedium = TextStyle(fontFamily = dmSans, fontWeight = FontWeight.SemiBold, fontSize = 24.sp),
31
+ titleMedium = TextStyle(fontFamily = dmSans, fontWeight = FontWeight.SemiBold, fontSize = 18.sp),
32
+ bodyLarge = TextStyle(fontFamily = dmSans, fontWeight = FontWeight.Normal, fontSize = 16.sp),
33
+ bodyMedium = TextStyle(fontFamily = dmSans, fontWeight = FontWeight.Normal, fontSize = 14.sp),
34
+ labelSmall = TextStyle(fontFamily = dmSans, fontWeight = FontWeight.Medium, fontSize = 12.sp),
35
+ )
36
+ }
@@ -0,0 +1,9 @@
1
+ package __PACKAGE__.core.connectivity
2
+
3
+ import kotlinx.coroutines.flow.MutableStateFlow
4
+ import kotlinx.coroutines.flow.StateFlow
5
+
6
+ // Desktop dev-client: treat the workstation as always online.
7
+ actual class NetworkMonitor actual constructor(context: Any?) {
8
+ actual val isOnline: StateFlow<Boolean> = MutableStateFlow(true)
9
+ }
@@ -0,0 +1,13 @@
1
+ package __PACKAGE__.data.local
2
+
3
+ import androidx.room.Room
4
+ import androidx.room.RoomDatabase
5
+ import java.io.File
6
+
7
+ // Room KMP on the JVM uses the same BundledSQLiteDriver as iOS (set in buildDatabase). The
8
+ // dev-client keeps its cache in the OS temp dir — disposable by design, like the dev loop itself.
9
+ actual fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
10
+ val dbDir = File(System.getProperty("java.io.tmpdir"), "__PACKAGE__-dev-client")
11
+ dbDir.mkdirs()
12
+ return Room.databaseBuilder<AppDatabase>(name = File(dbDir, "app.db").absolutePath)
13
+ }
@@ -0,0 +1,32 @@
1
+ package __PACKAGE__.di
2
+
3
+ import __PACKAGE__.core.connectivity.NetworkMonitor
4
+ // >>> cmp:feature room
5
+ import __PACKAGE__.data.local.AppDatabase
6
+ import __PACKAGE__.data.local.buildDatabase
7
+ // <<< cmp:feature room
8
+ import org.koin.core.context.startKoin
9
+ import org.koin.dsl.module
10
+
11
+ // Desktop (JVM) DI bindings for the dev-client window.
12
+ //
13
+ // No Firebase here — the dev-client runs fully offline. The example feature's data source
14
+ // (ItemRepositoryImpl, bound in appModules) is already an in-memory implementation serving the
15
+ // same sample data on every platform, so nothing needs swapping out of the box. When you add a
16
+ // real remote-backed repository, bind its desktop fake in THIS module so the dev-client keeps
17
+ // working without a backend:
18
+ //
19
+ // single<MyRepository> { InMemoryMyRepository() } // shadows the remote binding on desktop
20
+ val desktopModule = module {
21
+ // >>> cmp:feature room
22
+ single<AppDatabase> { buildDatabase() }
23
+ // <<< cmp:feature room
24
+ single { NetworkMonitor(null) }
25
+ }
26
+
27
+ // Mirrors AppApplication (Android) / KoinHelper (iOS) — Koin start for the desktop entry point.
28
+ fun initDesktopKoin() {
29
+ startKoin {
30
+ modules(desktopModule, *appModules.toTypedArray())
31
+ }
32
+ }
@@ -0,0 +1,29 @@
1
+ package __PACKAGE__
2
+
3
+ import androidx.compose.ui.unit.DpSize
4
+ import androidx.compose.ui.unit.dp
5
+ import androidx.compose.ui.window.Window
6
+ import androidx.compose.ui.window.application
7
+ import androidx.compose.ui.window.rememberWindowState
8
+ import __PACKAGE__.di.initDesktopKoin
9
+
10
+ // Desktop dev-client: the shared commonMain UI in a live, clickable JVM window, sized like a
11
+ // phone. This is the daily dev loop — run it with Compose Hot Reload and watch edits land
12
+ // without restarting:
13
+ //
14
+ // ./gradlew :composeApp:hotRunDesktop --auto # hot reload, auto-reload on save
15
+ // ./gradlew :composeApp:run # plain run (any JDK, no hot reload)
16
+ //
17
+ // See docs/dev-client.md for the full loop.
18
+ fun main() {
19
+ initDesktopKoin()
20
+ application {
21
+ Window(
22
+ onCloseRequest = ::exitApplication,
23
+ title = "__APP_NAME__ dev-client",
24
+ state = rememberWindowState(size = DpSize(411.dp, 891.dp)),
25
+ ) {
26
+ App()
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,6 @@
1
+ package __PACKAGE__.presentation.components
2
+
3
+ import androidx.compose.ui.Modifier
4
+
5
+ /** Desktop: no resource-id concept (skiko `ui` lacks testTagsAsResourceId) — no-op. */
6
+ actual fun Modifier.exposeTestTagsForAutomation(): Modifier = this
@@ -0,0 +1,50 @@
1
+ package __PACKAGE__
2
+
3
+ import __PACKAGE__.core.connectivity.NetworkMonitor
4
+ // >>> cmp:feature room
5
+ import __PACKAGE__.data.local.AppDatabase
6
+ import __PACKAGE__.data.local.buildDatabase
7
+ // <<< cmp:feature room
8
+ // >>> cmp:feature firebase
9
+ import __PACKAGE__.data.remote.FIREBASE_FUNCTIONS_REGION
10
+ import dev.gitlive.firebase.Firebase
11
+ import dev.gitlive.firebase.auth.auth
12
+ import dev.gitlive.firebase.firestore.firestore
13
+ import dev.gitlive.firebase.functions.functions
14
+ import dev.gitlive.firebase.storage.storage
15
+ // <<< cmp:feature firebase
16
+ import __PACKAGE__.di.appModules
17
+ import org.koin.core.context.startKoin
18
+ import org.koin.dsl.module
19
+
20
+ // >>> cmp:feature firebase
21
+ // Debug/QA: point GitLive Firebase at the local emulators. The iOS simulator shares the host
22
+ // network, so 127.0.0.1 reaches the emulators directly. Requires FirebaseApp.configure() to
23
+ // have run first (done in iOSApp.swift AppDelegate).
24
+ private fun configureFirebaseEmulators() {
25
+ val host = "127.0.0.1"
26
+ runCatching {
27
+ Firebase.auth.useEmulator(host, 9099)
28
+ Firebase.firestore.useEmulator(host, 8080)
29
+ Firebase.functions(FIREBASE_FUNCTIONS_REGION).useEmulator(host, 5001)
30
+ Firebase.storage.useEmulator(host, 9199)
31
+ }
32
+ }
33
+ // <<< cmp:feature firebase
34
+
35
+ fun initKoin() {
36
+ // >>> cmp:feature firebase
37
+ configureFirebaseEmulators()
38
+ // <<< cmp:feature firebase
39
+ startKoin {
40
+ modules(
41
+ module {
42
+ // >>> cmp:feature room
43
+ single<AppDatabase> { buildDatabase() }
44
+ // <<< cmp:feature room
45
+ single { NetworkMonitor(null) }
46
+ },
47
+ *appModules.toTypedArray()
48
+ )
49
+ }
50
+ }
@@ -0,0 +1,10 @@
1
+ package __PACKAGE__
2
+
3
+ import androidx.compose.ui.window.ComposeUIViewController
4
+
5
+ // Compose Multiplatform iOS entry point, bridged from ContentView.swift via ComposeView.
6
+ // CMP 1.10: the iOS accessibility tree syncs automatically, so Compose testTag values are
7
+ // exposed to XCUITest/Appium as accessibilityIdentifier without extra config.
8
+ fun MainViewController() = ComposeUIViewController {
9
+ App()
10
+ }
@@ -0,0 +1,32 @@
1
+ package __PACKAGE__.core.connectivity
2
+
3
+ import kotlinx.coroutines.flow.MutableStateFlow
4
+ import kotlinx.coroutines.flow.StateFlow
5
+ import kotlinx.coroutines.flow.asStateFlow
6
+ import platform.Network.nw_path_get_status
7
+ import platform.Network.nw_path_monitor_create
8
+ import platform.Network.nw_path_monitor_set_queue
9
+ import platform.Network.nw_path_monitor_set_update_handler
10
+ import platform.Network.nw_path_monitor_start
11
+ import platform.Network.nw_path_status_satisfied
12
+ import platform.darwin.DISPATCH_QUEUE_PRIORITY_DEFAULT
13
+ import platform.darwin.dispatch_get_global_queue
14
+
15
+ actual class NetworkMonitor actual constructor(context: Any?) {
16
+
17
+ // Kotlin/Native exposes the C nw_path_monitor API (the Obj-C NWPathMonitor wrapper isn't bridged).
18
+ private val monitor = nw_path_monitor_create()
19
+ private val _isOnline = MutableStateFlow(false)
20
+ actual val isOnline: StateFlow<Boolean> = _isOnline.asStateFlow()
21
+
22
+ init {
23
+ nw_path_monitor_set_update_handler(monitor) { path ->
24
+ _isOnline.value = nw_path_get_status(path) == nw_path_status_satisfied
25
+ }
26
+ nw_path_monitor_set_queue(
27
+ monitor,
28
+ dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.toLong(), 0u)
29
+ )
30
+ nw_path_monitor_start(monitor)
31
+ }
32
+ }