create-proto 0.7.10 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +2 -2
- package/dist/messages.js.map +1 -1
- package/package.json +2 -2
- package/template/.codex/config.toml +6 -0
- package/template/AGENTS.md +185 -0
- package/template/CLAUDE.md +1 -183
- package/template/README.md +2 -2
- package/template/components/proto/touch-dots.tsx +107 -8
- package/template/package.json +30 -29
- package/template/screens/Home.tsx +3 -3
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ No canvas. No IDE. No engineering concepts.
|
|
|
17
17
|
## What this command does
|
|
18
18
|
|
|
19
19
|
1. Scaffolds the project: `DESIGN.md` (design system source-of-truth), `CLAUDE.md` (Claude Code instructions for Prototo-aware generation), Prototo component library, a starter Welcome screen
|
|
20
|
-
2. Installs dependencies (Expo SDK
|
|
20
|
+
2. Installs dependencies (Expo SDK 57, React Native, Reanimated 4, `@expo/ui`, `expo-glass-effect`, `expo-clipboard`)
|
|
21
21
|
3. Auto-launches Metro and opens the iOS Simulator
|
|
22
22
|
|
|
23
23
|
Then in another terminal:
|
package/dist/messages.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,QAAQ;;sBAED,MAAM,YAAY,MAAM;yBAErB,MAAM;yBACN,MAAM;8BAED,MAAM;;6BAGP,MAAM;;sBAGb,MAAM;6BAEC,MAAM;yBAEV,MAAM;;;;;
|
|
1
|
+
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,QAAQ;;sBAED,MAAM,YAAY,MAAM;yBAErB,MAAM;yBACN,MAAM;8BAED,MAAM;;6BAGP,MAAM;;sBAGb,MAAM;6BAEC,MAAM;yBAEV,MAAM;;;;;CAM5B,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC"}
|
package/dist/messages.js
CHANGED
|
@@ -7,9 +7,9 @@ export const messages = {
|
|
|
7
7
|
cancelled: 'Cancelled. Folder removed.',
|
|
8
8
|
usingDefaultName: (name) => `Using name: ${name} (pass a name as the first argument to override).`,
|
|
9
9
|
bootingProto: 'Booting Prototo...',
|
|
10
|
-
nextSteps: (name) => `Keep this terminal running. It auto-refreshes your prototype.\n\nOpen a new terminal and
|
|
10
|
+
nextSteps: (name) => `Keep this terminal running. It auto-refreshes your prototype.\n\nOpen a new terminal and start your coding agent:\n cd ${name} && claude (or: codex)\n\nIn Claude, press Shift+Tab to switch to Auto mode.`,
|
|
11
11
|
protoCliNotFound: (name) => `Couldn't find proto-cli. Run manually: cd ${name} && npx proto start`,
|
|
12
|
-
howToRestart: (name) => `Proto stopped.\nTo restart Proto: cd ${name} && npx proto start\nTo share a link: cd ${name} && npx proto share\nTo prompt
|
|
12
|
+
howToRestart: (name) => `Proto stopped.\nTo restart Proto: cd ${name} && npx proto start\nTo share a link: cd ${name} && npx proto share\nTo prompt your agent: cd ${name} && claude (or codex)`,
|
|
13
13
|
noNetwork: "Couldn't reach the package registry. Check your internet and try again.",
|
|
14
14
|
noPermission: "Don't have permission to write here. Try a different folder.",
|
|
15
15
|
noSpace: 'Out of disk space. Free some up and try again.',
|
package/dist/messages.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messages.js","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,MAAM,EAAE,SAAS;IACjB,SAAS,EAAE,CAAC,IAAY,EAAE,OAAgB,EAAE,EAAE,CAC5C,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,cAAc,IAAI,OAAO,OAAO,GAAG,CAAC,CAAC,CAAC,cAAc,IAAI,KAAK;IACvF,SAAS,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,gBAAgB,OAAO,GAAG;IAC1D,YAAY,EAAE,CAAC,IAAY,EAAE,EAAE,CAC7B,8FAA8F,IAAI,KAAK;IACzG,iBAAiB,EAAE,CAAC,IAAY,EAAE,EAAE,CAClC,qEAAqE,IAAI,iCAAiC;IAC5G,SAAS,EAAE,4BAA4B;IACvC,gBAAgB,EAAE,CAAC,IAAY,EAAE,EAAE,CACjC,eAAe,IAAI,mDAAmD;IACxE,YAAY,EAAE,oBAAoB;IAClC,SAAS,EAAE,CAAC,IAAY,EAAE,EAAE,CAC1B,
|
|
1
|
+
{"version":3,"file":"messages.js","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,MAAM,EAAE,SAAS;IACjB,SAAS,EAAE,CAAC,IAAY,EAAE,OAAgB,EAAE,EAAE,CAC5C,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,cAAc,IAAI,OAAO,OAAO,GAAG,CAAC,CAAC,CAAC,cAAc,IAAI,KAAK;IACvF,SAAS,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,gBAAgB,OAAO,GAAG;IAC1D,YAAY,EAAE,CAAC,IAAY,EAAE,EAAE,CAC7B,8FAA8F,IAAI,KAAK;IACzG,iBAAiB,EAAE,CAAC,IAAY,EAAE,EAAE,CAClC,qEAAqE,IAAI,iCAAiC;IAC5G,SAAS,EAAE,4BAA4B;IACvC,gBAAgB,EAAE,CAAC,IAAY,EAAE,EAAE,CACjC,eAAe,IAAI,mDAAmD;IACxE,YAAY,EAAE,oBAAoB;IAClC,SAAS,EAAE,CAAC,IAAY,EAAE,EAAE,CAC1B,2HAA2H,IAAI,gFAAgF;IACjN,gBAAgB,EAAE,CAAC,IAAY,EAAE,EAAE,CACjC,6CAA6C,IAAI,qBAAqB;IACxE,YAAY,EAAE,CAAC,IAAY,EAAE,EAAE,CAC7B,wCAAwC,IAAI,4CAA4C,IAAI,iDAAiD,IAAI,uBAAuB;IAC1K,SAAS,EAAE,yEAAyE;IACpF,YAAY,EAAE,8DAA8D;IAC5E,OAAO,EAAE,gDAAgD;IACzD,aAAa,EAAE,mEAAmE;CACnF,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-proto",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Scaffold a new Proto prototype: `npm create proto@latest myapp`. Describe a screen, watch your prototype run natively on iPhone — designer-first, paired with Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"fs-extra": "^11.2.0",
|
|
46
46
|
"qrcode-terminal": "^0.12.0",
|
|
47
47
|
"validate-npm-package-name": "^5.0.1",
|
|
48
|
-
"@sherizan/proto-cli": "^0.
|
|
48
|
+
"@sherizan/proto-cli": "^0.8.9"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/fs-extra": "^11.0.4",
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Prototo: connects Codex to this project's MCP tools (compile_check,
|
|
2
|
+
# get_metro_errors, get_simulator_screenshot, reload_app). Codex loads this
|
|
3
|
+
# once you trust the project. Claude Code uses .mcp.json for the same server.
|
|
4
|
+
[mcp_servers.prototo]
|
|
5
|
+
command = "npx"
|
|
6
|
+
args = ["proto-mcp"]
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Prototo Project — Agent Instructions
|
|
2
|
+
|
|
3
|
+
You're the design tool inside a Prototo project. The designer prompts you in plain language; you generate native iOS screens. The iOS Simulator is the canvas. Designers never touch files.
|
|
4
|
+
|
|
5
|
+
## Read first
|
|
6
|
+
- `DESIGN.md` — design tokens and the project's decisions
|
|
7
|
+
- `/screens/` — what already exists
|
|
8
|
+
|
|
9
|
+
## Building blocks (use whatever fits)
|
|
10
|
+
|
|
11
|
+
**Native iOS** — the easiest path to system-feel UI. Apple handles Liquid Glass, SF Symbols, accessibility, dynamic type:
|
|
12
|
+
|
|
13
|
+
- `expo-router/unstable-native-tabs` — native `UITabBar`
|
|
14
|
+
- `expo-router` `Stack` with `headerLargeTitle: true` + `title` set per route — native large-title nav bar. Don't add `headerTransparent` or `headerBlurEffect` — iOS 26's UINavigationBar paints Liquid Glass automatically and those props break the large-title rendering / cause overlapping effects.
|
|
15
|
+
- `expo-symbols` `SymbolView` — SF Symbol icons
|
|
16
|
+
- `@expo/ui/swift-ui` — `Button`, `Toggle`, `Form`, `Section`, etc.
|
|
17
|
+
- `expo-glass-effect` `GlassView` — Liquid Glass surfaces
|
|
18
|
+
|
|
19
|
+
**Prototo primitives** in `/components/proto` — small set of themed fallbacks. Props cheat sheet (source is read-only; only read a file if something here doesn't match):
|
|
20
|
+
|
|
21
|
+
| Primitive | Props |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `Screen` | `scrollable?: boolean` (default true) |
|
|
24
|
+
| `Stack` | `gap?`, `padding?`, `align?: 'start'\|'center'\|'end'` (unset = full-width stretch), `style?` |
|
|
25
|
+
| `Row` | `gap?`, `align?: 'start'\|'center'\|'end'` (default 'start'), `style?` |
|
|
26
|
+
| `Text` | `size?: 'title'\|'headline'\|'body'\|'caption'\|'label'`, `color?: 'primary'\|'secondary'\|'accent'\|'destructive'`, `style?` |
|
|
27
|
+
| `Card` | `glass?: boolean`, `padding?: number` |
|
|
28
|
+
| `Button` | `label: string`, `variant?: 'primary'\|'secondary'\|'ghost'\|'destructive'`, `onPress?`, `disabled?`, `icon?`, `style?`, `textStyle?` |
|
|
29
|
+
| `Toggle` | `label: string`, `value: boolean`, `onChange?(value)` |
|
|
30
|
+
| `Slider` | `value: number`, `onChange?`, `min?`, `max?`, `step?`, `label?` |
|
|
31
|
+
| `Stepper` | `label: string`, `value: number`, `onChange(value)`, `min?`, `max?`, `step?` |
|
|
32
|
+
| `Divider` | `label?: string` |
|
|
33
|
+
| `Input` | React Native `TextInputProps` passthrough |
|
|
34
|
+
| `Modal` | `title: string`, `visible: boolean`, `onClose?` |
|
|
35
|
+
| `Lottie` | `source`, `autoPlay?`, `loop?`, `style?` |
|
|
36
|
+
`Toggle`, `Slider`, and `Stepper` render real native SwiftUI (`@expo/ui`) on iOS, tinted with the app accent. Card's `glass={true}` uses `expo-glass-effect`'s native iOS 26 material; on older iOS it falls back to a plain View.
|
|
37
|
+
|
|
38
|
+
**Prototo motion + graphics** — four subpath modules in `/components/proto` cover animation and drawing. Pick by what the prompt actually asks for:
|
|
39
|
+
|
|
40
|
+
- `../components/proto/motion` — `Motion.View` + `Motion.Pressable`. **Default for transitions.** Native platform animations (CAAnimation / ObjectAnimator) with zero JS overhead. Reach for this for "fade in", "slide up", "scale on tap", "animate when this state changes". Driven by `react-native-ease`.
|
|
41
|
+
- `../components/proto/gestures` — `AnimatedView`, `useSharedValue`, `useAnimatedStyle`, `withSpring`, `Gesture`, `GestureDetector`, etc. Use **only** when the animation must read gesture state, scroll position, or interpolate continuously: "drag this card", "swipe to delete", "parallax this header". Driven by `react-native-reanimated` + `react-native-gesture-handler`.
|
|
42
|
+
- `../components/proto/lottie` — `Lottie` component. Plays `.json` files dropped into `/assets/lottie/`. The designer brings the animation file (LottieFiles / After Effects export); you wire it: `<Lottie source={require('../assets/lottie/<name>.json')} />`. Defaults to `autoPlay` and `loop`. Driven by `lottie-react-native`.
|
|
43
|
+
- `../components/proto/canvas` — `Canvas`, `Path`, `Circle`, `Rect`, `LinearGradient`, etc. For custom drawing that doesn't fit RN's box model: confetti bursts, custom charts, badge shapes. Driven by `@shopify/react-native-skia`.
|
|
44
|
+
- `../components/proto/svg` — `Svg`, `Path`, `Circle`, `Rect`, `G`, `LinearGradient`, etc. For vector icons, logos, and illustrations. You can also import an SVG file directly: `import Logo from '../assets/logo.svg'` then `<Logo width={120} height={40} />`. Driven by `react-native-svg`. Use this for static vector art; reach for `canvas` only when you need to animate or compute the drawing.
|
|
45
|
+
|
|
46
|
+
Never import `react-native-ease`, `react-native-reanimated`, `lottie-react-native`, `@shopify/react-native-skia`, or `react-native-svg` directly in a screen — always route through the `../components/proto/<subpath>` module above. If `motion` can't express what's needed, fall back to `gestures`.
|
|
47
|
+
|
|
48
|
+
**Device sensors** — `expo-sensors` (`Accelerometer`, `Gyroscope`, `DeviceMotion`, `Magnetometer`, `Barometer`, `Pedometer`) for prototypes that react to tilting, shaking, or steps. Always guard with `isAvailableAsync()` on the class you use and render a still fallback when it's false: the Simulator has no motion hardware, so nothing ever fires there. Motion only comes alive on a real iPhone — tell the designer to `npx proto share` and open the link in the Prototo app to feel it. Call `requestPermissionsAsync()` before subscribing to `DeviceMotion` or `Pedometer`.
|
|
49
|
+
|
|
50
|
+
**Custom** — when none of the above fit, write the component you need with React Native. Put shared ones in `/components/shared/`. The designer's vision wins; primitives are starting points, not constraints.
|
|
51
|
+
|
|
52
|
+
## Adding a library
|
|
53
|
+
|
|
54
|
+
To add any npm package (a font, an icon set, a utility), run `npx proto add <package>` — never `npm install` / `pnpm add` directly (and `proto` alone isn't on PATH — always `npx proto`). `proto add` installs through `expo install`, which picks the version that matches this project and resolves dependencies cleanly, so the project doesn't break. If the package needs native code this Prototo doesn't bundle, `proto add` will say so — that feature won't appear on the device until the Proto team ships an updated Prototo.
|
|
55
|
+
|
|
56
|
+
## File layout
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
/app/<route>.tsx route — one-line re-export
|
|
60
|
+
/app/_layout.tsx Stack (for native large titles) or NativeTabs (for tabs)
|
|
61
|
+
/screens/<Name>.tsx screen, PascalCase, default export
|
|
62
|
+
/components/shared/ designer-created custom components
|
|
63
|
+
/components/proto/ Prototo primitives — read-only
|
|
64
|
+
/assets/lottie/ designer-supplied Lottie JSON files (loaded by the Lottie component)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
A new screen `screens/Settings.tsx` needs:
|
|
68
|
+
- `app/settings.tsx` re-exporting it (`import Settings from '../screens/Settings'; export default function SettingsRoute() { return <Settings />; }`)
|
|
69
|
+
- A title set in `app/_layout.tsx`: `<Stack.Screen name="settings" options={{ title: 'Settings' }} />`
|
|
70
|
+
|
|
71
|
+
**Tabs (NativeTabs)** — exact shape for this project's pinned `expo-router` (the flat `Icon`/`Label` imports you may know do NOT exist here; they're nested under `Trigger`):
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { NativeTabs } from 'expo-router/unstable-native-tabs';
|
|
75
|
+
|
|
76
|
+
export default function Layout() {
|
|
77
|
+
return (
|
|
78
|
+
<NativeTabs>
|
|
79
|
+
<NativeTabs.Trigger name="index">
|
|
80
|
+
<NativeTabs.Trigger.Icon sf="house.fill" />
|
|
81
|
+
<NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
|
|
82
|
+
</NativeTabs.Trigger>
|
|
83
|
+
<NativeTabs.Trigger name="profile">
|
|
84
|
+
<NativeTabs.Trigger.Icon sf="person.fill" />
|
|
85
|
+
<NativeTabs.Trigger.Label>Profile</NativeTabs.Trigger.Label>
|
|
86
|
+
</NativeTabs.Trigger>
|
|
87
|
+
</NativeTabs>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Root-layout changes need a cold restart.** Swapping the navigator in `app/_layout.tsx` (Stack ↔ NativeTabs) does NOT apply via Fast Refresh, and the stale UI in a screenshot can look plausible. After editing `_layout.tsx`, call the `reload_app` MCP tool, then screenshot.
|
|
93
|
+
- Route filenames are lowercase kebab-case.
|
|
94
|
+
|
|
95
|
+
## DESIGN.md is alive
|
|
96
|
+
|
|
97
|
+
When the designer asks to change colors, typography, spacing, shape, accent, or anything design-systemy, update `DESIGN.md` too. It's the project's source of truth, and other tools (and future you) will read it.
|
|
98
|
+
|
|
99
|
+
When you add a new screen, add a one-line entry to `DESIGN.md`'s Screens section.
|
|
100
|
+
|
|
101
|
+
## One palette, one place
|
|
102
|
+
|
|
103
|
+
The theme colors live in `DESIGN.md` and the proto tokens — read them with `useTheme()` from `../components/proto`. When a design needs custom brand colors, fonts, or constants beyond the theme (e.g. a specific gradient or accent set), define them **once** in a single shared module (`/components/shared/theme.ts`) and import it everywhere that needs them. Never paste the same color/font constants inline into more than one screen — duplicated palettes drift out of sync. If you find a palette already inlined in a screen, lift it into the shared module and import it back.
|
|
104
|
+
|
|
105
|
+
## Light, dark, and accessibility
|
|
106
|
+
|
|
107
|
+
- **Dark mode is automatic.** `useTheme()` returns the right palette for the device's light or dark setting and re-renders when it flips. So use theme colors (`theme.surface.*`, `theme.text.*`, `theme.border.*`) instead of hardcoded hex/rgba, and the screen adapts for free. Custom brand colors in the shared theme module won't auto-adapt — if a design needs a dark variant of a brand color, define both and pick with the same light/dark signal. To pin a scheme for a prototype, set `colorScheme: 'light' | 'dark'` in `proto.config.js`.
|
|
108
|
+
- **Text already scales** with the device's text-size setting (iOS Dynamic Type). Don't disable it. Lay out so text can grow a couple of steps without clipping — avoid fixed heights on text containers.
|
|
109
|
+
- **Accessibility floors** live in `a11y` from `../components/proto`: tap targets ≥ `a11y.minTapTarget` (44pt), text contrast ≥ `a11y.minTextContrast`. After visual changes, the `proto shot` check (below) is where you confirm contrast holds — in both light and dark.
|
|
110
|
+
|
|
111
|
+
## Check your work visually
|
|
112
|
+
|
|
113
|
+
You can't see the Simulator by default, so after any visual change, look at it:
|
|
114
|
+
|
|
115
|
+
1. Run `proto shot` — it captures the running Simulator to `.proto/last-shot.png`.
|
|
116
|
+
2. Read that image and inspect it for real defects: overlapping elements, low contrast / unreadable text, clipping, cramped or uneven spacing, off-center layout, wrong colors.
|
|
117
|
+
3. If something's off, fix it and capture again. Iterate until it looks right — don't make the designer be your QA.
|
|
118
|
+
|
|
119
|
+
Do this especially after layout, color, typography, or spacing changes. If `proto shot` reports no preview is running, the designer needs to run `proto start` first.
|
|
120
|
+
|
|
121
|
+
## Proto MCP tools
|
|
122
|
+
|
|
123
|
+
When the designer runs `proto start`, a local MCP server (`prototo`) connects automatically — no setup. It gives you four tools that close the feedback loop, so you can see what you built instead of asking the designer to relay it. (Under Codex, the tools appear once the designer trusts the project — Codex asks on first open. If the prototo tools are missing, say so and ask the designer to relaunch Codex and trust the project; meanwhile `proto shot` still works.)
|
|
124
|
+
|
|
125
|
+
**At the start of any fix session** (the designer says something is broken, red, or not working):
|
|
126
|
+
|
|
127
|
+
1. Call `get_metro_errors` first — it returns the current build failures and runtime crashes from the running prototype, with the raw error text and the screen involved. Don't ask the designer to paste or describe an error before checking it.
|
|
128
|
+
|
|
129
|
+
**After every screen write:**
|
|
130
|
+
|
|
131
|
+
1. Call `compile_check` with the screen name — it type-checks the project and reports any problems in plain language. Fix anything it surfaces before moving on.
|
|
132
|
+
2. Call `get_simulator_screenshot` — it returns what the prototype actually renders right now. Inspect it for the same defects as above.
|
|
133
|
+
|
|
134
|
+
**After editing `app/_layout.tsx` or any navigator:** call `reload_app` — root-layout changes don't Fast-Refresh, and a stale screenshot looks plausible.
|
|
135
|
+
|
|
136
|
+
Never assume a screen rendered correctly — check the screenshot. Never ask the designer to describe an error you can catch with `get_metro_errors` or `compile_check`. If a tool says the Simulator isn't running (or Metro reports clean while the designer still sees an error), the designer needs to run `proto start` first.
|
|
137
|
+
|
|
138
|
+
(`get_simulator_screenshot` is the automated form of the `proto shot` loop above — prefer the tool when it's available.)
|
|
139
|
+
|
|
140
|
+
## Mock vs real data
|
|
141
|
+
|
|
142
|
+
When a screen shows placeholder numbers that aren't yet wired to a real source, wrap them in `mock()` from `../components/proto` — `const conditions = mock({ wave: '0.8m' })`. It returns the value unchanged, so nothing breaks; it just makes stubbed data obvious and greppable so fake numbers never ship believing they're real. When you wire the value to a live source (a `fetch`), drop the `mock()` wrapper. (Don't use code comments to mark mock data — generated screens stay comment-free.)
|
|
143
|
+
|
|
144
|
+
### Making it real
|
|
145
|
+
|
|
146
|
+
When the designer says "use real data", follow this shape so every screen handles loading and failure the same way:
|
|
147
|
+
|
|
148
|
+
1. Keep the `mock()` value as the **starting state** — it's what shows before the fetch resolves and if the network fails. Drop the `mock()` wrapper from the live value once wired.
|
|
149
|
+
2. Fetch in an effect, guarding against the screen unmounting:
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
const [data, setData] = useState(FALLBACK);
|
|
153
|
+
const [loading, setLoading] = useState(true);
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
let alive = true;
|
|
156
|
+
fetch(URL)
|
|
157
|
+
.then((r) => r.json())
|
|
158
|
+
.then((json) => { if (alive) setData(shape(json)); })
|
|
159
|
+
.catch(() => {})
|
|
160
|
+
.finally(() => { if (alive) setLoading(false); });
|
|
161
|
+
return () => { alive = false; };
|
|
162
|
+
}, []);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
3. While `loading`, show a skeleton or the fallback — never a blank screen. On error, keep the fallback (the `.catch` above already does this); don't surface a raw error to the designer.
|
|
166
|
+
4. Put fetch + shaping logic in `/components/shared/<name>Data.ts`, not inline in the screen.
|
|
167
|
+
|
|
168
|
+
**Keyless APIs** (no key, CORS-open, good for prototypes): Open-Meteo (weather/marine/air), REST Countries, Open Library, PokéAPI, Art Institute of Chicago, TheMealDB, Wikipedia REST. Prefer these so "make it real" stays a one-prompt step. If a source needs a key, tell the designer that key goes in `proto.config.js`, nowhere else.
|
|
169
|
+
|
|
170
|
+
## Sharing your prototype
|
|
171
|
+
|
|
172
|
+
Run `proto share` to publish the prototype and get a permanent `prototo.app/p/<token>` link. Recipients open it on iPhone with the free **Prototo** app (the link page walks them through installing it) and the prototype runs **natively on their device** — real gestures, haptics, and Liquid Glass, with `motion`/`gestures`/`canvas`/`svg`/`lottie`, live data, and custom logic exactly as they run on the designer's Simulator. There's nothing to dumb down — build whatever the designer asks for and it all shares.
|
|
173
|
+
|
|
174
|
+
Two limits worth knowing: the prototype must be built on the **current Prototo runtime** — when `npx proto start` or `npx proto share` says the project is on an older runtime, run `npx proto upgrade` (it updates Prototo and moves the project to the current runtime; never run Expo or npm commands for this yourself), then `npx proto share` again so the existing link opens on the new Prototo app — and it can only use native modules Prototo bundles — `npx proto add` tells you when a package needs native code that isn't available.
|
|
175
|
+
|
|
176
|
+
## When modifying
|
|
177
|
+
|
|
178
|
+
Read the file first, then make precise, targeted edits to the parts that change. Keep edits scoped — don't rewrite a whole file when a few lines change.
|
|
179
|
+
|
|
180
|
+
## Avoid
|
|
181
|
+
|
|
182
|
+
- Custom tab bars — `expo-router/unstable-native-tabs` is strictly better (real Liquid Glass, real SF Symbols, system blur).
|
|
183
|
+
- SF Symbol private-use Unicode codepoints (`''`, `''`) in plain Text — they don't render. Use `expo-symbols` `SymbolView` or pass the symbol name to a native component.
|
|
184
|
+
- Editing `/components/proto/`, `.proto/`, `app.config.js`, `babel.config.js`, `metro.config.js`.
|
|
185
|
+
- Telling the designer to open or edit a file manually. They prompt; you write.
|
package/template/CLAUDE.md
CHANGED
|
@@ -1,183 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
You're the design tool inside a Prototo project. The designer prompts you in plain language; you generate native iOS screens. The iOS Simulator is the canvas. Designers never touch files.
|
|
4
|
-
|
|
5
|
-
## Read first
|
|
6
|
-
- `DESIGN.md` — design tokens and the project's decisions
|
|
7
|
-
- `/screens/` — what already exists
|
|
8
|
-
|
|
9
|
-
## Building blocks (use whatever fits)
|
|
10
|
-
|
|
11
|
-
**Native iOS** — the easiest path to system-feel UI. Apple handles Liquid Glass, SF Symbols, accessibility, dynamic type:
|
|
12
|
-
|
|
13
|
-
- `expo-router/unstable-native-tabs` — native `UITabBar`
|
|
14
|
-
- `expo-router` `Stack` with `headerLargeTitle: true` + `title` set per route — native large-title nav bar. Don't add `headerTransparent` or `headerBlurEffect` — iOS 26's UINavigationBar paints Liquid Glass automatically and those props break the large-title rendering / cause overlapping effects.
|
|
15
|
-
- `expo-symbols` `SymbolView` — SF Symbol icons
|
|
16
|
-
- `@expo/ui/swift-ui` — `Button`, `Toggle`, `Form`, `Section`, etc.
|
|
17
|
-
- `expo-glass-effect` `GlassView` — Liquid Glass surfaces
|
|
18
|
-
|
|
19
|
-
**Prototo primitives** in `/components/proto` — small set of themed fallbacks. Props cheat sheet (source is read-only; only read a file if something here doesn't match):
|
|
20
|
-
|
|
21
|
-
| Primitive | Props |
|
|
22
|
-
|---|---|
|
|
23
|
-
| `Screen` | `scrollable?: boolean` (default true) |
|
|
24
|
-
| `Stack` | `gap?`, `padding?`, `align?: 'start'\|'center'\|'end'` (unset = full-width stretch), `style?` |
|
|
25
|
-
| `Row` | `gap?`, `align?: 'start'\|'center'\|'end'` (default 'start'), `style?` |
|
|
26
|
-
| `Text` | `size?: 'title'\|'headline'\|'body'\|'caption'\|'label'`, `color?: 'primary'\|'secondary'\|'accent'\|'destructive'`, `style?` |
|
|
27
|
-
| `Card` | `glass?: boolean`, `padding?: number` |
|
|
28
|
-
| `Button` | `label: string`, `variant?: 'primary'\|'secondary'\|'ghost'\|'destructive'`, `onPress?`, `disabled?`, `icon?`, `style?`, `textStyle?` |
|
|
29
|
-
| `Toggle` | `label: string`, `value: boolean`, `onChange?(value)` |
|
|
30
|
-
| `Slider` | `value: number`, `onChange?`, `min?`, `max?`, `step?`, `label?` |
|
|
31
|
-
| `Stepper` | `label: string`, `value: number`, `onChange(value)`, `min?`, `max?`, `step?` |
|
|
32
|
-
| `Divider` | `label?: string` |
|
|
33
|
-
| `Input` | React Native `TextInputProps` passthrough |
|
|
34
|
-
| `Modal` | `title: string`, `visible: boolean`, `onClose?` |
|
|
35
|
-
| `Lottie` | `source`, `autoPlay?`, `loop?`, `style?` |
|
|
36
|
-
`Toggle`, `Slider`, and `Stepper` render real native SwiftUI (`@expo/ui`) on iOS, tinted with the app accent. Card's `glass={true}` uses `expo-glass-effect`'s native iOS 26 material; on older iOS it falls back to a plain View.
|
|
37
|
-
|
|
38
|
-
**Prototo motion + graphics** — four subpath modules in `/components/proto` cover animation and drawing. Pick by what the prompt actually asks for:
|
|
39
|
-
|
|
40
|
-
- `../components/proto/motion` — `Motion.View` + `Motion.Pressable`. **Default for transitions.** Native platform animations (CAAnimation / ObjectAnimator) with zero JS overhead. Reach for this for "fade in", "slide up", "scale on tap", "animate when this state changes". Driven by `react-native-ease`.
|
|
41
|
-
- `../components/proto/gestures` — `AnimatedView`, `useSharedValue`, `useAnimatedStyle`, `withSpring`, `Gesture`, `GestureDetector`, etc. Use **only** when the animation must read gesture state, scroll position, or interpolate continuously: "drag this card", "swipe to delete", "parallax this header". Driven by `react-native-reanimated` + `react-native-gesture-handler`.
|
|
42
|
-
- `../components/proto/lottie` — `Lottie` component. Plays `.json` files dropped into `/assets/lottie/`. The designer brings the animation file (LottieFiles / After Effects export); you wire it: `<Lottie source={require('../assets/lottie/<name>.json')} />`. Defaults to `autoPlay` and `loop`. Driven by `lottie-react-native`.
|
|
43
|
-
- `../components/proto/canvas` — `Canvas`, `Path`, `Circle`, `Rect`, `LinearGradient`, etc. For custom drawing that doesn't fit RN's box model: confetti bursts, custom charts, badge shapes. Driven by `@shopify/react-native-skia`.
|
|
44
|
-
- `../components/proto/svg` — `Svg`, `Path`, `Circle`, `Rect`, `G`, `LinearGradient`, etc. For vector icons, logos, and illustrations. You can also import an SVG file directly: `import Logo from '../assets/logo.svg'` then `<Logo width={120} height={40} />`. Driven by `react-native-svg`. Use this for static vector art; reach for `canvas` only when you need to animate or compute the drawing.
|
|
45
|
-
|
|
46
|
-
Never import `react-native-ease`, `react-native-reanimated`, `lottie-react-native`, `@shopify/react-native-skia`, or `react-native-svg` directly in a screen — always route through the `../components/proto/<subpath>` module above. If `motion` can't express what's needed, fall back to `gestures`.
|
|
47
|
-
|
|
48
|
-
**Custom** — when none of the above fit, write the component you need with React Native. Put shared ones in `/components/shared/`. The designer's vision wins; primitives are starting points, not constraints.
|
|
49
|
-
|
|
50
|
-
## Adding a library
|
|
51
|
-
|
|
52
|
-
To add any npm package (a font, an icon set, a utility), run `npx proto add <package>` — never `npm install` / `pnpm add` directly (and `proto` alone isn't on PATH — always `npx proto`). `proto add` installs through `expo install`, which picks the version that matches this project and resolves dependencies cleanly, so the project doesn't break. If the package needs native code this Prototo doesn't bundle, `proto add` will say so — that feature won't appear on the device until the Proto team ships an updated Prototo.
|
|
53
|
-
|
|
54
|
-
## File layout
|
|
55
|
-
|
|
56
|
-
```
|
|
57
|
-
/app/<route>.tsx route — one-line re-export
|
|
58
|
-
/app/_layout.tsx Stack (for native large titles) or NativeTabs (for tabs)
|
|
59
|
-
/screens/<Name>.tsx screen, PascalCase, default export
|
|
60
|
-
/components/shared/ designer-created custom components
|
|
61
|
-
/components/proto/ Prototo primitives — read-only
|
|
62
|
-
/assets/lottie/ designer-supplied Lottie JSON files (loaded by the Lottie component)
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
A new screen `screens/Settings.tsx` needs:
|
|
66
|
-
- `app/settings.tsx` re-exporting it (`import Settings from '../screens/Settings'; export default function SettingsRoute() { return <Settings />; }`)
|
|
67
|
-
- A title set in `app/_layout.tsx`: `<Stack.Screen name="settings" options={{ title: 'Settings' }} />`
|
|
68
|
-
|
|
69
|
-
**Tabs (NativeTabs)** — exact shape for this project's pinned `expo-router` (the flat `Icon`/`Label` imports you may know do NOT exist here; they're nested under `Trigger`):
|
|
70
|
-
|
|
71
|
-
```tsx
|
|
72
|
-
import { NativeTabs } from 'expo-router/unstable-native-tabs';
|
|
73
|
-
|
|
74
|
-
export default function Layout() {
|
|
75
|
-
return (
|
|
76
|
-
<NativeTabs>
|
|
77
|
-
<NativeTabs.Trigger name="index">
|
|
78
|
-
<NativeTabs.Trigger.Icon sf="house.fill" />
|
|
79
|
-
<NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
|
|
80
|
-
</NativeTabs.Trigger>
|
|
81
|
-
<NativeTabs.Trigger name="profile">
|
|
82
|
-
<NativeTabs.Trigger.Icon sf="person.fill" />
|
|
83
|
-
<NativeTabs.Trigger.Label>Profile</NativeTabs.Trigger.Label>
|
|
84
|
-
</NativeTabs.Trigger>
|
|
85
|
-
</NativeTabs>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
**Root-layout changes need a cold restart.** Swapping the navigator in `app/_layout.tsx` (Stack ↔ NativeTabs) does NOT apply via Fast Refresh, and the stale UI in a screenshot can look plausible. After editing `_layout.tsx`, call the `reload_app` MCP tool, then screenshot.
|
|
91
|
-
- Route filenames are lowercase kebab-case.
|
|
92
|
-
|
|
93
|
-
## DESIGN.md is alive
|
|
94
|
-
|
|
95
|
-
When the designer asks to change colors, typography, spacing, shape, accent, or anything design-systemy, update `DESIGN.md` too. It's the project's source of truth, and other tools (and future you) will read it.
|
|
96
|
-
|
|
97
|
-
When you add a new screen, add a one-line entry to `DESIGN.md`'s Screens section.
|
|
98
|
-
|
|
99
|
-
## One palette, one place
|
|
100
|
-
|
|
101
|
-
The theme colors live in `DESIGN.md` and the proto tokens — read them with `useTheme()` from `../components/proto`. When a design needs custom brand colors, fonts, or constants beyond the theme (e.g. a specific gradient or accent set), define them **once** in a single shared module (`/components/shared/theme.ts`) and import it everywhere that needs them. Never paste the same color/font constants inline into more than one screen — duplicated palettes drift out of sync. If you find a palette already inlined in a screen, lift it into the shared module and import it back.
|
|
102
|
-
|
|
103
|
-
## Light, dark, and accessibility
|
|
104
|
-
|
|
105
|
-
- **Dark mode is automatic.** `useTheme()` returns the right palette for the device's light or dark setting and re-renders when it flips. So use theme colors (`theme.surface.*`, `theme.text.*`, `theme.border.*`) instead of hardcoded hex/rgba, and the screen adapts for free. Custom brand colors in the shared theme module won't auto-adapt — if a design needs a dark variant of a brand color, define both and pick with the same light/dark signal. To pin a scheme for a prototype, set `colorScheme: 'light' | 'dark'` in `proto.config.js`.
|
|
106
|
-
- **Text already scales** with the device's text-size setting (iOS Dynamic Type). Don't disable it. Lay out so text can grow a couple of steps without clipping — avoid fixed heights on text containers.
|
|
107
|
-
- **Accessibility floors** live in `a11y` from `../components/proto`: tap targets ≥ `a11y.minTapTarget` (44pt), text contrast ≥ `a11y.minTextContrast`. After visual changes, the `proto shot` check (below) is where you confirm contrast holds — in both light and dark.
|
|
108
|
-
|
|
109
|
-
## Check your work visually
|
|
110
|
-
|
|
111
|
-
You can't see the Simulator by default, so after any visual change, look at it:
|
|
112
|
-
|
|
113
|
-
1. Run `proto shot` — it captures the running Simulator to `.proto/last-shot.png`.
|
|
114
|
-
2. Read that image and inspect it for real defects: overlapping elements, low contrast / unreadable text, clipping, cramped or uneven spacing, off-center layout, wrong colors.
|
|
115
|
-
3. If something's off, fix it and capture again. Iterate until it looks right — don't make the designer be your QA.
|
|
116
|
-
|
|
117
|
-
Do this especially after layout, color, typography, or spacing changes. If `proto shot` reports no preview is running, the designer needs to run `proto start` first.
|
|
118
|
-
|
|
119
|
-
## Proto MCP tools
|
|
120
|
-
|
|
121
|
-
When the designer runs `proto start`, a local MCP server (`prototo`) connects automatically — no setup. It gives you four tools that close the feedback loop, so you can see what you built instead of asking the designer to relay it.
|
|
122
|
-
|
|
123
|
-
**At the start of any fix session** (the designer says something is broken, red, or not working):
|
|
124
|
-
|
|
125
|
-
1. Call `get_metro_errors` first — it returns the current build failures and runtime crashes from the running prototype, with the raw error text and the screen involved. Don't ask the designer to paste or describe an error before checking it.
|
|
126
|
-
|
|
127
|
-
**After every screen write:**
|
|
128
|
-
|
|
129
|
-
1. Call `compile_check` with the screen name — it type-checks the project and reports any problems in plain language. Fix anything it surfaces before moving on.
|
|
130
|
-
2. Call `get_simulator_screenshot` — it returns what the prototype actually renders right now. Inspect it for the same defects as above.
|
|
131
|
-
|
|
132
|
-
**After editing `app/_layout.tsx` or any navigator:** call `reload_app` — root-layout changes don't Fast-Refresh, and a stale screenshot looks plausible.
|
|
133
|
-
|
|
134
|
-
Never assume a screen rendered correctly — check the screenshot. Never ask the designer to describe an error you can catch with `get_metro_errors` or `compile_check`. If a tool says the Simulator isn't running (or Metro reports clean while the designer still sees an error), the designer needs to run `proto start` first.
|
|
135
|
-
|
|
136
|
-
(`get_simulator_screenshot` is the automated form of the `proto shot` loop above — prefer the tool when it's available.)
|
|
137
|
-
|
|
138
|
-
## Mock vs real data
|
|
139
|
-
|
|
140
|
-
When a screen shows placeholder numbers that aren't yet wired to a real source, wrap them in `mock()` from `../components/proto` — `const conditions = mock({ wave: '0.8m' })`. It returns the value unchanged, so nothing breaks; it just makes stubbed data obvious and greppable so fake numbers never ship believing they're real. When you wire the value to a live source (a `fetch`), drop the `mock()` wrapper. (Don't use code comments to mark mock data — generated screens stay comment-free.)
|
|
141
|
-
|
|
142
|
-
### Making it real
|
|
143
|
-
|
|
144
|
-
When the designer says "use real data", follow this shape so every screen handles loading and failure the same way:
|
|
145
|
-
|
|
146
|
-
1. Keep the `mock()` value as the **starting state** — it's what shows before the fetch resolves and if the network fails. Drop the `mock()` wrapper from the live value once wired.
|
|
147
|
-
2. Fetch in an effect, guarding against the screen unmounting:
|
|
148
|
-
|
|
149
|
-
```tsx
|
|
150
|
-
const [data, setData] = useState(FALLBACK);
|
|
151
|
-
const [loading, setLoading] = useState(true);
|
|
152
|
-
useEffect(() => {
|
|
153
|
-
let alive = true;
|
|
154
|
-
fetch(URL)
|
|
155
|
-
.then((r) => r.json())
|
|
156
|
-
.then((json) => { if (alive) setData(shape(json)); })
|
|
157
|
-
.catch(() => {})
|
|
158
|
-
.finally(() => { if (alive) setLoading(false); });
|
|
159
|
-
return () => { alive = false; };
|
|
160
|
-
}, []);
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
3. While `loading`, show a skeleton or the fallback — never a blank screen. On error, keep the fallback (the `.catch` above already does this); don't surface a raw error to the designer.
|
|
164
|
-
4. Put fetch + shaping logic in `/components/shared/<name>Data.ts`, not inline in the screen.
|
|
165
|
-
|
|
166
|
-
**Keyless APIs** (no key, CORS-open, good for prototypes): Open-Meteo (weather/marine/air), REST Countries, Open Library, PokéAPI, Art Institute of Chicago, TheMealDB, Wikipedia REST. Prefer these so "make it real" stays a one-prompt step. If a source needs a key, tell the designer that key goes in `proto.config.js`, nowhere else.
|
|
167
|
-
|
|
168
|
-
## Sharing your prototype
|
|
169
|
-
|
|
170
|
-
Run `proto share` to publish the prototype and get a `prototo.app/p/<token>` link. Anyone can open it in a browser — no install — and see the **real prototype running**, streamed live from a cloud iPhone. Full fidelity: gestures, `motion`/`gestures`/`canvas`/`svg`/`lottie`, live data, and custom logic all show up in the shared link exactly as they run on the designer's device. There's nothing to dumb down — build whatever the designer asks for and it all shares.
|
|
171
|
-
|
|
172
|
-
Two limits worth knowing: the prototype must be built on the **current Prototo** (an older one won't stream — re-scaffold if prompted), and **real-hardware features** (camera, GPS) are simulated on the cloud device rather than reading a real sensor.
|
|
173
|
-
|
|
174
|
-
## When modifying
|
|
175
|
-
|
|
176
|
-
Read the file first, then make precise, targeted edits to the parts that change. Keep edits scoped — don't rewrite a whole file when a few lines change.
|
|
177
|
-
|
|
178
|
-
## Avoid
|
|
179
|
-
|
|
180
|
-
- Custom tab bars — `expo-router/unstable-native-tabs` is strictly better (real Liquid Glass, real SF Symbols, system blur).
|
|
181
|
-
- SF Symbol private-use Unicode codepoints (`''`, `''`) in plain Text — they don't render. Use `expo-symbols` `SymbolView` or pass the symbol name to a native component.
|
|
182
|
-
- Editing `/components/proto/`, `.proto/`, `app.config.js`, `babel.config.js`, `metro.config.js`.
|
|
183
|
-
- Telling the designer to open or edit a file manually. They prompt; you write.
|
|
1
|
+
@AGENTS.md
|
package/template/README.md
CHANGED
|
@@ -5,5 +5,5 @@ Built with Prototo.
|
|
|
5
5
|
Before running `proto start` on iPhone, install Prototo from the App Store.
|
|
6
6
|
|
|
7
7
|
Run `proto start` to preview on the iOS Simulator (auto) or your iPhone (scan QR).
|
|
8
|
-
Run `proto share` to
|
|
9
|
-
|
|
8
|
+
Run `proto share` to publish a link anyone can open on their iPhone with the free Prototo app.
|
|
9
|
+
To add a screen, describe it to your coding agent in plain language.
|
|
@@ -1,17 +1,99 @@
|
|
|
1
1
|
// Proto-managed. Draws a round dot wherever you touch WHILE `proto record` is
|
|
2
2
|
// running, so taps are visible in the recorded video (the recorder captures
|
|
3
|
-
// only what the app itself renders).
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
// only what the app itself renders). It also answers Prototo Desktop's
|
|
4
|
+
// point-and-edit: a tapped preview element is resolved to the screen file and
|
|
5
|
+
// line that renders it. And its screen-flow view: a clicked screen opens here. Dev-only twice over: everything is gated on __DEV__,
|
|
6
|
+
// and published shares are production bundles where __DEV__ is false —
|
|
7
|
+
// stakeholders can never see it. Safe to leave alone.
|
|
8
|
+
import { type ReactNode, useEffect, useRef, useState } from 'react';
|
|
9
|
+
import { Animated, Dimensions, type GestureResponderEvent, View } from 'react-native';
|
|
8
10
|
|
|
9
|
-
const POLL_MS =
|
|
11
|
+
const POLL_MS = 500;
|
|
10
12
|
const DOT = 36;
|
|
11
13
|
// A quick tap must linger long enough to be readable in the video.
|
|
12
14
|
const FADE_MS = 350;
|
|
13
15
|
|
|
14
16
|
type Dot = { id: number; x: number; y: number };
|
|
17
|
+
type InspectRequest = { id: number; x: number; y: number };
|
|
18
|
+
type NavigateRequest = { id: number; path: string };
|
|
19
|
+
|
|
20
|
+
// Flow view: open the route the desktop asked for, then confirm. expo-router is
|
|
21
|
+
// required at run time (every Prototo project has it; this file's own package
|
|
22
|
+
// doesn't), so a missing router is just a "no".
|
|
23
|
+
function navigateTo(req: NavigateRequest) {
|
|
24
|
+
let ok = false;
|
|
25
|
+
try {
|
|
26
|
+
const { router } = require('expo-router') as { router: { navigate: (href: string) => void } };
|
|
27
|
+
router.navigate(req.path);
|
|
28
|
+
ok = true;
|
|
29
|
+
} catch {
|
|
30
|
+
// unknown route or no router — the desktop already pasted the file
|
|
31
|
+
}
|
|
32
|
+
fetch('http://127.0.0.1:3001/navigate/result', {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
body: JSON.stringify({ id: req.id, ok }),
|
|
36
|
+
}).catch(() => {});
|
|
37
|
+
}
|
|
38
|
+
type DebugFiber = { _debugStack?: { stack?: unknown }; _debugOwner?: DebugFiber | null };
|
|
39
|
+
|
|
40
|
+
// Point-and-edit. React keeps, in dev, the JSX call site of every element on
|
|
41
|
+
// its fiber (`_debugStack`); walking the owner chain from the tapped view gives
|
|
42
|
+
// the designer's screen file first. `proto start` symbolicates the stacks. The
|
|
43
|
+
// renderer's inspector is reached through the DevTools hook — the same thing
|
|
44
|
+
// React Native's own element inspector does, without the deprecated deep import.
|
|
45
|
+
type InspectorData = { hierarchy?: unknown[]; closestInstance?: DebugFiber | null };
|
|
46
|
+
type Renderer = {
|
|
47
|
+
rendererConfig?: {
|
|
48
|
+
getInspectorDataForViewAtPoint?: (
|
|
49
|
+
view: View | null,
|
|
50
|
+
x: number,
|
|
51
|
+
y: number,
|
|
52
|
+
cb: (data: InspectorData) => boolean,
|
|
53
|
+
) => void;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function inspectAt(view: View | null, req: InspectRequest) {
|
|
58
|
+
let answered = false;
|
|
59
|
+
const post = (stacks: string[]) => {
|
|
60
|
+
if (answered) return;
|
|
61
|
+
answered = true;
|
|
62
|
+
fetch('http://127.0.0.1:3001/inspect/result', {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'Content-Type': 'application/json' },
|
|
65
|
+
body: JSON.stringify({ id: req.id, stacks }),
|
|
66
|
+
}).catch(() => {});
|
|
67
|
+
};
|
|
68
|
+
try {
|
|
69
|
+
const hook = (
|
|
70
|
+
globalThis as { __REACT_DEVTOOLS_GLOBAL_HOOK__?: { renderers: Map<number, Renderer> } }
|
|
71
|
+
).__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
72
|
+
const { width, height } = Dimensions.get('window');
|
|
73
|
+
for (const renderer of hook?.renderers.values() ?? []) {
|
|
74
|
+
renderer.rendererConfig?.getInspectorDataForViewAtPoint?.(
|
|
75
|
+
view,
|
|
76
|
+
req.x * width,
|
|
77
|
+
req.y * height,
|
|
78
|
+
(data) => {
|
|
79
|
+
if (!data.hierarchy?.length) return false;
|
|
80
|
+
const stacks: string[] = [];
|
|
81
|
+
let fiber: DebugFiber | null | undefined = data.closestInstance;
|
|
82
|
+
for (let i = 0; fiber && i < 12; i++) {
|
|
83
|
+
const stack = fiber._debugStack?.stack;
|
|
84
|
+
if (typeof stack === 'string') stacks.push(stack);
|
|
85
|
+
fiber = fiber._debugOwner;
|
|
86
|
+
}
|
|
87
|
+
post(stacks);
|
|
88
|
+
return true;
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// no renderer / not a dev build — the CLI times out and the desktop
|
|
94
|
+
// falls back to a label-only reference
|
|
95
|
+
}
|
|
96
|
+
}
|
|
15
97
|
type FadingDot = { key: number; x: number; y: number; opacity: Animated.Value };
|
|
16
98
|
|
|
17
99
|
// Brand-pink fill + white rim: reads on light AND dark content (a white or
|
|
@@ -32,6 +114,9 @@ export default function TouchDots({ children }: { children: ReactNode }) {
|
|
|
32
114
|
const [fading, setFading] = useState<FadingDot[]>([]);
|
|
33
115
|
const dotsRef = useRef<Dot[]>([]);
|
|
34
116
|
const fadeSeq = useRef(0);
|
|
117
|
+
const rootRef = useRef<View>(null);
|
|
118
|
+
const inspected = useRef(0);
|
|
119
|
+
const navigated = useRef(0);
|
|
35
120
|
|
|
36
121
|
// Poll `proto start`'s local server for the record flag (the Simulator
|
|
37
122
|
// shares the host loopback). Any failure just means "not recording".
|
|
@@ -41,8 +126,21 @@ export default function TouchDots({ children }: { children: ReactNode }) {
|
|
|
41
126
|
const tick = async () => {
|
|
42
127
|
try {
|
|
43
128
|
const res = await fetch('http://127.0.0.1:3001/recording');
|
|
44
|
-
const body = (await res.json()) as {
|
|
45
|
-
|
|
129
|
+
const body = (await res.json()) as {
|
|
130
|
+
recording?: boolean;
|
|
131
|
+
inspect?: InspectRequest;
|
|
132
|
+
navigate?: NavigateRequest;
|
|
133
|
+
};
|
|
134
|
+
if (!alive) return;
|
|
135
|
+
setRecording(body.recording === true);
|
|
136
|
+
if (body.inspect && body.inspect.id !== inspected.current) {
|
|
137
|
+
inspected.current = body.inspect.id;
|
|
138
|
+
inspectAt(rootRef.current, body.inspect);
|
|
139
|
+
}
|
|
140
|
+
if (body.navigate && body.navigate.id !== navigated.current) {
|
|
141
|
+
navigated.current = body.navigate.id;
|
|
142
|
+
navigateTo(body.navigate);
|
|
143
|
+
}
|
|
46
144
|
} catch {
|
|
47
145
|
if (alive) setRecording(false);
|
|
48
146
|
}
|
|
@@ -98,6 +196,7 @@ export default function TouchDots({ children }: { children: ReactNode }) {
|
|
|
98
196
|
// Plain touch events bubble to this wrapper no matter which child is the
|
|
99
197
|
// responder, so observing them here never steals the prototype's gestures.
|
|
100
198
|
<View
|
|
199
|
+
ref={rootRef}
|
|
101
200
|
style={{ flex: 1 }}
|
|
102
201
|
onTouchStart={recording ? onMove : undefined}
|
|
103
202
|
onTouchMove={recording ? onMove : undefined}
|
package/template/package.json
CHANGED
|
@@ -9,49 +9,50 @@
|
|
|
9
9
|
"proto": "proto"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@expo/ui": "~
|
|
12
|
+
"@expo/ui": "~57.0.18",
|
|
13
13
|
"@react-native-async-storage/async-storage": "2.2.0",
|
|
14
14
|
"@shopify/flash-list": "2.0.2",
|
|
15
15
|
"@shopify/react-native-skia": "2.6.2",
|
|
16
|
-
"expo": "~
|
|
17
|
-
"expo-asset": "~
|
|
18
|
-
"expo-audio": "~
|
|
19
|
-
"expo-camera": "~
|
|
20
|
-
"expo-clipboard": "~
|
|
21
|
-
"expo-constants": "~
|
|
22
|
-
"expo-dev-client": "~
|
|
23
|
-
"expo-font": "~
|
|
24
|
-
"expo-glass-effect": "~
|
|
25
|
-
"expo-haptics": "~
|
|
26
|
-
"expo-image": "~
|
|
27
|
-
"expo-image-picker": "~
|
|
28
|
-
"expo-linear-gradient": "~
|
|
29
|
-
"expo-linking": "~
|
|
30
|
-
"expo-location": "~
|
|
31
|
-
"expo-maps": "~
|
|
32
|
-
"expo-router": "~
|
|
33
|
-
"expo-screen-orientation": "~
|
|
34
|
-
"expo-
|
|
35
|
-
"expo-
|
|
36
|
-
"expo-
|
|
16
|
+
"expo": "~57.0.0",
|
|
17
|
+
"expo-asset": "~57.0.17",
|
|
18
|
+
"expo-audio": "~57.0.5",
|
|
19
|
+
"expo-camera": "~57.0.5",
|
|
20
|
+
"expo-clipboard": "~57.0.2",
|
|
21
|
+
"expo-constants": "~57.0.18",
|
|
22
|
+
"expo-dev-client": "~57.0.19",
|
|
23
|
+
"expo-font": "~57.0.4",
|
|
24
|
+
"expo-glass-effect": "~57.0.3",
|
|
25
|
+
"expo-haptics": "~57.0.3",
|
|
26
|
+
"expo-image": "~57.0.5",
|
|
27
|
+
"expo-image-picker": "~57.0.18",
|
|
28
|
+
"expo-linear-gradient": "~57.0.2",
|
|
29
|
+
"expo-linking": "~57.0.10",
|
|
30
|
+
"expo-location": "~57.0.18",
|
|
31
|
+
"expo-maps": "~57.0.3",
|
|
32
|
+
"expo-router": "~57.0.21",
|
|
33
|
+
"expo-screen-orientation": "~57.0.2",
|
|
34
|
+
"expo-sensors": "~57.0.3",
|
|
35
|
+
"expo-status-bar": "~57.0.1",
|
|
36
|
+
"expo-symbols": "~57.0.3",
|
|
37
|
+
"expo-video": "~57.0.4",
|
|
37
38
|
"lottie-react-native": "~7.3.4",
|
|
38
39
|
"react": "19.2.3",
|
|
39
40
|
"react-dom": "19.2.3",
|
|
40
|
-
"react-native": "0.
|
|
41
|
+
"react-native": "0.86.3",
|
|
41
42
|
"react-native-ease": "^0.7.2",
|
|
42
|
-
"react-native-gesture-handler": "~2.
|
|
43
|
-
"react-native-reanimated": "4.
|
|
43
|
+
"react-native-gesture-handler": "~2.32.0",
|
|
44
|
+
"react-native-reanimated": "4.5.1",
|
|
44
45
|
"react-native-safe-area-context": "~5.7.0",
|
|
45
|
-
"react-native-screens": "4.
|
|
46
|
+
"react-native-screens": "4.26.2",
|
|
46
47
|
"react-native-svg": "15.15.4",
|
|
47
48
|
"react-native-webview": "13.16.1",
|
|
48
|
-
"react-native-worklets": "0.
|
|
49
|
+
"react-native-worklets": "0.10.1"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"@babel/core": "^7.24.0",
|
|
52
|
-
"@sherizan/proto-cli": "^0.
|
|
53
|
+
"@sherizan/proto-cli": "^0.8.0",
|
|
53
54
|
"@types/react": "~19.2.15",
|
|
54
|
-
"babel-preset-expo": "~
|
|
55
|
+
"babel-preset-expo": "~57.0.0",
|
|
55
56
|
"react-native-svg-transformer": "^1.5.0",
|
|
56
57
|
"typescript": "~6.0.3"
|
|
57
58
|
}
|
|
@@ -14,8 +14,8 @@ import { Screen, Stack, Text, Card, Divider, Lottie } from '../components/proto'
|
|
|
14
14
|
|
|
15
15
|
// Prototo Desktop sets EXPO_PUBLIC_PROTO_DESKTOP=1 when it runs `proto start`
|
|
16
16
|
// (Metro inlines it at bundle time). In the desktop the terminal sits beside
|
|
17
|
-
// this preview with
|
|
18
|
-
// reaches the Mac, so the copy and
|
|
17
|
+
// this preview with the coding agent already running, and the simulator
|
|
18
|
+
// clipboard never reaches the Mac, so the copy and Copy affordance both change.
|
|
19
19
|
const IN_DESKTOP = process.env.EXPO_PUBLIC_PROTO_DESKTOP === '1';
|
|
20
20
|
|
|
21
21
|
const EXAMPLES = [
|
|
@@ -98,7 +98,7 @@ export default function Home() {
|
|
|
98
98
|
<Text size="body" color="secondary">
|
|
99
99
|
{IN_DESKTOP
|
|
100
100
|
? 'Type your first prompt in the terminal beside this preview, and watch it appear here.'
|
|
101
|
-
: `In a terminal: cd {{APP_NAME}} && claude. Then paste a prompt below.`}
|
|
101
|
+
: `In a terminal: cd {{APP_NAME}} && claude (or codex). Then paste a prompt below.`}
|
|
102
102
|
</Text>
|
|
103
103
|
</Stack>
|
|
104
104
|
</Card>
|