@dimina-kit/devtools 0.4.0-dev.20260612152115 → 0.4.0-dev.20260615083030

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 (43) hide show
  1. package/dist/main/index.bundle.js +49 -17
  2. package/dist/main/services/safe-area/index.d.ts +6 -4
  3. package/dist/main/services/safe-area/index.js +20 -14
  4. package/dist/main/services/simulator-storage/index.js +4 -2
  5. package/dist/main/services/views/view-manager.js +40 -8
  6. package/dist/main/utils/theme.d.ts +15 -0
  7. package/dist/main/utils/theme.js +34 -1
  8. package/dist/preload/runtime/native-host.d.ts +3 -0
  9. package/dist/preload/runtime/native-host.js +2 -0
  10. package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
  11. package/dist/preload/windows/main.cjs +3 -3
  12. package/dist/preload/windows/main.cjs.map +2 -2
  13. package/dist/preload/windows/simulator.cjs +1 -0
  14. package/dist/preload/windows/simulator.cjs.map +2 -2
  15. package/dist/preload/windows/simulator.js +1 -0
  16. package/dist/renderer/assets/index-BFwsvw4p.js +50 -0
  17. package/dist/renderer/assets/{input-DgqX2UvN.js → input-hGF71bn3.js} +2 -2
  18. package/dist/renderer/assets/{ipc-transport-CsFhDJ7q.js → ipc-transport-BKWkvwKK.js} +2 -2
  19. package/dist/renderer/assets/ipc-transport-Cg1q1gwD.css +1 -0
  20. package/dist/renderer/assets/{popover-D_2o_52M.js → popover-NNQNyjgI.js} +2 -2
  21. package/dist/renderer/assets/select-Bb1AKeSq.js +2 -0
  22. package/dist/renderer/assets/{settings-CivkfizP.js → settings-7uwimev-.js} +2 -2
  23. package/dist/renderer/assets/settings-api-DkrPHroq.js +2 -0
  24. package/dist/renderer/assets/{workbenchSettings-BwpwxuzM.js → workbenchSettings-EaMYvKHG.js} +2 -2
  25. package/dist/renderer/entries/main/index.html +6 -6
  26. package/dist/renderer/entries/popover/index.html +5 -5
  27. package/dist/renderer/entries/settings/index.html +5 -5
  28. package/dist/renderer/entries/workbench-settings/index.html +4 -4
  29. package/dist/shared/ipc-channels.d.ts +1 -1
  30. package/dist/shared/ipc-channels.js +7 -0
  31. package/dist/shared/ipc-schemas.d.ts +0 -1
  32. package/dist/shared/ipc-schemas.js +0 -1
  33. package/dist/simulator/assets/device-shell-BWX7Yopg.js +2 -0
  34. package/dist/simulator/assets/{device-shell-DsdcNEkt.css → device-shell-BiFE9Otv.css} +1 -1
  35. package/dist/simulator/assets/{simulator-mini-app-CYtW6bMd.js → simulator-mini-app-BfNuDF3R.js} +2 -2
  36. package/dist/simulator/assets/{simulator-cV1PERi6.js → simulator-sf-D0mhw.js} +3 -3
  37. package/dist/simulator/simulator.html +1 -1
  38. package/package.json +4 -4
  39. package/dist/renderer/assets/index-Bi_6SS-L.js +0 -50
  40. package/dist/renderer/assets/ipc-transport-SnOAk0Jq.css +0 -1
  41. package/dist/renderer/assets/select-BJYsdghn.js +0 -2
  42. package/dist/renderer/assets/settings-api-7mXkBy5-.js +0 -2
  43. package/dist/simulator/assets/device-shell-BEnc2k-W.js +0 -2
@@ -365,7 +365,14 @@ var WorkbenchSettingsChannel = {
365
365
  SetTheme: "workbenchSettings:setTheme",
366
366
  GetCdpStatus: "workbenchSettings:getCdpStatus",
367
367
  GetMcpStatus: "workbenchSettings:getMcpStatus",
368
- Init: "workbenchSettings:init"
368
+ Init: "workbenchSettings:init",
369
+ // Main → renderer push: the active color scheme flipped (OS change or in-app
370
+ // SetTheme). Payload is `isDark: boolean`. The app's CSS reacts to
371
+ // `prefers-color-scheme` automatically; this exists for the few JS consumers
372
+ // (Monaco's theme) that can't observe that media change — Electron does NOT
373
+ // dispatch the renderer's `matchMedia('(prefers-color-scheme)')` change event
374
+ // for programmatic `nativeTheme.themeSource` assignments.
375
+ ThemeChanged: "workbenchSettings:themeChanged"
369
376
  };
370
377
  var ProjectChannel = {
371
378
  Open: "project:open",
@@ -795,12 +802,21 @@ import { toDisposable } from "@dimina-kit/electron-deck/main";
795
802
  function themeBg() {
796
803
  return nativeTheme2.shouldUseDarkColors ? "#1a1a1a" : "#fafafa";
797
804
  }
805
+ function simDeskBg() {
806
+ return nativeTheme2.shouldUseDarkColors ? "#121212" : "#e8e8e8";
807
+ }
798
808
  function installThemeBackgroundSync() {
799
809
  const apply = () => {
800
810
  const bg = themeBg();
811
+ const isDark = nativeTheme2.shouldUseDarkColors;
801
812
  for (const win of BrowserWindow.getAllWindows()) {
802
- if (!win.isDestroyed()) {
813
+ if (win.isDestroyed()) continue;
814
+ try {
803
815
  win.setBackgroundColor(bg);
816
+ if (!win.webContents.isDestroyed()) {
817
+ win.webContents.send(WorkbenchSettingsChannel.ThemeChanged, isDark);
818
+ }
819
+ } catch {
804
820
  }
805
821
  }
806
822
  };
@@ -1018,7 +1034,7 @@ function createRendererNotifier(ctx) {
1018
1034
  }
1019
1035
 
1020
1036
  // src/main/services/views/view-manager.ts
1021
- import { ipcMain as ipcMain2, shell as shell2, WebContentsView as WebContentsView2, webContents } from "electron";
1037
+ import { ipcMain as ipcMain2, nativeTheme as nativeTheme3, shell as shell2, WebContentsView as WebContentsView2, webContents } from "electron";
1022
1038
  import path7 from "path";
1023
1039
 
1024
1040
  // src/shared/open-in-editor.ts
@@ -1075,22 +1091,23 @@ function resourceUrlToProjectRelativePath(resourceUrl, expectedOrigin) {
1075
1091
  }
1076
1092
 
1077
1093
  // src/main/services/safe-area/index.ts
1078
- function guestInsets(device) {
1094
+ function guestInsets(device, isTabPage2) {
1079
1095
  const top = device?.safeAreaInsets.top ?? 0;
1080
- return { top, topMax: top, right: 0, rightMax: 0, bottom: 0, bottomMax: 0, left: 0, leftMax: 0 };
1096
+ const bottom = isTabPage2 ? 0 : device?.safeAreaInsets.bottom ?? 0;
1097
+ return { top, topMax: top, right: 0, rightMax: 0, bottom, bottomMax: bottom, left: 0, leftMax: 0 };
1081
1098
  }
1082
1099
  function createSafeAreaController(options = {}) {
1083
- const attached = /* @__PURE__ */ new Set();
1084
- function override(wc, device) {
1100
+ const attached = /* @__PURE__ */ new Map();
1101
+ function override(wc, device, isTabPage2) {
1085
1102
  if (wc.isDestroyed()) return;
1086
- void wc.debugger.sendCommand("Emulation.setSafeAreaInsetsOverride", { insets: guestInsets(device) }).catch((err) => {
1103
+ void wc.debugger.sendCommand("Emulation.setSafeAreaInsetsOverride", { insets: guestInsets(device, isTabPage2) }).catch((err) => {
1087
1104
  console.warn("[safe-area] setSafeAreaInsetsOverride failed:", err instanceof Error ? err.message : err);
1088
1105
  });
1089
1106
  }
1090
1107
  return {
1091
- applyToGuest: (wc, device) => {
1108
+ applyToGuest: (wc, device, isTabPage2) => {
1092
1109
  if (!wc || wc.isDestroyed() || attached.has(wc)) {
1093
- if (wc && !wc.isDestroyed() && attached.has(wc)) override(wc, device);
1110
+ if (wc && !wc.isDestroyed() && attached.has(wc)) override(wc, device, isTabPage2);
1094
1111
  return;
1095
1112
  }
1096
1113
  try {
@@ -1099,19 +1116,19 @@ function createSafeAreaController(options = {}) {
1099
1116
  console.warn("[safe-area] debugger.attach failed; env(safe-area-inset-*) stays 0:", err instanceof Error ? err.message : err);
1100
1117
  return;
1101
1118
  }
1102
- attached.add(wc);
1119
+ attached.set(wc, isTabPage2);
1103
1120
  if (options.connections) {
1104
1121
  options.connections.acquire(wc).own(() => attached.delete(wc));
1105
1122
  } else {
1106
1123
  wc.once("destroyed", () => attached.delete(wc));
1107
1124
  }
1108
- override(wc, device);
1125
+ override(wc, device, isTabPage2);
1109
1126
  },
1110
1127
  reapplyAll: (device) => {
1111
- for (const wc of attached) override(wc, device);
1128
+ for (const [wc, isTabPage2] of attached) override(wc, device, isTabPage2);
1112
1129
  },
1113
1130
  dispose: () => {
1114
- for (const wc of attached) {
1131
+ for (const wc of attached.keys()) {
1115
1132
  try {
1116
1133
  if (!wc.isDestroyed()) wc.debugger.detach();
1117
1134
  } catch {
@@ -2313,22 +2330,38 @@ function createViewManager(ctx) {
2313
2330
  }
2314
2331
  });
2315
2332
  nativeSimulatorView = view;
2316
- view.setBackgroundColor("#121212");
2333
+ view.setBackgroundColor(simDeskBg());
2317
2334
  const simWc = view.webContents;
2335
+ const syncDeskBg = () => {
2336
+ try {
2337
+ if (!simWc.isDestroyed()) view.setBackgroundColor(simDeskBg());
2338
+ } catch {
2339
+ }
2340
+ };
2341
+ nativeTheme3.on("updated", syncDeskBg);
2342
+ ctx.connections.acquire(simWc).own(() => nativeTheme3.removeListener("updated", syncDeskBg));
2318
2343
  attachNativeCustomApiBridge(simWc);
2344
+ const pendingGuestIsTab = [];
2319
2345
  simWc.on("will-attach-webview", (_event, webPreferences, params) => {
2320
2346
  ;
2321
2347
  webPreferences.partition = partition;
2322
2348
  params.partition = partition;
2323
2349
  webPreferences.contextIsolation = false;
2324
2350
  webPreferences.sandbox = false;
2351
+ let isTab = false;
2352
+ try {
2353
+ isTab = new URL(params.src).searchParams.get("isTab") === "1";
2354
+ } catch {
2355
+ }
2356
+ pendingGuestIsTab.push(isTab);
2325
2357
  });
2326
2358
  simWc.on("did-attach-webview", (_event, guestWc) => {
2327
2359
  try {
2328
2360
  guestWc.setZoomFactor(currentZoomFactor);
2329
2361
  } catch {
2330
2362
  }
2331
- safeArea.applyToGuest(guestWc, ctx.bridge?.getDevice() ?? null);
2363
+ const isTabGuest = pendingGuestIsTab.shift() ?? false;
2364
+ safeArea.applyToGuest(guestWc, ctx.bridge?.getDevice() ?? null, isTabGuest);
2332
2365
  guestWc.setWindowOpenHandler(({ url }) => handleWindowOpenExternal(url));
2333
2366
  guestWc.on("will-navigate", (e, url) => {
2334
2367
  try {
@@ -3258,7 +3291,6 @@ var SimulatorSetDeviceInfoSchema = z3.tuple([
3258
3291
  screenWidth: z3.number().int().min(100).max(4e3),
3259
3292
  screenHeight: z3.number().int().min(100).max(4e3),
3260
3293
  statusBarHeight: z3.number().finite().min(0).max(400),
3261
- safeAreaBottom: z3.number().finite().min(0).max(400),
3262
3294
  notchType: z3.enum(["none", "notch", "dynamic-island"]),
3263
3295
  safeAreaInsets: z3.object({
3264
3296
  top: z3.number().finite().min(0).max(400),
@@ -3,10 +3,12 @@ import type { ConnectionRegistry } from '@dimina-kit/electron-deck/main';
3
3
  import type { NativeDeviceInfo } from '../../../shared/ipc-channels.js';
4
4
  export interface SafeAreaController {
5
5
  /** Attach the debugger to a freshly-attached render-host guest and push the
6
- * current device's insets. No-op (warn) if the guest is already claimed by an
7
- * external CDP client env then stays 0 rather than throwing. */
8
- applyToGuest(guestWc: WebContents, device: NativeDeviceInfo | null): void;
9
- /** Re-push insets to every still-attached guest after a device change. */
6
+ * current device's insets. `isTabPage` selects the bottom-inset policy (0 for
7
+ * tab pages, the real inset for full-bleed non-tab pages). No-op (warn) if the
8
+ * guest is already claimed by an external CDP client — env then stays 0. */
9
+ applyToGuest(guestWc: WebContents, device: NativeDeviceInfo | null, isTabPage: boolean): void;
10
+ /** Re-push insets to every still-attached guest after a device change (each
11
+ * guest keeps the page type it attached with). */
10
12
  reapplyAll(device: NativeDeviceInfo | null): void;
11
13
  /** Detach from all guests (teardown). */
12
14
  dispose(): void;
@@ -1,25 +1,31 @@
1
- function guestInsets(device) {
1
+ function guestInsets(device, isTabPage) {
2
2
  const top = device?.safeAreaInsets.top ?? 0;
3
- return { top, topMax: top, right: 0, rightMax: 0, bottom: 0, bottomMax: 0, left: 0, leftMax: 0 };
3
+ // A tab page's content sits above the shell-drawn tabBar (which fills the
4
+ // bottom safe area), so it never borders the bottom unsafe zone. A non-tab
5
+ // page is full-bleed to the device bottom, so surface the real inset for its
6
+ // own `env(safe-area-inset-bottom)` opt-in.
7
+ const bottom = isTabPage ? 0 : (device?.safeAreaInsets.bottom ?? 0);
8
+ return { top, topMax: top, right: 0, rightMax: 0, bottom, bottomMax: bottom, left: 0, leftMax: 0 };
4
9
  }
5
10
  export function createSafeAreaController(options = {}) {
6
- // Guests we successfully attached `wc.debugger` to (so we don't re-attach,
7
- // which throws). Pruned on guest destroy.
8
- const attached = new Set();
9
- function override(wc, device) {
11
+ // Guests we successfully attached `wc.debugger` to (value = the page's
12
+ // `isTabPage`, fixed for the guest's life — it's one page). So we don't
13
+ // re-attach (throws), and a device-change reapply reuses the same policy.
14
+ const attached = new Map();
15
+ function override(wc, device, isTabPage) {
10
16
  if (wc.isDestroyed())
11
17
  return;
12
18
  void wc.debugger
13
- .sendCommand('Emulation.setSafeAreaInsetsOverride', { insets: guestInsets(device) })
19
+ .sendCommand('Emulation.setSafeAreaInsetsOverride', { insets: guestInsets(device, isTabPage) })
14
20
  .catch((err) => {
15
21
  console.warn('[safe-area] setSafeAreaInsetsOverride failed:', err instanceof Error ? err.message : err);
16
22
  });
17
23
  }
18
24
  return {
19
- applyToGuest: (wc, device) => {
25
+ applyToGuest: (wc, device, isTabPage) => {
20
26
  if (!wc || wc.isDestroyed() || attached.has(wc)) {
21
27
  if (wc && !wc.isDestroyed() && attached.has(wc))
22
- override(wc, device);
28
+ override(wc, device, isTabPage);
23
29
  return;
24
30
  }
25
31
  try {
@@ -31,21 +37,21 @@ export function createSafeAreaController(options = {}) {
31
37
  console.warn('[safe-area] debugger.attach failed; env(safe-area-inset-*) stays 0:', err instanceof Error ? err.message : err);
32
38
  return;
33
39
  }
34
- attached.add(wc);
40
+ attached.set(wc, isTabPage);
35
41
  if (options.connections) {
36
42
  options.connections.acquire(wc).own(() => attached.delete(wc));
37
43
  }
38
44
  else {
39
45
  wc.once('destroyed', () => attached.delete(wc));
40
46
  }
41
- override(wc, device);
47
+ override(wc, device, isTabPage);
42
48
  },
43
49
  reapplyAll: (device) => {
44
- for (const wc of attached)
45
- override(wc, device);
50
+ for (const [wc, isTabPage] of attached)
51
+ override(wc, device, isTabPage);
46
52
  },
47
53
  dispose: () => {
48
- for (const wc of attached) {
54
+ for (const wc of attached.keys()) {
49
55
  try {
50
56
  if (!wc.isDestroyed())
51
57
  wc.debugger.detach();
@@ -403,8 +403,10 @@ export function setupSimulatorStorage(host, options) {
403
403
  const filtered = prefix
404
404
  ? result.entries.filter(([key]) => key.startsWith(prefix))
405
405
  : result.entries;
406
- // Keys are returned with their `${appId}_` prefix intact; stripping is
407
- // a follow-up so the panel UI can stay compatible with the raw keys.
406
+ // Keys are returned with their `${appId}_` prefix intact: every write
407
+ // path (Set/Remove) keys off the raw storage key. The Storage panel
408
+ // display-strips the prefix for readability (storage-panel.tsx), so the
409
+ // wire keys stay raw while the UI shows clean, Chrome-like keys.
408
410
  return filtered.map(([key, value]) => ({ key, value }));
409
411
  });
410
412
  }
@@ -1,6 +1,7 @@
1
- import { ipcMain, shell, WebContentsView, webContents } from 'electron';
1
+ import { ipcMain, nativeTheme, shell, WebContentsView, webContents } from 'electron';
2
2
  import path from 'path';
3
3
  import { cjsSiblingPreloadPath, mainPreloadPath } from '../../utils/paths.js';
4
+ import { simDeskBg } from '../../utils/theme.js';
4
5
  import { applyNavigationHardening, handleWindowOpenExternal, } from '../../windows/navigation-hardening.js';
5
6
  import { SimulatorCustomApiBridgeChannel } from '../../../shared/ipc-channels.js';
6
7
  import { OPEN_IN_EDITOR_SCHEME, decodeOpenInEditorUrl, resourceUrlToProjectRelativePath, } from '../../../shared/open-in-editor.js';
@@ -806,12 +807,27 @@ export function createViewManager(ctx) {
806
807
  },
807
808
  });
808
809
  nativeSimulatorView = view;
809
- // Paint the WCV surface the simulator-panel background ( --color-sim-bg
810
- // hsl(0 0% 7%)) so a height-resize that grows the region never flashes white
811
- // in the newly-exposed strip before DeviceShell's desk repaints — the WCV,
812
- // the desk, and the renderer placeholder behind it are all the same color.
813
- view.setBackgroundColor('#121212');
810
+ // Paint the WCV surface the themed desk color (simDeskBg(): dark #121212 /
811
+ // light #e8e8e8) so a height-resize that grows the region never flashes a
812
+ // mismatched strip before DeviceShell's desk repaints — the WCV, the desk,
813
+ // and the renderer placeholder behind it are all the same color.
814
+ view.setBackgroundColor(simDeskBg());
814
815
  const simWc = view.webContents;
816
+ // Keep the WCV surface in sync with the active color scheme. The
817
+ // process-wide installThemeBackgroundSync() re-syncs BrowserWindows on a
818
+ // theme switch, but this top-level WebContentsView is not a window, so its
819
+ // creation-time backgroundColor would otherwise freeze. Mirror simDeskBg()
820
+ // here on every nativeTheme `updated`; the listener is owned by the wc's
821
+ // connection so it detaches when the simulator view is torn down.
822
+ const syncDeskBg = () => {
823
+ try {
824
+ if (!simWc.isDestroyed())
825
+ view.setBackgroundColor(simDeskBg());
826
+ }
827
+ catch { /* view/wc gone */ }
828
+ };
829
+ nativeTheme.on('updated', syncDeskBg);
830
+ ctx.connections.acquire(simWc).own(() => nativeTheme.removeListener('updated', syncDeskBg));
815
831
  // Service the simulator-side `__diminaCustomApis` bridge: this top-level
816
832
  // WebContentsView has no embedder renderer to proxy through, so dispatch its
817
833
  // `sendToHost` requests straight to `ctx.simulatorApis` from main.
@@ -822,12 +838,23 @@ export function createViewManager(ctx) {
822
838
  // them with contextIsolation/sandbox off so the render runtime + its preload
823
839
  // share the page realm. (A top-level WebContentsView can host these guests; a
824
840
  // `<webview>` guest cannot — that's the whole point of Option A.)
841
+ // Page type (`isTab`) of each attaching guest, captured from its render-host
842
+ // URL in will-attach (where `params.src` carries the full URL) and consumed
843
+ // FIFO in the matching did-attach — `guestWc.getURL()` is still empty there.
844
+ // Per-attach scope: a fresh simWc + handlers are built on every (re)attach.
845
+ const pendingGuestIsTab = [];
825
846
  simWc.on('will-attach-webview', (_event, webPreferences, params) => {
826
847
  ;
827
848
  webPreferences.partition = partition;
828
849
  params.partition = partition;
829
850
  webPreferences.contextIsolation = false;
830
851
  webPreferences.sandbox = false;
852
+ let isTab = false;
853
+ try {
854
+ isTab = new URL(params.src).searchParams.get('isTab') === '1';
855
+ }
856
+ catch { /* keep false */ }
857
+ pendingGuestIsTab.push(isTab);
831
858
  });
832
859
  simWc.on('did-attach-webview', (_event, guestWc) => {
833
860
  // Scale the nested render-host page with the device zoom. The host WCV is
@@ -841,8 +868,13 @@ export function createViewManager(ctx) {
841
868
  }
842
869
  catch { /* guest not ready; setNativeSimulatorViewBounds re-applies */ }
843
870
  // Simulate this device's CSS env(safe-area-inset-*) on the fresh guest
844
- // before it paints, so notch-aware page layout resolves correctly.
845
- safeArea.applyToGuest(guestWc, ctx.bridge?.getDevice() ?? null);
871
+ // before it paints, so notch-aware page layout resolves correctly. The
872
+ // bottom inset is page-type-dependent (see services/safe-area): a tab
873
+ // page's content sits above the shell tabBar (bottom 0); a non-tab page
874
+ // is full-bleed (real bottom inset). The page type was captured from the
875
+ // render-host URL in will-attach (FIFO).
876
+ const isTabGuest = pendingGuestIsTab.shift() ?? false;
877
+ safeArea.applyToGuest(guestWc, ctx.bridge?.getDevice() ?? null, isTabGuest);
846
878
  guestWc.setWindowOpenHandler(({ url }) => handleWindowOpenExternal(url));
847
879
  guestWc.on('will-navigate', (e, url) => {
848
880
  try {
@@ -8,6 +8,21 @@ import { type Disposable } from '@dimina-kit/electron-deck/main';
8
8
  * Light: hsl(0 0% 98%) ≈ #fafafa (--color-bg)
9
9
  */
10
10
  export declare function themeBg(): string;
11
+ /**
12
+ * Backdrop ("desk") color for the native simulator WebContentsView — the
13
+ * surface the simulated phone sits on. Unlike {@link themeBg} (the window bg),
14
+ * the desk is a neutral grey kept a touch off the window so the light-colored
15
+ * phone keeps contrast against it in BOTH schemes.
16
+ *
17
+ * Dark: hsl(0 0% 7%) ≈ #121212 (the long-standing desk color — unchanged)
18
+ * Light: hsl(0 0% 91%) ≈ #e8e8e8 (neutral grey; the white phone reads on it)
19
+ *
20
+ * MUST stay equal to the renderer's `--color-sim-bg` (design.css) and the
21
+ * simulator page's `.device-shell-root` background (device-shell.css): the WCV,
22
+ * the desk, and the placeholder behind it are the same color so a height-resize
23
+ * never flashes a mismatched strip. Update all three together.
24
+ */
25
+ export declare function simDeskBg(): string;
11
26
  /**
12
27
  * Install a single process-wide listener that keeps every window's native
13
28
  * `backgroundColor` in sync with the active color scheme.
@@ -1,5 +1,6 @@
1
1
  import { BrowserWindow, nativeTheme } from 'electron';
2
2
  import { toDisposable } from '@dimina-kit/electron-deck/main';
3
+ import { WorkbenchSettingsChannel } from '../../shared/ipc-channels.js';
3
4
  /**
4
5
  * Background color that matches the current system color scheme.
5
6
  * Pass as `backgroundColor` when creating BrowserWindows to prevent
@@ -11,6 +12,23 @@ import { toDisposable } from '@dimina-kit/electron-deck/main';
11
12
  export function themeBg() {
12
13
  return nativeTheme.shouldUseDarkColors ? '#1a1a1a' : '#fafafa';
13
14
  }
15
+ /**
16
+ * Backdrop ("desk") color for the native simulator WebContentsView — the
17
+ * surface the simulated phone sits on. Unlike {@link themeBg} (the window bg),
18
+ * the desk is a neutral grey kept a touch off the window so the light-colored
19
+ * phone keeps contrast against it in BOTH schemes.
20
+ *
21
+ * Dark: hsl(0 0% 7%) ≈ #121212 (the long-standing desk color — unchanged)
22
+ * Light: hsl(0 0% 91%) ≈ #e8e8e8 (neutral grey; the white phone reads on it)
23
+ *
24
+ * MUST stay equal to the renderer's `--color-sim-bg` (design.css) and the
25
+ * simulator page's `.device-shell-root` background (device-shell.css): the WCV,
26
+ * the desk, and the placeholder behind it are the same color so a height-resize
27
+ * never flashes a mismatched strip. Update all three together.
28
+ */
29
+ export function simDeskBg() {
30
+ return nativeTheme.shouldUseDarkColors ? '#121212' : '#e8e8e8';
31
+ }
14
32
  /**
15
33
  * Install a single process-wide listener that keeps every window's native
16
34
  * `backgroundColor` in sync with the active color scheme.
@@ -36,10 +54,25 @@ export function themeBg() {
36
54
  export function installThemeBackgroundSync() {
37
55
  const apply = () => {
38
56
  const bg = themeBg();
57
+ const isDark = nativeTheme.shouldUseDarkColors;
39
58
  for (const win of BrowserWindow.getAllWindows()) {
40
- if (!win.isDestroyed()) {
59
+ if (win.isDestroyed())
60
+ continue;
61
+ // Isolate each window: a window closing mid-loop (or a renderer torn down
62
+ // between the isDestroyed() check and the send) must not abort the sync
63
+ // for the remaining windows.
64
+ try {
41
65
  win.setBackgroundColor(bg);
66
+ // Notify renderer JS consumers that can't observe the CSS
67
+ // `prefers-color-scheme` change (Monaco's theme). Electron does not
68
+ // dispatch the renderer's matchMedia change event for programmatic
69
+ // `nativeTheme.themeSource` flips, so push it from here — the one place
70
+ // that already centralizes color-scheme reactions.
71
+ if (!win.webContents.isDestroyed()) {
72
+ win.webContents.send(WorkbenchSettingsChannel.ThemeChanged, isDark);
73
+ }
42
74
  }
75
+ catch { /* window/webContents gone mid-loop */ }
43
76
  }
44
77
  };
45
78
  nativeTheme.on('updated', apply);
@@ -4,6 +4,9 @@ export interface RenderHostUrlOptions {
4
4
  bridgeId: string;
5
5
  appId: string;
6
6
  pagePath: string;
7
+ /** Whether this page is a tabBar page. Surfaced on the URL so main can pick
8
+ * the bottom safe-area policy at `did-attach-webview` (services/safe-area). */
9
+ isTab?: boolean;
7
10
  }
8
11
  export interface DiminaNativeHostBridge {
9
12
  enabled: boolean;
@@ -55,6 +55,8 @@ function buildBridge(cfg) {
55
55
  url.searchParams.set('bridgeId', opts.bridgeId);
56
56
  url.searchParams.set('appId', opts.appId);
57
57
  url.searchParams.set('pagePath', opts.pagePath);
58
+ if (opts.isTab)
59
+ url.searchParams.set('isTab', '1');
58
60
  return url.toString();
59
61
  },
60
62
  renderPreloadUrl: cfg.renderPreloadUrl,