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.
- package/LICENSE +202 -0
- package/bin/geeknative.js +12 -0
- package/package.json +31 -4
- package/src/cli.js +61 -0
- package/src/codegen/app.js +97 -0
- package/src/codegen/icons.js +87 -0
- package/src/codegen/index.js +44 -0
- package/src/codegen/strings.js +308 -0
- package/src/codegen/tokens.js +449 -0
- package/src/codegen/util.js +122 -0
- package/src/commands/check.js +143 -0
- package/src/commands/create.js +78 -0
- package/src/commands/doctor.js +108 -0
- package/src/commands/gen.js +44 -0
- package/src/commands/parity.js +134 -0
- package/src/commands/run.js +91 -0
- package/src/lib/exec.js +77 -0
- package/src/lib/fsutil.js +92 -0
- package/src/lib/log.js +37 -0
- package/src/lib/managed.js +80 -0
- package/src/lib/project.js +52 -0
- package/src/lib/toolchains.js +126 -0
- package/template/.claude/agents/android-engineer.md +15 -0
- package/template/.claude/agents/ios-engineer.md +15 -0
- package/template/.claude/agents/web-engineer.md +15 -0
- package/template/.claude/settings.json +65 -0
- package/template/.geeknative/framework.mk +41 -0
- package/template/.geeknative/hooks/after-edit.mjs +28 -0
- package/template/.geeknative/hooks/parity-guard.mjs +49 -0
- package/template/.geeknative/hooks/subagent-context.mjs +24 -0
- package/template/.geeknative/playbooks/bug.md +7 -0
- package/template/.geeknative/playbooks/design.md +10 -0
- package/template/.geeknative/playbooks/engineering.md +9 -0
- package/template/.geeknative/playbooks/feature.md +11 -0
- package/template/.geeknative/playbooks/platform-specific.md +8 -0
- package/template/.geeknative/playbooks/product.md +12 -0
- package/template/.geeknative/templates/feature.md +49 -0
- package/template/.gemini/settings.json +5 -0
- package/template/.github/dependabot.yml +18 -0
- package/template/AGENTS.md +97 -0
- package/template/CLAUDE.md +9 -0
- package/template/DESIGN.md +65 -0
- package/template/Makefile +6 -0
- package/template/PRODUCT.md +40 -0
- package/template/README.md +28 -0
- package/template/_gitignore +33 -0
- package/template/apps/android/AGENTS.md +66 -0
- package/template/apps/android/CLAUDE.md +1 -0
- package/template/apps/android/app/build.gradle.kts +68 -0
- package/template/apps/android/app/identity.properties +4 -0
- package/template/apps/android/app/src/main/AndroidManifest.xml +24 -0
- package/template/apps/android/app/src/main/kotlin/app/App.kt +15 -0
- package/template/apps/android/app/src/main/kotlin/app/MainActivity.kt +49 -0
- package/template/apps/android/app/src/main/kotlin/app/core/AppContainer.kt +27 -0
- package/template/apps/android/app/src/main/kotlin/app/core/designsystem/AppCard.kt +30 -0
- package/template/apps/android/app/src/main/kotlin/app/core/designsystem/AppTheme.kt +35 -0
- package/template/apps/android/app/src/main/kotlin/app/core/navigation/AppNavigation.kt +40 -0
- package/template/apps/android/app/src/main/kotlin/app/core/navigation/Route.kt +13 -0
- package/template/apps/android/app/src/main/kotlin/app/features/home/HomeScreen.kt +67 -0
- package/template/apps/android/app/src/main/kotlin/app/features/settings/Appearance.kt +22 -0
- package/template/apps/android/app/src/main/kotlin/app/features/settings/SettingsRepository.kt +40 -0
- package/template/apps/android/app/src/main/kotlin/app/features/settings/SettingsScreen.kt +113 -0
- package/template/apps/android/app/src/main/kotlin/app/features/settings/SettingsViewModel.kt +28 -0
- package/template/apps/android/app/src/main/kotlin/app/generated/AppIdentity.kt +10 -0
- package/template/apps/android/app/src/main/kotlin/app/generated/Icons.kt +15 -0
- package/template/apps/android/app/src/main/kotlin/app/generated/Tokens.kt +144 -0
- package/template/apps/android/app/src/main/res/drawable/ic_launcher_foreground.xml +17 -0
- package/template/apps/android/app/src/main/res/mipmap/ic_launcher.xml +10 -0
- package/template/apps/android/app/src/main/res/mipmap/ic_launcher_round.xml +10 -0
- package/template/apps/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
- package/template/apps/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
- package/template/apps/android/app/src/main/res/values/generated_colors.xml +15 -0
- package/template/apps/android/app/src/main/res/values/generated_strings.xml +16 -0
- package/template/apps/android/app/src/main/res/values/ic_launcher_background.xml +4 -0
- package/template/apps/android/app/src/main/res/values/themes.xml +7 -0
- package/template/apps/android/app/src/main/res/values-night/generated_colors.xml +15 -0
- package/template/apps/android/app/src/main/res/values-night/themes.xml +6 -0
- package/template/apps/android/app/src/test/kotlin/app/features/settings/SettingsTest.kt +90 -0
- package/template/apps/android/build.gradle.kts +5 -0
- package/template/apps/android/gradle/gradle-daemon-jvm.properties +12 -0
- package/template/apps/android/gradle/libs.versions.toml +37 -0
- package/template/apps/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/template/apps/android/gradle/wrapper/gradle-wrapper.properties +9 -0
- package/template/apps/android/gradle.properties +7 -0
- package/template/apps/android/gradlew +248 -0
- package/template/apps/android/gradlew.bat +82 -0
- package/template/apps/android/settings.gradle.kts +29 -0
- package/template/apps/ios/AGENTS.md +70 -0
- package/template/apps/ios/App/AppMain.swift +13 -0
- package/template/apps/ios/App/Core/AppContainer.swift +22 -0
- package/template/apps/ios/App/Core/DesignSystem/AppCard.swift +15 -0
- package/template/apps/ios/App/Core/Navigation/Route.swift +4 -0
- package/template/apps/ios/App/Core/RootView.swift +32 -0
- package/template/apps/ios/App/Features/Home/HomeScreen.swift +35 -0
- package/template/apps/ios/App/Features/Settings/Appearance.swift +27 -0
- package/template/apps/ios/App/Features/Settings/SettingsRepository.swift +40 -0
- package/template/apps/ios/App/Features/Settings/SettingsScreen.swift +40 -0
- package/template/apps/ios/App/Features/Settings/SettingsViewModel.swift +23 -0
- package/template/apps/ios/App/Generated/AppIdentity.swift +9 -0
- package/template/apps/ios/App/Generated/Colors.xcassets/AccentColor.colorset/Contents.json +38 -0
- package/template/apps/ios/App/Generated/Colors.xcassets/Contents.json +6 -0
- package/template/apps/ios/App/Generated/Icons.swift +8 -0
- package/template/apps/ios/App/Generated/Localizable.xcstrings +127 -0
- package/template/apps/ios/App/Generated/Strings.swift +40 -0
- package/template/apps/ios/App/Generated/Tokens.swift +65 -0
- package/template/apps/ios/App/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png +0 -0
- package/template/apps/ios/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
- package/template/apps/ios/App/Resources/Assets.xcassets/Contents.json +6 -0
- package/template/apps/ios/App.xcodeproj/project.pbxproj +298 -0
- package/template/apps/ios/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/template/apps/ios/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme +89 -0
- package/template/apps/ios/AppTests/SettingsTests.swift +54 -0
- package/template/apps/ios/CLAUDE.md +1 -0
- package/template/apps/ios/Config/App.xcconfig +23 -0
- package/template/apps/ios/Config/AppTests.xcconfig +10 -0
- package/template/apps/ios/Config/Base.xcconfig +25 -0
- package/template/apps/ios/Config/Debug.xcconfig +9 -0
- package/template/apps/ios/Config/Identity.xcconfig +5 -0
- package/template/apps/ios/Config/Release.xcconfig +6 -0
- package/template/apps/web/AGENTS.md +63 -0
- package/template/apps/web/CLAUDE.md +1 -0
- package/template/apps/web/package.json +38 -0
- package/template/apps/web/public/favicon.svg +6 -0
- package/template/apps/web/src/core/AppContainer.ts +18 -0
- package/template/apps/web/src/core/AppearanceEffect.tsx +31 -0
- package/template/apps/web/src/core/designsystem/AppCard.tsx +6 -0
- package/template/apps/web/src/features/home/HomeScreen.tsx +15 -0
- package/template/apps/web/src/features/home/home.test.tsx +22 -0
- package/template/apps/web/src/features/settings/Appearance.ts +29 -0
- package/template/apps/web/src/features/settings/LocalStorageSettingsRepository.ts +21 -0
- package/template/apps/web/src/features/settings/SettingsRepository.ts +7 -0
- package/template/apps/web/src/features/settings/SettingsScreen.tsx +44 -0
- package/template/apps/web/src/features/settings/settings.test.ts +45 -0
- package/template/apps/web/src/features/settings/useSettingsViewModel.ts +42 -0
- package/template/apps/web/src/generated/app.ts +9 -0
- package/template/apps/web/src/generated/icons.ts +9 -0
- package/template/apps/web/src/generated/strings.ts +88 -0
- package/template/apps/web/src/generated/tokens.css +86 -0
- package/template/apps/web/src/routeTree.gen.ts +86 -0
- package/template/apps/web/src/router.tsx +23 -0
- package/template/apps/web/src/routes/__root.tsx +72 -0
- package/template/apps/web/src/routes/index.tsx +6 -0
- package/template/apps/web/src/routes/settings.tsx +9 -0
- package/template/apps/web/src/styles.css +9 -0
- package/template/apps/web/tsconfig.json +26 -0
- package/template/apps/web/tsr.config.json +3 -0
- package/template/apps/web/vite.config.ts +9 -0
- package/template/apps/web/vitest.config.ts +9 -0
- package/template/contracts/app.json +7 -0
- package/template/contracts/icons.json +5 -0
- package/template/contracts/strings/en.json +13 -0
- package/template/contracts/tokens.json +40 -0
- package/template/package.json +17 -0
- package/template/spec/decisions.md +10 -0
- package/template/spec/features/home.md +39 -0
- package/template/spec/features/settings.md +50 -0
- package/template/spec/navigation.md +16 -0
- package/template/spec/parity.md +29 -0
- package/README.md +0 -3
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<Scheme
|
|
3
|
+
LastUpgradeVersion = "2630"
|
|
4
|
+
version = "1.7">
|
|
5
|
+
<BuildAction
|
|
6
|
+
parallelizeBuildables = "YES"
|
|
7
|
+
buildImplicitDependencies = "YES">
|
|
8
|
+
<BuildActionEntries>
|
|
9
|
+
<BuildActionEntry
|
|
10
|
+
buildForTesting = "YES"
|
|
11
|
+
buildForRunning = "YES"
|
|
12
|
+
buildForProfiling = "YES"
|
|
13
|
+
buildForArchiving = "YES"
|
|
14
|
+
buildForAnalyzing = "YES">
|
|
15
|
+
<BuildableReference
|
|
16
|
+
BuildableIdentifier = "primary"
|
|
17
|
+
BlueprintIdentifier = "A1B2C3D4E5F6000000000020"
|
|
18
|
+
BuildableName = "App.app"
|
|
19
|
+
BlueprintName = "App"
|
|
20
|
+
ReferencedContainer = "container:App.xcodeproj">
|
|
21
|
+
</BuildableReference>
|
|
22
|
+
</BuildActionEntry>
|
|
23
|
+
</BuildActionEntries>
|
|
24
|
+
</BuildAction>
|
|
25
|
+
<TestAction
|
|
26
|
+
buildConfiguration = "Debug"
|
|
27
|
+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
28
|
+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
29
|
+
shouldUseLaunchSchemeArgsEnv = "YES">
|
|
30
|
+
<Testables>
|
|
31
|
+
<TestableReference
|
|
32
|
+
skipped = "NO"
|
|
33
|
+
parallelizable = "YES">
|
|
34
|
+
<BuildableReference
|
|
35
|
+
BuildableIdentifier = "primary"
|
|
36
|
+
BlueprintIdentifier = "A1B2C3D4E5F6000000000030"
|
|
37
|
+
BuildableName = "AppTests.xctest"
|
|
38
|
+
BlueprintName = "AppTests"
|
|
39
|
+
ReferencedContainer = "container:App.xcodeproj">
|
|
40
|
+
</BuildableReference>
|
|
41
|
+
</TestableReference>
|
|
42
|
+
</Testables>
|
|
43
|
+
</TestAction>
|
|
44
|
+
<LaunchAction
|
|
45
|
+
buildConfiguration = "Debug"
|
|
46
|
+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
47
|
+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
48
|
+
launchStyle = "0"
|
|
49
|
+
useCustomWorkingDirectory = "NO"
|
|
50
|
+
ignoresPersistentStateOnLaunch = "NO"
|
|
51
|
+
debugDocumentVersioning = "YES"
|
|
52
|
+
debugServiceExtension = "internal"
|
|
53
|
+
allowLocationSimulation = "YES">
|
|
54
|
+
<BuildableProductRunnable
|
|
55
|
+
runnableDebuggingMode = "0">
|
|
56
|
+
<BuildableReference
|
|
57
|
+
BuildableIdentifier = "primary"
|
|
58
|
+
BlueprintIdentifier = "A1B2C3D4E5F6000000000020"
|
|
59
|
+
BuildableName = "App.app"
|
|
60
|
+
BlueprintName = "App"
|
|
61
|
+
ReferencedContainer = "container:App.xcodeproj">
|
|
62
|
+
</BuildableReference>
|
|
63
|
+
</BuildableProductRunnable>
|
|
64
|
+
</LaunchAction>
|
|
65
|
+
<ProfileAction
|
|
66
|
+
buildConfiguration = "Release"
|
|
67
|
+
shouldUseLaunchSchemeArgsEnv = "YES"
|
|
68
|
+
savedToolIdentifier = ""
|
|
69
|
+
useCustomWorkingDirectory = "NO"
|
|
70
|
+
debugDocumentVersioning = "YES">
|
|
71
|
+
<BuildableProductRunnable
|
|
72
|
+
runnableDebuggingMode = "0">
|
|
73
|
+
<BuildableReference
|
|
74
|
+
BuildableIdentifier = "primary"
|
|
75
|
+
BlueprintIdentifier = "A1B2C3D4E5F6000000000020"
|
|
76
|
+
BuildableName = "App.app"
|
|
77
|
+
BlueprintName = "App"
|
|
78
|
+
ReferencedContainer = "container:App.xcodeproj">
|
|
79
|
+
</BuildableReference>
|
|
80
|
+
</BuildableProductRunnable>
|
|
81
|
+
</ProfileAction>
|
|
82
|
+
<AnalyzeAction
|
|
83
|
+
buildConfiguration = "Debug">
|
|
84
|
+
</AnalyzeAction>
|
|
85
|
+
<ArchiveAction
|
|
86
|
+
buildConfiguration = "Release"
|
|
87
|
+
revealArchiveInOrganizer = "YES">
|
|
88
|
+
</ArchiveAction>
|
|
89
|
+
</Scheme>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Testing
|
|
3
|
+
import UIKit
|
|
4
|
+
@testable import App
|
|
5
|
+
|
|
6
|
+
@Suite("Settings")
|
|
7
|
+
struct SettingsTests {
|
|
8
|
+
/// An empty UserDefaults suite, so tests don't share state.
|
|
9
|
+
private func freshDefaults() -> UserDefaults {
|
|
10
|
+
let name = "SettingsTests.\(UUID().uuidString)"
|
|
11
|
+
let defaults = UserDefaults(suiteName: name)!
|
|
12
|
+
defaults.removePersistentDomain(forName: name)
|
|
13
|
+
return defaults
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@Test("AC-settings-1: appearance defaults to System")
|
|
17
|
+
func defaultsToSystem() {
|
|
18
|
+
#expect(UserDefaultsSettingsRepository(defaults: freshDefaults()).appearance == .system)
|
|
19
|
+
|
|
20
|
+
let defaults = freshDefaults()
|
|
21
|
+
defaults.set("sepia", forKey: Appearance.storageKey)
|
|
22
|
+
#expect(UserDefaultsSettingsRepository(defaults: defaults).appearance == .system)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Test("AC-settings-2: choosing an appearance applies it to the whole app immediately")
|
|
26
|
+
func appliesImmediately() {
|
|
27
|
+
let repository = InMemorySettingsRepository()
|
|
28
|
+
let viewModel = SettingsViewModel(repository: repository)
|
|
29
|
+
|
|
30
|
+
viewModel.selectAppearance(.dark)
|
|
31
|
+
|
|
32
|
+
// RootView applies the repository's appearance to every window as it changes.
|
|
33
|
+
#expect(repository.appearance == .dark)
|
|
34
|
+
#expect(viewModel.appearance == .dark)
|
|
35
|
+
#expect(Appearance.dark.interfaceStyle == .dark)
|
|
36
|
+
#expect(Appearance.system.interfaceStyle == .unspecified)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Test("AC-settings-3: the chosen appearance survives a restart")
|
|
40
|
+
func survivesRestart() {
|
|
41
|
+
let defaults = freshDefaults()
|
|
42
|
+
UserDefaultsSettingsRepository(defaults: defaults).setAppearance(.light)
|
|
43
|
+
|
|
44
|
+
#expect(UserDefaultsSettingsRepository(defaults: defaults).appearance == .light)
|
|
45
|
+
#expect(defaults.string(forKey: "settings.appearance") == "light")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Test("AC-settings-4: shows the app version and build number")
|
|
49
|
+
func showsVersion() {
|
|
50
|
+
let text = SettingsViewModel(repository: InMemorySettingsRepository()).versionText
|
|
51
|
+
#expect(text.contains(AppIdentity.version))
|
|
52
|
+
#expect(text.contains(String(AppIdentity.build)))
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// The app target. Its name, bundle ID and version come from contracts/app.json (via Identity.xcconfig).
|
|
2
|
+
#include "Identity.xcconfig"
|
|
3
|
+
|
|
4
|
+
PRODUCT_NAME = App
|
|
5
|
+
PRODUCT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_ID)
|
|
6
|
+
MARKETING_VERSION = $(APP_VERSION)
|
|
7
|
+
CURRENT_PROJECT_VERSION = $(APP_BUILD)
|
|
8
|
+
|
|
9
|
+
GENERATE_INFOPLIST_FILE = YES
|
|
10
|
+
INFOPLIST_KEY_CFBundleDisplayName = $(APP_DISPLAY_NAME)
|
|
11
|
+
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES
|
|
12
|
+
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES
|
|
13
|
+
INFOPLIST_KEY_UILaunchScreen_Generation = YES
|
|
14
|
+
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight
|
|
15
|
+
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight
|
|
16
|
+
|
|
17
|
+
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
|
|
18
|
+
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor
|
|
19
|
+
ENABLE_PREVIEWS = YES
|
|
20
|
+
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks
|
|
21
|
+
|
|
22
|
+
// To run on a device, set your Apple Developer team ID.
|
|
23
|
+
DEVELOPMENT_TEAM =
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The unit test target, hosted in the app.
|
|
2
|
+
#include "Identity.xcconfig"
|
|
3
|
+
|
|
4
|
+
PRODUCT_NAME = AppTests
|
|
5
|
+
PRODUCT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_ID).tests
|
|
6
|
+
MARKETING_VERSION = $(APP_VERSION)
|
|
7
|
+
CURRENT_PROJECT_VERSION = $(APP_BUILD)
|
|
8
|
+
GENERATE_INFOPLIST_FILE = YES
|
|
9
|
+
TEST_HOST = $(BUILT_PRODUCTS_DIR)/App.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/App
|
|
10
|
+
BUNDLE_LOADER = $(TEST_HOST)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Settings shared by every target and configuration.
|
|
2
|
+
// Edit build settings here, not in the Xcode project file.
|
|
3
|
+
|
|
4
|
+
SDKROOT = iphoneos
|
|
5
|
+
IPHONEOS_DEPLOYMENT_TARGET = 17.0
|
|
6
|
+
TARGETED_DEVICE_FAMILY = 1,2
|
|
7
|
+
|
|
8
|
+
SWIFT_VERSION = 6.0
|
|
9
|
+
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor
|
|
10
|
+
SWIFT_APPROACHABLE_CONCURRENCY = YES
|
|
11
|
+
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES
|
|
12
|
+
|
|
13
|
+
// contracts/strings is the source of copy; don't let Xcode extract strings into the generated catalog.
|
|
14
|
+
SWIFT_EMIT_LOC_STRINGS = NO
|
|
15
|
+
LOCALIZATION_PREFERS_STRING_CATALOGS = YES
|
|
16
|
+
|
|
17
|
+
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES
|
|
18
|
+
ENABLE_USER_SCRIPT_SANDBOXING = YES
|
|
19
|
+
ENABLE_STRICT_OBJC_MSGSEND = YES
|
|
20
|
+
CLANG_ENABLE_MODULES = YES
|
|
21
|
+
CLANG_ENABLE_OBJC_ARC = YES
|
|
22
|
+
GCC_C_LANGUAGE_STANDARD = gnu17
|
|
23
|
+
ALWAYS_SEARCH_USER_PATHS = NO
|
|
24
|
+
CODE_SIGN_STYLE = Automatic
|
|
25
|
+
COPY_PHASE_STRIP = NO
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#include "Base.xcconfig"
|
|
2
|
+
|
|
3
|
+
DEBUG_INFORMATION_FORMAT = dwarf
|
|
4
|
+
ENABLE_TESTABILITY = YES
|
|
5
|
+
GCC_OPTIMIZATION_LEVEL = 0
|
|
6
|
+
GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1 $(inherited)
|
|
7
|
+
ONLY_ACTIVE_ARCH = YES
|
|
8
|
+
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG $(inherited)
|
|
9
|
+
SWIFT_OPTIMIZATION_LEVEL = -Onone
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<!-- geeknative:begin (managed by geeknative: replaced on upgrade. Add your own rules under "Project rules" at the end.) -->
|
|
2
|
+
# Web app (React + TanStack Start)
|
|
3
|
+
|
|
4
|
+
Read the root AGENTS.md first. This file covers how the web app is built.
|
|
5
|
+
|
|
6
|
+
## Stack
|
|
7
|
+
|
|
8
|
+
- React 19 and TanStack Start (server rendering), with TanStack Router file routes and TanStack Query.
|
|
9
|
+
- Tailwind CSS v4 with the generated design tokens, and Lucide icons.
|
|
10
|
+
- TypeScript (strict), Vite, Vitest.
|
|
11
|
+
- Supports Safari 16.4+, Chrome 111+ and Firefox 128+ (Tailwind v4's minimum).
|
|
12
|
+
|
|
13
|
+
## Layout
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
apps/web/src/
|
|
17
|
+
router.tsx Creates the router with { queryClient, container } as context
|
|
18
|
+
routes/ One thin file per URL: head, loader, then render a feature's screen
|
|
19
|
+
__root.tsx HTML shell, site header, AppearanceEffect
|
|
20
|
+
core/ AppContainer, AppearanceEffect
|
|
21
|
+
designsystem/ Shared components (AppCard, ...)
|
|
22
|
+
features/<name>/ <Name>Screen.tsx, use<Name>ViewModel.ts, <Name>Repository.ts, implementations, tests
|
|
23
|
+
generated/ From contracts/ by `make gen`. Never edit.
|
|
24
|
+
styles.css Tailwind entry; imports generated/tokens.css
|
|
25
|
+
routeTree.gen.ts Generated by TanStack Router. Never edit.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Patterns
|
|
29
|
+
|
|
30
|
+
Same names and responsibilities as the iOS and Android apps.
|
|
31
|
+
|
|
32
|
+
| Concept | Web |
|
|
33
|
+
|---|---|
|
|
34
|
+
| Screen | `NotesScreen` in features/notes/, rendered by routes/notes.tsx |
|
|
35
|
+
| State and actions | `useNotesViewModel()`: reads data with TanStack Query, returns state and actions |
|
|
36
|
+
| Data | `NotesRepository` interface, plus implementations such as `LocalStorageNotesRepository` |
|
|
37
|
+
| Dependencies | `AppContainer`, in router context: `useRouteContext({ from: '__root__' }).container` |
|
|
38
|
+
| Navigation | A file in routes/ per screen; link with `<Link to="/notes">` |
|
|
39
|
+
| Copy | `strings.notesEmptyTitle` (generated) |
|
|
40
|
+
| Colors, spacing, radii, type | Tailwind token classes: `bg-primary`, `p-md`, `rounded-lg`, `text-title` |
|
|
41
|
+
| Icons | `<AppIcons.settings className="size-6" />` (generated) |
|
|
42
|
+
| Test IDs | `data-testid="notes.addButton"` |
|
|
43
|
+
|
|
44
|
+
Follow features/settings as the reference implementation.
|
|
45
|
+
|
|
46
|
+
## Rules
|
|
47
|
+
|
|
48
|
+
- Never hardcode user-facing text, colors or spacing. Add them to contracts/ and run `make gen`. Use token classes (`bg-surface`, `p-lg`), not arbitrary values (`bg-[#fff]`, `p-[13px]`).
|
|
49
|
+
- Server functions and API routes are only for web-only concerns (rendering, cookies, SEO). Logic the mobile apps need too goes through repositories, so a future backend can serve all three apps.
|
|
50
|
+
- Code that uses browser APIs (localStorage, window) must also run safely during server rendering.
|
|
51
|
+
- Use semantic HTML, give icon-only links and buttons an `aria-label` from strings, and keep focus visible.
|
|
52
|
+
- For complex primitives (dialogs, menus, popovers), use shadcn/ui: the tokens already map onto its CSS variables.
|
|
53
|
+
- Name tests after acceptance criteria: `test('AC-notes-2: ...')`.
|
|
54
|
+
|
|
55
|
+
## Commands
|
|
56
|
+
|
|
57
|
+
- `make check-web`: type-check, unit tests and a production build. Full log: .geeknative/logs/web.log.
|
|
58
|
+
- `make run-web`: dev server at http://localhost:3000.
|
|
59
|
+
<!-- geeknative:end -->
|
|
60
|
+
|
|
61
|
+
## Project rules
|
|
62
|
+
|
|
63
|
+
<!-- Your web-specific rules. They override the defaults above, and geeknative upgrades never touch them. -->
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "web",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"imports": {
|
|
6
|
+
"#/*": "./src/*"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "vite dev --port 3000",
|
|
10
|
+
"build": "vite build",
|
|
11
|
+
"preview": "vite preview",
|
|
12
|
+
"routes": "tsr generate",
|
|
13
|
+
"typecheck": "tsr generate && tsc --noEmit",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"check": "npm run typecheck && npm run test && npm run build"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@tailwindcss/vite": "4.3.3",
|
|
19
|
+
"@tanstack/react-query": "5.104.1",
|
|
20
|
+
"@tanstack/react-router": "1.170.42",
|
|
21
|
+
"@tanstack/react-router-ssr-query": "1.167.3",
|
|
22
|
+
"@tanstack/react-start": "1.168.61",
|
|
23
|
+
"lucide-react": "1.55.0",
|
|
24
|
+
"react": "^19.2.0",
|
|
25
|
+
"react-dom": "^19.2.0",
|
|
26
|
+
"tailwindcss": "4.3.3"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@tanstack/router-cli": "1.167.41",
|
|
30
|
+
"@types/node": "^24.0.0",
|
|
31
|
+
"@types/react": "^19.2.0",
|
|
32
|
+
"@types/react-dom": "^19.2.0",
|
|
33
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
34
|
+
"typescript": "^6.0.2",
|
|
35
|
+
"vite": "^8.0.0",
|
|
36
|
+
"vitest": "^5.0.3"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { LocalStorageSettingsRepository } from '#/features/settings/LocalStorageSettingsRepository'
|
|
2
|
+
import type { SettingsRepository } from '#/features/settings/SettingsRepository'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The app's dependencies, created once per router and shared through router context.
|
|
6
|
+
* Swap an implementation here (for example, a backend-backed repository) without touching screens.
|
|
7
|
+
*/
|
|
8
|
+
export interface AppContainer {
|
|
9
|
+
settingsRepository: SettingsRepository
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createAppContainer(): AppContainer {
|
|
13
|
+
// During server rendering there's no localStorage, so the repository falls back to defaults.
|
|
14
|
+
const storage = typeof window === 'undefined' ? undefined : window.localStorage
|
|
15
|
+
return {
|
|
16
|
+
settingsRepository: new LocalStorageSettingsRepository(storage),
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
import { APPEARANCE_STORAGE_KEY, isDark } from '#/features/settings/Appearance'
|
|
3
|
+
import { useAppearance } from '#/features/settings/useSettingsViewModel'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Runs in <head> before the first paint: applies the stored appearance (unknown or missing
|
|
7
|
+
* values count as "system"), so the page never flashes the wrong theme.
|
|
8
|
+
*/
|
|
9
|
+
export const appearanceScript = `(() => {
|
|
10
|
+
try {
|
|
11
|
+
const stored = localStorage.getItem(${JSON.stringify(APPEARANCE_STORAGE_KEY)})
|
|
12
|
+
const dark = stored === 'dark' || (stored !== 'light' && matchMedia('(prefers-color-scheme: dark)').matches)
|
|
13
|
+
document.documentElement.classList.toggle('dark', dark)
|
|
14
|
+
} catch {}
|
|
15
|
+
})()`
|
|
16
|
+
|
|
17
|
+
/** Keeps the "dark" class on <html> in step with the chosen appearance and the system setting. */
|
|
18
|
+
export function AppearanceEffect() {
|
|
19
|
+
const { data: appearance } = useAppearance()
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!appearance) return
|
|
23
|
+
const media = window.matchMedia('(prefers-color-scheme: dark)')
|
|
24
|
+
const apply = () => document.documentElement.classList.toggle('dark', isDark(appearance, media.matches))
|
|
25
|
+
apply()
|
|
26
|
+
media.addEventListener('change', apply)
|
|
27
|
+
return () => media.removeEventListener('change', apply)
|
|
28
|
+
}, [appearance])
|
|
29
|
+
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
/** A card for grouped content on a screen (DESIGN.md: Card). */
|
|
4
|
+
export function AppCard({ children, className = '' }: { children: ReactNode; className?: string }) {
|
|
5
|
+
return <section className={`rounded-lg bg-surface p-lg text-on-surface ${className}`}>{children}</section>
|
|
6
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AppCard } from '#/core/designsystem/AppCard'
|
|
2
|
+
import { AppIdentity } from '#/generated/app'
|
|
3
|
+
import { strings } from '#/generated/strings'
|
|
4
|
+
|
|
5
|
+
export function HomeScreen() {
|
|
6
|
+
return (
|
|
7
|
+
<div className="flex flex-col gap-xl">
|
|
8
|
+
<h1 className="text-display">{AppIdentity.name}</h1>
|
|
9
|
+
<AppCard>
|
|
10
|
+
<h2 className="text-title">{strings.homeWelcomeTitle}</h2>
|
|
11
|
+
<p className="mt-sm text-body text-on-surface-variant">{strings.homeWelcomeBody}</p>
|
|
12
|
+
</AppCard>
|
|
13
|
+
</div>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { renderToString } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, test } from 'vitest'
|
|
3
|
+
import { AppIdentity } from '#/generated/app'
|
|
4
|
+
import { strings } from '#/generated/strings'
|
|
5
|
+
import { HomeScreen } from './HomeScreen'
|
|
6
|
+
|
|
7
|
+
/** How React escapes text in server-rendered HTML. */
|
|
8
|
+
const escapeHtml = (text: string) =>
|
|
9
|
+
text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')
|
|
10
|
+
|
|
11
|
+
describe('Home', () => {
|
|
12
|
+
const html = renderToString(<HomeScreen />)
|
|
13
|
+
|
|
14
|
+
test('AC-home-1: shows the app name as its title', () => {
|
|
15
|
+
expect(html).toContain(`>${escapeHtml(AppIdentity.name)}</h1>`)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test("AC-home-2: shows the welcome card's title and text", () => {
|
|
19
|
+
expect(html).toContain(escapeHtml(strings.homeWelcomeTitle))
|
|
20
|
+
expect(html).toContain(escapeHtml(strings.homeWelcomeBody))
|
|
21
|
+
})
|
|
22
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { strings } from '#/generated/strings'
|
|
2
|
+
|
|
3
|
+
/** The appearance setting. Stored as these exact values on every platform. */
|
|
4
|
+
export const appearances = ['system', 'light', 'dark'] as const
|
|
5
|
+
export type Appearance = (typeof appearances)[number]
|
|
6
|
+
|
|
7
|
+
/** Storage key, shared with the iOS and Android apps. */
|
|
8
|
+
export const APPEARANCE_STORAGE_KEY = 'settings.appearance'
|
|
9
|
+
|
|
10
|
+
/** Missing or unknown stored values count as "system". */
|
|
11
|
+
export function parseAppearance(value: string | null | undefined): Appearance {
|
|
12
|
+
return appearances.find((appearance) => appearance === value) ?? 'system'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Whether to render dark, given the chosen appearance and the system setting. */
|
|
16
|
+
export function isDark(appearance: Appearance, systemIsDark: boolean) {
|
|
17
|
+
return appearance === 'dark' || (appearance === 'system' && systemIsDark)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function appearanceLabel(appearance: Appearance) {
|
|
21
|
+
switch (appearance) {
|
|
22
|
+
case 'system':
|
|
23
|
+
return strings.settingsAppearanceSystem
|
|
24
|
+
case 'light':
|
|
25
|
+
return strings.settingsAppearanceLight
|
|
26
|
+
case 'dark':
|
|
27
|
+
return strings.settingsAppearanceDark
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { APPEARANCE_STORAGE_KEY, parseAppearance, type Appearance } from './Appearance'
|
|
2
|
+
import type { SettingsRepository } from './SettingsRepository'
|
|
3
|
+
|
|
4
|
+
type KeyValueStorage = Pick<Storage, 'getItem' | 'setItem'>
|
|
5
|
+
|
|
6
|
+
/** Keeps settings in the browser's localStorage. Without storage (during server rendering) it returns defaults. */
|
|
7
|
+
export class LocalStorageSettingsRepository implements SettingsRepository {
|
|
8
|
+
private readonly storage: KeyValueStorage | undefined
|
|
9
|
+
|
|
10
|
+
constructor(storage: KeyValueStorage | undefined) {
|
|
11
|
+
this.storage = storage
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async getAppearance(): Promise<Appearance> {
|
|
15
|
+
return parseAppearance(this.storage?.getItem(APPEARANCE_STORAGE_KEY))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async setAppearance(appearance: Appearance): Promise<void> {
|
|
19
|
+
this.storage?.setItem(APPEARANCE_STORAGE_KEY, appearance)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Appearance } from './Appearance'
|
|
2
|
+
|
|
3
|
+
/** Stores the user's preferences. Screens use whichever implementation AppContainer provides. */
|
|
4
|
+
export interface SettingsRepository {
|
|
5
|
+
getAppearance(): Promise<Appearance>
|
|
6
|
+
setAppearance(appearance: Appearance): Promise<void>
|
|
7
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { AppCard } from '#/core/designsystem/AppCard'
|
|
2
|
+
import { strings } from '#/generated/strings'
|
|
3
|
+
import { useSettingsViewModel } from './useSettingsViewModel'
|
|
4
|
+
|
|
5
|
+
export function SettingsScreen() {
|
|
6
|
+
const viewModel = useSettingsViewModel()
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex flex-col gap-xl">
|
|
10
|
+
<h1 className="text-display">{strings.settingsTitle}</h1>
|
|
11
|
+
|
|
12
|
+
<AppCard>
|
|
13
|
+
<fieldset data-testid="settings.appearance">
|
|
14
|
+
<legend className="text-label text-on-surface-variant">{strings.settingsAppearanceTitle}</legend>
|
|
15
|
+
<div className="mt-md flex flex-col gap-xs">
|
|
16
|
+
{viewModel.appearanceOptions.map((option) => (
|
|
17
|
+
<label
|
|
18
|
+
key={option.id}
|
|
19
|
+
className="flex cursor-pointer items-center gap-md rounded-md p-sm hover:bg-surface-variant"
|
|
20
|
+
>
|
|
21
|
+
<input
|
|
22
|
+
type="radio"
|
|
23
|
+
name="appearance"
|
|
24
|
+
value={option.id}
|
|
25
|
+
checked={viewModel.appearance === option.id}
|
|
26
|
+
onChange={() => void viewModel.selectAppearance(option.id)}
|
|
27
|
+
className="size-4 accent-primary"
|
|
28
|
+
/>
|
|
29
|
+
<span className="text-body">{option.label}</span>
|
|
30
|
+
</label>
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
</fieldset>
|
|
34
|
+
</AppCard>
|
|
35
|
+
|
|
36
|
+
<AppCard>
|
|
37
|
+
<h2 className="text-label text-on-surface-variant">{strings.settingsAboutTitle}</h2>
|
|
38
|
+
<p data-testid="settings.version" className="mt-sm text-body">
|
|
39
|
+
{viewModel.versionText}
|
|
40
|
+
</p>
|
|
41
|
+
</AppCard>
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/react-query'
|
|
2
|
+
import { describe, expect, test } from 'vitest'
|
|
3
|
+
import { AppIdentity } from '#/generated/app'
|
|
4
|
+
import { isDark, parseAppearance } from './Appearance'
|
|
5
|
+
import { LocalStorageSettingsRepository } from './LocalStorageSettingsRepository'
|
|
6
|
+
import { appearanceQuery, selectAppearance, versionText } from './useSettingsViewModel'
|
|
7
|
+
|
|
8
|
+
function memoryStorage() {
|
|
9
|
+
const values = new Map<string, string>()
|
|
10
|
+
return {
|
|
11
|
+
getItem: (key: string) => values.get(key) ?? null,
|
|
12
|
+
setItem: (key: string, value: string) => void values.set(key, value),
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('Settings', () => {
|
|
17
|
+
test('AC-settings-1: appearance defaults to System', async () => {
|
|
18
|
+
const repository = new LocalStorageSettingsRepository(memoryStorage())
|
|
19
|
+
expect(await repository.getAppearance()).toBe('system')
|
|
20
|
+
expect(parseAppearance('sepia')).toBe('system')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('AC-settings-2: choosing an appearance applies it to the whole app immediately', async () => {
|
|
24
|
+
const queryClient = new QueryClient()
|
|
25
|
+
const repository = new LocalStorageSettingsRepository(memoryStorage())
|
|
26
|
+
const saving = selectAppearance(queryClient, repository, 'dark')
|
|
27
|
+
// The shared query changes before saving finishes, so every screen updates at once.
|
|
28
|
+
expect(queryClient.getQueryData(appearanceQuery(repository).queryKey)).toBe('dark')
|
|
29
|
+
await saving
|
|
30
|
+
expect(isDark('dark', false)).toBe(true)
|
|
31
|
+
expect(isDark('system', true)).toBe(true)
|
|
32
|
+
expect(isDark('light', true)).toBe(false)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('AC-settings-3: the chosen appearance survives a restart', async () => {
|
|
36
|
+
const storage = memoryStorage()
|
|
37
|
+
await new LocalStorageSettingsRepository(storage).setAppearance('light')
|
|
38
|
+
expect(await new LocalStorageSettingsRepository(storage).getAppearance()).toBe('light')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('AC-settings-4: shows the app version and build number', () => {
|
|
42
|
+
expect(versionText()).toContain(AppIdentity.version)
|
|
43
|
+
expect(versionText()).toContain(String(AppIdentity.build))
|
|
44
|
+
})
|
|
45
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { queryOptions, useQuery, useQueryClient, type QueryClient } from '@tanstack/react-query'
|
|
2
|
+
import { useRouteContext } from '@tanstack/react-router'
|
|
3
|
+
import { AppIdentity } from '#/generated/app'
|
|
4
|
+
import { strings } from '#/generated/strings'
|
|
5
|
+
import { appearanceLabel, appearances, type Appearance } from './Appearance'
|
|
6
|
+
import type { SettingsRepository } from './SettingsRepository'
|
|
7
|
+
|
|
8
|
+
export const appearanceQuery = (repository: SettingsRepository) =>
|
|
9
|
+
queryOptions({
|
|
10
|
+
queryKey: ['settings', 'appearance'] as const,
|
|
11
|
+
queryFn: () => repository.getAppearance(),
|
|
12
|
+
staleTime: Infinity,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
/** Applies the appearance everywhere at once (through the shared query), then saves it. */
|
|
16
|
+
export async function selectAppearance(queryClient: QueryClient, repository: SettingsRepository, appearance: Appearance) {
|
|
17
|
+
queryClient.setQueryData(appearanceQuery(repository).queryKey, appearance)
|
|
18
|
+
await repository.setAppearance(appearance)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const versionText = () =>
|
|
22
|
+
strings.settingsAboutVersion({ version: AppIdentity.version, build: AppIdentity.build })
|
|
23
|
+
|
|
24
|
+
/** The current appearance. Used by the Settings screen and by AppearanceEffect. */
|
|
25
|
+
export function useAppearance() {
|
|
26
|
+
const { container } = useRouteContext({ from: '__root__' })
|
|
27
|
+
return useQuery(appearanceQuery(container.settingsRepository))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** State and actions for the Settings screen. */
|
|
31
|
+
export function useSettingsViewModel() {
|
|
32
|
+
const { container } = useRouteContext({ from: '__root__' })
|
|
33
|
+
const queryClient = useQueryClient()
|
|
34
|
+
const { data: appearance = 'system' } = useAppearance()
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
appearance,
|
|
38
|
+
appearanceOptions: appearances.map((id) => ({ id, label: appearanceLabel(id) })),
|
|
39
|
+
selectAppearance: (next: Appearance) => selectAppearance(queryClient, container.settingsRepository, next),
|
|
40
|
+
versionText: versionText(),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// GENERATED by geeknative from contracts/app.json. Do not edit: change the contract, then run `make gen`.
|
|
2
|
+
|
|
3
|
+
/** The app's name, identifier and version, shared with the iOS and Android apps. */
|
|
4
|
+
export const AppIdentity = {
|
|
5
|
+
name: "GeekNative",
|
|
6
|
+
id: "com.example.geeknative",
|
|
7
|
+
version: "1.0.0",
|
|
8
|
+
build: 1,
|
|
9
|
+
} as const
|