getobsrv 0.14.0 → 0.16.0
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/out/main/cli.js +2 -2
- package/out/main/index.js +158 -6
- package/out/main/{targetSource-W6YddYef.js → targetSource-BNB1X7ds.js} +2 -0
- package/out/preload/app.js +13 -0
- package/out/renderer/assets/{index-WM0cwf7d.js → index-BJmd2r3W.js} +231 -49
- package/out/renderer/assets/{index-BpvyvKfD.css → index-DUmCxwNL.css} +76 -9
- package/out/renderer/index.html +2 -2
- package/out/shared/ipcPayloads.js +50 -0
- package/package.json +1 -1
package/out/main/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ const electron = require("electron");
|
|
|
3
3
|
const node_fs = require("node:fs");
|
|
4
4
|
const node_os = require("node:os");
|
|
5
5
|
const node_path = require("node:path");
|
|
6
|
-
const targetSource = require("./targetSource-
|
|
6
|
+
const targetSource = require("./targetSource-BNB1X7ds.js");
|
|
7
7
|
function boxDownsample(src, factor) {
|
|
8
8
|
if (!Number.isInteger(factor) || factor < 1) throw new RangeError("factor must be an integer >= 1");
|
|
9
9
|
const width = Math.floor(src.width / factor);
|
|
@@ -256,7 +256,7 @@ ${usage()}`);
|
|
|
256
256
|
return { command, url, spec, profileId, outDir, waitMs, timeoutMs };
|
|
257
257
|
}
|
|
258
258
|
const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
259
|
-
const DEFAULT_SETTLE_MS =
|
|
259
|
+
const DEFAULT_SETTLE_MS = targetSource.SETTLE_QUIET_MS;
|
|
260
260
|
function uncoveredBounds(mask, width, height) {
|
|
261
261
|
const rowHasGap = (y) => {
|
|
262
262
|
const row = y * width;
|
package/out/main/index.js
CHANGED
|
@@ -4,7 +4,7 @@ const node_fs = require("node:fs");
|
|
|
4
4
|
const promises = require("node:fs/promises");
|
|
5
5
|
const node_path = require("node:path");
|
|
6
6
|
const node_crypto = require("node:crypto");
|
|
7
|
-
const targetSource = require("./targetSource-
|
|
7
|
+
const targetSource = require("./targetSource-BNB1X7ds.js");
|
|
8
8
|
const node_http = require("node:http");
|
|
9
9
|
const node_url = require("node:url");
|
|
10
10
|
const MAX_SCROLL_SELECTOR = 512;
|
|
@@ -146,6 +146,35 @@ function parseScrollReport(raw) {
|
|
|
146
146
|
const warnings = Array.isArray(raw.warnings) ? raw.warnings.filter((w) => typeof w === "string").slice(0, MAX_SCROLL_WARNINGS) : [];
|
|
147
147
|
return { id, x, y, scroller, warnings };
|
|
148
148
|
}
|
|
149
|
+
const MAX_MENU_OPTIONS = 200;
|
|
150
|
+
const MAX_MENU_LABEL = 120;
|
|
151
|
+
function parseMenuRequest(raw) {
|
|
152
|
+
if (!isRecord$3(raw)) return null;
|
|
153
|
+
const { groups, value, ariaLabel, anchor } = raw;
|
|
154
|
+
if (typeof value !== "string" || value.length > MAX_MENU_LABEL) return null;
|
|
155
|
+
if (typeof ariaLabel !== "string" || ariaLabel.length > MAX_MENU_LABEL) return null;
|
|
156
|
+
const rect = parseRect(anchor);
|
|
157
|
+
if (!rect) return null;
|
|
158
|
+
if (!Array.isArray(groups)) return null;
|
|
159
|
+
const out = [];
|
|
160
|
+
let rows = 0;
|
|
161
|
+
for (const g of groups) {
|
|
162
|
+
if (!isRecord$3(g)) return null;
|
|
163
|
+
if (g.label !== void 0 && (typeof g.label !== "string" || g.label.length > MAX_MENU_LABEL)) return null;
|
|
164
|
+
if (!Array.isArray(g.options)) return null;
|
|
165
|
+
const options = [];
|
|
166
|
+
for (const o of g.options) {
|
|
167
|
+
if (!isRecord$3(o)) return null;
|
|
168
|
+
if (typeof o.value !== "string" || o.value.length > MAX_MENU_LABEL) return null;
|
|
169
|
+
if (typeof o.label !== "string" || o.label.length > MAX_MENU_LABEL) return null;
|
|
170
|
+
if (++rows > MAX_MENU_OPTIONS) return null;
|
|
171
|
+
options.push({ value: o.value, label: o.label });
|
|
172
|
+
}
|
|
173
|
+
out.push(g.label === void 0 ? { options } : { label: g.label, options });
|
|
174
|
+
}
|
|
175
|
+
if (rows === 0) return null;
|
|
176
|
+
return { groups: out, value, ariaLabel, anchor: rect };
|
|
177
|
+
}
|
|
149
178
|
const CONTROL_FILE_NAME = "control.json";
|
|
150
179
|
const CONTROL_TOKEN_BYTES = 32;
|
|
151
180
|
const CONTROL_COMMANDS = [
|
|
@@ -308,6 +337,12 @@ const IPC = {
|
|
|
308
337
|
setViewport: "obsrv:set-viewport",
|
|
309
338
|
setNativeBounds: "obsrv:set-native-bounds",
|
|
310
339
|
setNativeVisible: "obsrv:set-native-visible",
|
|
340
|
+
/** Chrome -> main: open a menu in the overlay view. */
|
|
341
|
+
menuOpen: "obsrv:menu-open",
|
|
342
|
+
/** Main -> overlay: draw this menu. */
|
|
343
|
+
menuShow: "obsrv:menu-show",
|
|
344
|
+
/** Overlay -> main: a row was chosen, or the menu was dismissed. */
|
|
345
|
+
menuPick: "obsrv:menu-pick",
|
|
311
346
|
setMode: "obsrv:set-mode",
|
|
312
347
|
sendInput: "obsrv:send-input",
|
|
313
348
|
getHostInfo: "obsrv:get-host-info",
|
|
@@ -754,7 +789,7 @@ function hostInfo(win) {
|
|
|
754
789
|
}
|
|
755
790
|
}
|
|
756
791
|
function registerIpc(ctx) {
|
|
757
|
-
const { win, bus, tabs } = ctx;
|
|
792
|
+
const { win, bus, tabs, overlay } = ctx;
|
|
758
793
|
const tab = () => tabs.active();
|
|
759
794
|
const settingsFile = node_path.join(electron.app.getPath("userData"), "settings.json");
|
|
760
795
|
let settings = loadSettings(settingsFile);
|
|
@@ -838,6 +873,28 @@ function registerIpc(ctx) {
|
|
|
838
873
|
applyNativeVisibility();
|
|
839
874
|
bus.setEnabled(tab().modeIsLive);
|
|
840
875
|
});
|
|
876
|
+
let pendingMenu = null;
|
|
877
|
+
const settleMenu = (value) => {
|
|
878
|
+
const resolve = pendingMenu;
|
|
879
|
+
pendingMenu = null;
|
|
880
|
+
overlay.hide();
|
|
881
|
+
resolve?.(value);
|
|
882
|
+
};
|
|
883
|
+
handle(IPC.menuOpen, (e, raw) => {
|
|
884
|
+
if (!fromRenderer(e)) return null;
|
|
885
|
+
const request = parseMenuRequest(raw);
|
|
886
|
+
if (!request) return null;
|
|
887
|
+
settleMenu(null);
|
|
888
|
+
overlay.show(request);
|
|
889
|
+
return new Promise((resolve) => {
|
|
890
|
+
pendingMenu = resolve;
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
on(IPC.menuPick, (e, raw) => {
|
|
894
|
+
if (e.sender !== overlay.webContents) return;
|
|
895
|
+
if (raw !== null && typeof raw !== "string") return;
|
|
896
|
+
settleMenu(raw);
|
|
897
|
+
});
|
|
841
898
|
on(IPC.setNativeVisible, (e, raw) => {
|
|
842
899
|
if (!fromRenderer(e)) return;
|
|
843
900
|
if (typeof raw !== "boolean") return;
|
|
@@ -999,6 +1056,7 @@ function registerIpc(ctx) {
|
|
|
999
1056
|
const SETTLE_STABLE_READS = 2;
|
|
1000
1057
|
const SETTLE_BUDGET_MS = 4e3;
|
|
1001
1058
|
const SETTLE_DRAW_MS = 120;
|
|
1059
|
+
const SETTLE_QUIET_BUDGET_MS = 3e3;
|
|
1002
1060
|
const nextFrame = (t, budgetMs) => new Promise((resolve) => {
|
|
1003
1061
|
if (!t.painting) {
|
|
1004
1062
|
resolve(false);
|
|
@@ -1016,6 +1074,26 @@ function registerIpc(ctx) {
|
|
|
1016
1074
|
t.on("frame", onFrame);
|
|
1017
1075
|
t.invalidate();
|
|
1018
1076
|
});
|
|
1077
|
+
const quiesce = (t, budgetMs) => new Promise((resolve) => {
|
|
1078
|
+
if (!t.painting) {
|
|
1079
|
+
resolve(false);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
let quiet;
|
|
1083
|
+
const done = (ok) => {
|
|
1084
|
+
clearTimeout(quiet);
|
|
1085
|
+
clearTimeout(cap);
|
|
1086
|
+
t.off("frame", onFrame);
|
|
1087
|
+
resolve(ok);
|
|
1088
|
+
};
|
|
1089
|
+
const onFrame = () => {
|
|
1090
|
+
clearTimeout(quiet);
|
|
1091
|
+
quiet = setTimeout(() => done(true), targetSource.SETTLE_QUIET_MS);
|
|
1092
|
+
};
|
|
1093
|
+
const cap = setTimeout(() => done(false), budgetMs);
|
|
1094
|
+
t.on("frame", onFrame);
|
|
1095
|
+
quiet = setTimeout(() => done(true), targetSource.SETTLE_QUIET_MS);
|
|
1096
|
+
});
|
|
1019
1097
|
const settleTarget = async (t = tab().target) => {
|
|
1020
1098
|
const deadline = Date.now() + SETTLE_BUDGET_MS;
|
|
1021
1099
|
let last = "";
|
|
@@ -1028,10 +1106,11 @@ function registerIpc(ctx) {
|
|
|
1028
1106
|
if (stable >= SETTLE_STABLE_READS) break;
|
|
1029
1107
|
await new Promise((r) => setTimeout(r, SETTLE_POLL_MS));
|
|
1030
1108
|
}
|
|
1031
|
-
if (stable < SETTLE_STABLE_READS) return
|
|
1109
|
+
if (stable < SETTLE_STABLE_READS) return "resizing";
|
|
1032
1110
|
const painted = await nextFrame(t, Math.max(0, deadline - Date.now()));
|
|
1111
|
+
const quiet = painted && await quiesce(t, SETTLE_QUIET_BUDGET_MS);
|
|
1033
1112
|
await new Promise((r) => setTimeout(r, SETTLE_DRAW_MS));
|
|
1034
|
-
return
|
|
1113
|
+
return quiet ? "settled" : "painting";
|
|
1035
1114
|
};
|
|
1036
1115
|
const roundRect = (r) => ({
|
|
1037
1116
|
x: Math.round(r.x),
|
|
@@ -1260,7 +1339,8 @@ function registerIpc(ctx) {
|
|
|
1260
1339
|
const warnings = [];
|
|
1261
1340
|
if (!known) warnings.push("the renderer has not reported the pane bounds yet; captured the full window instead");
|
|
1262
1341
|
else if (canvasBounds === null) warnings.push("the renderer has not reported the render bounds yet; captured the whole pane instead");
|
|
1263
|
-
if (
|
|
1342
|
+
if (settled === "resizing") warnings.push("the target was still resizing when the capture budget ran out; the PNG may show a transitional frame");
|
|
1343
|
+
else if (settled === "painting") warnings.push("the page was still painting when the capture budget ran out; the PNG may show a transitional frame — an animation, or a load that had not finished");
|
|
1264
1344
|
return {
|
|
1265
1345
|
data: image.toPNG().toString("base64"),
|
|
1266
1346
|
width: size.width,
|
|
@@ -1410,6 +1490,76 @@ function installMenu({ win, tabs }) {
|
|
|
1410
1490
|
];
|
|
1411
1491
|
electron.Menu.setApplicationMenu(electron.Menu.buildFromTemplate(template));
|
|
1412
1492
|
}
|
|
1493
|
+
class Overlay {
|
|
1494
|
+
constructor(win) {
|
|
1495
|
+
this.win = win;
|
|
1496
|
+
this.view = new electron.WebContentsView({
|
|
1497
|
+
webPreferences: {
|
|
1498
|
+
preload: node_path.join(__dirname, "../preload/app.js"),
|
|
1499
|
+
contextIsolation: true,
|
|
1500
|
+
sandbox: true,
|
|
1501
|
+
// Transparency has to be asked for at construction; setting the
|
|
1502
|
+
// background colour afterwards is not enough to keep the page's own
|
|
1503
|
+
// white from painting over the panes.
|
|
1504
|
+
transparent: true
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1507
|
+
this.view.setBackgroundColor("#00000000");
|
|
1508
|
+
this.win.contentView.addChildView(this.view);
|
|
1509
|
+
this.view.setVisible(false);
|
|
1510
|
+
this.layout();
|
|
1511
|
+
if (!electron.app.isPackaged && process.env.ELECTRON_RENDERER_URL)
|
|
1512
|
+
void this.view.webContents.loadURL(`${process.env.ELECTRON_RENDERER_URL}?overlay=1`);
|
|
1513
|
+
else
|
|
1514
|
+
void this.view.webContents.loadFile(node_path.join(__dirname, "../renderer/index.html"), {
|
|
1515
|
+
query: { overlay: "1" }
|
|
1516
|
+
});
|
|
1517
|
+
this.win.on("resize", () => this.layout());
|
|
1518
|
+
}
|
|
1519
|
+
view;
|
|
1520
|
+
open = false;
|
|
1521
|
+
destroyed = false;
|
|
1522
|
+
get webContents() {
|
|
1523
|
+
return this.view.webContents;
|
|
1524
|
+
}
|
|
1525
|
+
/** Covers the whole content area: the menu positions itself within it, and
|
|
1526
|
+
* the rest of the surface is what catches a click meant to dismiss. */
|
|
1527
|
+
layout() {
|
|
1528
|
+
const [width = 0, height = 0] = this.win.getContentSize();
|
|
1529
|
+
this.view.setBounds({ x: 0, y: 0, width, height });
|
|
1530
|
+
}
|
|
1531
|
+
show(request) {
|
|
1532
|
+
this.win.contentView.addChildView(this.view);
|
|
1533
|
+
this.layout();
|
|
1534
|
+
this.view.setVisible(true);
|
|
1535
|
+
this.open = true;
|
|
1536
|
+
this.view.webContents.focus();
|
|
1537
|
+
this.view.webContents.send(IPC.menuShow, request);
|
|
1538
|
+
}
|
|
1539
|
+
hide() {
|
|
1540
|
+
if (!this.open) return;
|
|
1541
|
+
this.open = false;
|
|
1542
|
+
this.view.setVisible(false);
|
|
1543
|
+
this.win.webContents.focus();
|
|
1544
|
+
}
|
|
1545
|
+
get isOpen() {
|
|
1546
|
+
return this.open;
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Idempotent, because the window's `close` handler is not guaranteed to run
|
|
1550
|
+
* once: quit.spec.ts emits `close` by hand to drive the teardown while the
|
|
1551
|
+
* window is still alive, and the real close then runs it again. A throw on
|
|
1552
|
+
* the second pass would take the rest of that handler with it — including the
|
|
1553
|
+
* destroy of the offscreen windows, without which `window-all-closed` never
|
|
1554
|
+
* fires and the app hangs instead of quitting.
|
|
1555
|
+
*/
|
|
1556
|
+
destroy() {
|
|
1557
|
+
if (this.destroyed) return;
|
|
1558
|
+
this.destroyed = true;
|
|
1559
|
+
this.win.contentView.removeChildView(this.view);
|
|
1560
|
+
if (!this.view.webContents.isDestroyed()) this.view.webContents.close();
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1413
1563
|
function closeTab(tabs, closeId, activeId) {
|
|
1414
1564
|
const index = tabs.findIndex((t) => t.id === closeId);
|
|
1415
1565
|
if (index === -1) return { tabs, activeId };
|
|
@@ -1967,12 +2117,14 @@ function createMainWindow() {
|
|
|
1967
2117
|
function boot() {
|
|
1968
2118
|
const win = createMainWindow();
|
|
1969
2119
|
const tabs = new TabManager(win);
|
|
1970
|
-
const
|
|
2120
|
+
const overlay = new Overlay(win);
|
|
2121
|
+
const ctx = { win, tabs, bus: tabs.bus, toolbarH: TOOLBAR_H, overlay };
|
|
1971
2122
|
const stopIpc = registerIpc(ctx);
|
|
1972
2123
|
installMenu(ctx);
|
|
1973
2124
|
win.on("close", () => {
|
|
1974
2125
|
stopIpc();
|
|
1975
2126
|
tabs.destroy();
|
|
2127
|
+
overlay.destroy();
|
|
1976
2128
|
});
|
|
1977
2129
|
exposeForTests(ctx);
|
|
1978
2130
|
}
|
|
@@ -88,6 +88,7 @@ function isFullFrame(dirty, frameWidth, frameHeight, deviceScaleFactor) {
|
|
|
88
88
|
function fitsFrame(dirty, frameWidth, frameHeight) {
|
|
89
89
|
return dirty.x >= 0 && dirty.y >= 0 && dirty.width > 0 && dirty.height > 0 && dirty.x + dirty.width <= frameWidth && dirty.y + dirty.height <= frameHeight;
|
|
90
90
|
}
|
|
91
|
+
const SETTLE_QUIET_MS = 400;
|
|
91
92
|
const SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
92
93
|
const LOOPBACK = /^(localhost|127\.0\.0\.1|\[::1\])(:\d+)?(\/|$)/i;
|
|
93
94
|
function normalizeUrl(input) {
|
|
@@ -427,6 +428,7 @@ exports.MAX_TABS_MAX = MAX_TABS_MAX;
|
|
|
427
428
|
exports.MAX_TABS_MIN = MAX_TABS_MIN;
|
|
428
429
|
exports.PANEL_PROFILES = PANEL_PROFILES;
|
|
429
430
|
exports.SCREEN_PRESETS = SCREEN_PRESETS;
|
|
431
|
+
exports.SETTLE_QUIET_MS = SETTLE_QUIET_MS;
|
|
430
432
|
exports.SPLIT_MAX = SPLIT_MAX;
|
|
431
433
|
exports.SPLIT_MIN = SPLIT_MIN;
|
|
432
434
|
exports.TargetSource = TargetSource;
|
package/out/preload/app.js
CHANGED
|
@@ -8,6 +8,12 @@ const IPC = {
|
|
|
8
8
|
setViewport: "obsrv:set-viewport",
|
|
9
9
|
setNativeBounds: "obsrv:set-native-bounds",
|
|
10
10
|
setNativeVisible: "obsrv:set-native-visible",
|
|
11
|
+
/** Chrome -> main: open a menu in the overlay view. */
|
|
12
|
+
menuOpen: "obsrv:menu-open",
|
|
13
|
+
/** Main -> overlay: draw this menu. */
|
|
14
|
+
menuShow: "obsrv:menu-show",
|
|
15
|
+
/** Overlay -> main: a row was chosen, or the menu was dismissed. */
|
|
16
|
+
menuPick: "obsrv:menu-pick",
|
|
11
17
|
setMode: "obsrv:set-mode",
|
|
12
18
|
sendInput: "obsrv:send-input",
|
|
13
19
|
getHostInfo: "obsrv:get-host-info",
|
|
@@ -69,6 +75,13 @@ const api = {
|
|
|
69
75
|
setViewport: (width, height, deviceScaleFactor) => electron.ipcRenderer.invoke(IPC.setViewport, width, height, deviceScaleFactor),
|
|
70
76
|
setNativeBounds: (rect) => electron.ipcRenderer.send(IPC.setNativeBounds, rect),
|
|
71
77
|
setNativeVisible: (visible) => electron.ipcRenderer.send(IPC.setNativeVisible, visible),
|
|
78
|
+
openMenu: (request) => electron.ipcRenderer.invoke(IPC.menuOpen, request),
|
|
79
|
+
onMenuShow: (fn) => {
|
|
80
|
+
const h = (_e, request) => fn(request);
|
|
81
|
+
electron.ipcRenderer.on(IPC.menuShow, h);
|
|
82
|
+
return () => electron.ipcRenderer.off(IPC.menuShow, h);
|
|
83
|
+
},
|
|
84
|
+
pickMenu: (value) => electron.ipcRenderer.send(IPC.menuPick, value),
|
|
72
85
|
setMode: (mode) => electron.ipcRenderer.send(IPC.setMode, mode),
|
|
73
86
|
sendInput: (ev) => electron.ipcRenderer.send(IPC.sendInput, ev),
|
|
74
87
|
getHostInfo: () => electron.ipcRenderer.invoke(IPC.getHostInfo),
|
|
@@ -13554,6 +13554,11 @@ function NumberField({ className, label, unit, value, min, step, onCommit, onInv
|
|
|
13554
13554
|
)
|
|
13555
13555
|
] });
|
|
13556
13556
|
}
|
|
13557
|
+
const SURROUNDS = [
|
|
13558
|
+
{ id: "black", label: "Black surround", swatch: "#000000" },
|
|
13559
|
+
{ id: "graphite", label: "Graphite surround", swatch: "#2a2a2a" },
|
|
13560
|
+
{ id: "grey50", label: "Neutral 50% surround", swatch: "#808080" }
|
|
13561
|
+
];
|
|
13557
13562
|
function SettingsPanel() {
|
|
13558
13563
|
const host = useStore(useShallow((s) => s.host));
|
|
13559
13564
|
const settings = useStore(useShallow((s) => s.settings));
|
|
@@ -13566,6 +13571,8 @@ function SettingsPanel() {
|
|
|
13566
13571
|
const fallback = useStore(selectScaleIsFallback);
|
|
13567
13572
|
const setSettings = useStore((s) => s.setSettings);
|
|
13568
13573
|
const setCustom = useStore((s) => s.setCustom);
|
|
13574
|
+
const surround = useStore((s) => s.surround);
|
|
13575
|
+
const setSurround = useStore((s) => s.setSurround);
|
|
13569
13576
|
const [hostError, setHostError] = reactExports.useState(null);
|
|
13570
13577
|
const [customError, setCustomError] = reactExports.useState(null);
|
|
13571
13578
|
const queue = reactExports.useRef(Promise.resolve());
|
|
@@ -13688,6 +13695,20 @@ function SettingsPanel() {
|
|
|
13688
13695
|
"."
|
|
13689
13696
|
] }),
|
|
13690
13697
|
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "readout", children: custom.diagonalInches > 0 ? `${ppi(custom.width, custom.height, custom.diagonalInches).toFixed(0)} PPI` : "Enter a diagonal to compute PPI" }),
|
|
13698
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("h2", { children: "Pane surround" }),
|
|
13699
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "hint", children: "What the panes are filled with around a render. It changes nothing in the render itself, but the same pixels read lighter or darker against a different ground — which is why it is a choice and not a constant. Black for judging a dark UI, 50% grey for a neutral reference." }),
|
|
13700
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "surround-control", role: "group", "aria-label": "Pane surround", children: SURROUNDS.map((s) => /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
13701
|
+
"button",
|
|
13702
|
+
{
|
|
13703
|
+
type: "button",
|
|
13704
|
+
title: s.label,
|
|
13705
|
+
"aria-label": s.label,
|
|
13706
|
+
"aria-pressed": surround === s.id,
|
|
13707
|
+
onClick: () => setSurround(s.id),
|
|
13708
|
+
children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "surround-swatch", style: { background: s.swatch } })
|
|
13709
|
+
},
|
|
13710
|
+
s.id
|
|
13711
|
+
)) }),
|
|
13691
13712
|
/* @__PURE__ */ jsxRuntimeExports.jsx("h2", { children: "Updates" }),
|
|
13692
13713
|
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "version-block", children: [
|
|
13693
13714
|
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "version-row", children: [
|
|
@@ -14829,22 +14850,30 @@ const createLucideIcon = (iconName, iconNode) => {
|
|
|
14829
14850
|
* This source code is licensed under the ISC license.
|
|
14830
14851
|
* See the LICENSE file in the root directory of this source tree.
|
|
14831
14852
|
*/
|
|
14832
|
-
const __iconNode$
|
|
14853
|
+
const __iconNode$b = [
|
|
14833
14854
|
["path", { d: "m12 19-7-7 7-7", key: "1l729n" }],
|
|
14834
14855
|
["path", { d: "M19 12H5", key: "x3x0zl" }]
|
|
14835
14856
|
];
|
|
14836
|
-
const ArrowLeft = createLucideIcon("arrow-left", __iconNode$
|
|
14857
|
+
const ArrowLeft = createLucideIcon("arrow-left", __iconNode$b);
|
|
14837
14858
|
/**
|
|
14838
14859
|
* @license lucide-react v1.34.0 - ISC
|
|
14839
14860
|
*
|
|
14840
14861
|
* This source code is licensed under the ISC license.
|
|
14841
14862
|
* See the LICENSE file in the root directory of this source tree.
|
|
14842
14863
|
*/
|
|
14843
|
-
const __iconNode$
|
|
14864
|
+
const __iconNode$a = [
|
|
14844
14865
|
["path", { d: "M5 12h14", key: "1ays0h" }],
|
|
14845
14866
|
["path", { d: "m12 5 7 7-7 7", key: "xquz4c" }]
|
|
14846
14867
|
];
|
|
14847
|
-
const ArrowRight = createLucideIcon("arrow-right", __iconNode$
|
|
14868
|
+
const ArrowRight = createLucideIcon("arrow-right", __iconNode$a);
|
|
14869
|
+
/**
|
|
14870
|
+
* @license lucide-react v1.34.0 - ISC
|
|
14871
|
+
*
|
|
14872
|
+
* This source code is licensed under the ISC license.
|
|
14873
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
14874
|
+
*/
|
|
14875
|
+
const __iconNode$9 = [["path", { d: "M20 6 9 17l-5-5", key: "1gmf2c" }]];
|
|
14876
|
+
const Check = createLucideIcon("check", __iconNode$9);
|
|
14848
14877
|
/**
|
|
14849
14878
|
* @license lucide-react v1.34.0 - ISC
|
|
14850
14879
|
*
|
|
@@ -14962,6 +14991,8 @@ const ICONS = {
|
|
|
14962
14991
|
sliders: SlidersHorizontal,
|
|
14963
14992
|
gear: Settings,
|
|
14964
14993
|
chevron: ChevronDown,
|
|
14994
|
+
// The listbox's tick. Marks the chosen row the way the native menu did.
|
|
14995
|
+
check: Check,
|
|
14965
14996
|
plus: Plus,
|
|
14966
14997
|
// The rotate control's two shapes. A plain outline of the screen you get is
|
|
14967
14998
|
// the whole affordance — no arrow, no device silhouette: the target may be a
|
|
@@ -15109,27 +15140,54 @@ Agent control is driving this tab` : label,
|
|
|
15109
15140
|
)
|
|
15110
15141
|
] });
|
|
15111
15142
|
}
|
|
15112
|
-
function Select({ className, value, label, ariaLabel,
|
|
15113
|
-
|
|
15114
|
-
|
|
15115
|
-
|
|
15116
|
-
|
|
15117
|
-
|
|
15118
|
-
|
|
15119
|
-
|
|
15120
|
-
|
|
15121
|
-
|
|
15122
|
-
|
|
15123
|
-
|
|
15124
|
-
|
|
15125
|
-
|
|
15126
|
-
|
|
15143
|
+
function Select({ className, value, label, ariaLabel, groups, onChange }) {
|
|
15144
|
+
const triggerRef = reactExports.useRef(null);
|
|
15145
|
+
const [open, setOpen] = reactExports.useState(false);
|
|
15146
|
+
const openMenu = async () => {
|
|
15147
|
+
const el = triggerRef.current;
|
|
15148
|
+
if (!el || open) return;
|
|
15149
|
+
const r = el.getBoundingClientRect();
|
|
15150
|
+
setOpen(true);
|
|
15151
|
+
const picked = await window.obsrv.openMenu({
|
|
15152
|
+
groups,
|
|
15153
|
+
value,
|
|
15154
|
+
ariaLabel,
|
|
15155
|
+
// Window coordinates: the overlay spans the content area, so the menu
|
|
15156
|
+
// needs no conversion to place itself under this control.
|
|
15157
|
+
anchor: { x: r.left, y: r.top, width: r.width, height: r.height }
|
|
15158
|
+
});
|
|
15159
|
+
setOpen(false);
|
|
15160
|
+
el.focus();
|
|
15161
|
+
if (picked !== null) onChange(picked);
|
|
15162
|
+
};
|
|
15163
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "select-shell", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
|
|
15164
|
+
"button",
|
|
15165
|
+
{
|
|
15166
|
+
type: "button",
|
|
15167
|
+
ref: triggerRef,
|
|
15168
|
+
className: `select-trigger ${className}`,
|
|
15169
|
+
role: "combobox",
|
|
15170
|
+
"aria-haspopup": "listbox",
|
|
15171
|
+
"aria-expanded": open,
|
|
15172
|
+
"aria-label": ariaLabel,
|
|
15173
|
+
"data-value": value,
|
|
15174
|
+
onClick: () => void openMenu(),
|
|
15175
|
+
onKeyDown: (e) => {
|
|
15176
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
15177
|
+
e.preventDefault();
|
|
15178
|
+
void openMenu();
|
|
15179
|
+
}
|
|
15180
|
+
},
|
|
15181
|
+
children: [
|
|
15182
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "select-label", children: [
|
|
15183
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "select-label-text", children: label }),
|
|
15184
|
+
groups.flatMap((g) => g.options).map((o) => /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "select-label-sizer", "aria-hidden": "true", children: o.label }, o.value))
|
|
15185
|
+
] }),
|
|
15186
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "select-chevron", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Icon, { name: "chevron", size: 14 }) })
|
|
15187
|
+
]
|
|
15188
|
+
}
|
|
15189
|
+
) });
|
|
15127
15190
|
}
|
|
15128
|
-
const SURROUNDS = [
|
|
15129
|
-
{ id: "black", label: "Black surround", swatch: "#000000" },
|
|
15130
|
-
{ id: "graphite", label: "Graphite surround", swatch: "#2a2a2a" },
|
|
15131
|
-
{ id: "grey50", label: "Neutral 50% surround", swatch: "#808080" }
|
|
15132
|
-
];
|
|
15133
15191
|
const VIEWS = [
|
|
15134
15192
|
{ id: "1:1", label: "1:1", title: "Actual size" },
|
|
15135
15193
|
{ id: "fit", label: "Fit", title: "Fit the pane — not pixel-exact" }
|
|
@@ -15138,6 +15196,17 @@ const PANES = [
|
|
|
15138
15196
|
{ id: "both", label: "Both", title: "Native and target side by side" },
|
|
15139
15197
|
{ id: "target", label: "Target", title: "The target render alone, full width" }
|
|
15140
15198
|
];
|
|
15199
|
+
const inGroup = (group) => SCREEN_PRESETS.filter((p) => p.group === group).map((p) => ({ value: p.id, label: p.label }));
|
|
15200
|
+
const PRESET_GROUPS = [
|
|
15201
|
+
{ label: "Laptops", options: inGroup("laptop") },
|
|
15202
|
+
{ label: "Desktops", options: inGroup("desktop") },
|
|
15203
|
+
{ label: "Mobile", options: inGroup("mobile") },
|
|
15204
|
+
// Ungrouped, as it was: "Custom" is not one more screen among the presets.
|
|
15205
|
+
{ options: [{ value: CUSTOM_PRESET_ID, label: "Custom" }] }
|
|
15206
|
+
];
|
|
15207
|
+
const PROFILE_GROUPS = [
|
|
15208
|
+
{ options: PANEL_PROFILES.map((p) => ({ value: p.id, label: p.label })) }
|
|
15209
|
+
];
|
|
15141
15210
|
function Toolbar({ drawer, onTogglePanel, onToggleSettings }) {
|
|
15142
15211
|
const mode = useStore((s) => selectTab(s).mode);
|
|
15143
15212
|
const presetId = useStore((s) => selectTab(s).presetId);
|
|
@@ -15156,9 +15225,7 @@ function Toolbar({ drawer, onTogglePanel, onToggleSettings }) {
|
|
|
15156
15225
|
const setProfile = useStore((s) => s.setProfile);
|
|
15157
15226
|
const setPixelExact = useStore((s) => s.setPixelExact);
|
|
15158
15227
|
const setError = useStore((s) => s.setError);
|
|
15159
|
-
const surround = useStore((s) => s.surround);
|
|
15160
15228
|
const update = useStore((s) => s.update);
|
|
15161
|
-
const setSurround = useStore((s) => s.setSurround);
|
|
15162
15229
|
const viewMode = useStore((s) => selectTab(s).viewMode);
|
|
15163
15230
|
const setViewMode = useStore((s) => s.setViewMode);
|
|
15164
15231
|
const panes = useStore((s) => s.panes);
|
|
@@ -15409,20 +15476,15 @@ function Toolbar({ drawer, onTogglePanel, onToggleSettings }) {
|
|
|
15409
15476
|
] }) })
|
|
15410
15477
|
] }),
|
|
15411
15478
|
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "chrome-row chrome-screen", children: [
|
|
15412
|
-
/* @__PURE__ */ jsxRuntimeExports.
|
|
15479
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
15413
15480
|
Select,
|
|
15414
15481
|
{
|
|
15415
15482
|
className: "preset-select",
|
|
15416
15483
|
value: presetId,
|
|
15417
15484
|
label: presetLabel,
|
|
15418
15485
|
ariaLabel: "Target screen",
|
|
15419
|
-
|
|
15420
|
-
|
|
15421
|
-
/* @__PURE__ */ jsxRuntimeExports.jsx("optgroup", { label: "Laptops", children: SCREEN_PRESETS.filter((p) => p.group === "laptop").map((p) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: p.id, children: p.label }, p.id)) }),
|
|
15422
|
-
/* @__PURE__ */ jsxRuntimeExports.jsx("optgroup", { label: "Desktops", children: SCREEN_PRESETS.filter((p) => p.group === "desktop").map((p) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: p.id, children: p.label }, p.id)) }),
|
|
15423
|
-
/* @__PURE__ */ jsxRuntimeExports.jsx("optgroup", { label: "Mobile", children: SCREEN_PRESETS.filter((p) => p.group === "mobile").map((p) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: p.id, children: p.label }, p.id)) }),
|
|
15424
|
-
/* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: CUSTOM_PRESET_ID, children: "Custom" })
|
|
15425
|
-
]
|
|
15486
|
+
groups: PRESET_GROUPS,
|
|
15487
|
+
onChange: setPreset
|
|
15426
15488
|
}
|
|
15427
15489
|
),
|
|
15428
15490
|
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "orientation-control", role: "group", "aria-label": "Screen orientation", children: ORIENTATIONS.map((value, i) => {
|
|
@@ -15469,22 +15531,10 @@ function Toolbar({ drawer, onTogglePanel, onToggleSettings }) {
|
|
|
15469
15531
|
value: profileId,
|
|
15470
15532
|
label: profileLabel,
|
|
15471
15533
|
ariaLabel: "Panel profile",
|
|
15472
|
-
|
|
15473
|
-
|
|
15534
|
+
groups: PROFILE_GROUPS,
|
|
15535
|
+
onChange: setProfile
|
|
15474
15536
|
}
|
|
15475
15537
|
),
|
|
15476
|
-
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "surround-control", role: "group", "aria-label": "Pane surround", children: SURROUNDS.map((s) => /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
15477
|
-
"button",
|
|
15478
|
-
{
|
|
15479
|
-
type: "button",
|
|
15480
|
-
title: s.label,
|
|
15481
|
-
"aria-label": s.label,
|
|
15482
|
-
"aria-pressed": surround === s.id,
|
|
15483
|
-
onClick: () => setSurround(s.id),
|
|
15484
|
-
children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "surround-swatch", style: { background: s.swatch } })
|
|
15485
|
-
},
|
|
15486
|
-
s.id
|
|
15487
|
-
)) }),
|
|
15488
15538
|
agentControl && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `agent-activity${agentActive ? " active" : ""}`, children: "AGENT" })
|
|
15489
15539
|
] })
|
|
15490
15540
|
] });
|
|
@@ -15733,6 +15783,134 @@ function App() {
|
|
|
15733
15783
|
)
|
|
15734
15784
|
);
|
|
15735
15785
|
}
|
|
15786
|
+
const MARGIN = 8;
|
|
15787
|
+
const TYPEAHEAD_MS = 700;
|
|
15788
|
+
function MenuLayer() {
|
|
15789
|
+
const [request, setRequest] = reactExports.useState(null);
|
|
15790
|
+
const [active, setActive] = reactExports.useState(0);
|
|
15791
|
+
const [pos, setPos] = reactExports.useState(null);
|
|
15792
|
+
const listRef = reactExports.useRef(null);
|
|
15793
|
+
const typed = reactExports.useRef({ text: "", at: 0 });
|
|
15794
|
+
const flat = request?.groups.flatMap((g) => g.options) ?? [];
|
|
15795
|
+
reactExports.useEffect(() => window.obsrv.onMenuShow(setRequest), []);
|
|
15796
|
+
reactExports.useEffect(() => {
|
|
15797
|
+
if (!request) return;
|
|
15798
|
+
setActive(Math.max(0, flat.findIndex((o) => o.value === request.value)));
|
|
15799
|
+
}, [request]);
|
|
15800
|
+
const finish = (value) => {
|
|
15801
|
+
window.obsrv.pickMenu(value);
|
|
15802
|
+
setRequest(null);
|
|
15803
|
+
setPos(null);
|
|
15804
|
+
};
|
|
15805
|
+
reactExports.useLayoutEffect(() => {
|
|
15806
|
+
if (!request) return;
|
|
15807
|
+
const list = listRef.current;
|
|
15808
|
+
if (!list) return;
|
|
15809
|
+
const a = request.anchor;
|
|
15810
|
+
const below = window.innerHeight - (a.y + a.height) - MARGIN;
|
|
15811
|
+
const above = a.y - MARGIN;
|
|
15812
|
+
const wanted = list.scrollHeight;
|
|
15813
|
+
const dropDown = below >= wanted || below >= above;
|
|
15814
|
+
const maxHeight = Math.max(80, dropDown ? below : above);
|
|
15815
|
+
const height = Math.min(wanted, maxHeight);
|
|
15816
|
+
const top = dropDown ? a.y + a.height : a.y - height;
|
|
15817
|
+
const left = Math.max(MARGIN, Math.min(a.x, window.innerWidth - list.offsetWidth - MARGIN));
|
|
15818
|
+
setPos({ left, top, maxHeight });
|
|
15819
|
+
}, [request]);
|
|
15820
|
+
reactExports.useEffect(() => {
|
|
15821
|
+
if (!request) return;
|
|
15822
|
+
const move = (to) => {
|
|
15823
|
+
const next = Math.max(0, Math.min(flat.length - 1, to));
|
|
15824
|
+
setActive(next);
|
|
15825
|
+
listRef.current?.querySelector(`[data-index="${next}"]`)?.scrollIntoView({ block: "nearest" });
|
|
15826
|
+
};
|
|
15827
|
+
const onKey = (e) => {
|
|
15828
|
+
switch (e.key) {
|
|
15829
|
+
case "Escape":
|
|
15830
|
+
e.preventDefault();
|
|
15831
|
+
return finish(null);
|
|
15832
|
+
case "ArrowDown":
|
|
15833
|
+
e.preventDefault();
|
|
15834
|
+
return move(active + 1);
|
|
15835
|
+
case "ArrowUp":
|
|
15836
|
+
e.preventDefault();
|
|
15837
|
+
return move(active - 1);
|
|
15838
|
+
case "Home":
|
|
15839
|
+
e.preventDefault();
|
|
15840
|
+
return move(0);
|
|
15841
|
+
case "End":
|
|
15842
|
+
e.preventDefault();
|
|
15843
|
+
return move(flat.length - 1);
|
|
15844
|
+
case "Tab":
|
|
15845
|
+
e.preventDefault();
|
|
15846
|
+
return finish(null);
|
|
15847
|
+
case "Enter":
|
|
15848
|
+
case " ":
|
|
15849
|
+
e.preventDefault();
|
|
15850
|
+
return finish(flat[active]?.value ?? null);
|
|
15851
|
+
}
|
|
15852
|
+
if (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
|
15853
|
+
const now = Date.now();
|
|
15854
|
+
typed.current.text = now - typed.current.at > TYPEAHEAD_MS ? e.key : typed.current.text + e.key;
|
|
15855
|
+
typed.current.at = now;
|
|
15856
|
+
const q = typed.current.text.toLowerCase();
|
|
15857
|
+
const hit = flat.findIndex((o) => o.label.toLowerCase().startsWith(q));
|
|
15858
|
+
if (hit >= 0) move(hit);
|
|
15859
|
+
}
|
|
15860
|
+
};
|
|
15861
|
+
window.addEventListener("keydown", onKey);
|
|
15862
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
15863
|
+
}, [request, active, flat.length]);
|
|
15864
|
+
reactExports.useEffect(() => {
|
|
15865
|
+
if (!request || !pos) return;
|
|
15866
|
+
listRef.current?.querySelector(`[data-index="${active}"]`)?.scrollIntoView({ block: "nearest" });
|
|
15867
|
+
}, [pos]);
|
|
15868
|
+
if (!request) return null;
|
|
15869
|
+
let index = -1;
|
|
15870
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "menu-backdrop", onMouseDown: () => finish(null), children: /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
15871
|
+
"div",
|
|
15872
|
+
{
|
|
15873
|
+
ref: listRef,
|
|
15874
|
+
className: "select-menu",
|
|
15875
|
+
role: "listbox",
|
|
15876
|
+
"aria-label": request.ariaLabel,
|
|
15877
|
+
style: pos ? { left: pos.left, top: pos.top, maxHeight: pos.maxHeight } : (
|
|
15878
|
+
// Hidden until measured: the first pass needs the real height, and
|
|
15879
|
+
// a menu that flashes in the wrong place is worse than one that
|
|
15880
|
+
// appears a frame later.
|
|
15881
|
+
{ visibility: "hidden", top: 0, left: 0 }
|
|
15882
|
+
),
|
|
15883
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
15884
|
+
children: request.groups.map((g, gi) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "select-group", role: "group", "aria-label": g.label, children: [
|
|
15885
|
+
g.label && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "select-group-label", "aria-hidden": "true", children: g.label }),
|
|
15886
|
+
g.options.map((o) => {
|
|
15887
|
+
index += 1;
|
|
15888
|
+
const i = index;
|
|
15889
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs(
|
|
15890
|
+
"div",
|
|
15891
|
+
{
|
|
15892
|
+
role: "option",
|
|
15893
|
+
"data-index": i,
|
|
15894
|
+
"data-value": o.value,
|
|
15895
|
+
className: `select-option${i === active ? " active" : ""}`,
|
|
15896
|
+
"aria-selected": o.value === request.value,
|
|
15897
|
+
onMouseDown: (e) => {
|
|
15898
|
+
e.stopPropagation();
|
|
15899
|
+
finish(o.value);
|
|
15900
|
+
},
|
|
15901
|
+
onMouseEnter: () => setActive(i),
|
|
15902
|
+
children: [
|
|
15903
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "select-tick", "aria-hidden": "true", children: o.value === request.value ? /* @__PURE__ */ jsxRuntimeExports.jsx(Icon, { name: "check", size: 12 }) : null }),
|
|
15904
|
+
o.label
|
|
15905
|
+
]
|
|
15906
|
+
},
|
|
15907
|
+
o.value
|
|
15908
|
+
);
|
|
15909
|
+
})
|
|
15910
|
+
] }, g.label ?? gi))
|
|
15911
|
+
}
|
|
15912
|
+
) });
|
|
15913
|
+
}
|
|
15736
15914
|
function BrowserNotice() {
|
|
15737
15915
|
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "browser-notice", role: "note", children: [
|
|
15738
15916
|
/* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "This is only Obsrv's renderer shell." }),
|
|
@@ -15751,4 +15929,8 @@ function BrowserNotice() {
|
|
|
15751
15929
|
] })
|
|
15752
15930
|
] });
|
|
15753
15931
|
}
|
|
15754
|
-
|
|
15932
|
+
const overlay = new URLSearchParams(location.search).has("overlay");
|
|
15933
|
+
if (overlay) document.documentElement.dataset.overlay = "";
|
|
15934
|
+
clientExports.createRoot(document.getElementById("root")).render(
|
|
15935
|
+
!("obsrv" in window) ? /* @__PURE__ */ jsxRuntimeExports.jsx(BrowserNotice, {}) : overlay ? /* @__PURE__ */ jsxRuntimeExports.jsx(MenuLayer, {}) : /* @__PURE__ */ jsxRuntimeExports.jsx(App, {})
|
|
15936
|
+
);
|
|
@@ -421,7 +421,10 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
421
421
|
/* Our surface, the platform's popup: the real select sits transparent on
|
|
422
422
|
top, keeping the native menu, keyboard handling and Playwright's
|
|
423
423
|
`selectOption` working unchanged. */
|
|
424
|
-
.select-shell {
|
|
424
|
+
.select-shell { position: relative; flex: 0 0 auto; display: inline-flex; }
|
|
425
|
+
/* The trigger carries the surface the shell used to: it is a real button now,
|
|
426
|
+
not a transparent native select sitting on top of a painted div. */
|
|
427
|
+
.select-trigger {
|
|
425
428
|
position: relative;
|
|
426
429
|
flex: 0 0 auto;
|
|
427
430
|
display: inline-flex;
|
|
@@ -429,22 +432,23 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
429
432
|
height: 30px;
|
|
430
433
|
box-sizing: border-box;
|
|
431
434
|
padding: 0 26px 0 9px;
|
|
435
|
+
font: inherit;
|
|
432
436
|
font-size: 11.5px;
|
|
433
437
|
white-space: nowrap;
|
|
434
438
|
background: var(--chrome-2);
|
|
435
439
|
color: var(--text-0);
|
|
436
440
|
border: 1px solid var(--line);
|
|
437
441
|
border-radius: 5px;
|
|
438
|
-
}
|
|
439
|
-
.select-shell select {
|
|
440
|
-
position: absolute;
|
|
441
|
-
inset: 0;
|
|
442
|
-
width: 100%;
|
|
443
|
-
height: 100%;
|
|
444
|
-
opacity: 0;
|
|
445
|
-
appearance: none;
|
|
446
442
|
cursor: pointer;
|
|
447
443
|
}
|
|
444
|
+
.select-trigger:hover { background: var(--chrome-3); }
|
|
445
|
+
/* The stack that fixes the width: one cell, every label in it, only the chosen
|
|
446
|
+
one visible. `visibility` rather than `display: none` because a hidden box
|
|
447
|
+
still has to take its width for this to work at all. */
|
|
448
|
+
.select-label { display: grid; align-items: center; }
|
|
449
|
+
.select-label > * { grid-area: 1 / 1; white-space: nowrap; text-align: left; }
|
|
450
|
+
.select-label-sizer { visibility: hidden; }
|
|
451
|
+
.select-trigger:focus-visible { outline: none; border-color: var(--text-1); }
|
|
448
452
|
.select-shell .select-chevron {
|
|
449
453
|
position: absolute;
|
|
450
454
|
right: 7px;
|
|
@@ -453,6 +457,56 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
453
457
|
pointer-events: none;
|
|
454
458
|
}
|
|
455
459
|
|
|
460
|
+
/* The menu. Fixed, so it is positioned against the window rather than against
|
|
461
|
+
whatever the toolbar's overflow happens to be, and clamped in JS to the
|
|
462
|
+
window's box — the whole point of replacing the native popup, which drew
|
|
463
|
+
outside the app entirely. */
|
|
464
|
+
.select-menu {
|
|
465
|
+
position: fixed;
|
|
466
|
+
z-index: 60;
|
|
467
|
+
/* Or `max-height` would bound the *content* box and the padding and border
|
|
468
|
+
would sit outside it — the menu overflowed the window's bottom edge by
|
|
469
|
+
exactly those 10px, which is the one thing this control exists to prevent. */
|
|
470
|
+
box-sizing: border-box;
|
|
471
|
+
min-width: 220px;
|
|
472
|
+
padding: 4px;
|
|
473
|
+
overflow-y: auto;
|
|
474
|
+
overscroll-behavior: contain;
|
|
475
|
+
font-size: 11.5px;
|
|
476
|
+
background: var(--chrome-1);
|
|
477
|
+
color: var(--text-0);
|
|
478
|
+
border: 1px solid var(--line);
|
|
479
|
+
border-radius: 6px;
|
|
480
|
+
/* The one shadow in the chrome. A menu is the only surface that genuinely
|
|
481
|
+
floats above the rest, and without it the panel reads as part of the page
|
|
482
|
+
under test rather than as app chrome. */
|
|
483
|
+
box-shadow: 0 8px 24px rgb(0 0 0 / 55%);
|
|
484
|
+
}
|
|
485
|
+
.select-group + .select-group { border-top: 1px solid var(--line); margin-top: 4px; padding-top: 4px; }
|
|
486
|
+
.select-group-label {
|
|
487
|
+
padding: 5px 9px 3px;
|
|
488
|
+
color: var(--text-1);
|
|
489
|
+
font-size: 10.5px;
|
|
490
|
+
letter-spacing: 0.06em;
|
|
491
|
+
text-transform: uppercase;
|
|
492
|
+
}
|
|
493
|
+
.select-option {
|
|
494
|
+
display: flex;
|
|
495
|
+
align-items: center;
|
|
496
|
+
gap: 6px;
|
|
497
|
+
padding: 5px 9px 5px 4px;
|
|
498
|
+
border-radius: 4px;
|
|
499
|
+
cursor: pointer;
|
|
500
|
+
white-space: nowrap;
|
|
501
|
+
}
|
|
502
|
+
/* Hover and keyboard share one highlight: two different treatments for "the
|
|
503
|
+
row you are about to choose" is one too many. */
|
|
504
|
+
.select-option.active { background: var(--chrome-3); }
|
|
505
|
+
/* The tick's column is always reserved, so labels do not shift by 12px when
|
|
506
|
+
the selection moves. */
|
|
507
|
+
.select-tick { display: flex; width: 14px; justify-content: center; color: var(--text-0); }
|
|
508
|
+
|
|
509
|
+
|
|
456
510
|
/* Segmented groups. Pressed is a fill step and weight, never hue. */
|
|
457
511
|
.segmented {
|
|
458
512
|
flex: 0 0 auto;
|
|
@@ -1074,3 +1128,16 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
1074
1128
|
being simulated, which is the one fact that survives having no page. Stacking
|
|
1075
1129
|
rather than insetting the overlay, so nothing depends on the footer's height. */
|
|
1076
1130
|
.pane-footer { position: relative; z-index: 3; }
|
|
1131
|
+
|
|
1132
|
+
/* --- overlay view --------------------------------------------------------- */
|
|
1133
|
+
/* The menu view is a transparent sheet over the whole window, so its page must
|
|
1134
|
+
paint nothing of its own — a background here would hide both panes. */
|
|
1135
|
+
:root[data-overlay] html,
|
|
1136
|
+
:root[data-overlay],
|
|
1137
|
+
:root[data-overlay] body,
|
|
1138
|
+
:root[data-overlay] #root {
|
|
1139
|
+
background: transparent;
|
|
1140
|
+
}
|
|
1141
|
+
/* Spans the overlay so a press anywhere outside the menu dismisses it, which is
|
|
1142
|
+
what a native menu's invisible tracking window does. */
|
|
1143
|
+
.menu-backdrop { position: fixed; inset: 0; }
|
package/out/renderer/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:" />
|
|
6
6
|
<title>Obsrv</title>
|
|
7
|
-
<script type="module" crossorigin src="./assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-BJmd2r3W.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="./assets/index-DUmCxwNL.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
|
@@ -12,6 +12,7 @@ exports.parseUiState = parseUiState;
|
|
|
12
12
|
exports.parseScrollPos = parseScrollPos;
|
|
13
13
|
exports.parseScrollRequest = parseScrollRequest;
|
|
14
14
|
exports.parseScrollReport = parseScrollReport;
|
|
15
|
+
exports.parseMenuRequest = parseMenuRequest;
|
|
15
16
|
const presets_1 = require("./presets");
|
|
16
17
|
const types_1 = require("./types");
|
|
17
18
|
/**
|
|
@@ -281,3 +282,52 @@ function parseScrollReport(raw) {
|
|
|
281
282
|
: [];
|
|
282
283
|
return { id, x, y, scroller, warnings };
|
|
283
284
|
}
|
|
285
|
+
/** Rows in one menu, and characters in a label. A renderer bug should not be
|
|
286
|
+
* able to ask main to hold an unbounded payload. */
|
|
287
|
+
const MAX_MENU_OPTIONS = 200;
|
|
288
|
+
const MAX_MENU_LABEL = 120;
|
|
289
|
+
/**
|
|
290
|
+
* The menu the chrome is asking the overlay to draw. Validated like anything
|
|
291
|
+
* else crossing into main: the two sides are separate web contents, and main is
|
|
292
|
+
* the only thing standing between them.
|
|
293
|
+
*/
|
|
294
|
+
function parseMenuRequest(raw) {
|
|
295
|
+
if (!isRecord(raw))
|
|
296
|
+
return null;
|
|
297
|
+
const { groups, value, ariaLabel, anchor } = raw;
|
|
298
|
+
if (typeof value !== 'string' || value.length > MAX_MENU_LABEL)
|
|
299
|
+
return null;
|
|
300
|
+
if (typeof ariaLabel !== 'string' || ariaLabel.length > MAX_MENU_LABEL)
|
|
301
|
+
return null;
|
|
302
|
+
const rect = parseRect(anchor);
|
|
303
|
+
if (!rect)
|
|
304
|
+
return null;
|
|
305
|
+
if (!Array.isArray(groups))
|
|
306
|
+
return null;
|
|
307
|
+
const out = [];
|
|
308
|
+
let rows = 0;
|
|
309
|
+
for (const g of groups) {
|
|
310
|
+
if (!isRecord(g))
|
|
311
|
+
return null;
|
|
312
|
+
if (g.label !== undefined && (typeof g.label !== 'string' || g.label.length > MAX_MENU_LABEL))
|
|
313
|
+
return null;
|
|
314
|
+
if (!Array.isArray(g.options))
|
|
315
|
+
return null;
|
|
316
|
+
const options = [];
|
|
317
|
+
for (const o of g.options) {
|
|
318
|
+
if (!isRecord(o))
|
|
319
|
+
return null;
|
|
320
|
+
if (typeof o.value !== 'string' || o.value.length > MAX_MENU_LABEL)
|
|
321
|
+
return null;
|
|
322
|
+
if (typeof o.label !== 'string' || o.label.length > MAX_MENU_LABEL)
|
|
323
|
+
return null;
|
|
324
|
+
if (++rows > MAX_MENU_OPTIONS)
|
|
325
|
+
return null;
|
|
326
|
+
options.push({ value: o.value, label: o.label });
|
|
327
|
+
}
|
|
328
|
+
out.push(g.label === undefined ? { options } : { label: g.label, options });
|
|
329
|
+
}
|
|
330
|
+
if (rows === 0)
|
|
331
|
+
return null;
|
|
332
|
+
return { groups: out, value, ariaLabel, anchor: rect };
|
|
333
|
+
}
|