geeknative 0.0.0-stage → 0.1.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 (159) hide show
  1. package/LICENSE +202 -0
  2. package/bin/geeknative.js +12 -0
  3. package/package.json +31 -4
  4. package/src/cli.js +61 -0
  5. package/src/codegen/app.js +97 -0
  6. package/src/codegen/icons.js +87 -0
  7. package/src/codegen/index.js +44 -0
  8. package/src/codegen/strings.js +308 -0
  9. package/src/codegen/tokens.js +449 -0
  10. package/src/codegen/util.js +122 -0
  11. package/src/commands/check.js +143 -0
  12. package/src/commands/create.js +78 -0
  13. package/src/commands/doctor.js +108 -0
  14. package/src/commands/gen.js +44 -0
  15. package/src/commands/parity.js +134 -0
  16. package/src/commands/run.js +91 -0
  17. package/src/lib/exec.js +77 -0
  18. package/src/lib/fsutil.js +92 -0
  19. package/src/lib/log.js +37 -0
  20. package/src/lib/managed.js +80 -0
  21. package/src/lib/project.js +52 -0
  22. package/src/lib/toolchains.js +126 -0
  23. package/template/.claude/agents/android-engineer.md +15 -0
  24. package/template/.claude/agents/ios-engineer.md +15 -0
  25. package/template/.claude/agents/web-engineer.md +15 -0
  26. package/template/.claude/settings.json +65 -0
  27. package/template/.geeknative/framework.mk +41 -0
  28. package/template/.geeknative/hooks/after-edit.mjs +28 -0
  29. package/template/.geeknative/hooks/parity-guard.mjs +49 -0
  30. package/template/.geeknative/hooks/subagent-context.mjs +24 -0
  31. package/template/.geeknative/playbooks/bug.md +7 -0
  32. package/template/.geeknative/playbooks/design.md +10 -0
  33. package/template/.geeknative/playbooks/engineering.md +9 -0
  34. package/template/.geeknative/playbooks/feature.md +11 -0
  35. package/template/.geeknative/playbooks/platform-specific.md +8 -0
  36. package/template/.geeknative/playbooks/product.md +12 -0
  37. package/template/.geeknative/templates/feature.md +49 -0
  38. package/template/.gemini/settings.json +5 -0
  39. package/template/.github/dependabot.yml +18 -0
  40. package/template/AGENTS.md +97 -0
  41. package/template/CLAUDE.md +9 -0
  42. package/template/DESIGN.md +65 -0
  43. package/template/Makefile +6 -0
  44. package/template/PRODUCT.md +40 -0
  45. package/template/README.md +28 -0
  46. package/template/_gitignore +33 -0
  47. package/template/apps/android/AGENTS.md +66 -0
  48. package/template/apps/android/CLAUDE.md +1 -0
  49. package/template/apps/android/app/build.gradle.kts +68 -0
  50. package/template/apps/android/app/identity.properties +4 -0
  51. package/template/apps/android/app/src/main/AndroidManifest.xml +24 -0
  52. package/template/apps/android/app/src/main/kotlin/app/App.kt +15 -0
  53. package/template/apps/android/app/src/main/kotlin/app/MainActivity.kt +49 -0
  54. package/template/apps/android/app/src/main/kotlin/app/core/AppContainer.kt +27 -0
  55. package/template/apps/android/app/src/main/kotlin/app/core/designsystem/AppCard.kt +30 -0
  56. package/template/apps/android/app/src/main/kotlin/app/core/designsystem/AppTheme.kt +35 -0
  57. package/template/apps/android/app/src/main/kotlin/app/core/navigation/AppNavigation.kt +40 -0
  58. package/template/apps/android/app/src/main/kotlin/app/core/navigation/Route.kt +13 -0
  59. package/template/apps/android/app/src/main/kotlin/app/features/home/HomeScreen.kt +67 -0
  60. package/template/apps/android/app/src/main/kotlin/app/features/settings/Appearance.kt +22 -0
  61. package/template/apps/android/app/src/main/kotlin/app/features/settings/SettingsRepository.kt +40 -0
  62. package/template/apps/android/app/src/main/kotlin/app/features/settings/SettingsScreen.kt +113 -0
  63. package/template/apps/android/app/src/main/kotlin/app/features/settings/SettingsViewModel.kt +28 -0
  64. package/template/apps/android/app/src/main/kotlin/app/generated/AppIdentity.kt +10 -0
  65. package/template/apps/android/app/src/main/kotlin/app/generated/Icons.kt +15 -0
  66. package/template/apps/android/app/src/main/kotlin/app/generated/Tokens.kt +144 -0
  67. package/template/apps/android/app/src/main/res/drawable/ic_launcher_foreground.xml +17 -0
  68. package/template/apps/android/app/src/main/res/mipmap/ic_launcher.xml +10 -0
  69. package/template/apps/android/app/src/main/res/mipmap/ic_launcher_round.xml +10 -0
  70. package/template/apps/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
  71. package/template/apps/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
  72. package/template/apps/android/app/src/main/res/values/generated_colors.xml +15 -0
  73. package/template/apps/android/app/src/main/res/values/generated_strings.xml +16 -0
  74. package/template/apps/android/app/src/main/res/values/ic_launcher_background.xml +4 -0
  75. package/template/apps/android/app/src/main/res/values/themes.xml +7 -0
  76. package/template/apps/android/app/src/main/res/values-night/generated_colors.xml +15 -0
  77. package/template/apps/android/app/src/main/res/values-night/themes.xml +6 -0
  78. package/template/apps/android/app/src/test/kotlin/app/features/settings/SettingsTest.kt +90 -0
  79. package/template/apps/android/build.gradle.kts +5 -0
  80. package/template/apps/android/gradle/gradle-daemon-jvm.properties +12 -0
  81. package/template/apps/android/gradle/libs.versions.toml +37 -0
  82. package/template/apps/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  83. package/template/apps/android/gradle/wrapper/gradle-wrapper.properties +9 -0
  84. package/template/apps/android/gradle.properties +7 -0
  85. package/template/apps/android/gradlew +248 -0
  86. package/template/apps/android/gradlew.bat +82 -0
  87. package/template/apps/android/settings.gradle.kts +29 -0
  88. package/template/apps/ios/AGENTS.md +70 -0
  89. package/template/apps/ios/App/AppMain.swift +13 -0
  90. package/template/apps/ios/App/Core/AppContainer.swift +22 -0
  91. package/template/apps/ios/App/Core/DesignSystem/AppCard.swift +15 -0
  92. package/template/apps/ios/App/Core/Navigation/Route.swift +4 -0
  93. package/template/apps/ios/App/Core/RootView.swift +32 -0
  94. package/template/apps/ios/App/Features/Home/HomeScreen.swift +35 -0
  95. package/template/apps/ios/App/Features/Settings/Appearance.swift +27 -0
  96. package/template/apps/ios/App/Features/Settings/SettingsRepository.swift +40 -0
  97. package/template/apps/ios/App/Features/Settings/SettingsScreen.swift +40 -0
  98. package/template/apps/ios/App/Features/Settings/SettingsViewModel.swift +23 -0
  99. package/template/apps/ios/App/Generated/AppIdentity.swift +9 -0
  100. package/template/apps/ios/App/Generated/Colors.xcassets/AccentColor.colorset/Contents.json +38 -0
  101. package/template/apps/ios/App/Generated/Colors.xcassets/Contents.json +6 -0
  102. package/template/apps/ios/App/Generated/Icons.swift +8 -0
  103. package/template/apps/ios/App/Generated/Localizable.xcstrings +127 -0
  104. package/template/apps/ios/App/Generated/Strings.swift +40 -0
  105. package/template/apps/ios/App/Generated/Tokens.swift +65 -0
  106. package/template/apps/ios/App/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png +0 -0
  107. package/template/apps/ios/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
  108. package/template/apps/ios/App/Resources/Assets.xcassets/Contents.json +6 -0
  109. package/template/apps/ios/App.xcodeproj/project.pbxproj +298 -0
  110. package/template/apps/ios/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  111. package/template/apps/ios/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme +89 -0
  112. package/template/apps/ios/AppTests/SettingsTests.swift +54 -0
  113. package/template/apps/ios/CLAUDE.md +1 -0
  114. package/template/apps/ios/Config/App.xcconfig +23 -0
  115. package/template/apps/ios/Config/AppTests.xcconfig +10 -0
  116. package/template/apps/ios/Config/Base.xcconfig +25 -0
  117. package/template/apps/ios/Config/Debug.xcconfig +9 -0
  118. package/template/apps/ios/Config/Identity.xcconfig +5 -0
  119. package/template/apps/ios/Config/Release.xcconfig +6 -0
  120. package/template/apps/web/AGENTS.md +63 -0
  121. package/template/apps/web/CLAUDE.md +1 -0
  122. package/template/apps/web/package.json +38 -0
  123. package/template/apps/web/public/favicon.svg +6 -0
  124. package/template/apps/web/src/core/AppContainer.ts +18 -0
  125. package/template/apps/web/src/core/AppearanceEffect.tsx +31 -0
  126. package/template/apps/web/src/core/designsystem/AppCard.tsx +6 -0
  127. package/template/apps/web/src/features/home/HomeScreen.tsx +15 -0
  128. package/template/apps/web/src/features/home/home.test.tsx +22 -0
  129. package/template/apps/web/src/features/settings/Appearance.ts +29 -0
  130. package/template/apps/web/src/features/settings/LocalStorageSettingsRepository.ts +21 -0
  131. package/template/apps/web/src/features/settings/SettingsRepository.ts +7 -0
  132. package/template/apps/web/src/features/settings/SettingsScreen.tsx +44 -0
  133. package/template/apps/web/src/features/settings/settings.test.ts +45 -0
  134. package/template/apps/web/src/features/settings/useSettingsViewModel.ts +42 -0
  135. package/template/apps/web/src/generated/app.ts +9 -0
  136. package/template/apps/web/src/generated/icons.ts +9 -0
  137. package/template/apps/web/src/generated/strings.ts +88 -0
  138. package/template/apps/web/src/generated/tokens.css +86 -0
  139. package/template/apps/web/src/routeTree.gen.ts +86 -0
  140. package/template/apps/web/src/router.tsx +23 -0
  141. package/template/apps/web/src/routes/__root.tsx +72 -0
  142. package/template/apps/web/src/routes/index.tsx +6 -0
  143. package/template/apps/web/src/routes/settings.tsx +9 -0
  144. package/template/apps/web/src/styles.css +9 -0
  145. package/template/apps/web/tsconfig.json +26 -0
  146. package/template/apps/web/tsr.config.json +3 -0
  147. package/template/apps/web/vite.config.ts +9 -0
  148. package/template/apps/web/vitest.config.ts +9 -0
  149. package/template/contracts/app.json +7 -0
  150. package/template/contracts/icons.json +5 -0
  151. package/template/contracts/strings/en.json +13 -0
  152. package/template/contracts/tokens.json +40 -0
  153. package/template/package.json +17 -0
  154. package/template/spec/decisions.md +10 -0
  155. package/template/spec/features/home.md +39 -0
  156. package/template/spec/features/settings.md +50 -0
  157. package/template/spec/navigation.md +16 -0
  158. package/template/spec/parity.md +29 -0
  159. package/README.md +0 -3
@@ -0,0 +1,144 @@
1
+ // GENERATED by geeknative from contracts/tokens.json. Do not edit: change the contract, then run `make gen`.
2
+ package app.generated
3
+
4
+ import androidx.compose.foundation.shape.RoundedCornerShape
5
+ import androidx.compose.material3.ColorScheme
6
+ import androidx.compose.material3.Shapes
7
+ import androidx.compose.material3.Typography
8
+ import androidx.compose.material3.darkColorScheme
9
+ import androidx.compose.material3.lightColorScheme
10
+ import androidx.compose.runtime.Immutable
11
+ import androidx.compose.ui.graphics.Color
12
+ import androidx.compose.ui.text.TextStyle
13
+ import androidx.compose.ui.text.font.FontWeight
14
+ import androidx.compose.ui.unit.dp
15
+ import androidx.compose.ui.unit.sp
16
+
17
+ /** Colors for one appearance (light or dark). In composables, read the current set with `AppTheme.colors`. */
18
+ @Immutable
19
+ data class AppColorTokens(
20
+ val background: Color,
21
+ val onBackground: Color,
22
+ val surface: Color,
23
+ val onSurface: Color,
24
+ val surfaceVariant: Color,
25
+ val onSurfaceVariant: Color,
26
+ val primary: Color,
27
+ val onPrimary: Color,
28
+ val outline: Color,
29
+ val error: Color,
30
+ val onError: Color,
31
+ )
32
+
33
+ val LightAppColors = AppColorTokens(
34
+ background = Color(0xFFFFFFFF),
35
+ onBackground = Color(0xFF0F172A),
36
+ surface = Color(0xFFF4F5F8),
37
+ onSurface = Color(0xFF0F172A),
38
+ surfaceVariant = Color(0xFFE9EBF1),
39
+ onSurfaceVariant = Color(0xFF566074),
40
+ primary = Color(0xFF4F46E5),
41
+ onPrimary = Color(0xFFFFFFFF),
42
+ outline = Color(0xFFD8DCE5),
43
+ error = Color(0xFFDC2626),
44
+ onError = Color(0xFFFFFFFF),
45
+ )
46
+
47
+ val DarkAppColors = AppColorTokens(
48
+ background = Color(0xFF0B0D12),
49
+ onBackground = Color(0xFFE6E8EE),
50
+ surface = Color(0xFF161A23),
51
+ onSurface = Color(0xFFE6E8EE),
52
+ surfaceVariant = Color(0xFF222735),
53
+ onSurfaceVariant = Color(0xFFA3ABBD),
54
+ primary = Color(0xFFA5B4FC),
55
+ onPrimary = Color(0xFF1E1B4B),
56
+ outline = Color(0xFF2D3343),
57
+ error = Color(0xFFF87171),
58
+ onError = Color(0xFF450A0A),
59
+ )
60
+
61
+ /** Maps the tokens onto Material 3 color roles, so stock components match the design. */
62
+ fun AppColorTokens.toColorScheme(dark: Boolean): ColorScheme =
63
+ (if (dark) darkColorScheme() else lightColorScheme()).copy(
64
+ primary = primary,
65
+ onPrimary = onPrimary,
66
+ primaryContainer = surfaceVariant,
67
+ onPrimaryContainer = onSurface,
68
+ inversePrimary = primary,
69
+ secondary = primary,
70
+ onSecondary = onPrimary,
71
+ secondaryContainer = surfaceVariant,
72
+ onSecondaryContainer = onSurface,
73
+ tertiary = primary,
74
+ onTertiary = onPrimary,
75
+ tertiaryContainer = surfaceVariant,
76
+ onTertiaryContainer = onSurface,
77
+ background = background,
78
+ onBackground = onBackground,
79
+ surface = background,
80
+ onSurface = onBackground,
81
+ surfaceVariant = surfaceVariant,
82
+ onSurfaceVariant = onSurfaceVariant,
83
+ surfaceTint = primary,
84
+ inverseSurface = onBackground,
85
+ inverseOnSurface = background,
86
+ error = error,
87
+ onError = onError,
88
+ outline = outline,
89
+ outlineVariant = outline,
90
+ surfaceBright = background,
91
+ surfaceDim = surfaceVariant,
92
+ surfaceContainerLowest = background,
93
+ surfaceContainerLow = surface,
94
+ surfaceContainer = surface,
95
+ surfaceContainerHigh = surface,
96
+ surfaceContainerHighest = surface,
97
+ )
98
+
99
+ /** Spacing. */
100
+ object AppSpacing {
101
+ val xs = 4.dp
102
+ val sm = 8.dp
103
+ val md = 12.dp
104
+ val lg = 16.dp
105
+ val xl = 24.dp
106
+ val xxl = 32.dp
107
+ }
108
+
109
+ /** Corner radii. */
110
+ object AppRadius {
111
+ val sm = 6.dp
112
+ val md = 10.dp
113
+ val lg = 16.dp
114
+ val full = 9999.dp
115
+ }
116
+
117
+ /** Text styles, in sp so they follow the user's font size setting. */
118
+ object AppTypography {
119
+ val display = TextStyle(fontWeight = FontWeight.Bold, fontSize = 34.sp, lineHeight = 41.sp)
120
+ val headline = TextStyle(fontWeight = FontWeight.Bold, fontSize = 28.sp, lineHeight = 34.sp)
121
+ val title = TextStyle(fontWeight = FontWeight.SemiBold, fontSize = 20.sp, lineHeight = 26.sp)
122
+ val body = TextStyle(fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp)
123
+ val label = TextStyle(fontWeight = FontWeight.Medium, fontSize = 14.sp, lineHeight = 20.sp)
124
+ val caption = TextStyle(fontWeight = FontWeight.Normal, fontSize = 12.sp, lineHeight = 16.sp)
125
+ }
126
+
127
+ /** The text styles mapped onto Material 3 type roles, so stock components use them. */
128
+ val AppMaterialTypography = Typography().copy(
129
+ displaySmall = AppTypography.display,
130
+ headlineMedium = AppTypography.headline,
131
+ titleLarge = AppTypography.title,
132
+ bodyLarge = AppTypography.body,
133
+ labelLarge = AppTypography.label,
134
+ bodySmall = AppTypography.caption,
135
+ )
136
+
137
+ /** The radii mapped onto Material 3 shape roles. */
138
+ val AppShapes = Shapes(
139
+ extraSmall = RoundedCornerShape(AppRadius.sm),
140
+ small = RoundedCornerShape(AppRadius.sm),
141
+ medium = RoundedCornerShape(AppRadius.md),
142
+ large = RoundedCornerShape(AppRadius.lg),
143
+ extraLarge = RoundedCornerShape(AppRadius.lg),
144
+ )
@@ -0,0 +1,17 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Placeholder launcher icon: three dots, one per platform. Replace with your own. -->
3
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
4
+ android:width="108dp"
5
+ android:height="108dp"
6
+ android:viewportWidth="108"
7
+ android:viewportHeight="108">
8
+ <path
9
+ android:fillColor="#FFFFFF"
10
+ android:pathData="M31,54a5.5,5.5 0,1 0,11 0a5.5,5.5 0,1 0,-11 0" />
11
+ <path
12
+ android:fillColor="#FFFFFF"
13
+ android:pathData="M48.5,54a5.5,5.5 0,1 0,11 0a5.5,5.5 0,1 0,-11 0" />
14
+ <path
15
+ android:fillColor="#FFFFFF"
16
+ android:pathData="M66,54a5.5,5.5 0,1 0,11 0a5.5,5.5 0,1 0,-11 0" />
17
+ </vector>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Launcher icon for Android 7.x, which has no adaptive icons. -->
3
+ <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
4
+ <item>
5
+ <shape android:shape="oval">
6
+ <solid android:color="@color/ic_launcher_background" />
7
+ </shape>
8
+ </item>
9
+ <item android:drawable="@drawable/ic_launcher_foreground" />
10
+ </layer-list>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Launcher icon for Android 7.x, which has no adaptive icons. -->
3
+ <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
4
+ <item>
5
+ <shape android:shape="oval">
6
+ <solid android:color="@color/ic_launcher_background" />
7
+ </shape>
8
+ </item>
9
+ <item android:drawable="@drawable/ic_launcher_foreground" />
10
+ </layer-list>
@@ -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="@color/ic_launcher_background" />
4
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
5
+ <monochrome android:drawable="@drawable/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="@color/ic_launcher_background" />
4
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
5
+ <monochrome android:drawable="@drawable/ic_launcher_foreground" />
6
+ </adaptive-icon>
@@ -0,0 +1,15 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- GENERATED by geeknative from contracts/tokens.json. Do not edit: change the contract, then run `make gen`. -->
3
+ <resources>
4
+ <color name="token_background">#FFFFFFFF</color>
5
+ <color name="token_on_background">#FF0F172A</color>
6
+ <color name="token_surface">#FFF4F5F8</color>
7
+ <color name="token_on_surface">#FF0F172A</color>
8
+ <color name="token_surface_variant">#FFE9EBF1</color>
9
+ <color name="token_on_surface_variant">#FF566074</color>
10
+ <color name="token_primary">#FF4F46E5</color>
11
+ <color name="token_on_primary">#FFFFFFFF</color>
12
+ <color name="token_outline">#FFD8DCE5</color>
13
+ <color name="token_error">#FFDC2626</color>
14
+ <color name="token_on_error">#FFFFFFFF</color>
15
+ </resources>
@@ -0,0 +1,16 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- GENERATED by geeknative from contracts/strings. Do not edit: change the contract, then run `make gen`. -->
3
+ <resources>
4
+ <string name="app_name" translatable="false">GeekNative</string>
5
+ <string name="common_back">Back</string>
6
+ <string name="home_open_settings">Settings</string>
7
+ <string name="home_welcome_body">This app is built natively for iOS, Android and the web from one shared spec. Describe what you want to build to your coding agent, and it will build it for all three.</string>
8
+ <string name="home_welcome_title">You\'re all set</string>
9
+ <string name="settings_about_title">About</string>
10
+ <string name="settings_about_version">Version %1$s (%2$d)</string>
11
+ <string name="settings_appearance_dark">Dark</string>
12
+ <string name="settings_appearance_light">Light</string>
13
+ <string name="settings_appearance_system">System</string>
14
+ <string name="settings_appearance_title">Appearance</string>
15
+ <string name="settings_title">Settings</string>
16
+ </resources>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ <color name="ic_launcher_background">#FF4F46E5</color>
4
+ </resources>
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ <!-- The window behind Compose. Colors come from contracts/tokens.json (generated_colors.xml). -->
4
+ <style name="Theme.App" parent="android:Theme.Material.Light.NoActionBar">
5
+ <item name="android:windowBackground">@color/token_background</item>
6
+ </style>
7
+ </resources>
@@ -0,0 +1,15 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- GENERATED by geeknative from contracts/tokens.json. Do not edit: change the contract, then run `make gen`. -->
3
+ <resources>
4
+ <color name="token_background">#FF0B0D12</color>
5
+ <color name="token_on_background">#FFE6E8EE</color>
6
+ <color name="token_surface">#FF161A23</color>
7
+ <color name="token_on_surface">#FFE6E8EE</color>
8
+ <color name="token_surface_variant">#FF222735</color>
9
+ <color name="token_on_surface_variant">#FFA3ABBD</color>
10
+ <color name="token_primary">#FFA5B4FC</color>
11
+ <color name="token_on_primary">#FF1E1B4B</color>
12
+ <color name="token_outline">#FF2D3343</color>
13
+ <color name="token_error">#FFF87171</color>
14
+ <color name="token_on_error">#FF450A0A</color>
15
+ </resources>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ <style name="Theme.App" parent="android:Theme.Material.NoActionBar">
4
+ <item name="android:windowBackground">@color/token_background</item>
5
+ </style>
6
+ </resources>
@@ -0,0 +1,90 @@
1
+ package app.features.settings
2
+
3
+ import androidx.datastore.preferences.core.PreferenceDataStoreFactory
4
+ import app.generated.AppIdentity
5
+ import java.io.File
6
+ import kotlinx.coroutines.CoroutineScope
7
+ import kotlinx.coroutines.Dispatchers
8
+ import kotlinx.coroutines.ExperimentalCoroutinesApi
9
+ import kotlinx.coroutines.Job
10
+ import kotlinx.coroutines.cancelAndJoin
11
+ import kotlinx.coroutines.flow.first
12
+ import kotlinx.coroutines.launch
13
+ import kotlinx.coroutines.test.UnconfinedTestDispatcher
14
+ import kotlinx.coroutines.test.resetMain
15
+ import kotlinx.coroutines.test.runTest
16
+ import kotlinx.coroutines.test.setMain
17
+ import org.junit.After
18
+ import org.junit.Assert.assertEquals
19
+ import org.junit.Assert.assertFalse
20
+ import org.junit.Assert.assertTrue
21
+ import org.junit.Before
22
+ import org.junit.Rule
23
+ import org.junit.Test
24
+ import org.junit.rules.TemporaryFolder
25
+
26
+ @OptIn(ExperimentalCoroutinesApi::class)
27
+ class SettingsTest {
28
+ @get:Rule
29
+ val folder = TemporaryFolder()
30
+
31
+ @Before
32
+ fun setUp() {
33
+ Dispatchers.setMain(UnconfinedTestDispatcher())
34
+ }
35
+
36
+ @After
37
+ fun tearDown() {
38
+ Dispatchers.resetMain()
39
+ }
40
+
41
+ /** A DataStore-backed repository over [file], living in its own scope (one "app launch"). */
42
+ private fun launchRepository(file: File, scope: CoroutineScope) =
43
+ DataStoreSettingsRepository(PreferenceDataStoreFactory.create(scope = scope, produceFile = { file }))
44
+
45
+ @Test
46
+ fun `AC-settings-1 appearance defaults to System`() = runTest {
47
+ val scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler) + Job())
48
+ val repository = launchRepository(File(folder.root, "settings.preferences_pb"), scope)
49
+
50
+ assertEquals(Appearance.System, repository.appearance.first())
51
+ assertEquals(Appearance.System, Appearance.fromId("sepia"))
52
+ scope.coroutineContext[Job]?.cancelAndJoin()
53
+ }
54
+
55
+ @Test
56
+ fun `AC-settings-2 choosing an appearance applies it to the whole app immediately`() = runTest {
57
+ val repository = InMemorySettingsRepository()
58
+ val viewModel = SettingsViewModel(repository)
59
+ backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { viewModel.uiState.collect {} }
60
+
61
+ viewModel.selectAppearance(Appearance.Dark)
62
+
63
+ // MainActivity themes the whole app from the repository's appearance.
64
+ assertEquals(Appearance.Dark, repository.appearance.first())
65
+ assertEquals(Appearance.Dark, viewModel.uiState.value.appearance)
66
+ assertTrue(Appearance.Dark.isDark(systemIsDark = false))
67
+ assertTrue(Appearance.System.isDark(systemIsDark = true))
68
+ assertFalse(Appearance.Light.isDark(systemIsDark = true))
69
+ }
70
+
71
+ @Test
72
+ fun `AC-settings-3 the chosen appearance survives a restart`() = runTest {
73
+ val file = File(folder.root, "settings.preferences_pb")
74
+
75
+ val firstLaunch = CoroutineScope(UnconfinedTestDispatcher(testScheduler) + Job())
76
+ launchRepository(file, firstLaunch).setAppearance(Appearance.Light)
77
+ firstLaunch.coroutineContext[Job]?.cancelAndJoin()
78
+
79
+ val secondLaunch = CoroutineScope(UnconfinedTestDispatcher(testScheduler) + Job())
80
+ assertEquals(Appearance.Light, launchRepository(file, secondLaunch).appearance.first())
81
+ secondLaunch.coroutineContext[Job]?.cancelAndJoin()
82
+ }
83
+
84
+ @Test
85
+ fun `AC-settings-4 shows the app version and build number`() {
86
+ val state = SettingsUiState()
87
+ assertEquals(AppIdentity.VERSION, state.version)
88
+ assertEquals(AppIdentity.BUILD, state.build)
89
+ }
90
+ }
@@ -0,0 +1,5 @@
1
+ plugins {
2
+ alias(libs.plugins.android.application) apply false
3
+ alias(libs.plugins.kotlin.compose) apply false
4
+ alias(libs.plugins.kotlin.serialization) apply false
5
+ }
@@ -0,0 +1,12 @@
1
+ #This file is generated by :updateDaemonJvm
2
+ toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/cb205e54cea236622eee3d62deb2c768/redirect
3
+ toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/452acd2202c1005abda962c6dae7fc5f/redirect
4
+ toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/cb205e54cea236622eee3d62deb2c768/redirect
5
+ toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/452acd2202c1005abda962c6dae7fc5f/redirect
6
+ toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/faa59212208d027d066d1bbe337c5da5/redirect
7
+ toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/adf5b89d64773eff73f900e61dd2d632/redirect
8
+ toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/cb205e54cea236622eee3d62deb2c768/redirect
9
+ toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/452acd2202c1005abda962c6dae7fc5f/redirect
10
+ toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e0e37f85a97c5f2df685b37b14a25072/redirect
11
+ toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ccc3884e65cd85bb71a09d2223c77229/redirect
12
+ toolchainVersion=21
@@ -0,0 +1,37 @@
1
+ [versions]
2
+ agp = "9.4.1"
3
+ kotlin = "2.4.21"
4
+ composeBom = "2026.09.00"
5
+ materialIcons = "1.7.8"
6
+ activityCompose = "1.13.0"
7
+ coreKtx = "1.19.1"
8
+ lifecycle = "2.11.0"
9
+ navigation3 = "1.2.0"
10
+ datastore = "1.2.1"
11
+ kotlinxSerialization = "1.11.0"
12
+ coroutines = "1.11.0"
13
+ junit = "4.13.2"
14
+
15
+ [libraries]
16
+ androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
17
+ androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
18
+ androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
19
+ androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
20
+ androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
21
+ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
22
+ androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
23
+ androidx-compose-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core", version.ref = "materialIcons" }
24
+ androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
25
+ androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
26
+ androidx-lifecycle-viewmodel-navigation3 = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-navigation3", version.ref = "lifecycle" }
27
+ androidx-navigation3-runtime = { group = "androidx.navigation3", name = "navigation3-runtime", version.ref = "navigation3" }
28
+ androidx-navigation3-ui = { group = "androidx.navigation3", name = "navigation3-ui", version.ref = "navigation3" }
29
+ androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
30
+ kotlinx-serialization-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-core", version.ref = "kotlinxSerialization" }
31
+ kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" }
32
+ junit = { group = "junit", name = "junit", version.ref = "junit" }
33
+
34
+ [plugins]
35
+ android-application = { id = "com.android.application", version.ref = "agp" }
36
+ kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
37
+ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
@@ -0,0 +1,9 @@
1
+ distributionBase=GRADLE_USER_HOME
2
+ distributionPath=wrapper/dists
3
+ distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
4
+ networkTimeout=10000
5
+ retries=0
6
+ retryBackOffMs=500
7
+ validateDistributionUrl=true
8
+ zipStoreBase=GRADLE_USER_HOME
9
+ zipStorePath=wrapper/dists
@@ -0,0 +1,7 @@
1
+ org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
2
+ org.gradle.caching=true
3
+ org.gradle.configuration-cache=true
4
+ org.gradle.parallel=true
5
+
6
+ android.nonTransitiveRClass=true
7
+ kotlin.code.style=official