@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.
- package/bin/wood.js +9 -1
- package/dist/client/app_env.cjs +39 -0
- package/dist/client/app_env.cjs.map +1 -0
- package/dist/client/app_env.d.cts +7 -0
- package/dist/client/app_env.d.ts +7 -0
- package/dist/client/app_env.js +14 -0
- package/dist/client/app_env.js.map +1 -0
- package/dist/client/index.cjs +33 -9
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +5 -1
- package/dist/client/index.d.ts +5 -1
- package/dist/client/index.js +32 -9
- package/dist/client/index.js.map +1 -1
- package/dist/codegen/channel_map_generator.cjs +102 -0
- package/dist/codegen/channel_map_generator.cjs.map +1 -0
- package/dist/codegen/channel_map_generator.d.cts +6 -0
- package/dist/codegen/channel_map_generator.d.ts +6 -0
- package/dist/codegen/channel_map_generator.js +78 -0
- package/dist/codegen/channel_map_generator.js.map +1 -0
- package/dist/codegen/controller_registration_generator.cjs +6 -1
- package/dist/codegen/controller_registration_generator.cjs.map +1 -1
- package/dist/codegen/controller_registration_generator.js +6 -1
- package/dist/codegen/controller_registration_generator.js.map +1 -1
- package/dist/codegen/generated_file_notice.cjs +40 -0
- package/dist/codegen/generated_file_notice.cjs.map +1 -0
- package/dist/codegen/generated_file_notice.d.cts +3 -0
- package/dist/codegen/generated_file_notice.d.ts +3 -0
- package/dist/codegen/generated_file_notice.js +16 -0
- package/dist/codegen/generated_file_notice.js.map +1 -0
- package/dist/codegen/index.cjs +3 -0
- package/dist/codegen/index.cjs.map +1 -1
- package/dist/codegen/index.d.cts +1 -0
- package/dist/codegen/index.d.ts +1 -0
- package/dist/codegen/index.js +2 -0
- package/dist/codegen/index.js.map +1 -1
- package/dist/codegen/preload_generator.cjs +97 -1
- package/dist/codegen/preload_generator.cjs.map +1 -1
- package/dist/codegen/preload_generator.js +97 -1
- package/dist/codegen/preload_generator.js.map +1 -1
- package/dist/codegen/renderer_app_generator.cjs +6 -1
- package/dist/codegen/renderer_app_generator.cjs.map +1 -1
- package/dist/codegen/renderer_app_generator.js +6 -1
- package/dist/codegen/renderer_app_generator.js.map +1 -1
- package/dist/codegen/renderer_controller_registration_generator.cjs +6 -1
- package/dist/codegen/renderer_controller_registration_generator.cjs.map +1 -1
- package/dist/codegen/renderer_controller_registration_generator.js +6 -1
- package/dist/codegen/renderer_controller_registration_generator.js.map +1 -1
- package/dist/codegen/renderer_view_manifest_generator.cjs +6 -1
- package/dist/codegen/renderer_view_manifest_generator.cjs.map +1 -1
- package/dist/codegen/renderer_view_manifest_generator.js +6 -1
- package/dist/codegen/renderer_view_manifest_generator.js.map +1 -1
- package/dist/codegen/runtime_generator.cjs +6 -1
- package/dist/codegen/runtime_generator.cjs.map +1 -1
- package/dist/codegen/runtime_generator.js +6 -1
- package/dist/codegen/runtime_generator.js.map +1 -1
- package/dist/codegen/test_mock_generator.cjs +5 -1
- package/dist/codegen/test_mock_generator.cjs.map +1 -1
- package/dist/codegen/test_mock_generator.js +5 -1
- package/dist/codegen/test_mock_generator.js.map +1 -1
- package/dist/codegen/type_generator.cjs +7 -1
- package/dist/codegen/type_generator.cjs.map +1 -1
- package/dist/codegen/type_generator.js +7 -1
- package/dist/codegen/type_generator.js.map +1 -1
- package/dist/index.cjs +19 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/main/index.cjs +287 -62
- package/dist/main/index.cjs.map +1 -1
- package/dist/main/index.d.cts +57 -3
- package/dist/main/index.d.ts +57 -3
- package/dist/main/index.js +289 -62
- package/dist/main/index.js.map +1 -1
- package/dist/main/renderer_trace_drain_coordinator.cjs +127 -0
- package/dist/main/renderer_trace_drain_coordinator.cjs.map +1 -0
- package/dist/main/renderer_trace_drain_coordinator.d.cts +46 -0
- package/dist/main/renderer_trace_drain_coordinator.d.ts +46 -0
- package/dist/main/renderer_trace_drain_coordinator.js +101 -0
- package/dist/main/renderer_trace_drain_coordinator.js.map +1 -0
- package/dist/router/wood_router.cjs +9 -1
- package/dist/router/wood_router.cjs.map +1 -1
- package/dist/router/wood_router.d.cts +1 -1
- package/dist/router/wood_router.d.ts +1 -1
- package/dist/router/wood_router.js +9 -1
- package/dist/router/wood_router.js.map +1 -1
- package/dist/runtime/app_env.cjs +52 -0
- package/dist/runtime/app_env.cjs.map +1 -0
- package/dist/runtime/app_env.d.cts +10 -0
- package/dist/runtime/app_env.d.ts +10 -0
- package/dist/runtime/app_env.js +18 -0
- package/dist/runtime/app_env.js.map +1 -0
- package/dist/runtime/index.cjs +9 -2
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +5 -1
- package/dist/runtime/index.d.ts +5 -1
- package/dist/runtime/index.js +9 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/renderer_log_dispatcher.cjs.map +1 -1
- package/dist/runtime/renderer_log_dispatcher.d.cts +1 -1
- package/dist/runtime/renderer_log_dispatcher.d.ts +1 -1
- package/dist/runtime/renderer_log_dispatcher.js.map +1 -1
- package/dist/runtime/renderer_trace_batch_receiver_registry.cjs +50 -0
- package/dist/runtime/renderer_trace_batch_receiver_registry.cjs.map +1 -0
- package/dist/runtime/renderer_trace_batch_receiver_registry.d.cts +10 -0
- package/dist/runtime/renderer_trace_batch_receiver_registry.d.ts +10 -0
- package/dist/runtime/renderer_trace_batch_receiver_registry.js +24 -0
- package/dist/runtime/renderer_trace_batch_receiver_registry.js.map +1 -0
- package/dist/runtime/runtime.cjs +75 -57
- package/dist/runtime/runtime.cjs.map +1 -1
- package/dist/runtime/runtime.d.cts +8 -1
- package/dist/runtime/runtime.d.ts +8 -1
- package/dist/runtime/runtime.js +80 -58
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/testing/browser.cjs +540 -11
- package/dist/testing/browser.cjs.map +1 -1
- package/dist/testing/browser.d.cts +219 -3
- package/dist/testing/browser.d.ts +219 -3
- package/dist/testing/browser.js +540 -11
- package/dist/testing/browser.js.map +1 -1
- package/dist/testing/create_controller_harness.d.cts +1 -1
- package/dist/testing/create_controller_harness.d.ts +1 -1
- package/dist/testing/index.cjs +13 -0
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +7 -2
- package/dist/testing/index.d.ts +7 -2
- package/dist/testing/index.js +14 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/trace-testing/index.d.cts +1 -1
- package/dist/trace-testing/index.d.ts +1 -1
- package/dist/trace-testing/trace_recorder.d.cts +1 -1
- package/dist/trace-testing/trace_recorder.d.ts +1 -1
- package/dist/trace-transport/index.cjs +32 -0
- package/dist/trace-transport/index.cjs.map +1 -0
- package/dist/trace-transport/index.d.cts +6 -0
- package/dist/trace-transport/index.d.ts +6 -0
- package/dist/trace-transport/index.js +7 -0
- package/dist/trace-transport/index.js.map +1 -0
- package/dist/trace_hub-Cob54qRx.d.cts +56 -0
- package/dist/trace_hub-Cob54qRx.d.ts +56 -0
- package/dist/tracing/index.cjs +14 -0
- package/dist/tracing/index.cjs.map +1 -1
- package/dist/tracing/index.d.cts +7 -1
- package/dist/tracing/index.d.ts +7 -1
- package/dist/tracing/index.js +12 -0
- package/dist/tracing/index.js.map +1 -1
- package/dist/tracing/renderer_trace_batch_host.cjs +80 -0
- package/dist/tracing/renderer_trace_batch_host.cjs.map +1 -0
- package/dist/tracing/renderer_trace_batch_host.d.cts +16 -0
- package/dist/tracing/renderer_trace_batch_host.d.ts +16 -0
- package/dist/tracing/renderer_trace_batch_host.js +56 -0
- package/dist/tracing/renderer_trace_batch_host.js.map +1 -0
- package/dist/tracing/renderer_trace_batch_transport.cjs +313 -0
- package/dist/tracing/renderer_trace_batch_transport.cjs.map +1 -0
- package/dist/tracing/renderer_trace_batch_transport.d.cts +53 -0
- package/dist/tracing/renderer_trace_batch_transport.d.ts +53 -0
- package/dist/tracing/renderer_trace_batch_transport.js +292 -0
- package/dist/tracing/renderer_trace_batch_transport.js.map +1 -0
- package/dist/tracing/trace_commit_writer.cjs +159 -0
- package/dist/tracing/trace_commit_writer.cjs.map +1 -0
- package/dist/tracing/trace_commit_writer.d.cts +97 -0
- package/dist/tracing/trace_commit_writer.d.ts +97 -0
- package/dist/tracing/trace_commit_writer.js +135 -0
- package/dist/tracing/trace_commit_writer.js.map +1 -0
- package/dist/tracing/trace_hub.cjs +58 -3
- package/dist/tracing/trace_hub.cjs.map +1 -1
- package/dist/tracing/trace_hub.d.cts +2 -46
- package/dist/tracing/trace_hub.d.ts +2 -46
- package/dist/tracing/trace_hub.js +56 -3
- package/dist/tracing/trace_hub.js.map +1 -1
- package/dist/tracing/trace_provider.cjs +42 -12
- package/dist/tracing/trace_provider.cjs.map +1 -1
- package/dist/tracing/trace_provider.d.cts +15 -1
- package/dist/tracing/trace_provider.d.ts +15 -1
- package/dist/tracing/trace_provider.js +43 -12
- package/dist/tracing/trace_provider.js.map +1 -1
- package/dist/tracing/types/wood_trace_batch.types.cjs +17 -0
- package/dist/tracing/types/wood_trace_batch.types.cjs.map +1 -0
- package/dist/tracing/types/wood_trace_batch.types.d.cts +124 -0
- package/dist/tracing/types/wood_trace_batch.types.d.ts +124 -0
- package/dist/tracing/types/wood_trace_batch.types.js +1 -0
- package/dist/tracing/types/wood_trace_batch.types.js.map +1 -0
- package/dist/tracing/types/wood_trace_sink.types.cjs +17 -0
- package/dist/tracing/types/wood_trace_sink.types.cjs.map +1 -0
- package/dist/tracing/types/wood_trace_sink.types.d.cts +2 -0
- package/dist/tracing/types/wood_trace_sink.types.d.ts +2 -0
- package/dist/tracing/types/wood_trace_sink.types.js +1 -0
- package/dist/tracing/types/wood_trace_sink.types.js.map +1 -0
- package/dist/tracing/wood_trace_batch_contract.cjs +229 -0
- package/dist/tracing/wood_trace_batch_contract.cjs.map +1 -0
- package/dist/tracing/wood_trace_batch_contract.d.cts +23 -0
- package/dist/tracing/wood_trace_batch_contract.d.ts +23 -0
- package/dist/tracing/wood_trace_batch_contract.js +205 -0
- package/dist/tracing/wood_trace_batch_contract.js.map +1 -0
- package/dist/transport/build_transport_bridge.cjs +62 -0
- package/dist/transport/build_transport_bridge.cjs.map +1 -0
- package/dist/transport/build_transport_bridge.d.cts +29 -0
- package/dist/transport/build_transport_bridge.d.ts +29 -0
- package/dist/transport/build_transport_bridge.js +38 -0
- package/dist/transport/build_transport_bridge.js.map +1 -0
- package/dist/transport/in_process_router_transport.cjs +41 -0
- package/dist/transport/in_process_router_transport.cjs.map +1 -0
- package/dist/transport/in_process_router_transport.d.cts +31 -0
- package/dist/transport/in_process_router_transport.d.ts +31 -0
- package/dist/transport/in_process_router_transport.js +17 -0
- package/dist/transport/in_process_router_transport.js.map +1 -0
- package/dist/transport/index.cjs +42 -0
- package/dist/transport/index.cjs.map +1 -0
- package/dist/transport/index.d.cts +8 -0
- package/dist/transport/index.d.ts +8 -0
- package/dist/transport/index.js +19 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/loopback_remote_transport.cjs +270 -0
- package/dist/transport/loopback_remote_transport.cjs.map +1 -0
- package/dist/transport/loopback_remote_transport.d.cts +49 -0
- package/dist/transport/loopback_remote_transport.d.ts +49 -0
- package/dist/transport/loopback_remote_transport.js +237 -0
- package/dist/transport/loopback_remote_transport.js.map +1 -0
- package/dist/transport/loopback_serialization.cjs +116 -0
- package/dist/transport/loopback_serialization.cjs.map +1 -0
- package/dist/transport/loopback_serialization.d.cts +28 -0
- package/dist/transport/loopback_serialization.d.ts +28 -0
- package/dist/transport/loopback_serialization.js +91 -0
- package/dist/transport/loopback_serialization.js.map +1 -0
- package/dist/transport/transport_contract_suite.cjs +110 -0
- package/dist/transport/transport_contract_suite.cjs.map +1 -0
- package/dist/transport/transport_contract_suite.d.cts +23 -0
- package/dist/transport/transport_contract_suite.d.ts +23 -0
- package/dist/transport/transport_contract_suite.js +85 -0
- package/dist/transport/transport_contract_suite.js.map +1 -0
- package/dist/transport/wood_rpc_transport.cjs +17 -0
- package/dist/transport/wood_rpc_transport.cjs.map +1 -0
- package/dist/transport/wood_rpc_transport.d.cts +44 -0
- package/dist/transport/wood_rpc_transport.d.ts +44 -0
- package/dist/transport/wood_rpc_transport.js +1 -0
- package/dist/transport/wood_rpc_transport.js.map +1 -0
- package/dist/types/config.cjs.map +1 -1
- package/dist/types/config.d.cts +12 -0
- package/dist/types/config.d.ts +12 -0
- package/dist/window/window_manager.cjs +3 -0
- package/dist/window/window_manager.cjs.map +1 -1
- package/dist/window/window_manager.js +3 -0
- package/dist/window/window_manager.js.map +1 -1
- package/docs/browser-testing.md +139 -1
- package/package.json +22 -3
- package/src/components/NavigationShell.browser.test.ts +236 -0
- 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":[]}
|
package/dist/types/config.d.cts
CHANGED
|
@@ -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 {
|
package/dist/types/config.d.ts
CHANGED
|
@@ -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":"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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.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":[]}
|
package/docs/browser-testing.md
CHANGED
|
@@ -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.
|
|
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.
|
|
121
|
+
"@noego/logger": "^0.0.4",
|
|
104
122
|
"@noego/stitch": "^1.0.3",
|
|
105
|
-
"@noego/
|
|
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",
|