@shaykec/app-agent 1.0.8 → 1.0.9
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/.claude/agents/android-customizer.md +9 -1
- package/.claude/agents/ios-customizer.md +9 -1
- package/.claude/agents/react-native-customizer.md +71 -0
- package/.claude/skills/android-customizer/SKILL.md +95 -23
- package/.claude/skills/ios-customizer/SKILL.md +102 -23
- package/.claude/skills/react-native-customizer/SKILL.md +85 -11
- package/.cursor/agents/android-customizer.md +15 -11
- package/.cursor/agents/ios-customizer.md +15 -10
- package/.cursor/agents/react-native-customizer.md +170 -0
- package/.cursor/mcp.json +2 -10
- package/.cursor/skills/android-customizer/SKILL.md +3 -3
- package/.cursor/skills/ios-customizer/SKILL.md +3 -3
- package/.cursor/skills/react-native-customizer/SKILL.md +69 -9
- package/package.json +1 -1
- package/templates/android/Skeleton/TESTING_MANIFEST.md +2 -1
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/MainActivity.kt +23 -2
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/core/theme/AppearanceManager.kt +42 -0
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/features/profile/ProfileScreen.kt +20 -8
- package/templates/android/Skeleton/tests/03_detail_screen.yaml +2 -1
- package/templates/android/Skeleton/tests/04_favorites.yaml +2 -1
- package/templates/android/Skeleton/tests/08_full_e2e.yaml +2 -1
- package/templates/android/Skeleton/tests/09_dark_mode.yaml +50 -0
- package/templates/ios/Skeleton/tests/09_dark_mode.yaml +52 -0
- package/templates/react-native/Skeleton/TESTING_MANIFEST.md +1 -0
- package/templates/react-native/Skeleton/tests/09_dark_mode.yaml +46 -0
|
@@ -86,18 +86,20 @@ iOS templates use a `Colors.swift` theme file:
|
|
|
86
86
|
- Ensure dark mode variants are provided
|
|
87
87
|
- Use semantic colors where possible (`Color.primary`, `Color.secondary`)
|
|
88
88
|
|
|
89
|
-
###
|
|
90
|
-
|
|
89
|
+
### Data Layer
|
|
90
|
+
Templates use a `DataRepository` protocol with `DataSourceResolver`:
|
|
91
91
|
```swift
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
// ViewModels depend on the protocol, not MockDataProvider directly
|
|
93
|
+
private let repository: any DataRepository
|
|
94
|
+
init(repository: any DataRepository = DataSourceResolver.repository) {
|
|
95
|
+
self.repository = repository
|
|
95
96
|
}
|
|
97
|
+
repository.addItem(newItem)
|
|
96
98
|
```
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
99
|
+
- ALWAYS use `DataSourceResolver.repository` in ViewModels, never `MockDataProvider.shared` directly
|
|
100
|
+
- Preserve MockDataProvider CRUD methods and `reset()` function
|
|
101
|
+
- Do NOT modify NetworkMonitor, LocalPersistence, or OfflineBanner
|
|
102
|
+
- Ensure OfflineBanner is integrated into main navigation views
|
|
101
103
|
|
|
102
104
|
## Output
|
|
103
105
|
|
|
@@ -116,6 +118,9 @@ POTENTIAL ISSUES:
|
|
|
116
118
|
- ONLY edit files under `output/` — never touch `templates/`
|
|
117
119
|
- Ensure all imports resolve after your changes
|
|
118
120
|
- Keep `@main` struct and `App` protocol conformance intact
|
|
119
|
-
- Preserve `MockDataProvider` function
|
|
121
|
+
- Preserve `MockDataProvider` CRUD methods and `reset()` function
|
|
122
|
+
- Do NOT modify NetworkMonitor, LocalPersistence, or OfflineBanner
|
|
120
123
|
- Use SF Symbols for tab and navigation icons
|
|
124
|
+
- Preserve ALL `.accessibilityIdentifier()` calls — do NOT remove test IDs
|
|
125
|
+
- Follow `{screen}_{element}_{role}` convention for new test IDs (see `templates/TEST_ID_CONVENTIONS.md`)
|
|
121
126
|
- Test that view hierarchy compiles (no circular references, no missing types)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-native-customizer
|
|
3
|
+
description: Specialized React Native/TypeScript subagent for customizing React Native app templates. Use when the target platform is React Native and the main agent needs to perform deep customization — updating package configs, rewriting React Native components, adjusting React Navigation, applying themes, and handling cross-platform features.
|
|
4
|
+
tools:
|
|
5
|
+
- Read
|
|
6
|
+
- Edit
|
|
7
|
+
- Write
|
|
8
|
+
- Grep
|
|
9
|
+
- Glob
|
|
10
|
+
- LS
|
|
11
|
+
- Shell
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# React Native Customizer
|
|
15
|
+
|
|
16
|
+
You are an expert React Native developer specializing in TypeScript and modern mobile development. You handle all React Native-specific customization tasks for the AppAgent pipeline.
|
|
17
|
+
|
|
18
|
+
## Context You Receive
|
|
19
|
+
|
|
20
|
+
The main agent passes you:
|
|
21
|
+
- **App name** — display name (e.g., "GigWallet")
|
|
22
|
+
- **App directory** — path under `output/` (e.g., `output/gigwallet-rn/`)
|
|
23
|
+
- **Platform** — `react-native`
|
|
24
|
+
- **App description** — what the app does
|
|
25
|
+
- **What has been done** — renaming, AppConfig, mock data updates already completed
|
|
26
|
+
- **Design brief** (from design-system skill) — color palette (light + dark hex codes), icon mapping, visual tone (playful/professional/minimal/bold), corner radius, and typography intent. Apply these tokens to `ThemeContext.tsx`, tab icons, and layout values.
|
|
27
|
+
- **Content brief** (from content-writer skill) — onboarding text, tab labels, section headers, empty state messages, button labels, search placeholders, and error messages. Apply these strings to React Native components.
|
|
28
|
+
|
|
29
|
+
## Logging Protocol (MANDATORY)
|
|
30
|
+
|
|
31
|
+
When you start, print:
|
|
32
|
+
`[SUBAGENT:react-native-customizer] Starting — React Native customization for {app-name}`
|
|
33
|
+
|
|
34
|
+
When you finish, print:
|
|
35
|
+
`[SUBAGENT:react-native-customizer] Completed — {count} files modified`
|
|
36
|
+
|
|
37
|
+
## Your Expertise
|
|
38
|
+
|
|
39
|
+
- React Native 0.73+, TypeScript 5+
|
|
40
|
+
- React Navigation 6+ (bottom tabs, native stack, type-safe navigation)
|
|
41
|
+
- MVVM architecture with custom hooks (`useXxxViewModel`)
|
|
42
|
+
- StyleSheet.create() for performant styling
|
|
43
|
+
- FlatList / SectionList for virtualized lists
|
|
44
|
+
- React Context for theming and global state
|
|
45
|
+
- Cross-platform patterns (iOS + Android from a single codebase)
|
|
46
|
+
|
|
47
|
+
## Tasks You Handle
|
|
48
|
+
|
|
49
|
+
### Package & Project Renaming
|
|
50
|
+
When renaming a React Native app:
|
|
51
|
+
1. Update `package.json` — `name` field
|
|
52
|
+
2. Update `app.json` — `name` and `displayName`
|
|
53
|
+
3. Update `android/app/build.gradle` — `applicationId`, `namespace`
|
|
54
|
+
4. Update `android/settings.gradle` — `rootProject.name`
|
|
55
|
+
5. Update `android/app/src/main/res/values/strings.xml` — `app_name`
|
|
56
|
+
6. Update `android/app/src/main/java/` — package directory structure, `MainActivity.kt`, `MainApplication.kt`
|
|
57
|
+
7. Update `ios/Podfile` — project target name
|
|
58
|
+
8. Update all `import` statements if module paths changed
|
|
59
|
+
|
|
60
|
+
### React Native Component Customization
|
|
61
|
+
When modifying components:
|
|
62
|
+
- Use functional components with `React.FC` typing
|
|
63
|
+
- Use custom hooks (`useXxxViewModel`) for state and business logic
|
|
64
|
+
- Use `StyleSheet.create()` for all styles — no inline style objects
|
|
65
|
+
- Use `useTheme()` hook for accessing theme colors and typography
|
|
66
|
+
- Use `FlatList` / `SectionList` for lists (not `map()` on `ScrollView`)
|
|
67
|
+
- Keep components stateless where possible — hoist state to ViewModel hooks
|
|
68
|
+
- Add `testID` props to ALL interactive elements
|
|
69
|
+
|
|
70
|
+
### Navigation Structure
|
|
71
|
+
Templates use React Navigation with bottom tabs and native stack:
|
|
72
|
+
```tsx
|
|
73
|
+
const Tab = createBottomTabNavigator();
|
|
74
|
+
const Stack = createNativeStackNavigator<RootStackParamList>();
|
|
75
|
+
|
|
76
|
+
function MainTabs() {
|
|
77
|
+
return (
|
|
78
|
+
<Tab.Navigator>
|
|
79
|
+
<Tab.Screen name="Home" component={HomeScreen} />
|
|
80
|
+
<Tab.Screen name="Explore" component={ExploreScreen} />
|
|
81
|
+
</Tab.Navigator>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function AppNavigator() {
|
|
86
|
+
return (
|
|
87
|
+
<NavigationContainer>
|
|
88
|
+
<Stack.Navigator>
|
|
89
|
+
<Stack.Screen name="MainTabs" component={MainTabs} />
|
|
90
|
+
<Stack.Screen name="Detail" component={DetailScreen} />
|
|
91
|
+
</Stack.Navigator>
|
|
92
|
+
</NavigationContainer>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
When adding/removing/renaming tabs:
|
|
97
|
+
- Update tab names and components in `AppNavigator.tsx`
|
|
98
|
+
- Apply tab labels from the content brief
|
|
99
|
+
- Apply icon text/emoji from the design brief
|
|
100
|
+
- Update `RootStackParamList` type for type-safe navigation
|
|
101
|
+
|
|
102
|
+
### Theme Customization
|
|
103
|
+
React Native templates use a `ThemeContext.tsx`:
|
|
104
|
+
- Apply primary, secondary, accent colors from the design brief (hex values)
|
|
105
|
+
- Provide both light and dark mode variants
|
|
106
|
+
- Apply corner radius and spacing tokens
|
|
107
|
+
- Apply typography settings (font sizes, weights)
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
const lightColors = {
|
|
111
|
+
primary: '#007AFF',
|
|
112
|
+
secondary: '#5856D6',
|
|
113
|
+
accent: '#FF9500',
|
|
114
|
+
background: '#FFFFFF',
|
|
115
|
+
surface: '#F2F2F7',
|
|
116
|
+
text: '#000000',
|
|
117
|
+
textSecondary: '#8E8E93',
|
|
118
|
+
border: '#C6C6C8',
|
|
119
|
+
error: '#FF3B30',
|
|
120
|
+
success: '#34C759',
|
|
121
|
+
};
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Data Layer
|
|
125
|
+
Templates use a `DataRepository` interface with `DataSourceResolver`:
|
|
126
|
+
```tsx
|
|
127
|
+
import { DataSourceResolver } from '../data/DataSourceResolver';
|
|
128
|
+
|
|
129
|
+
export function useHomeViewModel() {
|
|
130
|
+
const repository = DataSourceResolver.repository;
|
|
131
|
+
const [items, setItems] = useState<Item[]>([]);
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
setItems(repository.getItems());
|
|
134
|
+
const unsubscribe = repository.subscribe(() => setItems(repository.getItems()));
|
|
135
|
+
return unsubscribe;
|
|
136
|
+
}, []);
|
|
137
|
+
return { items };
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
- ALWAYS use `DataSourceResolver.repository` in ViewModel hooks, never `MockDataProvider` directly
|
|
141
|
+
- Preserve MockDataProvider CRUD methods and `reset()` function
|
|
142
|
+
- Do NOT modify NetworkMonitor, LocalPersistence, or OfflineBanner
|
|
143
|
+
- Ensure OfflineBanner is integrated into main navigation screens
|
|
144
|
+
|
|
145
|
+
## Output
|
|
146
|
+
|
|
147
|
+
When done, report what you changed:
|
|
148
|
+
```
|
|
149
|
+
FILES MODIFIED:
|
|
150
|
+
- {path}: {what changed}
|
|
151
|
+
- {path}: {what changed}
|
|
152
|
+
|
|
153
|
+
POTENTIAL ISSUES:
|
|
154
|
+
- {any concerns or items needing manual review}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Rules
|
|
158
|
+
|
|
159
|
+
- ONLY edit files under `output/` — never touch `templates/`
|
|
160
|
+
- Ensure all imports resolve after your changes
|
|
161
|
+
- Preserve `MockDataProvider` CRUD methods and `reset()` function
|
|
162
|
+
- Do NOT modify NetworkMonitor, LocalPersistence, or OfflineBanner
|
|
163
|
+
- Ensure OfflineBanner is integrated into main navigation screens
|
|
164
|
+
- Use `testID` props on ALL interactive elements
|
|
165
|
+
- Follow `{screen}_{element}_{role}` convention for test IDs (see `templates/TEST_ID_CONVENTIONS.md`)
|
|
166
|
+
- Use `StyleSheet.create()` for styles, not inline objects
|
|
167
|
+
- Use functional components with TypeScript
|
|
168
|
+
- Keep React Navigation structure intact
|
|
169
|
+
- Preserve ThemeContext provider wrapping in App.tsx
|
|
170
|
+
- Do NOT introduce new npm dependencies
|
package/.cursor/mcp.json
CHANGED
|
@@ -9,7 +9,7 @@ Use this skill to perform deep Android/Kotlin/Compose customization on a cloned
|
|
|
9
9
|
|
|
10
10
|
## When to Use
|
|
11
11
|
|
|
12
|
-
- At Step
|
|
12
|
+
- At Step 5 of the AppAgent workflow, when the platform is Android
|
|
13
13
|
- After AppConfig, mock data, and design tokens are already in place
|
|
14
14
|
- When you need to modify Compose screens, navigation, and themes
|
|
15
15
|
|
|
@@ -282,7 +282,7 @@ Write the report to `output/{app-name}/reports/05-customization.md` (append Andr
|
|
|
282
282
|
```markdown
|
|
283
283
|
# Customization Report
|
|
284
284
|
|
|
285
|
-
**Step:**
|
|
285
|
+
**Step:** 5
|
|
286
286
|
**Skill:** android-customizer
|
|
287
287
|
**Timestamp:** {ISO 8601}
|
|
288
288
|
**Result:** PASS
|
|
@@ -333,7 +333,7 @@ Update `output/{app-name}/reports/summary.json` — read the file, append this s
|
|
|
333
333
|
|
|
334
334
|
```json
|
|
335
335
|
{
|
|
336
|
-
"step":
|
|
336
|
+
"step": 5,
|
|
337
337
|
"name": "android-customization",
|
|
338
338
|
"startedAt": "{ISO 8601 timestamp}",
|
|
339
339
|
"durationSeconds": 0,
|
|
@@ -9,7 +9,7 @@ Use this skill to perform deep iOS/SwiftUI customization on a cloned template.
|
|
|
9
9
|
|
|
10
10
|
## When to Use
|
|
11
11
|
|
|
12
|
-
- At Step
|
|
12
|
+
- At Step 5 of the AppAgent workflow, when the platform is iOS
|
|
13
13
|
- After AppConfig, mock data, and design tokens are already in place
|
|
14
14
|
- When you need to modify SwiftUI views, navigation, and themes
|
|
15
15
|
|
|
@@ -262,7 +262,7 @@ Write the report to `output/{app-name}/reports/05-customization.md` (append iOS
|
|
|
262
262
|
```markdown
|
|
263
263
|
# Customization Report
|
|
264
264
|
|
|
265
|
-
**Step:**
|
|
265
|
+
**Step:** 5
|
|
266
266
|
**Skill:** ios-customizer
|
|
267
267
|
**Timestamp:** {ISO 8601}
|
|
268
268
|
**Result:** PASS
|
|
@@ -312,7 +312,7 @@ Update `output/{app-name}/reports/summary.json` — read the file, append this s
|
|
|
312
312
|
|
|
313
313
|
```json
|
|
314
314
|
{
|
|
315
|
-
"step":
|
|
315
|
+
"step": 5,
|
|
316
316
|
"name": "ios-customization",
|
|
317
317
|
"startedAt": "{ISO 8601 timestamp}",
|
|
318
318
|
"durationSeconds": 0,
|
|
@@ -133,12 +133,15 @@ const lightColors = {
|
|
|
133
133
|
};
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
### Step 5 — Data Layer &
|
|
136
|
+
### Step 5 — Data Layer & Offline Infrastructure
|
|
137
137
|
|
|
138
|
-
Templates
|
|
138
|
+
Templates include an **offline-first data layer**. Understand these components:
|
|
139
139
|
|
|
140
140
|
**Files you should NOT modify** (generic infrastructure):
|
|
141
141
|
- `src/data/DataRepository.ts` — interface definition
|
|
142
|
+
- `src/data/NetworkMonitor.ts` — connectivity detection
|
|
143
|
+
- `src/data/LocalPersistence.ts` — JSON file persistence
|
|
144
|
+
- `src/components/OfflineBanner.tsx` — offline status banner
|
|
142
145
|
- `src/theme/ThemeContext.tsx` — theme provider
|
|
143
146
|
|
|
144
147
|
**Files you MAY need to update** (domain-specific):
|
|
@@ -147,25 +150,29 @@ Templates use a `MockDataProvider.ts` that implements `DataRepository` interface
|
|
|
147
150
|
- All CRUD methods work with correct entity types
|
|
148
151
|
- Subscription/onChange pattern is working
|
|
149
152
|
- Reset method references correct defaults
|
|
153
|
+
- `src/data/SyncManager.ts` — if entity types were renamed, update domain methods
|
|
150
154
|
|
|
151
155
|
**ViewModel patterns** (how screens consume data):
|
|
152
156
|
|
|
153
|
-
ViewModels are custom React hooks that use MockDataProvider:
|
|
157
|
+
ViewModels are custom React hooks that use the `DataRepository` interface via `DataSourceResolver`, not `MockDataProvider` directly:
|
|
154
158
|
```tsx
|
|
159
|
+
import { DataSourceResolver } from '../data/DataSourceResolver';
|
|
160
|
+
|
|
155
161
|
export function useHomeViewModel() {
|
|
162
|
+
const repository = DataSourceResolver.repository;
|
|
156
163
|
const [items, setItems] = useState<Item[]>([]);
|
|
157
164
|
const [isLoading, setIsLoading] = useState(true);
|
|
158
165
|
|
|
159
166
|
useEffect(() => {
|
|
160
167
|
loadData();
|
|
161
|
-
const unsubscribe =
|
|
162
|
-
setItems(
|
|
168
|
+
const unsubscribe = repository.subscribe(() => {
|
|
169
|
+
setItems(repository.getItems());
|
|
163
170
|
});
|
|
164
171
|
return unsubscribe;
|
|
165
172
|
}, []);
|
|
166
173
|
|
|
167
174
|
const loadData = () => {
|
|
168
|
-
setItems(
|
|
175
|
+
setItems(repository.getItems());
|
|
169
176
|
setIsLoading(false);
|
|
170
177
|
};
|
|
171
178
|
|
|
@@ -173,6 +180,16 @@ export function useHomeViewModel() {
|
|
|
173
180
|
}
|
|
174
181
|
```
|
|
175
182
|
|
|
183
|
+
When customizing ViewModels, ALWAYS use `DataSourceResolver.repository`, never `MockDataProvider` directly. This ensures the app works with any data source (mock, localStorage, Firebase, Supabase, custom API).
|
|
184
|
+
|
|
185
|
+
**OfflineBanner integration** — add to main navigation screens:
|
|
186
|
+
```tsx
|
|
187
|
+
<View style={{ flex: 1 }}>
|
|
188
|
+
<OfflineBanner />
|
|
189
|
+
{/* ... rest of screen content */}
|
|
190
|
+
</View>
|
|
191
|
+
```
|
|
192
|
+
|
|
176
193
|
### Step 6 — Data Layer Verification
|
|
177
194
|
|
|
178
195
|
Verify the data layer uses correct patterns:
|
|
@@ -193,9 +210,36 @@ export interface DataRepository {
|
|
|
193
210
|
}
|
|
194
211
|
```
|
|
195
212
|
|
|
213
|
+
**MockDataProvider (implements DataRepository):**
|
|
214
|
+
```tsx
|
|
215
|
+
class MockDataProvider implements DataRepository {
|
|
216
|
+
private static instance: MockDataProvider;
|
|
217
|
+
private listeners: Set<() => void> = new Set();
|
|
218
|
+
|
|
219
|
+
getItems(): Item[] { ... }
|
|
220
|
+
addItem(item: Item): void { ... }
|
|
221
|
+
subscribe(callback: () => void): () => void { ... }
|
|
222
|
+
reset(): void { ... } // Reset to defaults
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**DataSourceResolver:**
|
|
227
|
+
```tsx
|
|
228
|
+
export class DataSourceResolver {
|
|
229
|
+
static get repository(): DataRepository {
|
|
230
|
+
switch (AppConfig.DataSource.active) {
|
|
231
|
+
case 'localStorage': return MockDataProvider.getInstance(); // TODO: LocalStorageProvider
|
|
232
|
+
case 'mock': return MockDataProvider.getInstance();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
196
238
|
Verify:
|
|
197
|
-
-
|
|
198
|
-
-
|
|
239
|
+
- DataRepository interface matches MockDataProvider's public methods
|
|
240
|
+
- MockDataProvider implements DataRepository
|
|
241
|
+
- DataSourceResolver returns the correct implementation
|
|
242
|
+
- ViewModel hooks use `DataSourceResolver.repository`, not `MockDataProvider` directly
|
|
199
243
|
- Seed data matches the new domain
|
|
200
244
|
- CRUD methods reference correct entity types
|
|
201
245
|
|
|
@@ -237,6 +281,14 @@ import { CelebrationOverlay } from '../shared/CelebrationEffects';
|
|
|
237
281
|
4. **Wrap all applied animations** with `useMotionPreferences` checks if MotionPreferences is integrated
|
|
238
282
|
5. **Do NOT add animation modifiers** to screens not listed in the manifest
|
|
239
283
|
|
|
284
|
+
### Step 6.7 — App Icon (if applicable)
|
|
285
|
+
|
|
286
|
+
React Native apps target both iOS and Android. App icon customization is handled per-platform:
|
|
287
|
+
- **iOS**: If `scripts/generate-app-icon.swift` exists, run it with the SF Symbol and background color from the manifest
|
|
288
|
+
- **Android**: If `templates/android/ICON_VECTORS.md` exists, update `android/app/src/main/res/drawable/ic_launcher_foreground.xml` and `ic_launcher_background` color
|
|
289
|
+
|
|
290
|
+
If the scripts/resources are not available, skip — the template's default icons will be used.
|
|
291
|
+
|
|
240
292
|
## Report Output
|
|
241
293
|
|
|
242
294
|
Write the report to `output/{app-name}/reports/05-customization.md`:
|
|
@@ -269,6 +321,12 @@ React Native (TypeScript)
|
|
|
269
321
|
- Empty states: {count}
|
|
270
322
|
- CTAs updated: {count}
|
|
271
323
|
|
|
324
|
+
## Offline Infrastructure
|
|
325
|
+
|
|
326
|
+
- OfflineBanner integrated: {yes/no}
|
|
327
|
+
- SyncManager methods updated: {list or "no changes needed"}
|
|
328
|
+
- Data persistence verified: {yes/no}
|
|
329
|
+
|
|
272
330
|
## Animation Modifiers Applied
|
|
273
331
|
|
|
274
332
|
- {modifier}: applied to {screen} — {description}
|
|
@@ -324,8 +382,10 @@ Templates include `testID` props on all interactive elements and a `TESTING_MANI
|
|
|
324
382
|
- ONLY edit files under `output/` — never touch `templates/`
|
|
325
383
|
- Ensure all imports resolve after your changes
|
|
326
384
|
- Preserve `MockDataProvider` CRUD methods and `reset()` function
|
|
385
|
+
- Do NOT modify NetworkMonitor, LocalPersistence, or OfflineBanner
|
|
386
|
+
- Ensure OfflineBanner is integrated into main navigation screens
|
|
327
387
|
- Use `testID` props on ALL interactive elements
|
|
328
|
-
- Follow `{screen}_{element}_{role}` convention for new test IDs
|
|
388
|
+
- Follow `{screen}_{element}_{role}` convention for new test IDs (see `templates/TEST_ID_CONVENTIONS.md`)
|
|
329
389
|
- Use `StyleSheet.create()` for styles, not inline objects
|
|
330
390
|
- Use functional components with TypeScript
|
|
331
391
|
- Keep React Navigation structure intact
|
package/package.json
CHANGED
|
@@ -48,7 +48,8 @@ This document catalogs all test tags in the Skeleton template for automated test
|
|
|
48
48
|
- `profile_name` — User name
|
|
49
49
|
- `profile_email` — User email
|
|
50
50
|
- `profile_notifications` — Notifications setting
|
|
51
|
-
- `profile_appearance` — Appearance
|
|
51
|
+
- `profile_appearance` — Appearance / Dark Mode row
|
|
52
|
+
- `profile_dark_mode_toggle` — Dark mode toggle switch
|
|
52
53
|
- `profile_motion_preferences` — Motion Preferences / Animations link
|
|
53
54
|
- `profile_version` — App version
|
|
54
55
|
- `profile_privacy` — Privacy policy
|
|
@@ -4,18 +4,39 @@ import android.os.Bundle
|
|
|
4
4
|
import androidx.activity.ComponentActivity
|
|
5
5
|
import androidx.activity.compose.setContent
|
|
6
6
|
import androidx.activity.enableEdgeToEdge
|
|
7
|
+
import androidx.compose.foundation.layout.Box
|
|
8
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
9
|
+
import androidx.compose.runtime.CompositionLocalProvider
|
|
10
|
+
import androidx.compose.runtime.remember
|
|
11
|
+
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
12
|
+
import androidx.compose.ui.Modifier
|
|
13
|
+
import androidx.compose.ui.semantics.semantics
|
|
14
|
+
import androidx.compose.ui.semantics.testTagsAsResourceId
|
|
15
|
+
import com.appship.skeleton.core.theme.AppearanceManager
|
|
16
|
+
import com.appship.skeleton.core.theme.LocalAppearanceManager
|
|
7
17
|
import com.appship.skeleton.core.theme.SkeletonTheme
|
|
18
|
+
import com.appship.skeleton.core.theme.resolveIsDarkTheme
|
|
8
19
|
import com.appship.skeleton.features.navigation.MainScreen
|
|
9
20
|
import dagger.hilt.android.AndroidEntryPoint
|
|
10
21
|
|
|
11
22
|
@AndroidEntryPoint
|
|
12
23
|
class MainActivity : ComponentActivity() {
|
|
24
|
+
@OptIn(ExperimentalComposeUiApi::class)
|
|
13
25
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
14
26
|
super.onCreate(savedInstanceState)
|
|
15
27
|
enableEdgeToEdge()
|
|
16
28
|
setContent {
|
|
17
|
-
|
|
18
|
-
|
|
29
|
+
val appearanceManager = remember { AppearanceManager(this) }
|
|
30
|
+
CompositionLocalProvider(LocalAppearanceManager provides appearanceManager) {
|
|
31
|
+
SkeletonTheme(darkTheme = resolveIsDarkTheme(appearanceManager)) {
|
|
32
|
+
Box(
|
|
33
|
+
modifier = Modifier
|
|
34
|
+
.fillMaxSize()
|
|
35
|
+
.semantics { testTagsAsResourceId = true }
|
|
36
|
+
) {
|
|
37
|
+
MainScreen()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
19
40
|
}
|
|
20
41
|
}
|
|
21
42
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
package com.appship.skeleton.core.theme
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.SharedPreferences
|
|
5
|
+
import androidx.compose.foundation.isSystemInDarkTheme
|
|
6
|
+
import androidx.compose.runtime.*
|
|
7
|
+
import androidx.compose.runtime.staticCompositionLocalOf
|
|
8
|
+
|
|
9
|
+
// CUSTOMIZE:APPEARANCE - Dark mode and appearance management
|
|
10
|
+
enum class AppearanceMode { SYSTEM, LIGHT, DARK }
|
|
11
|
+
|
|
12
|
+
class AppearanceManager(context: Context) {
|
|
13
|
+
private val prefs: SharedPreferences =
|
|
14
|
+
context.getSharedPreferences("appearance_prefs", Context.MODE_PRIVATE)
|
|
15
|
+
|
|
16
|
+
var appearanceMode: AppearanceMode by mutableStateOf(
|
|
17
|
+
try {
|
|
18
|
+
AppearanceMode.valueOf(prefs.getString("appearance_mode", "SYSTEM") ?: "SYSTEM")
|
|
19
|
+
} catch (_: Exception) {
|
|
20
|
+
AppearanceMode.SYSTEM
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
private set
|
|
24
|
+
|
|
25
|
+
fun setMode(mode: AppearanceMode) {
|
|
26
|
+
appearanceMode = mode
|
|
27
|
+
prefs.edit().putString("appearance_mode", mode.name).apply()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
val LocalAppearanceManager = staticCompositionLocalOf<AppearanceManager> {
|
|
32
|
+
error("No AppearanceManager provided")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Composable
|
|
36
|
+
fun resolveIsDarkTheme(appearanceManager: AppearanceManager): Boolean {
|
|
37
|
+
return when (appearanceManager.appearanceMode) {
|
|
38
|
+
AppearanceMode.SYSTEM -> isSystemInDarkTheme()
|
|
39
|
+
AppearanceMode.LIGHT -> false
|
|
40
|
+
AppearanceMode.DARK -> true
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -17,6 +17,8 @@ import androidx.compose.ui.res.stringResource
|
|
|
17
17
|
import androidx.compose.ui.unit.dp
|
|
18
18
|
import com.appship.skeleton.AppConfig
|
|
19
19
|
import com.appship.skeleton.R
|
|
20
|
+
import com.appship.skeleton.core.theme.AppearanceMode
|
|
21
|
+
import com.appship.skeleton.core.theme.LocalAppearanceManager
|
|
20
22
|
|
|
21
23
|
// PLACEHOLDER: Customize this screen for user profile and settings.
|
|
22
24
|
|
|
@@ -94,14 +96,24 @@ fun ProfileScreen(
|
|
|
94
96
|
.testTag("profile_notifications")
|
|
95
97
|
)
|
|
96
98
|
HorizontalDivider()
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
run {
|
|
100
|
+
val appearanceManager = LocalAppearanceManager.current
|
|
101
|
+
val isDark = appearanceManager.appearanceMode == AppearanceMode.DARK
|
|
102
|
+
ListItem(
|
|
103
|
+
headlineContent = { Text("Dark Mode") },
|
|
104
|
+
leadingContent = { Icon(Icons.Default.DarkMode, contentDescription = null) },
|
|
105
|
+
trailingContent = {
|
|
106
|
+
Switch(
|
|
107
|
+
checked = isDark,
|
|
108
|
+
onCheckedChange = { checked ->
|
|
109
|
+
appearanceManager.setMode(if (checked) AppearanceMode.DARK else AppearanceMode.SYSTEM)
|
|
110
|
+
},
|
|
111
|
+
modifier = Modifier.testTag("profile_dark_mode_toggle")
|
|
112
|
+
)
|
|
113
|
+
},
|
|
114
|
+
modifier = Modifier.testTag("profile_appearance")
|
|
115
|
+
)
|
|
116
|
+
}
|
|
105
117
|
if (AppConfig.Features.ENABLE_ANIMATIONS) {
|
|
106
118
|
HorizontalDivider()
|
|
107
119
|
ListItem(
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
appId: ${APP_PACKAGE_NAME}
|
|
2
|
+
---
|
|
3
|
+
- clearState
|
|
4
|
+
- launchApp
|
|
5
|
+
- extendedWaitUntil:
|
|
6
|
+
visible: "Home"
|
|
7
|
+
timeout: 3000
|
|
8
|
+
# Navigate to Profile
|
|
9
|
+
- tapOn: "Profile"
|
|
10
|
+
- extendedWaitUntil:
|
|
11
|
+
visible:
|
|
12
|
+
id: "profile_avatar"
|
|
13
|
+
timeout: 3000
|
|
14
|
+
# Verify dark mode toggle is visible
|
|
15
|
+
- assertVisible:
|
|
16
|
+
id: "profile_dark_mode_toggle"
|
|
17
|
+
# Toggle dark mode ON
|
|
18
|
+
- tapOn:
|
|
19
|
+
id: "profile_dark_mode_toggle"
|
|
20
|
+
# Verify profile still renders in dark mode
|
|
21
|
+
- assertVisible:
|
|
22
|
+
id: "profile_avatar"
|
|
23
|
+
optional: true
|
|
24
|
+
- assertVisible:
|
|
25
|
+
id: "profile_name"
|
|
26
|
+
optional: true
|
|
27
|
+
# Navigate to Home to verify dark mode persists across tabs
|
|
28
|
+
- tapOn: "Home"
|
|
29
|
+
- extendedWaitUntil:
|
|
30
|
+
visible: "Home"
|
|
31
|
+
timeout: 3000
|
|
32
|
+
# Navigate back to Profile
|
|
33
|
+
- tapOn: "Profile"
|
|
34
|
+
- extendedWaitUntil:
|
|
35
|
+
visible:
|
|
36
|
+
id: "profile_avatar"
|
|
37
|
+
timeout: 3000
|
|
38
|
+
# Verify toggle is still ON (dark mode persisted)
|
|
39
|
+
- assertVisible:
|
|
40
|
+
id: "profile_dark_mode_toggle"
|
|
41
|
+
# Toggle dark mode OFF (back to system)
|
|
42
|
+
- tapOn:
|
|
43
|
+
id: "profile_dark_mode_toggle"
|
|
44
|
+
# Verify profile renders normally
|
|
45
|
+
- assertVisible:
|
|
46
|
+
id: "profile_avatar"
|
|
47
|
+
optional: true
|
|
48
|
+
- assertVisible:
|
|
49
|
+
id: "profile_name"
|
|
50
|
+
optional: true
|