@blario/react-native 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.
- package/LICENSE +21 -0
- package/README.md +377 -2
- package/dist/BlarProvider.d.ts +74 -0
- package/dist/actions/batched.d.ts +1 -0
- package/dist/actions/click.d.ts +4 -0
- package/dist/actions/commit-watch.d.ts +15 -0
- package/dist/actions/events.d.ts +35 -0
- package/dist/actions/executor.d.ts +29 -0
- package/dist/actions/highlight.d.ts +4 -0
- package/dist/actions/interaction-guards.d.ts +9 -0
- package/dist/actions/landing.d.ts +11 -0
- package/dist/actions/navigation-driver.d.ts +22 -0
- package/dist/actions/parse.d.ts +4 -0
- package/dist/actions/result.d.ts +4 -0
- package/dist/actions/scrolling.d.ts +12 -0
- package/dist/actions/services.d.ts +20 -0
- package/dist/actions/settle.d.ts +13 -0
- package/dist/actions/switch.d.ts +3 -0
- package/dist/actions/target.d.ts +25 -0
- package/dist/actions/text-input.d.ts +9 -0
- package/dist/actions/types.d.ts +59 -0
- package/dist/agent/batch-commands.d.ts +17 -0
- package/dist/agent/compose.d.ts +23 -0
- package/dist/agent/controller.d.ts +28 -0
- package/dist/agent/frame-runner.d.ts +9 -0
- package/dist/agent/frames.d.ts +180 -0
- package/dist/agent/handlers/agent-command.d.ts +5 -0
- package/dist/agent/handlers/batch.d.ts +5 -0
- package/dist/agent/handlers/consent-screenshot.d.ts +4 -0
- package/dist/agent/handlers/context.d.ts +16 -0
- package/dist/agent/handlers/dom-state.d.ts +4 -0
- package/dist/agent/handlers/element-context.d.ts +2 -0
- package/dist/agent/handlers/screenshot.d.ts +3 -0
- package/dist/agent/handlers/text-dom.d.ts +5 -0
- package/dist/agent/stage.d.ts +13 -0
- package/dist/agent-mode/action-description.d.ts +3 -0
- package/dist/agent-mode/bubbles.d.ts +10 -0
- package/dist/agent-mode/colors.d.ts +12 -0
- package/dist/agent-mode/controller.d.ts +92 -0
- package/dist/agent-mode/cursor-follow.d.ts +12 -0
- package/dist/agent-mode/cursor.d.ts +37 -0
- package/dist/agent-mode/frames.d.ts +36 -0
- package/dist/agent-mode/haptics.d.ts +6 -0
- package/dist/agent-mode/keep-awake.d.ts +19 -0
- package/dist/agent-mode/layer-stacking.d.ts +7 -0
- package/dist/agent-mode/markdown.d.ts +19 -0
- package/dist/agent-mode/panel-geometry.d.ts +6 -0
- package/dist/agent-mode/resume.d.ts +13 -0
- package/dist/agent-mode/run-memory.d.ts +19 -0
- package/dist/agent-mode/spotlight-controller.d.ts +68 -0
- package/dist/agent-mode/spotlight.d.ts +6 -0
- package/dist/agent-mode/state.d.ts +9 -0
- package/dist/agent-mode/status.d.ts +11 -0
- package/dist/agent-mode/timers.d.ts +6 -0
- package/dist/agent-mode/ui/AgentDock.d.ts +37 -0
- package/dist/agent-mode/ui/AgentLayer.d.ts +19 -0
- package/dist/agent-mode/ui/AgentLayerHost.d.ts +8 -0
- package/dist/agent-mode/ui/AgentPanel.d.ts +19 -0
- package/dist/agent-mode/ui/AgentTap.d.ts +10 -0
- package/dist/agent-mode/ui/MarkdownText.d.ts +8 -0
- package/dist/agent-mode/ui/MessageBubbles.d.ts +9 -0
- package/dist/agent-mode/ui/PendingOverlay.d.ts +5 -0
- package/dist/agent-mode/ui/SpotlightLayer.d.ts +9 -0
- package/dist/agent-mode/ui/SpotlightOverlay.d.ts +7 -0
- package/dist/agent-mode/ui/StatusCard.d.ts +14 -0
- package/dist/agent-mode/ui/TargetPulse.d.ts +9 -0
- package/dist/agent-mode/ui/TouchShield.d.ts +5 -0
- package/dist/agent-mode/ui/WarningIcon.d.ts +6 -0
- package/dist/agent-mode/ui/full-window-overlay.d.ts +5 -0
- package/dist/agent-mode/ui/layer-host.d.ts +2 -0
- package/dist/backend/api-client.d.ts +38 -0
- package/dist/backend/api-types.d.ts +25 -0
- package/dist/backend/app-info.d.ts +14 -0
- package/dist/backend/backend-transport.d.ts +70 -0
- package/dist/backend/backoff.d.ts +9 -0
- package/dist/backend/base64.d.ts +1 -0
- package/dist/backend/errors.d.ts +17 -0
- package/dist/backend/foreground.d.ts +1 -0
- package/dist/backend/health.d.ts +17 -0
- package/dist/backend/identity-proof.d.ts +23 -0
- package/dist/backend/identity.d.ts +27 -0
- package/dist/backend/message-metadata.d.ts +19 -0
- package/dist/backend/presence.d.ts +20 -0
- package/dist/backend/response-cache.d.ts +9 -0
- package/dist/backend/session-change.d.ts +2 -0
- package/dist/backend/session-socket.d.ts +49 -0
- package/dist/backend/session.d.ts +39 -0
- package/dist/backend/socket-url.d.ts +9 -0
- package/dist/backend/storage.d.ts +13 -0
- package/dist/backend/tab-id.d.ts +2 -0
- package/dist/blar.d.ts +37 -0
- package/dist/chat/ChatLayer.d.ts +29 -0
- package/dist/chat/ChatSheet.d.ts +11 -0
- package/dist/chat/ChatView.d.ts +19 -0
- package/dist/chat/Launcher.d.ts +14 -0
- package/dist/chat/bridge.d.ts +22 -0
- package/dist/chat/chat-coordinator.d.ts +57 -0
- package/dist/chat/chat-state.d.ts +20 -0
- package/dist/chat/navigation.d.ts +5 -0
- package/dist/chat/options.d.ts +22 -0
- package/dist/chat/webview.d.ts +10 -0
- package/dist/chat/widget-bundle.generated.d.ts +2 -0
- package/dist/chat/widget-shell.d.ts +24 -0
- package/dist/confirmation/port.d.ts +12 -0
- package/dist/fiber/locate.d.ts +7 -0
- package/dist/fiber/narrow.d.ts +5 -0
- package/dist/fiber/reader.d.ts +10 -0
- package/dist/fiber/text.d.ts +4 -0
- package/dist/fiber/types.d.ts +21 -0
- package/dist/host/border-radius.d.ts +1 -0
- package/dist/host/instance.d.ts +9 -0
- package/dist/host/measure.d.ts +3 -0
- package/dist/host/rect.d.ts +22 -0
- package/dist/host/visible-area.d.ts +2 -0
- package/dist/index.d.ts +10 -0
- package/dist/nudge/NudgeBubble.d.ts +15 -0
- package/dist/nudge/nudge.d.ts +23 -0
- package/dist/optional/expo-haptics.d.ts +1 -0
- package/dist/optional/expo-keep-awake.d.ts +1 -0
- package/dist/optional/react-native-haptic-feedback.d.ts +1 -0
- package/dist/optional/react-native-keep-awake.d.ts +1 -0
- package/dist/overlay/AgentOverlay.d.ts +5 -0
- package/dist/screenshot/capture-backend.d.ts +6 -0
- package/dist/screenshot/screenshotter.d.ts +18 -0
- package/dist/screenshot/skia.d.ts +35 -0
- package/dist/screenshot/view-shot.d.ts +2 -0
- package/dist/snapshot/capture.d.ts +36 -0
- package/dist/snapshot/classify.d.ts +11 -0
- package/dist/snapshot/collect.d.ts +27 -0
- package/dist/snapshot/element-context.d.ts +9 -0
- package/dist/snapshot/icon.d.ts +2 -0
- package/dist/snapshot/kinds.d.ts +3 -0
- package/dist/snapshot/modals.d.ts +2 -0
- package/dist/snapshot/navigation.d.ts +14 -0
- package/dist/snapshot/ordering.d.ts +2 -0
- package/dist/snapshot/properties.d.ts +14 -0
- package/dist/snapshot/registry.d.ts +16 -0
- package/dist/snapshot/render.d.ts +10 -0
- package/dist/snapshot/route-summary.d.ts +3 -0
- package/dist/snapshot/scroll.d.ts +15 -0
- package/dist/snapshot/text-layout.d.ts +8 -0
- package/dist/snapshot/view.d.ts +30 -0
- package/dist/snapshot/visibility.d.ts +17 -0
- package/dist/snapshot/wrappers.d.ts +11 -0
- package/dist/telemetry/SdkErrorBoundary.d.ts +15 -0
- package/dist/telemetry/build-info.generated.d.ts +2 -0
- package/dist/telemetry/envelope.d.ts +53 -0
- package/dist/telemetry/reporter.d.ts +33 -0
- package/dist/telemetry/scrub.d.ts +1 -0
- package/dist/telemetry/sdk-reporter.d.ts +5 -0
- package/dist/transport/connection-options.d.ts +18 -0
- package/dist/transport/relay-transport.d.ts +17 -0
- package/dist/transport/transport.d.ts +7 -0
- package/dist/ui/insets.d.ts +8 -0
- package/dist/ui/strings.d.ts +33 -0
- package/dist/ui/useInsets.d.ts +2 -0
- package/dist/version.d.ts +1 -0
- package/package.json +85 -4
- package/src/BlarProvider.tsx +462 -0
- package/src/actions/batched.ts +16 -0
- package/src/actions/click.ts +54 -0
- package/src/actions/commit-watch.ts +86 -0
- package/src/actions/events.ts +58 -0
- package/src/actions/executor.ts +147 -0
- package/src/actions/highlight.ts +13 -0
- package/src/actions/interaction-guards.ts +44 -0
- package/src/actions/landing.ts +26 -0
- package/src/actions/navigation-driver.ts +131 -0
- package/src/actions/parse.ts +82 -0
- package/src/actions/result.ts +14 -0
- package/src/actions/scrolling.ts +112 -0
- package/src/actions/services.ts +24 -0
- package/src/actions/settle.ts +40 -0
- package/src/actions/switch.ts +31 -0
- package/src/actions/target.ts +77 -0
- package/src/actions/text-input.ts +82 -0
- package/src/actions/types.ts +52 -0
- package/src/agent/batch-commands.ts +33 -0
- package/src/agent/compose.ts +67 -0
- package/src/agent/controller.ts +106 -0
- package/src/agent/frame-runner.ts +27 -0
- package/src/agent/frames.ts +193 -0
- package/src/agent/handlers/agent-command.ts +74 -0
- package/src/agent/handlers/batch.ts +72 -0
- package/src/agent/handlers/consent-screenshot.ts +44 -0
- package/src/agent/handlers/context.ts +17 -0
- package/src/agent/handlers/dom-state.ts +19 -0
- package/src/agent/handlers/element-context.ts +9 -0
- package/src/agent/handlers/screenshot.ts +24 -0
- package/src/agent/handlers/text-dom.ts +17 -0
- package/src/agent/stage.ts +22 -0
- package/src/agent-mode/action-description.ts +25 -0
- package/src/agent-mode/bubbles.ts +31 -0
- package/src/agent-mode/colors.ts +40 -0
- package/src/agent-mode/controller.ts +459 -0
- package/src/agent-mode/cursor-follow.ts +43 -0
- package/src/agent-mode/cursor.ts +118 -0
- package/src/agent-mode/frames.ts +70 -0
- package/src/agent-mode/haptics.ts +49 -0
- package/src/agent-mode/keep-awake.ts +107 -0
- package/src/agent-mode/layer-stacking.ts +16 -0
- package/src/agent-mode/markdown.ts +84 -0
- package/src/agent-mode/panel-geometry.ts +27 -0
- package/src/agent-mode/resume.ts +16 -0
- package/src/agent-mode/run-memory.ts +65 -0
- package/src/agent-mode/spotlight-controller.ts +255 -0
- package/src/agent-mode/spotlight.ts +14 -0
- package/src/agent-mode/state.ts +36 -0
- package/src/agent-mode/status.ts +13 -0
- package/src/agent-mode/timers.ts +11 -0
- package/src/agent-mode/ui/AgentDock.tsx +285 -0
- package/src/agent-mode/ui/AgentLayer.tsx +123 -0
- package/src/agent-mode/ui/AgentLayerHost.tsx +37 -0
- package/src/agent-mode/ui/AgentPanel.tsx +187 -0
- package/src/agent-mode/ui/AgentTap.tsx +190 -0
- package/src/agent-mode/ui/MarkdownText.tsx +55 -0
- package/src/agent-mode/ui/MessageBubbles.tsx +58 -0
- package/src/agent-mode/ui/PendingOverlay.tsx +32 -0
- package/src/agent-mode/ui/SpotlightLayer.tsx +60 -0
- package/src/agent-mode/ui/SpotlightOverlay.tsx +35 -0
- package/src/agent-mode/ui/StatusCard.tsx +137 -0
- package/src/agent-mode/ui/TargetPulse.tsx +74 -0
- package/src/agent-mode/ui/TouchShield.tsx +22 -0
- package/src/agent-mode/ui/WarningIcon.tsx +32 -0
- package/src/agent-mode/ui/full-window-overlay.ts +16 -0
- package/src/agent-mode/ui/layer-host.ts +11 -0
- package/src/backend/api-client.ts +210 -0
- package/src/backend/api-types.ts +56 -0
- package/src/backend/app-info.ts +40 -0
- package/src/backend/backend-transport.ts +338 -0
- package/src/backend/backoff.ts +16 -0
- package/src/backend/base64.ts +25 -0
- package/src/backend/errors.ts +32 -0
- package/src/backend/foreground.ts +10 -0
- package/src/backend/health.ts +64 -0
- package/src/backend/identity-proof.ts +151 -0
- package/src/backend/identity.ts +31 -0
- package/src/backend/message-metadata.ts +28 -0
- package/src/backend/presence.ts +95 -0
- package/src/backend/response-cache.ts +45 -0
- package/src/backend/session-change.ts +14 -0
- package/src/backend/session-socket.ts +292 -0
- package/src/backend/session.ts +188 -0
- package/src/backend/socket-url.ts +25 -0
- package/src/backend/storage.ts +49 -0
- package/src/backend/tab-id.ts +24 -0
- package/src/blar.ts +100 -0
- package/src/chat/ChatLayer.tsx +109 -0
- package/src/chat/ChatSheet.tsx +82 -0
- package/src/chat/ChatView.tsx +127 -0
- package/src/chat/Launcher.tsx +72 -0
- package/src/chat/bridge.ts +56 -0
- package/src/chat/chat-coordinator.ts +189 -0
- package/src/chat/chat-state.ts +35 -0
- package/src/chat/navigation.ts +15 -0
- package/src/chat/options.ts +39 -0
- package/src/chat/webview.ts +23 -0
- package/src/chat/widget-bundle.generated.ts +2 -0
- package/src/chat/widget-shell.ts +170 -0
- package/src/confirmation/port.ts +15 -0
- package/src/fiber/locate.ts +41 -0
- package/src/fiber/narrow.ts +15 -0
- package/src/fiber/reader.ts +98 -0
- package/src/fiber/text.ts +52 -0
- package/src/fiber/types.ts +24 -0
- package/src/host/border-radius.ts +11 -0
- package/src/host/instance.ts +30 -0
- package/src/host/measure.ts +25 -0
- package/src/host/rect.ts +45 -0
- package/src/host/visible-area.ts +5 -0
- package/src/index.ts +10 -0
- package/src/nudge/NudgeBubble.tsx +117 -0
- package/src/nudge/nudge.ts +100 -0
- package/src/optional/expo-haptics.ts +11 -0
- package/src/optional/expo-keep-awake.ts +11 -0
- package/src/optional/react-native-haptic-feedback.ts +11 -0
- package/src/optional/react-native-keep-awake.ts +11 -0
- package/src/overlay/AgentOverlay.tsx +16 -0
- package/src/screenshot/capture-backend.ts +7 -0
- package/src/screenshot/screenshotter.ts +39 -0
- package/src/screenshot/skia.ts +107 -0
- package/src/screenshot/view-shot.ts +41 -0
- package/src/snapshot/capture.ts +163 -0
- package/src/snapshot/classify.ts +40 -0
- package/src/snapshot/collect.ts +136 -0
- package/src/snapshot/element-context.ts +66 -0
- package/src/snapshot/icon.ts +21 -0
- package/src/snapshot/kinds.ts +10 -0
- package/src/snapshot/modals.ts +21 -0
- package/src/snapshot/navigation.ts +47 -0
- package/src/snapshot/ordering.ts +26 -0
- package/src/snapshot/properties.ts +71 -0
- package/src/snapshot/registry.ts +33 -0
- package/src/snapshot/render.ts +140 -0
- package/src/snapshot/route-summary.ts +15 -0
- package/src/snapshot/scroll.ts +90 -0
- package/src/snapshot/text-layout.ts +59 -0
- package/src/snapshot/view.ts +87 -0
- package/src/snapshot/visibility.ts +63 -0
- package/src/snapshot/wrappers.ts +69 -0
- package/src/telemetry/SdkErrorBoundary.tsx +28 -0
- package/src/telemetry/build-info.generated.ts +2 -0
- package/src/telemetry/envelope.ts +83 -0
- package/src/telemetry/reporter.ts +170 -0
- package/src/telemetry/scrub.ts +10 -0
- package/src/telemetry/sdk-reporter.ts +37 -0
- package/src/transport/connection-options.ts +65 -0
- package/src/transport/relay-transport.ts +82 -0
- package/src/transport/transport.ts +8 -0
- package/src/ui/insets.ts +31 -0
- package/src/ui/strings.ts +102 -0
- package/src/ui/useInsets.ts +10 -0
- 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
|
+
});
|