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,9 @@
1
+ // GENERATED by geeknative from contracts/icons.json. Do not edit: change the contract, then run `make gen`.
2
+ import { ArrowLeft, House, Settings } from 'lucide-react'
3
+
4
+ /** Icons as Lucide components. Use as `<AppIcons.settings className="size-6" />`. */
5
+ export const AppIcons = {
6
+ back: ArrowLeft,
7
+ home: House,
8
+ settings: Settings,
9
+ } as const
@@ -0,0 +1,88 @@
1
+ // GENERATED by geeknative from contracts/strings. Do not edit: change the contract, then run `make gen`.
2
+
3
+ export const locales = ["en"] as const
4
+ export type Locale = (typeof locales)[number]
5
+ export const defaultLocale: Locale = "en"
6
+
7
+ type Plural = Partial<Record<Intl.LDMLPluralRule, string>> & { other: string }
8
+
9
+ const messages: Record<Locale, Record<string, string | Plural>> = {
10
+ "en": {
11
+ "common.back": "Back",
12
+ "home.openSettings": "Settings",
13
+ "home.welcomeBody": "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.",
14
+ "home.welcomeTitle": "You're all set",
15
+ "settings.about.title": "About",
16
+ "settings.about.version": "Version {version} ({build:int})",
17
+ "settings.appearance.dark": "Dark",
18
+ "settings.appearance.light": "Light",
19
+ "settings.appearance.system": "System",
20
+ "settings.appearance.title": "Appearance",
21
+ "settings.title": "Settings",
22
+ },
23
+ }
24
+
25
+ let currentLocale: Locale = defaultLocale
26
+
27
+ /** Switches the language `strings` returns. */
28
+ export function setLocale(locale: Locale) {
29
+ currentLocale = locale
30
+ }
31
+
32
+ function lookup(key: string) {
33
+ return messages[currentLocale][key] ?? messages[defaultLocale][key]
34
+ }
35
+
36
+ function text(key: string, args: Record<string, string | number> = {}): string {
37
+ return fill(lookup(key) as string, args)
38
+ }
39
+
40
+ function fill(template: string, args: Record<string, string | number>): string {
41
+ return template.replace(/\{(\w+)(?::\w+)?\}/g, (_, name: string) => String(args[name]))
42
+ }
43
+
44
+ /** User-facing copy. Keys and placeholders match the iOS and Android apps. */
45
+ export const strings = {
46
+ /** Back */
47
+ get commonBack() {
48
+ return text("common.back")
49
+ },
50
+ /** Settings */
51
+ get homeOpenSettings() {
52
+ return text("home.openSettings")
53
+ },
54
+ /** 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. */
55
+ get homeWelcomeBody() {
56
+ return text("home.welcomeBody")
57
+ },
58
+ /** You're all set */
59
+ get homeWelcomeTitle() {
60
+ return text("home.welcomeTitle")
61
+ },
62
+ /** About */
63
+ get settingsAboutTitle() {
64
+ return text("settings.about.title")
65
+ },
66
+ /** Version {version} ({build}) */
67
+ settingsAboutVersion: (args: { version: string; build: number }) => text("settings.about.version", args),
68
+ /** Dark */
69
+ get settingsAppearanceDark() {
70
+ return text("settings.appearance.dark")
71
+ },
72
+ /** Light */
73
+ get settingsAppearanceLight() {
74
+ return text("settings.appearance.light")
75
+ },
76
+ /** System */
77
+ get settingsAppearanceSystem() {
78
+ return text("settings.appearance.system")
79
+ },
80
+ /** Appearance */
81
+ get settingsAppearanceTitle() {
82
+ return text("settings.appearance.title")
83
+ },
84
+ /** Settings */
85
+ get settingsTitle() {
86
+ return text("settings.title")
87
+ },
88
+ }
@@ -0,0 +1,86 @@
1
+ /* GENERATED by geeknative from contracts/tokens.json. Do not edit: change the contract, then run `make gen`. */
2
+
3
+ /*
4
+ * Tailwind theme: bg-primary, text-on-surface, p-md, gap-lg, rounded-lg, text-title, ...
5
+ * Dark values apply when <html> has the "dark" class (see AppearanceEffect).
6
+ */
7
+ @theme {
8
+ --color-background: #ffffff;
9
+ --color-on-background: #0f172a;
10
+ --color-surface: #f4f5f8;
11
+ --color-on-surface: #0f172a;
12
+ --color-surface-variant: #e9ebf1;
13
+ --color-on-surface-variant: #566074;
14
+ --color-primary: #4f46e5;
15
+ --color-on-primary: #ffffff;
16
+ --color-outline: #d8dce5;
17
+ --color-error: #dc2626;
18
+ --color-on-error: #ffffff;
19
+
20
+ /* shadcn/ui names, mapped onto the tokens above */
21
+ --color-foreground: var(--color-on-background);
22
+ --color-card: var(--color-surface);
23
+ --color-card-foreground: var(--color-on-surface);
24
+ --color-popover: var(--color-surface);
25
+ --color-popover-foreground: var(--color-on-surface);
26
+ --color-primary-foreground: var(--color-on-primary);
27
+ --color-secondary: var(--color-surface-variant);
28
+ --color-secondary-foreground: var(--color-on-surface);
29
+ --color-muted: var(--color-surface-variant);
30
+ --color-muted-foreground: var(--color-on-surface-variant);
31
+ --color-accent: var(--color-surface-variant);
32
+ --color-accent-foreground: var(--color-on-surface);
33
+ --color-destructive: var(--color-error);
34
+ --color-destructive-foreground: var(--color-on-error);
35
+ --color-border: var(--color-outline);
36
+ --color-input: var(--color-outline);
37
+ --color-ring: var(--color-primary);
38
+
39
+ --spacing-xs: 4px;
40
+ --spacing-sm: 8px;
41
+ --spacing-md: 12px;
42
+ --spacing-lg: 16px;
43
+ --spacing-xl: 24px;
44
+ --spacing-xxl: 32px;
45
+
46
+ --radius-sm: 6px;
47
+ --radius-md: 10px;
48
+ --radius-lg: 16px;
49
+ --radius-full: calc(infinity * 1px);
50
+
51
+ --text-display: 2.125rem;
52
+ --text-display--line-height: 2.5625rem;
53
+ --text-display--font-weight: 700;
54
+ --text-headline: 1.75rem;
55
+ --text-headline--line-height: 2.125rem;
56
+ --text-headline--font-weight: 700;
57
+ --text-title: 1.25rem;
58
+ --text-title--line-height: 1.625rem;
59
+ --text-title--font-weight: 600;
60
+ --text-body: 1rem;
61
+ --text-body--line-height: 1.5rem;
62
+ --text-body--font-weight: 400;
63
+ --text-label: 0.875rem;
64
+ --text-label--line-height: 1.25rem;
65
+ --text-label--font-weight: 500;
66
+ --text-caption: 0.75rem;
67
+ --text-caption--line-height: 1rem;
68
+ --text-caption--font-weight: 400;
69
+
70
+ --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
71
+ }
72
+
73
+ .dark {
74
+ color-scheme: dark;
75
+ --color-background: #0b0d12;
76
+ --color-on-background: #e6e8ee;
77
+ --color-surface: #161a23;
78
+ --color-on-surface: #e6e8ee;
79
+ --color-surface-variant: #222735;
80
+ --color-on-surface-variant: #a3abbd;
81
+ --color-primary: #a5b4fc;
82
+ --color-on-primary: #1e1b4b;
83
+ --color-outline: #2d3343;
84
+ --color-error: #f87171;
85
+ --color-on-error: #450a0a;
86
+ }
@@ -0,0 +1,86 @@
1
+ /* eslint-disable */
2
+
3
+ // @ts-nocheck
4
+
5
+ // noinspection JSUnusedGlobalSymbols
6
+
7
+ // This file was automatically generated by TanStack Router.
8
+ // You should NOT make any changes in this file as it will be overwritten.
9
+ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
10
+
11
+ import { Route as rootRouteImport } from './routes/__root'
12
+ import { Route as IndexRouteImport } from './routes/index'
13
+ import { Route as SettingsRouteImport } from './routes/settings'
14
+
15
+ const IndexRoute = IndexRouteImport.update({
16
+ id: '/',
17
+ path: '/',
18
+ getParentRoute: () => rootRouteImport,
19
+ } as any)
20
+ const SettingsRoute = SettingsRouteImport.update({
21
+ id: '/settings',
22
+ path: '/settings',
23
+ getParentRoute: () => rootRouteImport,
24
+ } as any)
25
+
26
+ export interface FileRoutesByFullPath {
27
+ '/': typeof IndexRoute
28
+ '/settings': typeof SettingsRoute
29
+ }
30
+ export interface FileRoutesByTo {
31
+ '/': typeof IndexRoute
32
+ '/settings': typeof SettingsRoute
33
+ }
34
+ export interface FileRoutesById {
35
+ __root__: typeof rootRouteImport
36
+ '/': typeof IndexRoute
37
+ '/settings': typeof SettingsRoute
38
+ }
39
+ export interface FileRouteTypes {
40
+ fileRoutesByFullPath: FileRoutesByFullPath
41
+ fullPaths: '/' | '/settings'
42
+ fileRoutesByTo: FileRoutesByTo
43
+ to: '/' | '/settings'
44
+ id: '__root__' | '/' | '/settings'
45
+ fileRoutesById: FileRoutesById
46
+ }
47
+ export interface RootRouteChildren {
48
+ IndexRoute: typeof IndexRoute
49
+ SettingsRoute: typeof SettingsRoute
50
+ }
51
+
52
+ declare module '@tanstack/react-router' {
53
+ interface FileRoutesByPath {
54
+ '/': {
55
+ id: '/'
56
+ path: '/'
57
+ fullPath: '/'
58
+ preLoaderRoute: typeof IndexRouteImport
59
+ parentRoute: typeof rootRouteImport
60
+ }
61
+ '/settings': {
62
+ id: '/settings'
63
+ path: '/settings'
64
+ fullPath: '/settings'
65
+ preLoaderRoute: typeof SettingsRouteImport
66
+ parentRoute: typeof rootRouteImport
67
+ }
68
+ }
69
+ }
70
+
71
+ const rootRouteChildren: RootRouteChildren = {
72
+ IndexRoute: IndexRoute,
73
+ SettingsRoute: SettingsRoute,
74
+ }
75
+ export const routeTree = rootRouteImport
76
+ ._addFileChildren(rootRouteChildren)
77
+ ._addFileTypes<FileRouteTypes>()
78
+
79
+ import type { getRouter } from './router.tsx'
80
+ import type { createStart } from '@tanstack/react-start'
81
+ declare module '@tanstack/react-start' {
82
+ interface Register {
83
+ ssr: true
84
+ router: Awaited<ReturnType<typeof getRouter>>
85
+ }
86
+ }
@@ -0,0 +1,23 @@
1
+ import { QueryClient } from '@tanstack/react-query'
2
+ import { createRouter } from '@tanstack/react-router'
3
+ import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
4
+ import { createAppContainer } from './core/AppContainer'
5
+ import { routeTree } from './routeTree.gen'
6
+
7
+ export function getRouter() {
8
+ const queryClient = new QueryClient()
9
+ const router = createRouter({
10
+ routeTree,
11
+ context: { queryClient, container: createAppContainer() },
12
+ scrollRestoration: true,
13
+ defaultPreload: 'intent',
14
+ })
15
+ setupRouterSsrQueryIntegration({ router, queryClient })
16
+ return router
17
+ }
18
+
19
+ declare module '@tanstack/react-router' {
20
+ interface Register {
21
+ router: ReturnType<typeof getRouter>
22
+ }
23
+ }
@@ -0,0 +1,72 @@
1
+ import type { ReactNode } from 'react'
2
+ import type { QueryClient } from '@tanstack/react-query'
3
+ import { HeadContent, Link, Outlet, ScriptOnce, Scripts, createRootRouteWithContext } from '@tanstack/react-router'
4
+ import type { AppContainer } from '#/core/AppContainer'
5
+ import { AppearanceEffect, appearanceScript } from '#/core/AppearanceEffect'
6
+ import { AppIdentity } from '#/generated/app'
7
+ import { AppIcons } from '#/generated/icons'
8
+ import { defaultLocale, strings } from '#/generated/strings'
9
+ import appCss from '../styles.css?url'
10
+
11
+ export interface RouterContext {
12
+ queryClient: QueryClient
13
+ container: AppContainer
14
+ }
15
+
16
+ export const Route = createRootRouteWithContext<RouterContext>()({
17
+ head: () => ({
18
+ meta: [
19
+ { charSet: 'utf-8' },
20
+ { name: 'viewport', content: 'width=device-width, initial-scale=1' },
21
+ { title: AppIdentity.name },
22
+ ],
23
+ links: [
24
+ { rel: 'stylesheet', href: appCss },
25
+ { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },
26
+ ],
27
+ }),
28
+ shellComponent: RootDocument,
29
+ component: AppShell,
30
+ })
31
+
32
+ function RootDocument({ children }: { children: ReactNode }) {
33
+ return (
34
+ <html lang={defaultLocale} suppressHydrationWarning>
35
+ <head>
36
+ {/* Applies the stored appearance before the first paint, so there's no flash of the wrong theme. */}
37
+ <ScriptOnce children={appearanceScript} />
38
+ <HeadContent />
39
+ </head>
40
+ <body className="bg-background text-on-background antialiased">
41
+ {children}
42
+ <Scripts />
43
+ </body>
44
+ </html>
45
+ )
46
+ }
47
+
48
+ function AppShell() {
49
+ return (
50
+ <>
51
+ <AppearanceEffect />
52
+ <header className="border-b border-outline">
53
+ <nav className="mx-auto flex max-w-2xl items-center justify-between px-lg py-md">
54
+ <Link to="/" className="text-title">
55
+ {AppIdentity.name}
56
+ </Link>
57
+ <Link
58
+ to="/settings"
59
+ aria-label={strings.homeOpenSettings}
60
+ data-testid="home.openSettings"
61
+ className="rounded-full p-sm text-on-surface-variant hover:bg-surface-variant focus-visible:outline-2 focus-visible:outline-primary"
62
+ >
63
+ <AppIcons.settings aria-hidden className="size-6" />
64
+ </Link>
65
+ </nav>
66
+ </header>
67
+ <main className="mx-auto max-w-2xl px-lg py-xl">
68
+ <Outlet />
69
+ </main>
70
+ </>
71
+ )
72
+ }
@@ -0,0 +1,6 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { HomeScreen } from '#/features/home/HomeScreen'
3
+
4
+ export const Route = createFileRoute('/')({
5
+ component: HomeScreen,
6
+ })
@@ -0,0 +1,9 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { SettingsScreen } from '#/features/settings/SettingsScreen'
3
+ import { AppIdentity } from '#/generated/app'
4
+ import { strings } from '#/generated/strings'
5
+
6
+ export const Route = createFileRoute('/settings')({
7
+ head: () => ({ meta: [{ title: `${strings.settingsTitle} · ${AppIdentity.name}` }] }),
8
+ component: SettingsScreen,
9
+ })
@@ -0,0 +1,9 @@
1
+ @import 'tailwindcss';
2
+ @import './generated/tokens.css';
3
+
4
+ /* `dark:` utilities follow the "dark" class on <html>, which AppearanceEffect sets. */
5
+ @custom-variant dark (&:where(.dark, .dark *));
6
+
7
+ html {
8
+ font-family: var(--font-sans);
9
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "include": ["**/*.ts", "**/*.tsx"],
3
+ "exclude": ["node_modules", "dist", ".output"],
4
+ "compilerOptions": {
5
+ "target": "ES2022",
6
+ "jsx": "react-jsx",
7
+ "module": "ESNext",
8
+ "paths": {
9
+ "#/*": ["./src/*"]
10
+ },
11
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
12
+ "types": ["vite/client"],
13
+
14
+ "moduleResolution": "bundler",
15
+ "allowImportingTsExtensions": true,
16
+ "verbatimModuleSyntax": true,
17
+ "noEmit": true,
18
+
19
+ "skipLibCheck": true,
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "noFallthroughCasesInSwitch": true,
24
+ "noUncheckedSideEffectImports": true
25
+ }
26
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "target": "react"
3
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vite'
2
+ import tailwindcss from '@tailwindcss/vite'
3
+ import { tanstackStart } from '@tanstack/react-start/plugin/vite'
4
+ import viteReact from '@vitejs/plugin-react'
5
+
6
+ export default defineConfig({
7
+ resolve: { tsconfigPaths: true },
8
+ plugins: [tailwindcss(), tanstackStart(), viteReact()],
9
+ })
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ // Unit tests run in Node without the TanStack Start plugin.
4
+ export default defineConfig({
5
+ resolve: { tsconfigPaths: true },
6
+ test: {
7
+ include: ['src/**/*.test.{ts,tsx}'],
8
+ },
9
+ })
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "GeekNative",
3
+ "id": "com.example.geeknative",
4
+ "version": "1.0.0",
5
+ "build": 1,
6
+ "defaultLocale": "en"
7
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "back": { "ios": "chevron.backward", "android": "Icons.AutoMirrored.Filled.ArrowBack", "web": "ArrowLeft" },
3
+ "home": { "ios": "house", "android": "Icons.Filled.Home", "web": "House" },
4
+ "settings": { "ios": "gearshape", "android": "Icons.Filled.Settings", "web": "Settings" }
5
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "common.back": "Back",
3
+ "home.openSettings": "Settings",
4
+ "home.welcomeTitle": "You're all set",
5
+ "home.welcomeBody": "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.",
6
+ "settings.title": "Settings",
7
+ "settings.appearance.title": "Appearance",
8
+ "settings.appearance.system": "System",
9
+ "settings.appearance.light": "Light",
10
+ "settings.appearance.dark": "Dark",
11
+ "settings.about.title": "About",
12
+ "settings.about.version": "Version {version} ({build:int})"
13
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "color": {
3
+ "background": { "light": "#FFFFFF", "dark": "#0B0D12" },
4
+ "onBackground": { "light": "#0F172A", "dark": "#E6E8EE" },
5
+ "surface": { "light": "#F4F5F8", "dark": "#161A23" },
6
+ "onSurface": { "light": "#0F172A", "dark": "#E6E8EE" },
7
+ "surfaceVariant": { "light": "#E9EBF1", "dark": "#222735" },
8
+ "onSurfaceVariant": { "light": "#566074", "dark": "#A3ABBD" },
9
+ "primary": { "light": "#4F46E5", "dark": "#A5B4FC" },
10
+ "onPrimary": { "light": "#FFFFFF", "dark": "#1E1B4B" },
11
+ "outline": { "light": "#D8DCE5", "dark": "#2D3343" },
12
+ "error": { "light": "#DC2626", "dark": "#F87171" },
13
+ "onError": { "light": "#FFFFFF", "dark": "#450A0A" }
14
+ },
15
+ "spacing": {
16
+ "xs": 4,
17
+ "sm": 8,
18
+ "md": 12,
19
+ "lg": 16,
20
+ "xl": 24,
21
+ "xxl": 32
22
+ },
23
+ "radius": {
24
+ "sm": 6,
25
+ "md": 10,
26
+ "lg": 16,
27
+ "full": 9999
28
+ },
29
+ "typography": {
30
+ "display": { "size": 34, "lineHeight": 41, "weight": 700, "ios": "largeTitle", "android": "displaySmall" },
31
+ "headline": { "size": 28, "lineHeight": 34, "weight": 700, "ios": "title", "android": "headlineMedium" },
32
+ "title": { "size": 20, "lineHeight": 26, "weight": 600, "ios": "title3", "android": "titleLarge" },
33
+ "body": { "size": 16, "lineHeight": 24, "weight": 400, "ios": "body", "android": "bodyLarge" },
34
+ "label": { "size": 14, "lineHeight": 20, "weight": 500, "ios": "subheadline", "android": "labelLarge" },
35
+ "caption": { "size": 12, "lineHeight": 16, "weight": 400, "ios": "caption", "android": "bodySmall" }
36
+ },
37
+ "font": {
38
+ "family": "system"
39
+ }
40
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "geeknative-app",
3
+ "private": true,
4
+ "workspaces": [
5
+ "apps/web"
6
+ ],
7
+ "scripts": {
8
+ "gen": "geeknative gen",
9
+ "check": "geeknative check",
10
+ "doctor": "geeknative doctor",
11
+ "parity": "geeknative parity",
12
+ "dev": "npm run dev --workspace apps/web"
13
+ },
14
+ "devDependencies": {
15
+ "geeknative": "file:../packages/geeknative"
16
+ }
17
+ }
@@ -0,0 +1,10 @@
1
+ # Decisions
2
+
3
+ A dated log of product and technical decisions, and of assumptions made along the way. Newest first.
4
+
5
+ ## 2026-10-10: Starter app
6
+
7
+ - The starter app has two screens, Home and Settings, implemented the same way on all three platforms as a reference for new features.
8
+ - The appearance setting is stored locally under `settings.appearance` as `system`, `light` or `dark`, on every platform.
9
+ - There's no backend yet. Each feature reads and writes through a repository interface with a local implementation.
10
+ - Dependencies are wired by hand in an `AppContainer` on every platform, with no DI framework.
@@ -0,0 +1,39 @@
1
+ ---
2
+ id: home
3
+ title: Home
4
+ status:
5
+ ios: done
6
+ android: done
7
+ web: done
8
+ ---
9
+
10
+ # Home
11
+
12
+ ## Summary
13
+
14
+ The first screen. It names the app, welcomes the user and leads to Settings. It's a starting point: replace its content as the product takes shape.
15
+
16
+ ## Screens and states
17
+
18
+ **Home**
19
+
20
+ - Title: the app name, from contracts/app.json.
21
+ - A welcome card with `home.welcomeTitle` and `home.welcomeBody`.
22
+ - An entry point to Settings, labeled `home.openSettings`.
23
+ - Static content, so there are no loading, empty or error states.
24
+
25
+ ## Acceptance criteria
26
+
27
+ - AC-home-1: Home shows the app name as its title.
28
+ - AC-home-2: Home shows the welcome card's title and text.
29
+ - AC-home-3: Home has an entry point that opens Settings.
30
+
31
+ ## Platform notes
32
+
33
+ - iOS: large navigation title; the Settings entry is a gear button in the toolbar.
34
+ - Android: top app bar with the app name; the Settings entry is a gear action in the app bar.
35
+ - Web: the page heading is the app name; Settings is a gear link in the site header, on every page.
36
+
37
+ ## Test IDs
38
+
39
+ - `home.openSettings`: the Settings entry point.
@@ -0,0 +1,50 @@
1
+ ---
2
+ id: settings
3
+ title: Settings
4
+ status:
5
+ ios: done
6
+ android: done
7
+ web: done
8
+ ---
9
+
10
+ # Settings
11
+
12
+ ## Summary
13
+
14
+ Lets the user choose the app's appearance and see which version they're running.
15
+
16
+ ## Screens and states
17
+
18
+ **Settings**, opened from Home.
19
+
20
+ - Section `settings.appearance.title` with three choices: System, Light and Dark. The current choice is selected.
21
+ - Section `settings.about.title` showing `settings.about.version` with the app version and build number.
22
+
23
+ ## Behavior and rules
24
+
25
+ - The appearance applies to the whole app as soon as it's chosen. There's no save button.
26
+ - System follows the device's light or dark setting, including changes while the app is open.
27
+ - The choice is stored locally under the key `settings.appearance`, as `system`, `light` or `dark`. The key and values are the same on every platform.
28
+ - A missing or unknown stored value counts as `system`.
29
+
30
+ ## Acceptance criteria
31
+
32
+ - AC-settings-1: Appearance defaults to System.
33
+ - AC-settings-2: Choosing an appearance applies it to the whole app immediately.
34
+ - AC-settings-3: The chosen appearance is still applied after the app restarts.
35
+ - AC-settings-4: Settings shows the app version and build number.
36
+
37
+ ## Platform notes
38
+
39
+ - iOS: a `Form`, with the appearance as an inline `Picker` (checkmarks).
40
+ - Android: radio buttons in a selectable group; the top app bar has a back button.
41
+ - Web: a radio group in a card. The stored appearance is applied before the first paint, so there's no flash of the wrong theme.
42
+
43
+ ## Test IDs
44
+
45
+ - `settings.appearance`: the appearance choices.
46
+ - `settings.version`: the version text.
47
+
48
+ ## Assumptions
49
+
50
+ - No "reset to defaults" action yet.
@@ -0,0 +1,16 @@
1
+ # Navigation
2
+
3
+ The app's screens, their route names, and how each platform presents them. Route names are the same in every app: `Route.settings` on iOS, `Route.Settings` on Android and `/settings` on web.
4
+
5
+ | Screen | Route | Web path | Reached from |
6
+ |---|---|---|---|
7
+ | Home | `home` | `/` | App start |
8
+ | Settings | `settings` | `/settings` | Home's Settings entry |
9
+
10
+ ## Per platform
11
+
12
+ - **iOS:** one `NavigationStack` rooted at Home. Screens are pushed by appending a `Route` (apps/ios/App/Core/Navigation/Route.swift).
13
+ - **Android:** one Navigation 3 back stack starting at `Route.Home`. Screens are pushed with `backStack.add(...)` (apps/android/.../core/navigation).
14
+ - **Web:** TanStack Router file routes in apps/web/src/routes. Every screen has a URL.
15
+
16
+ When you add a screen, add a row here. Use the web path as the mobile deep link path when deep links are added.