rnxsim 0.0.0 → 0.1.312

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 (228) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +512 -0
  3. package/detox/colors.ts +54 -0
  4. package/detox/config-loader.ts +135 -0
  5. package/detox/element-types.ts +39 -0
  6. package/detox/expectations.ts +537 -0
  7. package/detox/gestures.ts +480 -0
  8. package/detox/index.ts +2118 -0
  9. package/detox/jest-preset.cjs +51 -0
  10. package/detox/jest-setup-after-env.cjs +18 -0
  11. package/detox/matchers.ts +30 -0
  12. package/detox/motion-change.cjs +109 -0
  13. package/detox/motion-change.d.cts +35 -0
  14. package/detox/navigation.ts +43 -0
  15. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  16. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  17. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  18. package/detox/screenshots/theme-blue-toggle.png +0 -0
  19. package/detox/screenshots/theme-blue.png +0 -0
  20. package/detox/screenshots/theme-red-toggle.png +0 -0
  21. package/detox/screenshots/theme-red.png +0 -0
  22. package/dist-cli/bin.js +15 -0
  23. package/dist-cli/chunks/agent-NXVDDCFG.js +48 -0
  24. package/dist-cli/chunks/agent-wrapper-DXJCFKL3.js +17 -0
  25. package/dist-cli/chunks/app-fonts-W5AJZQJF.js +4 -0
  26. package/dist-cli/chunks/assert-3LOJEUDT.js +49 -0
  27. package/dist-cli/chunks/auth-MFTYYPNX.js +12 -0
  28. package/dist-cli/chunks/beta-GT3NN67B.js +4 -0
  29. package/dist-cli/chunks/camera-6EAK7CUL.js +22 -0
  30. package/dist-cli/chunks/chunk-274UD5FD.js +4 -0
  31. package/dist-cli/chunks/chunk-2SA2VTGP.js +5 -0
  32. package/dist-cli/chunks/chunk-33FKQH54.js +5 -0
  33. package/dist-cli/chunks/chunk-37I5AIS7.js +3 -0
  34. package/dist-cli/chunks/chunk-3D57IBPC.js +4 -0
  35. package/dist-cli/chunks/chunk-4BXCZHAU.js +4 -0
  36. package/dist-cli/chunks/chunk-5OX7ANRA.js +5 -0
  37. package/dist-cli/chunks/chunk-66NF2KNG.js +9 -0
  38. package/dist-cli/chunks/chunk-6UKJB2VC.js +125 -0
  39. package/dist-cli/chunks/chunk-7INBNA3U.js +26 -0
  40. package/dist-cli/chunks/chunk-7VWOSTD5.js +5 -0
  41. package/dist-cli/chunks/chunk-AY47WKS5.js +23 -0
  42. package/dist-cli/chunks/chunk-B7KMNHX7.js +4 -0
  43. package/dist-cli/chunks/chunk-BKNOLP3J.js +4 -0
  44. package/dist-cli/chunks/chunk-BRIZOR4K.js +28 -0
  45. package/dist-cli/chunks/chunk-DM6KUAOZ.js +4 -0
  46. package/dist-cli/chunks/chunk-EUAVBTKU.js +4 -0
  47. package/dist-cli/chunks/chunk-FTC4M26Q.js +4 -0
  48. package/dist-cli/chunks/chunk-FWYYCUAZ.js +4 -0
  49. package/dist-cli/chunks/chunk-G74YIEVS.js +7 -0
  50. package/dist-cli/chunks/chunk-GZDHEONW.js +507 -0
  51. package/dist-cli/chunks/chunk-HG7ORJMT.js +5 -0
  52. package/dist-cli/chunks/chunk-HMDOLCKG.js +15 -0
  53. package/dist-cli/chunks/chunk-JG6S4RSH.js +4 -0
  54. package/dist-cli/chunks/chunk-KR5BGMBZ.js +4 -0
  55. package/dist-cli/chunks/chunk-KZW5WBGU.js +983 -0
  56. package/dist-cli/chunks/chunk-L24Z3CRS.js +30 -0
  57. package/dist-cli/chunks/chunk-LDQ7HVCW.js +4 -0
  58. package/dist-cli/chunks/chunk-MENV7VLD.js +4 -0
  59. package/dist-cli/chunks/chunk-MNGQ42QE.js +568 -0
  60. package/dist-cli/chunks/chunk-N6A3SXMR.js +4 -0
  61. package/dist-cli/chunks/chunk-NLEMQ4PY.js +6 -0
  62. package/dist-cli/chunks/chunk-NPVGMH2N.js +101 -0
  63. package/dist-cli/chunks/chunk-OR3XTXEB.js +107 -0
  64. package/dist-cli/chunks/chunk-PEPUGJVI.js +4 -0
  65. package/dist-cli/chunks/chunk-Q3RGLERJ.js +142 -0
  66. package/dist-cli/chunks/chunk-SI3BUTQR.js +4 -0
  67. package/dist-cli/chunks/chunk-T4FT6CM7.js +4 -0
  68. package/dist-cli/chunks/chunk-TBMVBI26.js +7 -0
  69. package/dist-cli/chunks/chunk-TCWQJILS.js +5 -0
  70. package/dist-cli/chunks/chunk-TGQNQ6XT.js +766 -0
  71. package/dist-cli/chunks/chunk-TYE3WBSR.js +5 -0
  72. package/dist-cli/chunks/chunk-U4PN6JOT.js +6 -0
  73. package/dist-cli/chunks/chunk-VUFRB347.js +4 -0
  74. package/dist-cli/chunks/chunk-VZBLFMYC.js +10 -0
  75. package/dist-cli/chunks/chunk-WBT32R6N.js +25 -0
  76. package/dist-cli/chunks/chunk-XRGEEPJ5.js +38 -0
  77. package/dist-cli/chunks/chunk-Y2FSZSO7.js +71 -0
  78. package/dist-cli/chunks/chunk-YRV7HR3P.js +74 -0
  79. package/dist-cli/chunks/chunk-YZRFF4YA.js +14 -0
  80. package/dist-cli/chunks/chunk-ZMZTHU7V.js +4 -0
  81. package/dist-cli/chunks/cleanup-H3GWQB63.js +25 -0
  82. package/dist-cli/chunks/cli-version-3QPAJ3LC.js +4 -0
  83. package/dist-cli/chunks/compat-5WG2XJV7.js +20 -0
  84. package/dist-cli/chunks/config-ANMBVVLL.js +13 -0
  85. package/dist-cli/chunks/control-NSGNQFJG.js +4 -0
  86. package/dist-cli/chunks/daemon-JHF4BJ3U.js +4 -0
  87. package/dist-cli/chunks/debug-CHV2O2KF.js +200 -0
  88. package/dist-cli/chunks/desktop-FIPIK4CH.js +31 -0
  89. package/dist-cli/chunks/detox-CVUJP7K6.js +52 -0
  90. package/dist-cli/chunks/device-6WCUM4W6.js +18 -0
  91. package/dist-cli/chunks/diagnose-I6EMWEII.js +43 -0
  92. package/dist-cli/chunks/disk-cleanup-P27CYFTN.js +4 -0
  93. package/dist-cli/chunks/drivers-HQ6AKZT6.js +4 -0
  94. package/dist-cli/chunks/film-UOQFFYNF.js +74 -0
  95. package/dist-cli/chunks/flow-N27MRRNL.js +4 -0
  96. package/dist-cli/chunks/help-SF6FEGEE.js +4 -0
  97. package/dist-cli/chunks/hidden-runtime-alias-NJYFSJKZ.js +4 -0
  98. package/dist-cli/chunks/home-paths-XOV44JEF.js +4 -0
  99. package/dist-cli/chunks/inspect-CWFRFQTY.js +4 -0
  100. package/dist-cli/chunks/install-desktop-KLQDNXZQ.js +4 -0
  101. package/dist-cli/chunks/login-BPFQENCX.js +28 -0
  102. package/dist-cli/chunks/logout-5A45ZGY7.js +4 -0
  103. package/dist-cli/chunks/maestro-E4CSDMLM.js +104 -0
  104. package/dist-cli/chunks/maestro-generate-CKMOJQX2.js +36 -0
  105. package/dist-cli/chunks/mode-SZYNMIQF.js +26 -0
  106. package/dist-cli/chunks/optional-demo-registry-FRH4SFVG.js +4 -0
  107. package/dist-cli/chunks/perf-VXU2XJAX.js +48 -0
  108. package/dist-cli/chunks/permissions-IRIAOVND.js +13 -0
  109. package/dist-cli/chunks/record-RY7FPLLH.js +72 -0
  110. package/dist-cli/chunks/report-issue-KNWXOE53.js +29 -0
  111. package/dist-cli/chunks/runtime-AR3GJTGK.js +4 -0
  112. package/dist-cli/chunks/screenshot-command-QLALR6GY.js +159 -0
  113. package/dist-cli/chunks/screenshot-layers-OQFMFVRL.js +20 -0
  114. package/dist-cli/chunks/screenshots-capture-6TSRJFGB.js +42 -0
  115. package/dist-cli/chunks/serve-F54J2NQO.js +44 -0
  116. package/dist-cli/chunks/setup-7V6UKX4U.js +32 -0
  117. package/dist-cli/chunks/skills-QQBOSETH.js +50 -0
  118. package/dist-cli/chunks/storage-TAPMKK5O.js +37 -0
  119. package/dist-cli/chunks/store-7NX3S43H.js +4 -0
  120. package/dist-cli/chunks/telemetry-E7BUK6RA.js +4 -0
  121. package/dist-cli/chunks/timeline-IAM564NA.js +24 -0
  122. package/dist-cli/chunks/upgrade-CD55QXIX.js +7 -0
  123. package/dist-cli/chunks/upload-GZ6LIMYM.js +4 -0
  124. package/dist-cli/chunks/version-3DYJ5AS3.js +6 -0
  125. package/dist-cli/chunks/web-6NW6YLPI.js +4 -0
  126. package/dist-cli/chunks/what-happened-CZZDOOVG.js +25 -0
  127. package/dist-lib/agent-daemon-client.cjs +468 -0
  128. package/dist-lib/agent-events.cjs +48 -0
  129. package/dist-lib/agent-identity.cjs +53 -0
  130. package/dist-lib/agent-sessions.cjs +725 -0
  131. package/dist-lib/attached-projects.cjs +526 -0
  132. package/dist-lib/auth/shared-session.cjs +195 -0
  133. package/dist-lib/backend-origin.cjs +70 -0
  134. package/dist-lib/beta.cjs +41 -0
  135. package/dist-lib/beta.mjs +13 -0
  136. package/dist-lib/bridge-constants.cjs +161 -0
  137. package/dist-lib/cli-constants.cjs +32 -0
  138. package/dist-lib/config.cjs +96 -0
  139. package/dist-lib/detox/index.cjs +2863 -0
  140. package/dist-lib/detox/jest-preset.cjs +51 -0
  141. package/dist-lib/dev-bundle-resolution.cjs +223 -0
  142. package/dist-lib/home-paths.cjs +421 -0
  143. package/dist-lib/host/bridge-host.cjs +5600 -0
  144. package/dist-lib/host/fetch-proxy-handler.cjs +373 -0
  145. package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
  146. package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
  147. package/dist-lib/host/websocket-proxy.cjs +214 -0
  148. package/dist-lib/index.cjs +513 -0
  149. package/dist-lib/jump-to-source-babel.cjs +93 -0
  150. package/dist-lib/menu.cjs +620 -0
  151. package/dist-lib/menu.mjs +581 -0
  152. package/dist-lib/metro.cjs +297 -0
  153. package/dist-lib/profiles.cjs +246 -0
  154. package/dist-lib/public-brand.cjs +39 -0
  155. package/dist-lib/render-mode.cjs +54 -0
  156. package/dist-lib/scripts/dev-server-scanner.cjs +1034 -0
  157. package/dist-lib/sdk.cjs +2333 -0
  158. package/dist-lib/sdk.mjs +2240 -0
  159. package/dist-lib/skills.cjs +23911 -0
  160. package/dist-lib/vite.cjs +358 -0
  161. package/package.json +189 -18
  162. package/scripts/dev-server-scanner.ts +941 -0
  163. package/scripts/optional-demo-registry.ts +113 -0
  164. package/scripts/postinstall.cjs +3 -0
  165. package/scripts/preuninstall.cjs +118 -0
  166. package/skills/contrast/SKILL.md +171 -0
  167. package/skills/rnx-debug/SKILL.md +425 -0
  168. package/skills/rnx-perf/SKILL.md +295 -0
  169. package/skills/rnx-setup/SKILL.md +160 -0
  170. package/skills/rnx-test/SKILL.md +292 -0
  171. package/skills/rnx-visual/SKILL.md +122 -0
  172. package/src/agent-daemon-client.ts +410 -0
  173. package/src/agent-events.ts +71 -0
  174. package/src/agent-identity.ts +23 -0
  175. package/src/agent-prompt.ts +71 -0
  176. package/src/agent-sessions.ts +573 -0
  177. package/src/attached-projects.ts +539 -0
  178. package/src/auth/shared-session.ts +204 -0
  179. package/src/backend-origin.ts +49 -0
  180. package/src/beta.ts +19 -0
  181. package/src/bridge-constants.ts +171 -0
  182. package/src/browser-cache.ts +16 -0
  183. package/src/cli-constants.ts +1 -0
  184. package/src/cli-version.ts +32 -0
  185. package/src/codex-client.ts +215 -0
  186. package/src/config.ts +134 -0
  187. package/src/dev-bundle-resolution.ts +236 -0
  188. package/src/disk-cleanup.ts +466 -0
  189. package/src/home-paths.ts +552 -0
  190. package/src/host/agent-host.ts +666 -0
  191. package/src/host/bridge-host.ts +2626 -0
  192. package/src/host/fetch-proxy-handler.ts +397 -0
  193. package/src/host/fetch-proxy-overrides.ts +51 -0
  194. package/src/host/open-url.ts +89 -0
  195. package/src/host/websocket-proxy.ts +210 -0
  196. package/src/index.ts +12 -705
  197. package/src/jump-to-source-babel.ts +135 -0
  198. package/src/menu.ts +734 -0
  199. package/src/metro-plugin.ts +139 -0
  200. package/src/native-dev-bundle-url.ts +96 -0
  201. package/src/native-seam-manifest.ts +341 -0
  202. package/src/plist.ts +27 -0
  203. package/src/poll-until.ts +92 -0
  204. package/src/profiles.ts +179 -0
  205. package/src/public-brand.ts +9 -0
  206. package/src/render-mode.ts +27 -0
  207. package/src/runtime-assets.ts +228 -0
  208. package/src/runtime-delivery.ts +54 -0
  209. package/src/screenshots/compose.ts +422 -0
  210. package/src/screenshots/frame-compose.ts +454 -0
  211. package/src/screenshots/orchestrate.ts +256 -0
  212. package/src/screenshots/org-deck.ts +91 -0
  213. package/src/screenshots/registry.ts +58 -0
  214. package/src/screenshots/schema.ts +364 -0
  215. package/src/sdk.ts +7 -0
  216. package/src/settings-bundle.ts +418 -0
  217. package/src/skills/builtin/a11y-review.ts +132 -0
  218. package/src/skills/builtin/compat-check.ts +71 -0
  219. package/src/skills/builtin/maestro-test.ts +121 -0
  220. package/src/skills/builtin/perf-profile.ts +90 -0
  221. package/src/skills/builtin/screenshot-all.ts +52 -0
  222. package/src/skills/builtin/visual-diff.ts +100 -0
  223. package/src/skills/registry.ts +107 -0
  224. package/src/skills/types.ts +41 -0
  225. package/src/team-machine-prompt.ts +73 -0
  226. package/src/vite-plugin-one.ts +190 -0
  227. package/src/vite-plugin.ts +1750 -0
  228. package/src/worklets-babel.ts +134 -0
package/src/menu.ts ADDED
@@ -0,0 +1,734 @@
1
+ import { rnxPublicBrand } from './public-brand'
2
+
3
+ // public sootsim menu protocol. this file is deliberately pure data and pure
4
+ // functions so hosts can import it without pulling the engine or canvaskit graph.
5
+
6
+ export const SOOTSIM_ENGINE_EVENT_NAME = 'sootsim:engine-event'
7
+ export const SOOTSIM_SHELL_COMMAND_NAME = 'sootsim:shell-command'
8
+
9
+ export type SootSimMenuPlatform = 'browser' | 'electron'
10
+ export type SootSimScaleMode = 'physical' | 'point' | 'pixel' | 'fit'
11
+
12
+ // ambient glow strength. one setting with three states rather than an on/off
13
+ // pair plus a separate strength: every host that carries the preference (the
14
+ // menus, the persisted setting, the ?ambientGlow= boot seed, the live
15
+ // set-ambient-glow push, electron's shared config) then carries exactly one
16
+ // value and cannot end up half-configured.
17
+ export type SootSimAmbientGlowLevel = 'off' | 'soft' | 'full'
18
+ export const SOOTSIM_AMBIENT_GLOW_LEVELS = ['off', 'soft', 'full'] as const
19
+
20
+ export function isSootSimAmbientGlowLevel(
21
+ value: unknown,
22
+ ): value is SootSimAmbientGlowLevel {
23
+ return value === 'off' || value === 'soft' || value === 'full'
24
+ }
25
+
26
+ export const SOOTSIM_AMBIENT_GLOW_LABELS: Record<SootSimAmbientGlowLevel, string> = {
27
+ off: 'Off',
28
+ soft: 'Soft',
29
+ full: 'Full',
30
+ }
31
+
32
+ export type SootSimMenuItemDef =
33
+ | { type: 'separator' }
34
+ | {
35
+ type?: 'normal' | 'checkbox' | 'radio'
36
+ label: string
37
+ accelerator?: string
38
+ browserAccelerator?: boolean
39
+ checked?: boolean
40
+ enabled?: boolean
41
+ action?: string
42
+ actionPayload?: string
43
+ shortcutLabel?: string
44
+ showInShortcutDocs?: boolean
45
+ items?: SootSimMenuItemDef[]
46
+ hiddenIn?: SootSimMenuPlatform
47
+ }
48
+
49
+ export type SootSimMenuDef = {
50
+ label: string
51
+ items: SootSimMenuItemDef[]
52
+ hiddenIn?: SootSimMenuPlatform
53
+ notMinimal?: boolean
54
+ brand?: boolean
55
+ }
56
+
57
+ export interface SootSimMenuState {
58
+ currentDevice: string
59
+ showBezels: boolean
60
+ showMenuBar: boolean
61
+ showRail: boolean
62
+ solidWindow: boolean
63
+ ambientGlow: SootSimAmbientGlowLevel
64
+ // presentation: the real rendering, or the clean structural wireframe
65
+ wireframe: boolean
66
+ screenshotMode: boolean
67
+ threeMode: boolean
68
+ inspectMode: boolean
69
+ devtoolsOpen: boolean
70
+ alwaysShowLiveWebViews: boolean
71
+ stayOnTop: boolean
72
+ scaleMode: SootSimScaleMode
73
+ }
74
+
75
+ export interface SootSimMenuDeviceChoice {
76
+ model: string
77
+ label: string
78
+ platform: string
79
+ }
80
+
81
+ export interface SootSimMenuShortcutEntry {
82
+ menuLabel: string
83
+ label: string
84
+ accelerator: string
85
+ action: string
86
+ actionPayload?: string
87
+ }
88
+
89
+ export interface SootSimShortcutDocEntry {
90
+ keys: string
91
+ label: string
92
+ }
93
+
94
+ export interface SootSimShortcutDocSection {
95
+ title: string
96
+ entries: SootSimShortcutDocEntry[]
97
+ }
98
+
99
+ export type MenuItemDef = SootSimMenuItemDef
100
+ export type MenuDef = SootSimMenuDef
101
+ export type MenuState = SootSimMenuState
102
+ export type AmbientGlowLevel = SootSimAmbientGlowLevel
103
+ export type ShortcutPlatform = SootSimMenuPlatform
104
+ export type MenuShortcutEntry = SootSimMenuShortcutEntry
105
+ export type ShortcutDocEntry = SootSimShortcutDocEntry
106
+ export type ShortcutDocSection = SootSimShortcutDocSection
107
+
108
+ export function buildSootSimMenuDefs(
109
+ state: SootSimMenuState,
110
+ deviceChoices: readonly SootSimMenuDeviceChoice[],
111
+ ): SootSimMenuDef[] {
112
+ return [
113
+ appMenu(),
114
+ fileMenu(state, deviceChoices),
115
+ editMenu(),
116
+ deviceMenu(state, deviceChoices),
117
+ debugMenu(state),
118
+ windowMenu(state),
119
+ helpMenu(),
120
+ ]
121
+ }
122
+
123
+ function appMenu(): SootSimMenuDef {
124
+ return {
125
+ label: rnxPublicBrand.name,
126
+ brand: true,
127
+ items: [
128
+ { label: `About ${rnxPublicBrand.name}`, action: 'about' },
129
+ { type: 'separator' },
130
+ {
131
+ label: 'Preferences...',
132
+ accelerator: 'CmdOrCtrl+,',
133
+ action: 'preferences',
134
+ browserAccelerator: true,
135
+ shortcutLabel: 'settings',
136
+ showInShortcutDocs: true,
137
+ },
138
+ {
139
+ label: 'Settings Pages',
140
+ items: [
141
+ {
142
+ label: 'Accessibility',
143
+ action: 'open-settings-page',
144
+ actionPayload: 'accessibility',
145
+ },
146
+ {
147
+ label: 'Display & Text Size',
148
+ action: 'open-settings-page',
149
+ actionPayload: 'display-and-text-size',
150
+ },
151
+ {
152
+ label: 'Notifications',
153
+ action: 'open-settings-page',
154
+ actionPayload: 'notifications',
155
+ },
156
+ {
157
+ label: 'Apps',
158
+ action: 'open-settings-page',
159
+ actionPayload: 'apps',
160
+ },
161
+ {
162
+ label: 'Privacy & Security',
163
+ action: 'open-settings-page',
164
+ actionPayload: 'privacy',
165
+ },
166
+ ],
167
+ },
168
+ { type: 'separator' },
169
+ {
170
+ label: 'Hide rnx',
171
+ accelerator: 'CmdOrCtrl+H',
172
+ action: 'hide',
173
+ hiddenIn: 'browser',
174
+ },
175
+ {
176
+ label: 'Hide Others',
177
+ accelerator: 'CmdOrCtrl+Alt+H',
178
+ action: 'hide-others',
179
+ hiddenIn: 'browser',
180
+ },
181
+ { label: 'Show All', action: 'unhide', hiddenIn: 'browser' },
182
+ { type: 'separator' },
183
+ {
184
+ label: 'Quit rnx',
185
+ accelerator: 'CmdOrCtrl+Q',
186
+ action: 'quit',
187
+ hiddenIn: 'browser',
188
+ },
189
+ ],
190
+ }
191
+ }
192
+
193
+ function fileMenu(
194
+ state: SootSimMenuState,
195
+ deviceChoices: readonly SootSimMenuDeviceChoice[],
196
+ ): SootSimMenuDef {
197
+ return {
198
+ label: 'File',
199
+ notMinimal: true,
200
+ items: [
201
+ { label: 'New Window', accelerator: 'CmdOrCtrl+N', action: 'new-window' },
202
+ {
203
+ label: 'New Simulator',
204
+ hiddenIn: 'browser',
205
+ items: buildSootSimDeviceChoiceDefs(state.currentDevice, deviceChoices, {
206
+ action: 'new-simulator',
207
+ }),
208
+ },
209
+ {
210
+ label: 'Close Window',
211
+ accelerator: 'CmdOrCtrl+W',
212
+ action: 'close-window',
213
+ hiddenIn: 'browser',
214
+ },
215
+ { type: 'separator' },
216
+ {
217
+ label: 'Save Screen',
218
+ accelerator: 'CmdOrCtrl+S',
219
+ action: 'save-screen',
220
+ },
221
+ {
222
+ label: 'Copy Screen',
223
+ accelerator: 'CmdOrCtrl+Alt+C',
224
+ action: 'copy-screen',
225
+ },
226
+ { type: 'separator' },
227
+ {
228
+ label: 'Record',
229
+ accelerator: 'Ctrl+CmdOrCtrl+R',
230
+ action: 'toggle-recording',
231
+ browserAccelerator: true,
232
+ shortcutLabel: 'start/stop recording',
233
+ showInShortcutDocs: true,
234
+ },
235
+ ],
236
+ }
237
+ }
238
+
239
+ function editMenu(): SootSimMenuDef {
240
+ return {
241
+ label: 'Edit',
242
+ hiddenIn: 'browser',
243
+ items: [
244
+ { label: 'Undo', accelerator: 'CmdOrCtrl+Z', action: 'undo' },
245
+ { label: 'Redo', accelerator: 'CmdOrCtrl+Shift+Z', action: 'redo' },
246
+ { type: 'separator' },
247
+ { label: 'Cut', accelerator: 'CmdOrCtrl+X', action: 'cut' },
248
+ { label: 'Copy', accelerator: 'CmdOrCtrl+C', action: 'copy' },
249
+ { label: 'Paste', accelerator: 'CmdOrCtrl+V', action: 'paste' },
250
+ { label: 'Select All', accelerator: 'CmdOrCtrl+A', action: 'select-all' },
251
+ ],
252
+ }
253
+ }
254
+
255
+ function deviceMenu(
256
+ state: SootSimMenuState,
257
+ deviceChoices: readonly SootSimMenuDeviceChoice[],
258
+ ): SootSimMenuDef {
259
+ return {
260
+ label: 'Device',
261
+ items: [
262
+ { label: 'Restart', accelerator: 'CmdOrCtrl+R', action: 'reload' },
263
+ { label: 'Erase All Content and Settings...', action: 'erase-all' },
264
+ { type: 'separator' },
265
+ {
266
+ label: 'Home',
267
+ accelerator: 'Shift+CmdOrCtrl+H',
268
+ action: 'home',
269
+ browserAccelerator: true,
270
+ shortcutLabel: 'go home',
271
+ showInShortcutDocs: true,
272
+ },
273
+ {
274
+ label: 'Lock',
275
+ accelerator: 'CmdOrCtrl+L',
276
+ action: 'lock',
277
+ shortcutLabel: 'lock / unlock',
278
+ showInShortcutDocs: true,
279
+ },
280
+ { label: 'Shake', accelerator: 'Ctrl+CmdOrCtrl+Z', action: 'shake' },
281
+ {
282
+ label: 'App Switcher',
283
+ accelerator: 'Ctrl+Shift+CmdOrCtrl+H',
284
+ action: 'app-switcher',
285
+ browserAccelerator: true,
286
+ shortcutLabel: 'app switcher',
287
+ showInShortcutDocs: true,
288
+ },
289
+ { type: 'separator' },
290
+ { label: 'Trigger Screenshot', action: 'screenshot' },
291
+ { type: 'separator' },
292
+ ...buildSootSimDeviceChoiceDefs(state.currentDevice, deviceChoices, {
293
+ action: 'switch-device',
294
+ }),
295
+ ],
296
+ }
297
+ }
298
+
299
+ function debugMenu(state: SootSimMenuState): SootSimMenuDef {
300
+ return {
301
+ label: 'Debug',
302
+ items: [
303
+ // both toggles are stateful in the browser, where the open state lives
304
+ // in the same document. electron's menu process cannot see it, so it
305
+ // keeps the plain (stateless) items rather than a permanently
306
+ // unchecked checkbox — same reasoning as the screenshot/3d entries
307
+ // being hidden there. accelerators stay single-registered per surface.
308
+ {
309
+ label: 'Toggle Devtools',
310
+ accelerator: 'Alt+I',
311
+ action: 'toggle-devtools',
312
+ showInShortcutDocs: true,
313
+ hiddenIn: 'browser',
314
+ },
315
+ {
316
+ label: 'Toggle Devtools',
317
+ type: 'checkbox',
318
+ checked: state.devtoolsOpen,
319
+ accelerator: 'Alt+I',
320
+ action: 'toggle-devtools',
321
+ browserAccelerator: true,
322
+ showInShortcutDocs: true,
323
+ hiddenIn: 'electron',
324
+ },
325
+ {
326
+ label: 'Inspect',
327
+ accelerator: 'CmdOrCtrl+I',
328
+ action: 'toggle-inspect',
329
+ hiddenIn: 'browser',
330
+ },
331
+ {
332
+ label: 'Inspect',
333
+ type: 'checkbox',
334
+ checked: state.inspectMode,
335
+ accelerator: 'CmdOrCtrl+I',
336
+ action: 'toggle-inspect',
337
+ browserAccelerator: true,
338
+ hiddenIn: 'electron',
339
+ },
340
+ { type: 'separator' },
341
+ {
342
+ label: 'Open Chrome Devtools',
343
+ accelerator: 'CmdOrCtrl+Alt+I',
344
+ action: 'open-devtools',
345
+ hiddenIn: 'browser',
346
+ shortcutLabel: 'developer tools',
347
+ showInShortcutDocs: true,
348
+ },
349
+ { type: 'separator' },
350
+ {
351
+ label: 'Always Show Live WebViews',
352
+ type: 'checkbox',
353
+ checked: state.alwaysShowLiveWebViews,
354
+ action: 'toggle-live-webviews',
355
+ },
356
+ { type: 'separator' },
357
+ {
358
+ label: 'Clear Cache and Reload',
359
+ accelerator: 'CmdOrCtrl+Shift+R',
360
+ action: 'clear-cache',
361
+ },
362
+ { label: 'Clear Local Storage', action: 'clear-localstorage' },
363
+ { label: 'Clear IndexedDB', action: 'clear-indexeddb' },
364
+ { label: 'Clear Service Workers', action: 'clear-serviceworkers' },
365
+ { type: 'separator' },
366
+ {
367
+ label: 'Open User Data Folder',
368
+ action: 'open-userdata',
369
+ hiddenIn: 'browser',
370
+ },
371
+ ],
372
+ }
373
+ }
374
+
375
+ function windowMenu(state: SootSimMenuState): SootSimMenuDef {
376
+ return {
377
+ label: 'Window',
378
+ items: [
379
+ {
380
+ label: 'Minimize',
381
+ accelerator: 'CmdOrCtrl+M',
382
+ action: 'minimize',
383
+ hiddenIn: 'browser',
384
+ },
385
+ {
386
+ label: 'Close',
387
+ accelerator: 'CmdOrCtrl+W',
388
+ action: 'close-window',
389
+ hiddenIn: 'browser',
390
+ },
391
+ { type: 'separator' },
392
+ {
393
+ label: 'Toggle Appearance',
394
+ accelerator: 'Shift+CmdOrCtrl+A',
395
+ action: 'toggle-appearance',
396
+ browserAccelerator: true,
397
+ shortcutLabel: 'toggle appearance',
398
+ showInShortcutDocs: true,
399
+ },
400
+ { type: 'separator' },
401
+ {
402
+ label: 'Show Device Bezels',
403
+ type: 'checkbox',
404
+ checked: state.showBezels,
405
+ action: 'toggle-bezels',
406
+ },
407
+ {
408
+ label: 'Show Menu Bar',
409
+ type: 'checkbox',
410
+ checked: state.showMenuBar,
411
+ action: 'toggle-menubar',
412
+ hiddenIn: 'electron',
413
+ },
414
+ {
415
+ label: 'Show Rail',
416
+ type: 'checkbox',
417
+ checked: state.showRail,
418
+ action: 'toggle-rail',
419
+ hiddenIn: 'electron',
420
+ },
421
+ {
422
+ // desktop only. the device window is frameless and transparent, which
423
+ // is right for a phone floating on the desktop and wrong for the
424
+ // full-window modes; this pins the solid style on all the time. 3d
425
+ // mode switches to it on its own either way.
426
+ label: 'Solid Window',
427
+ type: 'checkbox',
428
+ checked: state.solidWindow,
429
+ action: 'toggle-solid-window',
430
+ hiddenIn: 'browser',
431
+ },
432
+ {
433
+ // browser only: the wash needs several glow radii of stage around the
434
+ // device to fade out in, and electron's transparent window is sized to
435
+ // the device, so there it can only paint a clipped rectangle.
436
+ label: 'Ambient Glow',
437
+ hiddenIn: 'electron',
438
+ items: SOOTSIM_AMBIENT_GLOW_LEVELS.map((level) => ({
439
+ label: SOOTSIM_AMBIENT_GLOW_LABELS[level],
440
+ type: 'radio' as const,
441
+ checked: state.ambientGlow === level,
442
+ action: 'set-ambient-glow',
443
+ actionPayload: level,
444
+ })),
445
+ },
446
+ {
447
+ label: 'Presentation',
448
+ hiddenIn: 'electron',
449
+ items: [
450
+ {
451
+ label: 'Real',
452
+ type: 'radio' as const,
453
+ checked: !state.wireframe,
454
+ action: 'set-presentation',
455
+ actionPayload: 'real',
456
+ },
457
+ {
458
+ label: 'Clean Wireframe',
459
+ type: 'radio' as const,
460
+ checked: state.wireframe,
461
+ action: 'set-presentation',
462
+ actionPayload: 'wireframe',
463
+ },
464
+ ],
465
+ },
466
+ {
467
+ label: 'Screenshot',
468
+ type: 'checkbox',
469
+ checked: state.screenshotMode,
470
+ action: 'toggle-screenshot-mode',
471
+ hiddenIn: 'electron',
472
+ },
473
+ {
474
+ label: '3D',
475
+ type: 'checkbox',
476
+ checked: state.threeMode,
477
+ action: 'toggle-three-mode',
478
+ hiddenIn: 'electron',
479
+ },
480
+ {
481
+ label: 'Stay On Top',
482
+ type: 'checkbox',
483
+ checked: state.stayOnTop,
484
+ action: 'toggle-stay-on-top',
485
+ hiddenIn: 'browser',
486
+ },
487
+ { type: 'separator' },
488
+ {
489
+ label: 'Physical Size',
490
+ accelerator: 'CmdOrCtrl+1',
491
+ type: 'radio',
492
+ checked: state.scaleMode === 'physical',
493
+ action: 'scale-physical',
494
+ },
495
+ {
496
+ label: 'Point Accurate',
497
+ accelerator: 'CmdOrCtrl+2',
498
+ type: 'radio',
499
+ checked: state.scaleMode === 'point',
500
+ action: 'scale-point',
501
+ },
502
+ {
503
+ label: 'Pixel Accurate',
504
+ accelerator: 'CmdOrCtrl+3',
505
+ type: 'radio',
506
+ checked: state.scaleMode === 'pixel',
507
+ action: 'scale-pixel',
508
+ },
509
+ {
510
+ label: 'Fit Screen',
511
+ accelerator: 'CmdOrCtrl+0',
512
+ type: 'radio',
513
+ checked: state.scaleMode === 'fit',
514
+ action: 'scale-fit',
515
+ },
516
+ ],
517
+ }
518
+ }
519
+
520
+ function helpMenu(): SootSimMenuDef {
521
+ return {
522
+ label: 'Help',
523
+ notMinimal: true,
524
+ items: [
525
+ { label: `${rnxPublicBrand.name} Website`, action: 'open-website' },
526
+ {
527
+ label: 'Send Feedback...',
528
+ accelerator: 'CmdOrCtrl+Shift+I',
529
+ action: 'open-devtools-report',
530
+ showInShortcutDocs: true,
531
+ },
532
+ ],
533
+ }
534
+ }
535
+
536
+ export function buildSootSimDeviceChoiceDefs(
537
+ currentDevice: string,
538
+ deviceChoices: readonly SootSimMenuDeviceChoice[],
539
+ options: { action: 'switch-device' | 'new-simulator' },
540
+ ): SootSimMenuItemDef[] {
541
+ const items: SootSimMenuItemDef[] = []
542
+ let lastPlatform = ''
543
+ const isStateful = options.action === 'switch-device'
544
+
545
+ for (const choice of deviceChoices) {
546
+ if (lastPlatform && choice.platform !== lastPlatform)
547
+ items.push({ type: 'separator' })
548
+ lastPlatform = choice.platform
549
+
550
+ if (isStateful) {
551
+ items.push({
552
+ label: choice.label,
553
+ type: 'radio',
554
+ checked: choice.model === currentDevice,
555
+ action: options.action,
556
+ actionPayload: choice.model,
557
+ })
558
+ } else {
559
+ items.push({
560
+ label: choice.label,
561
+ action: options.action,
562
+ actionPayload: choice.model,
563
+ })
564
+ }
565
+ }
566
+
567
+ return items
568
+ }
569
+
570
+ function shouldIncludeItemForPlatform(
571
+ item: Exclude<SootSimMenuItemDef, { type: 'separator' }>,
572
+ platform: SootSimMenuPlatform,
573
+ ) {
574
+ if (item.hiddenIn === platform) return false
575
+ if (!item.accelerator || !item.action) return false
576
+ if (platform === 'browser') return item.browserAccelerator === true
577
+ return true
578
+ }
579
+
580
+ function shouldIncludeItemInDocs(
581
+ item: Exclude<SootSimMenuItemDef, { type: 'separator' }>,
582
+ platform: SootSimMenuPlatform,
583
+ ) {
584
+ if (!item.showInShortcutDocs) return false
585
+ if (platform === 'browser' && !item.browserAccelerator) return false
586
+ return true
587
+ }
588
+
589
+ function collectMenuShortcutsFromDefs(
590
+ menus: readonly SootSimMenuDef[],
591
+ platform: SootSimMenuPlatform,
592
+ includeItem: (
593
+ item: Exclude<SootSimMenuItemDef, { type: 'separator' }>,
594
+ platform: SootSimMenuPlatform,
595
+ ) => boolean,
596
+ ) {
597
+ const entries: SootSimMenuShortcutEntry[] = []
598
+
599
+ const visit = (menuLabel: string, items: readonly SootSimMenuItemDef[]) => {
600
+ for (const item of items) {
601
+ if (item.type === 'separator') continue
602
+ if (item.items?.length) visit(menuLabel, item.items)
603
+ if (!includeItem(item, platform)) continue
604
+ const { accelerator, action } = item
605
+ if (!accelerator || !action) continue
606
+ entries.push({
607
+ menuLabel,
608
+ label: item.shortcutLabel ?? item.label,
609
+ accelerator,
610
+ action,
611
+ actionPayload: item.actionPayload,
612
+ })
613
+ }
614
+ }
615
+
616
+ for (const menu of menus) {
617
+ if (menu.hiddenIn === platform) continue
618
+ visit(menu.label, menu.items)
619
+ }
620
+
621
+ return entries
622
+ }
623
+
624
+ export function getMenuShortcutEntriesFromDefs(
625
+ menus: readonly SootSimMenuDef[],
626
+ platform: SootSimMenuPlatform,
627
+ ): SootSimMenuShortcutEntry[] {
628
+ return collectMenuShortcutsFromDefs(menus, platform, shouldIncludeItemForPlatform)
629
+ }
630
+
631
+ export function getMenuShortcutEntries(
632
+ state: SootSimMenuState,
633
+ deviceChoices: readonly SootSimMenuDeviceChoice[],
634
+ platform: SootSimMenuPlatform,
635
+ ): SootSimMenuShortcutEntry[] {
636
+ return getMenuShortcutEntriesFromDefs(
637
+ buildSootSimMenuDefs(state, deviceChoices),
638
+ platform,
639
+ )
640
+ }
641
+
642
+ export function getShortcutDocSections(
643
+ state: SootSimMenuState,
644
+ deviceChoices: readonly SootSimMenuDeviceChoice[],
645
+ platform: SootSimMenuPlatform,
646
+ ): SootSimShortcutDocSection[] {
647
+ return getShortcutDocSectionsFromDefs(
648
+ buildSootSimMenuDefs(state, deviceChoices),
649
+ platform,
650
+ )
651
+ }
652
+
653
+ export function getShortcutDocSectionsFromDefs(
654
+ menus: readonly SootSimMenuDef[],
655
+ platform: SootSimMenuPlatform,
656
+ ): SootSimShortcutDocSection[] {
657
+ const sections: SootSimShortcutDocSection[] = []
658
+ const byMenu = new Map<string, SootSimShortcutDocEntry[]>()
659
+
660
+ for (const entry of collectMenuShortcutsFromDefs(
661
+ menus,
662
+ platform,
663
+ shouldIncludeItemInDocs,
664
+ )) {
665
+ const items = byMenu.get(entry.menuLabel) ?? []
666
+ items.push({
667
+ keys: formatAccelerator(entry.accelerator),
668
+ label: entry.label,
669
+ })
670
+ byMenu.set(entry.menuLabel, items)
671
+ }
672
+
673
+ for (const [title, entries] of byMenu) {
674
+ sections.push({
675
+ title: title.toLowerCase(),
676
+ entries,
677
+ })
678
+ }
679
+
680
+ return sections
681
+ }
682
+
683
+ export function formatAccelerator(accel: string): string {
684
+ return accel
685
+ .replace(/CmdOrCtrl\+/g, '\u2318')
686
+ .replace(/Ctrl\+/g, '\u2303')
687
+ .replace(/Alt\+/g, '\u2325')
688
+ .replace(/Shift\+/g, '\u21e7')
689
+ .replace(/Left/g, '\u2190')
690
+ .replace(/Right/g, '\u2192')
691
+ .replace(/Up/g, '\u2191')
692
+ .replace(/Down/g, '\u2193')
693
+ }
694
+
695
+ const ARROW_KEYS: Record<string, string> = {
696
+ Left: 'arrowleft',
697
+ Right: 'arrowright',
698
+ Up: 'arrowup',
699
+ Down: 'arrowdown',
700
+ }
701
+
702
+ function normalizeAcceleratorKey(key: string) {
703
+ return ARROW_KEYS[key] ?? key.toLowerCase()
704
+ }
705
+
706
+ function normalizeEventKey(key: string) {
707
+ return key.toLowerCase()
708
+ }
709
+
710
+ type KeyboardEventLike = Pick<
711
+ KeyboardEvent,
712
+ 'altKey' | 'ctrlKey' | 'key' | 'metaKey' | 'shiftKey'
713
+ >
714
+
715
+ export function matchesAccelerator(event: KeyboardEventLike, accelerator: string) {
716
+ const tokens = accelerator.split('+')
717
+ const keyToken = tokens[tokens.length - 1]
718
+ const wantsCmdOrCtrl = tokens.includes('CmdOrCtrl')
719
+ const wantsCtrl = tokens.includes('Ctrl')
720
+ const wantsAlt = tokens.includes('Alt')
721
+ const wantsShift = tokens.includes('Shift')
722
+
723
+ if (!keyToken) return false
724
+ if (event.altKey !== wantsAlt) return false
725
+ if (event.shiftKey !== wantsShift) return false
726
+
727
+ const hasCmdOrCtrl = event.metaKey || event.ctrlKey
728
+ if (wantsCmdOrCtrl && !hasCmdOrCtrl) return false
729
+ if (!wantsCmdOrCtrl && event.metaKey) return false
730
+ if (wantsCtrl && !event.ctrlKey) return false
731
+ if (!wantsCtrl && !wantsCmdOrCtrl && event.ctrlKey) return false
732
+
733
+ return normalizeEventKey(event.key) === normalizeAcceleratorKey(keyToken)
734
+ }