@noego/wood 0.4.5 → 0.4.7

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 (248) hide show
  1. package/bin/wood.js +9 -1
  2. package/dist/client/app_env.cjs +39 -0
  3. package/dist/client/app_env.cjs.map +1 -0
  4. package/dist/client/app_env.d.cts +7 -0
  5. package/dist/client/app_env.d.ts +7 -0
  6. package/dist/client/app_env.js +14 -0
  7. package/dist/client/app_env.js.map +1 -0
  8. package/dist/client/index.cjs +33 -9
  9. package/dist/client/index.cjs.map +1 -1
  10. package/dist/client/index.d.cts +5 -1
  11. package/dist/client/index.d.ts +5 -1
  12. package/dist/client/index.js +32 -9
  13. package/dist/client/index.js.map +1 -1
  14. package/dist/codegen/channel_map_generator.cjs +102 -0
  15. package/dist/codegen/channel_map_generator.cjs.map +1 -0
  16. package/dist/codegen/channel_map_generator.d.cts +6 -0
  17. package/dist/codegen/channel_map_generator.d.ts +6 -0
  18. package/dist/codegen/channel_map_generator.js +78 -0
  19. package/dist/codegen/channel_map_generator.js.map +1 -0
  20. package/dist/codegen/controller_registration_generator.cjs +6 -1
  21. package/dist/codegen/controller_registration_generator.cjs.map +1 -1
  22. package/dist/codegen/controller_registration_generator.js +6 -1
  23. package/dist/codegen/controller_registration_generator.js.map +1 -1
  24. package/dist/codegen/generated_file_notice.cjs +40 -0
  25. package/dist/codegen/generated_file_notice.cjs.map +1 -0
  26. package/dist/codegen/generated_file_notice.d.cts +3 -0
  27. package/dist/codegen/generated_file_notice.d.ts +3 -0
  28. package/dist/codegen/generated_file_notice.js +16 -0
  29. package/dist/codegen/generated_file_notice.js.map +1 -0
  30. package/dist/codegen/index.cjs +3 -0
  31. package/dist/codegen/index.cjs.map +1 -1
  32. package/dist/codegen/index.d.cts +1 -0
  33. package/dist/codegen/index.d.ts +1 -0
  34. package/dist/codegen/index.js +2 -0
  35. package/dist/codegen/index.js.map +1 -1
  36. package/dist/codegen/preload_generator.cjs +97 -1
  37. package/dist/codegen/preload_generator.cjs.map +1 -1
  38. package/dist/codegen/preload_generator.js +97 -1
  39. package/dist/codegen/preload_generator.js.map +1 -1
  40. package/dist/codegen/renderer_app_generator.cjs +6 -1
  41. package/dist/codegen/renderer_app_generator.cjs.map +1 -1
  42. package/dist/codegen/renderer_app_generator.js +6 -1
  43. package/dist/codegen/renderer_app_generator.js.map +1 -1
  44. package/dist/codegen/renderer_controller_registration_generator.cjs +6 -1
  45. package/dist/codegen/renderer_controller_registration_generator.cjs.map +1 -1
  46. package/dist/codegen/renderer_controller_registration_generator.js +6 -1
  47. package/dist/codegen/renderer_controller_registration_generator.js.map +1 -1
  48. package/dist/codegen/renderer_view_manifest_generator.cjs +6 -1
  49. package/dist/codegen/renderer_view_manifest_generator.cjs.map +1 -1
  50. package/dist/codegen/renderer_view_manifest_generator.js +6 -1
  51. package/dist/codegen/renderer_view_manifest_generator.js.map +1 -1
  52. package/dist/codegen/runtime_generator.cjs +6 -1
  53. package/dist/codegen/runtime_generator.cjs.map +1 -1
  54. package/dist/codegen/runtime_generator.js +6 -1
  55. package/dist/codegen/runtime_generator.js.map +1 -1
  56. package/dist/codegen/test_mock_generator.cjs +5 -1
  57. package/dist/codegen/test_mock_generator.cjs.map +1 -1
  58. package/dist/codegen/test_mock_generator.js +5 -1
  59. package/dist/codegen/test_mock_generator.js.map +1 -1
  60. package/dist/codegen/type_generator.cjs +7 -1
  61. package/dist/codegen/type_generator.cjs.map +1 -1
  62. package/dist/codegen/type_generator.js +7 -1
  63. package/dist/codegen/type_generator.js.map +1 -1
  64. package/dist/index.cjs +19 -6
  65. package/dist/index.cjs.map +1 -1
  66. package/dist/index.d.cts +7 -1
  67. package/dist/index.d.ts +7 -1
  68. package/dist/index.js +13 -1
  69. package/dist/index.js.map +1 -1
  70. package/dist/main/index.cjs +287 -62
  71. package/dist/main/index.cjs.map +1 -1
  72. package/dist/main/index.d.cts +57 -3
  73. package/dist/main/index.d.ts +57 -3
  74. package/dist/main/index.js +289 -62
  75. package/dist/main/index.js.map +1 -1
  76. package/dist/main/renderer_trace_drain_coordinator.cjs +127 -0
  77. package/dist/main/renderer_trace_drain_coordinator.cjs.map +1 -0
  78. package/dist/main/renderer_trace_drain_coordinator.d.cts +46 -0
  79. package/dist/main/renderer_trace_drain_coordinator.d.ts +46 -0
  80. package/dist/main/renderer_trace_drain_coordinator.js +101 -0
  81. package/dist/main/renderer_trace_drain_coordinator.js.map +1 -0
  82. package/dist/router/wood_router.cjs +9 -1
  83. package/dist/router/wood_router.cjs.map +1 -1
  84. package/dist/router/wood_router.d.cts +1 -1
  85. package/dist/router/wood_router.d.ts +1 -1
  86. package/dist/router/wood_router.js +9 -1
  87. package/dist/router/wood_router.js.map +1 -1
  88. package/dist/runtime/app_env.cjs +52 -0
  89. package/dist/runtime/app_env.cjs.map +1 -0
  90. package/dist/runtime/app_env.d.cts +10 -0
  91. package/dist/runtime/app_env.d.ts +10 -0
  92. package/dist/runtime/app_env.js +18 -0
  93. package/dist/runtime/app_env.js.map +1 -0
  94. package/dist/runtime/index.cjs +9 -2
  95. package/dist/runtime/index.cjs.map +1 -1
  96. package/dist/runtime/index.d.cts +5 -1
  97. package/dist/runtime/index.d.ts +5 -1
  98. package/dist/runtime/index.js +9 -1
  99. package/dist/runtime/index.js.map +1 -1
  100. package/dist/runtime/renderer_log_dispatcher.cjs.map +1 -1
  101. package/dist/runtime/renderer_log_dispatcher.d.cts +1 -1
  102. package/dist/runtime/renderer_log_dispatcher.d.ts +1 -1
  103. package/dist/runtime/renderer_log_dispatcher.js.map +1 -1
  104. package/dist/runtime/renderer_trace_batch_receiver_registry.cjs +50 -0
  105. package/dist/runtime/renderer_trace_batch_receiver_registry.cjs.map +1 -0
  106. package/dist/runtime/renderer_trace_batch_receiver_registry.d.cts +10 -0
  107. package/dist/runtime/renderer_trace_batch_receiver_registry.d.ts +10 -0
  108. package/dist/runtime/renderer_trace_batch_receiver_registry.js +24 -0
  109. package/dist/runtime/renderer_trace_batch_receiver_registry.js.map +1 -0
  110. package/dist/runtime/runtime.cjs +75 -57
  111. package/dist/runtime/runtime.cjs.map +1 -1
  112. package/dist/runtime/runtime.d.cts +8 -1
  113. package/dist/runtime/runtime.d.ts +8 -1
  114. package/dist/runtime/runtime.js +80 -58
  115. package/dist/runtime/runtime.js.map +1 -1
  116. package/dist/testing/browser.cjs +540 -11
  117. package/dist/testing/browser.cjs.map +1 -1
  118. package/dist/testing/browser.d.cts +219 -3
  119. package/dist/testing/browser.d.ts +219 -3
  120. package/dist/testing/browser.js +540 -11
  121. package/dist/testing/browser.js.map +1 -1
  122. package/dist/testing/create_controller_harness.d.cts +1 -1
  123. package/dist/testing/create_controller_harness.d.ts +1 -1
  124. package/dist/testing/index.cjs +13 -0
  125. package/dist/testing/index.cjs.map +1 -1
  126. package/dist/testing/index.d.cts +7 -2
  127. package/dist/testing/index.d.ts +7 -2
  128. package/dist/testing/index.js +14 -0
  129. package/dist/testing/index.js.map +1 -1
  130. package/dist/trace-testing/index.d.cts +1 -1
  131. package/dist/trace-testing/index.d.ts +1 -1
  132. package/dist/trace-testing/trace_recorder.d.cts +1 -1
  133. package/dist/trace-testing/trace_recorder.d.ts +1 -1
  134. package/dist/trace-transport/index.cjs +32 -0
  135. package/dist/trace-transport/index.cjs.map +1 -0
  136. package/dist/trace-transport/index.d.cts +6 -0
  137. package/dist/trace-transport/index.d.ts +6 -0
  138. package/dist/trace-transport/index.js +7 -0
  139. package/dist/trace-transport/index.js.map +1 -0
  140. package/dist/trace_hub-Cob54qRx.d.cts +56 -0
  141. package/dist/trace_hub-Cob54qRx.d.ts +56 -0
  142. package/dist/tracing/index.cjs +14 -0
  143. package/dist/tracing/index.cjs.map +1 -1
  144. package/dist/tracing/index.d.cts +7 -1
  145. package/dist/tracing/index.d.ts +7 -1
  146. package/dist/tracing/index.js +12 -0
  147. package/dist/tracing/index.js.map +1 -1
  148. package/dist/tracing/renderer_trace_batch_host.cjs +80 -0
  149. package/dist/tracing/renderer_trace_batch_host.cjs.map +1 -0
  150. package/dist/tracing/renderer_trace_batch_host.d.cts +16 -0
  151. package/dist/tracing/renderer_trace_batch_host.d.ts +16 -0
  152. package/dist/tracing/renderer_trace_batch_host.js +56 -0
  153. package/dist/tracing/renderer_trace_batch_host.js.map +1 -0
  154. package/dist/tracing/renderer_trace_batch_transport.cjs +313 -0
  155. package/dist/tracing/renderer_trace_batch_transport.cjs.map +1 -0
  156. package/dist/tracing/renderer_trace_batch_transport.d.cts +53 -0
  157. package/dist/tracing/renderer_trace_batch_transport.d.ts +53 -0
  158. package/dist/tracing/renderer_trace_batch_transport.js +292 -0
  159. package/dist/tracing/renderer_trace_batch_transport.js.map +1 -0
  160. package/dist/tracing/trace_commit_writer.cjs +159 -0
  161. package/dist/tracing/trace_commit_writer.cjs.map +1 -0
  162. package/dist/tracing/trace_commit_writer.d.cts +97 -0
  163. package/dist/tracing/trace_commit_writer.d.ts +97 -0
  164. package/dist/tracing/trace_commit_writer.js +135 -0
  165. package/dist/tracing/trace_commit_writer.js.map +1 -0
  166. package/dist/tracing/trace_hub.cjs +58 -3
  167. package/dist/tracing/trace_hub.cjs.map +1 -1
  168. package/dist/tracing/trace_hub.d.cts +2 -46
  169. package/dist/tracing/trace_hub.d.ts +2 -46
  170. package/dist/tracing/trace_hub.js +56 -3
  171. package/dist/tracing/trace_hub.js.map +1 -1
  172. package/dist/tracing/trace_provider.cjs +42 -12
  173. package/dist/tracing/trace_provider.cjs.map +1 -1
  174. package/dist/tracing/trace_provider.d.cts +15 -1
  175. package/dist/tracing/trace_provider.d.ts +15 -1
  176. package/dist/tracing/trace_provider.js +43 -12
  177. package/dist/tracing/trace_provider.js.map +1 -1
  178. package/dist/tracing/types/wood_trace_batch.types.cjs +17 -0
  179. package/dist/tracing/types/wood_trace_batch.types.cjs.map +1 -0
  180. package/dist/tracing/types/wood_trace_batch.types.d.cts +124 -0
  181. package/dist/tracing/types/wood_trace_batch.types.d.ts +124 -0
  182. package/dist/tracing/types/wood_trace_batch.types.js +1 -0
  183. package/dist/tracing/types/wood_trace_batch.types.js.map +1 -0
  184. package/dist/tracing/types/wood_trace_sink.types.cjs +17 -0
  185. package/dist/tracing/types/wood_trace_sink.types.cjs.map +1 -0
  186. package/dist/tracing/types/wood_trace_sink.types.d.cts +2 -0
  187. package/dist/tracing/types/wood_trace_sink.types.d.ts +2 -0
  188. package/dist/tracing/types/wood_trace_sink.types.js +1 -0
  189. package/dist/tracing/types/wood_trace_sink.types.js.map +1 -0
  190. package/dist/tracing/wood_trace_batch_contract.cjs +229 -0
  191. package/dist/tracing/wood_trace_batch_contract.cjs.map +1 -0
  192. package/dist/tracing/wood_trace_batch_contract.d.cts +23 -0
  193. package/dist/tracing/wood_trace_batch_contract.d.ts +23 -0
  194. package/dist/tracing/wood_trace_batch_contract.js +205 -0
  195. package/dist/tracing/wood_trace_batch_contract.js.map +1 -0
  196. package/dist/transport/build_transport_bridge.cjs +62 -0
  197. package/dist/transport/build_transport_bridge.cjs.map +1 -0
  198. package/dist/transport/build_transport_bridge.d.cts +29 -0
  199. package/dist/transport/build_transport_bridge.d.ts +29 -0
  200. package/dist/transport/build_transport_bridge.js +38 -0
  201. package/dist/transport/build_transport_bridge.js.map +1 -0
  202. package/dist/transport/in_process_router_transport.cjs +41 -0
  203. package/dist/transport/in_process_router_transport.cjs.map +1 -0
  204. package/dist/transport/in_process_router_transport.d.cts +31 -0
  205. package/dist/transport/in_process_router_transport.d.ts +31 -0
  206. package/dist/transport/in_process_router_transport.js +17 -0
  207. package/dist/transport/in_process_router_transport.js.map +1 -0
  208. package/dist/transport/index.cjs +42 -0
  209. package/dist/transport/index.cjs.map +1 -0
  210. package/dist/transport/index.d.cts +8 -0
  211. package/dist/transport/index.d.ts +8 -0
  212. package/dist/transport/index.js +19 -0
  213. package/dist/transport/index.js.map +1 -0
  214. package/dist/transport/loopback_remote_transport.cjs +270 -0
  215. package/dist/transport/loopback_remote_transport.cjs.map +1 -0
  216. package/dist/transport/loopback_remote_transport.d.cts +49 -0
  217. package/dist/transport/loopback_remote_transport.d.ts +49 -0
  218. package/dist/transport/loopback_remote_transport.js +237 -0
  219. package/dist/transport/loopback_remote_transport.js.map +1 -0
  220. package/dist/transport/loopback_serialization.cjs +116 -0
  221. package/dist/transport/loopback_serialization.cjs.map +1 -0
  222. package/dist/transport/loopback_serialization.d.cts +28 -0
  223. package/dist/transport/loopback_serialization.d.ts +28 -0
  224. package/dist/transport/loopback_serialization.js +91 -0
  225. package/dist/transport/loopback_serialization.js.map +1 -0
  226. package/dist/transport/transport_contract_suite.cjs +110 -0
  227. package/dist/transport/transport_contract_suite.cjs.map +1 -0
  228. package/dist/transport/transport_contract_suite.d.cts +23 -0
  229. package/dist/transport/transport_contract_suite.d.ts +23 -0
  230. package/dist/transport/transport_contract_suite.js +85 -0
  231. package/dist/transport/transport_contract_suite.js.map +1 -0
  232. package/dist/transport/wood_rpc_transport.cjs +17 -0
  233. package/dist/transport/wood_rpc_transport.cjs.map +1 -0
  234. package/dist/transport/wood_rpc_transport.d.cts +44 -0
  235. package/dist/transport/wood_rpc_transport.d.ts +44 -0
  236. package/dist/transport/wood_rpc_transport.js +1 -0
  237. package/dist/transport/wood_rpc_transport.js.map +1 -0
  238. package/dist/types/config.cjs.map +1 -1
  239. package/dist/types/config.d.cts +12 -0
  240. package/dist/types/config.d.ts +12 -0
  241. package/dist/window/window_manager.cjs +3 -0
  242. package/dist/window/window_manager.cjs.map +1 -1
  243. package/dist/window/window_manager.js +3 -0
  244. package/dist/window/window_manager.js.map +1 -1
  245. package/docs/browser-testing.md +139 -1
  246. package/package.json +22 -3
  247. package/src/components/NavigationShell.browser.test.ts +236 -0
  248. package/src/components/NavigationShell.svelte +162 -49
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The single seam every test-mode RPC path is built on.
3
+ *
4
+ * Production uses the Electron preload bridge (`ipcRenderer.invoke` +
5
+ * `ipcRenderer.on('__event:...')`). Tests use one of the in-process or loopback
6
+ * implementations. The generated bridge client is built over whichever transport
7
+ * is supplied, so application code never knows which one is active.
8
+ *
9
+ * IMPORTANT (fast-tier isolation): this module and `InProcessRouterTransport`
10
+ * must never import `electron` or `playwright`. Keep this file dependency-free.
11
+ */
12
+ interface WoodRpcTransport {
13
+ /**
14
+ * Dispatch a request/response call to a channel. `payload` is optional so
15
+ * inputless operations can be called as `invoke(channel)`.
16
+ */
17
+ invoke(channel: string, payload?: unknown): Promise<unknown>;
18
+ /**
19
+ * Subscribe to a main-process push channel (the `__event:`-prefixed events
20
+ * emitted by `WindowEventService`). Returns an unsubscribe function.
21
+ */
22
+ subscribe(channel: string, listener: (payload: unknown) => void): () => void;
23
+ }
24
+ /** One channel's input/output contract. The generated channel map is a record of these. */
25
+ interface WoodChannelContract {
26
+ input: unknown;
27
+ output: unknown;
28
+ }
29
+ /** Shape a generated channel map conforms to: channel name -> { input, output }. */
30
+ type WoodChannelMapLike = Record<string, WoodChannelContract>;
31
+ /** Extract the input type for a channel in a generated map. */
32
+ type WoodChannelInput<M extends WoodChannelMapLike, C extends keyof M> = M[C]['input'];
33
+ /** Extract the output type for a channel in a generated map. */
34
+ type WoodChannelOutput<M extends WoodChannelMapLike, C extends keyof M> = M[C]['output'];
35
+ /**
36
+ * A `call()` typed from a generated channel map. Harness `api.call` is this,
37
+ * parameterized on the app's generated `WoodChannelMap`:
38
+ *
39
+ * const call: TypedCall<WoodChannelMap> = ...;
40
+ * await call('messages.send', { threadId, text }); // fully typed
41
+ */
42
+ type TypedCall<M extends WoodChannelMapLike> = <C extends keyof M & string>(channel: C, ...payload: M[C]['input'] extends void ? [] : [M[C]['input']]) => Promise<M[C]['output']>;
43
+
44
+ export type { TypedCall, WoodChannelContract, WoodChannelInput, WoodChannelMapLike, WoodChannelOutput, WoodRpcTransport };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The single seam every test-mode RPC path is built on.
3
+ *
4
+ * Production uses the Electron preload bridge (`ipcRenderer.invoke` +
5
+ * `ipcRenderer.on('__event:...')`). Tests use one of the in-process or loopback
6
+ * implementations. The generated bridge client is built over whichever transport
7
+ * is supplied, so application code never knows which one is active.
8
+ *
9
+ * IMPORTANT (fast-tier isolation): this module and `InProcessRouterTransport`
10
+ * must never import `electron` or `playwright`. Keep this file dependency-free.
11
+ */
12
+ interface WoodRpcTransport {
13
+ /**
14
+ * Dispatch a request/response call to a channel. `payload` is optional so
15
+ * inputless operations can be called as `invoke(channel)`.
16
+ */
17
+ invoke(channel: string, payload?: unknown): Promise<unknown>;
18
+ /**
19
+ * Subscribe to a main-process push channel (the `__event:`-prefixed events
20
+ * emitted by `WindowEventService`). Returns an unsubscribe function.
21
+ */
22
+ subscribe(channel: string, listener: (payload: unknown) => void): () => void;
23
+ }
24
+ /** One channel's input/output contract. The generated channel map is a record of these. */
25
+ interface WoodChannelContract {
26
+ input: unknown;
27
+ output: unknown;
28
+ }
29
+ /** Shape a generated channel map conforms to: channel name -> { input, output }. */
30
+ type WoodChannelMapLike = Record<string, WoodChannelContract>;
31
+ /** Extract the input type for a channel in a generated map. */
32
+ type WoodChannelInput<M extends WoodChannelMapLike, C extends keyof M> = M[C]['input'];
33
+ /** Extract the output type for a channel in a generated map. */
34
+ type WoodChannelOutput<M extends WoodChannelMapLike, C extends keyof M> = M[C]['output'];
35
+ /**
36
+ * A `call()` typed from a generated channel map. Harness `api.call` is this,
37
+ * parameterized on the app's generated `WoodChannelMap`:
38
+ *
39
+ * const call: TypedCall<WoodChannelMap> = ...;
40
+ * await call('messages.send', { threadId, text }); // fully typed
41
+ */
42
+ type TypedCall<M extends WoodChannelMapLike> = <C extends keyof M & string>(channel: C, ...payload: M[C]['input'] extends void ? [] : [M[C]['input']]) => Promise<M[C]['output']>;
43
+
44
+ export type { TypedCall, WoodChannelContract, WoodChannelInput, WoodChannelMapLike, WoodChannelOutput, WoodRpcTransport };
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=wood_rpc_transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/types/config.ts"],"sourcesContent":["/** Configuration types for wood.config.yml */\n\n/** Top-level Wood configuration parsed from wood.config.yml */\nexport interface WoodConfig {\n root: string;\n outDir: string;\n app: {\n boot: string;\n watch: string[];\n };\n main: {\n entry: string;\n controllers: string;\n middleware: string;\n openapi: string;\n watch: string[];\n };\n renderer: {\n openapi: string;\n componentDir: string;\n controllers: string;\n entry?: string;\n watch?: string[];\n };\n windows: Record<string, WindowDef>;\n database?: {\n path: string;\n migrations?: string;\n };\n assets?: string[];\n dev?: DevConfig;\n build?: ElectronBuilderConfig;\n}\n\n/** BrowserWindow definition from the `windows` section of wood.config.yml */\nexport interface WindowDef {\n title: string;\n width: number;\n height: number;\n defaultRoute: string;\n frame?: boolean;\n transparent?: boolean;\n resizable?: boolean;\n alwaysOnTop?: boolean;\n minWidth?: number;\n minHeight?: number;\n maxWidth?: number;\n maxHeight?: number;\n show?: boolean;\n parent?: string;\n modal?: boolean;\n vibrancy?: string;\n backgroundMaterial?: string;\n rememberBounds?: boolean;\n contextMenu?: 'native' | 'custom';\n}\n\n/** Dev-mode settings from the `dev` section of wood.config.yml */\nexport interface DevConfig {\n port?: number;\n watch?: boolean;\n splitServe?: boolean;\n electronArgs?: string[];\n inspectPort?: number;\n backendInspectPort?: number;\n}\n\n/** Electron-builder packaging configuration from the `build` section */\nexport interface ElectronBuilderConfig {\n appId: string;\n productName: string;\n mac?: Record<string, unknown>;\n win?: Record<string, unknown>;\n linux?: Record<string, unknown>;\n [key: string]: unknown;\n}\n"],"mappings":";;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
1
+ {"version":3,"sources":["../../src/types/config.ts"],"sourcesContent":["/** Configuration types for wood.config.yml */\n\n/** Top-level Wood configuration parsed from wood.config.yml */\nexport interface WoodConfig {\n root: string;\n outDir: string;\n app: {\n boot: string;\n watch: string[];\n };\n main: {\n entry: string;\n controllers: string;\n middleware: string;\n openapi: string;\n watch: string[];\n };\n renderer: {\n openapi: string;\n componentDir: string;\n controllers: string;\n entry?: string;\n watch?: string[];\n };\n windows: Record<string, WindowDef>;\n database?: {\n path: string;\n migrations?: string;\n };\n assets?: string[];\n dev?: DevConfig;\n build?: ElectronBuilderConfig;\n}\n\n/** BrowserWindow definition from the `windows` section of wood.config.yml */\nexport interface WindowDef {\n title: string;\n width: number;\n height: number;\n defaultRoute: string;\n frame?: boolean;\n transparent?: boolean;\n resizable?: boolean;\n alwaysOnTop?: boolean;\n minWidth?: number;\n minHeight?: number;\n maxWidth?: number;\n maxHeight?: number;\n show?: boolean;\n parent?: string;\n modal?: boolean;\n vibrancy?: string;\n backgroundMaterial?: string;\n rememberBounds?: boolean;\n contextMenu?: 'native' | 'custom';\n acceptFirstMouse?: boolean;\n nativeQuitFallback?: boolean;\n titleBarStyle?: 'default' | 'hidden' | 'hiddenInset' | 'customButtonsOnHover';\n trafficLightPosition?: { x: number; y: number };\n titleBarOverlay?: boolean | { color?: string; symbolColor?: string; height?: number };\n}\n\n/** Dev-mode settings from the `dev` section of wood.config.yml */\nexport interface DevConfig {\n port?: number;\n watch?: boolean;\n splitServe?: boolean;\n electronArgs?: string[];\n inspectPort?: number;\n backendInspectPort?: number;\n}\n\n/** Electron-builder packaging configuration from the `build` section */\nexport interface ElectronBuilderConfig {\n appId: string;\n productName: string;\n mac?: Record<string, unknown>;\n win?: Record<string, unknown>;\n linux?: Record<string, unknown>;\n [key: string]: unknown;\n}\n"],"mappings":";;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
@@ -51,6 +51,18 @@ interface WindowDef {
51
51
  backgroundMaterial?: string;
52
52
  rememberBounds?: boolean;
53
53
  contextMenu?: 'native' | 'custom';
54
+ acceptFirstMouse?: boolean;
55
+ nativeQuitFallback?: boolean;
56
+ titleBarStyle?: 'default' | 'hidden' | 'hiddenInset' | 'customButtonsOnHover';
57
+ trafficLightPosition?: {
58
+ x: number;
59
+ y: number;
60
+ };
61
+ titleBarOverlay?: boolean | {
62
+ color?: string;
63
+ symbolColor?: string;
64
+ height?: number;
65
+ };
54
66
  }
55
67
  /** Dev-mode settings from the `dev` section of wood.config.yml */
56
68
  interface DevConfig {
@@ -51,6 +51,18 @@ interface WindowDef {
51
51
  backgroundMaterial?: string;
52
52
  rememberBounds?: boolean;
53
53
  contextMenu?: 'native' | 'custom';
54
+ acceptFirstMouse?: boolean;
55
+ nativeQuitFallback?: boolean;
56
+ titleBarStyle?: 'default' | 'hidden' | 'hiddenInset' | 'customButtonsOnHover';
57
+ trafficLightPosition?: {
58
+ x: number;
59
+ y: number;
60
+ };
61
+ titleBarOverlay?: boolean | {
62
+ color?: string;
63
+ symbolColor?: string;
64
+ height?: number;
65
+ };
54
66
  }
55
67
  /** Dev-mode settings from the `dev` section of wood.config.yml */
56
68
  interface DevConfig {
@@ -125,6 +125,9 @@ class WindowManager {
125
125
  ...def.maxHeight != null && { maxHeight: def.maxHeight },
126
126
  ...def.vibrancy && { vibrancy: def.vibrancy },
127
127
  ...def.backgroundMaterial && { backgroundMaterial: def.backgroundMaterial },
128
+ ...def.titleBarStyle && { titleBarStyle: def.titleBarStyle },
129
+ ...def.trafficLightPosition && { trafficLightPosition: def.trafficLightPosition },
130
+ ...def.titleBarOverlay != null && { titleBarOverlay: def.titleBarOverlay },
128
131
  ...parentWindow && { parent: parentWindow },
129
132
  webPreferences: {
130
133
  preload: this.preloadPath,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/window/window_manager.ts"],"sourcesContent":["import { join } from 'path';\nimport type { WindowDef } from '../types/index.cjs';\nimport type { ContextMenuItem } from '../types/context_menu.cjs';\nimport { getLogger } from '@noego/logger';\nimport { WindowStateStore } from './window_state_store.cjs';\nimport type { ScreenLike } from './window_state_store.cjs';\n\nconst logger = getLogger('wood:window');\n\n/** Subset of Electron BrowserWindow used by WindowManager (avoids compile-time electron dep). */\nexport interface BrowserWindowLike {\n loadURL(url: string): Promise<void>;\n loadFile(filePath: string): Promise<void>;\n close(): void;\n focus(): void;\n show(): void;\n destroy(): void;\n isDestroyed(): boolean;\n webContents: {\n send(channel: string, ...args: unknown[]): void;\n reload(): void;\n setWindowOpenHandler(handler: (details: { url: string }) => { action: 'deny' | 'allow' }): void;\n on(event: string, handler: (...args: any[]) => void): void;\n };\n on(event: string, handler: (...args: unknown[]) => void): void;\n getBounds(): { x: number; y: number; width: number; height: number };\n isMaximized(): boolean;\n isFullScreen(): boolean;\n maximize(): void;\n setFullScreen(flag: boolean): void;\n}\n\nexport interface BrowserWindowConstructor {\n new (options: Record<string, unknown>): BrowserWindowLike;\n getAllWindows(): BrowserWindowLike[];\n}\n\n/** Minimal shell interface for opening external URLs. */\nexport interface ShellLike {\n openExternal(url: string): Promise<unknown>;\n}\n\n/** Minimal clipboard interface for copying text. */\nexport interface ClipboardLike {\n writeText(text: string): void;\n}\n\n/** Minimal Menu interface for right-click context menus. */\nexport interface MenuLike {\n popup(options?: { window?: BrowserWindowLike }): void;\n}\n\nexport interface MenuConstructorLike {\n buildFromTemplate(template: Array<{ label: string; click: () => void; type?: 'separator'; enabled?: boolean }>): MenuLike;\n}\n\nexport class WindowManager {\n private windows = new Map<string, BrowserWindowLike>();\n private windowDefs: Record<string, WindowDef>;\n private BrowserWindowClass: BrowserWindowConstructor | null = null;\n private preloadPath: string;\n private rendererUrl?: string;\n private rendererDir?: string;\n private shell: ShellLike | null = null;\n private clipboard: ClipboardLike | null = null;\n private MenuClass: MenuConstructorLike | null = null;\n private stateStore = new WindowStateStore();\n\n constructor(options: {\n windowDefs: Record<string, WindowDef>;\n preloadPath: string;\n rendererUrl?: string;\n rendererDir?: string;\n applicationRoot?: string;\n }) {\n this.windowDefs = options.windowDefs;\n this.preloadPath = options.preloadPath;\n this.rendererUrl = options.rendererUrl;\n this.rendererDir = options.rendererDir;\n if (options.applicationRoot) {\n this.stateStore.setApplicationRoot(options.applicationRoot);\n }\n }\n\n /**\n * Set the BrowserWindow class (from electron).\n * Called at runtime so we don't need electron at compile time.\n */\n setBrowserWindow(BW: BrowserWindowConstructor): void {\n this.BrowserWindowClass = BW;\n }\n\n /**\n * Set the shell module (from electron) for opening external URLs.\n */\n setShell(shell: ShellLike): void {\n this.shell = shell;\n }\n\n /**\n * Set the clipboard module (from electron) for copying text.\n */\n setClipboard(clipboard: ClipboardLike): void {\n this.clipboard = clipboard;\n }\n\n /**\n * Set the Menu class (from electron) for context menus.\n */\n setMenu(menu: MenuConstructorLike): void {\n this.MenuClass = menu;\n }\n\n /**\n * Set the user data path for persisting window state.\n */\n setUserDataPath(path: string): void {\n this.stateStore.setUserDataPath(path);\n }\n\n /**\n * Set the application root directory for Wood JSON state persistence.\n */\n setApplicationRoot(path: string): void {\n this.stateStore.setApplicationRoot(path);\n }\n\n /**\n * Set the screen module (from electron) for display validation.\n */\n setScreen(screen: ScreenLike): void {\n this.stateStore.setScreen(screen);\n }\n\n /**\n * Create a window from its definition.\n */\n async createWindow(windowId: string, extraOptions?: Record<string, unknown>): Promise<BrowserWindowLike> {\n if (!this.BrowserWindowClass) {\n throw new Error('BrowserWindow class not set. Call setBrowserWindow() first.');\n }\n\n const def = this.windowDefs[windowId];\n if (!def) {\n throw new Error(`Window \"${windowId}\" not defined in config`);\n }\n\n // If the window already exists and isn't destroyed, just focus it\n const existing = this.windows.get(windowId);\n if (existing && !existing.isDestroyed()) {\n existing.focus();\n return existing;\n }\n\n // Resolve parent window\n let parentWindow: BrowserWindowLike | undefined;\n if (def.parent) {\n parentWindow = this.windows.get(def.parent);\n }\n\n // Resolve saved bounds for rememberBounds\n const useRememberBounds = def.rememberBounds === true;\n const savedBounds = useRememberBounds ? this.stateStore.getBounds(windowId) : null;\n\n // Determine if we should defer showing (anti-flicker for restored windows)\n const defShow = def.show ?? true;\n const deferShow = useRememberBounds && savedBounds != null && defShow && extraOptions?.show !== false;\n\n // Build BrowserWindow options from WindowDef\n const options: Record<string, unknown> = {\n title: def.title,\n width: savedBounds?.width ?? def.width,\n height: savedBounds?.height ?? def.height,\n ...(savedBounds && { x: savedBounds.x, y: savedBounds.y }),\n frame: def.frame ?? true,\n transparent: def.transparent ?? false,\n resizable: def.resizable ?? true,\n alwaysOnTop: def.alwaysOnTop ?? false,\n show: deferShow ? false : defShow,\n modal: def.modal ?? false,\n ...(def.minWidth != null && { minWidth: def.minWidth }),\n ...(def.minHeight != null && { minHeight: def.minHeight }),\n ...(def.maxWidth != null && { maxWidth: def.maxWidth }),\n ...(def.maxHeight != null && { maxHeight: def.maxHeight }),\n ...(def.vibrancy && { vibrancy: def.vibrancy }),\n ...(def.backgroundMaterial && { backgroundMaterial: def.backgroundMaterial }),\n ...(parentWindow && { parent: parentWindow }),\n webPreferences: {\n preload: this.preloadPath,\n contextIsolation: true,\n sandbox: true,\n nodeIntegration: false,\n },\n ...extraOptions,\n };\n\n const win = new this.BrowserWindowClass(options);\n\n // Restore maximized/fullscreen state\n if (savedBounds?.isMaximized) {\n win.maximize();\n } else if (savedBounds?.isFullScreen) {\n win.setFullScreen(true);\n }\n\n // Show window after restoring state (anti-flicker)\n if (deferShow) {\n win.show();\n }\n\n // Load content\n if (this.rendererUrl) {\n await win.loadURL(this.rendererUrl);\n } else if (this.rendererDir) {\n await win.loadFile(join(this.rendererDir, 'index.html'));\n }\n\n // Open http/https links in system browser instead of inside Electron\n if (this.shell) {\n const shellRef = this.shell;\n win.webContents.setWindowOpenHandler(({ url }) => {\n if (url.startsWith('http://') || url.startsWith('https://')) {\n shellRef.openExternal(url);\n }\n return { action: 'deny' };\n });\n }\n\n // Track window\n this.windows.set(windowId, win);\n logger.debug(`Created window \"${windowId}\"`);\n\n // Persist bounds on move/resize/close\n if (useRememberBounds) {\n const captureNormalBounds = () => {\n if (!win.isMaximized() && !win.isFullScreen()) {\n const rect = win.getBounds();\n this.stateStore.updateBoundsDebounced(windowId, {\n ...rect,\n isMaximized: false,\n isFullScreen: false,\n });\n }\n };\n\n win.on('move', captureNormalBounds);\n win.on('resize', captureNormalBounds);\n\n win.on('close', () => {\n const rect = win.getBounds();\n this.stateStore.saveBoundsSync(windowId, {\n ...rect,\n isMaximized: win.isMaximized(),\n isFullScreen: win.isFullScreen(),\n });\n });\n }\n\n // Cleanup on close\n win.on('closed', () => {\n this.windows.delete(windowId);\n logger.debug(`Window \"${windowId}\" closed`);\n });\n\n return win;\n }\n\n /**\n * Get a window by ID.\n */\n getWindow(windowId: string): BrowserWindowLike | undefined {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) return win;\n return undefined;\n }\n\n /**\n * Close a window by ID.\n */\n closeWindow(windowId: string): void {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) {\n win.close();\n }\n }\n\n /**\n * Focus a window by ID.\n */\n focusWindow(windowId: string): void {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) {\n win.show();\n win.focus();\n }\n }\n\n /**\n * Send an event to all open windows.\n */\n broadcastToAll(channel: string, ...data: unknown[]): void {\n for (const [, win] of this.windows) {\n if (!win.isDestroyed()) {\n win.webContents.send(channel, ...data);\n }\n }\n }\n\n /**\n * Get all open window IDs.\n */\n getOpenWindowIds(): string[] {\n return Array.from(this.windows.entries())\n .filter(([, win]) => !win.isDestroyed())\n .map(([id]) => id);\n }\n\n /**\n * Close all windows.\n */\n closeAll(): void {\n for (const [, win] of this.windows) {\n if (!win.isDestroyed()) {\n win.destroy();\n }\n }\n this.windows.clear();\n logger.debug('All windows closed');\n }\n\n /**\n * Handle the __window IPC channel.\n */\n async handleWindowOperation(data: {\n action: string;\n windowId?: string;\n options?: Record<string, unknown>;\n }): Promise<void> {\n switch (data.action) {\n case 'open':\n if (!data.windowId) throw new Error('windowId required for open');\n await this.createWindow(data.windowId, data.options);\n break;\n case 'close':\n if (data.windowId) {\n this.closeWindow(data.windowId);\n }\n break;\n case 'focus':\n if (!data.windowId) throw new Error('windowId required for focus');\n this.focusWindow(data.windowId);\n break;\n default:\n throw new Error(`Unknown window action: ${data.action}`);\n }\n }\n\n /**\n * Handle the __contextMenu IPC channel.\n *\n * Receives `{ action: 'show', items, position }` from the renderer's\n * `use:contextMenu` action. Checks the window's contextMenu config:\n * - **native**: builds Electron Menu from items, pops it. String actions\n * are handled on main. Callback items send `__contextMenu:callback`.\n * - **custom**: forwards items + position as `__contextMenu:show` push event.\n */\n handleContextMenu(\n windowId: string,\n data: { action: string; items: ContextMenuItem[]; position: { x: number; y: number } },\n ): void {\n const win = this.windows.get(windowId);\n if (!win || win.isDestroyed()) return;\n\n const def = this.windowDefs[windowId];\n if (!def) return;\n\n if (data.action !== 'show') return;\n\n if (def.contextMenu === 'native') {\n this.showNativeContextMenu(win, data.items);\n } else if (def.contextMenu === 'custom') {\n win.webContents.send('__event:__contextMenu:show', {\n items: data.items,\n position: data.position,\n });\n }\n }\n\n /**\n * Build and show a native Electron context menu from serialized items.\n */\n private showNativeContextMenu(win: BrowserWindowLike, items: ContextMenuItem[]): void {\n if (!this.MenuClass) return;\n\n const template = items.map(item => {\n if (item.type === 'separator') {\n return { label: '', click: () => {}, type: 'separator' as const, enabled: true };\n }\n return {\n label: item.label ?? '',\n enabled: item.disabled !== true,\n click: () => {\n if (item._callbackId) {\n win.webContents.send('__event:__contextMenu:callback', item._callbackId);\n } else if (item.action) {\n this.executeMenuAction(item.action, item.data, win);\n }\n },\n };\n });\n\n if (template.length === 0) return;\n\n const menu = this.MenuClass.buildFromTemplate(template);\n menu.popup({ window: win });\n }\n\n /**\n * Execute a built-in menu action on the main process.\n */\n private executeMenuAction(action: string, data: string | undefined, win: BrowserWindowLike): void {\n switch (action) {\n case 'openExternal':\n if (data && this.shell) {\n this.shell.openExternal(data);\n }\n break;\n case 'copyText':\n if (data && this.clipboard) {\n this.clipboard.writeText(data);\n }\n break;\n case 'copy':\n win.webContents.send('__contextMenu:exec', 'copy');\n break;\n case 'cut':\n win.webContents.send('__contextMenu:exec', 'cut');\n break;\n case 'paste':\n win.webContents.send('__contextMenu:exec', 'paste');\n break;\n case 'selectAll':\n win.webContents.send('__contextMenu:exec', 'selectAll');\n break;\n default:\n // Unknown action — dispatch to renderer\n win.webContents.send('__contextMenu:action', action, data);\n break;\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAqB;AAGrB,oBAA0B;AAC1B,gCAAiC;AAGjC,MAAM,aAAS,yBAAU,aAAa;AAiD/B,MAAM,cAAc;AAAA,EAYzB,YAAY,SAMT;AAjBH,SAAQ,UAAU,oBAAI,IAA+B;AAErD,SAAQ,qBAAsD;AAI9D,SAAQ,QAA0B;AAClC,SAAQ,YAAkC;AAC1C,SAAQ,YAAwC;AAChD,SAAQ,aAAa,IAAI,2CAAiB;AASxC,SAAK,aAAa,QAAQ;AAC1B,SAAK,cAAc,QAAQ;AAC3B,SAAK,cAAc,QAAQ;AAC3B,SAAK,cAAc,QAAQ;AAC3B,QAAI,QAAQ,iBAAiB;AAC3B,WAAK,WAAW,mBAAmB,QAAQ,eAAe;AAAA,IAC5D;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,iBAAiB,IAAoC;AACnD,SAAK,qBAAqB;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,OAAwB;AAC/B,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA,EAKA,aAAa,WAAgC;AAC3C,SAAK,YAAY;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAKA,QAAQ,MAAiC;AACvC,SAAK,YAAY;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,MAAoB;AAClC,SAAK,WAAW,gBAAgB,IAAI;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAmB,MAAoB;AACrC,SAAK,WAAW,mBAAmB,IAAI;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,QAA0B;AAClC,SAAK,WAAW,UAAU,MAAM;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aAAa,UAAkB,cAAoE;AACvG,QAAI,CAAC,KAAK,oBAAoB;AAC5B,YAAM,IAAI,MAAM,6DAA6D;AAAA,IAC/E;AAEA,UAAM,MAAM,KAAK,WAAW,QAAQ;AACpC,QAAI,CAAC,KAAK;AACR,YAAM,IAAI,MAAM,WAAW,QAAQ,yBAAyB;AAAA,IAC9D;AAGA,UAAM,WAAW,KAAK,QAAQ,IAAI,QAAQ;AAC1C,QAAI,YAAY,CAAC,SAAS,YAAY,GAAG;AACvC,eAAS,MAAM;AACf,aAAO;AAAA,IACT;AAGA,QAAI;AACJ,QAAI,IAAI,QAAQ;AACd,qBAAe,KAAK,QAAQ,IAAI,IAAI,MAAM;AAAA,IAC5C;AAGA,UAAM,oBAAoB,IAAI,mBAAmB;AACjD,UAAM,cAAc,oBAAoB,KAAK,WAAW,UAAU,QAAQ,IAAI;AAG9E,UAAM,UAAU,IAAI,QAAQ;AAC5B,UAAM,YAAY,qBAAqB,eAAe,QAAQ,WAAW,cAAc,SAAS;AAGhG,UAAM,UAAmC;AAAA,MACvC,OAAO,IAAI;AAAA,MACX,OAAO,aAAa,SAAS,IAAI;AAAA,MACjC,QAAQ,aAAa,UAAU,IAAI;AAAA,MACnC,GAAI,eAAe,EAAE,GAAG,YAAY,GAAG,GAAG,YAAY,EAAE;AAAA,MACxD,OAAO,IAAI,SAAS;AAAA,MACpB,aAAa,IAAI,eAAe;AAAA,MAChC,WAAW,IAAI,aAAa;AAAA,MAC5B,aAAa,IAAI,eAAe;AAAA,MAChC,MAAM,YAAY,QAAQ;AAAA,MAC1B,OAAO,IAAI,SAAS;AAAA,MACpB,GAAI,IAAI,YAAY,QAAQ,EAAE,UAAU,IAAI,SAAS;AAAA,MACrD,GAAI,IAAI,aAAa,QAAQ,EAAE,WAAW,IAAI,UAAU;AAAA,MACxD,GAAI,IAAI,YAAY,QAAQ,EAAE,UAAU,IAAI,SAAS;AAAA,MACrD,GAAI,IAAI,aAAa,QAAQ,EAAE,WAAW,IAAI,UAAU;AAAA,MACxD,GAAI,IAAI,YAAY,EAAE,UAAU,IAAI,SAAS;AAAA,MAC7C,GAAI,IAAI,sBAAsB,EAAE,oBAAoB,IAAI,mBAAmB;AAAA,MAC3E,GAAI,gBAAgB,EAAE,QAAQ,aAAa;AAAA,MAC3C,gBAAgB;AAAA,QACd,SAAS,KAAK;AAAA,QACd,kBAAkB;AAAA,QAClB,SAAS;AAAA,QACT,iBAAiB;AAAA,MACnB;AAAA,MACA,GAAG;AAAA,IACL;AAEA,UAAM,MAAM,IAAI,KAAK,mBAAmB,OAAO;AAG/C,QAAI,aAAa,aAAa;AAC5B,UAAI,SAAS;AAAA,IACf,WAAW,aAAa,cAAc;AACpC,UAAI,cAAc,IAAI;AAAA,IACxB;AAGA,QAAI,WAAW;AACb,UAAI,KAAK;AAAA,IACX;AAGA,QAAI,KAAK,aAAa;AACpB,YAAM,IAAI,QAAQ,KAAK,WAAW;AAAA,IACpC,WAAW,KAAK,aAAa;AAC3B,YAAM,IAAI,aAAS,kBAAK,KAAK,aAAa,YAAY,CAAC;AAAA,IACzD;AAGA,QAAI,KAAK,OAAO;AACd,YAAM,WAAW,KAAK;AACtB,UAAI,YAAY,qBAAqB,CAAC,EAAE,IAAI,MAAM;AAChD,YAAI,IAAI,WAAW,SAAS,KAAK,IAAI,WAAW,UAAU,GAAG;AAC3D,mBAAS,aAAa,GAAG;AAAA,QAC3B;AACA,eAAO,EAAE,QAAQ,OAAO;AAAA,MAC1B,CAAC;AAAA,IACH;AAGA,SAAK,QAAQ,IAAI,UAAU,GAAG;AAC9B,WAAO,MAAM,mBAAmB,QAAQ,GAAG;AAG3C,QAAI,mBAAmB;AACrB,YAAM,sBAAsB,MAAM;AAChC,YAAI,CAAC,IAAI,YAAY,KAAK,CAAC,IAAI,aAAa,GAAG;AAC7C,gBAAM,OAAO,IAAI,UAAU;AAC3B,eAAK,WAAW,sBAAsB,UAAU;AAAA,YAC9C,GAAG;AAAA,YACH,aAAa;AAAA,YACb,cAAc;AAAA,UAChB,CAAC;AAAA,QACH;AAAA,MACF;AAEA,UAAI,GAAG,QAAQ,mBAAmB;AAClC,UAAI,GAAG,UAAU,mBAAmB;AAEpC,UAAI,GAAG,SAAS,MAAM;AACpB,cAAM,OAAO,IAAI,UAAU;AAC3B,aAAK,WAAW,eAAe,UAAU;AAAA,UACvC,GAAG;AAAA,UACH,aAAa,IAAI,YAAY;AAAA,UAC7B,cAAc,IAAI,aAAa;AAAA,QACjC,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAGA,QAAI,GAAG,UAAU,MAAM;AACrB,WAAK,QAAQ,OAAO,QAAQ;AAC5B,aAAO,MAAM,WAAW,QAAQ,UAAU;AAAA,IAC5C,CAAC;AAED,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,UAAiD;AACzD,UAAM,MAAM,KAAK,QAAQ,IAAI,QAAQ;AACrC,QAAI,OAAO,CAAC,IAAI,YAAY,EAAG,QAAO;AACtC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,UAAwB;AAClC,UAAM,MAAM,KAAK,QAAQ,IAAI,QAAQ;AACrC,QAAI,OAAO,CAAC,IAAI,YAAY,GAAG;AAC7B,UAAI,MAAM;AAAA,IACZ;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,UAAwB;AAClC,UAAM,MAAM,KAAK,QAAQ,IAAI,QAAQ;AACrC,QAAI,OAAO,CAAC,IAAI,YAAY,GAAG;AAC7B,UAAI,KAAK;AACT,UAAI,MAAM;AAAA,IACZ;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe,YAAoB,MAAuB;AACxD,eAAW,CAAC,EAAE,GAAG,KAAK,KAAK,SAAS;AAClC,UAAI,CAAC,IAAI,YAAY,GAAG;AACtB,YAAI,YAAY,KAAK,SAAS,GAAG,IAAI;AAAA,MACvC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,mBAA6B;AAC3B,WAAO,MAAM,KAAK,KAAK,QAAQ,QAAQ,CAAC,EACrC,OAAO,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC,IAAI,YAAY,CAAC,EACtC,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA,EAKA,WAAiB;AACf,eAAW,CAAC,EAAE,GAAG,KAAK,KAAK,SAAS;AAClC,UAAI,CAAC,IAAI,YAAY,GAAG;AACtB,YAAI,QAAQ;AAAA,MACd;AAAA,IACF;AACA,SAAK,QAAQ,MAAM;AACnB,WAAO,MAAM,oBAAoB;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,sBAAsB,MAIV;AAChB,YAAQ,KAAK,QAAQ;AAAA,MACnB,KAAK;AACH,YAAI,CAAC,KAAK,SAAU,OAAM,IAAI,MAAM,4BAA4B;AAChE,cAAM,KAAK,aAAa,KAAK,UAAU,KAAK,OAAO;AACnD;AAAA,MACF,KAAK;AACH,YAAI,KAAK,UAAU;AACjB,eAAK,YAAY,KAAK,QAAQ;AAAA,QAChC;AACA;AAAA,MACF,KAAK;AACH,YAAI,CAAC,KAAK,SAAU,OAAM,IAAI,MAAM,6BAA6B;AACjE,aAAK,YAAY,KAAK,QAAQ;AAC9B;AAAA,MACF;AACE,cAAM,IAAI,MAAM,0BAA0B,KAAK,MAAM,EAAE;AAAA,IAC3D;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,kBACE,UACA,MACM;AACN,UAAM,MAAM,KAAK,QAAQ,IAAI,QAAQ;AACrC,QAAI,CAAC,OAAO,IAAI,YAAY,EAAG;AAE/B,UAAM,MAAM,KAAK,WAAW,QAAQ;AACpC,QAAI,CAAC,IAAK;AAEV,QAAI,KAAK,WAAW,OAAQ;AAE5B,QAAI,IAAI,gBAAgB,UAAU;AAChC,WAAK,sBAAsB,KAAK,KAAK,KAAK;AAAA,IAC5C,WAAW,IAAI,gBAAgB,UAAU;AACvC,UAAI,YAAY,KAAK,8BAA8B;AAAA,QACjD,OAAO,KAAK;AAAA,QACZ,UAAU,KAAK;AAAA,MACjB,CAAC;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,sBAAsB,KAAwB,OAAgC;AACpF,QAAI,CAAC,KAAK,UAAW;AAErB,UAAM,WAAW,MAAM,IAAI,UAAQ;AACjC,UAAI,KAAK,SAAS,aAAa;AAC7B,eAAO,EAAE,OAAO,IAAI,OAAO,MAAM;AAAA,QAAC,GAAG,MAAM,aAAsB,SAAS,KAAK;AAAA,MACjF;AACA,aAAO;AAAA,QACL,OAAO,KAAK,SAAS;AAAA,QACrB,SAAS,KAAK,aAAa;AAAA,QAC3B,OAAO,MAAM;AACX,cAAI,KAAK,aAAa;AACpB,gBAAI,YAAY,KAAK,kCAAkC,KAAK,WAAW;AAAA,UACzE,WAAW,KAAK,QAAQ;AACtB,iBAAK,kBAAkB,KAAK,QAAQ,KAAK,MAAM,GAAG;AAAA,UACpD;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAED,QAAI,SAAS,WAAW,EAAG;AAE3B,UAAM,OAAO,KAAK,UAAU,kBAAkB,QAAQ;AACtD,SAAK,MAAM,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA,EAKQ,kBAAkB,QAAgB,MAA0B,KAA8B;AAChG,YAAQ,QAAQ;AAAA,MACd,KAAK;AACH,YAAI,QAAQ,KAAK,OAAO;AACtB,eAAK,MAAM,aAAa,IAAI;AAAA,QAC9B;AACA;AAAA,MACF,KAAK;AACH,YAAI,QAAQ,KAAK,WAAW;AAC1B,eAAK,UAAU,UAAU,IAAI;AAAA,QAC/B;AACA;AAAA,MACF,KAAK;AACH,YAAI,YAAY,KAAK,sBAAsB,MAAM;AACjD;AAAA,MACF,KAAK;AACH,YAAI,YAAY,KAAK,sBAAsB,KAAK;AAChD;AAAA,MACF,KAAK;AACH,YAAI,YAAY,KAAK,sBAAsB,OAAO;AAClD;AAAA,MACF,KAAK;AACH,YAAI,YAAY,KAAK,sBAAsB,WAAW;AACtD;AAAA,MACF;AAEE,YAAI,YAAY,KAAK,wBAAwB,QAAQ,IAAI;AACzD;AAAA,IACJ;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/window/window_manager.ts"],"sourcesContent":["import { join } from 'path';\nimport type { WindowDef } from '../types/index.cjs';\nimport type { ContextMenuItem } from '../types/context_menu.cjs';\nimport { getLogger } from '@noego/logger';\nimport { WindowStateStore } from './window_state_store.cjs';\nimport type { ScreenLike } from './window_state_store.cjs';\n\nconst logger = getLogger('wood:window');\n\n/** Subset of Electron BrowserWindow used by WindowManager (avoids compile-time electron dep). */\nexport interface BrowserWindowLike {\n loadURL(url: string): Promise<void>;\n loadFile(filePath: string): Promise<void>;\n close(): void;\n focus(): void;\n show(): void;\n destroy(): void;\n isDestroyed(): boolean;\n webContents: {\n send(channel: string, ...args: unknown[]): void;\n reload(): void;\n setWindowOpenHandler(handler: (details: { url: string }) => { action: 'deny' | 'allow' }): void;\n on(event: string, handler: (...args: any[]) => void): void;\n };\n on(event: string, handler: (...args: unknown[]) => void): void;\n getBounds(): { x: number; y: number; width: number; height: number };\n isMaximized(): boolean;\n isFullScreen(): boolean;\n maximize(): void;\n setFullScreen(flag: boolean): void;\n}\n\nexport interface BrowserWindowConstructor {\n new (options: Record<string, unknown>): BrowserWindowLike;\n getAllWindows(): BrowserWindowLike[];\n}\n\n/** Minimal shell interface for opening external URLs. */\nexport interface ShellLike {\n openExternal(url: string): Promise<unknown>;\n}\n\n/** Minimal clipboard interface for copying text. */\nexport interface ClipboardLike {\n writeText(text: string): void;\n}\n\n/** Minimal Menu interface for right-click context menus. */\nexport interface MenuLike {\n popup(options?: { window?: BrowserWindowLike }): void;\n}\n\nexport interface MenuConstructorLike {\n buildFromTemplate(template: Array<{ label: string; click: () => void; type?: 'separator'; enabled?: boolean }>): MenuLike;\n}\n\nexport class WindowManager {\n private windows = new Map<string, BrowserWindowLike>();\n private windowDefs: Record<string, WindowDef>;\n private BrowserWindowClass: BrowserWindowConstructor | null = null;\n private preloadPath: string;\n private rendererUrl?: string;\n private rendererDir?: string;\n private shell: ShellLike | null = null;\n private clipboard: ClipboardLike | null = null;\n private MenuClass: MenuConstructorLike | null = null;\n private stateStore = new WindowStateStore();\n\n constructor(options: {\n windowDefs: Record<string, WindowDef>;\n preloadPath: string;\n rendererUrl?: string;\n rendererDir?: string;\n applicationRoot?: string;\n }) {\n this.windowDefs = options.windowDefs;\n this.preloadPath = options.preloadPath;\n this.rendererUrl = options.rendererUrl;\n this.rendererDir = options.rendererDir;\n if (options.applicationRoot) {\n this.stateStore.setApplicationRoot(options.applicationRoot);\n }\n }\n\n /**\n * Set the BrowserWindow class (from electron).\n * Called at runtime so we don't need electron at compile time.\n */\n setBrowserWindow(BW: BrowserWindowConstructor): void {\n this.BrowserWindowClass = BW;\n }\n\n /**\n * Set the shell module (from electron) for opening external URLs.\n */\n setShell(shell: ShellLike): void {\n this.shell = shell;\n }\n\n /**\n * Set the clipboard module (from electron) for copying text.\n */\n setClipboard(clipboard: ClipboardLike): void {\n this.clipboard = clipboard;\n }\n\n /**\n * Set the Menu class (from electron) for context menus.\n */\n setMenu(menu: MenuConstructorLike): void {\n this.MenuClass = menu;\n }\n\n /**\n * Set the user data path for persisting window state.\n */\n setUserDataPath(path: string): void {\n this.stateStore.setUserDataPath(path);\n }\n\n /**\n * Set the application root directory for Wood JSON state persistence.\n */\n setApplicationRoot(path: string): void {\n this.stateStore.setApplicationRoot(path);\n }\n\n /**\n * Set the screen module (from electron) for display validation.\n */\n setScreen(screen: ScreenLike): void {\n this.stateStore.setScreen(screen);\n }\n\n /**\n * Create a window from its definition.\n */\n async createWindow(windowId: string, extraOptions?: Record<string, unknown>): Promise<BrowserWindowLike> {\n if (!this.BrowserWindowClass) {\n throw new Error('BrowserWindow class not set. Call setBrowserWindow() first.');\n }\n\n const def = this.windowDefs[windowId];\n if (!def) {\n throw new Error(`Window \"${windowId}\" not defined in config`);\n }\n\n // If the window already exists and isn't destroyed, just focus it\n const existing = this.windows.get(windowId);\n if (existing && !existing.isDestroyed()) {\n existing.focus();\n return existing;\n }\n\n // Resolve parent window\n let parentWindow: BrowserWindowLike | undefined;\n if (def.parent) {\n parentWindow = this.windows.get(def.parent);\n }\n\n // Resolve saved bounds for rememberBounds\n const useRememberBounds = def.rememberBounds === true;\n const savedBounds = useRememberBounds ? this.stateStore.getBounds(windowId) : null;\n\n // Determine if we should defer showing (anti-flicker for restored windows)\n const defShow = def.show ?? true;\n const deferShow = useRememberBounds && savedBounds != null && defShow && extraOptions?.show !== false;\n\n // Build BrowserWindow options from WindowDef\n const options: Record<string, unknown> = {\n title: def.title,\n width: savedBounds?.width ?? def.width,\n height: savedBounds?.height ?? def.height,\n ...(savedBounds && { x: savedBounds.x, y: savedBounds.y }),\n frame: def.frame ?? true,\n transparent: def.transparent ?? false,\n resizable: def.resizable ?? true,\n alwaysOnTop: def.alwaysOnTop ?? false,\n show: deferShow ? false : defShow,\n modal: def.modal ?? false,\n ...(def.minWidth != null && { minWidth: def.minWidth }),\n ...(def.minHeight != null && { minHeight: def.minHeight }),\n ...(def.maxWidth != null && { maxWidth: def.maxWidth }),\n ...(def.maxHeight != null && { maxHeight: def.maxHeight }),\n ...(def.vibrancy && { vibrancy: def.vibrancy }),\n ...(def.backgroundMaterial && { backgroundMaterial: def.backgroundMaterial }),\n ...(def.titleBarStyle && { titleBarStyle: def.titleBarStyle }),\n ...(def.trafficLightPosition && { trafficLightPosition: def.trafficLightPosition }),\n ...(def.titleBarOverlay != null && { titleBarOverlay: def.titleBarOverlay }),\n ...(parentWindow && { parent: parentWindow }),\n webPreferences: {\n preload: this.preloadPath,\n contextIsolation: true,\n sandbox: true,\n nodeIntegration: false,\n },\n ...extraOptions,\n };\n\n const win = new this.BrowserWindowClass(options);\n\n // Restore maximized/fullscreen state\n if (savedBounds?.isMaximized) {\n win.maximize();\n } else if (savedBounds?.isFullScreen) {\n win.setFullScreen(true);\n }\n\n // Show window after restoring state (anti-flicker)\n if (deferShow) {\n win.show();\n }\n\n // Load content\n if (this.rendererUrl) {\n await win.loadURL(this.rendererUrl);\n } else if (this.rendererDir) {\n await win.loadFile(join(this.rendererDir, 'index.html'));\n }\n\n // Open http/https links in system browser instead of inside Electron\n if (this.shell) {\n const shellRef = this.shell;\n win.webContents.setWindowOpenHandler(({ url }) => {\n if (url.startsWith('http://') || url.startsWith('https://')) {\n shellRef.openExternal(url);\n }\n return { action: 'deny' };\n });\n }\n\n // Track window\n this.windows.set(windowId, win);\n logger.debug(`Created window \"${windowId}\"`);\n\n // Persist bounds on move/resize/close\n if (useRememberBounds) {\n const captureNormalBounds = () => {\n if (!win.isMaximized() && !win.isFullScreen()) {\n const rect = win.getBounds();\n this.stateStore.updateBoundsDebounced(windowId, {\n ...rect,\n isMaximized: false,\n isFullScreen: false,\n });\n }\n };\n\n win.on('move', captureNormalBounds);\n win.on('resize', captureNormalBounds);\n\n win.on('close', () => {\n const rect = win.getBounds();\n this.stateStore.saveBoundsSync(windowId, {\n ...rect,\n isMaximized: win.isMaximized(),\n isFullScreen: win.isFullScreen(),\n });\n });\n }\n\n // Cleanup on close\n win.on('closed', () => {\n this.windows.delete(windowId);\n logger.debug(`Window \"${windowId}\" closed`);\n });\n\n return win;\n }\n\n /**\n * Get a window by ID.\n */\n getWindow(windowId: string): BrowserWindowLike | undefined {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) return win;\n return undefined;\n }\n\n /**\n * Close a window by ID.\n */\n closeWindow(windowId: string): void {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) {\n win.close();\n }\n }\n\n /**\n * Focus a window by ID.\n */\n focusWindow(windowId: string): void {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) {\n win.show();\n win.focus();\n }\n }\n\n /**\n * Send an event to all open windows.\n */\n broadcastToAll(channel: string, ...data: unknown[]): void {\n for (const [, win] of this.windows) {\n if (!win.isDestroyed()) {\n win.webContents.send(channel, ...data);\n }\n }\n }\n\n /**\n * Get all open window IDs.\n */\n getOpenWindowIds(): string[] {\n return Array.from(this.windows.entries())\n .filter(([, win]) => !win.isDestroyed())\n .map(([id]) => id);\n }\n\n /**\n * Close all windows.\n */\n closeAll(): void {\n for (const [, win] of this.windows) {\n if (!win.isDestroyed()) {\n win.destroy();\n }\n }\n this.windows.clear();\n logger.debug('All windows closed');\n }\n\n /**\n * Handle the __window IPC channel.\n */\n async handleWindowOperation(data: {\n action: string;\n windowId?: string;\n options?: Record<string, unknown>;\n }): Promise<void> {\n switch (data.action) {\n case 'open':\n if (!data.windowId) throw new Error('windowId required for open');\n await this.createWindow(data.windowId, data.options);\n break;\n case 'close':\n if (data.windowId) {\n this.closeWindow(data.windowId);\n }\n break;\n case 'focus':\n if (!data.windowId) throw new Error('windowId required for focus');\n this.focusWindow(data.windowId);\n break;\n default:\n throw new Error(`Unknown window action: ${data.action}`);\n }\n }\n\n /**\n * Handle the __contextMenu IPC channel.\n *\n * Receives `{ action: 'show', items, position }` from the renderer's\n * `use:contextMenu` action. Checks the window's contextMenu config:\n * - **native**: builds Electron Menu from items, pops it. String actions\n * are handled on main. Callback items send `__contextMenu:callback`.\n * - **custom**: forwards items + position as `__contextMenu:show` push event.\n */\n handleContextMenu(\n windowId: string,\n data: { action: string; items: ContextMenuItem[]; position: { x: number; y: number } },\n ): void {\n const win = this.windows.get(windowId);\n if (!win || win.isDestroyed()) return;\n\n const def = this.windowDefs[windowId];\n if (!def) return;\n\n if (data.action !== 'show') return;\n\n if (def.contextMenu === 'native') {\n this.showNativeContextMenu(win, data.items);\n } else if (def.contextMenu === 'custom') {\n win.webContents.send('__event:__contextMenu:show', {\n items: data.items,\n position: data.position,\n });\n }\n }\n\n /**\n * Build and show a native Electron context menu from serialized items.\n */\n private showNativeContextMenu(win: BrowserWindowLike, items: ContextMenuItem[]): void {\n if (!this.MenuClass) return;\n\n const template = items.map(item => {\n if (item.type === 'separator') {\n return { label: '', click: () => {}, type: 'separator' as const, enabled: true };\n }\n return {\n label: item.label ?? '',\n enabled: item.disabled !== true,\n click: () => {\n if (item._callbackId) {\n win.webContents.send('__event:__contextMenu:callback', item._callbackId);\n } else if (item.action) {\n this.executeMenuAction(item.action, item.data, win);\n }\n },\n };\n });\n\n if (template.length === 0) return;\n\n const menu = this.MenuClass.buildFromTemplate(template);\n menu.popup({ window: win });\n }\n\n /**\n * Execute a built-in menu action on the main process.\n */\n private executeMenuAction(action: string, data: string | undefined, win: BrowserWindowLike): void {\n switch (action) {\n case 'openExternal':\n if (data && this.shell) {\n this.shell.openExternal(data);\n }\n break;\n case 'copyText':\n if (data && this.clipboard) {\n this.clipboard.writeText(data);\n }\n break;\n case 'copy':\n win.webContents.send('__contextMenu:exec', 'copy');\n break;\n case 'cut':\n win.webContents.send('__contextMenu:exec', 'cut');\n break;\n case 'paste':\n win.webContents.send('__contextMenu:exec', 'paste');\n break;\n case 'selectAll':\n win.webContents.send('__contextMenu:exec', 'selectAll');\n break;\n default:\n // Unknown action — dispatch to renderer\n win.webContents.send('__contextMenu:action', action, data);\n break;\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -102,6 +102,9 @@ class WindowManager {
102
102
  ...def.maxHeight != null && { maxHeight: def.maxHeight },
103
103
  ...def.vibrancy && { vibrancy: def.vibrancy },
104
104
  ...def.backgroundMaterial && { backgroundMaterial: def.backgroundMaterial },
105
+ ...def.titleBarStyle && { titleBarStyle: def.titleBarStyle },
106
+ ...def.trafficLightPosition && { trafficLightPosition: def.trafficLightPosition },
107
+ ...def.titleBarOverlay != null && { titleBarOverlay: def.titleBarOverlay },
105
108
  ...parentWindow && { parent: parentWindow },
106
109
  webPreferences: {
107
110
  preload: this.preloadPath,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/window/window_manager.ts"],"sourcesContent":["import { join } from 'path';\nimport type { WindowDef } from '../types/index.js';\nimport type { ContextMenuItem } from '../types/context_menu.js';\nimport { getLogger } from '@noego/logger';\nimport { WindowStateStore } from './window_state_store.js';\nimport type { ScreenLike } from './window_state_store.js';\n\nconst logger = getLogger('wood:window');\n\n/** Subset of Electron BrowserWindow used by WindowManager (avoids compile-time electron dep). */\nexport interface BrowserWindowLike {\n loadURL(url: string): Promise<void>;\n loadFile(filePath: string): Promise<void>;\n close(): void;\n focus(): void;\n show(): void;\n destroy(): void;\n isDestroyed(): boolean;\n webContents: {\n send(channel: string, ...args: unknown[]): void;\n reload(): void;\n setWindowOpenHandler(handler: (details: { url: string }) => { action: 'deny' | 'allow' }): void;\n on(event: string, handler: (...args: any[]) => void): void;\n };\n on(event: string, handler: (...args: unknown[]) => void): void;\n getBounds(): { x: number; y: number; width: number; height: number };\n isMaximized(): boolean;\n isFullScreen(): boolean;\n maximize(): void;\n setFullScreen(flag: boolean): void;\n}\n\nexport interface BrowserWindowConstructor {\n new (options: Record<string, unknown>): BrowserWindowLike;\n getAllWindows(): BrowserWindowLike[];\n}\n\n/** Minimal shell interface for opening external URLs. */\nexport interface ShellLike {\n openExternal(url: string): Promise<unknown>;\n}\n\n/** Minimal clipboard interface for copying text. */\nexport interface ClipboardLike {\n writeText(text: string): void;\n}\n\n/** Minimal Menu interface for right-click context menus. */\nexport interface MenuLike {\n popup(options?: { window?: BrowserWindowLike }): void;\n}\n\nexport interface MenuConstructorLike {\n buildFromTemplate(template: Array<{ label: string; click: () => void; type?: 'separator'; enabled?: boolean }>): MenuLike;\n}\n\nexport class WindowManager {\n private windows = new Map<string, BrowserWindowLike>();\n private windowDefs: Record<string, WindowDef>;\n private BrowserWindowClass: BrowserWindowConstructor | null = null;\n private preloadPath: string;\n private rendererUrl?: string;\n private rendererDir?: string;\n private shell: ShellLike | null = null;\n private clipboard: ClipboardLike | null = null;\n private MenuClass: MenuConstructorLike | null = null;\n private stateStore = new WindowStateStore();\n\n constructor(options: {\n windowDefs: Record<string, WindowDef>;\n preloadPath: string;\n rendererUrl?: string;\n rendererDir?: string;\n applicationRoot?: string;\n }) {\n this.windowDefs = options.windowDefs;\n this.preloadPath = options.preloadPath;\n this.rendererUrl = options.rendererUrl;\n this.rendererDir = options.rendererDir;\n if (options.applicationRoot) {\n this.stateStore.setApplicationRoot(options.applicationRoot);\n }\n }\n\n /**\n * Set the BrowserWindow class (from electron).\n * Called at runtime so we don't need electron at compile time.\n */\n setBrowserWindow(BW: BrowserWindowConstructor): void {\n this.BrowserWindowClass = BW;\n }\n\n /**\n * Set the shell module (from electron) for opening external URLs.\n */\n setShell(shell: ShellLike): void {\n this.shell = shell;\n }\n\n /**\n * Set the clipboard module (from electron) for copying text.\n */\n setClipboard(clipboard: ClipboardLike): void {\n this.clipboard = clipboard;\n }\n\n /**\n * Set the Menu class (from electron) for context menus.\n */\n setMenu(menu: MenuConstructorLike): void {\n this.MenuClass = menu;\n }\n\n /**\n * Set the user data path for persisting window state.\n */\n setUserDataPath(path: string): void {\n this.stateStore.setUserDataPath(path);\n }\n\n /**\n * Set the application root directory for Wood JSON state persistence.\n */\n setApplicationRoot(path: string): void {\n this.stateStore.setApplicationRoot(path);\n }\n\n /**\n * Set the screen module (from electron) for display validation.\n */\n setScreen(screen: ScreenLike): void {\n this.stateStore.setScreen(screen);\n }\n\n /**\n * Create a window from its definition.\n */\n async createWindow(windowId: string, extraOptions?: Record<string, unknown>): Promise<BrowserWindowLike> {\n if (!this.BrowserWindowClass) {\n throw new Error('BrowserWindow class not set. Call setBrowserWindow() first.');\n }\n\n const def = this.windowDefs[windowId];\n if (!def) {\n throw new Error(`Window \"${windowId}\" not defined in config`);\n }\n\n // If the window already exists and isn't destroyed, just focus it\n const existing = this.windows.get(windowId);\n if (existing && !existing.isDestroyed()) {\n existing.focus();\n return existing;\n }\n\n // Resolve parent window\n let parentWindow: BrowserWindowLike | undefined;\n if (def.parent) {\n parentWindow = this.windows.get(def.parent);\n }\n\n // Resolve saved bounds for rememberBounds\n const useRememberBounds = def.rememberBounds === true;\n const savedBounds = useRememberBounds ? this.stateStore.getBounds(windowId) : null;\n\n // Determine if we should defer showing (anti-flicker for restored windows)\n const defShow = def.show ?? true;\n const deferShow = useRememberBounds && savedBounds != null && defShow && extraOptions?.show !== false;\n\n // Build BrowserWindow options from WindowDef\n const options: Record<string, unknown> = {\n title: def.title,\n width: savedBounds?.width ?? def.width,\n height: savedBounds?.height ?? def.height,\n ...(savedBounds && { x: savedBounds.x, y: savedBounds.y }),\n frame: def.frame ?? true,\n transparent: def.transparent ?? false,\n resizable: def.resizable ?? true,\n alwaysOnTop: def.alwaysOnTop ?? false,\n show: deferShow ? false : defShow,\n modal: def.modal ?? false,\n ...(def.minWidth != null && { minWidth: def.minWidth }),\n ...(def.minHeight != null && { minHeight: def.minHeight }),\n ...(def.maxWidth != null && { maxWidth: def.maxWidth }),\n ...(def.maxHeight != null && { maxHeight: def.maxHeight }),\n ...(def.vibrancy && { vibrancy: def.vibrancy }),\n ...(def.backgroundMaterial && { backgroundMaterial: def.backgroundMaterial }),\n ...(parentWindow && { parent: parentWindow }),\n webPreferences: {\n preload: this.preloadPath,\n contextIsolation: true,\n sandbox: true,\n nodeIntegration: false,\n },\n ...extraOptions,\n };\n\n const win = new this.BrowserWindowClass(options);\n\n // Restore maximized/fullscreen state\n if (savedBounds?.isMaximized) {\n win.maximize();\n } else if (savedBounds?.isFullScreen) {\n win.setFullScreen(true);\n }\n\n // Show window after restoring state (anti-flicker)\n if (deferShow) {\n win.show();\n }\n\n // Load content\n if (this.rendererUrl) {\n await win.loadURL(this.rendererUrl);\n } else if (this.rendererDir) {\n await win.loadFile(join(this.rendererDir, 'index.html'));\n }\n\n // Open http/https links in system browser instead of inside Electron\n if (this.shell) {\n const shellRef = this.shell;\n win.webContents.setWindowOpenHandler(({ url }) => {\n if (url.startsWith('http://') || url.startsWith('https://')) {\n shellRef.openExternal(url);\n }\n return { action: 'deny' };\n });\n }\n\n // Track window\n this.windows.set(windowId, win);\n logger.debug(`Created window \"${windowId}\"`);\n\n // Persist bounds on move/resize/close\n if (useRememberBounds) {\n const captureNormalBounds = () => {\n if (!win.isMaximized() && !win.isFullScreen()) {\n const rect = win.getBounds();\n this.stateStore.updateBoundsDebounced(windowId, {\n ...rect,\n isMaximized: false,\n isFullScreen: false,\n });\n }\n };\n\n win.on('move', captureNormalBounds);\n win.on('resize', captureNormalBounds);\n\n win.on('close', () => {\n const rect = win.getBounds();\n this.stateStore.saveBoundsSync(windowId, {\n ...rect,\n isMaximized: win.isMaximized(),\n isFullScreen: win.isFullScreen(),\n });\n });\n }\n\n // Cleanup on close\n win.on('closed', () => {\n this.windows.delete(windowId);\n logger.debug(`Window \"${windowId}\" closed`);\n });\n\n return win;\n }\n\n /**\n * Get a window by ID.\n */\n getWindow(windowId: string): BrowserWindowLike | undefined {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) return win;\n return undefined;\n }\n\n /**\n * Close a window by ID.\n */\n closeWindow(windowId: string): void {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) {\n win.close();\n }\n }\n\n /**\n * Focus a window by ID.\n */\n focusWindow(windowId: string): void {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) {\n win.show();\n win.focus();\n }\n }\n\n /**\n * Send an event to all open windows.\n */\n broadcastToAll(channel: string, ...data: unknown[]): void {\n for (const [, win] of this.windows) {\n if (!win.isDestroyed()) {\n win.webContents.send(channel, ...data);\n }\n }\n }\n\n /**\n * Get all open window IDs.\n */\n getOpenWindowIds(): string[] {\n return Array.from(this.windows.entries())\n .filter(([, win]) => !win.isDestroyed())\n .map(([id]) => id);\n }\n\n /**\n * Close all windows.\n */\n closeAll(): void {\n for (const [, win] of this.windows) {\n if (!win.isDestroyed()) {\n win.destroy();\n }\n }\n this.windows.clear();\n logger.debug('All windows closed');\n }\n\n /**\n * Handle the __window IPC channel.\n */\n async handleWindowOperation(data: {\n action: string;\n windowId?: string;\n options?: Record<string, unknown>;\n }): Promise<void> {\n switch (data.action) {\n case 'open':\n if (!data.windowId) throw new Error('windowId required for open');\n await this.createWindow(data.windowId, data.options);\n break;\n case 'close':\n if (data.windowId) {\n this.closeWindow(data.windowId);\n }\n break;\n case 'focus':\n if (!data.windowId) throw new Error('windowId required for focus');\n this.focusWindow(data.windowId);\n break;\n default:\n throw new Error(`Unknown window action: ${data.action}`);\n }\n }\n\n /**\n * Handle the __contextMenu IPC channel.\n *\n * Receives `{ action: 'show', items, position }` from the renderer's\n * `use:contextMenu` action. Checks the window's contextMenu config:\n * - **native**: builds Electron Menu from items, pops it. String actions\n * are handled on main. Callback items send `__contextMenu:callback`.\n * - **custom**: forwards items + position as `__contextMenu:show` push event.\n */\n handleContextMenu(\n windowId: string,\n data: { action: string; items: ContextMenuItem[]; position: { x: number; y: number } },\n ): void {\n const win = this.windows.get(windowId);\n if (!win || win.isDestroyed()) return;\n\n const def = this.windowDefs[windowId];\n if (!def) return;\n\n if (data.action !== 'show') return;\n\n if (def.contextMenu === 'native') {\n this.showNativeContextMenu(win, data.items);\n } else if (def.contextMenu === 'custom') {\n win.webContents.send('__event:__contextMenu:show', {\n items: data.items,\n position: data.position,\n });\n }\n }\n\n /**\n * Build and show a native Electron context menu from serialized items.\n */\n private showNativeContextMenu(win: BrowserWindowLike, items: ContextMenuItem[]): void {\n if (!this.MenuClass) return;\n\n const template = items.map(item => {\n if (item.type === 'separator') {\n return { label: '', click: () => {}, type: 'separator' as const, enabled: true };\n }\n return {\n label: item.label ?? '',\n enabled: item.disabled !== true,\n click: () => {\n if (item._callbackId) {\n win.webContents.send('__event:__contextMenu:callback', item._callbackId);\n } else if (item.action) {\n this.executeMenuAction(item.action, item.data, win);\n }\n },\n };\n });\n\n if (template.length === 0) return;\n\n const menu = this.MenuClass.buildFromTemplate(template);\n menu.popup({ window: win });\n }\n\n /**\n * Execute a built-in menu action on the main process.\n */\n private executeMenuAction(action: string, data: string | undefined, win: BrowserWindowLike): void {\n switch (action) {\n case 'openExternal':\n if (data && this.shell) {\n this.shell.openExternal(data);\n }\n break;\n case 'copyText':\n if (data && this.clipboard) {\n this.clipboard.writeText(data);\n }\n break;\n case 'copy':\n win.webContents.send('__contextMenu:exec', 'copy');\n break;\n case 'cut':\n win.webContents.send('__contextMenu:exec', 'cut');\n break;\n case 'paste':\n win.webContents.send('__contextMenu:exec', 'paste');\n break;\n case 'selectAll':\n win.webContents.send('__contextMenu:exec', 'selectAll');\n break;\n default:\n // Unknown action — dispatch to renderer\n win.webContents.send('__contextMenu:action', action, data);\n break;\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/window/window_manager.ts"],"sourcesContent":["import { join } from 'path';\nimport type { WindowDef } from '../types/index.js';\nimport type { ContextMenuItem } from '../types/context_menu.js';\nimport { getLogger } from '@noego/logger';\nimport { WindowStateStore } from './window_state_store.js';\nimport type { ScreenLike } from './window_state_store.js';\n\nconst logger = getLogger('wood:window');\n\n/** Subset of Electron BrowserWindow used by WindowManager (avoids compile-time electron dep). */\nexport interface BrowserWindowLike {\n loadURL(url: string): Promise<void>;\n loadFile(filePath: string): Promise<void>;\n close(): void;\n focus(): void;\n show(): void;\n destroy(): void;\n isDestroyed(): boolean;\n webContents: {\n send(channel: string, ...args: unknown[]): void;\n reload(): void;\n setWindowOpenHandler(handler: (details: { url: string }) => { action: 'deny' | 'allow' }): void;\n on(event: string, handler: (...args: any[]) => void): void;\n };\n on(event: string, handler: (...args: unknown[]) => void): void;\n getBounds(): { x: number; y: number; width: number; height: number };\n isMaximized(): boolean;\n isFullScreen(): boolean;\n maximize(): void;\n setFullScreen(flag: boolean): void;\n}\n\nexport interface BrowserWindowConstructor {\n new (options: Record<string, unknown>): BrowserWindowLike;\n getAllWindows(): BrowserWindowLike[];\n}\n\n/** Minimal shell interface for opening external URLs. */\nexport interface ShellLike {\n openExternal(url: string): Promise<unknown>;\n}\n\n/** Minimal clipboard interface for copying text. */\nexport interface ClipboardLike {\n writeText(text: string): void;\n}\n\n/** Minimal Menu interface for right-click context menus. */\nexport interface MenuLike {\n popup(options?: { window?: BrowserWindowLike }): void;\n}\n\nexport interface MenuConstructorLike {\n buildFromTemplate(template: Array<{ label: string; click: () => void; type?: 'separator'; enabled?: boolean }>): MenuLike;\n}\n\nexport class WindowManager {\n private windows = new Map<string, BrowserWindowLike>();\n private windowDefs: Record<string, WindowDef>;\n private BrowserWindowClass: BrowserWindowConstructor | null = null;\n private preloadPath: string;\n private rendererUrl?: string;\n private rendererDir?: string;\n private shell: ShellLike | null = null;\n private clipboard: ClipboardLike | null = null;\n private MenuClass: MenuConstructorLike | null = null;\n private stateStore = new WindowStateStore();\n\n constructor(options: {\n windowDefs: Record<string, WindowDef>;\n preloadPath: string;\n rendererUrl?: string;\n rendererDir?: string;\n applicationRoot?: string;\n }) {\n this.windowDefs = options.windowDefs;\n this.preloadPath = options.preloadPath;\n this.rendererUrl = options.rendererUrl;\n this.rendererDir = options.rendererDir;\n if (options.applicationRoot) {\n this.stateStore.setApplicationRoot(options.applicationRoot);\n }\n }\n\n /**\n * Set the BrowserWindow class (from electron).\n * Called at runtime so we don't need electron at compile time.\n */\n setBrowserWindow(BW: BrowserWindowConstructor): void {\n this.BrowserWindowClass = BW;\n }\n\n /**\n * Set the shell module (from electron) for opening external URLs.\n */\n setShell(shell: ShellLike): void {\n this.shell = shell;\n }\n\n /**\n * Set the clipboard module (from electron) for copying text.\n */\n setClipboard(clipboard: ClipboardLike): void {\n this.clipboard = clipboard;\n }\n\n /**\n * Set the Menu class (from electron) for context menus.\n */\n setMenu(menu: MenuConstructorLike): void {\n this.MenuClass = menu;\n }\n\n /**\n * Set the user data path for persisting window state.\n */\n setUserDataPath(path: string): void {\n this.stateStore.setUserDataPath(path);\n }\n\n /**\n * Set the application root directory for Wood JSON state persistence.\n */\n setApplicationRoot(path: string): void {\n this.stateStore.setApplicationRoot(path);\n }\n\n /**\n * Set the screen module (from electron) for display validation.\n */\n setScreen(screen: ScreenLike): void {\n this.stateStore.setScreen(screen);\n }\n\n /**\n * Create a window from its definition.\n */\n async createWindow(windowId: string, extraOptions?: Record<string, unknown>): Promise<BrowserWindowLike> {\n if (!this.BrowserWindowClass) {\n throw new Error('BrowserWindow class not set. Call setBrowserWindow() first.');\n }\n\n const def = this.windowDefs[windowId];\n if (!def) {\n throw new Error(`Window \"${windowId}\" not defined in config`);\n }\n\n // If the window already exists and isn't destroyed, just focus it\n const existing = this.windows.get(windowId);\n if (existing && !existing.isDestroyed()) {\n existing.focus();\n return existing;\n }\n\n // Resolve parent window\n let parentWindow: BrowserWindowLike | undefined;\n if (def.parent) {\n parentWindow = this.windows.get(def.parent);\n }\n\n // Resolve saved bounds for rememberBounds\n const useRememberBounds = def.rememberBounds === true;\n const savedBounds = useRememberBounds ? this.stateStore.getBounds(windowId) : null;\n\n // Determine if we should defer showing (anti-flicker for restored windows)\n const defShow = def.show ?? true;\n const deferShow = useRememberBounds && savedBounds != null && defShow && extraOptions?.show !== false;\n\n // Build BrowserWindow options from WindowDef\n const options: Record<string, unknown> = {\n title: def.title,\n width: savedBounds?.width ?? def.width,\n height: savedBounds?.height ?? def.height,\n ...(savedBounds && { x: savedBounds.x, y: savedBounds.y }),\n frame: def.frame ?? true,\n transparent: def.transparent ?? false,\n resizable: def.resizable ?? true,\n alwaysOnTop: def.alwaysOnTop ?? false,\n show: deferShow ? false : defShow,\n modal: def.modal ?? false,\n ...(def.minWidth != null && { minWidth: def.minWidth }),\n ...(def.minHeight != null && { minHeight: def.minHeight }),\n ...(def.maxWidth != null && { maxWidth: def.maxWidth }),\n ...(def.maxHeight != null && { maxHeight: def.maxHeight }),\n ...(def.vibrancy && { vibrancy: def.vibrancy }),\n ...(def.backgroundMaterial && { backgroundMaterial: def.backgroundMaterial }),\n ...(def.titleBarStyle && { titleBarStyle: def.titleBarStyle }),\n ...(def.trafficLightPosition && { trafficLightPosition: def.trafficLightPosition }),\n ...(def.titleBarOverlay != null && { titleBarOverlay: def.titleBarOverlay }),\n ...(parentWindow && { parent: parentWindow }),\n webPreferences: {\n preload: this.preloadPath,\n contextIsolation: true,\n sandbox: true,\n nodeIntegration: false,\n },\n ...extraOptions,\n };\n\n const win = new this.BrowserWindowClass(options);\n\n // Restore maximized/fullscreen state\n if (savedBounds?.isMaximized) {\n win.maximize();\n } else if (savedBounds?.isFullScreen) {\n win.setFullScreen(true);\n }\n\n // Show window after restoring state (anti-flicker)\n if (deferShow) {\n win.show();\n }\n\n // Load content\n if (this.rendererUrl) {\n await win.loadURL(this.rendererUrl);\n } else if (this.rendererDir) {\n await win.loadFile(join(this.rendererDir, 'index.html'));\n }\n\n // Open http/https links in system browser instead of inside Electron\n if (this.shell) {\n const shellRef = this.shell;\n win.webContents.setWindowOpenHandler(({ url }) => {\n if (url.startsWith('http://') || url.startsWith('https://')) {\n shellRef.openExternal(url);\n }\n return { action: 'deny' };\n });\n }\n\n // Track window\n this.windows.set(windowId, win);\n logger.debug(`Created window \"${windowId}\"`);\n\n // Persist bounds on move/resize/close\n if (useRememberBounds) {\n const captureNormalBounds = () => {\n if (!win.isMaximized() && !win.isFullScreen()) {\n const rect = win.getBounds();\n this.stateStore.updateBoundsDebounced(windowId, {\n ...rect,\n isMaximized: false,\n isFullScreen: false,\n });\n }\n };\n\n win.on('move', captureNormalBounds);\n win.on('resize', captureNormalBounds);\n\n win.on('close', () => {\n const rect = win.getBounds();\n this.stateStore.saveBoundsSync(windowId, {\n ...rect,\n isMaximized: win.isMaximized(),\n isFullScreen: win.isFullScreen(),\n });\n });\n }\n\n // Cleanup on close\n win.on('closed', () => {\n this.windows.delete(windowId);\n logger.debug(`Window \"${windowId}\" closed`);\n });\n\n return win;\n }\n\n /**\n * Get a window by ID.\n */\n getWindow(windowId: string): BrowserWindowLike | undefined {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) return win;\n return undefined;\n }\n\n /**\n * Close a window by ID.\n */\n closeWindow(windowId: string): void {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) {\n win.close();\n }\n }\n\n /**\n * Focus a window by ID.\n */\n focusWindow(windowId: string): void {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) {\n win.show();\n win.focus();\n }\n }\n\n /**\n * Send an event to all open windows.\n */\n broadcastToAll(channel: string, ...data: unknown[]): void {\n for (const [, win] of this.windows) {\n if (!win.isDestroyed()) {\n win.webContents.send(channel, ...data);\n }\n }\n }\n\n /**\n * Get all open window IDs.\n */\n getOpenWindowIds(): string[] {\n return Array.from(this.windows.entries())\n .filter(([, win]) => !win.isDestroyed())\n .map(([id]) => id);\n }\n\n /**\n * Close all windows.\n */\n closeAll(): void {\n for (const [, win] of this.windows) {\n if (!win.isDestroyed()) {\n win.destroy();\n }\n }\n this.windows.clear();\n logger.debug('All windows closed');\n }\n\n /**\n * Handle the __window IPC channel.\n */\n async handleWindowOperation(data: {\n action: string;\n windowId?: string;\n options?: Record<string, unknown>;\n }): Promise<void> {\n switch (data.action) {\n case 'open':\n if (!data.windowId) throw new Error('windowId required for open');\n await this.createWindow(data.windowId, data.options);\n break;\n case 'close':\n if (data.windowId) {\n this.closeWindow(data.windowId);\n }\n break;\n case 'focus':\n if (!data.windowId) throw new Error('windowId required for focus');\n this.focusWindow(data.windowId);\n break;\n default:\n throw new Error(`Unknown window action: ${data.action}`);\n }\n }\n\n /**\n * Handle the __contextMenu IPC channel.\n *\n * Receives `{ action: 'show', items, position }` from the renderer's\n * `use:contextMenu` action. Checks the window's contextMenu config:\n * - **native**: builds Electron Menu from items, pops it. String actions\n * are handled on main. Callback items send `__contextMenu:callback`.\n * - **custom**: forwards items + position as `__contextMenu:show` push event.\n */\n handleContextMenu(\n windowId: string,\n data: { action: string; items: ContextMenuItem[]; position: { x: number; y: number } },\n ): void {\n const win = this.windows.get(windowId);\n if (!win || win.isDestroyed()) return;\n\n const def = this.windowDefs[windowId];\n if (!def) return;\n\n if (data.action !== 'show') return;\n\n if (def.contextMenu === 'native') {\n this.showNativeContextMenu(win, data.items);\n } else if (def.contextMenu === 'custom') {\n win.webContents.send('__event:__contextMenu:show', {\n items: data.items,\n position: data.position,\n });\n }\n }\n\n /**\n * Build and show a native Electron context menu from serialized items.\n */\n private showNativeContextMenu(win: BrowserWindowLike, items: ContextMenuItem[]): void {\n if (!this.MenuClass) return;\n\n const template = items.map(item => {\n if (item.type === 'separator') {\n return { label: '', click: () => {}, type: 'separator' as const, enabled: true };\n }\n return {\n label: item.label ?? '',\n enabled: item.disabled !== true,\n click: () => {\n if (item._callbackId) {\n win.webContents.send('__event:__contextMenu:callback', item._callbackId);\n } else if (item.action) {\n this.executeMenuAction(item.action, item.data, win);\n }\n },\n };\n });\n\n if (template.length === 0) return;\n\n const menu = this.MenuClass.buildFromTemplate(template);\n menu.popup({ window: win });\n }\n\n /**\n * Execute a built-in menu action on the main process.\n */\n private executeMenuAction(action: string, data: string | undefined, win: BrowserWindowLike): void {\n switch (action) {\n case 'openExternal':\n if (data && this.shell) {\n this.shell.openExternal(data);\n }\n break;\n case 'copyText':\n if (data && this.clipboard) {\n this.clipboard.writeText(data);\n }\n break;\n case 'copy':\n win.webContents.send('__contextMenu:exec', 'copy');\n break;\n case 'cut':\n win.webContents.send('__contextMenu:exec', 'cut');\n break;\n case 'paste':\n win.webContents.send('__contextMenu:exec', 'paste');\n break;\n case 'selectAll':\n win.webContents.send('__contextMenu:exec', 'selectAll');\n break;\n default:\n // Unknown action — dispatch to renderer\n win.webContents.send('__contextMenu:action', action, data);\n break;\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -257,11 +257,149 @@ await mounted.expect.hidden('.menu');
257
257
  await mounted.expect.insideViewport('.composer', { margin: 8 });
258
258
  await mounted.expect.notClipped('.popover');
259
259
  await mounted.expect.notOverlapping('.toast', '.composer');
260
- await mounted.expect.zIndexAbove('.modal', '.backdrop');
261
260
  await mounted.expect.stableAfterAnimation('.drawer');
262
261
  await mounted.expect.screenshot('settings-drawer');
263
262
  ```
264
263
 
264
+ ### Geometry assertion kit
265
+
266
+ Honest alignment, occlusion, and front/behind primitives. They read real boxes
267
+ (`getBoundingClientRect`), query Chromium's real paint tree (CDP
268
+ `DOMSnapshot.captureSnapshot`), and hit-test real painted pixels
269
+ (`document.elementFromPoint`) rather than trusting computed `z-index`.
270
+
271
+ **Prefer the whole-element mechanisms.** They answer "which layer is on top" and
272
+ "is any foreign layer painted over this element" for the entire element, with no
273
+ point to pick. The point-sampling forms are the special case for point-specific
274
+ questions ("does a click at THIS spot land here").
275
+
276
+ The whole-element visibility/stacking hierarchy is:
277
+
278
+ - `inFrontOf` — paint-order front/behind between two named elements.
279
+ - `notCovered` — paint-order coverage: is ANY foreign element painted over the
280
+ target's rect. This is the visibility check (replaces the removed
281
+ `fullyVisible`).
282
+ - `unoccluded` — click-target point check honouring `pointer-events`.
283
+
284
+ ```ts
285
+ // Batch rect reader (one snapshot per selector, in order).
286
+ const [head, value] = await mounted.expect.rects(['.col-head', '.col-value']);
287
+
288
+ // Alignment: shared edge / shared center axis within a tolerance (default 1px).
289
+ await mounted.expect.sharedEdge('.col-head', '.col-value', 'right', { tolerance: 1 });
290
+ await mounted.expect.sharedAxis('.col-head', '.col-value', 'centerX', { tolerance: 1 });
291
+
292
+ // Spacing: edge-to-edge gap between two elements along an axis. Order is derived
293
+ // from the rects (leftmost/topmost is the earlier box), not the argument order.
294
+ // At least one of equals/min/max is required; all are within `tolerance` (1px).
295
+ await mounted.expect.gapBetween('.chip-a', '.chip-b', 'horizontal', { equals: 8 });
296
+ await mounted.expect.gapBetween('.row-a', '.row-b', 'vertical', { min: 4, max: 12 });
297
+
298
+ // Spacing: 3+ elements evenly spaced along an axis (sorted by position, adjacent
299
+ // gaps compared). Passes when max gap - min gap <= tolerance.
300
+ await mounted.expect.evenlySpaced(['.tick-1', '.tick-2', '.tick-3'], 'horizontal', { tolerance: 1 });
301
+
302
+ // Front/behind (DEFAULT: whole-element paint order via CDP DOMSnapshot).
303
+ // Passes when `.modal` has a strictly greater global paint-order index than
304
+ // `.backdrop` — painted later == on top.
305
+ await mounted.expect.inFrontOf('.modal', '.backdrop');
306
+
307
+ // Whole-element visibility (DEFAULT): no foreign layer paints over the target.
308
+ // Paint-order coverage via CDP DOMSnapshot — inspectable, not a black box.
309
+ await mounted.expect.notCovered('.primary-button');
310
+
311
+ // --- Point-specific special cases ---
312
+
313
+ // "Does a click at this spot land on the target?" (honours pointer-events).
314
+ await mounted.expect.unoccluded('.primary-button');
315
+
316
+ // Front/behind AT ONE PIXEL: hit-test an explicit point (or the intersection
317
+ // center when omitted). Use only when the question is point-specific.
318
+ await mounted.expect.inFrontOf('.tooltip', '.row', { at: { x: 120, y: 48 } });
319
+
320
+ // Low-level hit test: describes the top painted element at a point.
321
+ const hit = await mounted.expect.hitTest('.primary-button', 'center');
322
+ // hit.isTarget / hit.isDescendantOfTarget / hit.description
323
+ ```
324
+
325
+ `gapBetween` and `evenlySpaced` (spacing): `gapBetween(a, b, axis, opts)` measures
326
+ the edge-to-edge distance between two elements along the axis. The order is read
327
+ from the rects, not the argument order — the box with the smaller leading edge
328
+ (left for `'horizontal'`, top for `'vertical'`) is the earlier box and the gap is
329
+ `later.leading - earlier.trailing`. At least one of `equals` / `min` / `max` is
330
+ required. A negative gap means the border boxes OVERLAP along the axis; that fails
331
+ with a clear overlap message UNLESS the constraint explicitly allows negatives (a
332
+ negative `equals` or `min`), so you can assert an intentional overlap.
333
+ `evenlySpaced(selectors, axis, opts)` needs 3+ elements, sorts them by position,
334
+ computes adjacent gaps, and passes when `max gap - min gap <= tolerance`; its
335
+ failure message lists every gap and names the widest and narrowest (offending)
336
+ pairs.
337
+
338
+ > **Border-box caveat.** Both spacing matchers read border boxes
339
+ > (`getBoundingClientRect`), so the gap is the distance between the elements'
340
+ > outer borders and EXCLUDES any padding inside them. A test that cares about
341
+ > visual whitespace between inner content should target the inner content
342
+ > elements, not their padded wrappers.
343
+
344
+ `inFrontOf` (default) and `zIndexAbove` (deprecated): `zIndexAbove` compares
345
+ numeric computed `z-index` only and therefore lies whenever stacking contexts,
346
+ DOM paint order (auto/equal z-index), or transform/opacity contexts decide the
347
+ real order. `inFrontOf` reads the browser's actual global paint order, so it
348
+ agrees with the painted pixel. Higher paint-order index == painted later == on
349
+ top (verified against the stacking-context trap, where a `z-index:9999`
350
+ descendant of a `z-index:1` parent still paints BEHIND a root-context sibling at
351
+ `z-index:2`). Multiple paint fragments collapse to the element's max index. Paint
352
+ order is Chromium-only (needs a CDP session); pass `{ at }` to use the
353
+ point hit-test form on other browsers. Non-overlapping elements still get a valid
354
+ verdict — the failure message notes there is no shared pixel where front/behind is
355
+ visually observable.
356
+
357
+ `notCovered` vs `unoccluded`:
358
+
359
+ - `notCovered` is the **painted-coverage / visibility** check. From the real
360
+ paint tree (CDP `DOMSnapshot.captureSnapshot` with paint order + computed
361
+ styles + DOM rects) it finds every element whose rect overlaps the target,
362
+ excludes the target's own ancestors/descendants, and excludes candidates that
363
+ paint nothing visible in the overlap per an explicit, documented
364
+ transparent-wrapper heuristic (`opacity:0`/`visibility:hidden`, or no
365
+ background-color alpha AND `background-image:none` AND all border widths `0px`
366
+ AND no own text/replaced content — `box-shadow`/`outline` are NOT counted as
367
+ coverage). It then fails if any remaining foreign element has a higher paint
368
+ order than the target, naming that element, its rect, and both paint indices.
369
+ - `unoccluded` is the **click-target** check. It hit-tests one pixel and honours
370
+ `pointer-events` — a `pointer-events: none` overlay painted on top does NOT
371
+ fail it, because it does not intercept the click. It is not a general
372
+ visibility check.
373
+
374
+ Because `notCovered` tracks PAINTED coverage, a visually-opaque
375
+ `pointer-events: none` overlay DOES fail `notCovered` while `unoccluded` passes —
376
+ the honest, complementary difference between "covered by a painted layer" and
377
+ "reachable by a click". The transparent-wrapper heuristic is the key asymmetry:
378
+ a bare positioning wrapper spanning the target passes `notCovered` (it paints
379
+ nothing), while an opaque overlay fails.
380
+
381
+ > **Why not IntersectionObserver v2?** `notCovered` replaced an earlier
382
+ > `fullyVisible` matcher built on IOv2 (`{ trackVisibility: true, delay: 100 }`,
383
+ > read via `entry.isVisible`). IOv2's `isVisible` is a black box: Chromium
384
+ > reports `false` not only for distorting ancestor effects (`opacity < 1`,
385
+ > `filter`, `transform`, `mix-blend-mode`) but also for its OCCLUSION pass — ANY
386
+ > element painted later than the target within its rect disqualifies visibility,
387
+ > **even a fully transparent one**, and IOv2 never checks whether the covering
388
+ > pixels are actually opaque. This was pinned empirically against the demo app's
389
+ > global stylesheet: the culprit is a single `#app::after { content:''; inset:0;
390
+ > z-index:9999 }` frame-overlay pseudo-element. Reduced to just
391
+ > `content:''; position:absolute; inset:0` (no background, no shadow, no
392
+ > z-index) it still forced `isVisible:false` for every element beneath it;
393
+ > removing `content` (so no pseudo box is generated) restored `true`. A matcher
394
+ > whose "not visible" verdict can mean either real occlusion or an invisible
395
+ > overlay is untrustworthy — so it was replaced by `notCovered`, whose coverage
396
+ > rule is fully inspectable and exempts transparent layers by construction.
397
+
398
+ > `elementFromPoint` (used by `unoccluded` and `inFrontOf { at }`) honours
399
+ > `pointer-events`. An element with `pointer-events: none` painted on top is
400
+ > transparent to those point checks — usually the intended contract for a
401
+ > click-target assertion.
402
+
265
403
  Screenshots are written to `test-results/wood-browser`.
266
404
 
267
405
  ## Lifecycle
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noego/wood",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -49,6 +49,24 @@
49
49
  "import": "./dist/trace-testing/index.js",
50
50
  "default": "./dist/trace-testing/index.js"
51
51
  },
52
+ "./transport": {
53
+ "types": "./dist/transport/index.d.ts",
54
+ "require": "./dist/transport/index.cjs",
55
+ "import": "./dist/transport/index.js",
56
+ "default": "./dist/transport/index.js"
57
+ },
58
+ "./trace-transport": {
59
+ "types": "./dist/trace-transport/index.d.ts",
60
+ "require": "./dist/trace-transport/index.cjs",
61
+ "import": "./dist/trace-transport/index.js",
62
+ "default": "./dist/trace-transport/index.js"
63
+ },
64
+ "./runtime": {
65
+ "types": "./dist/runtime/index.d.ts",
66
+ "require": "./dist/runtime/index.cjs",
67
+ "import": "./dist/runtime/index.js",
68
+ "default": "./dist/runtime/index.js"
69
+ },
52
70
  "./client": {
53
71
  "types": "./dist/client/index.d.ts",
54
72
  "require": "./dist/client/index.cjs",
@@ -100,9 +118,10 @@
100
118
  },
101
119
  "dependencies": {
102
120
  "@noego/ioc": "^0.3.1",
103
- "@noego/logger": "^0.0.2",
121
+ "@noego/logger": "^0.0.4",
104
122
  "@noego/stitch": "^1.0.3",
105
- "@noego/trace": "^0.0.2",
123
+ "@noego/telemetry": "^0.0.1",
124
+ "@noego/trace": "^0.0.6",
106
125
  "ajv": "^8.x",
107
126
  "ajv-formats": "^3.x",
108
127
  "chokidar": "^4.x",