@tapthrough/expo 0.0.0-stage → 0.1.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.
Files changed (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -2
  3. package/app.plugin.d.ts +37 -0
  4. package/app.plugin.js +79 -0
  5. package/dist/Annotator.d.ts +14 -0
  6. package/dist/Annotator.d.ts.map +1 -0
  7. package/dist/Annotator.js +160 -0
  8. package/dist/Annotator.js.map +1 -0
  9. package/dist/Bubble.d.ts +27 -0
  10. package/dist/Bubble.d.ts.map +1 -0
  11. package/dist/Bubble.js +234 -0
  12. package/dist/Bubble.js.map +1 -0
  13. package/dist/Feedback.d.ts +14 -0
  14. package/dist/Feedback.d.ts.map +1 -0
  15. package/dist/Feedback.js +977 -0
  16. package/dist/Feedback.js.map +1 -0
  17. package/dist/Overlay.d.ts +13 -0
  18. package/dist/Overlay.d.ts.map +1 -0
  19. package/dist/Overlay.js +44 -0
  20. package/dist/Overlay.js.map +1 -0
  21. package/dist/Popover.d.ts +98 -0
  22. package/dist/Popover.d.ts.map +1 -0
  23. package/dist/Popover.js +531 -0
  24. package/dist/Popover.js.map +1 -0
  25. package/dist/Tapthrough.d.ts +27 -0
  26. package/dist/Tapthrough.d.ts.map +1 -0
  27. package/dist/Tapthrough.js +111 -0
  28. package/dist/Tapthrough.js.map +1 -0
  29. package/dist/core/annotations.d.ts +63 -0
  30. package/dist/core/annotations.d.ts.map +1 -0
  31. package/dist/core/annotations.js +76 -0
  32. package/dist/core/annotations.js.map +1 -0
  33. package/dist/core/bubble.d.ts +21 -0
  34. package/dist/core/bubble.d.ts.map +1 -0
  35. package/dist/core/bubble.js +25 -0
  36. package/dist/core/bubble.js.map +1 -0
  37. package/dist/core/controller.d.ts +46 -0
  38. package/dist/core/controller.d.ts.map +1 -0
  39. package/dist/core/controller.js +103 -0
  40. package/dist/core/controller.js.map +1 -0
  41. package/dist/core/diagnostics.d.ts +47 -0
  42. package/dist/core/diagnostics.d.ts.map +1 -0
  43. package/dist/core/diagnostics.js +101 -0
  44. package/dist/core/diagnostics.js.map +1 -0
  45. package/dist/core/feedback.d.ts +302 -0
  46. package/dist/core/feedback.d.ts.map +1 -0
  47. package/dist/core/feedback.js +345 -0
  48. package/dist/core/feedback.js.map +1 -0
  49. package/dist/core/flags.d.ts +13 -0
  50. package/dist/core/flags.d.ts.map +1 -0
  51. package/dist/core/flags.js +25 -0
  52. package/dist/core/flags.js.map +1 -0
  53. package/dist/core/ideas.d.ts +166 -0
  54. package/dist/core/ideas.d.ts.map +1 -0
  55. package/dist/core/ideas.js +159 -0
  56. package/dist/core/ideas.js.map +1 -0
  57. package/dist/core/launch.d.ts +43 -0
  58. package/dist/core/launch.d.ts.map +1 -0
  59. package/dist/core/launch.js +81 -0
  60. package/dist/core/launch.js.map +1 -0
  61. package/dist/core/reset.d.ts +17 -0
  62. package/dist/core/reset.d.ts.map +1 -0
  63. package/dist/core/reset.js +19 -0
  64. package/dist/core/reset.js.map +1 -0
  65. package/dist/core/review.d.ts +49 -0
  66. package/dist/core/review.d.ts.map +1 -0
  67. package/dist/core/review.js +92 -0
  68. package/dist/core/review.js.map +1 -0
  69. package/dist/core/url.d.ts +9 -0
  70. package/dist/core/url.d.ts.map +1 -0
  71. package/dist/core/url.js +23 -0
  72. package/dist/core/url.js.map +1 -0
  73. package/dist/hooks.d.ts +6 -0
  74. package/dist/hooks.d.ts.map +1 -0
  75. package/dist/hooks.js +13 -0
  76. package/dist/hooks.js.map +1 -0
  77. package/dist/index.d.ts +9 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +7 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/native-intent.d.ts +18 -0
  82. package/dist/native-intent.d.ts.map +1 -0
  83. package/dist/native-intent.js +32 -0
  84. package/dist/native-intent.js.map +1 -0
  85. package/dist/runtime.d.ts +24 -0
  86. package/dist/runtime.d.ts.map +1 -0
  87. package/dist/runtime.js +17 -0
  88. package/dist/runtime.js.map +1 -0
  89. package/dist/screenshot.d.ts +7 -0
  90. package/dist/screenshot.d.ts.map +1 -0
  91. package/dist/screenshot.js +48 -0
  92. package/dist/screenshot.js.map +1 -0
  93. package/dist/supabase.d.ts +20 -0
  94. package/dist/supabase.d.ts.map +1 -0
  95. package/dist/supabase.js +18 -0
  96. package/dist/supabase.js.map +1 -0
  97. package/dist/ui/native.d.ts +34 -0
  98. package/dist/ui/native.d.ts.map +1 -0
  99. package/dist/ui/native.js +76 -0
  100. package/dist/ui/native.js.map +1 -0
  101. package/dist/ui/parts.d.ts +62 -0
  102. package/dist/ui/parts.d.ts.map +1 -0
  103. package/dist/ui/parts.js +110 -0
  104. package/dist/ui/parts.js.map +1 -0
  105. package/dist/ui/theme.d.ts +90 -0
  106. package/dist/ui/theme.d.ts.map +1 -0
  107. package/dist/ui/theme.js +61 -0
  108. package/dist/ui/theme.js.map +1 -0
  109. package/package.json +82 -3
  110. package/src/Annotator.tsx +279 -0
  111. package/src/Bubble.tsx +331 -0
  112. package/src/Feedback.tsx +1739 -0
  113. package/src/Overlay.tsx +61 -0
  114. package/src/Popover.tsx +1393 -0
  115. package/src/Tapthrough.tsx +171 -0
  116. package/src/core/annotations.ts +116 -0
  117. package/src/core/bubble.ts +45 -0
  118. package/src/core/controller.ts +119 -0
  119. package/src/core/diagnostics.ts +128 -0
  120. package/src/core/feedback.ts +589 -0
  121. package/src/core/flags.ts +31 -0
  122. package/src/core/ideas.ts +337 -0
  123. package/src/core/launch.ts +119 -0
  124. package/src/core/reset.ts +28 -0
  125. package/src/core/review.ts +148 -0
  126. package/src/core/url.ts +21 -0
  127. package/src/hooks.ts +15 -0
  128. package/src/index.ts +8 -0
  129. package/src/native-intent.ts +36 -0
  130. package/src/runtime.ts +30 -0
  131. package/src/screenshot.ts +64 -0
  132. package/src/supabase.ts +27 -0
  133. package/src/ui/native.ts +116 -0
  134. package/src/ui/parts.tsx +249 -0
  135. 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
+ })