@sayknow-cli/coding-agent 0.5.13 → 0.5.15
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/CHANGELOG.md +21 -0
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/defaults/skc-ui-skills.d.ts +12 -0
- package/dist/types/hooks/native-skill-hook.d.ts +1 -0
- package/dist/types/hooks/ui-skill-keywords.d.ts +84 -0
- package/dist/types/session-import/redact.d.ts +1 -1
- package/dist/types/setup/external-ui-skills.d.ts +37 -0
- package/package.json +7 -7
- package/src/cli/setup-cli.ts +22 -1
- package/src/cli/skills-cli.ts +29 -10
- package/src/commands/setup.ts +1 -0
- package/src/defaults/skc/ui-skills/LICENSE.appllama +21 -0
- package/src/defaults/skc/ui-skills/LICENSE.emilkowalski +21 -0
- package/src/defaults/skc/ui-skills/NOTICE.md +43 -0
- package/src/defaults/skc/ui-skills/animate/SKILL.md +536 -0
- package/src/defaults/skc/ui-skills/animation-vocabulary/SKILL.md +178 -0
- package/src/defaults/skc/ui-skills/apple-design/SKILL.md +285 -0
- package/src/defaults/skc/ui-skills/appllama-app-design-skill/SKILL.md +821 -0
- package/src/defaults/skc/ui-skills/ask-sonner/SKILL.md +157 -0
- package/src/defaults/skc/ui-skills/emil-design-eng/SKILL.md +671 -0
- package/src/defaults/skc/ui-skills/find-animation-opportunities/SKILL.md +137 -0
- package/src/defaults/skc/ui-skills/improve-animations/SKILL.md +305 -0
- package/src/defaults/skc/ui-skills/mobile-native/SKILL.md +308 -0
- package/src/defaults/skc/ui-skills/pick-ui-library/SKILL.md +82 -0
- package/src/defaults/skc/ui-skills/prototype/SKILL.md +300 -0
- package/src/defaults/skc/ui-skills/react-bits/SKILL.md +113 -0
- package/src/defaults/skc/ui-skills/review-animations/SKILL.md +312 -0
- package/src/defaults/skc-ui-skills.ts +108 -0
- package/src/extensibility/runtime-skill-discovery.ts +8 -2
- package/src/hooks/native-skill-hook.ts +21 -0
- package/src/hooks/ui-skill-keywords.ts +316 -0
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/prompts/agents/architect.md +1 -0
- package/src/prompts/agents/critic.md +1 -0
- package/src/prompts/agents/executor.md +1 -0
- package/src/prompts/agents/planner.md +1 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/skill.md +2 -2
- package/src/sdk/session.ts +7 -5
- package/src/session-import/redact.ts +11 -6
- package/src/setup/external-ui-skills.ts +132 -0
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: appllama-app-design-skill
|
|
3
|
+
description: Build native-feeling, benchmark-quality mobile app screens (Expo / React Native). Use when designing or implementing any mobile UI — screens, flows, onboarding, paywalls, tab bars, sheets, settings, empty states — or when polishing motion, navigation, typography, dark mode, or perceived performance. Enforces Apple HIG fidelity, semantic colors, native controls, anti-slop discipline, navigation semantics (push vs replace, modal vs sheet vs overlay, the one-way doors where back must not exist), purposeful Reanimated motion, a full-motion simulator-verified iteration loop, and a study-real-apps-first workflow (pairs with the Appllama MCP). Trigger on "build a screen", "make this screen better", "design the onboarding", "wire up this flow", "polish the UI", "make it feel native", or any mobile design/implementation task.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: Appllama (appllama.io)
|
|
7
|
+
version: 1.3.0
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Appllama App Design Skill
|
|
11
|
+
|
|
12
|
+
## SKC invocation
|
|
13
|
+
|
|
14
|
+
SKC loads this skill automatically for matching frontend UI/UX work.
|
|
15
|
+
Do not wait for a follow-up question. Apply the craft rules immediately
|
|
16
|
+
and keep going on the user's actual task.
|
|
17
|
+
|
|
18
|
+
This skill's companion reference files are appended inline at the end of
|
|
19
|
+
this document. Read them there; they are not separate files on disk.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
You are building screens that will sit on a phone next to the best-designed apps
|
|
23
|
+
in the world. The user will compare your output to those apps within seconds of
|
|
24
|
+
launching it. This skill defines the bar and the method for clearing it.
|
|
25
|
+
|
|
26
|
+
## The Prime Directive: study before you draw
|
|
27
|
+
|
|
28
|
+
Never design a screen from imagination when you can study how top apps solved
|
|
29
|
+
the same screen. Real, shipping, revenue-ranked apps encode thousands of hours
|
|
30
|
+
of design iteration and A/B testing. Your first move on any screen is research:
|
|
31
|
+
|
|
32
|
+
1. If the **Appllama MCP** is connected, pull real screens for the category and
|
|
33
|
+
screen type you are building (see the `appllama-usage` skill for the exact
|
|
34
|
+
research playbooks). Study 20–30 screens before writing a line of UI code.
|
|
35
|
+
2. Extract the **pattern, not the pixels**: layout skeleton, information
|
|
36
|
+
hierarchy, control choices, spacing rhythm, where the primary CTA sits, what
|
|
37
|
+
gets an illustration vs. plain text, how progress is communicated.
|
|
38
|
+
Note: every Appllama image and video carries a small Appllama watermark in
|
|
39
|
+
the top-left corner. It is provenance, not design — ignore it when reading
|
|
40
|
+
a screen (it may sit over the status bar or a back button) and never
|
|
41
|
+
reproduce it in anything you build.
|
|
42
|
+
3. Then design **your** screen: same proven skeleton, your product's voice.
|
|
43
|
+
Copying a competitor's screen 1:1 is both lazy and legally risky; shipping a
|
|
44
|
+
screen that ignores every convention users already know is worse.
|
|
45
|
+
|
|
46
|
+
## Platform baseline
|
|
47
|
+
|
|
48
|
+
Default stack assumptions (override only if the project already differs):
|
|
49
|
+
|
|
50
|
+
- **Expo + Expo Router**, React Native, TypeScript.
|
|
51
|
+
- `react-native-reanimated` for motion, `react-native-gesture-handler` for
|
|
52
|
+
gestures, `@shopify/flash-list` (or FlashList v2) for any list that can grow.
|
|
53
|
+
- `expo-image` for images (and SF Symbols via `source="sf:name"` on iOS),
|
|
54
|
+
`expo-video` / `expo-audio` (never the deprecated `expo-av`).
|
|
55
|
+
- `react-native-safe-area-context` for insets. Never hard-code notch numbers.
|
|
56
|
+
- `process.env.EXPO_OS` over `Platform.OS` for compile-time platform checks.
|
|
57
|
+
|
|
58
|
+
## Native fidelity laws
|
|
59
|
+
|
|
60
|
+
These are the details that separate "web page in a wrapper" from "native app".
|
|
61
|
+
Violating any of them is a finding, not a style preference.
|
|
62
|
+
|
|
63
|
+
1. **Semantic colors, both themes, day one.** Use system/semantic color tokens
|
|
64
|
+
(e.g. `Color` from `expo-router` on iOS: `Color.ios.label`,
|
|
65
|
+
`Color.ios.secondarySystemBackground`; Material dynamic colors on Android).
|
|
66
|
+
Every screen must render correctly in light AND dark before it is "done".
|
|
67
|
+
Never pass semantic color objects into Reanimated animated styles — resolve
|
|
68
|
+
to strings first.
|
|
69
|
+
2. **Native controls over rebuilt ones.** Switch, Slider, SegmentedControl,
|
|
70
|
+
context menus, date pickers: use the native control or a faithful wrapper.
|
|
71
|
+
A rebuilt toggle that animates 50 ms differently than iOS's reads as fake
|
|
72
|
+
instantly.
|
|
73
|
+
3. **SF Symbols / Material Symbols for iconography.** On iOS prefer SF Symbols
|
|
74
|
+
(`expo-image` with `sf:` sources, or `expo-symbols`); they inherit weight,
|
|
75
|
+
optical size, and Dynamic Type behavior. Do not mix three icon families on
|
|
76
|
+
one screen.
|
|
77
|
+
4. **Typography is hierarchy.** Use the platform type ramp (Large Title / Title
|
|
78
|
+
/ Headline / Body / Footnote on iOS). One display size per screen. Tabular
|
|
79
|
+
numerals (`fontVariant: ['tabular-nums']`) for anything that counts, times,
|
|
80
|
+
or prices. `Text selectable` on data users may want to copy.
|
|
81
|
+
5. **Continuous corners.** `borderCurve: 'continuous'` on every rounded
|
|
82
|
+
rectangle. Squircles are the single cheapest "feels iOS" win that exists.
|
|
83
|
+
6. **Shadows via CSS `boxShadow`**, not legacy `shadow*`/`elevation` props.
|
|
84
|
+
Shadows are for elevation logic, not decoration — one elevation system per
|
|
85
|
+
app.
|
|
86
|
+
7. **Spacing rhythm.** Pick a base unit (4 or 8) and never leave it. Prefer
|
|
87
|
+
flexbox `gap` over margin stacking. ScrollView padding goes in
|
|
88
|
+
`contentContainerStyle`, never on the ScrollView itself.
|
|
89
|
+
8. **Safe areas and the Dynamic Island are part of the design.** Screens must
|
|
90
|
+
be verified with content scrolled under the island / status bar (does the
|
|
91
|
+
blur/fade treatment hold?), with the home indicator (does the bottom CTA
|
|
92
|
+
clear it?), and in landscape if supported.
|
|
93
|
+
9. **Navigation titles belong to the navigator.** Use the stack's native title
|
|
94
|
+
(and large-title collapse behavior on iOS) rather than a hand-rolled header
|
|
95
|
+
whenever possible.
|
|
96
|
+
10. **Haptics are punctuation.** Selection tick when a value passes a step,
|
|
97
|
+
light impact when something snaps home, notification success/error for
|
|
98
|
+
outcomes — on the same frame as the visual, one per user action, never
|
|
99
|
+
the only feedback. Never on scroll, never in loops.
|
|
100
|
+
11. **Format numbers like a product, not a database**: 1.4M, 38k, $4.99. Trim
|
|
101
|
+
trailing zeros. Localize dates.
|
|
102
|
+
12. **Root scroll behavior**: screens that can ever overflow wrap content in a
|
|
103
|
+
ScrollView (first component in the route) with
|
|
104
|
+
`contentInsetAdjustmentBehavior="automatic"`. Use `useWindowDimensions`,
|
|
105
|
+
never `Dimensions.get()`.
|
|
106
|
+
|
|
107
|
+
## Navigation laws
|
|
108
|
+
|
|
109
|
+
Navigation is the part of a screen a screenshot can't show, and users feel
|
|
110
|
+
it in ten seconds. Every transition answers three questions: what is the
|
|
111
|
+
destination to here, must the user be able to come back, and what does back
|
|
112
|
+
(chevron, iOS edge swipe, Android hardware back) do afterwards.
|
|
113
|
+
|
|
114
|
+
1. **Push goes deeper, replace moves on.** `router.push` when the user will
|
|
115
|
+
want to return here; `router.replace` / `<Redirect>` when coming back
|
|
116
|
+
would land in a state the world has moved past; `router.dismissTo(href)`
|
|
117
|
+
for "finish this flow and land on X". Back undoes *navigation*, never
|
|
118
|
+
*events*.
|
|
119
|
+
2. **Presentation is meaning.** A self-contained task with steps →
|
|
120
|
+
`presentation: 'modal'` with its own stack and its own Cancel/Done; a
|
|
121
|
+
short interruption (picker, filters, item options) → `formSheet` with
|
|
122
|
+
detents, drag-to-dismiss; immersive content → `fullScreenModal` with an
|
|
123
|
+
explicit Close; something floating over a still-visible screen (confirm
|
|
124
|
+
card, lightbox, coach mark) → `transparentModal` overlay; destructive
|
|
125
|
+
confirms → action sheet; item actions → native context menu; share /
|
|
126
|
+
web / photo picking → the system controller, never a rebuilt route. A
|
|
127
|
+
sheet that grows a second step was a modal all along; if a link could
|
|
128
|
+
open it, it is a route, not a `useState` sheet.
|
|
129
|
+
3. **One-way doors leave the stack.** Sign-in on a wall app, finished
|
|
130
|
+
onboarding (Skip included), a purchase, a completed session: guard with
|
|
131
|
+
`Stack.Protected` and land with `replace`, so back can never re-enter
|
|
132
|
+
the old state — Android back from home exits the app, never shows
|
|
133
|
+
Login; a paid paywall never re-opens. But keep the user's *place*:
|
|
134
|
+
sign-in demanded by one action (save, follow, buy) is a modal over the
|
|
135
|
+
screen that completes the action where it was tapped, and a paywall
|
|
136
|
+
opened from a feature dismisses back onto the feature, unlocked — never
|
|
137
|
+
`replace('/(tabs)')` from there.
|
|
138
|
+
4. **Back is blocked in exactly two cases** — an irreversible request in
|
|
139
|
+
flight (seconds, with visible progress) and unsaved work in a modal
|
|
140
|
+
(ask first), both via `usePreventRemove` on the modal's root screen.
|
|
141
|
+
Transient in-screen state (selection mode, an expanded search, an open
|
|
142
|
+
in-screen sheet) consumes the first back, then back leaves. Anything
|
|
143
|
+
else that traps back — a funnel, a rating prompt — is a defect; the
|
|
144
|
+
edge swipe works everywhere else.
|
|
145
|
+
5. **Tabs are peers.** No slide between tabs, each tab keeps its own stack,
|
|
146
|
+
re-tapping the active tab pops to its root; full-attention screens
|
|
147
|
+
(composer, player, checkout) live in the root stack *above* the tabs.
|
|
148
|
+
Deep links land with a real stack underneath (`initialRouteName` /
|
|
149
|
+
`withAnchor`); cold start lands by state, splash held until session
|
|
150
|
+
state has resolved — never a Login flash before Home.
|
|
151
|
+
6. **Study the grammar, not just the pixels.** Walking a winning flow on
|
|
152
|
+
Appllama, note what each step *is* — push, modal, sheet — and copy that
|
|
153
|
+
consistency.
|
|
154
|
+
|
|
155
|
+
## Anti-slop laws
|
|
156
|
+
|
|
157
|
+
AI-built apps share a look, and users file it under "template" within seconds.
|
|
158
|
+
Each of these is a *default ban* — there is always an override when the brand
|
|
159
|
+
explicitly asks for the thing AND you can articulate why it fits this product.
|
|
160
|
+
|
|
161
|
+
1. **No AI-default styling.** Purple/indigo gradient CTAs with a glow,
|
|
162
|
+
glassmorphism on every card, mesh-gradient heroes, confetti for minor
|
|
163
|
+
events, sparkles in headings — that is the model's house style, not
|
|
164
|
+
design. Your palette, materials, and layout come from the reference
|
|
165
|
+
screens you studied, never from the priors you'd reach for unprompted.
|
|
166
|
+
2. **One accent, locked.** Pick one accent color and it is THE accent on
|
|
167
|
+
every screen — no blue CTA on one screen and teal on the next, no new hue
|
|
168
|
+
appearing in screen seven. Neutrals carry the app; the accent is spent
|
|
169
|
+
where the money is (primary action, active state, progress).
|
|
170
|
+
3. **One grey family.** Warm greys or cool greys — never both in one app.
|
|
171
|
+
4. **Shape lock.** One corner-radius scale, stated as a rule ("actions are
|
|
172
|
+
pills, cards 16, inputs 8") and never violated. Mixed radii without a
|
|
173
|
+
stated rule read as assembled-from-parts.
|
|
174
|
+
5. **No emoji as iconography.** Icons are SF Symbols / Material Symbols
|
|
175
|
+
(fidelity law 3). Emoji appear only when the product's voice is genuinely
|
|
176
|
+
chat-native or playful — sparingly, in content, never in chrome.
|
|
177
|
+
6. **One label per intent.** "Get started", "Start now", and "Begin" are the
|
|
178
|
+
same intent — pick one phrasing and use it everywhere it appears.
|
|
179
|
+
7. **Emphasis stays in the family.** Emphasize a word with weight or italic
|
|
180
|
+
of the same typeface; injecting a serif word into a sans headline (or vice
|
|
181
|
+
versa) for visual interest is amateur.
|
|
182
|
+
8. **Ship full state cycles, not the happy path.** Static-successful-state-
|
|
183
|
+
only is the default failure mode: skeletons must match the final layout's
|
|
184
|
+
shape, empty states are composed (and say how to fill them), errors are
|
|
185
|
+
inline and specific.
|
|
186
|
+
9. **The slop pre-flight is mechanical.** Before any flow reaches the
|
|
187
|
+
simulator pass, count: distinct accent hues (must be 1), distinct corner
|
|
188
|
+
radii (all from the stated scale), emoji in UI chrome (0), gradients
|
|
189
|
+
without a brand reason (0), duplicate labels for one intent (0). A failed
|
|
190
|
+
count is a fix, not a judgment call.
|
|
191
|
+
|
|
192
|
+
## Motion laws
|
|
193
|
+
|
|
194
|
+
Motion is the highest-leverage polish surface and the easiest to overdo.
|
|
195
|
+
Decide in this order:
|
|
196
|
+
|
|
197
|
+
- **The frequency gate comes first.** Met 100+ times a day (tab switch,
|
|
198
|
+
keyboard, scroll, back) → the platform default and nothing else; tens a
|
|
199
|
+
day (press, row select) → near-imperceptible, under 150 ms; occasional
|
|
200
|
+
(sheets, modals, toasts) → standard motion; delight only on rare,
|
|
201
|
+
first-time moments. Tabs never slide; screen transitions stay native.
|
|
202
|
+
Passing this gate with zero lines of code is a success — when unsure,
|
|
203
|
+
the strongest move is to delete the animation.
|
|
204
|
+
- **Name the purpose in one word** — feedback, spatial continuity, state
|
|
205
|
+
change, preventing a jarring cut, explanation, delight — or don't build
|
|
206
|
+
it. Data the user is reading never moves for style.
|
|
207
|
+
- **If a finger was involved, it's a spring.** Start from the live value
|
|
208
|
+
(capture it on grab), hand the release velocity into the spring, pick
|
|
209
|
+
the target from projected momentum so a flick commits, rubber-band past
|
|
210
|
+
boundaries, stay grabbable mid-flight. One vocabulary per app —
|
|
211
|
+
`{ duration: 400, dampingRatio: 1 }` to settle, `{ 300, 0.8 }` for
|
|
212
|
+
sheets — and bounce only when the gesture carried momentum.
|
|
213
|
+
- **Everything else is timing, under 300 ms, strong ease-out**
|
|
214
|
+
(`Easing.bezier(0.23, 1, 0.32, 1)` — built-in curves are too weak; never
|
|
215
|
+
ease-in on an entrance). Press feedback lands on press-*in*, 100–150 ms:
|
|
216
|
+
scale 0.97 on buttons and cards, a background highlight (never scale) on
|
|
217
|
+
list rows, opacity on bar buttons. Exits are faster than entrances and
|
|
218
|
+
leave the way they came in; enter from `scale(0.95)` + fade, never
|
|
219
|
+
`scale(0)`; menus grow from their trigger (centered modals exempt).
|
|
220
|
+
- **Gesture → animation never hops the JS thread.** Worklets + shared
|
|
221
|
+
values (`.get()`/`.set()`; `scheduleOnRN` — Reanimated 4's `runOnJS` —
|
|
222
|
+
only at gesture end), `transform`/`opacity` only, no `entering` on
|
|
223
|
+
recycled list rows, never animate a header's height (translate inside a
|
|
224
|
+
fixed clip), keyboard-tracking UI via `react-native-keyboard-controller`
|
|
225
|
+
— never a keyboard listener plus a guessed duration.
|
|
226
|
+
- **Respect Reduce Motion**: your spatial motion collapses to cross-fades;
|
|
227
|
+
native transitions stay the system's.
|
|
228
|
+
- The bar: 60 fps through the hero flow, measured on a **release build on
|
|
229
|
+
the slowest device you support** — Expo Go and dev builds hide exactly
|
|
230
|
+
the jank you're hunting
|
|
231
|
+
([references/performance.md](#appendix-performance-md)). Watch the
|
|
232
|
+
recording once for feel, once frame by frame, and again next day with
|
|
233
|
+
fresh eyes.
|
|
234
|
+
|
|
235
|
+
## State architecture
|
|
236
|
+
|
|
237
|
+
Screens that feel great are screens whose state is boring:
|
|
238
|
+
|
|
239
|
+
- **Server state** in TanStack Query (or the project's equivalent): caching,
|
|
240
|
+
retries, optimistic updates. Never `useEffect`+`fetch`.
|
|
241
|
+
- **Client state** in a small atomic store (Zustand/Jotai). Broad "app state"
|
|
242
|
+
contexts cause the re-render cascades that make UIs feel heavy.
|
|
243
|
+
- **Ephemeral UI state** (open/closed, focus, scroll) stays local to the
|
|
244
|
+
component.
|
|
245
|
+
- **Optimistic by default**: taps reflect instantly, reconcile in the
|
|
246
|
+
background, roll back loudly on failure.
|
|
247
|
+
- Uncontrolled `TextInput`s for high-frequency typing surfaces; controlled
|
|
248
|
+
inputs are a top-3 cause of typing jank.
|
|
249
|
+
- Persist tiny client state in MMKV, not AsyncStorage, when latency shows.
|
|
250
|
+
|
|
251
|
+
## Perceived performance
|
|
252
|
+
|
|
253
|
+
- Skeletons only for content whose shape you know; otherwise progressive
|
|
254
|
+
reveal. Never a full-screen spinner for a partial update.
|
|
255
|
+
- FlashList for every list; give stable keys.
|
|
256
|
+
- Preload the next screen's data on press-in, not on navigation-complete.
|
|
257
|
+
- Images: right-size sources, `expo-image` with `recyclingKey` in lists,
|
|
258
|
+
thumbhash/blurhash placeholders.
|
|
259
|
+
- Cold-start TTI and bundle discipline live in
|
|
260
|
+
[references/performance.md](#appendix-performance-md) — apply the
|
|
261
|
+
measure → optimize → re-measure loop, never blind memoization.
|
|
262
|
+
|
|
263
|
+
## Image & illustration assets
|
|
264
|
+
|
|
265
|
+
When a screen calls for illustration, empty-state art, hero imagery, or icons
|
|
266
|
+
beyond the symbol set:
|
|
267
|
+
|
|
268
|
+
- Generate assets with the **best image model available to you** (e.g. an
|
|
269
|
+
imagegen tool or the Higgsfield MCP/CLI if connected) at the **highest
|
|
270
|
+
quality settings**, then downscale to @1x/@2x/@3x. Never upscale.
|
|
271
|
+
- One visual language per app: pick a style (gradient-mesh, flat-duotone,
|
|
272
|
+
3D-clay, hand-drawn, mascot style) and generate ALL assets in that same style, same
|
|
273
|
+
palette, same lighting. A mixed-style asset set reads as template slop.
|
|
274
|
+
- Prompt for **transparent or solid-flat backgrounds** matched to your surface
|
|
275
|
+
color; composite artifacts (white halos, wrong-color mattes) are an
|
|
276
|
+
automatic redo.
|
|
277
|
+
- Full asset pipeline and prompt patterns:
|
|
278
|
+
[references/image-assets.md](#appendix-image-assets-md).
|
|
279
|
+
|
|
280
|
+
## The simulator loop (non-negotiable)
|
|
281
|
+
|
|
282
|
+
A screen does not exist until you have seen it running. The loop:
|
|
283
|
+
|
|
284
|
+
1. Implement → launch in the iOS Simulator (or Android emulator).
|
|
285
|
+
2. Screenshot and **actually look**: alignment, optical centering, spacing
|
|
286
|
+
rhythm, truncation with long content, dark mode, Dynamic Type at XL.
|
|
287
|
+
3. Run the **full-motion pass** below — screenshots prove layout; they prove
|
|
288
|
+
nothing about motion.
|
|
289
|
+
4. Fix, relaunch, re-verify. Repeat until you cannot find a defect — then run
|
|
290
|
+
the checklist in [references/simulator-loop.md](#appendix-simulator-loop-md)
|
|
291
|
+
once more.
|
|
292
|
+
|
|
293
|
+
Do not declare a screen finished from code review alone. Do not stop at "looks
|
|
294
|
+
fine" — stop at "cannot find a flaw at 100% zoom".
|
|
295
|
+
|
|
296
|
+
### The full-motion pass (mandatory, per flow)
|
|
297
|
+
|
|
298
|
+
Every flow is evaluated as **moving pictures in the simulator, never as
|
|
299
|
+
stills**. Screen-record the entire flow end to end
|
|
300
|
+
(`xcrun simctl io booted recordVideo flow.mov`), exercising ALL of it:
|
|
301
|
+
|
|
302
|
+
- every screen transition, push/pop, tab switch
|
|
303
|
+
- every back path — chevron, edge swipe, Android hardware back — and, after
|
|
304
|
+
each one-way door (sign-in, onboarding done, purchase, finished session),
|
|
305
|
+
an attempt to go back that must fail to re-enter the old state
|
|
306
|
+
- every modal and sheet: present, drag, dismiss — and cancel mid-drag
|
|
307
|
+
- the keyboard, both directions: appear (does the layout glide, is the
|
|
308
|
+
focused input visible?) and dismiss (does anything jump-cut?)
|
|
309
|
+
- every user interaction: press states, gesture follow-through, interrupted
|
|
310
|
+
gestures, rapid taps, scroll flings at the extremes
|
|
311
|
+
|
|
312
|
+
Watch the recording **twice**: once at full speed for feel, once scrubbing
|
|
313
|
+
frame by frame. You are hunting:
|
|
314
|
+
|
|
315
|
+
- dropped or stuttered frames — the bar is a sustained **60 fps** through
|
|
316
|
+
every transition, measured, not vibed
|
|
317
|
+
- one-frame flashes: white/unstyled first paint, wrong-theme frames mid-
|
|
318
|
+
transition, color pops where a surface briefly renders the wrong token
|
|
319
|
+
- layout jumps, double-render pops, springs that clip or overshoot into
|
|
320
|
+
content, elements that reflow after appearing
|
|
321
|
+
|
|
322
|
+
The whole recording must play like one native piece — smooth end to end,
|
|
323
|
+
zero UX glitches. One glitchy frame means the flow is not done.
|
|
324
|
+
|
|
325
|
+
## Definition of done, per screen
|
|
326
|
+
|
|
327
|
+
- [ ] Studied 10+ real reference screens for this screen type (via Appllama
|
|
328
|
+
MCP when available) and can name the pattern you adopted
|
|
329
|
+
- [ ] Navigation answered: what this screen *is* (push / modal / sheet /
|
|
330
|
+
overlay / replace), what back does from it on iOS and Android, and —
|
|
331
|
+
behind a one-way door — that back cannot re-enter the old state
|
|
332
|
+
- [ ] Light + dark mode verified in the simulator
|
|
333
|
+
- [ ] Safe areas / Dynamic Island / home indicator verified
|
|
334
|
+
- [ ] Long-content, empty, loading, and error states designed — not defaulted
|
|
335
|
+
- [ ] Motion: the full flow screen-recorded and scrubbed — entrances,
|
|
336
|
+
presses, transitions, modals, keyboard — native feel, zero glitch or
|
|
337
|
+
wrong-color frames; Reduce Motion respected; 60 fps measured on a
|
|
338
|
+
release build on the slowest supported device
|
|
339
|
+
- [ ] Dynamic Type XL doesn't break layout; text is selectable where useful
|
|
340
|
+
- [ ] All tap targets ≥ 44pt; contrast passes in both themes
|
|
341
|
+
- [ ] Assets: single style family, crisp at @3x, no compositing halos
|
|
342
|
+
- [ ] List surfaces virtualized; no controlled-input jank; no re-render storms
|
|
343
|
+
(profiled, not guessed)
|
|
344
|
+
|
|
345
|
+
## References
|
|
346
|
+
|
|
347
|
+
| File | Load when |
|
|
348
|
+
|---|---|
|
|
349
|
+
| [references/native-controls.md](#appendix-native-controls-md) | Choosing/wiring iOS+Android native controls, menus, pickers, sheets |
|
|
350
|
+
| [references/motion.md](#appendix-motion-md) | Any Reanimated work: gestures, transitions, springs, layout animations |
|
|
351
|
+
| [references/performance.md](#appendix-performance-md) | Jank, slow TTI, big bundles, memory leaks, profiling method |
|
|
352
|
+
| [references/image-assets.md](#appendix-image-assets-md) | Generating illustrations/icons/hero art with image models |
|
|
353
|
+
| [references/simulator-loop.md](#appendix-simulator-loop-md) | Final verification checklist + device matrix |
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Appendix: image-assets.md
|
|
358
|
+
|
|
359
|
+
# Image & illustration assets — generation pipeline
|
|
360
|
+
|
|
361
|
+
App-quality artwork is generated, curated, and post-processed — never "one
|
|
362
|
+
prompt, ship it". This is the pipeline.
|
|
363
|
+
|
|
364
|
+
## 1. Define the style system BEFORE generating anything
|
|
365
|
+
|
|
366
|
+
Write down, once per app:
|
|
367
|
+
|
|
368
|
+
- **Style family**: flat-duotone / gradient-mesh / 3D-clay / paper-collage /
|
|
369
|
+
hand-drawn-ink / photographic. Pick ONE.
|
|
370
|
+
- **Palette**: 3–5 hexes lifted from the app's design tokens, including the
|
|
371
|
+
exact surface color assets will sit on.
|
|
372
|
+
- **Lighting/texture**: soft top-left studio light, matte, no specular — or
|
|
373
|
+
whatever fits; but the same words in every prompt.
|
|
374
|
+
- **Subject grammar**: mascot? abstract shapes? objects? People (and if so,
|
|
375
|
+
what rendering style)?
|
|
376
|
+
|
|
377
|
+
Every asset prompt = style system + subject. This is what makes 12 assets
|
|
378
|
+
read as one commissioned set instead of 12 stock images.
|
|
379
|
+
|
|
380
|
+
## 2. Generate with the best tool available
|
|
381
|
+
|
|
382
|
+
Use whatever image generation capability is present in your environment — an
|
|
383
|
+
imagegen tool, the Higgsfield MCP/CLI, or another state-of-the-art model.
|
|
384
|
+
Rules:
|
|
385
|
+
|
|
386
|
+
- **Highest quality settings, largest size**, then downscale. Target at least
|
|
387
|
+
2× the largest rendered size (an asset shown at 200 pt needs ≥ 1200 px for
|
|
388
|
+
@3x). Never upscale a small generation.
|
|
389
|
+
- Generate **3–4 candidates** per asset, pick the best, regenerate the rest of
|
|
390
|
+
the set to match the winner if the winner drifted in style.
|
|
391
|
+
- For icon sets and repeated elements, generate a **sheet** in one prompt
|
|
392
|
+
(same lighting/palette guaranteed), then slice.
|
|
393
|
+
- Backgrounds: ask for the exact surface hex as a solid background, or true
|
|
394
|
+
transparency if the tool supports it. Inspect edges at 400% — halos, matte
|
|
395
|
+
fringes, or JPEG blocking around the subject mean regenerate or run a
|
|
396
|
+
background-removal pass.
|
|
397
|
+
|
|
398
|
+
## 3. Prompt patterns that work
|
|
399
|
+
|
|
400
|
+
```
|
|
401
|
+
[subject], [style family] illustration, [palette words + hexes],
|
|
402
|
+
[lighting], solid background #0F0F13, centered composition,
|
|
403
|
+
generous negative space, app illustration, no text, no watermark
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
- Always append `no text` — models love baking in gibberish labels.
|
|
407
|
+
- For empty states: subject should be *quiet* (a resting object, a soft
|
|
408
|
+
scene), not a busy hero.
|
|
409
|
+
- For celebration/success: motion implied by composition (confetti arcs,
|
|
410
|
+
tilt), not literal speed lines.
|
|
411
|
+
- For onboarding heroes: leave the upper or lower third empty for the
|
|
412
|
+
headline; say so in the prompt ("composition weighted to bottom half").
|
|
413
|
+
|
|
414
|
+
## 4. Post-process
|
|
415
|
+
|
|
416
|
+
1. Trim to content + consistent padding.
|
|
417
|
+
2. Export @1x/@2x/@3x PNG (or a single high-res + let expo-image scale, for
|
|
418
|
+
full-bleed art). WebP for large photographic assets.
|
|
419
|
+
3. Verify on-device in BOTH themes: an asset generated on a dark ground often
|
|
420
|
+
shows a halo on light ground. If the app is dual-theme, either generate
|
|
421
|
+
theme twins or use assets on theme-invariant surfaces.
|
|
422
|
+
4. Check file sizes: a decorative illustration should not cost 2 MB. Target
|
|
423
|
+
< 200 KB per screen-level asset after compression.
|
|
424
|
+
|
|
425
|
+
## 5. App icon & store assets
|
|
426
|
+
|
|
427
|
+
- App icon: generate at 1024×1024, no transparency, no rounded corners (the
|
|
428
|
+
OS masks it). Test it at 60 px — if the concept dies small, simplify.
|
|
429
|
+
- Screenshot frames/marketing come later; do not let store-asset style drift
|
|
430
|
+
from in-app style.
|
|
431
|
+
|
|
432
|
+
## 6. Quality gate
|
|
433
|
+
|
|
434
|
+
Reject an asset if ANY of:
|
|
435
|
+
- Style drifts from the set (different lighting, palette, line weight)
|
|
436
|
+
- Halo/fringe on its background at 400% zoom
|
|
437
|
+
- Baked-in text or watermark artifacts
|
|
438
|
+
- Composition fights the layout (subject cropped by safe areas, focal point
|
|
439
|
+
under a button)
|
|
440
|
+
- Looks like clip-art / default-model-style with no art direction
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## Appendix: motion.md
|
|
445
|
+
|
|
446
|
+
# Motion — Reanimated patterns that read as native
|
|
447
|
+
|
|
448
|
+
Motion quality is judged in the first 10 seconds of using an app. This file is
|
|
449
|
+
the working reference for gesture-driven and system-driven animation.
|
|
450
|
+
|
|
451
|
+
## The two families of motion
|
|
452
|
+
|
|
453
|
+
| Family | Driver | Curve | Examples |
|
|
454
|
+
|---|---|---|---|
|
|
455
|
+
| **Responsive** (user is touching it) | Gesture position/velocity | Spring, seeded with gesture velocity | Sheet drag, swipe-to-dismiss, pull-to-refresh, card pan |
|
|
456
|
+
| **Narrative** (system initiated) | Time | `withTiming`, ease-out, 150–350 ms | Screen entrances, fades, reveals, toasts |
|
|
457
|
+
|
|
458
|
+
Mixing them up is the #1 tell of non-native motion: a sheet that closes on a
|
|
459
|
+
fixed 300 ms timing after a fling feels dead; a button that springs for 800 ms
|
|
460
|
+
on tap feels like a toy.
|
|
461
|
+
|
|
462
|
+
## Springs
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
// The designer form (duration is perceptual): critically damped, no oscillation
|
|
466
|
+
const SNAP = { duration: 400, dampingRatio: 1 };
|
|
467
|
+
// Playful: one soft overshoot — use sparingly (celebrations, mascots)
|
|
468
|
+
const POP = { duration: 400, dampingRatio: 0.8 };
|
|
469
|
+
|
|
470
|
+
offset.set(withSpring(dest, { ...SNAP, velocity: event.velocityY }));
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
- Always pass the **gesture velocity** into the spring when a gesture releases.
|
|
474
|
+
- One spring vocabulary per app. Define `SNAP`/`POP` once, import everywhere.
|
|
475
|
+
|
|
476
|
+
## Gesture → animation, all on the UI thread
|
|
477
|
+
|
|
478
|
+
```ts
|
|
479
|
+
const pan = Gesture.Pan()
|
|
480
|
+
.onChange((e) => { offset.set(offset.get() + e.changeY); }) // worklet
|
|
481
|
+
.onEnd((e) => {
|
|
482
|
+
const dismiss = offset.get() > H * 0.3 || e.velocityY > 800;
|
|
483
|
+
offset.set(withSpring(dismiss ? H : 0, { ...SNAP, velocity: e.velocityY }));
|
|
484
|
+
if (dismiss) scheduleOnRN(onClose); // react-native-worklets; replaces the deprecated runOnJS
|
|
485
|
+
});
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
Rules:
|
|
489
|
+
- Never read/write React state inside `onChange`. `scheduleOnRN` only at
|
|
490
|
+
gesture end, for navigation/effects — and read/write shared values with
|
|
491
|
+
`.get()`/`.set()`, never during render.
|
|
492
|
+
- Thresholds combine **distance OR velocity** — a fast flick from 10 px away
|
|
493
|
+
must dismiss.
|
|
494
|
+
- Interruptible: starting a new gesture mid-spring must grab the current
|
|
495
|
+
animated value, not the destination.
|
|
496
|
+
|
|
497
|
+
## Entrances, exits, layout
|
|
498
|
+
|
|
499
|
+
```tsx
|
|
500
|
+
<Animated.View
|
|
501
|
+
entering={FadeInDown.duration(220).springify().damping(30)}
|
|
502
|
+
exiting={FadeOut.duration(150)}
|
|
503
|
+
layout={LinearTransition.springify().damping(30)}
|
|
504
|
+
/>
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
- Stagger list entrances by index (`delay(index * 40)`), cap the stagger at
|
|
508
|
+
~8 items — beyond that, enter as a block.
|
|
509
|
+
- Exits are always faster than entrances (~0.7×).
|
|
510
|
+
- `layout` transitions on containers whose children reorder/resize — this is
|
|
511
|
+
what makes filter chips, expanding cards, and reordering lists feel expensive.
|
|
512
|
+
|
|
513
|
+
## Shared-element feel without shared elements
|
|
514
|
+
|
|
515
|
+
True shared-element transitions are still niche; fake the continuity:
|
|
516
|
+
- Keep the tapped thumbnail's position stable while the detail screen fades in
|
|
517
|
+
over it (measure with `measure()` in a worklet).
|
|
518
|
+
- Match corner radius and aspect ratio between the origin card and the
|
|
519
|
+
destination hero so the eye reads them as the same object.
|
|
520
|
+
|
|
521
|
+
## Scroll-linked effects
|
|
522
|
+
|
|
523
|
+
```ts
|
|
524
|
+
const y = useScrollViewOffset(scrollRef);
|
|
525
|
+
const headerStyle = useAnimatedStyle(() => ({
|
|
526
|
+
opacity: interpolate(y.value, [0, 64], [0, 1], Extrapolation.CLAMP),
|
|
527
|
+
transform: [{ translateY: interpolate(y.value, [-100, 0], [-50, 0], Extrapolation.CLAMP) }],
|
|
528
|
+
}));
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
Standard native behaviors worth reproducing exactly:
|
|
532
|
+
- Large title collapses into the nav bar between ~0 and ~52 pt of scroll.
|
|
533
|
+
- Content scrolling under a translucent bar gets a fade/blur mask, not a hard
|
|
534
|
+
clip.
|
|
535
|
+
- Overscroll stretch on hero images (`interpolate` negative offsets into scale).
|
|
536
|
+
|
|
537
|
+
## Reduce Motion
|
|
538
|
+
|
|
539
|
+
```ts
|
|
540
|
+
const reduceMotion = useReducedMotion(); // react-native-reanimated
|
|
541
|
+
// spatial → opacity-only
|
|
542
|
+
entering={reduceMotion ? FadeIn.duration(150) : SlideInDown.springify()}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
Every spatial animation needs its cross-fade fallback. This is an accessibility
|
|
546
|
+
requirement, not a nice-to-have.
|
|
547
|
+
|
|
548
|
+
## Performance guardrails
|
|
549
|
+
|
|
550
|
+
- Animate only `transform` and `opacity` where possible; animating layout
|
|
551
|
+
props (width/height/padding) forces layout every frame.
|
|
552
|
+
- One `useAnimatedStyle` per animated node; don't share a giant style across
|
|
553
|
+
20 list items.
|
|
554
|
+
- Never allocate inside a worklet's hot path (no `.map`, no object spread per
|
|
555
|
+
frame).
|
|
556
|
+
- If a transition stutters: profile first (see performance.md) — the usual
|
|
557
|
+
culprits are a JS-thread stall from a heavy render committed mid-animation,
|
|
558
|
+
or an image decode on the UI thread.
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## Appendix: native-controls.md
|
|
563
|
+
|
|
564
|
+
# Native controls — use the platform's, wire them right
|
|
565
|
+
|
|
566
|
+
A native-feeling app is mostly assembled from controls the OS already ships.
|
|
567
|
+
Rebuild a control only when the design genuinely diverges — and then match the
|
|
568
|
+
platform's timing and haptics so it still reads as native.
|
|
569
|
+
|
|
570
|
+
## Control selection table
|
|
571
|
+
|
|
572
|
+
| Need | iOS | Android | Package |
|
|
573
|
+
|---|---|---|---|
|
|
574
|
+
| Toggle | Switch | Material Switch | `react-native` `Switch` (renders native on both) |
|
|
575
|
+
| Single choice, 2–5 options | Segmented control | Tabs / segmented buttons | `@react-native-segmented-control/segmented-control` |
|
|
576
|
+
| Value in a range | Slider | Material Slider | `@react-native-community/slider` |
|
|
577
|
+
| Date / time | Wheel or inline calendar | Material pickers | `@react-native-community/datetimepicker` (`display="inline"` for calendars on iOS) |
|
|
578
|
+
| Contextual actions | Context menu (long-press/tap) | Popup menu | `zeego` (native menus on both) — never a JS dropdown for item actions |
|
|
579
|
+
| Destructive confirm | Action sheet | Bottom sheet / dialog | `ActionSheetIOS` via `@expo/react-native-action-sheet` |
|
|
580
|
+
| Bottom sheet content | Detented sheet | Bottom sheet | `@gorhom/bottom-sheet` (see notes) |
|
|
581
|
+
| Search | Nav-bar integrated search | SearchView | Expo Router `headerSearchBarOptions` |
|
|
582
|
+
| Pull to refresh | UIRefreshControl | SwipeRefreshLayout | `RefreshControl` on the ScrollView/list |
|
|
583
|
+
| Haptics | UIImpactFeedbackGenerator | Vibrator | `expo-haptics` |
|
|
584
|
+
| In-app browser | SFSafariViewController | Custom Tabs | `expo-web-browser` |
|
|
585
|
+
|
|
586
|
+
## Menus (zeego / native context menus)
|
|
587
|
+
|
|
588
|
+
Item-level actions (rename, share, delete) belong in a native context menu
|
|
589
|
+
anchored to the element, with SF Symbols on iOS:
|
|
590
|
+
|
|
591
|
+
```tsx
|
|
592
|
+
<ContextMenu.Root>
|
|
593
|
+
<ContextMenu.Trigger>{card}</ContextMenu.Trigger>
|
|
594
|
+
<ContextMenu.Content>
|
|
595
|
+
<ContextMenu.Item key="share" onSelect={share}>
|
|
596
|
+
<ContextMenu.ItemTitle>Share</ContextMenu.ItemTitle>
|
|
597
|
+
<ContextMenu.ItemIcon ios={{ name: 'square.and.arrow.up' }} />
|
|
598
|
+
</ContextMenu.Item>
|
|
599
|
+
<ContextMenu.Item key="delete" destructive onSelect={confirmDelete}>
|
|
600
|
+
<ContextMenu.ItemTitle>Delete</ContextMenu.ItemTitle>
|
|
601
|
+
<ContextMenu.ItemIcon ios={{ name: 'trash' }} />
|
|
602
|
+
</ContextMenu.Item>
|
|
603
|
+
</ContextMenu.Content>
|
|
604
|
+
</ContextMenu.Root>
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
Destructive items: `destructive` role + a confirm step (action sheet), never a
|
|
608
|
+
bare tap-to-delete.
|
|
609
|
+
|
|
610
|
+
## Bottom sheets
|
|
611
|
+
|
|
612
|
+
- Detents should be content-derived (`enableDynamicSizing`) or the platform
|
|
613
|
+
set (medium/large) — arbitrary 37%/63% detents feel arbitrary.
|
|
614
|
+
- The sheet's drag must hand off to inner scroll correctly: use the
|
|
615
|
+
library's provided `BottomSheetScrollView`/`BottomSheetFlashList`, never a
|
|
616
|
+
plain ScrollView inside.
|
|
617
|
+
- Backdrop: fade in with sheet position (`interpolate` on `animatedIndex`),
|
|
618
|
+
tap-to-dismiss, and dim to the platform's standard (~40% black).
|
|
619
|
+
- Keyboard: `keyboardBlurBehavior="restore"`, and test with the keyboard up —
|
|
620
|
+
half the bottom-sheet bugs in the wild are keyboard interactions.
|
|
621
|
+
|
|
622
|
+
## Forms and inputs
|
|
623
|
+
|
|
624
|
+
- Labels above fields, not placeholders-as-labels.
|
|
625
|
+
- `keyboardType`, `autoComplete`, `textContentType` on every input — enables
|
|
626
|
+
autofill and the right keyboard. `textContentType="oneTimeCode"` for OTPs.
|
|
627
|
+
- Return-key chaining: `returnKeyType="next"` + focus the next field;
|
|
628
|
+
final field submits.
|
|
629
|
+
- Validate on blur or submit, never on keystroke; errors appear beneath the
|
|
630
|
+
field in the platform's error color and *stay* until fixed.
|
|
631
|
+
- Wrap forms in a keyboard-avoiding strategy you have actually tested
|
|
632
|
+
(`react-native-keyboard-controller` is the current best answer).
|
|
633
|
+
|
|
634
|
+
## Navigation patterns
|
|
635
|
+
|
|
636
|
+
- Stack for drill-in, tabs for top-level destinations, modal for
|
|
637
|
+
self-contained tasks. Do not put a back-navigable flow inside a modal deeper
|
|
638
|
+
than 2 steps — use a stack inside the modal with its own header.
|
|
639
|
+
- iOS: swipe-back must always work (don't block the interactive pop gesture).
|
|
640
|
+
- Tab bars: 3–5 items, SF Symbols with the filled variant for the active tab,
|
|
641
|
+
labels always on (icon-only tab bars fail recognition tests).
|
|
642
|
+
- Deep links: every screen reachable by URL via Expo Router's file routes.
|
|
643
|
+
|
|
644
|
+
## When you DO rebuild a control
|
|
645
|
+
|
|
646
|
+
Match the OS's numbers, not your instincts:
|
|
647
|
+
- iOS switch: thumb travel ~22 pt in ~0.2 s with a slight squish; haptic on
|
|
648
|
+
toggle.
|
|
649
|
+
- Pressed states: opacity 0.4 for plain-text buttons, scale 0.97 + slight
|
|
650
|
+
darken for filled buttons, spring back on release.
|
|
651
|
+
- Selection cells: checkmark animates in with a short fade+scale, row flashes
|
|
652
|
+
the selection color for ~150 ms.
|
|
653
|
+
- Always add the platform haptic the real control would emit.
|
|
654
|
+
|
|
655
|
+
---
|
|
656
|
+
|
|
657
|
+
## Appendix: performance.md
|
|
658
|
+
|
|
659
|
+
# Performance — measure, fix, re-measure
|
|
660
|
+
|
|
661
|
+
Perceived quality is half design, half frame rate. This reference is the
|
|
662
|
+
method; never optimize on vibes.
|
|
663
|
+
|
|
664
|
+
## The loop
|
|
665
|
+
|
|
666
|
+
1. **Measure** a baseline for the exact interaction that feels bad (FPS during
|
|
667
|
+
the transition, TTI from cold start, commit counts during typing).
|
|
668
|
+
2. **Optimize** the one thing the measurement indicts.
|
|
669
|
+
3. **Re-measure** the same way.
|
|
670
|
+
4. **Validate** with a number: 45 → 60 fps, TTI 3.2 s → 1.8 s. No number, no
|
|
671
|
+
claim.
|
|
672
|
+
|
|
673
|
+
Do not recommend `useMemo`/`useCallback`/`React.memo` without profiler
|
|
674
|
+
evidence of wasted renders. Do not flag "stale closure risk" without a repro.
|
|
675
|
+
Measure the target interaction, not tree depth.
|
|
676
|
+
|
|
677
|
+
## FPS & re-renders (highest impact)
|
|
678
|
+
|
|
679
|
+
- Long or growable list on ScrollView → replace with FlashList. This single
|
|
680
|
+
swap fixes more RN jank than everything else combined.
|
|
681
|
+
- Re-render storms: profile with React DevTools; the classic causes are a
|
|
682
|
+
broad context/store consumed by leaves, inline object/array props into
|
|
683
|
+
memoized children, and parent state that should be local.
|
|
684
|
+
- Prefer atomic state (Zustand selectors, Jotai atoms) so a change re-renders
|
|
685
|
+
only its consumers.
|
|
686
|
+
- React Compiler: enable once profiling shows cascading re-renders; it
|
|
687
|
+
replaces most manual memoization. Watch for bailouts (mutations, non-plain
|
|
688
|
+
patterns) — a bailed-out hot component silently loses the win.
|
|
689
|
+
- `useDeferredValue` for expensive derived UI (filter results, search
|
|
690
|
+
highlighting) behind fast-changing inputs.
|
|
691
|
+
|
|
692
|
+
## Typing performance
|
|
693
|
+
|
|
694
|
+
Controlled TextInputs re-render the tree per keystroke. For search bars and
|
|
695
|
+
forms: uncontrolled inputs (`defaultValue` + `onChangeText` into a ref or
|
|
696
|
+
store), commit to state on submit/debounce.
|
|
697
|
+
|
|
698
|
+
## Startup / TTI
|
|
699
|
+
|
|
700
|
+
- Measure only cold starts, with `react-native-performance` markers.
|
|
701
|
+
- Native navigation (`react-native-screens`) enabled.
|
|
702
|
+
- Defer everything not needed for first paint: heavy SDK init, analytics,
|
|
703
|
+
below-the-fold data.
|
|
704
|
+
- Hermes: check bundle compression guidance for your RN version (mmap).
|
|
705
|
+
- Inspect the bundle when it grows: `npx react-native bundle … --dev false`
|
|
706
|
+
then `source-map-explorer`. Barrel imports (`import { x } from '@/components'`)
|
|
707
|
+
are the classic silent bloat — import from the source file.
|
|
708
|
+
|
|
709
|
+
## Memory
|
|
710
|
+
|
|
711
|
+
- Symptoms: growing RSS while navigating back and forth. Hunt JS leaks with
|
|
712
|
+
heap snapshots (retained listeners, intervals, subscriptions in effects
|
|
713
|
+
missing cleanup) before blaming native.
|
|
714
|
+
- Lists: `recyclingKey` on `expo-image` items, no inline closures capturing
|
|
715
|
+
huge parent scopes in `renderItem`.
|
|
716
|
+
|
|
717
|
+
## Animations
|
|
718
|
+
|
|
719
|
+
- Anything janky mid-gesture: confirm the animation runs as a worklet on the
|
|
720
|
+
UI thread; a single `runOnJS` in `onChange` is enough to ruin it.
|
|
721
|
+
- Heavy screens committed during a transition stall the JS thread and hitch
|
|
722
|
+
even UI-thread animations — defer the destination screen's expensive work
|
|
723
|
+
until `InteractionManager.runAfterInteractions` / after the transition ends.
|
|
724
|
+
|
|
725
|
+
## Budgets to hold
|
|
726
|
+
|
|
727
|
+
| Metric | Budget |
|
|
728
|
+
|---|---|
|
|
729
|
+
| Transition/gesture FPS | 60 (no dropped frames in the hero flow) |
|
|
730
|
+
| Cold-start TTI (mid-tier device) | < 2 s |
|
|
731
|
+
| Keystroke → echo | < 50 ms |
|
|
732
|
+
| List scroll (FlashList) | blank-cell-free at fling speed |
|
|
733
|
+
| JS bundle (initial) | watch the trend; investigate any +10% jump |
|
|
734
|
+
|
|
735
|
+
---
|
|
736
|
+
|
|
737
|
+
## Appendix: simulator-loop.md
|
|
738
|
+
|
|
739
|
+
# The simulator loop — verification checklist & device matrix
|
|
740
|
+
|
|
741
|
+
A screen is finished when it survives this checklist on a real simulator, not
|
|
742
|
+
when the code compiles. Budget as many loop iterations as it takes; the goal
|
|
743
|
+
is "cannot find a flaw", not "looks fine".
|
|
744
|
+
|
|
745
|
+
## Loop mechanics
|
|
746
|
+
|
|
747
|
+
1. Launch on the iOS Simulator (primary) — `npx expo start` + `i`, or your
|
|
748
|
+
dev build. Android emulator second.
|
|
749
|
+
2. Screenshot the screen (simctl: `xcrun simctl io booted screenshot s.png`,
|
|
750
|
+
or your agent's screenshot tool). **Open the screenshot and study it** —
|
|
751
|
+
do not trust memory of what you wrote.
|
|
752
|
+
3. Interact: tap every control, type overlong text, background/foreground the
|
|
753
|
+
app, rotate if supported.
|
|
754
|
+
4. For motion: screen-record the ENTIRE flow
|
|
755
|
+
(`xcrun simctl io booted recordVideo m.mov`) — not just the hero
|
|
756
|
+
transition. Watch it at full speed for feel, then scrub frame by frame.
|
|
757
|
+
Stills cannot catch a one-frame flash, a dropped spring, or a keyboard
|
|
758
|
+
jump-cut; only the recording can.
|
|
759
|
+
5. Fix → relaunch → re-verify. Never batch more than a few fixes between
|
|
760
|
+
looks; regressions hide in batches.
|
|
761
|
+
|
|
762
|
+
If a UI-testing tool (e.g. Maestro) is available, script the flow's happy
|
|
763
|
+
path once it stabilizes — taps, assertions, screenshots — so later changes
|
|
764
|
+
re-verify for free.
|
|
765
|
+
|
|
766
|
+
## Per-screen checklist
|
|
767
|
+
|
|
768
|
+
**Layout**
|
|
769
|
+
- [ ] Nothing clipped by the Dynamic Island / status bar; scrolled content
|
|
770
|
+
passes *under* it with the intended fade/blur, not a hard edge
|
|
771
|
+
- [ ] Bottom CTA clears the home indicator (safe-area inset respected)
|
|
772
|
+
- [ ] Optical alignment: icons vs text baselines, centered things actually
|
|
773
|
+
look centered (check at 2× zoom)
|
|
774
|
+
- [ ] Spacing rhythm consistent (no rogue 13px gaps in an 8pt system)
|
|
775
|
+
- [ ] Long text: 2× length titles truncate/wrap by design, not by accident
|
|
776
|
+
- [ ] Empty state, loading state, error state each verified by forcing them
|
|
777
|
+
|
|
778
|
+
**Theming & type**
|
|
779
|
+
- [ ] Dark mode AND light mode screenshots taken and inspected
|
|
780
|
+
- [ ] Dynamic Type at XL: no overlap, no clipped labels
|
|
781
|
+
- [ ] Contrast: secondary text still readable in both themes
|
|
782
|
+
|
|
783
|
+
**Motion (evaluated on the full-flow recording, never on stills)**
|
|
784
|
+
- [ ] Entrance plays once, correctly, on first mount (and NOT again on
|
|
785
|
+
back-navigation)
|
|
786
|
+
- [ ] Gesture follows the finger 1:1; release springs with velocity;
|
|
787
|
+
cancelling mid-gesture settles cleanly
|
|
788
|
+
- [ ] Frame-by-frame: no pop at animation start/end, no double-render flash,
|
|
789
|
+
no one-frame white/wrong-theme/wrong-color frames during transitions
|
|
790
|
+
- [ ] Every modal/sheet cycle recorded: present, drag, dismiss, cancel —
|
|
791
|
+
smooth in both directions
|
|
792
|
+
- [ ] Keyboard appear AND dismiss recorded: layout glides with it, focused
|
|
793
|
+
input stays visible, nothing jump-cuts or reflows after settling
|
|
794
|
+
- [ ] Sustained 60 fps through every transition of the flow (measure with
|
|
795
|
+
the perf monitor / Instruments, don't eyeball)
|
|
796
|
+
- [ ] Reduce Motion enabled → spatial animations become fades
|
|
797
|
+
|
|
798
|
+
**Interaction**
|
|
799
|
+
- [ ] Every tappable ≥ 44pt; press states visible; haptics where native
|
|
800
|
+
controls would have them
|
|
801
|
+
- [ ] Keyboard: appears with the right type, doesn't cover the focused input,
|
|
802
|
+
dismisses sensibly
|
|
803
|
+
- [ ] Back gesture (iOS edge swipe) works everywhere it should
|
|
804
|
+
- [ ] Rapid double-taps don't double-navigate or double-submit
|
|
805
|
+
|
|
806
|
+
**State**
|
|
807
|
+
- [ ] Background the app mid-flow → return: state intact
|
|
808
|
+
- [ ] Kill and relaunch: persisted state restores, ephemeral state resets
|
|
809
|
+
- [ ] Offline: actions queue or fail loudly — never silently
|
|
810
|
+
|
|
811
|
+
## Device matrix (minimum)
|
|
812
|
+
|
|
813
|
+
| Profile | Why |
|
|
814
|
+
|---|---|
|
|
815
|
+
| Latest iPhone Pro (Dynamic Island) | Primary design target |
|
|
816
|
+
| iPhone SE-class (small, no island) | Layout compression + button reachability |
|
|
817
|
+
| Latest Pixel (Android) | Material behaviors, back gesture, font metrics |
|
|
818
|
+
| One tablet/iPad IF the app claims support | Otherwise explicitly letterbox |
|
|
819
|
+
|
|
820
|
+
Run the full checklist on the primary; on the others, verify layout,
|
|
821
|
+
safe areas, and the hero flow.
|