@tapthrough/expo 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +105 -2
- package/app.plugin.d.ts +37 -0
- package/app.plugin.js +79 -0
- package/dist/Annotator.d.ts +14 -0
- package/dist/Annotator.d.ts.map +1 -0
- package/dist/Annotator.js +160 -0
- package/dist/Annotator.js.map +1 -0
- package/dist/Bubble.d.ts +27 -0
- package/dist/Bubble.d.ts.map +1 -0
- package/dist/Bubble.js +234 -0
- package/dist/Bubble.js.map +1 -0
- package/dist/Feedback.d.ts +14 -0
- package/dist/Feedback.d.ts.map +1 -0
- package/dist/Feedback.js +977 -0
- package/dist/Feedback.js.map +1 -0
- package/dist/Overlay.d.ts +13 -0
- package/dist/Overlay.d.ts.map +1 -0
- package/dist/Overlay.js +44 -0
- package/dist/Overlay.js.map +1 -0
- package/dist/Popover.d.ts +98 -0
- package/dist/Popover.d.ts.map +1 -0
- package/dist/Popover.js +531 -0
- package/dist/Popover.js.map +1 -0
- package/dist/Tapthrough.d.ts +27 -0
- package/dist/Tapthrough.d.ts.map +1 -0
- package/dist/Tapthrough.js +111 -0
- package/dist/Tapthrough.js.map +1 -0
- package/dist/core/annotations.d.ts +63 -0
- package/dist/core/annotations.d.ts.map +1 -0
- package/dist/core/annotations.js +76 -0
- package/dist/core/annotations.js.map +1 -0
- package/dist/core/bubble.d.ts +21 -0
- package/dist/core/bubble.d.ts.map +1 -0
- package/dist/core/bubble.js +25 -0
- package/dist/core/bubble.js.map +1 -0
- package/dist/core/controller.d.ts +46 -0
- package/dist/core/controller.d.ts.map +1 -0
- package/dist/core/controller.js +103 -0
- package/dist/core/controller.js.map +1 -0
- package/dist/core/diagnostics.d.ts +47 -0
- package/dist/core/diagnostics.d.ts.map +1 -0
- package/dist/core/diagnostics.js +101 -0
- package/dist/core/diagnostics.js.map +1 -0
- package/dist/core/feedback.d.ts +302 -0
- package/dist/core/feedback.d.ts.map +1 -0
- package/dist/core/feedback.js +345 -0
- package/dist/core/feedback.js.map +1 -0
- package/dist/core/flags.d.ts +13 -0
- package/dist/core/flags.d.ts.map +1 -0
- package/dist/core/flags.js +25 -0
- package/dist/core/flags.js.map +1 -0
- package/dist/core/ideas.d.ts +166 -0
- package/dist/core/ideas.d.ts.map +1 -0
- package/dist/core/ideas.js +159 -0
- package/dist/core/ideas.js.map +1 -0
- package/dist/core/launch.d.ts +43 -0
- package/dist/core/launch.d.ts.map +1 -0
- package/dist/core/launch.js +81 -0
- package/dist/core/launch.js.map +1 -0
- package/dist/core/reset.d.ts +17 -0
- package/dist/core/reset.d.ts.map +1 -0
- package/dist/core/reset.js +19 -0
- package/dist/core/reset.js.map +1 -0
- package/dist/core/review.d.ts +49 -0
- package/dist/core/review.d.ts.map +1 -0
- package/dist/core/review.js +92 -0
- package/dist/core/review.js.map +1 -0
- package/dist/core/url.d.ts +9 -0
- package/dist/core/url.d.ts.map +1 -0
- package/dist/core/url.js +23 -0
- package/dist/core/url.js.map +1 -0
- package/dist/hooks.d.ts +6 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +13 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/native-intent.d.ts +18 -0
- package/dist/native-intent.d.ts.map +1 -0
- package/dist/native-intent.js +32 -0
- package/dist/native-intent.js.map +1 -0
- package/dist/runtime.d.ts +24 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +17 -0
- package/dist/runtime.js.map +1 -0
- package/dist/screenshot.d.ts +7 -0
- package/dist/screenshot.d.ts.map +1 -0
- package/dist/screenshot.js +48 -0
- package/dist/screenshot.js.map +1 -0
- package/dist/supabase.d.ts +20 -0
- package/dist/supabase.d.ts.map +1 -0
- package/dist/supabase.js +18 -0
- package/dist/supabase.js.map +1 -0
- package/dist/ui/native.d.ts +34 -0
- package/dist/ui/native.d.ts.map +1 -0
- package/dist/ui/native.js +76 -0
- package/dist/ui/native.js.map +1 -0
- package/dist/ui/parts.d.ts +62 -0
- package/dist/ui/parts.d.ts.map +1 -0
- package/dist/ui/parts.js +110 -0
- package/dist/ui/parts.js.map +1 -0
- package/dist/ui/theme.d.ts +90 -0
- package/dist/ui/theme.d.ts.map +1 -0
- package/dist/ui/theme.js +61 -0
- package/dist/ui/theme.js.map +1 -0
- package/package.json +82 -3
- package/src/Annotator.tsx +279 -0
- package/src/Bubble.tsx +331 -0
- package/src/Feedback.tsx +1739 -0
- package/src/Overlay.tsx +61 -0
- package/src/Popover.tsx +1393 -0
- package/src/Tapthrough.tsx +171 -0
- package/src/core/annotations.ts +116 -0
- package/src/core/bubble.ts +45 -0
- package/src/core/controller.ts +119 -0
- package/src/core/diagnostics.ts +128 -0
- package/src/core/feedback.ts +589 -0
- package/src/core/flags.ts +31 -0
- package/src/core/ideas.ts +337 -0
- package/src/core/launch.ts +119 -0
- package/src/core/reset.ts +28 -0
- package/src/core/review.ts +148 -0
- package/src/core/url.ts +21 -0
- package/src/hooks.ts +15 -0
- package/src/index.ts +8 -0
- package/src/native-intent.ts +36 -0
- package/src/runtime.ts +30 -0
- package/src/screenshot.ts +64 -0
- package/src/supabase.ts +27 -0
- package/src/ui/native.ts +116 -0
- package/src/ui/parts.tsx +249 -0
- package/src/ui/theme.ts +75 -0
package/src/Bubble.tsx
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
ActivityIndicator,
|
|
4
|
+
Animated,
|
|
5
|
+
PanResponder,
|
|
6
|
+
Platform,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
Text,
|
|
9
|
+
useWindowDimensions,
|
|
10
|
+
View,
|
|
11
|
+
} from 'react-native'
|
|
12
|
+
import {
|
|
13
|
+
type Bounds,
|
|
14
|
+
BUBBLE_SIZE,
|
|
15
|
+
defaultPosition,
|
|
16
|
+
isTap,
|
|
17
|
+
type Point,
|
|
18
|
+
snapToEdge,
|
|
19
|
+
} from './core/bubble.js'
|
|
20
|
+
import type { Badge } from './core/feedback.js'
|
|
21
|
+
import { haptics, loadGlass } from './ui/native.js'
|
|
22
|
+
import { Icon } from './ui/parts.js'
|
|
23
|
+
|
|
24
|
+
/** Where the reviewer last left it, for the rest of the session. */
|
|
25
|
+
let rememberedPosition: Point | null = null
|
|
26
|
+
|
|
27
|
+
const IDLE_OPACITY = 0.55
|
|
28
|
+
const IDLE_AFTER_MS = 2500
|
|
29
|
+
const PRESSED_SCALE = 0.9
|
|
30
|
+
|
|
31
|
+
/** The screen, less conservative insets for the status bar and home indicator. */
|
|
32
|
+
export function screenBounds(width: number, height: number): Bounds {
|
|
33
|
+
// Conservative insets, so the SDK doesn't need a safe-area dependency.
|
|
34
|
+
return {
|
|
35
|
+
width,
|
|
36
|
+
height,
|
|
37
|
+
insetTop: Platform.OS === 'ios' ? 60 : 32,
|
|
38
|
+
insetBottom: Platform.OS === 'ios' ? 34 : 24,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A small, draggable Review button. Drag it anywhere; it snaps to the nearer
|
|
44
|
+
* side. It fades while idle so it stays out of the way of the app under test.
|
|
45
|
+
*/
|
|
46
|
+
export function Bubble({
|
|
47
|
+
onPress,
|
|
48
|
+
badge,
|
|
49
|
+
busy,
|
|
50
|
+
active = false,
|
|
51
|
+
onRest,
|
|
52
|
+
}: {
|
|
53
|
+
onPress: () => void
|
|
54
|
+
badge: Badge | null
|
|
55
|
+
busy: boolean
|
|
56
|
+
/** The popover is open: show ✕, stay fully visible and hide the badge. */
|
|
57
|
+
active?: boolean
|
|
58
|
+
/** Where the button settles, so the popover can open beside it. */
|
|
59
|
+
onRest?: (at: Point) => void
|
|
60
|
+
}) {
|
|
61
|
+
const { width, height } = useWindowDimensions()
|
|
62
|
+
const area = screenBounds(width, height)
|
|
63
|
+
const start = rememberedPosition ? snapToEdge(rememberedPosition, area) : defaultPosition(area)
|
|
64
|
+
const position = useRef(new Animated.ValueXY(start)).current
|
|
65
|
+
const opacity = useRef(new Animated.Value(1)).current
|
|
66
|
+
const scale = useRef(new Animated.Value(1)).current
|
|
67
|
+
const GlassView = loadGlass()?.GlassView
|
|
68
|
+
const state = useRef({
|
|
69
|
+
at: start,
|
|
70
|
+
area,
|
|
71
|
+
onPress,
|
|
72
|
+
idle: undefined as ReturnType<typeof setTimeout> | undefined,
|
|
73
|
+
active,
|
|
74
|
+
onRest,
|
|
75
|
+
})
|
|
76
|
+
state.current.area = area
|
|
77
|
+
state.current.onPress = onPress
|
|
78
|
+
state.current.active = active
|
|
79
|
+
state.current.onRest = onRest
|
|
80
|
+
|
|
81
|
+
// Report the starting spot once.
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
state.current.onRest?.(state.current.at)
|
|
84
|
+
}, [])
|
|
85
|
+
|
|
86
|
+
// Both values drive the same view, so both stay on the JS driver.
|
|
87
|
+
function wake() {
|
|
88
|
+
clearTimeout(state.current.idle)
|
|
89
|
+
Animated.timing(opacity, { toValue: 1, duration: 120, useNativeDriver: false }).start()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function press(down: boolean) {
|
|
93
|
+
Animated.spring(scale, {
|
|
94
|
+
toValue: down ? PRESSED_SCALE : 1,
|
|
95
|
+
useNativeDriver: false,
|
|
96
|
+
friction: 6,
|
|
97
|
+
tension: 300,
|
|
98
|
+
}).start()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sleepSoon() {
|
|
102
|
+
clearTimeout(state.current.idle)
|
|
103
|
+
// Glass is already see-through, and fading it breaks the effect.
|
|
104
|
+
if (GlassView || state.current.active) return
|
|
105
|
+
state.current.idle = setTimeout(() => {
|
|
106
|
+
Animated.timing(opacity, {
|
|
107
|
+
toValue: IDLE_OPACITY,
|
|
108
|
+
duration: 400,
|
|
109
|
+
useNativeDriver: false,
|
|
110
|
+
}).start()
|
|
111
|
+
}, IDLE_AFTER_MS)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Fully visible when something new shows up or the popover opens, then fade again.
|
|
115
|
+
const badgeKey = `${active}:${badge ? `${badge.kind}:${'count' in badge ? badge.count : ''}` : ''}`
|
|
116
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: wakes on badge changes only
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
wake()
|
|
119
|
+
sleepSoon()
|
|
120
|
+
return () => clearTimeout(state.current.idle)
|
|
121
|
+
}, [badgeKey])
|
|
122
|
+
|
|
123
|
+
// Rotation or a resized window: keep it on screen and on an edge.
|
|
124
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: reruns on size changes only
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
const next = snapToEdge(state.current.at, area)
|
|
127
|
+
state.current.at = next
|
|
128
|
+
position.setValue(next)
|
|
129
|
+
state.current.onRest?.(next)
|
|
130
|
+
}, [area.width, area.height])
|
|
131
|
+
|
|
132
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: reads current values through refs
|
|
133
|
+
const responder = useMemo(
|
|
134
|
+
() =>
|
|
135
|
+
PanResponder.create({
|
|
136
|
+
onStartShouldSetPanResponder: () => true,
|
|
137
|
+
onMoveShouldSetPanResponder: () => true,
|
|
138
|
+
onPanResponderTerminationRequest: () => false,
|
|
139
|
+
onPanResponderGrant: () => {
|
|
140
|
+
wake()
|
|
141
|
+
press(true)
|
|
142
|
+
},
|
|
143
|
+
onPanResponderMove: (_event, gesture) => {
|
|
144
|
+
const { at } = state.current
|
|
145
|
+
position.setValue({ x: at.x + gesture.dx, y: at.y + gesture.dy })
|
|
146
|
+
},
|
|
147
|
+
onPanResponderRelease: (_event, gesture) => {
|
|
148
|
+
press(false)
|
|
149
|
+
const { at } = state.current
|
|
150
|
+
if (isTap(gesture.dx, gesture.dy)) {
|
|
151
|
+
position.setValue(at)
|
|
152
|
+
sleepSoon()
|
|
153
|
+
state.current.onPress()
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
const next = snapToEdge(
|
|
157
|
+
{ x: at.x + gesture.dx, y: at.y + gesture.dy },
|
|
158
|
+
state.current.area,
|
|
159
|
+
)
|
|
160
|
+
state.current.at = next
|
|
161
|
+
rememberedPosition = next
|
|
162
|
+
haptics.light()
|
|
163
|
+
state.current.onRest?.(next)
|
|
164
|
+
Animated.spring(position, {
|
|
165
|
+
toValue: next,
|
|
166
|
+
useNativeDriver: false,
|
|
167
|
+
friction: 7,
|
|
168
|
+
tension: 80,
|
|
169
|
+
}).start()
|
|
170
|
+
sleepSoon()
|
|
171
|
+
},
|
|
172
|
+
onPanResponderTerminate: () => {
|
|
173
|
+
press(false)
|
|
174
|
+
position.setValue(state.current.at)
|
|
175
|
+
sleepSoon()
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
[],
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<Animated.View
|
|
183
|
+
{...responder.panHandlers}
|
|
184
|
+
style={[
|
|
185
|
+
styles.bubble,
|
|
186
|
+
GlassView ? null : styles.solid,
|
|
187
|
+
{ opacity, transform: [...position.getTranslateTransform(), { scale }] },
|
|
188
|
+
]}
|
|
189
|
+
accessible
|
|
190
|
+
accessibilityRole="button"
|
|
191
|
+
accessibilityLabel={active ? 'Close review' : accessibilityLabel(badge)}
|
|
192
|
+
accessibilityHint={active ? undefined : 'Shows what to test. Drag to move.'}
|
|
193
|
+
accessibilityActions={[{ name: 'activate' }]}
|
|
194
|
+
onAccessibilityAction={() => onPress()}
|
|
195
|
+
>
|
|
196
|
+
{GlassView ? (
|
|
197
|
+
<GlassView
|
|
198
|
+
style={styles.glass}
|
|
199
|
+
glassEffectStyle="regular"
|
|
200
|
+
tintColor="rgba(14, 22, 32, 0.55)"
|
|
201
|
+
pointerEvents="none"
|
|
202
|
+
/>
|
|
203
|
+
) : null}
|
|
204
|
+
{busy ? (
|
|
205
|
+
<ActivityIndicator color="#ffffff" />
|
|
206
|
+
) : active ? (
|
|
207
|
+
<Icon name="xmark" size={18} color="#ffffff" />
|
|
208
|
+
) : (
|
|
209
|
+
<Mark />
|
|
210
|
+
)}
|
|
211
|
+
{badge && !busy && !active ? <BadgeView badge={badge} /> : null}
|
|
212
|
+
</Animated.View>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function accessibilityLabel(badge: Badge | null) {
|
|
217
|
+
switch (badge?.kind) {
|
|
218
|
+
case 'new-build':
|
|
219
|
+
return 'Review this pull request. A new build is ready.'
|
|
220
|
+
case 'failed':
|
|
221
|
+
return 'Review this pull request. A step failed.'
|
|
222
|
+
case 'remaining':
|
|
223
|
+
return `Review this pull request. ${badge.count} step${badge.count === 1 ? '' : 's'} left.`
|
|
224
|
+
case 'done':
|
|
225
|
+
return 'Review this pull request. Every step passed.'
|
|
226
|
+
case 'idea':
|
|
227
|
+
return 'Review. An idea needs you.'
|
|
228
|
+
default:
|
|
229
|
+
return 'Review this pull request'
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The Tapthrough mark: a phone with a strip of tape across it. */
|
|
234
|
+
function Mark() {
|
|
235
|
+
return (
|
|
236
|
+
<View style={styles.mark}>
|
|
237
|
+
<View style={styles.phone} />
|
|
238
|
+
<View style={styles.tape} />
|
|
239
|
+
</View>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function BadgeView({ badge }: { badge: Badge }) {
|
|
244
|
+
switch (badge.kind) {
|
|
245
|
+
case 'new-build':
|
|
246
|
+
case 'idea':
|
|
247
|
+
return <View style={[styles.badge, styles.dot, { backgroundColor: COLORS.tape }]} />
|
|
248
|
+
case 'failed':
|
|
249
|
+
return (
|
|
250
|
+
<View style={[styles.badge, { backgroundColor: COLORS.fail }]}>
|
|
251
|
+
<Text style={styles.badgeText}>!</Text>
|
|
252
|
+
</View>
|
|
253
|
+
)
|
|
254
|
+
case 'done':
|
|
255
|
+
return (
|
|
256
|
+
<View style={[styles.badge, { backgroundColor: COLORS.pass }]}>
|
|
257
|
+
<Text style={styles.badgeText}>✓</Text>
|
|
258
|
+
</View>
|
|
259
|
+
)
|
|
260
|
+
case 'remaining':
|
|
261
|
+
return (
|
|
262
|
+
<View style={[styles.badge, { backgroundColor: COLORS.count }]}>
|
|
263
|
+
<Text style={styles.badgeText}>{badge.count > 9 ? '9+' : badge.count}</Text>
|
|
264
|
+
</View>
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export const COLORS = {
|
|
270
|
+
ink: '#0e1620',
|
|
271
|
+
tape: '#ecd88e',
|
|
272
|
+
tapeInk: '#2a2414',
|
|
273
|
+
/** Steps left: the iOS system blue, the SDK's one tint. */
|
|
274
|
+
count: '#0A84FF',
|
|
275
|
+
pass: '#30D158',
|
|
276
|
+
fail: '#FF453A',
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const styles = StyleSheet.create({
|
|
280
|
+
bubble: {
|
|
281
|
+
position: 'absolute',
|
|
282
|
+
top: 0,
|
|
283
|
+
left: 0,
|
|
284
|
+
width: BUBBLE_SIZE,
|
|
285
|
+
height: BUBBLE_SIZE,
|
|
286
|
+
borderRadius: BUBBLE_SIZE / 2,
|
|
287
|
+
alignItems: 'center',
|
|
288
|
+
justifyContent: 'center',
|
|
289
|
+
},
|
|
290
|
+
/** Without Liquid Glass: a dark, slightly see-through disc with a soft lift. */
|
|
291
|
+
solid: {
|
|
292
|
+
backgroundColor: 'rgba(18, 24, 33, 0.88)',
|
|
293
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
294
|
+
borderColor: 'rgba(255, 255, 255, 0.22)',
|
|
295
|
+
shadowColor: '#000000',
|
|
296
|
+
shadowOpacity: 0.22,
|
|
297
|
+
shadowRadius: 12,
|
|
298
|
+
shadowOffset: { width: 0, height: 4 },
|
|
299
|
+
elevation: 8,
|
|
300
|
+
},
|
|
301
|
+
glass: { ...StyleSheet.absoluteFill, borderRadius: BUBBLE_SIZE / 2 },
|
|
302
|
+
mark: { width: 24, height: 24, alignItems: 'center', justifyContent: 'center' },
|
|
303
|
+
phone: {
|
|
304
|
+
width: 14,
|
|
305
|
+
height: 21,
|
|
306
|
+
borderRadius: 4,
|
|
307
|
+
borderWidth: 2,
|
|
308
|
+
borderColor: '#ffffff',
|
|
309
|
+
},
|
|
310
|
+
tape: {
|
|
311
|
+
position: 'absolute',
|
|
312
|
+
width: 22,
|
|
313
|
+
height: 6,
|
|
314
|
+
borderRadius: 1,
|
|
315
|
+
backgroundColor: COLORS.tape,
|
|
316
|
+
transform: [{ rotate: '-8deg' }],
|
|
317
|
+
},
|
|
318
|
+
badge: {
|
|
319
|
+
position: 'absolute',
|
|
320
|
+
top: -3,
|
|
321
|
+
right: -3,
|
|
322
|
+
minWidth: 18,
|
|
323
|
+
height: 18,
|
|
324
|
+
paddingHorizontal: 4,
|
|
325
|
+
borderRadius: 9,
|
|
326
|
+
alignItems: 'center',
|
|
327
|
+
justifyContent: 'center',
|
|
328
|
+
},
|
|
329
|
+
dot: { minWidth: 14, width: 14, height: 14, paddingHorizontal: 0, top: -1, right: -1 },
|
|
330
|
+
badgeText: { color: '#ffffff', fontSize: 12, fontWeight: '600' },
|
|
331
|
+
})
|