@dimina-kit/electron-deck 0.1.0-dev.20260702175315 → 0.1.0-dev.20260703051110

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 (63) hide show
  1. package/README.md +45 -15
  2. package/dist/client/index.d.ts +8 -2
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +1 -0
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/dock-react/dock-view.d.ts +43 -19
  7. package/dist/dock-react/dock-view.d.ts.map +1 -1
  8. package/dist/dock-react/drag-redock.d.ts +18 -0
  9. package/dist/dock-react/drag-redock.d.ts.map +1 -1
  10. package/dist/dock-react/group-view.d.ts +18 -0
  11. package/dist/dock-react/group-view.d.ts.map +1 -0
  12. package/dist/dock-react/index.d.ts +3 -2
  13. package/dist/dock-react/index.d.ts.map +1 -1
  14. package/dist/dock-react/index.js +384 -289
  15. package/dist/dock-react/index.js.map +1 -1
  16. package/dist/dock-react/panel-body.d.ts +24 -0
  17. package/dist/dock-react/panel-body.d.ts.map +1 -0
  18. package/dist/dock-react/split-sizing.d.ts +109 -0
  19. package/dist/dock-react/split-sizing.d.ts.map +1 -0
  20. package/dist/dock-react/split-view.d.ts +15 -0
  21. package/dist/dock-react/split-view.d.ts.map +1 -0
  22. package/dist/{electron-deck-B9fun93z.js → electron-deck-Bi-u-Aze.js} +175 -79
  23. package/dist/electron-deck-Bi-u-Aze.js.map +1 -0
  24. package/dist/electron-deck.d.ts +5 -0
  25. package/dist/electron-deck.d.ts.map +1 -1
  26. package/dist/events.d.ts +1 -0
  27. package/dist/events.d.ts.map +1 -1
  28. package/dist/host/index.d.ts +5 -0
  29. package/dist/host/index.d.ts.map +1 -1
  30. package/dist/host/index.js +1 -1
  31. package/dist/index.js +1 -1
  32. package/dist/internal/deck-app-view-move.d.ts +17 -0
  33. package/dist/internal/deck-app-view-move.d.ts.map +1 -0
  34. package/dist/internal/deck-app.d.ts +58 -1
  35. package/dist/internal/deck-app.d.ts.map +1 -1
  36. package/dist/layout/index.d.ts +1 -0
  37. package/dist/layout/index.d.ts.map +1 -1
  38. package/dist/layout/index.js +2 -2
  39. package/dist/layout/model.d.ts.map +1 -1
  40. package/dist/layout/mutations.d.ts.map +1 -1
  41. package/dist/layout/tree-query.d.ts +18 -0
  42. package/dist/layout/tree-query.d.ts.map +1 -0
  43. package/dist/layout/types.d.ts +11 -9
  44. package/dist/layout/types.d.ts.map +1 -1
  45. package/dist/{layout-ByOiOdnc.js → layout-D-LYJIpM.js} +57 -41
  46. package/dist/layout-D-LYJIpM.js.map +1 -0
  47. package/dist/main/compositor.d.ts.map +1 -1
  48. package/dist/main/index.js +1 -1
  49. package/dist/main/view-handle.d.ts.map +1 -1
  50. package/dist/preload/index.cjs +5 -0
  51. package/dist/preload/index.cjs.map +1 -1
  52. package/dist/preload/index.d.ts +5 -0
  53. package/dist/preload/index.d.ts.map +1 -1
  54. package/dist/preload/index.js +5 -0
  55. package/dist/preload/index.js.map +1 -1
  56. package/dist/types.d.ts +32 -10
  57. package/dist/types.d.ts.map +1 -1
  58. package/dist/{view-handle-DDhpBuW-.js → view-handle-zav25k3-.js} +141 -86
  59. package/dist/view-handle-zav25k3-.js.map +1 -0
  60. package/package.json +11 -5
  61. package/dist/electron-deck-B9fun93z.js.map +0 -1
  62. package/dist/layout-ByOiOdnc.js.map +0 -1
  63. package/dist/view-handle-DDhpBuW-.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"split-view.d.ts","sourceRoot":"","sources":["../../src/dock-react/split-view.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAkC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtE,OAAO,KAAK,EAAc,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAU/D,OAAO,EAAc,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAW/D,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,SAAS,CAAA;IACf,GAAG,EAAE,aAAa,CAAA;CAClB;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,SAAS,CAmR1D"}
@@ -1,7 +1,7 @@
1
1
  import { n as DeckRemoteError, r as EventNotBoundError } from "./errors-NE2ig7L5.js";
2
2
  import { i as DeckChannel, t as BRIDGE_PROTOCOL_VERSION } from "./protocol-B6TOT9AP.js";
3
- import { i as createScope, r as createCompositor, t as createViewHandle } from "./view-handle-DDhpBuW-.js";
4
- import { i as reconcile, n as dispatchOps, r as createInitialState, t as cleanSnapshot } from "./layout-ByOiOdnc.js";
3
+ import { i as createScope, r as createCompositor, t as createViewHandle } from "./view-handle-zav25k3-.js";
4
+ import { i as reconcile, n as dispatchOps, r as createInitialState, t as cleanSnapshot } from "./layout-D-LYJIpM.js";
5
5
  import { randomUUID } from "node:crypto";
6
6
  //#region src/events.ts
7
7
  var internals = /* @__PURE__ */ new WeakMap();
@@ -33,6 +33,7 @@ var HostEventImpl = class {
33
33
  } };
34
34
  }
35
35
  };
36
+ /** @experimental No production consumer yet — see the note on {@link HostEvent}. */
36
37
  function defineEvent(name) {
37
38
  if (typeof name !== "string" || name.length === 0) throw new TypeError("defineEvent(name): name must be a non-empty string");
38
39
  return new HostEventImpl(name);
@@ -62,6 +63,37 @@ function unbindHostEvent(event) {
62
63
  }
63
64
  }
64
65
  //#endregion
66
+ //#region src/internal/deck-app-view-move.ts
67
+ /**
68
+ * `DeckViewHandle.moveTo` failure cleanup. `inner.moveTo` does the atomic
69
+ * native Compositor migration + Scope adopt/rollback; when it rejects, the
70
+ * Compositor's own rollback only reverts state it TRACKED (`order`) — a
71
+ * native `addChildView` that throws MID-APPLY can leave the WCV attached to
72
+ * the dest window's `contentView` with no tracked record to remove it. This
73
+ * balances that leak with an explicit dest detach, then undoes the dest
74
+ * substrate registration made before the move attempt.
75
+ *
76
+ * Each step is independently guarded (its own try/catch) so a cleanup
77
+ * failure in one step never masks the other step or the ORIGINAL move
78
+ * error — `originalErr` is always rethrown, never shadowed.
79
+ */
80
+ function cleanupDestOnMoveToFailure(destWin, destSub, wcv, viewId, originalErr) {
81
+ try {
82
+ if (!destWin.isDestroyed()) {
83
+ const destChildren = destWin.contentView.children;
84
+ if (destChildren == null || destChildren.includes(wcv)) destWin.contentView.removeChildView(wcv);
85
+ }
86
+ } catch (cleanupErr) {
87
+ console.error("[electron-deck] moveTo dest detach failed (original error rethrown):", cleanupErr);
88
+ }
89
+ try {
90
+ destSub.unregisterView(viewId);
91
+ } catch (cleanupErr) {
92
+ console.error("[electron-deck] moveTo dest unregister failed (original error rethrown):", cleanupErr);
93
+ }
94
+ throw originalErr;
95
+ }
96
+ //#endregion
65
97
  //#region src/internal/event-bus.ts
66
98
  /**
67
99
  * Framework-internal event bus。
@@ -949,42 +981,76 @@ var DeckApp = class {
949
981
  if (this.startCalled) throw new Error("DeckApp.start() already called");
950
982
  this.startCalled = true;
951
983
  validateConfig(this.config);
952
- const hasWebviewContent = !!(this.config.toolbar || this.config.windows);
953
- if (this.options.electron && hasWebviewContent && !this.options.wireTransport) throw new Error("DeckAppOptions: wireTransport.ipcMain is required when config has toolbar or windows");
984
+ this.assertWireTransportPresentForWebviewContent();
954
985
  const app = this.options.electron?.app;
955
986
  if (app) {
956
- if (this.config.app?.singleInstance && typeof app.requestSingleInstanceLock === "function") {
957
- if (!app.requestSingleInstanceLock()) {
958
- try {
959
- app.quit();
960
- } catch {}
961
- return;
962
- }
963
- }
964
- if (this.options.backend?.beforeReady) await this.options.backend.beforeReady(app);
965
- else if (this.config.app?.name) try {
966
- app.setName(this.config.app.name);
967
- } catch {}
968
- await app.whenReady();
969
- this.bindAppLifecycle(app);
987
+ if (!await this.runPreReadyGate(app)) return;
970
988
  }
971
989
  try {
972
- this.lifecycle.enter("bind");
973
- this.bindDeclarativeFields();
974
- this.assembleElectron();
975
- this.bindWireTransport();
976
- this.loadAssembledSources();
977
- this.lifecycle.enter("setup");
978
- this._runtime = this.buildRuntime();
979
- if (this.options.backend) await this.options.backend.assemble(this._runtime);
980
- if (this.config.setup) await this.config.setup(this._runtime);
981
- this.lifecycle.enter("ready");
990
+ await this.runBindAndSetupPhases();
982
991
  } catch (err) {
983
992
  await this.cleanupOnError();
984
993
  throw err;
985
994
  }
986
995
  }
987
996
  /**
997
+ * half-state guard: electron + (toolbar | windows) without wireTransport
998
+ * would leave the host with webviews that can never reach back via
999
+ * __electron-deck:invoke. Reject early with a clear msg.
1000
+ */
1001
+ assertWireTransportPresentForWebviewContent() {
1002
+ const hasWebviewContent = !!(this.config.toolbar || this.config.windows);
1003
+ if (this.options.electron && hasWebviewContent && !this.options.wireTransport) throw new Error("DeckAppOptions: wireTransport.ipcMain is required when config has toolbar or windows");
1004
+ }
1005
+ /**
1006
+ * Pre-`whenReady` sequencing: single-instance lock (opt-in, must quit
1007
+ * BEFORE any other side effect) → `backend.beforeReady` (or the framework's
1008
+ * best-effort `app.setName`) → `await app.whenReady()` → app-level lifecycle
1009
+ * bindings. Returns `false` when the single-instance lock was lost (the
1010
+ * second instance already called `app.quit()` — `start()` must stop with
1011
+ * phase still `init`), `true` otherwise.
1012
+ */
1013
+ async runPreReadyGate(app) {
1014
+ if (!this.acquireSingleInstanceLock(app)) return false;
1015
+ if (this.options.backend?.beforeReady) await this.options.backend.beforeReady(app);
1016
+ else if (this.config.app?.name) try {
1017
+ app.setName(this.config.app.name);
1018
+ } catch {}
1019
+ await app.whenReady();
1020
+ this.bindAppLifecycle(app);
1021
+ return true;
1022
+ }
1023
+ /**
1024
+ * Single-instance gate (opt-in): a second instance must quit BEFORE any
1025
+ * side effect (beforeReady bootstrap / whenReady / window). Returns `true`
1026
+ * when the caller should continue (not opted in, or this instance holds the
1027
+ * lock); `false` after the losing instance has called `app.quit()`.
1028
+ */
1029
+ acquireSingleInstanceLock(app) {
1030
+ if (!this.config.app?.singleInstance || typeof app.requestSingleInstanceLock !== "function") return true;
1031
+ if (app.requestSingleInstanceLock()) return true;
1032
+ try {
1033
+ app.quit();
1034
+ } catch {}
1035
+ return false;
1036
+ }
1037
+ /** Init → Bind → Setup → Ready. Assembly/bind must run AFTER the
1038
+ * whenReady gate (handled by the caller); loadURL/loadFile must run
1039
+ * AFTER bindWireTransport (a preload calling the bridge before its
1040
+ * ipcMain handler is registered would see a "no handler" reject). */
1041
+ async runBindAndSetupPhases() {
1042
+ this.lifecycle.enter("bind");
1043
+ this.bindDeclarativeFields();
1044
+ this.assembleElectron();
1045
+ this.bindWireTransport();
1046
+ this.loadAssembledSources();
1047
+ this.lifecycle.enter("setup");
1048
+ this._runtime = this.buildRuntime();
1049
+ if (this.options.backend) await this.options.backend.assemble(this._runtime);
1050
+ if (this.config.setup) await this.config.setup(this._runtime);
1051
+ this.lifecycle.enter("ready");
1052
+ }
1053
+ /**
988
1054
  * Process-level Electron lifecycle bindings (post-whenReady, independent of
989
1055
  * `ownsWindows` — these are app events, not window events):
990
1056
  * - `will-quit` → framework teardown (idempotent via `shutdownPromise`).
@@ -1180,8 +1246,8 @@ var DeckApp = class {
1180
1246
  * registration) — windows with no per-window deciders fall through to Electron's
1181
1247
  * default close (the framework does not preventDefault).
1182
1248
  */
1183
- armSubWindowCloseMachine(win, controlWc) {
1184
- win.on("close", (e) => {
1249
+ armSubWindowCloseMachine(win, controlWc, windowScope) {
1250
+ const onClose = (e) => {
1185
1251
  const rec = this.windowRegistrations.get(controlWc);
1186
1252
  if (!rec || rec.deciders.length === 0) return;
1187
1253
  e?.preventDefault?.();
@@ -1204,7 +1270,19 @@ var DeckApp = class {
1204
1270
  rec.closing = null;
1205
1271
  if (!win.isDestroyed()) win.destroy();
1206
1272
  });
1207
- });
1273
+ };
1274
+ win.on("close", onClose);
1275
+ windowScope.own(() => this.removeWindowListener(win, "close", onClose));
1276
+ }
1277
+ /**
1278
+ * Remove a previously-registered window event listener (Electron's
1279
+ * `removeListener`/`off`). Tolerates minimal test fakes that expose neither by
1280
+ * degrading to a no-op — the caller's ownership contract is best-effort cleanup.
1281
+ */
1282
+ removeWindowListener(win, event, listener) {
1283
+ const w = win;
1284
+ const remove = w.removeListener ?? w.off;
1285
+ if (typeof remove === "function") remove.call(win, event, listener);
1208
1286
  }
1209
1287
  /**
1210
1288
  * @internal Lifetime accessors. The live shadow map mirroring `trackedWindows`,
@@ -1519,6 +1597,16 @@ var DeckApp = class {
1519
1597
  this.warnedInvalidKeepAliveMax = true;
1520
1598
  console.warn(`[electron-deck] runtime.view keepAlive.max must be a non-negative integer (got: ${max}); the view is NOT keep-alive-managed (no eviction).`);
1521
1599
  }
1600
+ /** Drop `viewId` from `groupKey`'s HIDDEN list only (the group's `handles`
1601
+ * entry stays — the view is still keep-alive-managed, just no longer
1602
+ * evictable while visible). Idempotent: a no-op when the view isn't on the
1603
+ * hidden list or the group doesn't exist. */
1604
+ dropViewFromKeepAliveHidden(groupKey, viewId) {
1605
+ const group = this.keepAliveGroups.get(groupKey);
1606
+ if (!group) return;
1607
+ const i = group.hidden.indexOf(viewId);
1608
+ if (i >= 0) group.hidden.splice(i, 1);
1609
+ }
1522
1610
  constructWindow(opts, autoTrust, deferLoad = false) {
1523
1611
  const electron = this.options.electron;
1524
1612
  if (!electron) throw new Error("DeckApp.constructWindow called without electron injection");
@@ -1550,7 +1638,7 @@ var DeckApp = class {
1550
1638
  this.bindNavigationGrantReset(wc);
1551
1639
  }
1552
1640
  this.buildDeckWindow(win, wc, windowScope);
1553
- this.armSubWindowCloseMachine(win, wc);
1641
+ this.armSubWindowCloseMachine(win, wc, windowScope);
1554
1642
  if (!deferLoad) this.safeLoad(win.webContents, opts.source);
1555
1643
  return win;
1556
1644
  }
@@ -1593,10 +1681,11 @@ var DeckApp = class {
1593
1681
  };
1594
1682
  if (typeof win.prependListener === "function") win.prependListener("closed", revoke);
1595
1683
  else win.on("closed", revoke);
1684
+ windowScope.own(() => this.removeWindowListener(win, "closed", revoke));
1596
1685
  this.admitTrust(wc, windowScope);
1597
1686
  this.bindNavigationGrantReset(wc);
1598
1687
  this.buildDeckWindow(win, wc, windowScope);
1599
- this.armSubWindowCloseMachine(win, wc);
1688
+ this.armSubWindowCloseMachine(win, wc, windowScope);
1600
1689
  windowScope.own(() => {
1601
1690
  this.windowSubstrates.delete(wc);
1602
1691
  this.adoptedWindows.delete(wc);
@@ -1902,6 +1991,12 @@ var DeckApp = class {
1902
1991
  this.bindNavigationGrantReset(wc);
1903
1992
  return lease;
1904
1993
  }
1994
+ const adopted = this.wcRecords.get(wc);
1995
+ if (adopted) {
1996
+ const lease = this.admitTrust(wc, adopted.windowScope);
1997
+ this.bindNavigationGrantReset(wc);
1998
+ return lease;
1999
+ }
1905
2000
  return this._trustWebContents(wc);
1906
2001
  },
1907
2002
  adopt: (win, opts) => {
@@ -2042,30 +2137,11 @@ var DeckApp = class {
2042
2137
  rehome: moveOpts.rehome
2043
2138
  });
2044
2139
  } catch (e) {
2045
- try {
2046
- if (!destWin.isDestroyed()) {
2047
- const destChildren = destWin.contentView.children;
2048
- if (destChildren == null || destChildren.includes(wcv)) destWin.contentView.removeChildView(wcv);
2049
- }
2050
- } catch (cleanupErr) {
2051
- console.error("[electron-deck] moveTo dest detach failed (original error rethrown):", cleanupErr);
2052
- }
2053
- try {
2054
- destSub.unregisterView(viewId);
2055
- } catch (cleanupErr) {
2056
- console.error("[electron-deck] moveTo dest unregister failed (original error rethrown):", cleanupErr);
2057
- }
2058
- throw e;
2140
+ cleanupDestOnMoveToFailure(destWin, destSub, wcv, viewId, e);
2059
2141
  }
2060
2142
  if (srcSub && srcSub !== destSub) srcSub.unregisterView(viewId);
2061
2143
  placedSubstrate = destSub;
2062
- if (groupKey) {
2063
- const group = this.keepAliveGroups.get(groupKey);
2064
- if (group) {
2065
- const hi = group.hidden.indexOf(viewId);
2066
- if (hi >= 0) group.hidden.splice(hi, 1);
2067
- }
2068
- }
2144
+ if (groupKey) this.dropViewFromKeepAliveHidden(groupKey, viewId);
2069
2145
  if (slotToken) {
2070
2146
  this.slotTokens.delete(slotToken);
2071
2147
  slotToken = void 0;
@@ -2087,7 +2163,9 @@ var DeckApp = class {
2087
2163
  while (keepAlive && group.hidden.length > keepAlive.max) {
2088
2164
  const victimId = group.hidden.shift();
2089
2165
  const victim = group.handles.get(victimId);
2090
- if (victim) victim.dispose();
2166
+ if (victim) victim.dispose().catch((e) => {
2167
+ console.error("[electron-deck] keepAlive eviction dispose failed:", e);
2168
+ });
2091
2169
  }
2092
2170
  }
2093
2171
  }
@@ -2209,6 +2287,11 @@ async function electronDeck(config, options) {
2209
2287
  *
2210
2288
  * Invalid config throws a `TypeError` synchronously (matching `electronDeck`'s
2211
2289
  * validate-first contract) — the error surfaces, never silently deadlocks.
2290
+ *
2291
+ * `@experimental` No production consumer yet — the only caller in this repo is
2292
+ * `packages/devtools/spike/popout/harness.mjs`; devtools' real entry
2293
+ * (`launch.ts`) uses `electronDeck({ backend })` instead. Contract may change
2294
+ * until a second real consumer adopts it.
2212
2295
  */
2213
2296
  function startElectronDeck(config, options) {
2214
2297
  validateConfig(config);
@@ -2280,37 +2363,50 @@ function buildAppOptions(electron, ipcMain, opts) {
2280
2363
  */
2281
2364
  function validateConfig(config) {
2282
2365
  if (config === null || typeof config !== "object") throw new TypeError("electronDeck(config): config must be an object");
2283
- if (config.backend !== void 0) {
2284
- const backend = config.backend;
2285
- if (backend === null || typeof backend !== "object" || typeof backend.assemble !== "function") throw new TypeError("config.backend must be a RuntimeBackend (an object with an assemble() function)");
2286
- }
2366
+ validateBackendField(config.backend);
2287
2367
  if (config.simulatorApis !== void 0) validateHandlerMap("simulatorApis", config.simulatorApis);
2288
2368
  if (config.hostServices !== void 0) validateHandlerMap("hostServices", config.hostServices);
2289
- if (config.events !== void 0) {
2290
- if (!Array.isArray(config.events)) throw new TypeError("events must be an array of HostEvent (from defineEvent())");
2291
- for (const ev of config.events) if (!isHostEvent(ev)) throw new TypeError("events: every entry must be a HostEvent produced by defineEvent() — duck-typed shapes are rejected to avoid bind-time failures");
2292
- const names = /* @__PURE__ */ new Set();
2293
- for (const ev of config.events) {
2294
- if (names.has(ev.name)) throw new Error(`events: duplicate HostEvent name "${ev.name}"`);
2295
- names.add(ev.name);
2296
- }
2297
- }
2298
- if (config.toolbar !== void 0) {
2299
- const { source, preloadPath, height } = config.toolbar;
2300
- if (source === null || typeof source !== "object") throw new TypeError("toolbar.source must be { url } or { file }");
2301
- const hasUrl = "url" in source && typeof source.url === "string";
2302
- const hasFile = "file" in source && typeof source.file === "string";
2303
- if (hasUrl && hasFile) throw new TypeError("toolbar.source must be either { url } or { file }, not both");
2304
- if (!hasUrl && !hasFile) throw new TypeError("toolbar.source must be { url } or { file }");
2305
- if (typeof preloadPath !== "string" || preloadPath.length === 0) throw new TypeError("toolbar.preloadPath is required and must be a non-empty string");
2306
- if (typeof height !== "number" || !Number.isFinite(height) || height <= 0) throw new TypeError("toolbar.height is required and must be a positive finite number");
2307
- }
2369
+ if (config.events !== void 0) validateEventsField(config.events);
2370
+ if (config.toolbar !== void 0) validateToolbarField(config.toolbar);
2371
+ }
2372
+ function validateBackendField(backend) {
2373
+ if (backend === void 0) return;
2374
+ const candidate = backend;
2375
+ if (candidate === null || typeof candidate !== "object" || typeof candidate.assemble !== "function") throw new TypeError("config.backend must be a RuntimeBackend (an object with an assemble() function)");
2308
2376
  }
2309
2377
  function validateHandlerMap(fieldName, value) {
2310
2378
  if (value === null || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`${fieldName} must be an object of handlers`);
2311
2379
  for (const [name, handler] of Object.entries(value)) if (typeof handler !== "function") throw new TypeError(`${fieldName}["${name}"] must be a function`);
2312
2380
  }
2381
+ function validateEventsField(events) {
2382
+ if (!Array.isArray(events)) throw new TypeError("events must be an array of HostEvent (from defineEvent())");
2383
+ assertAllHostEvents(events);
2384
+ assertUniqueEventNames(events);
2385
+ }
2386
+ /** duck-typed 事件形状会被拒绝——只接受 `defineEvent()` 产出的真实 HostEvent,避免 bind-time 才炸。 */
2387
+ function assertAllHostEvents(events) {
2388
+ for (const ev of events) if (!isHostEvent(ev)) throw new TypeError("events: every entry must be a HostEvent produced by defineEvent() — duck-typed shapes are rejected to avoid bind-time failures");
2389
+ }
2390
+ function assertUniqueEventNames(events) {
2391
+ const names = /* @__PURE__ */ new Set();
2392
+ for (const ev of events) {
2393
+ if (names.has(ev.name)) throw new Error(`events: duplicate HostEvent name "${ev.name}"`);
2394
+ names.add(ev.name);
2395
+ }
2396
+ }
2397
+ function validateToolbarField(toolbar) {
2398
+ assertToolbarSourceShape(toolbar.source);
2399
+ if (typeof toolbar.preloadPath !== "string" || toolbar.preloadPath.length === 0) throw new TypeError("toolbar.preloadPath is required and must be a non-empty string");
2400
+ if (typeof toolbar.height !== "number" || !Number.isFinite(toolbar.height) || toolbar.height <= 0) throw new TypeError("toolbar.height is required and must be a positive finite number");
2401
+ }
2402
+ function assertToolbarSourceShape(source) {
2403
+ if (source === null || typeof source !== "object") throw new TypeError("toolbar.source must be { url } or { file }");
2404
+ const hasUrl = "url" in source && typeof source.url === "string";
2405
+ const hasFile = "file" in source && typeof source.file === "string";
2406
+ if (hasUrl && hasFile) throw new TypeError("toolbar.source must be either { url } or { file }, not both");
2407
+ if (!hasUrl && !hasFile) throw new TypeError("toolbar.source must be { url } or { file }");
2408
+ }
2313
2409
  //#endregion
2314
2410
  export { WireTransport as a, InMemoryTypedIpcRegistry as c, createControlBus as i, EventBus as l, startElectronDeck as n, createCapabilityRegistry as o, validateConfig as r, createTrustSet as s, electronDeck as t, defineEvent as u };
2315
2411
 
2316
- //# sourceMappingURL=electron-deck-B9fun93z.js.map
2412
+ //# sourceMappingURL=electron-deck-Bi-u-Aze.js.map