create-proto 0.7.8 → 0.7.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-proto",
3
- "version": "0.7.8",
3
+ "version": "0.7.9",
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",
@@ -0,0 +1,124 @@
1
+ // Proto-managed. Draws a round dot wherever you touch WHILE `proto record` is
2
+ // running, so taps are visible in the recorded video (the recorder captures
3
+ // only what the app itself renders). Dev-only twice over: everything is gated
4
+ // on __DEV__, and published shares are production bundles where __DEV__ is
5
+ // false — stakeholders can never see it. Safe to leave alone.
6
+ import { useEffect, useRef, useState, type ReactNode } from 'react';
7
+ import { Animated, View, type GestureResponderEvent } from 'react-native';
8
+
9
+ const POLL_MS = 1500;
10
+ const DOT = 36;
11
+ // A quick tap must linger long enough to be readable in the video.
12
+ const FADE_MS = 350;
13
+
14
+ type Dot = { id: number; x: number; y: number };
15
+ type FadingDot = { key: number; x: number; y: number; opacity: Animated.Value };
16
+
17
+ const circle = {
18
+ position: 'absolute' as const,
19
+ width: DOT,
20
+ height: DOT,
21
+ borderRadius: DOT / 2,
22
+ backgroundColor: 'rgba(255,255,255,0.35)',
23
+ borderWidth: 1.5,
24
+ borderColor: 'rgba(255,255,255,0.85)',
25
+ };
26
+
27
+ export default function TouchDots({ children }: { children: ReactNode }) {
28
+ const [recording, setRecording] = useState(false);
29
+ const [dots, setDots] = useState<Dot[]>([]);
30
+ const [fading, setFading] = useState<FadingDot[]>([]);
31
+ const dotsRef = useRef<Dot[]>([]);
32
+ const fadeSeq = useRef(0);
33
+
34
+ // Poll `proto start`'s local server for the record flag (the Simulator
35
+ // shares the host loopback). Any failure just means "not recording".
36
+ useEffect(() => {
37
+ if (!__DEV__) return;
38
+ let alive = true;
39
+ const tick = async () => {
40
+ try {
41
+ const res = await fetch('http://127.0.0.1:3001/recording');
42
+ const body = (await res.json()) as { recording?: boolean };
43
+ if (alive) setRecording(body.recording === true);
44
+ } catch {
45
+ if (alive) setRecording(false);
46
+ }
47
+ };
48
+ void tick();
49
+ const timer = setInterval(() => void tick(), POLL_MS);
50
+ return () => {
51
+ alive = false;
52
+ clearInterval(timer);
53
+ };
54
+ }, []);
55
+
56
+ if (!__DEV__) return <>{children}</>;
57
+
58
+ const readTouches = (evt: GestureResponderEvent): Dot[] =>
59
+ evt.nativeEvent.touches.map((t) => ({
60
+ id: Number(t.identifier),
61
+ x: t.pageX,
62
+ y: t.pageY,
63
+ }));
64
+
65
+ const setBoth = (next: Dot[]) => {
66
+ dotsRef.current = next;
67
+ setDots(next);
68
+ };
69
+
70
+ const onMove = (evt: GestureResponderEvent) => setBoth(readTouches(evt));
71
+
72
+ const onRelease = (evt: GestureResponderEvent) => {
73
+ const remaining = readTouches(evt);
74
+ const remainingIds = new Set(remaining.map((d) => d.id));
75
+ const lifted = dotsRef.current.filter((d) => !remainingIds.has(d.id));
76
+ if (lifted.length) {
77
+ const entries = lifted.map((d) => ({
78
+ key: fadeSeq.current++,
79
+ x: d.x,
80
+ y: d.y,
81
+ opacity: new Animated.Value(1),
82
+ }));
83
+ setFading((prev) => [...prev, ...entries]);
84
+ for (const entry of entries) {
85
+ Animated.timing(entry.opacity, {
86
+ toValue: 0,
87
+ duration: FADE_MS,
88
+ useNativeDriver: true,
89
+ }).start(() => setFading((prev) => prev.filter((f) => f.key !== entry.key)));
90
+ }
91
+ }
92
+ setBoth(remaining);
93
+ };
94
+
95
+ return (
96
+ // Plain touch events bubble to this wrapper no matter which child is the
97
+ // responder, so observing them here never steals the prototype's gestures.
98
+ <View
99
+ style={{ flex: 1 }}
100
+ onTouchStart={recording ? onMove : undefined}
101
+ onTouchMove={recording ? onMove : undefined}
102
+ onTouchEnd={recording ? onRelease : undefined}
103
+ onTouchCancel={recording ? onRelease : undefined}
104
+ >
105
+ {children}
106
+ {recording && (dots.length > 0 || fading.length > 0) && (
107
+ <View
108
+ pointerEvents="none"
109
+ style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}
110
+ >
111
+ {dots.map((d) => (
112
+ <View key={d.id} style={[circle, { left: d.x - DOT / 2, top: d.y - DOT / 2 }]} />
113
+ ))}
114
+ {fading.map((f) => (
115
+ <Animated.View
116
+ key={f.key}
117
+ style={[circle, { left: f.x - DOT / 2, top: f.y - DOT / 2, opacity: f.opacity }]}
118
+ />
119
+ ))}
120
+ </View>
121
+ )}
122
+ </View>
123
+ );
124
+ }