@omercnet/paseo-shared-browser 1.2.2 → 2.0.0-next.249.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/OVERVIEW.md +2 -2
- package/README.md +316 -27
- package/REMOTE_BROWSER_RESEARCH.md +395 -0
- package/STREAMING_DESIGN.md +196 -0
- package/client/browser-auto-capture-density.test.ts +102 -0
- package/client/browser-auto-capture-density.ts +30 -0
- package/client/browser-canvas-input.test.ts +198 -0
- package/client/browser-canvas-input.ts +202 -0
- package/client/browser-canvas-keyboard-web.test.ts +113 -0
- package/client/browser-canvas-keyboard-web.ts +230 -0
- package/client/browser-canvas-keyboard.test.ts +123 -0
- package/client/browser-canvas-keyboard.ts +126 -0
- package/client/browser-canvas-layout.test.ts +109 -0
- package/client/browser-canvas-layout.ts +51 -0
- package/client/browser-canvas-viewport-web.test.ts +77 -0
- package/client/browser-canvas-viewport-web.ts +63 -0
- package/client/browser-canvas-viewport.tsx +152 -0
- package/client/browser-capture-cadence.test.ts +9 -0
- package/client/browser-capture-cadence.ts +6 -0
- package/client/browser-capture-density-controls.tsx +75 -0
- package/client/browser-chrome.tsx +234 -0
- package/client/browser-client-presence.ts +184 -0
- package/client/browser-compose-controls.test.tsx +275 -0
- package/client/browser-compose-controls.tsx +174 -0
- package/client/browser-control-button.tsx +109 -0
- package/client/browser-control-dialog.tsx +71 -0
- package/client/browser-control-request.test.ts +27 -0
- package/client/browser-control-request.ts +57 -0
- package/client/browser-control-tooltip-web.ts +10 -0
- package/client/browser-dialog-body.tsx +22 -0
- package/client/browser-emulation-mode.test.ts +75 -0
- package/client/browser-emulation-mode.ts +98 -0
- package/client/browser-fill-viewport.test.ts +45 -0
- package/client/browser-fill-viewport.ts +28 -0
- package/client/browser-frame-buffer.test.ts +157 -0
- package/client/browser-frame-buffer.ts +148 -0
- package/client/browser-frame-image.tsx +62 -0
- package/client/browser-image-load.test.ts +23 -0
- package/client/browser-image-load.ts +26 -0
- package/client/browser-input-queue.test.ts +1254 -0
- package/client/browser-input-queue.ts +519 -0
- package/client/browser-menu-placement.test.ts +90 -0
- package/client/browser-menu-placement.ts +78 -0
- package/client/browser-panel-styles.ts +384 -0
- package/client/browser-quality-controls.test.ts +26 -0
- package/client/browser-quality-controls.tsx +139 -0
- package/client/browser-resolution-picker.tsx +189 -0
- package/client/browser-tabs.tsx +117 -0
- package/client/browser-toolbar-menu-web.test.ts +185 -0
- package/client/browser-toolbar-menu-web.ts +159 -0
- package/client/browser-toolbar-menu.tsx +255 -0
- package/client/browser-toolbar-submenu.tsx +93 -0
- package/client/browser-video-bytes.test.ts +44 -0
- package/client/browser-video-currency.ts +45 -0
- package/client/browser-video-decoder.test.ts +473 -0
- package/client/browser-video-decoder.ts +311 -0
- package/client/browser-video-surface.tsx +21 -0
- package/client/browser-video-visibility.test.ts +45 -0
- package/client/browser-viewer-recovery.test.ts +111 -0
- package/client/browser-viewer-recovery.ts +78 -0
- package/client/browser.tsx +1650 -1579
- package/client/favorite-star.tsx +23 -0
- package/client/frame-lifecycle.ts +62 -59
- package/client/sidebar.tsx +1 -2
- package/client/use-browser-auto-capture-density.test.ts +104 -0
- package/client/use-browser-auto-capture-density.ts +43 -0
- package/client/use-browser-canvas-input.ts +375 -0
- package/client/use-browser-capture-density.ts +87 -0
- package/client/use-browser-emulation-mode.ts +56 -0
- package/client/use-browser-fill-viewport.test.ts +108 -0
- package/client/use-browser-fill-viewport.ts +62 -0
- package/client/use-browser-frame-buffer.ts +66 -0
- package/client/use-browser-image-capture.ts +139 -0
- package/client/use-browser-scoped-mutation.test.ts +118 -0
- package/client/use-browser-scoped-mutation.ts +59 -0
- package/client/use-browser-video.test.ts +1060 -0
- package/client/use-browser-video.ts +481 -0
- package/client/use-browser-viewer-recovery.ts +26 -0
- package/client/use-resolution-favorites.ts +90 -0
- package/client/web.test.ts +125 -0
- package/client/web.ts +405 -0
- package/index.client.tsx +0 -10
- package/index.server.ts +32 -4
- package/npm-shrinkwrap.json +9 -9
- package/package.json +10 -5
- package/paseo-plugin.json +1 -1
- package/scripts/prepare-runtime.mjs +1 -1
- package/server/agent-browser-runtime.ts +1601 -314
- package/server/browser-cursor.test.ts +64 -0
- package/server/browser-cursor.ts +69 -0
- package/server/browser-frame-context.ts +22 -0
- package/server/browser-frame-epoch.test.ts +95 -0
- package/server/browser-frame-rejection.ts +17 -0
- package/server/browser-gesture.ts +120 -0
- package/server/browser-operation-diagnostics.test.ts +79 -0
- package/server/browser-operation-diagnostics.ts +103 -0
- package/server/browser-policy.ts +1316 -151
- package/server/browser-video-input-authority.test.ts +211 -0
- package/server/browser-video-policy.test.ts +564 -0
- package/server/browser-video-socket.test.ts +368 -0
- package/server/browser.ts +151 -16
- package/server/capture-transport-policy.test.ts +105 -0
- package/server/capture-transport-policy.ts +154 -0
- package/server/capture-transport-runtime.test.ts +213 -0
- package/server/daemon-launch-chain.test.ts +70 -0
- package/server/daemon-shutdown.test.ts +289 -0
- package/server/discrete-input-freshness.test.ts +364 -0
- package/server/discrete-input-native-lifetime.test.ts +150 -0
- package/server/display-loss-recovery.test.ts +294 -0
- package/server/emulation-restoration.test.ts +262 -0
- package/server/input-cleanup.ts +23 -0
- package/server/input-generation.test.ts +31 -0
- package/server/input-generation.ts +36 -0
- package/server/jpeg-capture-demand.test.ts +76 -0
- package/server/jpeg-capture-demand.ts +98 -0
- package/server/jpeg-frame.ts +29 -0
- package/server/keyboard-input.test.ts +61 -0
- package/server/keyboard-input.ts +104 -0
- package/server/live-input-policy.test.ts +958 -0
- package/server/live-input-runtime.test.ts +732 -0
- package/server/live-input-supervisor.test.ts +147 -0
- package/server/mcp-entry.ts +44 -2
- package/server/native-video-backpressure.test.ts +228 -0
- package/server/native-video-buffer.test.ts +141 -0
- package/server/native-video-buffer.ts +114 -0
- package/server/native-video-capture.test.ts +599 -0
- package/server/native-video-capture.ts +497 -0
- package/server/native-video-extension-source.test.ts +201 -0
- package/server/native-video-extension-source.ts +364 -0
- package/server/native-video-extension.test.ts +41 -0
- package/server/native-video-extension.ts +83 -0
- package/server/native-video-packet.ts +9 -0
- package/server/native-video-settings.test.ts +245 -0
- package/server/navigation-budget.ts +8 -0
- package/server/navigation-commit.ts +125 -0
- package/server/navigation-policy.test.ts +153 -0
- package/server/process-identity.ts +58 -0
- package/server/reattach-held-input.test.ts +143 -0
- package/server/runtime-frame-reuse.test.ts +295 -0
- package/server/runtime-jpeg-demand.test.ts +200 -0
- package/server/runtime-jpeg-shutdown.test.ts +210 -0
- package/server/runtime-jpeg-source-order.test.ts +201 -0
- package/server/runtime-launch-environment.test.ts +111 -0
- package/server/runtime-navigation.test.ts +327 -0
- package/server/runtime-owner-display.test.ts +152 -0
- package/server/runtime-owner.ts +340 -31
- package/server/runtime-page-state.test.ts +249 -0
- package/server/runtime-protocol.ts +32 -2
- package/server/runtime-target-retention.test.ts +181 -0
- package/server/runtime-video-opt-in.test.ts +82 -0
- package/server/runtime-video.test.ts +444 -0
- package/server/screencast-source-time.ts +46 -0
- package/server/stale-wheel-admission.test.ts +136 -0
- package/server/supervisor-client.ts +28 -4
- package/server/supervisor-heartbeat.test.ts +80 -0
- package/server/supervisor.ts +553 -55
- package/server/tab-close-confirmation.test.ts +97 -0
- package/server/video-source-recovery.test.ts +20 -0
- package/server/video-source-recovery.ts +64 -0
- package/server/viewer-capture-lifetime.ts +51 -0
- package/server/virtual-display.test.ts +184 -0
- package/server/virtual-display.ts +236 -0
- package/shared/browser-display-preferences.ts +46 -0
- package/shared/browser-video.ts +83 -0
- package/shared/browser.ts +286 -65
- package/shared/capture-density.ts +16 -0
- package/shared/capture-settings.ts +17 -0
- package/shared/device-presets.ts +90 -0
- package/shared/resolution-menu.test.ts +120 -0
- package/shared/resolution-menu.ts +72 -0
- package/shared/video-settings.test.ts +38 -0
- package/shared/video-settings.ts +34 -0
- package/shared/viewport-limits.ts +5 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { configureBrowserCanvasWebScrolling } from "./browser-canvas-viewport-web";
|
|
3
|
+
|
|
4
|
+
describe("local web browser viewport overflow", () => {
|
|
5
|
+
it("enables both overflow axes on the same public scroll node, preserving Actual offsets", () => {
|
|
6
|
+
const node = {
|
|
7
|
+
style: { overflowX: "hidden", overflowY: "auto" },
|
|
8
|
+
scrollLeft: 300,
|
|
9
|
+
scrollTop: 150,
|
|
10
|
+
};
|
|
11
|
+
configureBrowserCanvasWebScrolling({ getScrollableNode: () => node }, "actual", true);
|
|
12
|
+
expect(node).toEqual({
|
|
13
|
+
style: { overflowX: "auto", overflowY: "auto" },
|
|
14
|
+
scrollLeft: 300,
|
|
15
|
+
scrollTop: 150,
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("hides overflow and resets both local offsets when returning to Fit", () => {
|
|
20
|
+
const node = {
|
|
21
|
+
style: { overflowX: "auto", overflowY: "auto" },
|
|
22
|
+
scrollLeft: 300,
|
|
23
|
+
scrollTop: 150,
|
|
24
|
+
};
|
|
25
|
+
configureBrowserCanvasWebScrolling({ getScrollableNode: () => node }, "fit", true);
|
|
26
|
+
expect(node).toEqual({
|
|
27
|
+
style: { overflowX: "hidden", overflowY: "hidden" },
|
|
28
|
+
scrollLeft: 0,
|
|
29
|
+
scrollTop: 0,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("retains an Actual observation position when local panning is disabled", () => {
|
|
34
|
+
const node = {
|
|
35
|
+
style: { overflowX: "auto", overflowY: "auto" },
|
|
36
|
+
scrollLeft: 300,
|
|
37
|
+
scrollTop: 150,
|
|
38
|
+
};
|
|
39
|
+
configureBrowserCanvasWebScrolling({ getScrollableNode: () => node }, "actual", false);
|
|
40
|
+
expect(node).toEqual({
|
|
41
|
+
style: { overflowX: "hidden", overflowY: "hidden" },
|
|
42
|
+
scrollLeft: 300,
|
|
43
|
+
scrollTop: 150,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("uses the documented native host fallback while preserving the handle receiver", () => {
|
|
48
|
+
const node = { style: { overflowX: "", overflowY: "" }, scrollLeft: 0, scrollTop: 0 };
|
|
49
|
+
const handle = {
|
|
50
|
+
node,
|
|
51
|
+
getScrollableNode: () => null,
|
|
52
|
+
getNativeScrollRef() {
|
|
53
|
+
return this.node;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
configureBrowserCanvasWebScrolling(handle, "actual", true);
|
|
57
|
+
expect(node.style).toEqual({ overflowX: "auto", overflowY: "auto" });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("ignores missing, disappearing and non-DOM platform refs without changing unrelated nodes", () => {
|
|
61
|
+
expect(() => configureBrowserCanvasWebScrolling(null, "actual", true)).not.toThrow();
|
|
62
|
+
expect(() =>
|
|
63
|
+
configureBrowserCanvasWebScrolling({ getScrollableNode: () => 42 }, "actual", true),
|
|
64
|
+
).not.toThrow();
|
|
65
|
+
expect(() =>
|
|
66
|
+
configureBrowserCanvasWebScrolling(
|
|
67
|
+
{
|
|
68
|
+
getScrollableNode: () => {
|
|
69
|
+
throw new Error("Detached");
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
"actual",
|
|
73
|
+
true,
|
|
74
|
+
),
|
|
75
|
+
).not.toThrow();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web-only overflow adapter for the public React Native ScrollView host. A single
|
|
3
|
+
* DOM scroll node keeps both native desktop scrollbars at the viewer's edges.
|
|
4
|
+
* Only local overflow/offsets are changed; child remote-input listeners are untouched.
|
|
5
|
+
*/
|
|
6
|
+
import type { BrowserCanvasDisplayMode } from "./browser-canvas-layout";
|
|
7
|
+
|
|
8
|
+
interface WebScrollNode {
|
|
9
|
+
style: { overflowX: string; overflowY: string };
|
|
10
|
+
scrollLeft: number;
|
|
11
|
+
scrollTop: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface BrowserCanvasScrollHandle {
|
|
15
|
+
getScrollableNode?(): unknown;
|
|
16
|
+
getNativeScrollRef?(): unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Apply public ScrollView's second overflow axis before paint. Fit resets both
|
|
21
|
+
* offsets without replacing children. An unavailable/non-web host is a no-op.
|
|
22
|
+
* Actual disabled panning retains its previous observation offset.
|
|
23
|
+
*/
|
|
24
|
+
export function configureBrowserCanvasWebScrolling(
|
|
25
|
+
handle: BrowserCanvasScrollHandle | null,
|
|
26
|
+
mode: BrowserCanvasDisplayMode,
|
|
27
|
+
localPanEnabled: boolean,
|
|
28
|
+
): void {
|
|
29
|
+
if (!handle) return;
|
|
30
|
+
const node = readScrollNode(handle);
|
|
31
|
+
if (!node) return;
|
|
32
|
+
|
|
33
|
+
const overflow = mode === "actual" && localPanEnabled ? "auto" : "hidden";
|
|
34
|
+
node.style.overflowX = overflow;
|
|
35
|
+
node.style.overflowY = overflow;
|
|
36
|
+
if (mode === "fit") {
|
|
37
|
+
node.scrollLeft = 0;
|
|
38
|
+
node.scrollTop = 0;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Prefer the documented scrollable host; tolerate platform refs that expose only the native scroll ref. */
|
|
43
|
+
function readScrollNode(handle: BrowserCanvasScrollHandle): WebScrollNode | null {
|
|
44
|
+
for (const read of [handle.getScrollableNode, handle.getNativeScrollRef]) {
|
|
45
|
+
if (!read) continue;
|
|
46
|
+
try {
|
|
47
|
+
const candidate: unknown = read.call(handle);
|
|
48
|
+
if (!candidate || typeof candidate !== "object") continue;
|
|
49
|
+
const node = candidate as Partial<WebScrollNode>;
|
|
50
|
+
if (
|
|
51
|
+
!node.style ||
|
|
52
|
+
typeof node.style.overflowX !== "string" ||
|
|
53
|
+
typeof node.style.overflowY !== "string"
|
|
54
|
+
)
|
|
55
|
+
continue;
|
|
56
|
+
if (typeof node.scrollLeft !== "number" || typeof node.scrollTop !== "number") continue;
|
|
57
|
+
return node as WebScrollNode;
|
|
58
|
+
} catch {
|
|
59
|
+
// A disappearing host must not break frame rendering or input cleanup.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable local scroll container for browser frames and their input overlay.
|
|
3
|
+
* Wrappers and children never change identity when Fit/Actual changes, preserving
|
|
4
|
+
* decoded frame slots and refs. Only Actual enables local scrolling. The parent
|
|
5
|
+
* cancels active remote input before changing geometry and owns overlay authority.
|
|
6
|
+
*/
|
|
7
|
+
import { type ReactNode, type Ref, useLayoutEffect, useRef } from "react";
|
|
8
|
+
import {
|
|
9
|
+
type LayoutChangeEvent,
|
|
10
|
+
Platform,
|
|
11
|
+
ScrollView,
|
|
12
|
+
type StyleProp,
|
|
13
|
+
StyleSheet,
|
|
14
|
+
View,
|
|
15
|
+
type ViewStyle,
|
|
16
|
+
} from "react-native";
|
|
17
|
+
import type { FrameSize } from "../shared/browser";
|
|
18
|
+
import type { BrowserCanvasDisplayMode } from "./browser-canvas-layout";
|
|
19
|
+
import { configureBrowserCanvasWebScrolling } from "./browser-canvas-viewport-web";
|
|
20
|
+
|
|
21
|
+
export interface BrowserCanvasViewportProps {
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
/** Frame wrapper from getBrowserCanvasLayout, measured in local logical pixels. */
|
|
24
|
+
contentSize: FrameSize;
|
|
25
|
+
mode?: BrowserCanvasDisplayMode;
|
|
26
|
+
/** Desktop permits local scrollbars; native observation permits pan, remote control does not. */
|
|
27
|
+
localPanEnabled: boolean;
|
|
28
|
+
onLayout(event: LayoutChangeEvent): void;
|
|
29
|
+
style?: StyleProp<ViewStyle>;
|
|
30
|
+
viewportRef?: Ref<View>;
|
|
31
|
+
contentRef?: Ref<View>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const styles = StyleSheet.create({
|
|
35
|
+
viewport: { flex: 1, minWidth: 0, minHeight: 0, overflow: "hidden" },
|
|
36
|
+
scroll: { flex: 1 },
|
|
37
|
+
horizontalContent: { height: "100%" },
|
|
38
|
+
vertical: { height: "100%" },
|
|
39
|
+
content: { position: "relative" },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** Keep the frame coordinate origin inside the scrolling content, rather than the clipped viewport. */
|
|
43
|
+
export function BrowserCanvasViewport({
|
|
44
|
+
children,
|
|
45
|
+
contentSize,
|
|
46
|
+
mode = "fit",
|
|
47
|
+
localPanEnabled,
|
|
48
|
+
onLayout,
|
|
49
|
+
style,
|
|
50
|
+
viewportRef,
|
|
51
|
+
contentRef,
|
|
52
|
+
}: BrowserCanvasViewportProps) {
|
|
53
|
+
const horizontalRef = useRef<ScrollView>(null);
|
|
54
|
+
const verticalRef = useRef<ScrollView>(null);
|
|
55
|
+
const actual = mode === "actual";
|
|
56
|
+
const scrollEnabled = actual && localPanEnabled;
|
|
57
|
+
const web = Platform.OS === "web";
|
|
58
|
+
// Center a fitting Actual frame in the scrollport, not a spacer sized from
|
|
59
|
+
// the outer pane. A scrollbar on one axis must not create overflow on the
|
|
60
|
+
// other. Minimums keep overflowing frames reachable from origin zero.
|
|
61
|
+
const actualContent = {
|
|
62
|
+
flexGrow: 1,
|
|
63
|
+
alignItems: "center" as const,
|
|
64
|
+
justifyContent: "center" as const,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
useLayoutEffect(() => {
|
|
68
|
+
if (web) {
|
|
69
|
+
configureBrowserCanvasWebScrolling(horizontalRef.current, mode, localPanEnabled);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (actual) return;
|
|
73
|
+
// Reset local offsets without remounting either frame-buffer layer. A hidden
|
|
74
|
+
// Actual offset must not shift Fit's overlay away from its displayed pixels.
|
|
75
|
+
horizontalRef.current?.scrollTo({ x: 0, y: 0, animated: false });
|
|
76
|
+
verticalRef.current?.scrollTo({ x: 0, y: 0, animated: false });
|
|
77
|
+
}, [actual, mode, localPanEnabled, web, contentSize.width, contentSize.height]);
|
|
78
|
+
|
|
79
|
+
const content = (
|
|
80
|
+
<View ref={contentRef} style={[styles.content, contentSize]}>
|
|
81
|
+
{children}
|
|
82
|
+
</View>
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (web) {
|
|
86
|
+
return (
|
|
87
|
+
<View ref={viewportRef} style={[styles.viewport, style]} onLayout={onLayout}>
|
|
88
|
+
<ScrollView
|
|
89
|
+
ref={horizontalRef}
|
|
90
|
+
style={styles.scroll}
|
|
91
|
+
contentContainerStyle={
|
|
92
|
+
actual
|
|
93
|
+
? [
|
|
94
|
+
actualContent,
|
|
95
|
+
{
|
|
96
|
+
minWidth: contentSize.width,
|
|
97
|
+
minHeight: contentSize.height,
|
|
98
|
+
},
|
|
99
|
+
]
|
|
100
|
+
: contentSize
|
|
101
|
+
}
|
|
102
|
+
scrollEnabled={scrollEnabled}
|
|
103
|
+
showsHorizontalScrollIndicator={scrollEnabled}
|
|
104
|
+
showsVerticalScrollIndicator={scrollEnabled}
|
|
105
|
+
keyboardShouldPersistTaps="always"
|
|
106
|
+
removeClippedSubviews={false}
|
|
107
|
+
>
|
|
108
|
+
{content}
|
|
109
|
+
</ScrollView>
|
|
110
|
+
</View>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<View ref={viewportRef} style={[styles.viewport, style]} onLayout={onLayout}>
|
|
116
|
+
<ScrollView
|
|
117
|
+
ref={horizontalRef}
|
|
118
|
+
horizontal
|
|
119
|
+
style={styles.scroll}
|
|
120
|
+
contentContainerStyle={
|
|
121
|
+
actual
|
|
122
|
+
? [styles.horizontalContent, actualContent, { minWidth: contentSize.width }]
|
|
123
|
+
: [styles.horizontalContent, { width: contentSize.width }]
|
|
124
|
+
}
|
|
125
|
+
scrollEnabled={scrollEnabled}
|
|
126
|
+
showsHorizontalScrollIndicator={scrollEnabled}
|
|
127
|
+
nestedScrollEnabled
|
|
128
|
+
directionalLockEnabled={false}
|
|
129
|
+
keyboardShouldPersistTaps="always"
|
|
130
|
+
removeClippedSubviews={false}
|
|
131
|
+
>
|
|
132
|
+
<ScrollView
|
|
133
|
+
ref={verticalRef}
|
|
134
|
+
style={[styles.vertical, { width: contentSize.width }]}
|
|
135
|
+
contentContainerStyle={
|
|
136
|
+
actual
|
|
137
|
+
? [actualContent, { minHeight: contentSize.height }]
|
|
138
|
+
: { height: contentSize.height }
|
|
139
|
+
}
|
|
140
|
+
scrollEnabled={scrollEnabled}
|
|
141
|
+
showsVerticalScrollIndicator={scrollEnabled}
|
|
142
|
+
nestedScrollEnabled
|
|
143
|
+
directionalLockEnabled={false}
|
|
144
|
+
keyboardShouldPersistTaps="always"
|
|
145
|
+
removeClippedSubviews={false}
|
|
146
|
+
>
|
|
147
|
+
{content}
|
|
148
|
+
</ScrollView>
|
|
149
|
+
</ScrollView>
|
|
150
|
+
</View>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { expect, it } from "vitest";
|
|
2
|
+
import { browserCaptureInterval } from "./browser-capture-cadence";
|
|
3
|
+
|
|
4
|
+
it("keeps idle/hover polling bounded and only accelerates actual ready input", () => {
|
|
5
|
+
expect(browserCaptureInterval("ready", false)).toBe(250);
|
|
6
|
+
expect(browserCaptureInterval("ready", true)).toBe(100);
|
|
7
|
+
expect(browserCaptureInterval("starting", true)).toBe(1_500);
|
|
8
|
+
expect(browserCaptureInterval(undefined, false)).toBe(1_500);
|
|
9
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Single-flight capture cadence; active input improves playback without changing image quality. */
|
|
2
|
+
export function browserCaptureInterval(status: string | undefined, activeInput: boolean): number {
|
|
3
|
+
if (status !== "ready") return 1_500;
|
|
4
|
+
if (activeInput) return 100;
|
|
5
|
+
return 250;
|
|
6
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** Controller-only physical capture density, independent of CSS resolution and emulation. */
|
|
2
|
+
import type { PluginHostProps } from "@getpaseo/plugin/client";
|
|
3
|
+
import { Pressable, StyleSheet, Text, View } from "react-native";
|
|
4
|
+
import { canUseCaptureDensity } from "../shared/browser";
|
|
5
|
+
import { MAX_VIEWPORT } from "../shared/viewport-limits";
|
|
6
|
+
import type { CaptureDensityMode } from "./browser-auto-capture-density";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
theme: PluginHostProps["theme"];
|
|
10
|
+
density: number;
|
|
11
|
+
mode: CaptureDensityMode;
|
|
12
|
+
viewport: { width: number; height: number } | null;
|
|
13
|
+
disabled: boolean;
|
|
14
|
+
onChange(value: CaptureDensityMode): void;
|
|
15
|
+
}
|
|
16
|
+
const styles = StyleSheet.create({
|
|
17
|
+
group: { gap: 8 },
|
|
18
|
+
heading: { fontSize: 11, fontWeight: "600" },
|
|
19
|
+
choice: { minHeight: 44, padding: 12, borderWidth: 1, borderRadius: 8 },
|
|
20
|
+
label: { fontSize: 13, fontWeight: "600" },
|
|
21
|
+
detail: { fontSize: 11 },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/** Selection reflects actual state, including a preset whose density was changed independently. */
|
|
25
|
+
export function BrowserCaptureDensityControls(props: Props) {
|
|
26
|
+
const colors = props.theme.colors;
|
|
27
|
+
return (
|
|
28
|
+
<View style={styles.group}>
|
|
29
|
+
<Text style={[styles.heading, { color: colors.foregroundMuted }]}>Capture density</Text>
|
|
30
|
+
{(["auto", 1, 2] as const).map((density) => {
|
|
31
|
+
const supported =
|
|
32
|
+
props.viewport !== null &&
|
|
33
|
+
(density === "auto" || canUseCaptureDensity(props.viewport, density));
|
|
34
|
+
const disabled = props.disabled || !supported;
|
|
35
|
+
const selected = props.mode === density;
|
|
36
|
+
let label = `${density}×`;
|
|
37
|
+
if (density === "auto") {
|
|
38
|
+
label = selected ? `Auto (${props.density}×)` : "Auto";
|
|
39
|
+
}
|
|
40
|
+
return (
|
|
41
|
+
<Pressable
|
|
42
|
+
key={density}
|
|
43
|
+
accessibilityRole="button"
|
|
44
|
+
accessibilityLabel={`Capture density: ${label}`}
|
|
45
|
+
accessibilityState={{ selected, disabled }}
|
|
46
|
+
disabled={disabled}
|
|
47
|
+
onPress={() => {
|
|
48
|
+
if (!disabled) props.onChange(density);
|
|
49
|
+
}}
|
|
50
|
+
style={({ pressed }) => [
|
|
51
|
+
styles.choice,
|
|
52
|
+
{
|
|
53
|
+
borderColor: selected ? colors.accent : colors.border,
|
|
54
|
+
backgroundColor: selected ? colors.surface2 : colors.surface1,
|
|
55
|
+
opacity: disabled ? 0.45 : pressed ? 0.72 : 1,
|
|
56
|
+
},
|
|
57
|
+
]}
|
|
58
|
+
>
|
|
59
|
+
<Text style={[styles.label, { color: colors.foreground }]}>{label}</Text>
|
|
60
|
+
</Pressable>
|
|
61
|
+
);
|
|
62
|
+
})}
|
|
63
|
+
<Text style={[styles.detail, { color: colors.foregroundMuted }]}>
|
|
64
|
+
Auto matches the displayed size and screen density up to 2× while controlling. Higher
|
|
65
|
+
density uses more bandwidth.
|
|
66
|
+
</Text>
|
|
67
|
+
{props.viewport && !canUseCaptureDensity(props.viewport, 2) ? (
|
|
68
|
+
<Text style={[styles.detail, { color: colors.foregroundMuted }]}>
|
|
69
|
+
2× requires a page width of {Math.floor(MAX_VIEWPORT.width / 2)} or less and a height of{" "}
|
|
70
|
+
{Math.floor(MAX_VIEWPORT.height / 2)} or less.
|
|
71
|
+
</Text>
|
|
72
|
+
) : null}
|
|
73
|
+
</View>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/** Public themed panel controls and notices. Leaves own only focus/hover styling, never browser authority. */
|
|
2
|
+
import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
|
|
3
|
+
import { Icon, TextInput } from "@getpaseo/plugin/client/react-native";
|
|
4
|
+
import { type ReactNode, useCallback, useState } from "react";
|
|
5
|
+
import {
|
|
6
|
+
ActivityIndicator,
|
|
7
|
+
Pressable,
|
|
8
|
+
type StyleProp,
|
|
9
|
+
Text,
|
|
10
|
+
type TextInputProps,
|
|
11
|
+
type TextStyle,
|
|
12
|
+
View,
|
|
13
|
+
type ViewStyle,
|
|
14
|
+
} from "react-native";
|
|
15
|
+
import { ControlButton, type ControlButtonStyles } from "./browser-control-button";
|
|
16
|
+
import { setBrowserControlTooltip } from "./browser-control-tooltip-web";
|
|
17
|
+
import { DIMENSION } from "./browser-panel-styles";
|
|
18
|
+
|
|
19
|
+
type Theme = PluginWorkspacePanelProps["theme"];
|
|
20
|
+
|
|
21
|
+
export { ControlButton } from "./browser-control-button";
|
|
22
|
+
|
|
23
|
+
interface FieldStyles {
|
|
24
|
+
field: TextStyle;
|
|
25
|
+
fieldFocused: TextStyle;
|
|
26
|
+
fieldDisabled: TextStyle;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ErrorNoticeStyles extends ControlButtonStyles {
|
|
30
|
+
errorRow: ViewStyle;
|
|
31
|
+
errorText: TextStyle;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface CanvasPlaceholderStyles {
|
|
35
|
+
canvasState: ViewStyle;
|
|
36
|
+
canvasTitle: TextStyle;
|
|
37
|
+
canvasDetail: TextStyle;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ChromeIconButtonStyles {
|
|
41
|
+
chromeIconButton: ViewStyle;
|
|
42
|
+
chromeIconButtonHovered: ViewStyle;
|
|
43
|
+
chromeIconButtonPressed: ViewStyle;
|
|
44
|
+
chromeIconButtonDisabled: ViewStyle;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Themed button leaf using public SDK/RN primitives. */
|
|
48
|
+
export function ChromeIconButton({
|
|
49
|
+
styles,
|
|
50
|
+
theme,
|
|
51
|
+
label,
|
|
52
|
+
icon,
|
|
53
|
+
iconNode,
|
|
54
|
+
selected = false,
|
|
55
|
+
expanded,
|
|
56
|
+
disabled = false,
|
|
57
|
+
onPress,
|
|
58
|
+
}: {
|
|
59
|
+
styles: ChromeIconButtonStyles;
|
|
60
|
+
theme: Theme;
|
|
61
|
+
label: string;
|
|
62
|
+
icon: string;
|
|
63
|
+
iconNode?: ReactNode;
|
|
64
|
+
expanded?: boolean;
|
|
65
|
+
selected?: boolean;
|
|
66
|
+
disabled?: boolean;
|
|
67
|
+
onPress(): void;
|
|
68
|
+
}) {
|
|
69
|
+
const [hovered, setHovered] = useState(false);
|
|
70
|
+
const tooltipRef = useCallback((node: unknown) => setBrowserControlTooltip(node, label), [label]);
|
|
71
|
+
return (
|
|
72
|
+
<Pressable
|
|
73
|
+
ref={tooltipRef}
|
|
74
|
+
accessibilityRole="button"
|
|
75
|
+
accessibilityLabel={label}
|
|
76
|
+
accessibilityState={{ disabled, selected, ...(expanded === undefined ? {} : { expanded }) }}
|
|
77
|
+
disabled={disabled}
|
|
78
|
+
onHoverIn={() => setHovered(true)}
|
|
79
|
+
onHoverOut={() => setHovered(false)}
|
|
80
|
+
onPress={onPress}
|
|
81
|
+
style={({ pressed }) => [
|
|
82
|
+
styles.chromeIconButton,
|
|
83
|
+
hovered ? styles.chromeIconButtonHovered : null,
|
|
84
|
+
pressed ? styles.chromeIconButtonPressed : null,
|
|
85
|
+
disabled ? styles.chromeIconButtonDisabled : null,
|
|
86
|
+
]}
|
|
87
|
+
>
|
|
88
|
+
{iconNode ?? (
|
|
89
|
+
<Icon
|
|
90
|
+
name={icon}
|
|
91
|
+
size={16}
|
|
92
|
+
color={selected ? theme.colors.accent : theme.colors.foregroundMuted}
|
|
93
|
+
/>
|
|
94
|
+
)}
|
|
95
|
+
</Pressable>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface FieldProps {
|
|
100
|
+
styles: FieldStyles;
|
|
101
|
+
theme: Theme;
|
|
102
|
+
value: string;
|
|
103
|
+
accessibilityLabel: string;
|
|
104
|
+
placeholder?: string;
|
|
105
|
+
editable?: boolean;
|
|
106
|
+
/** Read-only information can stay legible without implying it is editable. */
|
|
107
|
+
dimWhenReadOnly?: boolean;
|
|
108
|
+
maxLength?: number;
|
|
109
|
+
keyboardType?: TextInputProps["keyboardType"];
|
|
110
|
+
inputMode?: TextInputProps["inputMode"];
|
|
111
|
+
returnKeyType?: TextInputProps["returnKeyType"];
|
|
112
|
+
selectTextOnFocus?: boolean;
|
|
113
|
+
style?: StyleProp<TextStyle>;
|
|
114
|
+
onChangeText(value: string): void;
|
|
115
|
+
onSubmit?(): void;
|
|
116
|
+
onFocus?(): void;
|
|
117
|
+
onBlur?(): void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Themed panel field leaf using public SDK/RN primitives. */
|
|
121
|
+
export function Field({
|
|
122
|
+
styles,
|
|
123
|
+
theme,
|
|
124
|
+
value,
|
|
125
|
+
accessibilityLabel,
|
|
126
|
+
placeholder,
|
|
127
|
+
editable = true,
|
|
128
|
+
dimWhenReadOnly = true,
|
|
129
|
+
maxLength,
|
|
130
|
+
keyboardType,
|
|
131
|
+
inputMode,
|
|
132
|
+
returnKeyType,
|
|
133
|
+
selectTextOnFocus,
|
|
134
|
+
style,
|
|
135
|
+
onChangeText,
|
|
136
|
+
onSubmit,
|
|
137
|
+
onFocus,
|
|
138
|
+
onBlur,
|
|
139
|
+
}: FieldProps) {
|
|
140
|
+
const [focused, setFocused] = useState(false);
|
|
141
|
+
return (
|
|
142
|
+
<TextInput
|
|
143
|
+
accessibilityLabel={accessibilityLabel}
|
|
144
|
+
autoCapitalize="none"
|
|
145
|
+
autoCorrect={false}
|
|
146
|
+
spellCheck={false}
|
|
147
|
+
editable={editable}
|
|
148
|
+
keyboardType={keyboardType}
|
|
149
|
+
inputMode={inputMode}
|
|
150
|
+
maxLength={maxLength}
|
|
151
|
+
onBlur={() => {
|
|
152
|
+
setFocused(false);
|
|
153
|
+
onBlur?.();
|
|
154
|
+
}}
|
|
155
|
+
onChangeText={onChangeText}
|
|
156
|
+
onFocus={() => {
|
|
157
|
+
setFocused(true);
|
|
158
|
+
onFocus?.();
|
|
159
|
+
}}
|
|
160
|
+
onSubmitEditing={onSubmit}
|
|
161
|
+
placeholder={placeholder}
|
|
162
|
+
placeholderTextColor={theme.colors.foregroundMuted}
|
|
163
|
+
returnKeyType={returnKeyType}
|
|
164
|
+
selectionColor={theme.colors.accent}
|
|
165
|
+
selectTextOnFocus={selectTextOnFocus}
|
|
166
|
+
style={[
|
|
167
|
+
styles.field,
|
|
168
|
+
style,
|
|
169
|
+
focused ? styles.fieldFocused : null,
|
|
170
|
+
!editable && dimWhenReadOnly ? styles.fieldDisabled : null,
|
|
171
|
+
]}
|
|
172
|
+
value={value}
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Themed panel errornotice leaf using public SDK/RN primitives. */
|
|
178
|
+
export function ErrorNotice({
|
|
179
|
+
styles,
|
|
180
|
+
theme,
|
|
181
|
+
message,
|
|
182
|
+
action,
|
|
183
|
+
onAction,
|
|
184
|
+
actionDisabled = false,
|
|
185
|
+
}: {
|
|
186
|
+
styles: ErrorNoticeStyles;
|
|
187
|
+
theme: Theme;
|
|
188
|
+
message: string;
|
|
189
|
+
action?: string | undefined;
|
|
190
|
+
onAction?: (() => void) | undefined;
|
|
191
|
+
actionDisabled?: boolean;
|
|
192
|
+
}) {
|
|
193
|
+
return (
|
|
194
|
+
<View accessibilityRole="alert" style={styles.errorRow}>
|
|
195
|
+
<Icon name="CircleAlert" size={DIMENSION.icon} color={theme.colors.statusDanger} />
|
|
196
|
+
<Text style={styles.errorText}>{message}</Text>
|
|
197
|
+
{action && onAction ? (
|
|
198
|
+
<ControlButton
|
|
199
|
+
styles={styles}
|
|
200
|
+
theme={theme}
|
|
201
|
+
label={action}
|
|
202
|
+
disabled={actionDisabled}
|
|
203
|
+
onPress={onAction}
|
|
204
|
+
/>
|
|
205
|
+
) : null}
|
|
206
|
+
</View>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Themed panel canvasplaceholder leaf using public SDK/RN primitives. */
|
|
211
|
+
export function CanvasPlaceholder({
|
|
212
|
+
styles,
|
|
213
|
+
theme,
|
|
214
|
+
title,
|
|
215
|
+
detail,
|
|
216
|
+
loading = false,
|
|
217
|
+
children,
|
|
218
|
+
}: {
|
|
219
|
+
styles: CanvasPlaceholderStyles;
|
|
220
|
+
theme: Theme;
|
|
221
|
+
title: string;
|
|
222
|
+
detail: string;
|
|
223
|
+
loading?: boolean;
|
|
224
|
+
children?: ReactNode;
|
|
225
|
+
}) {
|
|
226
|
+
return (
|
|
227
|
+
<View style={styles.canvasState}>
|
|
228
|
+
{loading ? <ActivityIndicator color={theme.colors.accent} /> : null}
|
|
229
|
+
<Text style={styles.canvasTitle}>{title}</Text>
|
|
230
|
+
<Text style={styles.canvasDetail}>{detail}</Text>
|
|
231
|
+
{children}
|
|
232
|
+
</View>
|
|
233
|
+
);
|
|
234
|
+
}
|