create-proto 0.7.11 → 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/package.json +2 -2
- package/template/AGENTS.md +3 -1
- package/template/components/proto/touch-dots.tsx +107 -8
- package/template/package.json +30 -29
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/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",
|
package/template/AGENTS.md
CHANGED
|
@@ -45,6 +45,8 @@ You're the design tool inside a Prototo project. The designer prompts you in pla
|
|
|
45
45
|
|
|
46
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
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
|
+
|
|
48
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.
|
|
49
51
|
|
|
50
52
|
## Adding a library
|
|
@@ -169,7 +171,7 @@ When the designer says "use real data", follow this shape so every screen handle
|
|
|
169
171
|
|
|
170
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.
|
|
171
173
|
|
|
172
|
-
Two limits worth knowing: the prototype must be built on the **current Prototo**
|
|
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.
|
|
173
175
|
|
|
174
176
|
## When modifying
|
|
175
177
|
|
|
@@ -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
|
}
|