@blario/react-native 0.0.0 → 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 (313) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +378 -0
  3. package/dist/BlarProvider.d.ts +74 -0
  4. package/dist/actions/batched.d.ts +1 -0
  5. package/dist/actions/click.d.ts +4 -0
  6. package/dist/actions/commit-watch.d.ts +15 -0
  7. package/dist/actions/events.d.ts +35 -0
  8. package/dist/actions/executor.d.ts +29 -0
  9. package/dist/actions/highlight.d.ts +4 -0
  10. package/dist/actions/interaction-guards.d.ts +9 -0
  11. package/dist/actions/landing.d.ts +11 -0
  12. package/dist/actions/navigation-driver.d.ts +22 -0
  13. package/dist/actions/parse.d.ts +4 -0
  14. package/dist/actions/result.d.ts +4 -0
  15. package/dist/actions/scrolling.d.ts +12 -0
  16. package/dist/actions/services.d.ts +20 -0
  17. package/dist/actions/settle.d.ts +13 -0
  18. package/dist/actions/switch.d.ts +3 -0
  19. package/dist/actions/target.d.ts +25 -0
  20. package/dist/actions/text-input.d.ts +9 -0
  21. package/dist/actions/types.d.ts +59 -0
  22. package/dist/agent/batch-commands.d.ts +17 -0
  23. package/dist/agent/compose.d.ts +23 -0
  24. package/dist/agent/controller.d.ts +28 -0
  25. package/dist/agent/frame-runner.d.ts +9 -0
  26. package/dist/agent/frames.d.ts +180 -0
  27. package/dist/agent/handlers/agent-command.d.ts +5 -0
  28. package/dist/agent/handlers/batch.d.ts +5 -0
  29. package/dist/agent/handlers/consent-screenshot.d.ts +4 -0
  30. package/dist/agent/handlers/context.d.ts +16 -0
  31. package/dist/agent/handlers/dom-state.d.ts +4 -0
  32. package/dist/agent/handlers/element-context.d.ts +2 -0
  33. package/dist/agent/handlers/screenshot.d.ts +3 -0
  34. package/dist/agent/handlers/text-dom.d.ts +5 -0
  35. package/dist/agent/stage.d.ts +13 -0
  36. package/dist/agent-mode/action-description.d.ts +3 -0
  37. package/dist/agent-mode/bubbles.d.ts +10 -0
  38. package/dist/agent-mode/colors.d.ts +12 -0
  39. package/dist/agent-mode/controller.d.ts +92 -0
  40. package/dist/agent-mode/cursor-follow.d.ts +12 -0
  41. package/dist/agent-mode/cursor.d.ts +37 -0
  42. package/dist/agent-mode/frames.d.ts +36 -0
  43. package/dist/agent-mode/haptics.d.ts +6 -0
  44. package/dist/agent-mode/keep-awake.d.ts +19 -0
  45. package/dist/agent-mode/layer-stacking.d.ts +7 -0
  46. package/dist/agent-mode/markdown.d.ts +19 -0
  47. package/dist/agent-mode/panel-geometry.d.ts +6 -0
  48. package/dist/agent-mode/resume.d.ts +13 -0
  49. package/dist/agent-mode/run-memory.d.ts +19 -0
  50. package/dist/agent-mode/spotlight-controller.d.ts +68 -0
  51. package/dist/agent-mode/spotlight.d.ts +6 -0
  52. package/dist/agent-mode/state.d.ts +9 -0
  53. package/dist/agent-mode/status.d.ts +11 -0
  54. package/dist/agent-mode/timers.d.ts +6 -0
  55. package/dist/agent-mode/ui/AgentDock.d.ts +37 -0
  56. package/dist/agent-mode/ui/AgentLayer.d.ts +19 -0
  57. package/dist/agent-mode/ui/AgentLayerHost.d.ts +8 -0
  58. package/dist/agent-mode/ui/AgentPanel.d.ts +19 -0
  59. package/dist/agent-mode/ui/AgentTap.d.ts +10 -0
  60. package/dist/agent-mode/ui/MarkdownText.d.ts +8 -0
  61. package/dist/agent-mode/ui/MessageBubbles.d.ts +9 -0
  62. package/dist/agent-mode/ui/PendingOverlay.d.ts +5 -0
  63. package/dist/agent-mode/ui/SpotlightLayer.d.ts +9 -0
  64. package/dist/agent-mode/ui/SpotlightOverlay.d.ts +7 -0
  65. package/dist/agent-mode/ui/StatusCard.d.ts +14 -0
  66. package/dist/agent-mode/ui/TargetPulse.d.ts +9 -0
  67. package/dist/agent-mode/ui/TouchShield.d.ts +5 -0
  68. package/dist/agent-mode/ui/WarningIcon.d.ts +6 -0
  69. package/dist/agent-mode/ui/full-window-overlay.d.ts +5 -0
  70. package/dist/agent-mode/ui/layer-host.d.ts +2 -0
  71. package/dist/backend/api-client.d.ts +38 -0
  72. package/dist/backend/api-types.d.ts +25 -0
  73. package/dist/backend/app-info.d.ts +14 -0
  74. package/dist/backend/backend-transport.d.ts +70 -0
  75. package/dist/backend/backoff.d.ts +9 -0
  76. package/dist/backend/base64.d.ts +1 -0
  77. package/dist/backend/errors.d.ts +17 -0
  78. package/dist/backend/foreground.d.ts +1 -0
  79. package/dist/backend/health.d.ts +17 -0
  80. package/dist/backend/identity-proof.d.ts +23 -0
  81. package/dist/backend/identity.d.ts +27 -0
  82. package/dist/backend/message-metadata.d.ts +19 -0
  83. package/dist/backend/presence.d.ts +20 -0
  84. package/dist/backend/response-cache.d.ts +9 -0
  85. package/dist/backend/session-change.d.ts +2 -0
  86. package/dist/backend/session-socket.d.ts +49 -0
  87. package/dist/backend/session.d.ts +39 -0
  88. package/dist/backend/socket-url.d.ts +9 -0
  89. package/dist/backend/storage.d.ts +13 -0
  90. package/dist/backend/tab-id.d.ts +2 -0
  91. package/dist/blar.d.ts +37 -0
  92. package/dist/chat/ChatLayer.d.ts +29 -0
  93. package/dist/chat/ChatSheet.d.ts +11 -0
  94. package/dist/chat/ChatView.d.ts +19 -0
  95. package/dist/chat/Launcher.d.ts +14 -0
  96. package/dist/chat/bridge.d.ts +22 -0
  97. package/dist/chat/chat-coordinator.d.ts +57 -0
  98. package/dist/chat/chat-state.d.ts +20 -0
  99. package/dist/chat/navigation.d.ts +5 -0
  100. package/dist/chat/options.d.ts +22 -0
  101. package/dist/chat/webview.d.ts +10 -0
  102. package/dist/chat/widget-bundle.generated.d.ts +2 -0
  103. package/dist/chat/widget-shell.d.ts +24 -0
  104. package/dist/confirmation/port.d.ts +12 -0
  105. package/dist/fiber/locate.d.ts +7 -0
  106. package/dist/fiber/narrow.d.ts +5 -0
  107. package/dist/fiber/reader.d.ts +10 -0
  108. package/dist/fiber/text.d.ts +4 -0
  109. package/dist/fiber/types.d.ts +21 -0
  110. package/dist/host/border-radius.d.ts +1 -0
  111. package/dist/host/instance.d.ts +9 -0
  112. package/dist/host/measure.d.ts +3 -0
  113. package/dist/host/rect.d.ts +22 -0
  114. package/dist/host/visible-area.d.ts +2 -0
  115. package/dist/index.d.ts +10 -0
  116. package/dist/nudge/NudgeBubble.d.ts +15 -0
  117. package/dist/nudge/nudge.d.ts +23 -0
  118. package/dist/optional/expo-haptics.d.ts +1 -0
  119. package/dist/optional/expo-keep-awake.d.ts +1 -0
  120. package/dist/optional/react-native-haptic-feedback.d.ts +1 -0
  121. package/dist/optional/react-native-keep-awake.d.ts +1 -0
  122. package/dist/overlay/AgentOverlay.d.ts +5 -0
  123. package/dist/screenshot/capture-backend.d.ts +6 -0
  124. package/dist/screenshot/screenshotter.d.ts +18 -0
  125. package/dist/screenshot/skia.d.ts +35 -0
  126. package/dist/screenshot/view-shot.d.ts +2 -0
  127. package/dist/snapshot/capture.d.ts +36 -0
  128. package/dist/snapshot/classify.d.ts +11 -0
  129. package/dist/snapshot/collect.d.ts +27 -0
  130. package/dist/snapshot/element-context.d.ts +9 -0
  131. package/dist/snapshot/icon.d.ts +2 -0
  132. package/dist/snapshot/kinds.d.ts +3 -0
  133. package/dist/snapshot/modals.d.ts +2 -0
  134. package/dist/snapshot/navigation.d.ts +14 -0
  135. package/dist/snapshot/ordering.d.ts +2 -0
  136. package/dist/snapshot/properties.d.ts +14 -0
  137. package/dist/snapshot/registry.d.ts +16 -0
  138. package/dist/snapshot/render.d.ts +10 -0
  139. package/dist/snapshot/route-summary.d.ts +3 -0
  140. package/dist/snapshot/scroll.d.ts +15 -0
  141. package/dist/snapshot/text-layout.d.ts +8 -0
  142. package/dist/snapshot/view.d.ts +30 -0
  143. package/dist/snapshot/visibility.d.ts +17 -0
  144. package/dist/snapshot/wrappers.d.ts +11 -0
  145. package/dist/telemetry/SdkErrorBoundary.d.ts +15 -0
  146. package/dist/telemetry/build-info.generated.d.ts +2 -0
  147. package/dist/telemetry/envelope.d.ts +53 -0
  148. package/dist/telemetry/reporter.d.ts +33 -0
  149. package/dist/telemetry/scrub.d.ts +1 -0
  150. package/dist/telemetry/sdk-reporter.d.ts +5 -0
  151. package/dist/transport/connection-options.d.ts +18 -0
  152. package/dist/transport/relay-transport.d.ts +17 -0
  153. package/dist/transport/transport.d.ts +7 -0
  154. package/dist/ui/insets.d.ts +8 -0
  155. package/dist/ui/strings.d.ts +33 -0
  156. package/dist/ui/useInsets.d.ts +2 -0
  157. package/dist/version.d.ts +1 -0
  158. package/package.json +84 -3
  159. package/src/BlarProvider.tsx +462 -0
  160. package/src/actions/batched.ts +16 -0
  161. package/src/actions/click.ts +54 -0
  162. package/src/actions/commit-watch.ts +86 -0
  163. package/src/actions/events.ts +58 -0
  164. package/src/actions/executor.ts +147 -0
  165. package/src/actions/highlight.ts +13 -0
  166. package/src/actions/interaction-guards.ts +44 -0
  167. package/src/actions/landing.ts +26 -0
  168. package/src/actions/navigation-driver.ts +131 -0
  169. package/src/actions/parse.ts +82 -0
  170. package/src/actions/result.ts +14 -0
  171. package/src/actions/scrolling.ts +112 -0
  172. package/src/actions/services.ts +24 -0
  173. package/src/actions/settle.ts +40 -0
  174. package/src/actions/switch.ts +31 -0
  175. package/src/actions/target.ts +77 -0
  176. package/src/actions/text-input.ts +82 -0
  177. package/src/actions/types.ts +52 -0
  178. package/src/agent/batch-commands.ts +33 -0
  179. package/src/agent/compose.ts +67 -0
  180. package/src/agent/controller.ts +106 -0
  181. package/src/agent/frame-runner.ts +27 -0
  182. package/src/agent/frames.ts +193 -0
  183. package/src/agent/handlers/agent-command.ts +74 -0
  184. package/src/agent/handlers/batch.ts +72 -0
  185. package/src/agent/handlers/consent-screenshot.ts +44 -0
  186. package/src/agent/handlers/context.ts +17 -0
  187. package/src/agent/handlers/dom-state.ts +19 -0
  188. package/src/agent/handlers/element-context.ts +9 -0
  189. package/src/agent/handlers/screenshot.ts +24 -0
  190. package/src/agent/handlers/text-dom.ts +17 -0
  191. package/src/agent/stage.ts +22 -0
  192. package/src/agent-mode/action-description.ts +25 -0
  193. package/src/agent-mode/bubbles.ts +31 -0
  194. package/src/agent-mode/colors.ts +40 -0
  195. package/src/agent-mode/controller.ts +459 -0
  196. package/src/agent-mode/cursor-follow.ts +43 -0
  197. package/src/agent-mode/cursor.ts +118 -0
  198. package/src/agent-mode/frames.ts +70 -0
  199. package/src/agent-mode/haptics.ts +49 -0
  200. package/src/agent-mode/keep-awake.ts +107 -0
  201. package/src/agent-mode/layer-stacking.ts +16 -0
  202. package/src/agent-mode/markdown.ts +84 -0
  203. package/src/agent-mode/panel-geometry.ts +27 -0
  204. package/src/agent-mode/resume.ts +16 -0
  205. package/src/agent-mode/run-memory.ts +65 -0
  206. package/src/agent-mode/spotlight-controller.ts +255 -0
  207. package/src/agent-mode/spotlight.ts +14 -0
  208. package/src/agent-mode/state.ts +36 -0
  209. package/src/agent-mode/status.ts +13 -0
  210. package/src/agent-mode/timers.ts +11 -0
  211. package/src/agent-mode/ui/AgentDock.tsx +285 -0
  212. package/src/agent-mode/ui/AgentLayer.tsx +123 -0
  213. package/src/agent-mode/ui/AgentLayerHost.tsx +37 -0
  214. package/src/agent-mode/ui/AgentPanel.tsx +187 -0
  215. package/src/agent-mode/ui/AgentTap.tsx +190 -0
  216. package/src/agent-mode/ui/MarkdownText.tsx +55 -0
  217. package/src/agent-mode/ui/MessageBubbles.tsx +58 -0
  218. package/src/agent-mode/ui/PendingOverlay.tsx +32 -0
  219. package/src/agent-mode/ui/SpotlightLayer.tsx +60 -0
  220. package/src/agent-mode/ui/SpotlightOverlay.tsx +35 -0
  221. package/src/agent-mode/ui/StatusCard.tsx +137 -0
  222. package/src/agent-mode/ui/TargetPulse.tsx +74 -0
  223. package/src/agent-mode/ui/TouchShield.tsx +22 -0
  224. package/src/agent-mode/ui/WarningIcon.tsx +32 -0
  225. package/src/agent-mode/ui/full-window-overlay.ts +16 -0
  226. package/src/agent-mode/ui/layer-host.ts +11 -0
  227. package/src/backend/api-client.ts +210 -0
  228. package/src/backend/api-types.ts +56 -0
  229. package/src/backend/app-info.ts +40 -0
  230. package/src/backend/backend-transport.ts +338 -0
  231. package/src/backend/backoff.ts +16 -0
  232. package/src/backend/base64.ts +25 -0
  233. package/src/backend/errors.ts +32 -0
  234. package/src/backend/foreground.ts +10 -0
  235. package/src/backend/health.ts +64 -0
  236. package/src/backend/identity-proof.ts +151 -0
  237. package/src/backend/identity.ts +31 -0
  238. package/src/backend/message-metadata.ts +28 -0
  239. package/src/backend/presence.ts +95 -0
  240. package/src/backend/response-cache.ts +45 -0
  241. package/src/backend/session-change.ts +14 -0
  242. package/src/backend/session-socket.ts +292 -0
  243. package/src/backend/session.ts +188 -0
  244. package/src/backend/socket-url.ts +25 -0
  245. package/src/backend/storage.ts +49 -0
  246. package/src/backend/tab-id.ts +24 -0
  247. package/src/blar.ts +100 -0
  248. package/src/chat/ChatLayer.tsx +109 -0
  249. package/src/chat/ChatSheet.tsx +82 -0
  250. package/src/chat/ChatView.tsx +127 -0
  251. package/src/chat/Launcher.tsx +72 -0
  252. package/src/chat/bridge.ts +56 -0
  253. package/src/chat/chat-coordinator.ts +189 -0
  254. package/src/chat/chat-state.ts +35 -0
  255. package/src/chat/navigation.ts +15 -0
  256. package/src/chat/options.ts +39 -0
  257. package/src/chat/webview.ts +23 -0
  258. package/src/chat/widget-bundle.generated.ts +2 -0
  259. package/src/chat/widget-shell.ts +170 -0
  260. package/src/confirmation/port.ts +15 -0
  261. package/src/fiber/locate.ts +41 -0
  262. package/src/fiber/narrow.ts +15 -0
  263. package/src/fiber/reader.ts +98 -0
  264. package/src/fiber/text.ts +52 -0
  265. package/src/fiber/types.ts +24 -0
  266. package/src/host/border-radius.ts +11 -0
  267. package/src/host/instance.ts +30 -0
  268. package/src/host/measure.ts +25 -0
  269. package/src/host/rect.ts +45 -0
  270. package/src/host/visible-area.ts +5 -0
  271. package/src/index.ts +10 -0
  272. package/src/nudge/NudgeBubble.tsx +117 -0
  273. package/src/nudge/nudge.ts +100 -0
  274. package/src/optional/expo-haptics.ts +11 -0
  275. package/src/optional/expo-keep-awake.ts +11 -0
  276. package/src/optional/react-native-haptic-feedback.ts +11 -0
  277. package/src/optional/react-native-keep-awake.ts +11 -0
  278. package/src/overlay/AgentOverlay.tsx +16 -0
  279. package/src/screenshot/capture-backend.ts +7 -0
  280. package/src/screenshot/screenshotter.ts +39 -0
  281. package/src/screenshot/skia.ts +107 -0
  282. package/src/screenshot/view-shot.ts +41 -0
  283. package/src/snapshot/capture.ts +163 -0
  284. package/src/snapshot/classify.ts +40 -0
  285. package/src/snapshot/collect.ts +136 -0
  286. package/src/snapshot/element-context.ts +66 -0
  287. package/src/snapshot/icon.ts +21 -0
  288. package/src/snapshot/kinds.ts +10 -0
  289. package/src/snapshot/modals.ts +21 -0
  290. package/src/snapshot/navigation.ts +47 -0
  291. package/src/snapshot/ordering.ts +26 -0
  292. package/src/snapshot/properties.ts +71 -0
  293. package/src/snapshot/registry.ts +33 -0
  294. package/src/snapshot/render.ts +140 -0
  295. package/src/snapshot/route-summary.ts +15 -0
  296. package/src/snapshot/scroll.ts +90 -0
  297. package/src/snapshot/text-layout.ts +59 -0
  298. package/src/snapshot/view.ts +87 -0
  299. package/src/snapshot/visibility.ts +63 -0
  300. package/src/snapshot/wrappers.ts +69 -0
  301. package/src/telemetry/SdkErrorBoundary.tsx +28 -0
  302. package/src/telemetry/build-info.generated.ts +2 -0
  303. package/src/telemetry/envelope.ts +83 -0
  304. package/src/telemetry/reporter.ts +170 -0
  305. package/src/telemetry/scrub.ts +10 -0
  306. package/src/telemetry/sdk-reporter.ts +37 -0
  307. package/src/transport/connection-options.ts +65 -0
  308. package/src/transport/relay-transport.ts +82 -0
  309. package/src/transport/transport.ts +8 -0
  310. package/src/ui/insets.ts +31 -0
  311. package/src/ui/strings.ts +102 -0
  312. package/src/ui/useInsets.ts +10 -0
  313. package/src/version.ts +1 -0
@@ -0,0 +1,190 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { Animated, Easing, StyleSheet, View } from 'react-native';
3
+
4
+ import { centerOf, type Point } from '../../host/rect.ts';
5
+ import type { CursorSnapshot } from '../cursor.ts';
6
+
7
+ export interface AgentTapProps {
8
+ snapshot: CursorSnapshot;
9
+ color: string;
10
+ reduceMotion: boolean;
11
+ onAttach: () => void;
12
+ onDetach: () => void;
13
+ }
14
+
15
+ interface TapRingProps {
16
+ point: Point;
17
+ landed: boolean;
18
+ ending: boolean;
19
+ color: string;
20
+ reduceMotion: boolean;
21
+ onEnded: () => void;
22
+ }
23
+
24
+ interface Ripple {
25
+ id: number;
26
+ point: Point;
27
+ }
28
+
29
+ const RING = 40;
30
+ const WRAP = RING + 2;
31
+ const APPROACH_MS = 220;
32
+ const PRESS_MS = 90;
33
+ const RELEASE_MS = 160;
34
+ const FADE_MS = 200;
35
+ const RIPPLE_MS = 500;
36
+ const END_FADE_MS = 150;
37
+ const REDUCED_IN_MS = 150;
38
+ const REDUCED_HOLD_MS = 250;
39
+ const REST_FILL = 0.24;
40
+ const PRESSED_FILL = 0.5;
41
+
42
+ const APPROACH = Easing.bezier(0.22, 1, 0.36, 1);
43
+ const PRESS = Easing.bezier(0.33, 0, 0.67, 1);
44
+ const RELEASE = Easing.bezier(0.34, 1.56, 0.64, 1);
45
+ const RIPPLE = Easing.bezier(0.16, 1, 0.3, 1);
46
+ const EASE_OUT = Easing.out(Easing.ease);
47
+
48
+ function timing(value: Animated.Value, toValue: number, duration: number, easing: (value: number) => number, delay = 0): Animated.CompositeAnimation {
49
+ return Animated.timing(value, { toValue, duration, easing, delay, useNativeDriver: true });
50
+ }
51
+
52
+ function useLatest<T>(value: T): { current: T } {
53
+ const ref = useRef(value);
54
+ ref.current = value;
55
+ return ref;
56
+ }
57
+
58
+ function placed(point: Point): { left: number; top: number } {
59
+ return { left: point.x - WRAP / 2, top: point.y - WRAP / 2 };
60
+ }
61
+
62
+ function TapRing({ point, landed, ending, color, reduceMotion, onEnded }: TapRingProps): React.JSX.Element {
63
+ const opacity = useRef(new Animated.Value(0)).current;
64
+ const scale = useRef(new Animated.Value(reduceMotion ? 1 : 1.5)).current;
65
+ const fill = useRef(new Animated.Value(REST_FILL)).current;
66
+ const ended = useLatest(onEnded);
67
+
68
+ useEffect(() => {
69
+ const approach = reduceMotion
70
+ ? timing(opacity, 1, REDUCED_IN_MS, EASE_OUT)
71
+ : Animated.parallel([timing(opacity, 1, APPROACH_MS, APPROACH), timing(scale, 1, APPROACH_MS, APPROACH)]);
72
+ approach.start();
73
+ }, [opacity, scale, reduceMotion]);
74
+
75
+ useEffect(() => {
76
+ if (!landed) {
77
+ return;
78
+ }
79
+ if (reduceMotion) {
80
+ fill.setValue(PRESSED_FILL);
81
+ timing(opacity, 0, FADE_MS, EASE_OUT, REDUCED_HOLD_MS).start();
82
+ return;
83
+ }
84
+ Animated.parallel([
85
+ Animated.sequence([timing(scale, 0.84, PRESS_MS, PRESS), timing(scale, 1, RELEASE_MS, RELEASE)]),
86
+ Animated.sequence([timing(fill, PRESSED_FILL, PRESS_MS, PRESS), timing(fill, REST_FILL, RELEASE_MS, RELEASE)]),
87
+ timing(opacity, 0, FADE_MS, EASE_OUT, PRESS_MS + RELEASE_MS),
88
+ ]).start();
89
+ }, [landed, reduceMotion, fill, opacity, scale]);
90
+
91
+ useEffect(() => {
92
+ if (ending) {
93
+ timing(opacity, 0, END_FADE_MS, EASE_OUT).start(() => ended.current());
94
+ }
95
+ }, [ending, opacity, ended]);
96
+
97
+ return (
98
+ <Animated.View style={[styles.wrap, placed(point), { opacity, transform: [{ scale }] }]}>
99
+ <Animated.View style={[styles.fill, { backgroundColor: color, opacity: fill }]} />
100
+ <View style={styles.ring} />
101
+ <View style={styles.innerHairline} />
102
+ </Animated.View>
103
+ );
104
+ }
105
+
106
+ function TapRipple({ point, onDone }: { point: Point; onDone: () => void }): React.JSX.Element {
107
+ const progress = useRef(new Animated.Value(0)).current;
108
+ const done = useLatest(onDone);
109
+ useEffect(() => {
110
+ timing(progress, 1, RIPPLE_MS, RIPPLE, PRESS_MS).start(() => done.current());
111
+ }, [progress, done]);
112
+ return (
113
+ <Animated.View
114
+ style={[
115
+ styles.rippleWrap,
116
+ placed(point),
117
+ {
118
+ opacity: progress.interpolate({ inputRange: [0, 0.001, 1], outputRange: [0, 0.9, 0] }),
119
+ transform: [{ scale: progress.interpolate({ inputRange: [0, 1], outputRange: [1, 2.2] }) }],
120
+ },
121
+ ]}
122
+ >
123
+ <View style={styles.rippleRing} />
124
+ </Animated.View>
125
+ );
126
+ }
127
+
128
+ export function AgentTap({ snapshot, color, reduceMotion, onAttach, onDetach }: AgentTapProps): React.JSX.Element {
129
+ const { tap, target, landed } = snapshot;
130
+ const [ripples, setRipples] = useState<Ripple[]>([]);
131
+ const [endedTap, setEndedTap] = useState(0);
132
+ const lastPoint = useRef<{ tap: number; point: Point } | null>(null);
133
+
134
+ useEffect(() => onDetach, [onDetach]);
135
+
136
+ if (target !== null) {
137
+ const next = centerOf(target);
138
+ const last = lastPoint.current;
139
+ if (last === null || last.tap !== tap || last.point.x !== next.x || last.point.y !== next.y) {
140
+ lastPoint.current = { tap, point: next };
141
+ }
142
+ }
143
+ const point = lastPoint.current !== null && lastPoint.current.tap === tap ? lastPoint.current.point : null;
144
+
145
+ useEffect(() => {
146
+ if (!landed || point === null || reduceMotion) {
147
+ return;
148
+ }
149
+ setRipples((current) => [...current.filter((existing) => existing.id !== tap), { id: tap, point }]);
150
+ }, [landed, tap, point, reduceMotion]);
151
+
152
+ const showRing = point !== null && tap !== endedTap && !(target === null && landed);
153
+ return (
154
+ <View pointerEvents="none" style={StyleSheet.absoluteFill} onLayout={onAttach}>
155
+ {ripples.map((ripple) => (
156
+ <TapRipple key={ripple.id} point={ripple.point} onDone={() => setRipples((current) => current.filter((existing) => existing.id !== ripple.id))} />
157
+ ))}
158
+ {showRing ? (
159
+ <TapRing key={tap} point={point} landed={landed} ending={target === null} color={color} reduceMotion={reduceMotion} onEnded={() => setEndedTap(tap)} />
160
+ ) : null}
161
+ </View>
162
+ );
163
+ }
164
+
165
+ const styles = StyleSheet.create({
166
+ wrap: {
167
+ position: 'absolute',
168
+ width: WRAP,
169
+ height: WRAP,
170
+ borderRadius: WRAP / 2,
171
+ borderWidth: 1,
172
+ borderColor: 'rgba(0, 0, 0, 0.2)',
173
+ shadowColor: '#000000',
174
+ shadowOffset: { width: 0, height: 3 },
175
+ shadowOpacity: 0.28,
176
+ shadowRadius: 5,
177
+ },
178
+ fill: { ...StyleSheet.absoluteFillObject, borderRadius: RING / 2 },
179
+ ring: { ...StyleSheet.absoluteFillObject, borderRadius: RING / 2, borderWidth: 2.5, borderColor: '#ffffff' },
180
+ innerHairline: { position: 'absolute', left: 2.5, top: 2.5, right: 2.5, bottom: 2.5, borderRadius: RING / 2 - 2.5, borderWidth: 1, borderColor: 'rgba(0, 0, 0, 0.12)' },
181
+ rippleWrap: {
182
+ position: 'absolute',
183
+ width: WRAP,
184
+ height: WRAP,
185
+ borderRadius: WRAP / 2,
186
+ borderWidth: 1,
187
+ borderColor: 'rgba(0, 0, 0, 0.16)',
188
+ },
189
+ rippleRing: { ...StyleSheet.absoluteFillObject, borderRadius: RING / 2, borderWidth: 2, borderColor: '#ffffff' },
190
+ });
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+ import { StyleSheet, Text, View, type TextStyle } from 'react-native';
3
+
4
+ import { type Inline, type InlineKind, type MarkdownBlock, parseMarkdown } from '../markdown.ts';
5
+
6
+ export interface MarkdownTextProps {
7
+ source: string;
8
+ linkColor: string;
9
+ textStyle?: TextStyle;
10
+ }
11
+
12
+ const INLINE_STYLES: Readonly<Record<InlineKind, TextStyle>> = {
13
+ text: {},
14
+ bold: { fontWeight: '600' },
15
+ italic: { fontStyle: 'italic' },
16
+ code: { fontFamily: 'monospace', fontSize: 12, backgroundColor: '#f3f4f6' },
17
+ link: {},
18
+ };
19
+
20
+ function renderInlines(inlines: Inline[], linkColor: string): React.JSX.Element[] {
21
+ return inlines.map((inline, index) => (
22
+ <Text key={index} style={[INLINE_STYLES[inline.kind], inline.kind === 'link' ? { color: linkColor } : null]}>
23
+ {inline.text}
24
+ </Text>
25
+ ));
26
+ }
27
+
28
+ function renderBlock(block: MarkdownBlock, index: number, last: boolean, linkColor: string, textStyle: TextStyle | undefined): React.JSX.Element {
29
+ const spacing = last ? null : styles.spaced;
30
+ if (block.kind === 'paragraph') {
31
+ return (
32
+ <Text key={index} style={[styles.text, textStyle, spacing]}>
33
+ {renderInlines(block.inlines, linkColor)}
34
+ </Text>
35
+ );
36
+ }
37
+ return (
38
+ <View key={index} style={[styles.item, spacing]}>
39
+ <Text style={[styles.text, textStyle]}>{block.kind === 'bullet' ? '•' : block.marker}</Text>
40
+ <Text style={[styles.text, textStyle, styles.itemText]}>{renderInlines(block.inlines, linkColor)}</Text>
41
+ </View>
42
+ );
43
+ }
44
+
45
+ export function MarkdownText({ source, linkColor, textStyle }: MarkdownTextProps): React.JSX.Element {
46
+ const blocks = parseMarkdown(source);
47
+ return <View>{blocks.map((block, index) => renderBlock(block, index, index === blocks.length - 1, linkColor, textStyle))}</View>;
48
+ }
49
+
50
+ const styles = StyleSheet.create({
51
+ text: { fontSize: 14, lineHeight: 20, color: '#1f2937' },
52
+ spaced: { marginBottom: 6 },
53
+ item: { flexDirection: 'row', gap: 6, paddingLeft: 4 },
54
+ itemText: { flex: 1 },
55
+ });
@@ -0,0 +1,58 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Animated, ScrollView, StyleSheet } from 'react-native';
3
+
4
+ import type { AgentBubble } from '../bubbles.ts';
5
+ import { MarkdownText } from './MarkdownText.tsx';
6
+
7
+ export interface MessageBubblesProps {
8
+ bubbles: AgentBubble[];
9
+ width: number;
10
+ bottom: number;
11
+ linkColor: string;
12
+ }
13
+
14
+ const SLIDE_MS = 300;
15
+ const SLIDE_DISTANCE = 20;
16
+
17
+ function Bubble({ bubble, linkColor }: { bubble: AgentBubble; linkColor: string }): React.JSX.Element {
18
+ const shown = useRef(new Animated.Value(0)).current;
19
+ useEffect(() => {
20
+ Animated.timing(shown, { toValue: bubble.leaving ? 0 : 1, duration: SLIDE_MS, useNativeDriver: true }).start();
21
+ }, [bubble.leaving, shown]);
22
+ const translateY = shown.interpolate({ inputRange: [0, 1], outputRange: [SLIDE_DISTANCE, 0] });
23
+ return (
24
+ <Animated.View style={[styles.bubble, { opacity: shown, transform: [{ translateY }] }]}>
25
+ <ScrollView style={styles.scroll} contentContainerStyle={styles.content}>
26
+ <MarkdownText source={bubble.text} linkColor={linkColor} />
27
+ </ScrollView>
28
+ </Animated.View>
29
+ );
30
+ }
31
+
32
+ export function MessageBubbles({ bubbles, width, bottom, linkColor }: MessageBubblesProps): React.JSX.Element | null {
33
+ if (bubbles.length === 0) {
34
+ return null;
35
+ }
36
+ return (
37
+ <Animated.View style={[styles.column, { width, bottom }]} pointerEvents="box-none">
38
+ {bubbles.map((bubble) => (
39
+ <Bubble key={bubble.id} bubble={bubble} linkColor={linkColor} />
40
+ ))}
41
+ </Animated.View>
42
+ );
43
+ }
44
+
45
+ const styles = StyleSheet.create({
46
+ column: { position: 'absolute', alignSelf: 'center', gap: 10 },
47
+ bubble: {
48
+ backgroundColor: '#ffffff',
49
+ borderRadius: 12,
50
+ elevation: 6,
51
+ shadowColor: '#000000',
52
+ shadowOpacity: 0.12,
53
+ shadowRadius: 16,
54
+ shadowOffset: { width: 0, height: 4 },
55
+ },
56
+ scroll: { maxHeight: 140 },
57
+ content: { paddingHorizontal: 14, paddingVertical: 12 },
58
+ });
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import { StyleSheet, Text, View } from 'react-native';
3
+
4
+ export interface PendingOverlayProps {
5
+ text: string;
6
+ }
7
+
8
+ export function PendingOverlay({ text }: PendingOverlayProps): React.JSX.Element {
9
+ return (
10
+ <View style={styles.backdrop} onStartShouldSetResponder={() => true} accessibilityLiveRegion="polite">
11
+ <View style={styles.pill}>
12
+ <Text style={styles.text}>{text}</Text>
13
+ </View>
14
+ </View>
15
+ );
16
+ }
17
+
18
+ const styles = StyleSheet.create({
19
+ backdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0, 0, 0, 0.3)', alignItems: 'center', justifyContent: 'center' },
20
+ pill: {
21
+ backgroundColor: 'rgba(255, 255, 255, 0.85)',
22
+ borderRadius: 10,
23
+ paddingHorizontal: 24,
24
+ paddingVertical: 14,
25
+ elevation: 6,
26
+ shadowColor: '#000000',
27
+ shadowOpacity: 0.15,
28
+ shadowRadius: 16,
29
+ shadowOffset: { width: 0, height: 4 },
30
+ },
31
+ text: { fontSize: 14, fontWeight: '500', color: '#1f2937' },
32
+ });
@@ -0,0 +1,60 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { Animated, Easing, Pressable, StyleSheet, View } from 'react-native';
3
+
4
+ import type { SpotlightSnapshot } from '../spotlight-controller.ts';
5
+ import type { ActiveSpotlight } from './AgentPanel.tsx';
6
+ import { SPOTLIGHT_DIM, SpotlightOverlay } from './SpotlightOverlay.tsx';
7
+ import { TargetPulse } from './TargetPulse.tsx';
8
+
9
+ export interface SpotlightLayerProps {
10
+ spotlight: SpotlightSnapshot;
11
+ color: string;
12
+ reduceMotion: boolean;
13
+ onBackdropPress: () => void;
14
+ }
15
+
16
+ const FADE_MS = 200;
17
+ const REDUCED_MS = 150;
18
+
19
+ export function SpotlightLayer({ spotlight, color, reduceMotion, onBackdropPress }: SpotlightLayerProps): React.JSX.Element | null {
20
+ const visible = spotlight.kind !== 'none' && spotlight.shown;
21
+ const last = useRef<ActiveSpotlight | null>(null);
22
+ const opacity = useRef(new Animated.Value(0)).current;
23
+ const [gone, setGone] = useState(true);
24
+
25
+ if (spotlight.kind !== 'none' && spotlight.shown) {
26
+ last.current = spotlight;
27
+ }
28
+
29
+ useEffect(() => {
30
+ const duration = reduceMotion ? REDUCED_MS : FADE_MS;
31
+ if (visible) {
32
+ setGone(false);
33
+ Animated.timing(opacity, { toValue: 1, duration, easing: Easing.out(Easing.ease), useNativeDriver: true }).start();
34
+ return;
35
+ }
36
+ Animated.timing(opacity, { toValue: 0, duration, easing: Easing.out(Easing.ease), useNativeDriver: true }).start(({ finished }) => {
37
+ if (finished) {
38
+ setGone(true);
39
+ }
40
+ });
41
+ }, [visible, reduceMotion, opacity]);
42
+
43
+ const current = last.current;
44
+ if (current === null || (gone && !visible)) {
45
+ return null;
46
+ }
47
+ return (
48
+ <Animated.View pointerEvents={visible ? 'box-none' : 'none'} style={[StyleSheet.absoluteFill, { opacity }]}>
49
+ {current.kind === 'confirming' ? <Pressable style={StyleSheet.absoluteFill} onPress={onBackdropPress} /> : null}
50
+ {current.hole === null ? <View pointerEvents="none" style={styles.dim} /> : <SpotlightOverlay hole={current.hole} />}
51
+ {current.kind === 'confirming' && current.target !== null ? (
52
+ <TargetPulse target={current.target} radius={current.request.target?.radius ?? 0} color={color} reduceMotion={reduceMotion} />
53
+ ) : null}
54
+ </Animated.View>
55
+ );
56
+ }
57
+
58
+ const styles = StyleSheet.create({
59
+ dim: { ...StyleSheet.absoluteFillObject, backgroundColor: SPOTLIGHT_DIM },
60
+ });
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ import { StyleSheet, useWindowDimensions, View } from 'react-native';
3
+
4
+ import type { Hole } from '../spotlight.ts';
5
+
6
+ export interface SpotlightOverlayProps {
7
+ hole: Hole;
8
+ }
9
+
10
+ export const SPOTLIGHT_DIM = 'rgba(0, 0, 0, 0.6)';
11
+
12
+ export function SpotlightOverlay({ hole }: SpotlightOverlayProps): React.JSX.Element {
13
+ const { width, height } = useWindowDimensions();
14
+ const span = Math.max(width, height) * 2;
15
+ return (
16
+ <View
17
+ pointerEvents="none"
18
+ style={[
19
+ styles.mask,
20
+ {
21
+ left: hole.x - span,
22
+ top: hole.y - span,
23
+ width: hole.width + span * 2,
24
+ height: hole.height + span * 2,
25
+ borderWidth: span,
26
+ borderRadius: hole.radius + span,
27
+ },
28
+ ]}
29
+ />
30
+ );
31
+ }
32
+
33
+ const styles = StyleSheet.create({
34
+ mask: { position: 'absolute', borderColor: SPOTLIGHT_DIM },
35
+ });
@@ -0,0 +1,137 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { Animated, Pressable, StyleSheet, Text, View } from 'react-native';
3
+
4
+ import { onColor, withAlpha } from '../colors.ts';
5
+
6
+ export interface StatusCardProps {
7
+ status: string;
8
+ queued: string | null;
9
+ stopLabel: string;
10
+ color: string;
11
+ stopHidden: boolean;
12
+ corners: StatusCorners;
13
+ pulse: number;
14
+ cleared: boolean;
15
+ onStop: () => void;
16
+ }
17
+
18
+ export type StatusCorners = 'round' | 'square-top' | 'square-bottom';
19
+
20
+ const RADIUS = 16;
21
+
22
+ function cornersOf(corners: StatusCorners): { borderTopLeftRadius: number; borderTopRightRadius: number; borderBottomLeftRadius: number; borderBottomRightRadius: number } {
23
+ const top = corners === 'square-top' ? 0 : RADIUS;
24
+ const bottom = corners === 'square-bottom' ? 0 : RADIUS;
25
+ return { borderTopLeftRadius: top, borderTopRightRadius: top, borderBottomLeftRadius: bottom, borderBottomRightRadius: bottom };
26
+ }
27
+
28
+ const STOP_HIT_SLOP = 8;
29
+ const PULSE_STEP_MS = 400;
30
+ const PULSE_TIMES = 3;
31
+ const BORDER_MS = 2_500;
32
+ const CLEAR_MS = 150;
33
+ const RETURN_MS = 200;
34
+
35
+ function useClearance(cleared: boolean): Animated.Value {
36
+ const opacity = useRef(new Animated.Value(1)).current;
37
+ useEffect(() => {
38
+ Animated.timing(opacity, { toValue: cleared ? 0 : 1, duration: cleared ? CLEAR_MS : RETURN_MS, useNativeDriver: true }).start();
39
+ }, [cleared, opacity]);
40
+ return opacity;
41
+ }
42
+
43
+ function useStopPulse(pulse: number): { scale: Animated.Value; bordered: boolean } {
44
+ const scale = useRef(new Animated.Value(1)).current;
45
+ const [bordered, setBordered] = useState(false);
46
+ useEffect(() => {
47
+ if (pulse === 0) {
48
+ return undefined;
49
+ }
50
+ setBordered(true);
51
+ const beat = Animated.sequence([
52
+ Animated.timing(scale, { toValue: 1.2, duration: PULSE_STEP_MS, useNativeDriver: true }),
53
+ Animated.timing(scale, { toValue: 1, duration: PULSE_STEP_MS, useNativeDriver: true }),
54
+ ]);
55
+ const animation = Animated.loop(beat, { iterations: PULSE_TIMES });
56
+ animation.start();
57
+ const timer = setTimeout(() => setBordered(false), BORDER_MS);
58
+ return () => {
59
+ animation.stop();
60
+ clearTimeout(timer);
61
+ };
62
+ }, [pulse, scale]);
63
+ return { scale, bordered };
64
+ }
65
+
66
+ export function StatusCard({ status, queued, stopLabel, color, stopHidden, corners, pulse, cleared, onStop }: StatusCardProps): React.JSX.Element {
67
+ const { scale, bordered } = useStopPulse(pulse);
68
+ const opacity = useClearance(cleared);
69
+ const ink = onColor(color);
70
+ return (
71
+ <Animated.View
72
+ pointerEvents={cleared ? 'none' : 'auto'}
73
+ style={[styles.card, cornersOf(corners), { backgroundColor: color, opacity }]}
74
+ accessibilityLiveRegion="polite"
75
+ >
76
+ <View style={styles.header}>
77
+ <Text style={[styles.status, { color: withAlpha(ink, 0.85) }]} numberOfLines={2}>
78
+ {status}
79
+ </Text>
80
+ <Animated.View style={[{ transform: [{ scale }] }, stopHidden ? styles.hidden : null]}>
81
+ <Pressable
82
+ accessibilityRole="button"
83
+ accessibilityLabel={stopLabel}
84
+ hitSlop={STOP_HIT_SLOP}
85
+ style={[styles.stop, { backgroundColor: withAlpha(ink, 0.2) }, bordered ? { borderWidth: 2, borderColor: withAlpha(ink, 0.8) } : null]}
86
+ onPress={onStop}
87
+ >
88
+ <View style={[styles.stopIcon, { backgroundColor: ink }]} />
89
+ </Pressable>
90
+ </Animated.View>
91
+ </View>
92
+ {queued === null ? null : (
93
+ <View style={styles.queued}>
94
+ <Text style={styles.queuedIcon}>✓</Text>
95
+ <Text style={styles.queuedText} numberOfLines={1}>
96
+ {queued}
97
+ </Text>
98
+ </View>
99
+ )}
100
+ </Animated.View>
101
+ );
102
+ }
103
+
104
+ const styles = StyleSheet.create({
105
+ card: {
106
+ gap: 10,
107
+ paddingHorizontal: 18,
108
+ paddingVertical: 14,
109
+ elevation: 8,
110
+ shadowColor: '#000000',
111
+ shadowOpacity: 0.15,
112
+ shadowRadius: 12,
113
+ shadowOffset: { width: 0, height: 4 },
114
+ },
115
+ header: { flexDirection: 'row', alignItems: 'center', gap: 10, minHeight: 28 },
116
+ hidden: { display: 'none' },
117
+ status: { flex: 1, fontSize: 13, lineHeight: 17, fontWeight: '500', fontStyle: 'italic' },
118
+ stop: {
119
+ width: 28,
120
+ height: 28,
121
+ borderRadius: 6,
122
+ alignItems: 'center',
123
+ justifyContent: 'center',
124
+ },
125
+ stopIcon: { width: 12, height: 12, borderRadius: 1 },
126
+ queued: {
127
+ flexDirection: 'row',
128
+ alignItems: 'center',
129
+ gap: 6,
130
+ paddingHorizontal: 10,
131
+ paddingVertical: 6,
132
+ borderRadius: 8,
133
+ backgroundColor: 'rgba(134, 239, 172, 0.2)',
134
+ },
135
+ queuedIcon: { fontSize: 12, color: '#86efac' },
136
+ queuedText: { flex: 1, fontSize: 11, color: '#86efac' },
137
+ });
@@ -0,0 +1,74 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Animated, Easing, StyleSheet, View } from 'react-native';
3
+
4
+ import type { Rect } from '../../host/rect.ts';
5
+ import { withAlpha } from '../colors.ts';
6
+
7
+ export interface TargetPulseProps {
8
+ target: Rect;
9
+ radius: number;
10
+ color: string;
11
+ reduceMotion: boolean;
12
+ }
13
+
14
+ const OUTLINE_OFFSET = 4;
15
+ const OUTLINE_WIDTH = 3;
16
+ // A white hairline right outside the outline keeps a near-black brand readable on the dim.
17
+ const EDGE_WIDTH = 1.5;
18
+ const EDGE_COLOR = 'rgba(255, 255, 255, 0.9)';
19
+ const PULSE_MS = 2_000;
20
+
21
+ function ring(target: Rect, radius: number, spread: number): { left: number; top: number; width: number; height: number; borderRadius: number } {
22
+ return {
23
+ left: target.x - spread,
24
+ top: target.y - spread,
25
+ width: target.width + spread * 2,
26
+ height: target.height + spread * 2,
27
+ borderRadius: radius + spread,
28
+ };
29
+ }
30
+
31
+ export function TargetPulse({ target, radius, color, reduceMotion }: TargetPulseProps): React.JSX.Element {
32
+ const phase = useRef(new Animated.Value(0)).current;
33
+ useEffect(() => {
34
+ if (reduceMotion) {
35
+ phase.setValue(0.5);
36
+ return undefined;
37
+ }
38
+ const loop = Animated.loop(
39
+ Animated.sequence([
40
+ Animated.timing(phase, { toValue: 1, duration: PULSE_MS / 2, easing: Easing.inOut(Easing.ease), useNativeDriver: false }),
41
+ Animated.timing(phase, { toValue: 0, duration: PULSE_MS / 2, easing: Easing.inOut(Easing.ease), useNativeDriver: false }),
42
+ ]),
43
+ );
44
+ loop.start();
45
+ return () => loop.stop();
46
+ }, [phase, reduceMotion]);
47
+ const spread = phase.interpolate({ inputRange: [0, 1], outputRange: [4, 8] });
48
+ const outer = ring(target, radius, OUTLINE_OFFSET + OUTLINE_WIDTH);
49
+ const edge = ring(target, radius, OUTLINE_OFFSET + OUTLINE_WIDTH + EDGE_WIDTH);
50
+ return (
51
+ <View pointerEvents="none" style={StyleSheet.absoluteFill}>
52
+ <Animated.View
53
+ style={[
54
+ styles.band,
55
+ {
56
+ left: Animated.subtract(target.x, spread),
57
+ top: Animated.subtract(target.y, spread),
58
+ width: Animated.add(target.width, Animated.multiply(spread, 2)),
59
+ height: Animated.add(target.height, Animated.multiply(spread, 2)),
60
+ borderWidth: spread,
61
+ borderRadius: Animated.add(radius, spread),
62
+ borderColor: phase.interpolate({ inputRange: [0, 1], outputRange: [withAlpha(color, 0.4), withAlpha(color, 0.3)] }),
63
+ },
64
+ ]}
65
+ />
66
+ <View style={[styles.band, outer, { borderWidth: OUTLINE_WIDTH, borderColor: color }]} />
67
+ <View style={[styles.band, edge, { borderWidth: EDGE_WIDTH, borderColor: EDGE_COLOR }]} />
68
+ </View>
69
+ );
70
+ }
71
+
72
+ const styles = StyleSheet.create({
73
+ band: { position: 'absolute' },
74
+ });
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+
4
+ export interface TouchShieldProps {
5
+ onBlockedTap: () => void;
6
+ }
7
+
8
+ export function TouchShield({ onBlockedTap }: TouchShieldProps): React.JSX.Element {
9
+ return (
10
+ <View
11
+ style={styles.shield}
12
+ onStartShouldSetResponder={() => true}
13
+ onResponderRelease={onBlockedTap}
14
+ accessibilityElementsHidden
15
+ importantForAccessibility="no-hide-descendants"
16
+ />
17
+ );
18
+ }
19
+
20
+ const styles = StyleSheet.create({
21
+ shield: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0, 0, 0, 0.08)' },
22
+ });