@omercnet/paseo-shared-browser 1.2.2-next.232.1 → 1.2.2-next.237.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 (168) hide show
  1. package/README.md +312 -25
  2. package/REMOTE_BROWSER_RESEARCH.md +395 -0
  3. package/STREAMING_DESIGN.md +191 -0
  4. package/client/browser-auto-capture-density.test.ts +102 -0
  5. package/client/browser-auto-capture-density.ts +30 -0
  6. package/client/browser-canvas-input.test.ts +198 -0
  7. package/client/browser-canvas-input.ts +202 -0
  8. package/client/browser-canvas-keyboard-web.test.ts +113 -0
  9. package/client/browser-canvas-keyboard-web.ts +230 -0
  10. package/client/browser-canvas-keyboard.test.ts +123 -0
  11. package/client/browser-canvas-keyboard.ts +126 -0
  12. package/client/browser-canvas-layout.test.ts +109 -0
  13. package/client/browser-canvas-layout.ts +51 -0
  14. package/client/browser-canvas-viewport-web.test.ts +77 -0
  15. package/client/browser-canvas-viewport-web.ts +63 -0
  16. package/client/browser-canvas-viewport.tsx +152 -0
  17. package/client/browser-capture-cadence.test.ts +9 -0
  18. package/client/browser-capture-cadence.ts +6 -0
  19. package/client/browser-capture-density-controls.tsx +75 -0
  20. package/client/browser-chrome.tsx +234 -0
  21. package/client/browser-client-presence.ts +184 -0
  22. package/client/browser-compose-controls.test.tsx +275 -0
  23. package/client/browser-compose-controls.tsx +174 -0
  24. package/client/browser-control-button.tsx +109 -0
  25. package/client/browser-control-dialog.tsx +71 -0
  26. package/client/browser-control-request.test.ts +27 -0
  27. package/client/browser-control-request.ts +57 -0
  28. package/client/browser-control-tooltip-web.ts +10 -0
  29. package/client/browser-dialog-body.tsx +22 -0
  30. package/client/browser-emulation-mode.test.ts +75 -0
  31. package/client/browser-emulation-mode.ts +98 -0
  32. package/client/browser-fill-viewport.test.ts +45 -0
  33. package/client/browser-fill-viewport.ts +28 -0
  34. package/client/browser-frame-buffer.test.ts +157 -0
  35. package/client/browser-frame-buffer.ts +148 -0
  36. package/client/browser-frame-image.tsx +62 -0
  37. package/client/browser-image-load.test.ts +23 -0
  38. package/client/browser-image-load.ts +26 -0
  39. package/client/browser-input-queue.test.ts +1201 -0
  40. package/client/browser-input-queue.ts +505 -0
  41. package/client/browser-menu-placement.test.ts +90 -0
  42. package/client/browser-menu-placement.ts +78 -0
  43. package/client/browser-panel-styles.ts +384 -0
  44. package/client/browser-quality-controls.test.ts +26 -0
  45. package/client/browser-quality-controls.tsx +139 -0
  46. package/client/browser-resolution-picker.tsx +189 -0
  47. package/client/browser-tabs.tsx +117 -0
  48. package/client/browser-toolbar-menu-web.test.ts +185 -0
  49. package/client/browser-toolbar-menu-web.ts +159 -0
  50. package/client/browser-toolbar-menu.tsx +255 -0
  51. package/client/browser-toolbar-submenu.tsx +93 -0
  52. package/client/browser-video-bytes.test.ts +44 -0
  53. package/client/browser-video-decoder.test.ts +473 -0
  54. package/client/browser-video-decoder.ts +311 -0
  55. package/client/browser-video-surface.tsx +21 -0
  56. package/client/browser-video-visibility.test.ts +45 -0
  57. package/client/browser-viewer-recovery.test.ts +111 -0
  58. package/client/browser-viewer-recovery.ts +78 -0
  59. package/client/browser.tsx +1642 -1569
  60. package/client/favorite-star.tsx +23 -0
  61. package/client/frame-lifecycle.ts +62 -59
  62. package/client/use-browser-auto-capture-density.test.ts +104 -0
  63. package/client/use-browser-auto-capture-density.ts +43 -0
  64. package/client/use-browser-canvas-input.ts +370 -0
  65. package/client/use-browser-capture-density.ts +87 -0
  66. package/client/use-browser-emulation-mode.ts +56 -0
  67. package/client/use-browser-fill-viewport.test.ts +108 -0
  68. package/client/use-browser-fill-viewport.ts +62 -0
  69. package/client/use-browser-frame-buffer.ts +66 -0
  70. package/client/use-browser-image-capture.ts +139 -0
  71. package/client/use-browser-scoped-mutation.test.ts +118 -0
  72. package/client/use-browser-scoped-mutation.ts +59 -0
  73. package/client/use-browser-video.test.ts +991 -0
  74. package/client/use-browser-video.ts +474 -0
  75. package/client/use-browser-viewer-recovery.ts +26 -0
  76. package/client/use-resolution-favorites.ts +90 -0
  77. package/client/web.test.ts +125 -0
  78. package/client/web.ts +405 -0
  79. package/index.client.tsx +0 -10
  80. package/index.server.ts +32 -0
  81. package/package.json +9 -5
  82. package/paseo-plugin.json +3 -1
  83. package/scripts/prepare-runtime.mjs +1 -1
  84. package/server/agent-browser-runtime.ts +1598 -314
  85. package/server/browser-cursor.test.ts +64 -0
  86. package/server/browser-cursor.ts +69 -0
  87. package/server/browser-frame-context.ts +22 -0
  88. package/server/browser-frame-epoch.test.ts +95 -0
  89. package/server/browser-frame-rejection.ts +17 -0
  90. package/server/browser-gesture.ts +120 -0
  91. package/server/browser-operation-diagnostics.test.ts +79 -0
  92. package/server/browser-operation-diagnostics.ts +103 -0
  93. package/server/browser-policy.ts +1313 -151
  94. package/server/browser-video-input-authority.test.ts +208 -0
  95. package/server/browser-video-policy.test.ts +564 -0
  96. package/server/browser-video-socket.test.ts +368 -0
  97. package/server/browser.ts +151 -16
  98. package/server/capture-transport-policy.test.ts +105 -0
  99. package/server/capture-transport-policy.ts +154 -0
  100. package/server/capture-transport-runtime.test.ts +213 -0
  101. package/server/daemon-launch-chain.test.ts +70 -0
  102. package/server/daemon-shutdown.test.ts +289 -0
  103. package/server/discrete-input-freshness.test.ts +364 -0
  104. package/server/discrete-input-native-lifetime.test.ts +150 -0
  105. package/server/display-loss-recovery.test.ts +294 -0
  106. package/server/emulation-restoration.test.ts +262 -0
  107. package/server/input-cleanup.ts +23 -0
  108. package/server/input-generation.test.ts +31 -0
  109. package/server/input-generation.ts +36 -0
  110. package/server/jpeg-capture-demand.test.ts +76 -0
  111. package/server/jpeg-capture-demand.ts +98 -0
  112. package/server/jpeg-frame.ts +29 -0
  113. package/server/keyboard-input.test.ts +61 -0
  114. package/server/keyboard-input.ts +104 -0
  115. package/server/live-input-policy.test.ts +958 -0
  116. package/server/live-input-runtime.test.ts +732 -0
  117. package/server/live-input-supervisor.test.ts +147 -0
  118. package/server/mcp-entry.ts +44 -2
  119. package/server/native-video-backpressure.test.ts +228 -0
  120. package/server/native-video-buffer.test.ts +141 -0
  121. package/server/native-video-buffer.ts +114 -0
  122. package/server/native-video-capture.test.ts +599 -0
  123. package/server/native-video-capture.ts +497 -0
  124. package/server/native-video-extension-source.test.ts +201 -0
  125. package/server/native-video-extension-source.ts +364 -0
  126. package/server/native-video-extension.test.ts +41 -0
  127. package/server/native-video-extension.ts +83 -0
  128. package/server/native-video-packet.ts +9 -0
  129. package/server/native-video-settings.test.ts +245 -0
  130. package/server/navigation-budget.ts +8 -0
  131. package/server/navigation-commit.ts +125 -0
  132. package/server/navigation-policy.test.ts +153 -0
  133. package/server/process-identity.ts +58 -0
  134. package/server/reattach-held-input.test.ts +143 -0
  135. package/server/runtime-frame-reuse.test.ts +295 -0
  136. package/server/runtime-jpeg-demand.test.ts +200 -0
  137. package/server/runtime-jpeg-shutdown.test.ts +210 -0
  138. package/server/runtime-jpeg-source-order.test.ts +201 -0
  139. package/server/runtime-launch-environment.test.ts +111 -0
  140. package/server/runtime-navigation.test.ts +327 -0
  141. package/server/runtime-owner-display.test.ts +152 -0
  142. package/server/runtime-owner.ts +305 -31
  143. package/server/runtime-page-state.test.ts +249 -0
  144. package/server/runtime-protocol.ts +31 -1
  145. package/server/runtime-target-retention.test.ts +181 -0
  146. package/server/runtime-video-opt-in.test.ts +82 -0
  147. package/server/runtime-video.test.ts +444 -0
  148. package/server/screencast-source-time.ts +46 -0
  149. package/server/stale-wheel-admission.test.ts +136 -0
  150. package/server/supervisor-client.ts +28 -4
  151. package/server/supervisor-heartbeat.test.ts +80 -0
  152. package/server/supervisor.ts +553 -55
  153. package/server/video-source-recovery.test.ts +20 -0
  154. package/server/video-source-recovery.ts +64 -0
  155. package/server/viewer-capture-lifetime.ts +51 -0
  156. package/server/virtual-display.test.ts +184 -0
  157. package/server/virtual-display.ts +236 -0
  158. package/shared/browser-display-preferences.ts +46 -0
  159. package/shared/browser-video.ts +80 -0
  160. package/shared/browser.ts +286 -65
  161. package/shared/capture-density.ts +16 -0
  162. package/shared/capture-settings.ts +17 -0
  163. package/shared/device-presets.ts +90 -0
  164. package/shared/resolution-menu.test.ts +120 -0
  165. package/shared/resolution-menu.ts +72 -0
  166. package/shared/video-settings.test.ts +38 -0
  167. package/shared/video-settings.ts +34 -0
  168. package/shared/viewport-limits.ts +5 -0
@@ -0,0 +1,384 @@
1
+ /** Own themed panel spacing and styles; geometry/input policy lives in the panel and canvas modules. */
2
+ import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
3
+ import { StyleSheet } from "react-native";
4
+
5
+ type Theme = PluginWorkspacePanelProps["theme"];
6
+
7
+ export const SPACE = {
8
+ xxs: 2,
9
+ xs: 4,
10
+ sm: 8,
11
+ md: 12,
12
+ lg: 16,
13
+ } as const;
14
+ const RADIUS = { sm: 6, md: 8, lg: 10 } as const;
15
+ const TYPE = { caption: 11, body: 13, title: 14 } as const;
16
+ export const DIMENSION = {
17
+ control: 34,
18
+ touch: 44,
19
+ icon: 15,
20
+ addressRegular: 220,
21
+ canvasCompact: 220,
22
+ canvasRegular: 320,
23
+ helperMax: 420,
24
+ viewportField: 72,
25
+ screenRadius: 20,
26
+ } as const;
27
+ /** Build stable theme/compact styles without changing shared browser state. */
28
+ export function createStyles(theme: Theme, compact: boolean) {
29
+ return StyleSheet.create({
30
+ screen: {
31
+ flex: 1,
32
+ minHeight: 0,
33
+ backgroundColor: theme.colors.surface0,
34
+ },
35
+ tabBar: {
36
+ minHeight: DIMENSION.touch,
37
+ paddingHorizontal: SPACE.sm,
38
+ flexDirection: "row",
39
+ alignItems: "center",
40
+ gap: SPACE.xs,
41
+ borderBottomWidth: 1,
42
+ borderBottomColor: theme.colors.border,
43
+ backgroundColor: theme.colors.surface1,
44
+ },
45
+ tabList: {
46
+ flex: 1,
47
+ minWidth: 0,
48
+ },
49
+ tabListContent: {
50
+ alignItems: "center",
51
+ gap: SPACE.xs,
52
+ },
53
+ tabItem: {
54
+ minHeight: DIMENSION.touch,
55
+ width: compact ? 132 : 176,
56
+ flexDirection: "row",
57
+ alignItems: "center",
58
+ borderBottomWidth: 2,
59
+ borderBottomColor: "transparent",
60
+ },
61
+ tabItemSelected: {
62
+ width: compact ? 200 : 230,
63
+ borderBottomColor: theme.colors.accent,
64
+ },
65
+ tabSelect: {
66
+ flex: 1,
67
+ minWidth: 0,
68
+ paddingHorizontal: SPACE.sm,
69
+ paddingVertical: SPACE.xxs,
70
+ justifyContent: "center",
71
+ },
72
+ tabMetaRow: {
73
+ flexDirection: "row",
74
+ alignItems: "center",
75
+ gap: SPACE.xs,
76
+ },
77
+ tabMeta: {
78
+ flexShrink: 1,
79
+ color: theme.colors.foregroundMuted,
80
+ fontSize: TYPE.caption,
81
+ },
82
+ tabActions: {
83
+ flexDirection: "row",
84
+ alignItems: "center",
85
+ gap: SPACE.xxs,
86
+ paddingRight: SPACE.xs,
87
+ },
88
+ tabLabel: {
89
+ color: theme.colors.foregroundMuted,
90
+ fontSize: TYPE.body,
91
+ },
92
+ tabLabelSelected: {
93
+ color: theme.colors.foreground,
94
+ fontWeight: "600",
95
+ },
96
+ statusDot: {
97
+ width: SPACE.sm,
98
+ height: SPACE.sm,
99
+ borderRadius: RADIUS.sm,
100
+ },
101
+ mutedText: {
102
+ color: theme.colors.foregroundMuted,
103
+ fontSize: TYPE.caption,
104
+ },
105
+ chrome: {
106
+ minHeight: 40,
107
+ paddingHorizontal: SPACE.sm,
108
+ paddingVertical: SPACE.xs,
109
+ borderBottomWidth: 1,
110
+ borderBottomColor: theme.colors.border,
111
+ backgroundColor: theme.colors.surface0,
112
+ },
113
+ addressRow: {
114
+ flexDirection: "row",
115
+ alignItems: "center",
116
+ gap: SPACE.xs,
117
+ },
118
+ addressInput: {
119
+ flex: 1,
120
+ minWidth: compact ? 40 : DIMENSION.addressRegular,
121
+ },
122
+ chromeAddressInput: {
123
+ height: compact ? 34 : 28,
124
+ borderRadius: RADIUS.md,
125
+ backgroundColor: theme.colors.surface1,
126
+ // Compact chrome must override the themed input's form-sized padding.
127
+ // Keep a full text line even on native hosts with their own font padding.
128
+ paddingVertical: 0,
129
+ paddingHorizontal: SPACE.xs,
130
+ fontSize: TYPE.body,
131
+ lineHeight: 18,
132
+ includeFontPadding: false,
133
+ textAlignVertical: "center",
134
+ },
135
+ chromeIconButton: {
136
+ width: 28,
137
+ height: 28,
138
+ borderRadius: RADIUS.md,
139
+ alignItems: "center",
140
+ justifyContent: "center",
141
+ },
142
+ chromeIconButtonHovered: {
143
+ backgroundColor: theme.colors.surface2,
144
+ },
145
+ chromeIconButtonPressed: {
146
+ opacity: 0.72,
147
+ },
148
+ chromeIconButtonDisabled: {
149
+ opacity: 0.45,
150
+ },
151
+
152
+ button: {
153
+ minHeight: DIMENSION.control,
154
+ minWidth: DIMENSION.control,
155
+ paddingHorizontal: SPACE.sm,
156
+ borderWidth: 1,
157
+ borderColor: theme.colors.border,
158
+ borderRadius: RADIUS.sm,
159
+ backgroundColor: theme.colors.surface2,
160
+ flexDirection: "row",
161
+ alignItems: "center",
162
+ justifyContent: "center",
163
+ gap: SPACE.xs,
164
+ },
165
+ buttonIconOnly: {
166
+ width: DIMENSION.control,
167
+ minWidth: DIMENSION.control,
168
+ paddingHorizontal: 0,
169
+ },
170
+ buttonSelected: {
171
+ borderColor: theme.colors.accent,
172
+ backgroundColor: theme.colors.accent,
173
+ },
174
+ buttonPrimary: {
175
+ borderColor: theme.colors.accent,
176
+ backgroundColor: theme.colors.accent,
177
+ },
178
+ buttonDanger: {
179
+ borderColor: theme.colors.statusWarning,
180
+ },
181
+ buttonHovered: {
182
+ borderColor: theme.colors.foregroundMuted,
183
+ },
184
+ buttonPressed: {
185
+ opacity: 0.72,
186
+ },
187
+ buttonFocused: {
188
+ borderColor: theme.colors.accent,
189
+ borderWidth: 2,
190
+ },
191
+ buttonDisabled: {
192
+ opacity: 0.42,
193
+ },
194
+ buttonText: {
195
+ color: theme.colors.foreground,
196
+ fontSize: TYPE.caption,
197
+ fontWeight: "600",
198
+ },
199
+ buttonTextSelected: {
200
+ color: theme.colors.accentForeground,
201
+ },
202
+ field: {
203
+ height: DIMENSION.control,
204
+ paddingHorizontal: SPACE.sm,
205
+ borderWidth: 1,
206
+ borderColor: theme.colors.border,
207
+ borderRadius: RADIUS.sm,
208
+ color: theme.colors.foreground,
209
+ backgroundColor: theme.colors.surface2,
210
+ fontSize: TYPE.body,
211
+ },
212
+ fieldFocused: {
213
+ borderColor: theme.colors.accent,
214
+ borderWidth: 2,
215
+ },
216
+ fieldDisabled: {
217
+ opacity: 0.5,
218
+ },
219
+ errorRow: {
220
+ paddingHorizontal: SPACE.sm,
221
+ paddingVertical: SPACE.xs,
222
+ borderLeftWidth: SPACE.xs,
223
+ borderLeftColor: theme.colors.statusDanger,
224
+ backgroundColor: theme.colors.surface1,
225
+ flexDirection: "row",
226
+ alignItems: "center",
227
+ gap: SPACE.sm,
228
+ },
229
+ errorText: {
230
+ flex: 1,
231
+ color: theme.colors.statusDanger,
232
+ fontSize: TYPE.caption,
233
+ },
234
+ canvasShell: {
235
+ flex: 1,
236
+ minHeight: compact ? DIMENSION.canvasCompact : DIMENSION.canvasRegular,
237
+ minWidth: 0,
238
+ // Keep the compact side inset without adding blank bands above and below the feed.
239
+ marginHorizontal: compact ? SPACE.sm : 0,
240
+ borderRadius: compact ? DIMENSION.screenRadius : 0,
241
+ overflow: "hidden",
242
+ backgroundColor: theme.colors.surface1,
243
+ },
244
+ canvas: {
245
+ flex: 1,
246
+ minHeight: 0,
247
+ overflow: "hidden",
248
+ },
249
+ interactionLayer: {
250
+ position: "absolute",
251
+ },
252
+ canvasState: {
253
+ flex: 1,
254
+ alignItems: "center",
255
+ justifyContent: "center",
256
+ padding: SPACE.lg,
257
+ gap: SPACE.sm,
258
+ },
259
+ canvasTitle: {
260
+ color: theme.colors.foreground,
261
+ fontSize: TYPE.title,
262
+ fontWeight: "600",
263
+ textAlign: "center",
264
+ },
265
+ canvasDetail: {
266
+ color: theme.colors.foregroundMuted,
267
+ fontSize: TYPE.caption,
268
+ textAlign: "center",
269
+ maxWidth: DIMENSION.helperMax,
270
+ },
271
+ canvasFooter: {
272
+ minHeight: DIMENSION.control,
273
+ paddingHorizontal: SPACE.sm,
274
+ borderTopWidth: 1,
275
+ borderTopColor: theme.colors.border,
276
+ flexDirection: "row",
277
+ alignItems: "center",
278
+ justifyContent: "space-between",
279
+ gap: SPACE.sm,
280
+ backgroundColor: theme.colors.surface1,
281
+ },
282
+ canvasFooterText: {
283
+ color: theme.colors.foregroundMuted,
284
+ fontSize: TYPE.caption,
285
+ flexShrink: 1,
286
+ },
287
+
288
+ mobileRow: {
289
+ flexDirection: "row",
290
+ alignItems: "center",
291
+ gap: SPACE.sm,
292
+ },
293
+ sheetContent: {
294
+ gap: SPACE.md,
295
+ padding: SPACE.sm,
296
+ },
297
+ sheetGrid: {
298
+ flexDirection: "row",
299
+ flexWrap: "wrap",
300
+ gap: SPACE.sm,
301
+ },
302
+ controlStrip: {
303
+ minHeight: DIMENSION.control,
304
+ flexDirection: "row",
305
+ alignItems: "center",
306
+ gap: SPACE.xs,
307
+ },
308
+ dialogBody: {
309
+ gap: SPACE.md,
310
+ paddingBottom: SPACE.xs,
311
+ },
312
+ dialogMessage: {
313
+ color: theme.colors.foregroundMuted,
314
+ fontSize: TYPE.body,
315
+ lineHeight: 20,
316
+ textAlign: "left",
317
+ },
318
+ dialogActions: {
319
+ flexDirection: "row",
320
+ justifyContent: "flex-end",
321
+ alignItems: "center",
322
+ gap: SPACE.sm,
323
+ paddingTop: SPACE.xs,
324
+ },
325
+ stripLabel: {
326
+ color: theme.colors.foregroundMuted,
327
+ fontSize: TYPE.caption,
328
+ fontWeight: "600",
329
+ marginRight: SPACE.xs,
330
+ },
331
+ viewportField: {
332
+ width: DIMENSION.viewportField,
333
+ textAlign: "center",
334
+ },
335
+ multiply: {
336
+ color: theme.colors.foregroundMuted,
337
+ fontSize: TYPE.body,
338
+ },
339
+
340
+ deviceModalContent: {
341
+ gap: SPACE.sm,
342
+ padding: SPACE.sm,
343
+ },
344
+ devicePresetRow: {
345
+ minHeight: 46,
346
+ paddingHorizontal: SPACE.md,
347
+ paddingVertical: SPACE.sm,
348
+ borderWidth: 1,
349
+ borderColor: theme.colors.border,
350
+ borderRadius: RADIUS.md,
351
+ backgroundColor: theme.colors.surface1,
352
+ flexDirection: "row",
353
+ alignItems: "center",
354
+ gap: SPACE.sm,
355
+ },
356
+ devicePresetRowSelected: {
357
+ borderColor: theme.colors.accent,
358
+ backgroundColor: theme.colors.surface2,
359
+ },
360
+ devicePresetText: {
361
+ flex: 1,
362
+ color: theme.colors.foreground,
363
+ fontSize: TYPE.body,
364
+ fontWeight: "600",
365
+ },
366
+ devicePresetDetail: {
367
+ color: theme.colors.foregroundMuted,
368
+ fontSize: TYPE.caption,
369
+ },
370
+ customViewportRow: {
371
+ flexDirection: "row",
372
+ alignItems: "center",
373
+ gap: SPACE.xs,
374
+ },
375
+ buttonLarge: {
376
+ minHeight: DIMENSION.touch,
377
+ paddingHorizontal: SPACE.md,
378
+ borderRadius: RADIUS.md,
379
+ },
380
+ buttonFill: {
381
+ flex: 1,
382
+ },
383
+ });
384
+ }
@@ -0,0 +1,26 @@
1
+ import { expect, it, vi } from "vitest";
2
+
3
+ vi.mock("react-native", () => ({
4
+ Pressable: "Pressable",
5
+ StyleSheet: { create: (styles: unknown) => styles },
6
+ Text: "Text",
7
+ View: "View",
8
+ }));
9
+ vi.mock("./browser-capture-density-controls", () => ({
10
+ BrowserCaptureDensityControls: () => null,
11
+ }));
12
+
13
+ const { DEFAULT_CAPTURE_QUALITY } = await import("../shared/capture-settings");
14
+ const { jpegQualityOptions } = await import("./browser-quality-controls");
15
+
16
+ it("badges exactly the actual default JPEG quality, not a hard-coded tier", () => {
17
+ const options = jpegQualityOptions();
18
+ expect(options.filter((option) => "detail" in option && option.detail === "Default")).toEqual([
19
+ expect.objectContaining({ value: DEFAULT_CAPTURE_QUALITY, label: "65%" }),
20
+ ]);
21
+ expect(options.find((option) => option.value === "high")).not.toHaveProperty("detail");
22
+ expect(options.find((option) => option.value === "maximum")).toHaveProperty(
23
+ "detail",
24
+ "Largest JPEGs",
25
+ );
26
+ });
@@ -0,0 +1,139 @@
1
+ /** Independent viewer compression/rate choices. These settings never resize or
2
+ * navigate the shared page; pixel density remains its existing owner control. */
3
+ import type { PluginHostProps } from "@getpaseo/plugin/client";
4
+ import { Pressable, StyleSheet, Text, View } from "react-native";
5
+ import type { BrowserDisplayPreferences } from "../shared/browser-display-preferences";
6
+ import { DEFAULT_CAPTURE_QUALITY, JPEG_QUALITY } from "../shared/capture-settings";
7
+ import { VIDEO_BITRATES, VIDEO_FRAME_RATES } from "../shared/video-settings";
8
+ import type { CaptureDensityMode } from "./browser-auto-capture-density";
9
+ import { BrowserCaptureDensityControls } from "./browser-capture-density-controls";
10
+
11
+ const styles = StyleSheet.create({
12
+ group: { gap: 8 },
13
+ heading: { fontSize: 11, fontWeight: "600" },
14
+ choice: {
15
+ minHeight: 44,
16
+ paddingHorizontal: 12,
17
+ paddingVertical: 8,
18
+ borderWidth: 1,
19
+ borderRadius: 8,
20
+ flexDirection: "row",
21
+ alignItems: "center",
22
+ gap: 8,
23
+ },
24
+ label: { flex: 1, fontSize: 13, fontWeight: "600" },
25
+ detail: { fontSize: 11 },
26
+ });
27
+ interface Props {
28
+ theme: PluginHostProps["theme"];
29
+ disabled: boolean;
30
+ density: number;
31
+ densityMode: CaptureDensityMode;
32
+ viewport: { width: number; height: number } | null;
33
+ densityDisabled: boolean;
34
+ onDensityChange(value: CaptureDensityMode): void;
35
+ captureQuality: BrowserDisplayPreferences["captureQuality"];
36
+ videoBitrate: BrowserDisplayPreferences["videoBitrate"];
37
+ videoFps: BrowserDisplayPreferences["videoFps"];
38
+ onQualityChange(value: Props["captureQuality"]): void;
39
+ onVideoBitrateChange(value: Props["videoBitrate"]): void;
40
+ onVideoFpsChange(value: Props["videoFps"]): void;
41
+ }
42
+ function Choices<T extends string | number>(props: {
43
+ theme: Props["theme"];
44
+ disabled: boolean;
45
+ title: string;
46
+ value: T;
47
+ options: readonly { value: T; label: string; detail?: string }[];
48
+ onChange(value: T): void;
49
+ }) {
50
+ const colors = props.theme.colors;
51
+ return (
52
+ <View style={styles.group}>
53
+ <Text style={[styles.heading, { color: colors.foregroundMuted }]}>{props.title}</Text>
54
+ {props.options.map((option) => {
55
+ const selected = option.value === props.value;
56
+ return (
57
+ <Pressable
58
+ key={option.value}
59
+ accessibilityRole="button"
60
+ accessibilityLabel={`${props.title}: ${option.label}`}
61
+ accessibilityState={{ selected, disabled: props.disabled }}
62
+ disabled={props.disabled}
63
+ onPress={() => props.onChange(option.value)}
64
+ style={({ pressed }) => [
65
+ styles.choice,
66
+ {
67
+ borderColor: selected ? colors.accent : colors.border,
68
+ backgroundColor: selected ? colors.surface2 : colors.surface1,
69
+ opacity: props.disabled ? 0.45 : pressed ? 0.72 : 1,
70
+ },
71
+ ]}
72
+ >
73
+ <Text style={[styles.label, { color: colors.foreground }]}>{option.label}</Text>
74
+ {option.detail ? (
75
+ <Text style={[styles.detail, { color: colors.foregroundMuted }]}>
76
+ {option.detail}
77
+ </Text>
78
+ ) : null}
79
+ </Pressable>
80
+ );
81
+ })}
82
+ </View>
83
+ );
84
+ }
85
+ /** Explicit values describe the actual JPEG encoder and independently bounded video cohorts. */
86
+ export function jpegQualityOptions() {
87
+ return (["low", "medium", "high", "maximum"] as const).map((value) => ({
88
+ value,
89
+ label: `${JPEG_QUALITY[value]}%`,
90
+ ...(value === DEFAULT_CAPTURE_QUALITY
91
+ ? { detail: "Default" }
92
+ : value === "maximum"
93
+ ? { detail: "Largest JPEGs" }
94
+ : {}),
95
+ }));
96
+ }
97
+ export function BrowserQualityControls(props: Props) {
98
+ const jpeg = jpegQualityOptions();
99
+ return (
100
+ <View style={{ gap: 12 }}>
101
+ <BrowserCaptureDensityControls
102
+ theme={props.theme}
103
+ density={props.density}
104
+ mode={props.densityMode}
105
+ viewport={props.viewport}
106
+ disabled={props.densityDisabled}
107
+ onChange={props.onDensityChange}
108
+ />
109
+ <Choices
110
+ theme={props.theme}
111
+ disabled={props.disabled}
112
+ title="JPEG quality target"
113
+ value={props.captureQuality}
114
+ options={jpeg}
115
+ onChange={props.onQualityChange}
116
+ />
117
+ <Choices
118
+ theme={props.theme}
119
+ disabled={props.disabled}
120
+ title="Video bitrate"
121
+ value={props.videoBitrate}
122
+ options={VIDEO_BITRATES.map((value) => ({ value, label: `${value / 1_000_000} Mbps` }))}
123
+ onChange={props.onVideoBitrateChange}
124
+ />
125
+ <Choices
126
+ theme={props.theme}
127
+ disabled={props.disabled}
128
+ title="Video frame rate"
129
+ value={props.videoFps}
130
+ options={VIDEO_FRAME_RATES.map((value) => ({ value, label: `${value} FPS` }))}
131
+ onChange={props.onVideoFpsChange}
132
+ />
133
+ <Text style={[styles.detail, { color: props.theme.colors.foregroundMuted }]}>
134
+ Large JPEGs may reduce quality to fit the frame limit. Video settings apply where supported.
135
+ Other clients use JPEGs. Capture density changes detail without resizing the page.
136
+ </Text>
137
+ </View>
138
+ );
139
+ }