@blario/react-native 0.0.0 → 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.
Files changed (313) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +401 -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 +87 -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 +84 -0
  250. package/src/chat/ChatView.tsx +133 -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,11 @@
1
+ export interface Timers {
2
+ set: (callback: () => void, milliseconds: number) => unknown;
3
+ clear: (handle: unknown) => void;
4
+ now: () => number;
5
+ }
6
+
7
+ export const SYSTEM_TIMERS: Timers = {
8
+ set: (callback, milliseconds) => setTimeout(callback, milliseconds),
9
+ clear: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
10
+ now: () => Date.now(),
11
+ };
@@ -0,0 +1,285 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { Animated, Easing, Keyboard, type LayoutRectangle, StyleSheet, View } from 'react-native';
3
+
4
+ import type { Band, Rect } from '../../host/rect.ts';
5
+ import type { UiStrings } from '../../ui/strings.ts';
6
+ import { chooseDock, freeBand, messageCap, type PanelDock, STACK_GAP } from '../panel-geometry.ts';
7
+ import type { SpotlightSnapshot } from '../spotlight-controller.ts';
8
+ import { type ActiveSpotlight, AgentPanel, panelChrome } from './AgentPanel.tsx';
9
+ import { StatusCard, type StatusCorners } from './StatusCard.tsx';
10
+
11
+ export interface DockActions {
12
+ place: (id: number, band: Band) => void | Promise<void>;
13
+ approve: () => void;
14
+ cancel: () => void;
15
+ skip: () => void;
16
+ }
17
+
18
+ export interface DockStatus {
19
+ status: string;
20
+ queued: string | null;
21
+ stopLabel: string;
22
+ pulse: number;
23
+ cleared: boolean;
24
+ onStop: () => void;
25
+ }
26
+
27
+ export interface DockLayout {
28
+ width: number;
29
+ height: number;
30
+ cardWidth: number;
31
+ bottom: number;
32
+ safeTop: number;
33
+ }
34
+
35
+ export interface AgentDockProps {
36
+ spotlight: SpotlightSnapshot;
37
+ actions: DockActions;
38
+ card: DockStatus;
39
+ strings: UiStrings;
40
+ color: string;
41
+ reduceMotion: boolean;
42
+ nudge: number;
43
+ layout: DockLayout;
44
+ onCardFrame: (frame: Rect) => void;
45
+ }
46
+
47
+ const OPEN_MS = 260;
48
+ const CLOSE_MS = 200;
49
+ const REDUCED_MS = 150;
50
+ const STACK_FADE_MS = 150;
51
+ const STACK_SWAP_PAUSE_MS = 60;
52
+ const STACK_RETURN_PAUSE_MS = 140;
53
+ const STACK_RETURN_MS = 200;
54
+ const ENTER_SHIFT = 16;
55
+ const NUDGE_STEP_MS = 80;
56
+
57
+ const OPEN = Easing.bezier(0.22, 1, 0.36, 1);
58
+ const CLOSE = Easing.bezier(0.4, 0, 1, 1);
59
+
60
+ function animate(value: Animated.Value, toValue: number, duration: number, easing: (value: number) => number): Animated.CompositeAnimation {
61
+ return Animated.timing(value, { toValue, duration, easing, useNativeDriver: true });
62
+ }
63
+
64
+ function keyboardTop(height: number): number {
65
+ return Keyboard.metrics()?.screenY ?? height;
66
+ }
67
+
68
+ export function AgentDock({ spotlight, actions, card, strings, color, reduceMotion, nudge, layout, onCardFrame }: AgentDockProps): React.JSX.Element {
69
+ const { width, height, cardWidth, bottom, safeTop } = layout;
70
+ const [presented, setPresented] = useState<ActiveSpotlight | null>(null);
71
+ const [dock, setDock] = useState<PanelDock>('bottom');
72
+ const [atTop, setAtTop] = useState(false);
73
+ const [open, setOpen] = useState(false);
74
+ const [cardLayout, setCardLayout] = useState<LayoutRectangle | null>(null);
75
+ const [stackY, setStackY] = useState(0);
76
+ const [panelHeight, setPanelHeight] = useState(0);
77
+
78
+ const panelOpacity = useRef(new Animated.Value(0)).current;
79
+ const panelShift = useRef(new Animated.Value(ENTER_SHIFT)).current;
80
+ const stackOpacity = useRef(new Animated.Value(1)).current;
81
+ const stackShift = useRef(new Animated.Value(0)).current;
82
+ const nudgeShift = useRef(new Animated.Value(0)).current;
83
+
84
+ const presentedId = useRef<number | null>(null);
85
+ const placedId = useRef<number | null>(null);
86
+ const openRef = useRef(false);
87
+ const dockRef = useRef<PanelDock>('bottom');
88
+ const pending = useRef<Array<ReturnType<typeof setTimeout>>>([]);
89
+
90
+ const later = (callback: () => void, milliseconds: number): void => {
91
+ pending.current.push(setTimeout(callback, milliseconds));
92
+ };
93
+
94
+ const rest = (): void => {
95
+ pending.current.forEach(clearTimeout);
96
+ pending.current = [];
97
+ [panelOpacity, panelShift, stackOpacity, stackShift].forEach((value) => value.stopAnimation());
98
+ panelOpacity.setValue(0);
99
+ panelShift.setValue(ENTER_SHIFT);
100
+ stackOpacity.setValue(1);
101
+ stackShift.setValue(0);
102
+ openRef.current = false;
103
+ dockRef.current = 'bottom';
104
+ placedId.current = null;
105
+ setOpen(false);
106
+ setDock('bottom');
107
+ setAtTop(false);
108
+ setPanelHeight(0);
109
+ };
110
+
111
+ const leave = (): void => {
112
+ presentedId.current = null;
113
+ if (!openRef.current) {
114
+ rest();
115
+ setPresented(null);
116
+ return;
117
+ }
118
+ const duration = reduceMotion ? REDUCED_MS : CLOSE_MS;
119
+ const easing = reduceMotion ? Easing.linear : CLOSE;
120
+ if (dockRef.current === 'bottom') {
121
+ Animated.parallel([animate(panelOpacity, 0, duration, easing), animate(panelShift, reduceMotion ? 0 : ENTER_SHIFT, duration, easing)]).start(() => {
122
+ if (presentedId.current === null) {
123
+ rest();
124
+ setPresented(null);
125
+ }
126
+ });
127
+ return;
128
+ }
129
+ Animated.parallel([animate(stackOpacity, 0, duration, easing), animate(stackShift, reduceMotion ? 0 : -ENTER_SHIFT, duration, easing)]).start(() => {
130
+ if (presentedId.current !== null) {
131
+ return;
132
+ }
133
+ rest();
134
+ stackOpacity.setValue(0);
135
+ setPresented(null);
136
+ later(() => animate(stackOpacity, 1, STACK_RETURN_MS, Easing.out(Easing.ease)).start(), STACK_RETURN_PAUSE_MS);
137
+ });
138
+ };
139
+
140
+ useEffect(() => {
141
+ if (spotlight.kind === 'none') {
142
+ if (presentedId.current !== null) {
143
+ leave();
144
+ }
145
+ return;
146
+ }
147
+ if (presentedId.current !== spotlight.id) {
148
+ rest();
149
+ presentedId.current = spotlight.id;
150
+ }
151
+ setPresented(spotlight);
152
+ }, [spotlight]);
153
+
154
+ useEffect(() => () => pending.current.forEach(clearTimeout), []);
155
+
156
+ const cardHeight = cardLayout?.height ?? 0;
157
+ const cardBottom = height - bottom;
158
+ const keyboard = keyboardTop(height);
159
+
160
+ useEffect(() => {
161
+ if (presented === null || presented.shown || placedId.current === presented.id || panelHeight === 0 || cardHeight === 0) {
162
+ return;
163
+ }
164
+ placedId.current = presented.id;
165
+ const stackTop = cardBottom - cardHeight - panelHeight;
166
+ void actions.place(presented.id, freeBand(safeTop, stackTop, keyboard));
167
+ }, [presented, panelHeight, cardHeight, cardBottom, safeTop, keyboard, actions]);
168
+
169
+ useEffect(() => {
170
+ if (presented === null || !presented.shown || openRef.current) {
171
+ return;
172
+ }
173
+ openRef.current = true;
174
+ const left = (width - cardWidth) / 2;
175
+ const stackTop = cardBottom - cardHeight - panelHeight;
176
+ const chosen =
177
+ presented.target === null
178
+ ? 'bottom'
179
+ : chooseDock(
180
+ presented.target,
181
+ { x: left, y: stackTop - STACK_GAP, width: cardWidth, height: cardBottom - stackTop + STACK_GAP },
182
+ { x: left, y: safeTop + STACK_GAP, width: cardWidth, height: cardHeight + panelHeight + STACK_GAP },
183
+ );
184
+ dockRef.current = chosen;
185
+ setDock(chosen);
186
+ const duration = reduceMotion ? REDUCED_MS : OPEN_MS;
187
+ const easing = reduceMotion ? Easing.linear : OPEN;
188
+ if (chosen === 'bottom') {
189
+ setOpen(true);
190
+ if (reduceMotion) {
191
+ panelShift.setValue(0);
192
+ }
193
+ Animated.parallel([animate(panelOpacity, 1, duration, easing), animate(panelShift, 0, duration, easing)]).start();
194
+ return;
195
+ }
196
+ animate(stackOpacity, 0, STACK_FADE_MS, Easing.out(Easing.ease)).start(() => {
197
+ later(() => {
198
+ if (presentedId.current !== presented.id) {
199
+ return;
200
+ }
201
+ setAtTop(true);
202
+ setOpen(true);
203
+ panelOpacity.setValue(1);
204
+ panelShift.setValue(0);
205
+ stackShift.setValue(reduceMotion ? 0 : -ENTER_SHIFT);
206
+ Animated.parallel([animate(stackOpacity, 1, duration, easing), animate(stackShift, 0, duration, easing)]).start();
207
+ }, STACK_SWAP_PAUSE_MS);
208
+ });
209
+ }, [presented]);
210
+
211
+ useEffect(() => {
212
+ if (nudge === 0 || reduceMotion) {
213
+ return;
214
+ }
215
+ const step = (toValue: number): Animated.CompositeAnimation => animate(nudgeShift, toValue, NUDGE_STEP_MS, Easing.out(Easing.ease));
216
+ Animated.sequence([step(-6), step(0), step(-3), step(0)]).start();
217
+ }, [nudge, reduceMotion, nudgeShift]);
218
+
219
+ useEffect(() => {
220
+ if (cardLayout !== null) {
221
+ onCardFrame({ x: (width - cardWidth) / 2, y: stackY + cardLayout.y, width: cardWidth, height: cardLayout.height });
222
+ }
223
+ }, [cardLayout, stackY, width, cardWidth, onCardFrame]);
224
+
225
+ const confirming = presented?.kind === 'confirming';
226
+ const corners: StatusCorners = !open ? 'round' : atTop ? 'square-bottom' : 'square-top';
227
+ const room = Math.min(cardBottom - cardHeight - (presented === null ? 0 : panelChrome(presented)), keyboard) - safeTop - STACK_GAP * 2;
228
+
229
+ const statusCard = (
230
+ <View onLayout={(event) => setCardLayout(event.nativeEvent.layout)}>
231
+ <StatusCard
232
+ status={confirming ? strings.confirmationTitle : card.status}
233
+ queued={card.queued}
234
+ stopLabel={card.stopLabel}
235
+ color={color}
236
+ stopHidden={confirming}
237
+ corners={corners}
238
+ pulse={card.pulse}
239
+ cleared={card.cleared}
240
+ onStop={card.onStop}
241
+ />
242
+ </View>
243
+ );
244
+
245
+ const panel =
246
+ presented === null ? null : (
247
+ <Animated.View
248
+ pointerEvents={open ? 'auto' : 'none'}
249
+ onLayout={(event) => setPanelHeight(event.nativeEvent.layout.height)}
250
+ style={{ opacity: panelOpacity, transform: [{ translateY: panelShift }] }}
251
+ >
252
+ <AgentPanel
253
+ spotlight={presented}
254
+ dock={dock}
255
+ strings={strings}
256
+ color={color}
257
+ messageMaxHeight={messageCap(room)}
258
+ onConfirm={() => actions.approve()}
259
+ onStop={() => actions.cancel()}
260
+ onSkip={() => actions.skip()}
261
+ />
262
+ </Animated.View>
263
+ );
264
+
265
+ return (
266
+ <Animated.View
267
+ pointerEvents="box-none"
268
+ onLayout={(event) => setStackY(event.nativeEvent.layout.y)}
269
+ style={[
270
+ styles.stack,
271
+ { left: (width - cardWidth) / 2, width: cardWidth },
272
+ atTop ? { top: safeTop + STACK_GAP } : { bottom },
273
+ { opacity: stackOpacity, transform: [{ translateY: Animated.add(stackShift, nudgeShift) }] },
274
+ ]}
275
+ >
276
+ {atTop ? statusCard : null}
277
+ {panel}
278
+ {atTop ? null : statusCard}
279
+ </Animated.View>
280
+ );
281
+ }
282
+
283
+ const styles = StyleSheet.create({
284
+ stack: { position: 'absolute' },
285
+ });
@@ -0,0 +1,123 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { StyleSheet, Text, useWindowDimensions, View } from 'react-native';
3
+
4
+ import { intersects, type Rect } from '../../host/rect.ts';
5
+ import type { UiStrings } from '../../ui/strings.ts';
6
+ import { useInsets } from '../../ui/useInsets.ts';
7
+ import type { AgentModeSnapshot } from '../controller.ts';
8
+ import type { CursorSnapshot } from '../cursor.ts';
9
+ import type { SpotlightSnapshot } from '../spotlight-controller.ts';
10
+ import type { AgentStatus } from '../status.ts';
11
+ import { AgentDock, type DockActions } from './AgentDock.tsx';
12
+ import { AgentTap } from './AgentTap.tsx';
13
+ import { MessageBubbles } from './MessageBubbles.tsx';
14
+ import { PendingOverlay } from './PendingOverlay.tsx';
15
+ import { SpotlightLayer } from './SpotlightLayer.tsx';
16
+ import { TouchShield } from './TouchShield.tsx';
17
+
18
+ export interface AgentLayerProps {
19
+ snapshot: AgentModeSnapshot;
20
+ strings: UiStrings;
21
+ color: string;
22
+ spotlight: SpotlightSnapshot;
23
+ spotlightActions: DockActions;
24
+ cursor: CursorSnapshot;
25
+ reduceMotion: boolean;
26
+ onCursorAttach: () => void;
27
+ onCursorDetach: () => void;
28
+ onStop: () => void;
29
+ }
30
+
31
+ const CARD_MAX_WIDTH = 380;
32
+ const CARD_MARGIN = 16;
33
+ const CARD_BOTTOM = 20;
34
+ const BUBBLES_BOTTOM = 96;
35
+ const TOAST_BOTTOM = 100;
36
+ const TOAST_MS = 2_000;
37
+
38
+ function statusText(status: AgentStatus, strings: UiStrings): string {
39
+ switch (status.kind) {
40
+ case 'thinking':
41
+ return strings.agentThinking;
42
+ case 'executing':
43
+ return strings.agentExecuting;
44
+ case 'text':
45
+ return status.text;
46
+ }
47
+ }
48
+
49
+ function useBlockedTap(): { toast: boolean; pulse: number; block: () => void } {
50
+ const [toast, setToast] = useState(false);
51
+ const [pulse, setPulse] = useState(0);
52
+ const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
53
+ useEffect(
54
+ () => () => {
55
+ if (timer.current !== null) {
56
+ clearTimeout(timer.current);
57
+ }
58
+ },
59
+ [],
60
+ );
61
+ const block = (): void => {
62
+ setToast(true);
63
+ setPulse((count) => count + 1);
64
+ if (timer.current !== null) {
65
+ clearTimeout(timer.current);
66
+ }
67
+ timer.current = setTimeout(() => setToast(false), TOAST_MS);
68
+ };
69
+ return { toast, pulse, block };
70
+ }
71
+
72
+ export function AgentLayer({ snapshot, strings, color, spotlight, spotlightActions, cursor, reduceMotion, onCursorAttach, onCursorDetach, onStop }: AgentLayerProps): React.JSX.Element {
73
+ const insets = useInsets();
74
+ const { width, height } = useWindowDimensions();
75
+ const { toast, pulse, block } = useBlockedTap();
76
+ const [cardFrame, setCardFrame] = useState<Rect | null>(null);
77
+ const [nudge, setNudge] = useState(0);
78
+ if (snapshot.phase === 'pending') {
79
+ return <PendingOverlay text={strings.agentTakingControl} />;
80
+ }
81
+ const cardWidth = Math.min(CARD_MAX_WIDTH, width - CARD_MARGIN * 2);
82
+ const tapUnderCard = cursor.target !== null && cardFrame !== null && intersects(cursor.target, cardFrame);
83
+ const queued = snapshot.queued === null ? null : (snapshot.queued.message ?? strings.agentQueued);
84
+ const answering = spotlight.kind === 'confirming' && spotlight.shown;
85
+ return (
86
+ <View style={StyleSheet.absoluteFill} pointerEvents="box-none">
87
+ {answering ? null : <TouchShield onBlockedTap={block} />}
88
+ {spotlight.kind === 'none' ? (
89
+ <MessageBubbles bubbles={snapshot.bubbles} width={cardWidth} bottom={BUBBLES_BOTTOM + insets.bottom} linkColor={color} />
90
+ ) : null}
91
+ {toast ? (
92
+ <View pointerEvents="none" style={[styles.toast, { bottom: TOAST_BOTTOM + insets.bottom }]}>
93
+ <Text style={styles.toastText}>{strings.agentTapStopToControl}</Text>
94
+ </View>
95
+ ) : null}
96
+ <SpotlightLayer spotlight={spotlight} color={color} reduceMotion={reduceMotion} onBackdropPress={() => setNudge((count) => count + 1)} />
97
+ <AgentDock
98
+ spotlight={spotlight}
99
+ actions={spotlightActions}
100
+ card={{ status: statusText(snapshot.status, strings), queued, stopLabel: strings.agentStop, pulse, cleared: tapUnderCard, onStop }}
101
+ strings={strings}
102
+ color={color}
103
+ reduceMotion={reduceMotion}
104
+ nudge={nudge}
105
+ layout={{ width, height, cardWidth, bottom: CARD_BOTTOM + insets.bottom, safeTop: insets.top }}
106
+ onCardFrame={setCardFrame}
107
+ />
108
+ <AgentTap snapshot={cursor} color={color} reduceMotion={reduceMotion} onAttach={onCursorAttach} onDetach={onCursorDetach} />
109
+ </View>
110
+ );
111
+ }
112
+
113
+ const styles = StyleSheet.create({
114
+ toast: {
115
+ position: 'absolute',
116
+ alignSelf: 'center',
117
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
118
+ borderRadius: 8,
119
+ paddingHorizontal: 20,
120
+ paddingVertical: 10,
121
+ },
122
+ toastText: { color: '#ffffff', fontSize: 14 },
123
+ });
@@ -0,0 +1,37 @@
1
+ import React, { type ReactNode } from 'react';
2
+ import { Modal, Platform, StyleSheet, View } from 'react-native';
3
+
4
+ import { loadFullWindowOverlay } from './full-window-overlay.ts';
5
+ import { chooseLayerHost } from './layer-host.ts';
6
+
7
+ export interface AgentLayerHostProps {
8
+ visible: boolean;
9
+ generation: number;
10
+ onRequestClose: () => void;
11
+ children: ReactNode;
12
+ }
13
+
14
+ const FULL_WINDOW_OVERLAY = Platform.OS === 'ios' ? loadFullWindowOverlay() : null;
15
+ const HOST = chooseLayerHost(Platform.OS, FULL_WINDOW_OVERLAY !== null);
16
+
17
+ export function AgentLayerHost({ visible, generation, onRequestClose, children }: AgentLayerHostProps): React.JSX.Element | null {
18
+ if (!visible) {
19
+ return null;
20
+ }
21
+ if (HOST === 'modal') {
22
+ return (
23
+ <Modal key={generation} visible transparent animationType="none" hardwareAccelerated onRequestClose={onRequestClose}>
24
+ {children}
25
+ </Modal>
26
+ );
27
+ }
28
+ const content = (
29
+ <View style={StyleSheet.absoluteFill} pointerEvents="box-none">
30
+ {children}
31
+ </View>
32
+ );
33
+ if (HOST === 'full-window-overlay' && FULL_WINDOW_OVERLAY !== null) {
34
+ return <FULL_WINDOW_OVERLAY>{content}</FULL_WINDOW_OVERLAY>;
35
+ }
36
+ return content;
37
+ }
@@ -0,0 +1,187 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { AccessibilityInfo, Animated, Easing, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
3
+
4
+ import type { UiStrings } from '../../ui/strings.ts';
5
+ import { describeAction } from '../action-description.ts';
6
+ import { luminance, onColor } from '../colors.ts';
7
+ import { plainText } from '../markdown.ts';
8
+ import type { PanelDock } from '../panel-geometry.ts';
9
+ import type { SpotlightSnapshot } from '../spotlight-controller.ts';
10
+ import { MarkdownText } from './MarkdownText.tsx';
11
+ import { WarningIcon } from './WarningIcon.tsx';
12
+
13
+ export type ActiveSpotlight = Exclude<SpotlightSnapshot, { kind: 'none' }>;
14
+
15
+ export interface AgentPanelProps {
16
+ spotlight: ActiveSpotlight;
17
+ dock: PanelDock;
18
+ strings: UiStrings;
19
+ color: string;
20
+ messageMaxHeight: number;
21
+ onConfirm: () => void;
22
+ onStop: () => void;
23
+ onSkip: () => void;
24
+ }
25
+
26
+ const DESTRUCTIVE = '#dc2626';
27
+ const WARNING_TEXT = '#92400e';
28
+ const WARNING_BACKGROUND = '#fef3c7';
29
+ const INK = '#1f2937';
30
+
31
+ const PADDING = 18;
32
+ const BUTTON_HEIGHT = 48;
33
+ const SKIP_HEIGHT = 44;
34
+ const WARNING_HEIGHT = 42;
35
+ const CONFIRM_GAP = 16;
36
+ const DESTRUCTIVE_GAP = 12;
37
+ const READING_GAP = 10;
38
+ const READING_BOTTOM = 6;
39
+
40
+ export function panelChrome(spotlight: ActiveSpotlight): number {
41
+ if (spotlight.kind === 'reading') {
42
+ return PADDING + READING_GAP + SKIP_HEIGHT + READING_BOTTOM;
43
+ }
44
+ if (spotlight.request.riskLevel === 'destructive') {
45
+ return PADDING + DESTRUCTIVE_GAP + WARNING_HEIGHT + DESTRUCTIVE_GAP + 4 + BUTTON_HEIGHT + PADDING;
46
+ }
47
+ return PADDING + CONFIRM_GAP + BUTTON_HEIGHT + PADDING;
48
+ }
49
+
50
+ function messageOf(spotlight: ActiveSpotlight, strings: UiStrings): string {
51
+ if (spotlight.kind === 'reading') {
52
+ return spotlight.text ?? strings.actionFallback;
53
+ }
54
+ return spotlight.request.explanation?.trim() || describeAction(spotlight.request.action, strings);
55
+ }
56
+
57
+ function barColor(color: string): string {
58
+ return 1.05 / ((luminance(color) ?? 0) + 0.05) < 1.5 ? INK : color;
59
+ }
60
+
61
+ function Progress({ durationMs, running, color }: { durationMs: number; running: boolean; color: string }): React.JSX.Element {
62
+ const progress = useRef(new Animated.Value(0)).current;
63
+ useEffect(() => {
64
+ if (running) {
65
+ Animated.timing(progress, { toValue: 1, duration: durationMs, easing: Easing.linear, useNativeDriver: true }).start();
66
+ }
67
+ }, [running, durationMs, progress]);
68
+ return (
69
+ <View style={styles.track}>
70
+ <Animated.View style={[styles.bar, { backgroundColor: barColor(color), transform: [{ scaleX: progress }] }]} />
71
+ </View>
72
+ );
73
+ }
74
+
75
+ export function AgentPanel({ spotlight, dock, strings, color, messageMaxHeight, onConfirm, onStop, onSkip }: AgentPanelProps): React.JSX.Element {
76
+ const message = messageOf(spotlight, strings);
77
+ const confirming = spotlight.kind === 'confirming';
78
+ const destructive = confirming && spotlight.request.riskLevel === 'destructive';
79
+
80
+ useEffect(() => {
81
+ if (confirming && spotlight.shown) {
82
+ AccessibilityInfo.announceForAccessibility(plainText(message));
83
+ }
84
+ }, [confirming, spotlight.shown, message]);
85
+
86
+ const body = (
87
+ <ScrollView style={{ maxHeight: messageMaxHeight }} bounces={false} nestedScrollEnabled>
88
+ <View style={spotlight.kind === 'reading' ? styles.readingMessage : null}>
89
+ <MarkdownText source={message} linkColor={color} textStyle={styles.message} />
90
+ </View>
91
+ </ScrollView>
92
+ );
93
+
94
+ return (
95
+ <View
96
+ accessibilityViewIsModal={confirming}
97
+ style={[
98
+ styles.panel,
99
+ dock === 'bottom' ? styles.bottom : styles.top,
100
+ confirming ? (destructive ? styles.destructive : styles.confirm) : styles.reading,
101
+ ]}
102
+ >
103
+ {body}
104
+ {spotlight.kind === 'reading' ? (
105
+ <View style={styles.progressRow}>
106
+ <Progress durationMs={spotlight.durationMs} running={spotlight.shown} color={color} />
107
+ <Pressable accessibilityRole="button" onPress={onSkip} style={styles.skip}>
108
+ <Text style={styles.skipText}>{strings.readingSkip}</Text>
109
+ </Pressable>
110
+ </View>
111
+ ) : (
112
+ <>
113
+ {destructive ? (
114
+ <View style={styles.warning}>
115
+ <WarningIcon color={WARNING_TEXT} background={WARNING_BACKGROUND} />
116
+ <Text style={styles.warningText}>{strings.confirmCannotBeUndone}</Text>
117
+ </View>
118
+ ) : null}
119
+ <View style={[styles.buttons, destructive ? styles.destructiveButtons : null]}>
120
+ <Pressable accessibilityRole="button" onPress={onStop} style={[styles.button, styles.stop]}>
121
+ <Text style={[styles.buttonText, { color: INK }]}>{strings.confirmStop}</Text>
122
+ </Pressable>
123
+ <Pressable
124
+ accessibilityRole="button"
125
+ onPress={onConfirm}
126
+ style={[styles.button, destructive ? { backgroundColor: DESTRUCTIVE } : [styles.confirmButton, { backgroundColor: color }]]}
127
+ >
128
+ <Text style={[styles.buttonText, { color: destructive ? '#ffffff' : onColor(color) }]}>
129
+ {destructive ? strings.confirmProceedDestructive : strings.confirmProceed}
130
+ </Text>
131
+ </Pressable>
132
+ </View>
133
+ </>
134
+ )}
135
+ </View>
136
+ );
137
+ }
138
+
139
+ const styles = StyleSheet.create({
140
+ panel: {
141
+ backgroundColor: '#ffffff',
142
+ shadowColor: '#000000',
143
+ elevation: 12,
144
+ },
145
+ bottom: {
146
+ borderTopLeftRadius: 16,
147
+ borderTopRightRadius: 16,
148
+ shadowOffset: { width: 0, height: -6 },
149
+ shadowOpacity: 0.14,
150
+ shadowRadius: 9,
151
+ },
152
+ top: {
153
+ borderBottomLeftRadius: 16,
154
+ borderBottomRightRadius: 16,
155
+ shadowOffset: { width: 0, height: 8 },
156
+ shadowOpacity: 0.18,
157
+ shadowRadius: 10,
158
+ },
159
+ confirm: { padding: PADDING, gap: CONFIRM_GAP },
160
+ destructive: { padding: PADDING, gap: DESTRUCTIVE_GAP },
161
+ reading: { paddingTop: PADDING, paddingRight: 12, paddingBottom: READING_BOTTOM, paddingLeft: PADDING, gap: READING_GAP },
162
+ message: { fontSize: 16, lineHeight: 23, color: INK },
163
+ readingMessage: { paddingRight: 6 },
164
+ warning: {
165
+ flexDirection: 'row',
166
+ alignItems: 'center',
167
+ gap: 8,
168
+ minHeight: WARNING_HEIGHT,
169
+ paddingHorizontal: 12,
170
+ borderWidth: 1,
171
+ borderColor: '#f59e0b',
172
+ borderRadius: 10,
173
+ backgroundColor: WARNING_BACKGROUND,
174
+ },
175
+ warningText: { flex: 1, fontSize: 14, color: WARNING_TEXT },
176
+ buttons: { flexDirection: 'row', gap: 10 },
177
+ destructiveButtons: { marginTop: 4 },
178
+ button: { flex: 1, height: BUTTON_HEIGHT, borderRadius: 12, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 12 },
179
+ stop: { backgroundColor: '#eef0f3' },
180
+ confirmButton: { borderWidth: 1, borderColor: 'rgba(0, 0, 0, 0.1)' },
181
+ buttonText: { fontSize: 16, fontWeight: '600' },
182
+ progressRow: { flexDirection: 'row', alignItems: 'center', gap: 8 },
183
+ track: { flex: 1, height: 4, borderRadius: 2, overflow: 'hidden', backgroundColor: 'rgba(0, 0, 0, 0.08)' },
184
+ bar: { height: '100%', width: '100%', transformOrigin: 'left' },
185
+ skip: { height: SKIP_HEIGHT, paddingHorizontal: 12, borderRadius: 10, alignItems: 'center', justifyContent: 'center' },
186
+ skipText: { fontSize: 15, fontWeight: '600', color: '#4b5563' },
187
+ });