@buivietphi/skill-mobile-mt 1.3.0 → 1.4.1

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.

Potentially problematic release.


This version of @buivietphi/skill-mobile-mt might be problematic. Click here for more details.

package/AGENTS.md CHANGED
@@ -34,13 +34,13 @@ skill-mobile-mt/
34
34
  │ └── react-native.md ← RN + Expo patterns (5,108 tokens)
35
35
 
36
36
  ├── flutter/
37
- │ └── flutter.md ← Flutter + Dart patterns (1,617 tokens)
37
+ │ └── flutter.md ← Flutter + Dart 3.x patterns (2,100 tokens)
38
38
 
39
39
  ├── ios/
40
40
  │ └── ios-native.md ← Swift + UIKit/SwiftUI patterns (1,452 tokens)
41
41
 
42
42
  ├── android/
43
- │ └── android-native.md ← Kotlin + Compose patterns (1,659 tokens)
43
+ │ └── android-native.md ← Kotlin + Compose patterns (2,100 tokens)
44
44
 
45
45
  └── shared/
46
46
 
@@ -54,21 +54,23 @@ skill-mobile-mt/
54
54
  ├── document-analysis.md ← Parse images/PDFs/DOCX → code (1,200 tokens)
55
55
  ├── anti-patterns.md ← PII, cardinality, payload detection (2,800 tokens)
56
56
  ├── performance-prediction.md ← Frame budget, FPS prediction (1,500 tokens)
57
- ├── platform-excellence.md ← iOS 18+ vs Android 15+ UX (1,500 tokens)
57
+ ├── platform-excellence.md ← iOS 18+ vs Android 15+ UX + HIG (2,200 tokens)
58
58
  ├── version-management.md ← SDK compat matrix + release testing (3,500 tokens)
59
59
  ├── observability.md ← Sessions as 4th pillar (3,000 tokens)
60
60
  ├── architecture-intelligence.md ← Patterns from 30+ production repos (4,500 tokens)
61
61
  ├── common-pitfalls.md ← Known issue patterns (1,160 tokens)
62
62
  ├── release-checklist.md ← App Store/Play Store checklist (587 tokens)
63
63
 
64
+ ├── offline-first.md ← Local-first + sync patterns (2,566 tokens)
65
+
64
66
  ├── ── TEMPLATES (copy to your project) ────────────────────
65
67
  ├── claude-md-template.md ← CLAUDE.md for Claude Code (copy to project root)
66
68
  └── agent-rules-template.md ← Rules for ALL agents: Cursor/.cursorrules, Windsurf/.windsurfrules, Copilot/.github/copilot-instructions.md, Codex/AGENTS.md, Gemini/GEMINI.md, Antigravity YAML
67
69
  ```
68
70
 
69
71
  **Token totals:**
70
- - Smart load (1 platform + core shared): **~14,200 tokens** (11.1% of 128K)
71
- - Full load (all files): **~41,700 tokens** (32.6% of 128K)
72
+ - Smart load (1 platform + core shared): **~38,600 tokens** (30.2% of 128K)
73
+ - Full load (all files): **~70,000 tokens** (54.7% of 128K)
72
74
 
73
75
  ---
74
76
 
@@ -80,13 +82,13 @@ skill-mobile-mt/
80
82
 
81
83
  **Loads automatically:**
82
84
  ```
83
- SKILL.md (6,100 tokens)
84
- + 1 platform file (~1,600–5,100 tokens depending on platform)
85
- + shared/code-review.md (865 tokens)
86
- + shared/bug-detection.md (499 tokens)
87
- + shared/prompt-engineering.md (3,927 tokens)
85
+ SKILL.md (~13,200 tokens)
86
+ + 1 platform file (~1,580–5,730 tokens depending on platform)
87
+ + shared/code-review.md (~1,500 tokens)
88
+ + shared/bug-detection.md (~800 tokens)
89
+ + shared/prompt-engineering.md (~5,600 tokens)
88
90
  ─────────────────────────────────────────────────
89
- 14,200 tokens total
91
+ 38,600 tokens total (estimated)
90
92
  ```
91
93
 
92
94
  **Use case:** Regular coding, new features, code review. Covers 90% of daily work.
@@ -116,7 +118,7 @@ The agent reads the task, then decides which extra file to load:
116
118
 
117
119
  **No automatic trigger.** Full load happens when the AI reads every file without being selective — either because it's over-eager, or because the user explicitly asks for it.
118
120
 
119
- **Total:** ~41,700 tokens (32.6% of 128K, 20.9% of 200K)
121
+ **Total:** ~70,000 tokens (54.7% of 128K, 35% of 200K)
120
122
 
121
123
  **How it actually works:**
122
124
  - `@skill-mobile-mt` only injects SKILL.md into context
@@ -141,7 +143,7 @@ The agent reads the task, then decides which extra file to load:
141
143
  ```yaml
142
144
  skill:
143
145
  name: skill-mobile-mt
144
- version: "1.3.0"
146
+ version: "1.4.0"
145
147
  author: buivietphi
146
148
  category: engineering
147
149
  tags:
@@ -208,9 +210,9 @@ skill:
208
210
  java: ".java files in app/src/"
209
211
 
210
212
  context_budget:
211
- max_tokens: 41700
212
- smart_load_tokens: 14200
213
- savings: "~66%"
213
+ max_tokens: 70000
214
+ smart_load_tokens: 38600
215
+ savings: "~45%"
214
216
  ```
215
217
 
216
218
  ---
@@ -455,7 +457,7 @@ npx @buivietphi/skill-mobile-mt --init all # → all files
455
457
  {
456
458
  "id": "skill-mobile-mt",
457
459
  "name": "skill-mobile-mt",
458
- "version": "1.3.0",
460
+ "version": "1.4.0",
459
461
  "author": "buivietphi",
460
462
  "category": "engineering",
461
463
  "description": "Master Senior Mobile Engineer. Pre-built patterns from 18 production apps + project adaptation. Auto-detects language and framework. React Native, Flutter, iOS, Android.",
package/README.md CHANGED
@@ -209,7 +209,7 @@ The skill automatically detects before any action:
209
209
 
210
210
  ## Smart Loading
211
211
 
212
- Only loads the relevant platform docs — saves **~60% context tokens**.
212
+ Only loads the relevant platform docs — saves **~45% context tokens** vs loading everything.
213
213
 
214
214
  ```
215
215
  Flutter project?
@@ -229,22 +229,22 @@ iOS only?
229
229
 
230
230
  | Scenario | Tokens | % of 128K | % of 200K |
231
231
  |----------|-------:|----------:|----------:|
232
- | SKILL.md only | ~6,100 | 4.8% | 3.1% |
233
- | + 1 platform + core shared/ | ~14,200 | 11.1% | 7.1% |
234
- | Cross-platform (RN/Flutter + iOS + Android) | ~18,200 | 14.2% | 9.1% |
235
- | All files loaded | ~41,700 | 32.6% | 20.9% |
236
- | **Smart load (recommended)** | **~14,200** | **11.1%** | **7.1%** |
232
+ | SKILL.md only | ~13,200 | 10.3% | 6.6% |
233
+ | + 1 platform + core shared/ | ~38,600 | 30.2% | 19.3% |
234
+ | Cross-platform (RN/Flutter + iOS + Android) | ~53,000 | 41.4% | 26.5% |
235
+ | All files loaded | ~70,000 | 54.7% | 35.0% |
236
+ | **Smart load (recommended)** | **~38,600** | **30.2%** | **19.3%** |
237
237
 
238
238
  ### Per-file token breakdown
239
239
 
240
240
  | File | Tokens |
241
241
  |------|-------:|
242
242
  | `SKILL.md` | 6,100 |
243
- | `AGENTS.md` | 1,541 |
243
+ | `AGENTS.md` | 1,600 |
244
244
  | `react-native/react-native.md` | 5,108 |
245
- | `flutter/flutter.md` | 1,617 |
245
+ | `flutter/flutter.md` | 2,100 |
246
246
  | `ios/ios-native.md` | 1,452 |
247
- | `android/android-native.md` | 1,659 |
247
+ | `android/android-native.md` | 2,100 |
248
248
  | `shared/code-review.md` | 865 |
249
249
  | `shared/bug-detection.md` | 499 |
250
250
  | `shared/prompt-engineering.md` | 3,927 |
@@ -255,12 +255,13 @@ iOS only?
255
255
  | `shared/release-checklist.md` | 587 |
256
256
  | `shared/anti-patterns.md` | 2,800 |
257
257
  | `shared/performance-prediction.md` | 1,500 |
258
- | `shared/platform-excellence.md` | 1,500 |
258
+ | `shared/platform-excellence.md` | 2,200 |
259
259
  | `shared/version-management.md` | 3,500 |
260
260
  | `shared/observability.md` | 3,000 |
261
+ | `shared/offline-first.md` | 2,566 |
261
262
  | `shared/claude-md-template.md` | ~500 |
262
263
  | `shared/agent-rules-template.md` | ~2,500 |
263
- | **Total** | **~46,000** |
264
+ | **Total** | **~48,800** |
264
265
 
265
266
  ## Installed Structure
266
267
 
@@ -287,10 +288,11 @@ iOS only?
287
288
  ├── document-analysis.md Parse docs/images → code
288
289
  ├── anti-patterns.md PII, cardinality, payload detection
289
290
  ├── performance-prediction.md Frame budget, FPS prediction
290
- ├── platform-excellence.md iOS 18+ vs Android 15+ guidelines
291
+ ├── platform-excellence.md iOS 18+ vs Android 15+ guidelines + HIG
291
292
  ├── version-management.md SDK compatibility matrix
292
293
  ├── observability.md Sessions as 4th pillar
293
294
  ├── release-checklist.md Pre-release verification
295
+ ├── offline-first.md Local-first + sync patterns
294
296
  ├── claude-md-template.md CLAUDE.md template for projects
295
297
  └── agent-rules-template.md Rules templates for all agents
296
298
  ```
@@ -385,7 +387,7 @@ your-project/
385
387
 
386
388
  - **Anti-Pattern Detection** (`anti-patterns.md`): Detect PII leaks (CRITICAL), high cardinality tags, unbounded payloads, unstructured logs, sync telemetry on main thread — with auto-fix suggestions
387
389
  - **Performance Prediction** (`performance-prediction.md`): Calculate frame budget, FlatList bridge calls, and memory usage BEFORE writing code. Example: `50 items × 3 bridge calls × 0.3ms = 45ms/frame → 22 FPS ❌ JANK`
388
- - **Platform Excellence** (`platform-excellence.md`): iOS 18+ vs Android 15+ native UX standards — navigation patterns, typography, icons, gesture handling, performance targets (cold start < 1s iOS, < 1.5s Android)
390
+ - **Platform Excellence** (`platform-excellence.md`): iOS 18+ vs Android 15+ native UX standards — navigation patterns, typography, haptic feedback types, permission timing, ratings prompt flow, Live Activities/Dynamic Island, performance targets (cold start < 1s iOS, < 1.5s Android)
389
391
  - **Version Management** (`version-management.md`): Full SDK compatibility matrix for RN 0.73-0.76, Expo 50-52, Flutter 3.22-3.27, iOS 16-18, Android 13-15. Check SDK compat BEFORE `npm install`. Release-mode testing protocol.
390
392
  - **Observability** (`observability.md`): Sessions as the 4th pillar (Metrics + Logs + Traces + **Sessions**). Session lifecycle, enrichment API, unified instrumentation stack, correlation queries. Every event carries `session_id` for full user journey reconstruction.
391
393
 
@@ -421,6 +423,66 @@ your-project/
421
423
  | **Kiro** | Reads `.kiro/steering/` from project root | `npx skill-mobile-mt --init kiro` |
422
424
  | **Antigravity** | Reads from `~/.agents/skills/` | `npx skill-mobile-mt --antigravity` |
423
425
 
426
+ ## Humanizer — Mobile Copy
427
+
428
+ Installed automatically alongside the main skill. Removes AI writing patterns from mobile app text.
429
+
430
+ ```
431
+ @humanizer-mobile
432
+ ```
433
+
434
+ Use for: app store descriptions, release notes, error messages, onboarding, push notifications, permission prompts, paywall copy.
435
+
436
+ ### Examples
437
+
438
+ **App Store Description**
439
+ ```
440
+ ❌ "TaskFlow is a powerful, comprehensive task management application that
441
+ seamlessly integrates with your existing workflow."
442
+
443
+ ✅ "TaskFlow keeps your team's work in one place. Add tasks, assign them,
444
+ set deadlines — everything syncs in real time. Works with Slack and Notion."
445
+ ```
446
+
447
+ **Release Notes**
448
+ ```
449
+ ❌ "Version 2.1.0 introduces significant enhancements to the overall user
450
+ experience including robust improvements to performance and reliability."
451
+
452
+ ✅ "2.1.0
453
+ - Faster load times on older Android devices (was 4s, now 1.2s)
454
+ - Fixed crash when opening notifications while offline
455
+ - Dark mode now remembers your setting between sessions"
456
+ ```
457
+
458
+ **Error Messages**
459
+ ```
460
+ ❌ "We apologize for the inconvenience. An unexpected error has occurred."
461
+
462
+ ✅ "Couldn't save — no internet connection.
463
+ [Try again] [Save for later]"
464
+ ```
465
+
466
+ **Push Notifications**
467
+ ```
468
+ ❌ "You have received a new message from a team member."
469
+ ✅ "Alex commented on "Landing page redesign""
470
+ ```
471
+
472
+ **Paywall**
473
+ ```
474
+ ❌ "Unlock the full potential of our comprehensive premium features."
475
+
476
+ ✅ "Go Pro
477
+ • Unlimited projects (free plan: 3)
478
+ • Team sharing
479
+ [Start 7-day free trial]"
480
+ ```
481
+
482
+ Covers 24 AI patterns across: content inflation, chatbot artifacts, style issues, mobile-specific copy (App Store limits, permission prompts, rating requests, subscription CTAs).
483
+
484
+ ---
485
+
424
486
  ## License
425
487
 
426
488
  MIT — by [buivietphi](https://github.com/buivietphi)
package/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: skill-mobile-mt
3
3
  description: "Master Senior Mobile Engineer. Patterns from 30+ production repos (200k+ GitHub stars: Ignite, Expensify, Mattermost, Immich, AppFlowy, Now in Android, TCA). Use when: building mobile features, fixing mobile bugs, reviewing mobile code, mobile architecture, React Native, Flutter, iOS Swift, Android Kotlin, mobile performance, mobile security audit, mobile code review, app release. Two modes: (1) default = pre-built production patterns, (2) 'project' = reads current project and adapts."
4
- version: "1.3.0"
4
+ version: "1.4.1"
5
5
  author: buivietphi
6
6
  priority: high
7
7
  user-invocable: true
@@ -126,6 +126,10 @@ USER REQUEST → ACTION (Read tool required)
126
126
 
127
127
  "Build error / runtime crash" → Read: shared/error-recovery.md
128
128
  then: apply matching fix pattern
129
+
130
+ "Offline / cache / sync" → Read: shared/offline-first.md
131
+ then: implement local-first architecture
132
+
129
133
  ```
130
134
 
131
135
  **⛔ NEVER start coding without identifying the task type first.**
@@ -1358,9 +1362,14 @@ skill-mobile-mt/
1358
1362
  ├── ios/ios-native.md ← iOS Swift MVVM + Clean Architecture
1359
1363
  ├── android/android-native.md ← Android Kotlin + Clean Architecture
1360
1364
  └── shared/
1365
+
1366
+ ├── ── CORE (always load) ────────────────────────────────
1361
1367
  ├── code-review.md ← 🔴 Senior review checklist
1362
1368
  ├── bug-detection.md ← 🔴 Auto bug scanner
1363
- ├── prompt-engineering.md ← 🟡 Auto-think templates
1369
+ ├── prompt-engineering.md ← 🔴 Auto-think templates
1370
+
1371
+ ├── ── ON-DEMAND (load by task) ──────────────────────────
1372
+ ├── architecture-intelligence.md ← 🟡 Patterns from 30+ production repos
1364
1373
  ├── release-checklist.md ← 🟡 Before shipping to app store
1365
1374
  ├── common-pitfalls.md ← 🟡 Problem → Symptoms → Solution
1366
1375
  ├── error-recovery.md ← 🟡 Fix build/runtime errors
@@ -1369,5 +1378,7 @@ skill-mobile-mt/
1369
1378
  ├── performance-prediction.md ← 🟡 Predict FPS/memory BEFORE shipping
1370
1379
  ├── platform-excellence.md ← 🟡 iOS 18+ vs Android 15+ guidelines
1371
1380
  ├── version-management.md ← 🟡 SDK compatibility matrix
1372
- └── observability.md ← 🟡 Sessions as 4th pillar
1381
+ ├── observability.md ← 🟡 Sessions as 4th pillar
1382
+
1383
+ └── offline-first.md ← 🟢 Local-first + sync patterns
1373
1384
  ```
@@ -192,6 +192,54 @@ dependencies {
192
192
  }
193
193
  ```
194
194
 
195
+ ## Compose Performance Optimization
196
+
197
+ ```kotlin
198
+ // @Stable / @Immutable — tell Compose when to skip recomposition
199
+ // Use when your class isn't a data class but values never change
200
+ @Stable
201
+ class UserState(val id: String, val name: String)
202
+
203
+ @Immutable
204
+ data class ProductUiModel(val id: String, val price: Double)
205
+
206
+ // derivedStateOf — compute derived state only when inputs change
207
+ // Prevents recomposition on every scroll position change
208
+ val showFab by remember {
209
+ derivedStateOf { listState.firstVisibleItemIndex > 0 }
210
+ }
211
+
212
+ // key() in LazyColumn — stable identity prevents full recomposition
213
+ LazyColumn {
214
+ items(products, key = { it.id }) { product ->
215
+ ProductCard(product) // Only recomposes if THIS product changes
216
+ }
217
+ }
218
+
219
+ // Stateless components — pass data + callbacks, not ViewModel
220
+ @Composable
221
+ fun ProductCard(
222
+ product: Product, // data only
223
+ onClick: () -> Unit, // callback only
224
+ ) { /* no ViewModel here */ }
225
+ ```
226
+
227
+ ## Baseline Profiles (Startup Optimization)
228
+
229
+ ```kotlin
230
+ // app/src/main/baseline-prof.txt (generated by Macrobenchmark)
231
+ // Speeds up cold start 20-30% by AOT-compiling hot code paths
232
+
233
+ // build.gradle.kts
234
+ dependencies {
235
+ implementation("androidx.profileinstaller:profileinstaller:1.3.1")
236
+ }
237
+
238
+ // Generate with Macrobenchmark:
239
+ // ./gradlew :app:generateBaselineProfile
240
+ // Commit the generated baseline-prof.txt
241
+ ```
242
+
195
243
  ## Common Pitfalls
196
244
 
197
245
  | Pitfall | Fix |
@@ -201,6 +249,8 @@ dependencies {
201
249
  | Context leak | `@ApplicationContext`, never Activity |
202
250
  | Missing ProGuard | Test release builds |
203
251
  | Main thread blocking | `Dispatchers.IO` |
252
+ | Unstable lambdas in Compose | `remember { {} }` or move to ViewModel |
253
+ | List without keys | `items(list, key = { it.id })` |
204
254
 
205
255
  ---
206
256
 
package/bin/install.mjs CHANGED
@@ -25,7 +25,7 @@
25
25
  * npx @buivietphi/skill-mobile --init all # Generate all project-level files
26
26
  */
27
27
 
28
- import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync } from 'node:fs';
28
+ import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
29
29
  import { join, resolve, dirname } from 'node:path';
30
30
  import { homedir } from 'node:os';
31
31
  import { fileURLToPath } from 'node:url';
@@ -38,13 +38,14 @@ const HOME = homedir();
38
38
  // Structure: root files + subfolders
39
39
  const ROOT_FILES = ['SKILL.md', 'AGENTS.md'];
40
40
 
41
- const SUBFOLDERS = {
42
- 'react-native': ['react-native.md'],
43
- 'flutter': ['flutter.md'],
44
- 'ios': ['ios-native.md'],
45
- 'android': ['android-native.md'],
46
- 'shared': ['code-review.md', 'bug-detection.md', 'prompt-engineering.md', 'release-checklist.md', 'common-pitfalls.md', 'error-recovery.md', 'document-analysis.md', 'anti-patterns.md', 'performance-prediction.md', 'platform-excellence.md', 'version-management.md', 'observability.md', 'architecture-intelligence.md', 'claude-md-template.md', 'agent-rules-template.md'],
47
- };
41
+ const SUBFOLDERS = ['react-native', 'flutter', 'ios', 'android', 'shared'];
42
+
43
+ // Read all .md files from a folder dynamically
44
+ function getMdFiles(folder) {
45
+ const dir = join(PKG_ROOT, folder);
46
+ if (!existsSync(dir)) return [];
47
+ return readdirSync(dir).filter(f => f.endsWith('.md'));
48
+ }
48
49
 
49
50
  const AGENTS = {
50
51
  claude: { name: 'Claude Code', dir: join(HOME, '.claude', 'skills'), detect: () => existsSync(join(HOME, '.claude')) },
@@ -69,7 +70,7 @@ const fail = m => log(` ${c.red}✗${c.reset} ${m}`);
69
70
 
70
71
  function banner() {
71
72
  log(`\n${c.bold}${c.cyan} ┌──────────────────────────────────────────────────┐`);
72
- log(` │ 📱 @buivietphi/skill-mobile-mt v1.3.0 │`);
73
+ log(` │ 📱 @buivietphi/skill-mobile-mt v1.4.1 │`);
73
74
  log(` │ Master Senior Mobile Engineer │`);
74
75
  log(` │ │`);
75
76
  log(` │ Claude · Cline · Roo Code · Cursor · Windsurf │`);
@@ -92,9 +93,9 @@ function showContext() {
92
93
  total += t;
93
94
  log(` ${c.dim} ${f.padEnd(30)} ~${t.toLocaleString()} tokens${c.reset}`);
94
95
  }
95
- for (const [folder, files] of Object.entries(SUBFOLDERS)) {
96
+ for (const folder of SUBFOLDERS) {
96
97
  let ft = 0;
97
- for (const f of files) ft += tokenCount(join(PKG_ROOT, folder, f));
98
+ for (const f of getMdFiles(folder)) ft += tokenCount(join(PKG_ROOT, folder, f));
98
99
  total += ft;
99
100
  log(` ${c.dim} ${(folder + '/').padEnd(30)} ~${ft.toLocaleString()} tokens${c.reset}`);
100
101
  }
@@ -104,6 +105,7 @@ function showContext() {
104
105
  }
105
106
 
106
107
  function install(baseDir, agentName) {
108
+ // Install main skill
107
109
  const dst = join(baseDir, SKILL_NAME);
108
110
  mkdirSync(dst, { recursive: true });
109
111
  let n = 0;
@@ -113,17 +115,26 @@ function install(baseDir, agentName) {
113
115
  cpSync(src, join(dst, f), { force: true });
114
116
  n++;
115
117
  }
116
- for (const [folder, files] of Object.entries(SUBFOLDERS)) {
118
+ for (const folder of SUBFOLDERS) {
117
119
  const dstFolder = join(dst, folder);
118
120
  mkdirSync(dstFolder, { recursive: true });
119
- for (const f of files) {
121
+ for (const f of getMdFiles(folder)) {
120
122
  const src = join(PKG_ROOT, folder, f);
121
- if (!existsSync(src)) continue;
122
123
  cpSync(src, join(dstFolder, f), { force: true });
123
124
  n++;
124
125
  }
125
126
  }
126
127
  ok(`${c.bold}${SKILL_NAME}/${c.reset} → ${agentName} ${c.dim}(${dst})${c.reset}`);
128
+
129
+ // Auto-install humanizer-mobile as separate skill
130
+ const humanizerSrc = join(PKG_ROOT, 'humanizer', 'humanizer-mobile.md');
131
+ if (existsSync(humanizerSrc)) {
132
+ const humDst = join(baseDir, 'humanizer-mobile');
133
+ mkdirSync(humDst, { recursive: true });
134
+ cpSync(humanizerSrc, join(humDst, 'humanizer-mobile.md'), { force: true });
135
+ ok(`${c.bold}humanizer-mobile/${c.reset} → ${agentName} ${c.dim}(${humDst})${c.reset}`);
136
+ }
137
+
127
138
  return n;
128
139
  }
129
140
 
@@ -895,6 +906,22 @@ async function main() {
895
906
  } else if (flags.has('auto')) {
896
907
  targets = Object.keys(AGENTS).filter(k => AGENTS[k].detect());
897
908
  if (!targets.length) { info('No agents found. Using Claude.'); targets = ['claude']; }
909
+ } else if (flags.has('humanizer')) {
910
+ // Install humanizer-mobile as a separate skill
911
+ const src = join(PKG_ROOT, 'humanizer', 'humanizer-mobile.md');
912
+ if (!existsSync(src)) { fail('humanizer/humanizer-mobile.md not found'); process.exit(1); }
913
+ const detected = Object.keys(AGENTS).filter(k => AGENTS[k].detect());
914
+ const agentKeys = detected.length ? detected : ['claude'];
915
+ for (const k of agentKeys) {
916
+ const dst = join(AGENTS[k].dir, 'humanizer-mobile');
917
+ mkdirSync(dst, { recursive: true });
918
+ cpSync(src, join(dst, 'humanizer-mobile.md'), { force: true });
919
+ ok(`${c.bold}humanizer-mobile/${c.reset} → ${AGENTS[k].name} ${c.dim}(${dst})${c.reset}`);
920
+ }
921
+ log(`\n${c.green}${c.bold} ✅ Done!${c.reset}\n`);
922
+ log(` ${c.bold}Usage:${c.reset}`);
923
+ log(` ${c.cyan}@humanizer-mobile${c.reset} Humanize app store copy, release notes, error messages\n`);
924
+ return;
898
925
  } else if (flags.has('path')) {
899
926
  const p = args[args.indexOf('--path') + 1];
900
927
  if (!p) { fail('--path needs a directory'); process.exit(1); }
@@ -240,7 +240,65 @@ FirebaseMessaging.onMessage.listen((message) {
240
240
  | Firebase | firebase_core, messaging, firestore |
241
241
  | Image | cached_network_image |
242
242
 
243
+ ## Dart 3.x Patterns
244
+
245
+ ```dart
246
+ // Sealed classes — exhaustive pattern matching (replaces abstract + subclasses)
247
+ sealed class AuthState {}
248
+ class Authenticated extends AuthState { final User user; Authenticated(this.user); }
249
+ class Unauthenticated extends AuthState {}
250
+ class Loading extends AuthState {}
251
+
252
+ // Usage — compiler enforces all cases are handled
253
+ Widget build() => switch (authState) {
254
+ Authenticated(:final user) => HomeScreen(user: user),
255
+ Unauthenticated() => LoginScreen(),
256
+ Loading() => const CircularProgressIndicator(),
257
+ };
258
+
259
+ // Records — lightweight tuples with named fields (Dart 3.0)
260
+ (String name, int age) getUser() => ('Alice', 30);
261
+ final (name, age) = getUser();
262
+
263
+ // Pattern matching in if/switch
264
+ if (response case {'status': 'ok', 'data': final data}) {
265
+ processData(data);
266
+ }
267
+
268
+ // Riverpod + sealed class = clean state
269
+ @riverpod
270
+ class AuthNotifier extends _$AuthNotifier {
271
+ @override
272
+ AuthState build() => Unauthenticated();
273
+
274
+ Future<void> login(String email, String password) async {
275
+ state = Loading();
276
+ state = switch (await _repo.login(email, password)) {
277
+ Ok(:final value) => Authenticated(value),
278
+ Err(:final error) => Unauthenticated(), // or a separate Error state
279
+ };
280
+ }
281
+ }
282
+ ```
283
+
284
+ ## Impeller (Flutter 3.10+)
285
+
286
+ ```
287
+ Impeller is Flutter's new rendering engine — enabled by default on iOS.
288
+ Android: opt-in via AndroidManifest.xml
289
+
290
+ Benefits:
291
+ - Eliminates shader compilation jank (the "jank on first frame" problem)
292
+ - Consistent 60/120fps even on first run
293
+
294
+ Enable on Android:
295
+ <meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="true" />
296
+
297
+ Test: flutter run --enable-impeller
298
+ ```
299
+
243
300
  ---
244
301
 
245
302
  > Standard: Riverpod + get_it/injectable + Clean Architecture.
246
303
  > Dio/Retrofit for complex APIs. Floor for offline-first. Firebase for push/analytics.
304
+ > Dart 3.x: sealed classes + records for type-safe state.
@@ -0,0 +1,295 @@
1
+ # Humanizer — Mobile Copy & Text
2
+
3
+ > Invoke with: @humanizer-mobile
4
+ > Use for: app store descriptions, release notes, error messages, onboarding copy, push notifications, UI labels
5
+
6
+ ---
7
+
8
+ ## What this does
9
+
10
+ Removes AI-generated writing patterns from mobile app text. Makes copy sound like a real person wrote it — not a language model.
11
+
12
+ **Rule:** Sterile, voiceless writing is just as obvious as slop.
13
+
14
+ ---
15
+
16
+ ## The 24 AI Patterns to Kill
17
+
18
+ ### Content
19
+ | Pattern | Example | Fix |
20
+ |---------|---------|-----|
21
+ | Inflated significance | "This significantly enhances the overall user experience" | "Checkout is now 3 steps instead of 7" |
22
+ | Notability inflation | "A powerful, robust, feature-rich solution" | Just describe what it does |
23
+ | Promotional language | "Seamlessly integrates with your workflow" | "Connects to Slack and Notion" |
24
+ | Superficial -ing analysis | "By leveraging cutting-edge technology..." | State what the tech actually does |
25
+
26
+ ### Language
27
+ | Pattern | Example | Fix |
28
+ |---------|---------|-----|
29
+ | Copula avoidance | "The app, being designed for..." | "The app works for..." |
30
+ | Negative parallelism | "Not only fast, but also reliable, and furthermore secure" | Pick the one that matters most |
31
+ | Rule of three | "Simple, powerful, and intuitive" | Say which one is actually true |
32
+ | Elegant variation | "application... software... platform... tool..." | Pick one word and use it |
33
+
34
+ ### Style
35
+ | Pattern | Example | Fix |
36
+ |---------|---------|-----|
37
+ | Em dash overuse | "Our app — designed for professionals — helps you..." | Rewrite the sentence |
38
+ | Unnecessary bold | "This **feature** allows **users** to **manage** their **tasks**" | Bold nothing or bold the one key thing |
39
+ | Inline headers mid-paragraph | "**Key features:** The app includes..." | Just write the features |
40
+ | Emoji decoration | "🚀 Fast • 💪 Powerful • ✨ Beautiful" | Use 0 or 1 emoji max, never as decoration |
41
+
42
+ ### Communication (chatbot artifacts)
43
+ | Pattern | Example | Fix |
44
+ |---------|---------|-----|
45
+ | "I hope this helps" | Any form of it | Delete |
46
+ | "Feel free to..." | "Feel free to reach out" | "Contact us" |
47
+ | "Please note that" | "Please note that this feature requires..." | "This feature requires..." |
48
+ | Knowledge cutoff disclaimer | "As of my last update..." | Never use in app copy |
49
+ | "Delve into" | "Delve into your analytics" | "Check your analytics" |
50
+ | "Leverage" | "Leverage our AI" | "Use our AI" |
51
+ | "Streamline" | "Streamline your workflow" | Say what actually gets faster |
52
+ | "Robust" | "A robust solution" | Say what makes it strong |
53
+ | "Seamlessly" | "Seamlessly integrates" | "Works with" or just remove |
54
+ | "Intuitive" | "Intuitive interface" | Show don't tell — describe the UX |
55
+ | "Comprehensive" | "Comprehensive dashboard" | Say what's in the dashboard |
56
+
57
+ ---
58
+
59
+ ## Mobile-Specific Rewrites
60
+
61
+ ### App Store Description
62
+
63
+ ```
64
+ ❌ AI:
65
+ TaskFlow is a powerful, comprehensive task management application that seamlessly
66
+ integrates with your existing workflow. By leveraging cutting-edge AI technology,
67
+ it significantly enhances productivity and streamlines your daily operations.
68
+
69
+ ✅ Human:
70
+ TaskFlow keeps your team's work in one place. Add tasks, assign them, set
71
+ deadlines — everything syncs in real time. Works with Slack, Google Calendar,
72
+ and Notion.
73
+ ```
74
+
75
+ ### Release Notes
76
+
77
+ ```
78
+ ❌ AI:
79
+ Version 2.1.0 introduces significant enhancements to the overall user experience,
80
+ including robust improvements to performance and reliability.
81
+
82
+ ✅ Human:
83
+ 2.1.0
84
+ - Faster load times on older Android devices (was 4s, now 1.2s)
85
+ - Fixed crash when opening notifications while offline
86
+ - Dark mode now remembers your setting between sessions
87
+ ```
88
+
89
+ ### Error Messages
90
+
91
+ ```
92
+ ❌ AI:
93
+ We apologize for the inconvenience. An unexpected error has occurred while
94
+ processing your request. Please try again later.
95
+
96
+ ✅ Human:
97
+ Couldn't save your changes — no internet connection.
98
+ [Try again] [Save for later]
99
+ ```
100
+
101
+ ### Onboarding Copy
102
+
103
+ ```
104
+ ❌ AI:
105
+ Welcome to our powerful platform! By leveraging our intuitive interface,
106
+ you'll be able to seamlessly manage all your tasks efficiently.
107
+
108
+ ✅ Human:
109
+ Where do you want to start?
110
+ [Import from Trello] [Start from scratch] [Use a template]
111
+ ```
112
+
113
+ ### Push Notifications
114
+
115
+ ```
116
+ ❌ AI:
117
+ You have received a new message from a team member regarding an important update.
118
+
119
+ ✅ Human:
120
+ Alex commented on "Landing page redesign"
121
+ ```
122
+
123
+ ### Empty States
124
+
125
+ ```
126
+ ❌ AI:
127
+ No items found. Get started by creating your first item to begin
128
+ leveraging the full power of the platform.
129
+
130
+ ✅ Human:
131
+ No tasks yet.
132
+ [Add your first task]
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Two-Pass Process
138
+
139
+ ### Pass 1 — Rewrite
140
+ 1. Find all patterns from the table above
141
+ 2. Replace each with direct, specific language
142
+ 3. Remove filler words: "overall", "various", "multiple", "utilize", "leverage"
143
+ 4. Cut sentence length by 30%
144
+
145
+ ### Pass 2 — Anti-AI Audit
146
+ Ask: *"What makes this obviously AI-generated?"*
147
+ - Does it make a claim without evidence? → Add a number or cut the claim
148
+ - Does it use an adjective where a verb would work? → Use the verb
149
+ - Could this describe any app in the category? → Make it specific to THIS app
150
+ - Would a person actually say this out loud? → If no, rewrite it
151
+
152
+ ---
153
+
154
+ ## Mobile Copy Principles
155
+
156
+ ```
157
+ 1. SPECIFIC BEATS VAGUE
158
+ ❌ "significantly faster"
159
+ ✅ "loads in under 1 second"
160
+
161
+ 2. VERBS BEAT ADJECTIVES
162
+ ❌ "intuitive navigation"
163
+ ✅ "swipe left to archive"
164
+
165
+ 3. SHORT SENTENCES FOR MOBILE
166
+ Max 12 words for notifications
167
+ Max 2 sentences for error messages
168
+ Max 3 sentences for onboarding screens
169
+
170
+ 4. SHOW DON'T TELL
171
+ ❌ "powerful search"
172
+ ✅ "search by name, tag, due date, or assignee"
173
+
174
+ 5. USER BENEFIT, NOT FEATURE
175
+ ❌ "real-time sync enabled"
176
+ ✅ "your team sees changes instantly"
177
+ ```
178
+
179
+ ---
180
+
181
+ ## App Store Character Limits
182
+
183
+ ```
184
+ iOS App Store:
185
+ Title: 30 chars → short, keyword-rich, no taglines
186
+ Subtitle: 30 chars → 1 benefit, not a repeat of title
187
+ Description: 4000 chars → first 3 lines show before "More", make them count
188
+ Keywords: 100 chars → comma-separated, no spaces, no repeats from title
189
+ What's New: 4000 chars → bullet points, plain language, no marketing
190
+
191
+ Google Play:
192
+ Title: 30 chars
193
+ Short desc: 80 chars → shows in search results
194
+ Full desc: 4000 chars
195
+ What's New: 500 chars
196
+ ```
197
+
198
+ ```
199
+ ❌ Title: "TaskFlow - Ultimate Productivity & Task Management Solution"
200
+ ✅ Title: "TaskFlow: Team Tasks & Projects"
201
+
202
+ ❌ Subtitle: "The most powerful way to manage your workflow seamlessly"
203
+ ✅ Subtitle: "Shared tasks with real-time sync"
204
+
205
+ ❌ What's New: "This update introduces significant improvements to the overall
206
+ user experience with enhanced performance and reliability."
207
+ ✅ What's New:
208
+ • Fixed crash on iPhone 14 when swiping between projects
209
+ • Dark mode now loads instantly instead of flashing white
210
+ • Added swipe-to-complete on task cards
211
+ ```
212
+
213
+ ---
214
+
215
+ ## Permission Request Copy
216
+
217
+ ```
218
+ ❌ AI default (iOS):
219
+ "[App] Would Like to Access Your Camera"
220
+ (No context, user taps Don't Allow)
221
+
222
+ ✅ Custom NSCameraUsageDescription:
223
+ "To scan receipts and attach photos to expenses"
224
+
225
+ ❌ "Notifications" permission with no context
226
+
227
+ ✅ Request at the right moment + explain:
228
+ "Get notified when teammates comment on your tasks"
229
+ [Allow] [Not now]
230
+ ```
231
+
232
+ Common permission descriptions:
233
+ | Permission | Bad | Good |
234
+ |------------|-----|------|
235
+ | Camera | "Access camera" | "Scan QR codes to join a workspace" |
236
+ | Location | "Use your location" | "Show nearby team members on the map" |
237
+ | Contacts | "Access contacts" | "Invite teammates by name instead of email" |
238
+ | Notifications | "Send notifications" | "Alert you when your order ships" |
239
+ | Microphone | "Access microphone" | "Record voice notes on tasks" |
240
+
241
+ ---
242
+
243
+ ## Rating Prompt Copy
244
+
245
+ ```
246
+ ❌ AI default:
247
+ "Are you enjoying [App Name]? Rate us 5 stars!"
248
+
249
+ ✅ Human — ask a real question first:
250
+ "Is [App] helping you get things done?"
251
+ [Yes!] [Not really]
252
+
253
+ If Yes → "Mind leaving a review? It helps us a lot."
254
+ [Sure] [Maybe later]
255
+ If No → "What's getting in the way?"
256
+ [Give feedback]
257
+ ```
258
+
259
+ ---
260
+
261
+ ## Subscription & Paywall Copy
262
+
263
+ ```
264
+ ❌ AI:
265
+ "Unlock the full potential of our comprehensive premium features
266
+ to seamlessly enhance your productivity experience."
267
+
268
+ ✅ Human — state what unlocks:
269
+ "Go Pro
270
+ • Unlimited projects (free plan: 3)
271
+ • Team sharing
272
+ • Priority support"
273
+ [Start 7-day free trial]
274
+ [See what's included]
275
+
276
+ ❌ CTA: "Subscribe Now" / "Upgrade Today" / "Get Premium"
277
+ ✅ CTA: "Start free trial" / "Unlock [specific feature]" / "Try Pro free"
278
+
279
+ ❌ After trial ends: "Your trial has expired. Please subscribe to continue."
280
+ ✅ "Your free trial ended. Pick a plan to keep your 12 projects."
281
+ ```
282
+
283
+ ---
284
+
285
+ ## Word Blacklist (delete on sight)
286
+
287
+ ```
288
+ seamlessly / robust / powerful / comprehensive / intuitive / streamline /
289
+ leverage / utilize / cutting-edge / state-of-the-art / innovative /
290
+ revolutionary / game-changing / transformative / holistic / synergy /
291
+ delve / Furthermore / Additionally / Moreover / In conclusion /
292
+ It is worth noting / Please note that / Feel free to /
293
+ experience the difference / take your [X] to the next level /
294
+ designed with you in mind / your one-stop solution
295
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buivietphi/skill-mobile-mt",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "Master Senior Mobile Engineer skill for AI agents. Pre-built patterns from 18 production apps + local project adaptation. React Native, Flutter, iOS, Android. Supports Claude, Gemini, Kimi, Cursor, Copilot, Antigravity.",
5
5
  "author": "buivietphi",
6
6
  "license": "MIT",
@@ -31,7 +31,8 @@
31
31
  "flutter/",
32
32
  "ios/",
33
33
  "android/",
34
- "shared/"
34
+ "shared/",
35
+ "humanizer/"
35
36
  ],
36
37
  "scripts": {
37
38
  "postinstall": "node bin/install.mjs --auto",
@@ -0,0 +1,377 @@
1
+ # Offline-First — Mobile Data Strategy
2
+
3
+ > On-demand. Load when: "offline", "offline-first", "cache", "sync", "local database", "persistence"
4
+ > Source: Mattermost, Immich, Expensify, Ignite
5
+
6
+ ---
7
+
8
+ ## Architecture
9
+
10
+ ```
11
+ ┌─────────────────────────────────────┐
12
+ │ UI Layer (reads local only) │
13
+ └──────────────┬──────────────────────┘
14
+
15
+ ┌──────────────▼──────────────────────┐
16
+ │ Repository Layer (sync logic) │
17
+ └──────┬──────────────────────┬───────┘
18
+ │ │
19
+ ┌──────▼──────┐ ┌─────────▼───────┐
20
+ │ Local DB │◄───►│ Remote API │
21
+ │ (primary) │ │ (sync only) │
22
+ └─────────────┘ └─────────────────┘
23
+ ```
24
+
25
+ **Rule:** UI always reads local. API is sync-only, never primary.
26
+
27
+ ---
28
+
29
+ ## React Native — WatermelonDB (Reference Implementation)
30
+
31
+ ### Schema
32
+
33
+ ```typescript
34
+ // db/schema.ts
35
+ export const schema = appSchema({
36
+ version: 1,
37
+ tables: [
38
+ tableSchema({
39
+ name: 'posts',
40
+ columns: [
41
+ { name: 'title', type: 'string' },
42
+ { name: 'body', type: 'string' },
43
+ { name: 'author_id', type: 'string' },
44
+ { name: 'created_at', type: 'number' },
45
+ { name: 'updated_at', type: 'number' },
46
+ { name: 'is_synced', type: 'boolean' },
47
+ { name: 'is_deleted', type: 'boolean' }, // soft delete required for sync
48
+ ],
49
+ }),
50
+ ],
51
+ });
52
+ ```
53
+
54
+ ### Model
55
+
56
+ ```typescript
57
+ export default class Post extends Model {
58
+ static table = 'posts';
59
+ static associations = {
60
+ comments: { type: 'has_many', foreignKey: 'post_id' },
61
+ };
62
+
63
+ @field('title') title!: string;
64
+ @field('body') body!: string;
65
+ @field('author_id') authorId!: string;
66
+ @readonly @date('created_at') createdAt!: Date;
67
+ @field('updated_at') updatedAt!: number;
68
+ @field('is_synced') isSynced!: boolean;
69
+ @field('is_deleted') isDeleted!: boolean;
70
+
71
+ async markAsDeleted() {
72
+ await this.update(post => {
73
+ post.isDeleted = true;
74
+ post.isSynced = false;
75
+ });
76
+ }
77
+ }
78
+ ```
79
+
80
+ ### Sync Engine
81
+
82
+ ```typescript
83
+ export async function syncDatabase() {
84
+ await synchronize({
85
+ database,
86
+ pullChanges: async ({ lastPulledAt }) => {
87
+ const { changes, timestamp } = await api.get('/sync', {
88
+ params: { last_pulled_at: lastPulledAt },
89
+ });
90
+ return { changes, timestamp };
91
+ },
92
+ pushChanges: async ({ changes, lastPulledAt }) => {
93
+ await api.post('/sync', { changes, last_pulled_at: lastPulledAt });
94
+ },
95
+ migrationsEnabledAtVersion: 1,
96
+ });
97
+ }
98
+
99
+ export function startBackgroundSync() {
100
+ const interval = setInterval(syncDatabase, 30000);
101
+ AppState.addEventListener('change', state => {
102
+ if (state === 'active') syncDatabase();
103
+ });
104
+ return () => clearInterval(interval);
105
+ }
106
+ ```
107
+
108
+ ### Repository
109
+
110
+ ```typescript
111
+ export class PostRepository {
112
+ private posts = database.get<Post>('posts');
113
+
114
+ getAll() {
115
+ return this.posts.query(
116
+ Q.where('is_deleted', false),
117
+ Q.sortBy('created_at', Q.desc),
118
+ ).fetch();
119
+ }
120
+
121
+ observeAll() {
122
+ return this.posts.query(
123
+ Q.where('is_deleted', false),
124
+ Q.sortBy('created_at', Q.desc),
125
+ ).observe();
126
+ }
127
+
128
+ async create(data: { title: string; body: string; authorId: string }) {
129
+ return database.write(async () => {
130
+ return this.posts.create(post => {
131
+ Object.assign(post, data);
132
+ post.isSynced = false;
133
+ post.isDeleted = false;
134
+ post.updatedAt = Date.now();
135
+ });
136
+ });
137
+ }
138
+
139
+ async update(post: Post, data: Partial<{ title: string; body: string }>) {
140
+ return database.write(async () =>
141
+ post.update(p => { Object.assign(p, data); p.isSynced = false; p.updatedAt = Date.now(); })
142
+ );
143
+ }
144
+
145
+ async delete(post: Post) {
146
+ return database.write(async () => post.markAsDeleted());
147
+ }
148
+ }
149
+ ```
150
+
151
+ ---
152
+
153
+ ## Flutter — Drift + Riverpod
154
+
155
+ ### Key differences from RN
156
+
157
+ ```dart
158
+ // Table definition
159
+ class Posts extends Table {
160
+ TextColumn get id => text()();
161
+ TextColumn get title => text()();
162
+ BoolColumn get isSynced => boolean().withDefault(const Constant(false))();
163
+ BoolColumn get isDeleted => boolean().withDefault(const Constant(false))();
164
+ @override Set<Column> get primaryKey => {id};
165
+ }
166
+
167
+ // Watch (reactive, like observeAll)
168
+ Stream<List<Post>> watchAllPosts() => (select(posts)
169
+ ..where((p) => p.isDeleted.equals(false))
170
+ ..orderBy([(p) => Ordering.desc(p.createdAt)]))
171
+ .watch();
172
+
173
+ // Soft delete
174
+ Future<void> softDeletePost(String id) => (update(posts)).write(
175
+ PostsCompanion(id: Value(id), isDeleted: const Value(true), isSynced: const Value(false)),
176
+ );
177
+ ```
178
+
179
+ ```dart
180
+ // Sync: check connectivity first
181
+ Future<void> sync() async {
182
+ final result = await Connectivity().checkConnectivity();
183
+ if (result == ConnectivityResult.none) return;
184
+ await _pushChanges();
185
+ await _pullChanges();
186
+ }
187
+
188
+ // Background sync on connectivity restore
189
+ Connectivity().onConnectivityChanged.listen((result) {
190
+ if (result != ConnectivityResult.none) sync();
191
+ });
192
+ ```
193
+
194
+ ---
195
+
196
+ ## iOS — SwiftData (iOS 17+) / Core Data
197
+
198
+ ```swift
199
+ @Model final class Post {
200
+ @Attribute(.unique) var id: String
201
+ var title: String
202
+ var isSynced: Bool = false
203
+ var isDeleted: Bool = false
204
+ var updatedAt: Date?
205
+ }
206
+
207
+ // Sync with NWPathMonitor
208
+ private func startNetworkMonitoring() {
209
+ monitor.pathUpdateHandler = { [weak self] path in
210
+ Task { @MainActor in
211
+ if path.status == .satisfied { await self?.sync() }
212
+ }
213
+ }
214
+ monitor.start(queue: queue)
215
+ }
216
+
217
+ func sync() async {
218
+ guard isConnected else { return }
219
+ let unsynced = try await repository.fetchUnsynced()
220
+ for post in unsynced {
221
+ post.isDeleted
222
+ ? try await apiClient.deletePost(post.id)
223
+ : try await apiClient.upsertPost(post)
224
+ post.isSynced = true
225
+ }
226
+ let remote = try await apiClient.fetchPosts()
227
+ for post in remote { try await repository.upsert(post) }
228
+ }
229
+ ```
230
+
231
+ ---
232
+
233
+ ## Android — Room + WorkManager
234
+
235
+ ```kotlin
236
+ @Entity(tableName = "posts")
237
+ data class PostEntity(
238
+ @PrimaryKey val id: String,
239
+ val title: String,
240
+ val isSynced: Boolean = false,
241
+ val isDeleted: Boolean = false,
242
+ val updatedAt: Long? = null,
243
+ )
244
+
245
+ @Dao interface PostDao {
246
+ @Query("SELECT * FROM posts WHERE isDeleted = 0 ORDER BY createdAt DESC")
247
+ fun observeAll(): Flow<List<PostEntity>>
248
+
249
+ @Query("SELECT * FROM posts WHERE isSynced = 0")
250
+ suspend fun getUnsynced(): List<PostEntity>
251
+
252
+ @Query("UPDATE posts SET isDeleted = 1, isSynced = 0 WHERE id = :id")
253
+ suspend fun softDelete(id: String)
254
+ }
255
+
256
+ // Use WorkManager for background sync (respects battery/network constraints)
257
+ fun triggerSync() {
258
+ val request = OneTimeWorkRequestBuilder<SyncWorker>()
259
+ .setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
260
+ .build()
261
+ WorkManager.getInstance(context).enqueue(request)
262
+ }
263
+ ```
264
+
265
+ ---
266
+
267
+ ## Conflict Resolution
268
+
269
+ ```typescript
270
+ // 1. Last Write Wins (simple, most apps)
271
+ const resolve = (local: Post, remote: Post) =>
272
+ local.updatedAt > remote.updatedAt ? local : remote;
273
+
274
+ // 2. Field-Level Merge (collaborative editing)
275
+ const merge = (local: Post, remote: Post, base: Post) => ({
276
+ ...base,
277
+ title: local.title !== base.title ? local.title : remote.title,
278
+ body: local.body !== base.body ? local.body : remote.body,
279
+ updatedAt: Date.now(),
280
+ });
281
+
282
+ // 3. Operational Transform (counters/accumulators)
283
+ const resolveCounter = (local: Counter, remote: Counter, base: Counter) => ({
284
+ ...remote,
285
+ value: base.value + (local.value - base.value) + (remote.value - base.value),
286
+ });
287
+ ```
288
+
289
+ ---
290
+
291
+ ## UI Components
292
+
293
+ ```typescript
294
+ // Offline banner
295
+ export function OfflineBanner() {
296
+ const [isOffline, setIsOffline] = useState(false);
297
+ useEffect(() => NetInfo.addEventListener(s => setIsOffline(!s.isConnected)), []);
298
+ if (!isOffline) return null;
299
+ return <Banner message="Offline — changes sync when connected." />;
300
+ }
301
+
302
+ // Sync status
303
+ type SyncStatus = 'synced' | 'pending' | 'syncing' | 'error';
304
+ const STATUS_CONFIG = {
305
+ synced: { icon: 'check-circle', color: 'green' },
306
+ pending: { icon: 'clock', color: 'orange' },
307
+ syncing: { icon: 'sync', color: 'blue' },
308
+ error: { icon: 'alert-circle', color: 'red' },
309
+ };
310
+
311
+ // Optimistic update with rollback
312
+ const updateOptimistic = async (id: string, updates: Partial<Post>) => {
313
+ const prev = queryClient.getQueryData(['posts', id]);
314
+ queryClient.setQueryData(['posts', id], old => ({ ...old, ...updates }));
315
+ try {
316
+ await repository.update(id, updates);
317
+ await syncDatabase();
318
+ } catch {
319
+ queryClient.setQueryData(['posts', id], prev);
320
+ }
321
+ };
322
+ ```
323
+
324
+ ---
325
+
326
+ ## Database Selection
327
+
328
+ | DB | Platform | Use when |
329
+ |----|----------|----------|
330
+ | **WatermelonDB** | React Native | Complex queries, observables, built-in sync |
331
+ | **MMKV** | React Native | Key-value only, speed critical |
332
+ | **Realm** | RN / Flutter / iOS | Cross-platform, reactive |
333
+ | **SQLite** | All | Full SQL control |
334
+ | **Drift** | Flutter | Type-safe, migrations, code gen |
335
+ | **SwiftData** | iOS 17+ | Simple models, native |
336
+ | **Core Data** | iOS | Complex relationships, migrations |
337
+ | **Room** | Android | Flow/LiveData integration |
338
+
339
+ ---
340
+
341
+ ## Checklist
342
+
343
+ ```
344
+ Data:
345
+ □ All data written to local DB first
346
+ □ Soft deletes (never hard delete)
347
+ □ isSynced flag per record
348
+ □ Conflict resolution strategy defined
349
+ □ Retry for failed syncs
350
+
351
+ UI:
352
+ □ Offline banner
353
+ □ Sync status indicator
354
+ □ Optimistic updates
355
+ □ Pull-to-refresh triggers sync
356
+
357
+ Sync:
358
+ □ Sync on app foreground
359
+ □ Sync on connectivity restore
360
+ □ Background sync (30s interval)
361
+ □ Sync doesn't block UI thread
362
+ ```
363
+
364
+ ---
365
+
366
+ ## Anti-Patterns
367
+
368
+ ```
369
+ ❌ UI reads directly from API
370
+ ❌ Blocking UI during sync
371
+ ❌ Hard deletes (breaks sync)
372
+ ❌ Syncing on every keystroke
373
+ ❌ No retry for failed syncs
374
+ ❌ No offline indicator
375
+ ❌ Assuming network is available
376
+ ❌ Losing data on conflict
377
+ ```
@@ -144,6 +144,86 @@ export const LoginScreen = Platform.select({
144
144
 
145
145
  ---
146
146
 
147
+ ## iOS Haptics
148
+
149
+ ```swift
150
+ // 3 feedback types — use the right one
151
+ UIImpactFeedbackGenerator(style: .medium).impactOccurred() // button tap, card flip
152
+ UINotificationFeedbackGenerator().notificationOccurred(.success) // save success / error / warning
153
+ UISelectionFeedbackGenerator().selectionChanged() // picker scroll, toggle
154
+
155
+ // ✅ Rules
156
+ // - Impact: physical interactions (drag drop, button press)
157
+ // - Notification: outcomes (success, error, warning) — max 1 per action
158
+ // - Selection: discrete value changes (picker, slider step)
159
+ // ⛔ Never chain multiple haptics in <300ms
160
+ // ⛔ Never use for routine navigation (back, tab switch)
161
+ ```
162
+
163
+ ## Permission Timing (iOS/Android)
164
+
165
+ ```
166
+ RULE: Ask ONLY when the feature needs it — not at launch
167
+
168
+ Permission When to ask
169
+ ─────────────────────────────────────────────────────
170
+ Camera User taps "Take Photo" button
171
+ Location User taps "Find Nearby" or map feature
172
+ Contacts User taps "Invite from Contacts"
173
+ Notifications After onboarding, show a pre-permission dialog first
174
+ Microphone User taps "Record Voice Note"
175
+
176
+ PRE-PERMISSION DIALOG (iOS — before system prompt):
177
+ "Get notified when teammates reply"
178
+ [Allow] [Not now]
179
+ → Only show system prompt if user taps Allow
180
+ → Saves 1 chance at permission — don't waste it at cold start
181
+ ```
182
+
183
+ ## Ratings Timing
184
+
185
+ ```
186
+ // 2-step flow — ask only after success
187
+ Step 1: "Is [App] helping you get things done?"
188
+ [Yes!] [Not really]
189
+
190
+ Step 2 (if Yes): "Mind leaving a review? It helps us a lot."
191
+ [Sure] [Maybe later]
192
+ Step 2 (if No): "What's getting in the way?" [Give feedback]
193
+
194
+ // iOS: Use SKStoreReviewController.requestReview() — max 3x/year
195
+ // Android: Use ReviewManager from Play Core library
196
+ // NEVER ask after an error, payment, or on app cold start
197
+ ```
198
+
199
+ ## Live Activities / Dynamic Island (iOS 16.1+)
200
+
201
+ ```swift
202
+ // 1. Define attributes
203
+ struct DeliveryAttributes: ActivityAttributes {
204
+ struct ContentState: Codable, Hashable {
205
+ var status: String
206
+ var eta: Date
207
+ }
208
+ var orderId: String
209
+ }
210
+
211
+ // 2. Start activity
212
+ let initialState = DeliveryAttributes.ContentState(status: "Preparing", eta: Date())
213
+ let activity = try? Activity.request(
214
+ attributes: DeliveryAttributes(orderId: "123"),
215
+ content: .init(state: initialState, staleDate: nil)
216
+ )
217
+
218
+ // 3. Update
219
+ await activity?.update(.init(state: .init(status: "Out for delivery", eta: Date()), staleDate: nil))
220
+
221
+ // 4. End
222
+ await activity?.end(dismissalPolicy: .default)
223
+ ```
224
+
225
+ ---
226
+
147
227
  ## Anti-Patterns
148
228
 
149
229
  ```
@@ -151,9 +231,14 @@ export const LoginScreen = Platform.select({
151
231
  ❌ iOS navigation on Android
152
232
  ❌ Ignoring platform conventions
153
233
  ❌ "Write once, look mediocre everywhere"
234
+ ❌ Asking permissions at app launch
235
+ ❌ Chaining multiple haptics back-to-back
236
+ ❌ Rating prompt right after install
154
237
 
155
238
  ✅ Native look & feel per platform
156
239
  ✅ Shared logic, platform UI
157
240
  ✅ Respect platform guidelines
158
241
  ✅ "Write once, look native everywhere"
242
+ ✅ Ask permissions at the moment they're needed
243
+ ✅ Rating prompts only after clear success moments
159
244
  ```