peachsim 0.1.527

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 (324) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +640 -0
  3. package/cli/app-config.ts +82 -0
  4. package/cli/app-fonts.ts +388 -0
  5. package/cli/app-project.ts +59 -0
  6. package/cli/app-splash.ts +171 -0
  7. package/cli/app-state-reset.ts +26 -0
  8. package/cli/auth.ts +184 -0
  9. package/cli/bin.ts +59 -0
  10. package/cli/bridge-diagnostics.ts +226 -0
  11. package/cli/bridge-flow-runner.ts +2806 -0
  12. package/cli/browser-backend.ts +44 -0
  13. package/cli/browser-evals.ts +96 -0
  14. package/cli/cli-update.ts +261 -0
  15. package/cli/cloud-client.ts +1530 -0
  16. package/cli/cloud-dispatch.ts +206 -0
  17. package/cli/cloud-session.ts +227 -0
  18. package/cli/command-contract.ts +221 -0
  19. package/cli/command-registry.ts +304 -0
  20. package/cli/commands/agent-wrapper.ts +1048 -0
  21. package/cli/commands/agent.ts +439 -0
  22. package/cli/commands/app-fonts.ts +99 -0
  23. package/cli/commands/assert.ts +542 -0
  24. package/cli/commands/auth.ts +71 -0
  25. package/cli/commands/box/bundle-plane.ts +71 -0
  26. package/cli/commands/box/checkout-plane.ts +256 -0
  27. package/cli/commands/box/plane-share.ts +208 -0
  28. package/cli/commands/box/rnx-command.ts +60 -0
  29. package/cli/commands/box/search-command.ts +157 -0
  30. package/cli/commands/box.ts +925 -0
  31. package/cli/commands/camera.ts +266 -0
  32. package/cli/commands/cleanup.ts +169 -0
  33. package/cli/commands/compat.ts +176 -0
  34. package/cli/commands/config.ts +34 -0
  35. package/cli/commands/control.ts +2154 -0
  36. package/cli/commands/cpu-profile.ts +941 -0
  37. package/cli/commands/daemon-mac-app.ts +165 -0
  38. package/cli/commands/daemon.ts +800 -0
  39. package/cli/commands/debug.ts +751 -0
  40. package/cli/commands/desktop.ts +39 -0
  41. package/cli/commands/detect.ts +201 -0
  42. package/cli/commands/detox.ts +481 -0
  43. package/cli/commands/device.ts +134 -0
  44. package/cli/commands/diagnose.ts +589 -0
  45. package/cli/commands/do-chain.ts +408 -0
  46. package/cli/commands/electron.ts +94 -0
  47. package/cli/commands/film.ts +379 -0
  48. package/cli/commands/flow.ts +1240 -0
  49. package/cli/commands/inspect/actions.ts +516 -0
  50. package/cli/commands/inspect/core.ts +2086 -0
  51. package/cli/commands/inspect/count.ts +17 -0
  52. package/cli/commands/inspect/describe.ts +119 -0
  53. package/cli/commands/inspect/env.ts +25 -0
  54. package/cli/commands/inspect/find.ts +169 -0
  55. package/cli/commands/inspect/get-layout.ts +53 -0
  56. package/cli/commands/inspect/keyboard.ts +54 -0
  57. package/cli/commands/inspect/list.ts +175 -0
  58. package/cli/commands/inspect/memory.ts +215 -0
  59. package/cli/commands/inspect/redaction.ts +39 -0
  60. package/cli/commands/inspect/resolve-target.ts +77 -0
  61. package/cli/commands/inspect/screens.ts +78 -0
  62. package/cli/commands/inspect/settle.ts +32 -0
  63. package/cli/commands/inspect/settling.ts +197 -0
  64. package/cli/commands/inspect/shared.ts +259 -0
  65. package/cli/commands/inspect/sleep.ts +15 -0
  66. package/cli/commands/inspect/tree.ts +23 -0
  67. package/cli/commands/inspect/url.ts +17 -0
  68. package/cli/commands/inspect/wait-event.ts +211 -0
  69. package/cli/commands/inspect/wait-idle.ts +25 -0
  70. package/cli/commands/inspect/wait-ready.ts +87 -0
  71. package/cli/commands/inspect/wait-selector.ts +59 -0
  72. package/cli/commands/inspect/wait.ts +32 -0
  73. package/cli/commands/inspect.ts +4738 -0
  74. package/cli/commands/install-cli.ts +275 -0
  75. package/cli/commands/install-desktop.ts +385 -0
  76. package/cli/commands/login.ts +357 -0
  77. package/cli/commands/logout.ts +24 -0
  78. package/cli/commands/maestro-generate.ts +375 -0
  79. package/cli/commands/maestro.ts +536 -0
  80. package/cli/commands/mode.ts +58 -0
  81. package/cli/commands/no-bridge-hint.ts +80 -0
  82. package/cli/commands/perf.ts +67 -0
  83. package/cli/commands/permissions.ts +203 -0
  84. package/cli/commands/platform.ts +410 -0
  85. package/cli/commands/preview.ts +103 -0
  86. package/cli/commands/profile.ts +106 -0
  87. package/cli/commands/react.ts +353 -0
  88. package/cli/commands/record.ts +1435 -0
  89. package/cli/commands/report-issue.ts +305 -0
  90. package/cli/commands/reset.ts +89 -0
  91. package/cli/commands/runtime.ts +348 -0
  92. package/cli/commands/screenshot-command.ts +106 -0
  93. package/cli/commands/screenshot-layers.ts +144 -0
  94. package/cli/commands/screenshot-mode.ts +38 -0
  95. package/cli/commands/screenshot.ts +492 -0
  96. package/cli/commands/screenshots-capture.ts +608 -0
  97. package/cli/commands/screenshots.ts +127 -0
  98. package/cli/commands/serve.ts +139 -0
  99. package/cli/commands/setup.ts +416 -0
  100. package/cli/commands/shell-boolean-mode.ts +82 -0
  101. package/cli/commands/skills.ts +468 -0
  102. package/cli/commands/slides.ts +357 -0
  103. package/cli/commands/state.ts +94 -0
  104. package/cli/commands/storage.ts +58 -0
  105. package/cli/commands/telemetry.ts +55 -0
  106. package/cli/commands/test.ts +535 -0
  107. package/cli/commands/three-mode.ts +771 -0
  108. package/cli/commands/timeline.ts +121 -0
  109. package/cli/commands/upgrade.ts +167 -0
  110. package/cli/commands/upload.ts +1203 -0
  111. package/cli/commands/version.ts +63 -0
  112. package/cli/commands/welcome.ts +17 -0
  113. package/cli/commands/what-happened.ts +328 -0
  114. package/cli/current-sim.ts +217 -0
  115. package/cli/desktop-companion.ts +315 -0
  116. package/cli/drivers/electron.ts +70 -0
  117. package/cli/drivers/index.ts +20 -0
  118. package/cli/drivers/playwright-provisioning.ts +184 -0
  119. package/cli/drivers/playwright-sim-host.ts +537 -0
  120. package/cli/drivers/playwright.ts +209 -0
  121. package/cli/drivers/registry.ts +65 -0
  122. package/cli/drivers/types.ts +102 -0
  123. package/cli/flow-export.ts +55 -0
  124. package/cli/flow-file.ts +142 -0
  125. package/cli/flow-live-status.ts +126 -0
  126. package/cli/flow-session.ts +140 -0
  127. package/cli/help-core.ts +143 -0
  128. package/cli/help.ts +44 -0
  129. package/cli/hidden-runtime-alias.ts +19 -0
  130. package/cli/hints.ts +214 -0
  131. package/cli/inspect-notice-state.ts +114 -0
  132. package/cli/internal-child.ts +176 -0
  133. package/cli/maestro-js.ts +323 -0
  134. package/cli/main.ts +704 -0
  135. package/cli/open-url.ts +8 -0
  136. package/cli/outbound-endpoints.ts +830 -0
  137. package/cli/parent-pid.ts +204 -0
  138. package/cli/parse-args.ts +189 -0
  139. package/cli/privacy.ts +53 -0
  140. package/cli/prompt.ts +56 -0
  141. package/cli/recording-access.ts +108 -0
  142. package/cli/registry.ts +2 -0
  143. package/cli/resolve-assets.ts +63 -0
  144. package/cli/rnx-in-process.ts +98 -0
  145. package/cli/run-registry.ts +226 -0
  146. package/cli/run-rnx-core.ts +148 -0
  147. package/cli/run-rnx.ts +137 -0
  148. package/cli/runtime-notes.ts +66 -0
  149. package/cli/runtime-summary.ts +25 -0
  150. package/cli/secure-file.ts +31 -0
  151. package/cli/self-invocation.ts +58 -0
  152. package/cli/send-to-box.ts +109 -0
  153. package/cli/setup-repository.ts +191 -0
  154. package/cli/shell-init.ts +98 -0
  155. package/cli/standalone.ts +4 -0
  156. package/cli/telemetry.ts +84 -0
  157. package/cli/ui.ts +68 -0
  158. package/cli/ws-bridge.ts +1020 -0
  159. package/detox/attempt-events.cjs +23 -0
  160. package/detox/colors.ts +54 -0
  161. package/detox/config-loader.ts +135 -0
  162. package/detox/element-types.ts +48 -0
  163. package/detox/expectations.ts +534 -0
  164. package/detox/gestures.ts +501 -0
  165. package/detox/index.ts +2694 -0
  166. package/detox/jest-preset.cjs +29 -0
  167. package/detox/jest-setup-after-env.cjs +22 -0
  168. package/detox/matchers.ts +30 -0
  169. package/detox/motion-change.cjs +109 -0
  170. package/detox/motion-change.d.cts +35 -0
  171. package/detox/navigation-lifecycle.ts +290 -0
  172. package/detox/navigation.ts +43 -0
  173. package/detox/proof-frame.cjs +114 -0
  174. package/detox/record-block-execution.cjs +78 -0
  175. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  176. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  177. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  178. package/detox/screenshots/theme-blue-toggle.png +0 -0
  179. package/detox/screenshots/theme-blue.png +0 -0
  180. package/detox/screenshots/theme-red-toggle.png +0 -0
  181. package/detox/screenshots/theme-red.png +0 -0
  182. package/dist-lib/agent-daemon-client.cjs +502 -0
  183. package/dist-lib/agent-events.cjs +48 -0
  184. package/dist-lib/agent-identity.cjs +53 -0
  185. package/dist-lib/agent-sessions.cjs +759 -0
  186. package/dist-lib/attached-projects.cjs +526 -0
  187. package/dist-lib/auth/shared-session.cjs +208 -0
  188. package/dist-lib/backend-origin.cjs +88 -0
  189. package/dist-lib/beta.cjs +41 -0
  190. package/dist-lib/beta.mjs +13 -0
  191. package/dist-lib/bridge-constants.cjs +161 -0
  192. package/dist-lib/bridge-contract-input.cjs +451 -0
  193. package/dist-lib/bridge-contract-input.mjs +414 -0
  194. package/dist-lib/bridge-contract.cjs +130 -0
  195. package/dist-lib/bridge-contract.mjs +94 -0
  196. package/dist-lib/capture-contract.cjs +220 -0
  197. package/dist-lib/capture-contract.mjs +190 -0
  198. package/dist-lib/cli-constants.cjs +32 -0
  199. package/dist-lib/cloud-contract.cjs +194 -0
  200. package/dist-lib/cloud-contract.mjs +145 -0
  201. package/dist-lib/cloud.cjs +670 -0
  202. package/dist-lib/cloud.mjs +639 -0
  203. package/dist-lib/config.cjs +96 -0
  204. package/dist-lib/detox/index.cjs +3789 -0
  205. package/dist-lib/detox/jest-preset.cjs +29 -0
  206. package/dist-lib/dev-bundle-resolution.cjs +290 -0
  207. package/dist-lib/home-paths.cjs +638 -0
  208. package/dist-lib/host/bridge-host.cjs +10903 -0
  209. package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
  210. package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
  211. package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
  212. package/dist-lib/host/replacement-module-handler.cjs +469 -0
  213. package/dist-lib/host/websocket-proxy.cjs +298 -0
  214. package/dist-lib/index.cjs +4268 -0
  215. package/dist-lib/jump-to-source-babel.cjs +93 -0
  216. package/dist-lib/jump-to-source-native.cjs +112 -0
  217. package/dist-lib/menu.cjs +646 -0
  218. package/dist-lib/menu.mjs +604 -0
  219. package/dist-lib/metro-fingerprint-registry.cjs +186 -0
  220. package/dist-lib/metro-fingerprint-registry.mjs +156 -0
  221. package/dist-lib/metro-production-bundle.cjs +329 -0
  222. package/dist-lib/metro-production-bundle.mjs +295 -0
  223. package/dist-lib/metro.cjs +904 -0
  224. package/dist-lib/profiles.cjs +246 -0
  225. package/dist-lib/public-brand.cjs +39 -0
  226. package/dist-lib/react-native-host-modules.cjs +51 -0
  227. package/dist-lib/react-native-host-modules.mjs +25 -0
  228. package/dist-lib/render-mode.cjs +54 -0
  229. package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
  230. package/dist-lib/sdk.cjs +2433 -0
  231. package/dist-lib/sdk.mjs +2336 -0
  232. package/dist-lib/skills.cjs +39132 -0
  233. package/dist-lib/swift-submit.cjs +92 -0
  234. package/dist-lib/swift-submit.mjs +65 -0
  235. package/dist-lib/swift.cjs +1646 -0
  236. package/dist-lib/vite.cjs +1591 -0
  237. package/package.json +290 -0
  238. package/scripts/dev-server-scanner.ts +1291 -0
  239. package/scripts/optional-demo-registry.ts +113 -0
  240. package/skills/contrast/SKILL.md +165 -0
  241. package/skills/rnx-debug/SKILL.md +433 -0
  242. package/skills/rnx-perf/SKILL.md +399 -0
  243. package/skills/rnx-setup/SKILL.md +172 -0
  244. package/skills/rnx-test/SKILL.md +321 -0
  245. package/skills/rnx-visual/SKILL.md +126 -0
  246. package/src/agent-daemon-client.ts +410 -0
  247. package/src/agent-events.ts +71 -0
  248. package/src/agent-identity.ts +23 -0
  249. package/src/agent-prompt.ts +71 -0
  250. package/src/agent-sessions.ts +580 -0
  251. package/src/app-url.ts +39 -0
  252. package/src/attached-projects.ts +539 -0
  253. package/src/auth/shared-session.ts +235 -0
  254. package/src/backend-origin.ts +70 -0
  255. package/src/beta.ts +19 -0
  256. package/src/bridge-constants.ts +175 -0
  257. package/src/bridge-contract-input.ts +502 -0
  258. package/src/bridge-contract.ts +558 -0
  259. package/src/browser-cache.ts +16 -0
  260. package/src/capture-contract.ts +587 -0
  261. package/src/cli-constants.ts +1 -0
  262. package/src/cli-version.ts +40 -0
  263. package/src/cloud-contract.ts +274 -0
  264. package/src/cloud.ts +762 -0
  265. package/src/codex-client.ts +234 -0
  266. package/src/config.ts +132 -0
  267. package/src/connect.ts +101 -0
  268. package/src/dev-bundle-resolution.ts +346 -0
  269. package/src/dev-server-open.ts +198 -0
  270. package/src/disk-cleanup.ts +540 -0
  271. package/src/flow-timeout-scale.ts +15 -0
  272. package/src/home-paths.ts +806 -0
  273. package/src/host/agent-host.ts +666 -0
  274. package/src/host/bridge-host.ts +2788 -0
  275. package/src/host/fetch-proxy-handler.ts +449 -0
  276. package/src/host/fetch-proxy-overrides.ts +51 -0
  277. package/src/host/open-url.ts +89 -0
  278. package/src/host/plane-host.ts +203 -0
  279. package/src/host/replacement-module-handler.ts +158 -0
  280. package/src/host/websocket-proxy.ts +315 -0
  281. package/src/index.ts +71 -0
  282. package/src/jump-to-source-babel.ts +135 -0
  283. package/src/jump-to-source-native.ts +102 -0
  284. package/src/menu.ts +777 -0
  285. package/src/metro-fingerprint-registry.ts +302 -0
  286. package/src/metro-plugin.ts +341 -0
  287. package/src/metro-production-bundle.ts +471 -0
  288. package/src/native-dev-bundle-url.ts +118 -0
  289. package/src/native-seam-manifest.ts +341 -0
  290. package/src/playwright-host-log.ts +16 -0
  291. package/src/plist.ts +27 -0
  292. package/src/poll-until.ts +123 -0
  293. package/src/profiles.ts +179 -0
  294. package/src/public-brand.ts +9 -0
  295. package/src/react-native-host-modules.ts +26 -0
  296. package/src/render-mode.ts +27 -0
  297. package/src/rolldown-plugin-swift.ts +240 -0
  298. package/src/runtime-assets.ts +233 -0
  299. package/src/runtime-delivery.ts +54 -0
  300. package/src/screenshots/compose.ts +422 -0
  301. package/src/screenshots/frame-compose.ts +514 -0
  302. package/src/screenshots/orchestrate.ts +256 -0
  303. package/src/screenshots/org-deck.ts +91 -0
  304. package/src/screenshots/registry.ts +58 -0
  305. package/src/screenshots/schema.ts +364 -0
  306. package/src/sdk.ts +15 -0
  307. package/src/settings-bundle.ts +418 -0
  308. package/src/shell-bridge-identity.ts +15 -0
  309. package/src/sim-client.ts +697 -0
  310. package/src/skills/builtin/a11y-review.ts +132 -0
  311. package/src/skills/builtin/compat-check.ts +71 -0
  312. package/src/skills/builtin/maestro-test.ts +121 -0
  313. package/src/skills/builtin/perf-profile.ts +90 -0
  314. package/src/skills/builtin/screenshot-all.ts +52 -0
  315. package/src/skills/builtin/visual-diff.ts +100 -0
  316. package/src/skills/registry.ts +107 -0
  317. package/src/skills/types.ts +41 -0
  318. package/src/swift-submit.ts +185 -0
  319. package/src/swift.ts +7 -0
  320. package/src/team-machine-prompt.ts +73 -0
  321. package/src/vite-plugin-one.ts +264 -0
  322. package/src/vite-plugin-swift.ts +620 -0
  323. package/src/vite-plugin.ts +1946 -0
  324. package/src/worklets-babel.ts +250 -0
@@ -0,0 +1,514 @@
1
+ import {
2
+ LEGACY_BOTTOM_BEZEL,
3
+ LEGACY_FRAME_RADIUS,
4
+ LEGACY_HOME_BUTTON_RING,
5
+ LEGACY_HOME_BUTTON_SIZE,
6
+ LEGACY_SIDE_BEZEL,
7
+ LEGACY_TOP_BEZEL,
8
+ HINGE_BARREL_BACKGROUND,
9
+ HINGE_BARREL_SHADOW,
10
+ HOUSING_RAIL_COLOR,
11
+ HOUSING_RAIL_OUTLINE,
12
+ SIDE_BUTTON_RESERVE,
13
+ } from 'sootsim-engine/ios/chrome-metrics'
14
+ import {
15
+ cornerRadii,
16
+ formatCornerRadii,
17
+ getDevice,
18
+ resolveDeviceFrameGeometry,
19
+ type CornerRadii,
20
+ type DeviceModel,
21
+ } from 'sootsim-engine/settings'
22
+ import { launchReapedChrome } from '../../../../scripts/lib/reap-browser'
23
+
24
+ const METALLIC_RING_SHADOW =
25
+ 'inset 0 0 0 0.5px #000, inset 0 0 0 2px #757575, inset 0 0 0 5px #212121'
26
+ const FRAME_OUTLINE_BLEED = 1
27
+
28
+ type FrameButtonLayout = {
29
+ side: 'left' | 'right'
30
+ top: number
31
+ height: number
32
+ width: number
33
+ }
34
+
35
+ type LegacyHomeButtonLayout = {
36
+ top: number
37
+ left: number
38
+ size: number
39
+ ring: number
40
+ }
41
+
42
+ export interface FramedScreenshotLayout {
43
+ model: DeviceModel
44
+ renderScale: number
45
+ outerWidth: number
46
+ outerHeight: number
47
+ logicalOuterWidth: number
48
+ logicalOuterHeight: number
49
+ logicalFrameWidth: number
50
+ logicalFrameHeight: number
51
+ logicalFrameLeft: number
52
+ logicalFrameTop: number
53
+ logicalScreenWidth: number
54
+ logicalScreenHeight: number
55
+ logicalScreenLeft: number
56
+ logicalScreenTop: number
57
+ logicalScreenRadius: CornerRadii
58
+ logicalFrameRadius: CornerRadii
59
+ // light rail around a black glass ring; null draws one black body
60
+ housing: ReturnType<typeof resolveDeviceFrameGeometry>['housing']
61
+ frameBackground: string
62
+ frameOutline: string
63
+ metallicRingShadow: string | null
64
+ buttons: FrameButtonLayout[]
65
+ legacyHomeButton: LegacyHomeButtonLayout | null
66
+ showHomeIndicator: boolean
67
+ logicalHomeIndicatorStripHeight: number
68
+ }
69
+
70
+ // Exact shell device frame layout for still exports.
71
+ // Mirrors DeviceFrame's bezel/button geometry while intentionally excluding:
72
+ // - the electron simulator top bar
73
+ // - the browser rail / gutter
74
+ // The screen bitmap itself still comes from the raw bridge screenshot.
75
+ export function getFramedScreenshotLayout(model: DeviceModel): FramedScreenshotLayout {
76
+ const spec = getDevice(model)
77
+ const renderScale = spec.scale
78
+ // legacy = home-button iPhone (SE). the canvas workspace also has no island /
79
+ // home indicator / corner radius, but no status bar, so gate on statusBar > 0
80
+ // to keep it out of the SE shell (mirrors DeviceFrame.isLegacyHardware).
81
+ const isLegacyHardware =
82
+ spec.statusBarHeight > 0 &&
83
+ !spec.dynamicIsland &&
84
+ spec.homeIndicatorHeight === 0 &&
85
+ // home-button iphones have a square display, so any corner reads 0
86
+ spec.cornerRadius.topLeft === 0
87
+ const logicalScreenWidth = spec.width
88
+ const logicalScreenHeight = spec.height
89
+ const modernGeometry = resolveDeviceFrameGeometry(spec)
90
+ const housing = isLegacyHardware ? null : modernGeometry.housing
91
+ const logicalFrameWidth = isLegacyHardware
92
+ ? spec.width + LEGACY_SIDE_BEZEL * 2
93
+ : modernGeometry.frameWidth
94
+ const logicalFrameHeight = isLegacyHardware
95
+ ? spec.height + LEGACY_TOP_BEZEL + LEGACY_BOTTOM_BEZEL
96
+ : modernGeometry.frameHeight
97
+ const logicalFrameLeft = SIDE_BUTTON_RESERVE
98
+ const logicalFrameTop = FRAME_OUTLINE_BLEED
99
+ const logicalOuterWidth = logicalFrameWidth + logicalFrameLeft * 2
100
+ const logicalOuterHeight = logicalFrameHeight + FRAME_OUTLINE_BLEED * 2
101
+ const logicalScreenLeft =
102
+ logicalFrameLeft + (isLegacyHardware ? LEGACY_SIDE_BEZEL : modernGeometry.screenLeft)
103
+ const logicalScreenTop = isLegacyHardware ? LEGACY_TOP_BEZEL : modernGeometry.screenTop
104
+ const logicalFrameRadius = isLegacyHardware
105
+ ? cornerRadii(LEGACY_FRAME_RADIUS)
106
+ : modernGeometry.frameRadius
107
+ const buttons: FrameButtonLayout[] = [
108
+ {
109
+ side: spec.hardwareButtons.lock.side,
110
+ top: spec.hardwareButtons.lock.top,
111
+ height: spec.hardwareButtons.lock.height,
112
+ width: spec.hardwareButtons.width,
113
+ },
114
+ {
115
+ side: spec.hardwareButtons.ringToggle.side,
116
+ top: spec.hardwareButtons.ringToggle.top,
117
+ height: spec.hardwareButtons.ringToggle.height,
118
+ width: spec.hardwareButtons.width,
119
+ },
120
+ {
121
+ side: spec.hardwareButtons.volumeUp.side,
122
+ top: spec.hardwareButtons.volumeUp.top,
123
+ height: spec.hardwareButtons.volumeUp.height,
124
+ width: spec.hardwareButtons.width,
125
+ },
126
+ {
127
+ side: spec.hardwareButtons.volumeDown.side,
128
+ top: spec.hardwareButtons.volumeDown.top,
129
+ height: spec.hardwareButtons.volumeDown.height,
130
+ width: spec.hardwareButtons.width,
131
+ },
132
+ ]
133
+
134
+ return {
135
+ model,
136
+ renderScale,
137
+ outerWidth: Math.round(logicalOuterWidth * renderScale),
138
+ outerHeight: Math.round(logicalOuterHeight * renderScale),
139
+ logicalOuterWidth,
140
+ logicalOuterHeight,
141
+ logicalFrameWidth,
142
+ logicalFrameHeight,
143
+ logicalFrameLeft,
144
+ logicalFrameTop,
145
+ logicalScreenWidth,
146
+ logicalScreenHeight,
147
+ logicalScreenLeft,
148
+ logicalScreenTop,
149
+ logicalScreenRadius: isLegacyHardware ? cornerRadii(0) : modernGeometry.screenRadius,
150
+ logicalFrameRadius,
151
+ housing,
152
+ frameBackground: isLegacyHardware
153
+ ? 'linear-gradient(180deg, #1b1c20 0%, #0f1012 42%, #050608 100%)'
154
+ : housing
155
+ ? HOUSING_RAIL_COLOR
156
+ : '#000000',
157
+ frameOutline: isLegacyHardware
158
+ ? '0 0 0 1px #1f2125'
159
+ : housing
160
+ ? HOUSING_RAIL_OUTLINE
161
+ : '0 0 0 1px #333',
162
+ metallicRingShadow: isLegacyHardware || housing ? null : METALLIC_RING_SHADOW,
163
+ buttons,
164
+ legacyHomeButton: isLegacyHardware
165
+ ? {
166
+ top:
167
+ LEGACY_TOP_BEZEL +
168
+ spec.height +
169
+ (LEGACY_BOTTOM_BEZEL - LEGACY_HOME_BUTTON_SIZE) / 2,
170
+ left: spec.width / 2 - LEGACY_HOME_BUTTON_SIZE / 2 + LEGACY_SIDE_BEZEL,
171
+ size: LEGACY_HOME_BUTTON_SIZE,
172
+ ring: LEGACY_HOME_BUTTON_RING,
173
+ }
174
+ : null,
175
+ showHomeIndicator: spec.homeIndicatorHeight > 0,
176
+ logicalHomeIndicatorStripHeight: spec.homeIndicatorHeight,
177
+ }
178
+ }
179
+
180
+ function renderSideButtonHtml(button: FrameButtonLayout): string {
181
+ const isRight = button.side === 'right'
182
+ const clip = isRight ? 'inset(-3px -3px -3px 0)' : 'inset(-3px 0 -3px -3px)'
183
+ return `
184
+ <div
185
+ aria-hidden="true"
186
+ style="
187
+ position:absolute;
188
+ ${isRight ? `right:-${button.width}px;` : `left:-${button.width}px;`}
189
+ top:${button.top}px;
190
+ width:${button.width}px;
191
+ height:${button.height}px;
192
+ background-color:#212121;
193
+ border-top-left-radius:${isRight ? 0 : 1.5}px;
194
+ border-bottom-left-radius:${isRight ? 0 : 1.5}px;
195
+ border-top-right-radius:${isRight ? 1.5 : 0}px;
196
+ border-bottom-right-radius:${isRight ? 1.5 : 0}px;
197
+ box-shadow:
198
+ inset 0 0.5px 0 #616161,
199
+ inset 0 -0.5px 0 #3F3F3F,
200
+ ${isRight ? 'inset -0.5px 0 0 #3F3F3F,' : 'inset 0.5px 0 0 #3F3F3F,'}
201
+ 0 0 0 0.5px #676767,
202
+ 0 0 0 1.5px #000;
203
+ clip-path:${clip};
204
+ pointer-events:none;
205
+ z-index:2;
206
+ "
207
+ ></div>
208
+ `
209
+ }
210
+
211
+ function renderFrameHtml(
212
+ layout: FramedScreenshotLayout,
213
+ rawImageDataUrl: string,
214
+ ): string {
215
+ const buttonsHtml = layout.buttons.map(renderSideButtonHtml).join('')
216
+
217
+ const ringHtml = layout.metallicRingShadow
218
+ ? `
219
+ <div
220
+ aria-hidden="true"
221
+ style="
222
+ position:absolute;
223
+ inset:0;
224
+ border-radius:${formatCornerRadii(layout.logicalFrameRadius)};
225
+ box-shadow:${layout.metallicRingShadow};
226
+ pointer-events:none;
227
+ z-index:10;
228
+ "
229
+ ></div>
230
+ `
231
+ : ''
232
+
233
+ // black glass ring inside the rail, then the hinge barrel over the leading
234
+ // rail. the ring carries no z-index so the screen div after it paints on top.
235
+ const housingHtml = layout.housing
236
+ ? `
237
+ <div
238
+ aria-hidden="true"
239
+ style="
240
+ position:absolute;
241
+ top:${layout.housing.rail.top}px;
242
+ right:${layout.housing.rail.right}px;
243
+ bottom:${layout.housing.rail.bottom}px;
244
+ left:${layout.housing.rail.left}px;
245
+ border-radius:${formatCornerRadii(layout.housing.glassRadius)};
246
+ background:#000000;
247
+ pointer-events:none;
248
+ "
249
+ ></div>
250
+ ${
251
+ layout.housing.hingeBarrel
252
+ ? `
253
+ <div
254
+ aria-hidden="true"
255
+ style="
256
+ position:absolute;
257
+ top:${layout.housing.hingeBarrel.endInset}px;
258
+ bottom:${layout.housing.hingeBarrel.endInset}px;
259
+ left:0;
260
+ width:${layout.housing.hingeBarrel.width}px;
261
+ border-radius:${layout.housing.hingeBarrel.width / 2}px 0 0 ${layout.housing.hingeBarrel.width / 2}px;
262
+ background:${HINGE_BARREL_BACKGROUND};
263
+ box-shadow:${HINGE_BARREL_SHADOW};
264
+ pointer-events:none;
265
+ z-index:11;
266
+ "
267
+ ></div>
268
+ `
269
+ : ''
270
+ }
271
+ `
272
+ : ''
273
+
274
+ const legacyHomeButtonHtml = layout.legacyHomeButton
275
+ ? `
276
+ <div
277
+ aria-hidden="true"
278
+ style="
279
+ position:absolute;
280
+ top:${layout.legacyHomeButton.top}px;
281
+ left:${layout.legacyHomeButton.left}px;
282
+ width:${layout.legacyHomeButton.size}px;
283
+ height:${layout.legacyHomeButton.size}px;
284
+ border-radius:50%;
285
+ background:
286
+ radial-gradient(circle at 35% 32%, rgba(62,64,68,0.35) 0%, rgba(17,18,20,0.98) 72%, rgba(8,9,11,1) 100%);
287
+ box-shadow:
288
+ inset 0 0 0 1px rgba(255,255,255,0.06),
289
+ inset 0 0 0 ${layout.legacyHomeButton.ring}px rgba(184,192,204,0.45),
290
+ 0 0 0 1px rgba(0,0,0,0.5);
291
+ z-index:3;
292
+ pointer-events:none;
293
+ display:flex;
294
+ align-items:center;
295
+ justify-content:center;
296
+ "
297
+ >
298
+ <div
299
+ style="
300
+ width:16px;
301
+ height:16px;
302
+ border-radius:4px;
303
+ box-shadow:inset 0 0 0 1.5px rgba(188,194,202,0.5);
304
+ "
305
+ ></div>
306
+ </div>
307
+ `
308
+ : ''
309
+
310
+ const homeIndicatorHtml = layout.showHomeIndicator
311
+ ? `
312
+ <div
313
+ aria-hidden="true"
314
+ style="
315
+ position:absolute;
316
+ left:0;
317
+ right:0;
318
+ bottom:4px;
319
+ height:${layout.logicalHomeIndicatorStripHeight}px;
320
+ display:flex;
321
+ align-items:center;
322
+ justify-content:center;
323
+ pointer-events:none;
324
+ z-index:10;
325
+ "
326
+ >
327
+ <div
328
+ style="
329
+ width:134px;
330
+ height:5px;
331
+ border-radius:2.5px;
332
+ background-color:rgb(150, 150, 150);
333
+ opacity:0.5;
334
+ "
335
+ ></div>
336
+ </div>
337
+ `
338
+ : ''
339
+
340
+ return `<!doctype html>
341
+ <html>
342
+ <head>
343
+ <meta charset="utf-8" />
344
+ <style>
345
+ html, body {
346
+ margin: 0;
347
+ width: ${layout.outerWidth}px;
348
+ height: ${layout.outerHeight}px;
349
+ background: transparent;
350
+ }
351
+ body {
352
+ overflow: hidden;
353
+ }
354
+ img {
355
+ display: block;
356
+ width: 100%;
357
+ height: 100%;
358
+ }
359
+ </style>
360
+ </head>
361
+ <body>
362
+ <div
363
+ id="frame-export-root"
364
+ style="
365
+ position:relative;
366
+ width:${layout.outerWidth}px;
367
+ height:${layout.outerHeight}px;
368
+ overflow:hidden;
369
+ background:transparent;
370
+ "
371
+ >
372
+ <div
373
+ id="frame-scale-layer"
374
+ style="
375
+ position:absolute;
376
+ top:0;
377
+ left:0;
378
+ width:${layout.logicalOuterWidth}px;
379
+ height:${layout.logicalOuterHeight}px;
380
+ transform:scale(${layout.renderScale});
381
+ transform-origin:top left;
382
+ overflow:visible;
383
+ "
384
+ >
385
+ <div
386
+ id="frame"
387
+ style="
388
+ position:absolute;
389
+ top:${layout.logicalFrameTop}px;
390
+ left:${layout.logicalFrameLeft}px;
391
+ width:${layout.logicalFrameWidth}px;
392
+ height:${layout.logicalFrameHeight}px;
393
+ border-radius:${formatCornerRadii(layout.logicalFrameRadius)};
394
+ box-sizing:border-box;
395
+ overflow:visible;
396
+ background:${layout.frameBackground};
397
+ box-shadow:${layout.frameOutline};
398
+ "
399
+ >
400
+ ${buttonsHtml}
401
+ ${ringHtml}
402
+ ${housingHtml}
403
+ ${legacyHomeButtonHtml}
404
+ <div
405
+ id="screen"
406
+ style="
407
+ position:absolute;
408
+ top:${layout.logicalScreenTop}px;
409
+ left:${layout.logicalScreenLeft - layout.logicalFrameLeft}px;
410
+ width:${layout.logicalScreenWidth}px;
411
+ height:${layout.logicalScreenHeight}px;
412
+ border-radius:${formatCornerRadii(layout.logicalScreenRadius)};
413
+ overflow:hidden;
414
+ background:#000;
415
+ "
416
+ >
417
+ <img src="${rawImageDataUrl}" alt="" />
418
+ ${homeIndicatorHtml}
419
+ </div>
420
+ </div>
421
+ </div>
422
+ </div>
423
+ </body>
424
+ </html>`
425
+ }
426
+
427
+ export async function composeFramedScreenshot(
428
+ rawPng: Buffer,
429
+ model: DeviceModel,
430
+ ): Promise<Buffer> {
431
+ const { chromium } = await import('playwright')
432
+ const browser = await launchReapedChrome((options) => chromium.launch(options), {
433
+ headless: true,
434
+ })
435
+ try {
436
+ const composer = await createFramedScreenshotComposer(browser)
437
+ try {
438
+ return await composer.compose(rawPng, model)
439
+ } finally {
440
+ await composer.close()
441
+ }
442
+ } finally {
443
+ await browser.close()
444
+ }
445
+ }
446
+
447
+ type BrowserLike = {
448
+ newContext: (opts: {
449
+ viewport: { width: number; height: number }
450
+ deviceScaleFactor: number
451
+ }) => Promise<{
452
+ newPage: () => Promise<{
453
+ setContent: (html: string, opts: { waitUntil: 'load' }) => Promise<void>
454
+ waitForTimeout: (ms: number) => Promise<void>
455
+ screenshot: (opts: {
456
+ type: 'png'
457
+ clip: { x: number; y: number; width: number; height: number }
458
+ omitBackground: true
459
+ }) => Promise<Buffer>
460
+ }>
461
+ close: () => Promise<void>
462
+ }>
463
+ }
464
+
465
+ type CachedFramePage = {
466
+ layout: FramedScreenshotLayout
467
+ context: Awaited<ReturnType<BrowserLike['newContext']>>
468
+ page: Awaited<ReturnType<Awaited<ReturnType<BrowserLike['newContext']>>['newPage']>>
469
+ }
470
+
471
+ export async function createFramedScreenshotComposer(browser: BrowserLike) {
472
+ const cache = new Map<DeviceModel, CachedFramePage>()
473
+
474
+ async function getCachedPage(model: DeviceModel): Promise<CachedFramePage> {
475
+ const existing = cache.get(model)
476
+ if (existing) return existing
477
+ const layout = getFramedScreenshotLayout(model)
478
+ const context = await browser.newContext({
479
+ viewport: { width: layout.outerWidth, height: layout.outerHeight },
480
+ deviceScaleFactor: 1,
481
+ })
482
+ const page = await context.newPage()
483
+ const created = { layout, context, page }
484
+ cache.set(model, created)
485
+ return created
486
+ }
487
+
488
+ return {
489
+ async compose(rawPng: Buffer, model: DeviceModel): Promise<Buffer> {
490
+ const cached = await getCachedPage(model)
491
+ const rawImageDataUrl = `data:image/png;base64,${rawPng.toString('base64')}`
492
+ await cached.page.setContent(renderFrameHtml(cached.layout, rawImageDataUrl), {
493
+ waitUntil: 'load',
494
+ })
495
+ await cached.page.waitForTimeout(20)
496
+ return (await cached.page.screenshot({
497
+ type: 'png',
498
+ clip: {
499
+ x: 0,
500
+ y: 0,
501
+ width: cached.layout.outerWidth,
502
+ height: cached.layout.outerHeight,
503
+ },
504
+ omitBackground: true,
505
+ })) as Buffer
506
+ },
507
+ async close(): Promise<void> {
508
+ for (const entry of cache.values()) {
509
+ await entry.context.close()
510
+ }
511
+ cache.clear()
512
+ },
513
+ }
514
+ }