getobsrv 0.8.0 → 0.9.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/README.md +9 -1
- package/out/main/index.js +35 -6
- package/out/mcp/server.js +11 -1
- package/out/preload/app.js +10 -0
- package/out/renderer/assets/{index-BeroS4wv.css → index-CasW01OP.css} +208 -80
- package/out/renderer/assets/{index-Cgx8iixu.js → index-CpeTWcl6.js} +529 -145
- package/out/renderer/index.html +2 -2
- package/out/shared/control.js +12 -1
- package/out/shared/ipcPayloads.js +7 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -79,6 +79,12 @@ middle-button drag, Option+drag or Option+wheel, or switch the toolbar's
|
|
|
79
79
|
pixel-exact — the footer says so) and click anywhere in it to jump back to 1:1
|
|
80
80
|
at that spot.
|
|
81
81
|
|
|
82
|
+
The `Both / Target` control beside it hides the native pane so the target render
|
|
83
|
+
takes the whole window — useful for a small mobile preset that would otherwise
|
|
84
|
+
sit in half a window, and for agent captures. The native pane stays loaded while
|
|
85
|
+
hidden, so the URL bar, back/forward and link clicks keep working exactly as
|
|
86
|
+
they do side by side.
|
|
87
|
+
|
|
82
88
|
## Agent & CI use
|
|
83
89
|
|
|
84
90
|
The same rendering pipeline runs headless — no window, JSON on stdout, humans
|
|
@@ -116,7 +122,9 @@ raster density — the PNG comes back as an inline image up to 1.5 MiB),
|
|
|
116
122
|
If the desktop app is open with the toolbar's **Agent control** toggle on,
|
|
117
123
|
`obsrv_snap` drives the *visible* window instead: you watch the URL load and
|
|
118
124
|
the preset flip, and the agent gets back a capture of the app exactly as you
|
|
119
|
-
see it (plus `obsrv_drive` to flip URL/preset/profile directly
|
|
125
|
+
see it (plus `obsrv_drive` to flip URL/preset/profile/panes directly — `panes:
|
|
126
|
+
'target'` gives the target render the whole window, which is usually what you
|
|
127
|
+
want before a capture). Agents can
|
|
120
128
|
also scroll, click, pan and highlight while you watch — a drive session works
|
|
121
129
|
as a guided demo. A `scroll` reports the offset it actually reached
|
|
122
130
|
(`scrolled` / `scroller`), finds the inner scroll container on pages whose
|
package/out/main/index.js
CHANGED
|
@@ -14,6 +14,7 @@ const IPC = {
|
|
|
14
14
|
forward: "obsrv:forward",
|
|
15
15
|
setViewport: "obsrv:set-viewport",
|
|
16
16
|
setNativeBounds: "obsrv:set-native-bounds",
|
|
17
|
+
setNativeVisible: "obsrv:set-native-visible",
|
|
17
18
|
setMode: "obsrv:set-mode",
|
|
18
19
|
sendInput: "obsrv:send-input",
|
|
19
20
|
getHostInfo: "obsrv:get-host-info",
|
|
@@ -26,6 +27,7 @@ const IPC = {
|
|
|
26
27
|
hostChanged: "obsrv:host-changed",
|
|
27
28
|
targetLoading: "obsrv:target-loading",
|
|
28
29
|
targetNavigating: "obsrv:target-navigating",
|
|
30
|
+
nativeFocused: "obsrv:native-focused",
|
|
29
31
|
syncScroll: "obsrv:sync-scroll",
|
|
30
32
|
applyScroll: "obsrv:apply-scroll",
|
|
31
33
|
scrollResult: "obsrv:scroll-result",
|
|
@@ -157,10 +159,13 @@ function parseUiState(raw) {
|
|
|
157
159
|
if (typeof profileId !== "string" || profileId.length === 0 || profileId.length > MAX_UI_ID) return null;
|
|
158
160
|
if (viewMode !== "1:1" && viewMode !== "fit") return null;
|
|
159
161
|
if (mode !== "url" && mode !== "image") return null;
|
|
162
|
+
const panes = raw.panes ?? "both";
|
|
163
|
+
if (panes !== "both" && panes !== "target") return null;
|
|
160
164
|
return {
|
|
161
165
|
presetId,
|
|
162
166
|
profileId,
|
|
163
167
|
viewMode,
|
|
168
|
+
panes,
|
|
164
169
|
mode,
|
|
165
170
|
targetBounds: parseRect(raw.targetBounds),
|
|
166
171
|
canvasBounds: parseRect(raw.canvasBounds)
|
|
@@ -200,6 +205,7 @@ const CONTROL_COMMANDS = [
|
|
|
200
205
|
"setPreset",
|
|
201
206
|
"setProfile",
|
|
202
207
|
"setViewMode",
|
|
208
|
+
"setPanes",
|
|
203
209
|
"captureVisible",
|
|
204
210
|
// v0.5 drive controls (spec §14 "Drive controls").
|
|
205
211
|
"scroll",
|
|
@@ -244,6 +250,9 @@ function profileApplyError(id) {
|
|
|
244
250
|
function viewModeApplyError(v) {
|
|
245
251
|
return v === "1:1" || v === "fit" ? null : `setViewMode payload must be { mode: '1:1' | 'fit' }`;
|
|
246
252
|
}
|
|
253
|
+
function panesApplyError(v) {
|
|
254
|
+
return v === "both" || v === "target" ? null : `setPanes payload must be { panes: 'both' | 'target' }`;
|
|
255
|
+
}
|
|
247
256
|
function pixelExactApplyError(v) {
|
|
248
257
|
return typeof v === "boolean" ? null : "setPixelExact payload must be { on: boolean }";
|
|
249
258
|
}
|
|
@@ -466,6 +475,12 @@ class ControlServer {
|
|
|
466
475
|
const mode = payload.mode;
|
|
467
476
|
return this.applyAndConfirm({ viewMode: mode }, (s) => s.viewMode === mode);
|
|
468
477
|
}
|
|
478
|
+
case "setPanes": {
|
|
479
|
+
const err = panesApplyError(payload.panes);
|
|
480
|
+
if (err) return reply(400, { error: err });
|
|
481
|
+
const panes = payload.panes;
|
|
482
|
+
return this.applyAndConfirm({ panes }, (s) => s.panes === panes);
|
|
483
|
+
}
|
|
469
484
|
case "captureVisible": {
|
|
470
485
|
const capture = await this.deps.captureVisible();
|
|
471
486
|
return reply(200, { ok: true, ...capture });
|
|
@@ -603,7 +618,7 @@ async function checkForUpdate(current, now) {
|
|
|
603
618
|
request.end();
|
|
604
619
|
});
|
|
605
620
|
}
|
|
606
|
-
const TOOLBAR_H =
|
|
621
|
+
const TOOLBAR_H = 82;
|
|
607
622
|
const MAX_IMAGE_FILE_BYTES = 64 * 1024 * 1024;
|
|
608
623
|
const SCROLL_REPLY_TIMEOUT_MS = 1e3;
|
|
609
624
|
function hostInfo(win) {
|
|
@@ -675,13 +690,24 @@ function registerIpc(ctx) {
|
|
|
675
690
|
} catch {
|
|
676
691
|
}
|
|
677
692
|
});
|
|
693
|
+
let modeIsLive = true;
|
|
694
|
+
let panesShowNative = true;
|
|
695
|
+
const applyNativeVisibility = () => {
|
|
696
|
+
native.setVisible(modeIsLive && panesShowNative);
|
|
697
|
+
};
|
|
678
698
|
electron.ipcMain.on(IPC.setMode, (e, raw) => {
|
|
679
699
|
if (!fromRenderer(e)) return;
|
|
680
700
|
const mode = parseMode(raw);
|
|
681
701
|
if (!mode) return;
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
bus.setEnabled(
|
|
702
|
+
modeIsLive = mode === "url";
|
|
703
|
+
applyNativeVisibility();
|
|
704
|
+
bus.setEnabled(modeIsLive);
|
|
705
|
+
});
|
|
706
|
+
electron.ipcMain.on(IPC.setNativeVisible, (e, raw) => {
|
|
707
|
+
if (!fromRenderer(e)) return;
|
|
708
|
+
if (typeof raw !== "boolean") return;
|
|
709
|
+
panesShowNative = raw;
|
|
710
|
+
applyNativeVisibility();
|
|
685
711
|
});
|
|
686
712
|
let rendererDrivesLayout = false;
|
|
687
713
|
const fallbackLayout = () => {
|
|
@@ -766,7 +792,7 @@ function registerIpc(ctx) {
|
|
|
766
792
|
settings = s;
|
|
767
793
|
if (s.agentControl !== wasEnabled) applyAgentControl(s.agentControl);
|
|
768
794
|
});
|
|
769
|
-
const uiState = { presetId: "1080p-24", profileId: "reference", viewMode: "1:1", mode: "url" };
|
|
795
|
+
const uiState = { presetId: "1080p-24", profileId: "reference", viewMode: "1:1", panes: "both", mode: "url" };
|
|
770
796
|
let targetBounds = null;
|
|
771
797
|
let canvasBounds = null;
|
|
772
798
|
const MAX_PENDING_APPLIES = 32;
|
|
@@ -1213,6 +1239,9 @@ function boot() {
|
|
|
1213
1239
|
if (!win.isDestroyed()) win.webContents.send(IPC.openImagePath, path);
|
|
1214
1240
|
}
|
|
1215
1241
|
});
|
|
1242
|
+
native.webContents.on("focus", () => {
|
|
1243
|
+
if (!win.isDestroyed()) win.webContents.send(IPC.nativeFocused);
|
|
1244
|
+
});
|
|
1216
1245
|
const target = new targetSource.TargetSource();
|
|
1217
1246
|
target.on("load-error", (err) => {
|
|
1218
1247
|
if (!win.isDestroyed()) win.webContents.send(IPC.loadError, err);
|
|
@@ -1227,7 +1256,7 @@ function boot() {
|
|
|
1227
1256
|
const sync = attachSyncBus(native, target, (url) => {
|
|
1228
1257
|
if (!win.isDestroyed()) win.webContents.send(IPC.urlChanged, url);
|
|
1229
1258
|
});
|
|
1230
|
-
const ctx = { win, native, target, bus, sync };
|
|
1259
|
+
const ctx = { win, native, target, bus, sync, toolbarH: TOOLBAR_H };
|
|
1231
1260
|
registerIpc(ctx);
|
|
1232
1261
|
installMenu(ctx);
|
|
1233
1262
|
win.on("close", () => {
|
package/out/mcp/server.js
CHANGED
|
@@ -160,6 +160,7 @@ const snapOutputShape = {
|
|
|
160
160
|
presetId: zod_1.z.string().optional().describe('Live only: the screen preset selected in the app.'),
|
|
161
161
|
profileId: zod_1.z.string().optional().describe('Live only: the panel profile selected in the app.'),
|
|
162
162
|
viewMode: zod_1.z.string().optional().describe("Live only: the app's target-pane view (1:1 or fit)."),
|
|
163
|
+
panes: zod_1.z.string().optional().describe("Live only: 'both' (native pane beside the target) or 'target' (the target render has the whole window)."),
|
|
163
164
|
width: zod_1.z
|
|
164
165
|
.number()
|
|
165
166
|
.optional()
|
|
@@ -245,6 +246,10 @@ const driveInputShape = {
|
|
|
245
246
|
preset: zod_1.z.enum(PRESET_IDS).optional().describe('Apply this screen preset, exactly as clicking the toolbar would.'),
|
|
246
247
|
profile: zod_1.z.enum(PROFILE_IDS).optional().describe('Apply this panel profile in the app.'),
|
|
247
248
|
viewMode: zod_1.z.enum(['1:1', 'fit']).optional().describe("Switch the app's target pane between 1:1 (actual size) and fit."),
|
|
249
|
+
panes: zod_1.z
|
|
250
|
+
.enum(['both', 'target'])
|
|
251
|
+
.optional()
|
|
252
|
+
.describe("Show both panes, or give the target render the whole window ('target'). Solo target is usually what you want before a capture."),
|
|
248
253
|
pixelExact: zod_1.z.boolean().optional().describe("Toggle the toolbar's pixel-exact checkbox (pins the magnification to the host scale)."),
|
|
249
254
|
focus: zod_1.z.boolean().optional().describe('true: bring the Obsrv window to the front first, so the user sees what follows.'),
|
|
250
255
|
reload: zod_1.z.boolean().optional().describe('true: reload both panes (the same action as the toolbar reload).'),
|
|
@@ -303,6 +308,7 @@ const driveOutputShape = {
|
|
|
303
308
|
presetId: zod_1.z.string(),
|
|
304
309
|
profileId: zod_1.z.string(),
|
|
305
310
|
viewMode: zod_1.z.string(),
|
|
311
|
+
panes: zod_1.z.string(),
|
|
306
312
|
mode: zod_1.z.string().describe("The app's pane mode: 'url' (live page) or 'image' (a dropped design export)."),
|
|
307
313
|
scrolled: zod_1.z
|
|
308
314
|
.object({ x: zod_1.z.number(), y: zod_1.z.number() })
|
|
@@ -475,6 +481,7 @@ async function liveSnap(app, input, notes) {
|
|
|
475
481
|
presetId: status.presetId,
|
|
476
482
|
profileId: status.profileId,
|
|
477
483
|
viewMode: status.viewMode,
|
|
484
|
+
panes: status.panes,
|
|
478
485
|
width,
|
|
479
486
|
height,
|
|
480
487
|
settled,
|
|
@@ -620,7 +627,7 @@ server.registerTool('obsrv_drive', {
|
|
|
620
627
|
`both panes, pan the target pane to a pixel, click the live page, and highlight a rect with a temporary ` +
|
|
621
628
|
`neutral marker, all while the user watches.\n\n` +
|
|
622
629
|
`Only the supplied inputs run (none = just read the current state), in this fixed order: focus → url → ` +
|
|
623
|
-
`preset → profile → viewMode → pixelExact → reload → back → forward → scroll → panTo → click → highlight → ` +
|
|
630
|
+
`preset → profile → viewMode → panes → pixelExact → reload → back → forward → scroll → panTo → click → highlight → ` +
|
|
624
631
|
`capture. ` +
|
|
625
632
|
`The result is the final status: app version, the URL showing, and the selected preset/profile/view. A ` +
|
|
626
633
|
`click that navigates is reflected in that status — the call waits briefly (up to 2 s) for the commit. A ` +
|
|
@@ -664,6 +671,9 @@ server.registerTool('obsrv_drive', {
|
|
|
664
671
|
if (input.viewMode !== undefined) {
|
|
665
672
|
await (0, control_2.controlCall)(live.info, 'setViewMode', { mode: input.viewMode }, LIVE_APPLY_TIMEOUT_MS);
|
|
666
673
|
}
|
|
674
|
+
if (input.panes !== undefined) {
|
|
675
|
+
await (0, control_2.controlCall)(live.info, 'setPanes', { panes: input.panes }, LIVE_APPLY_TIMEOUT_MS);
|
|
676
|
+
}
|
|
667
677
|
if (input.pixelExact !== undefined) {
|
|
668
678
|
await (0, control_2.controlCall)(live.info, 'setPixelExact', { on: input.pixelExact }, LIVE_APPLY_TIMEOUT_MS);
|
|
669
679
|
}
|
package/out/preload/app.js
CHANGED
|
@@ -7,6 +7,7 @@ const IPC = {
|
|
|
7
7
|
forward: "obsrv:forward",
|
|
8
8
|
setViewport: "obsrv:set-viewport",
|
|
9
9
|
setNativeBounds: "obsrv:set-native-bounds",
|
|
10
|
+
setNativeVisible: "obsrv:set-native-visible",
|
|
10
11
|
setMode: "obsrv:set-mode",
|
|
11
12
|
sendInput: "obsrv:send-input",
|
|
12
13
|
getHostInfo: "obsrv:get-host-info",
|
|
@@ -19,6 +20,7 @@ const IPC = {
|
|
|
19
20
|
hostChanged: "obsrv:host-changed",
|
|
20
21
|
targetLoading: "obsrv:target-loading",
|
|
21
22
|
targetNavigating: "obsrv:target-navigating",
|
|
23
|
+
nativeFocused: "obsrv:native-focused",
|
|
22
24
|
openImage: "obsrv:open-image",
|
|
23
25
|
focusUrl: "obsrv:focus-url",
|
|
24
26
|
openImagePath: "obsrv:open-image-path",
|
|
@@ -57,6 +59,7 @@ const api = {
|
|
|
57
59
|
forward: () => electron.ipcRenderer.send(IPC.forward),
|
|
58
60
|
setViewport: (width, height, deviceScaleFactor) => electron.ipcRenderer.invoke(IPC.setViewport, width, height, deviceScaleFactor),
|
|
59
61
|
setNativeBounds: (rect) => electron.ipcRenderer.send(IPC.setNativeBounds, rect),
|
|
62
|
+
setNativeVisible: (visible) => electron.ipcRenderer.send(IPC.setNativeVisible, visible),
|
|
60
63
|
setMode: (mode) => electron.ipcRenderer.send(IPC.setMode, mode),
|
|
61
64
|
sendInput: (ev) => electron.ipcRenderer.send(IPC.sendInput, ev),
|
|
62
65
|
getHostInfo: () => electron.ipcRenderer.invoke(IPC.getHostInfo),
|
|
@@ -67,6 +70,13 @@ const api = {
|
|
|
67
70
|
onLoadError: (cb) => subscribe(IPC.loadError, cb),
|
|
68
71
|
onHostChanged: (cb) => subscribe(IPC.hostChanged, cb),
|
|
69
72
|
onTargetLoading: (cb) => subscribe(IPC.targetLoading, cb),
|
|
73
|
+
onNativeFocused: (cb) => {
|
|
74
|
+
const listener = () => cb();
|
|
75
|
+
electron.ipcRenderer.on(IPC.nativeFocused, listener);
|
|
76
|
+
return () => {
|
|
77
|
+
electron.ipcRenderer.removeListener(IPC.nativeFocused, listener);
|
|
78
|
+
};
|
|
79
|
+
},
|
|
70
80
|
onTargetNavigating: (cb) => {
|
|
71
81
|
const listener = () => cb();
|
|
72
82
|
electron.ipcRenderer.on(IPC.targetNavigating, listener);
|
|
@@ -106,6 +106,7 @@
|
|
|
106
106
|
--chrome-0: #141414;
|
|
107
107
|
--chrome-1: #1c1c1c;
|
|
108
108
|
--chrome-2: #262626;
|
|
109
|
+
--chrome-3: #3a3a3a;
|
|
109
110
|
--line: #333333;
|
|
110
111
|
--text-0: #ededed;
|
|
111
112
|
--text-1: #8a8a8a;
|
|
@@ -131,66 +132,206 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
131
132
|
.app { height: 100%; display: flex; flex-direction: column; }
|
|
132
133
|
.muted { color: var(--text-1); }
|
|
133
134
|
.warn { color: var(--warn); }
|
|
134
|
-
.
|
|
135
|
-
|
|
136
|
-
/*
|
|
137
|
-
.
|
|
138
|
-
|
|
135
|
+
.chrome .warn { white-space: nowrap; }
|
|
136
|
+
|
|
137
|
+
/* Two strips. Browsing above; the controls that describe the simulated
|
|
138
|
+
screen below, on a darker ground so the rows read as separate registers.
|
|
139
|
+
44 + 38 = 82 must equal TOOLBAR_H in src/main/ipc.ts. Pinned heights also
|
|
140
|
+
keep a late reflow from resizing the panes under NativeSlot's observer. */
|
|
141
|
+
.chrome { flex: 0 0 auto; }
|
|
142
|
+
.chrome-row {
|
|
143
|
+
box-sizing: border-box;
|
|
139
144
|
display: flex;
|
|
140
145
|
align-items: center;
|
|
141
146
|
gap: 8px;
|
|
142
147
|
padding: 0 10px;
|
|
143
|
-
background: var(--chrome-1);
|
|
144
148
|
border-bottom: 1px solid var(--line);
|
|
145
149
|
}
|
|
146
|
-
.
|
|
150
|
+
.chrome-browse { height: 44px; background: var(--chrome-1); }
|
|
151
|
+
.chrome-screen { height: 38px; background: var(--chrome-0); }
|
|
152
|
+
.chrome-spacer { flex: 1 1 auto; }
|
|
153
|
+
|
|
154
|
+
/* No blanket `button` rule here, ever. The old `.toolbar button { width:
|
|
155
|
+
26px }` was specificity (0,1,1) and silently clipped the update button to
|
|
156
|
+
"v0."; every control now sizes itself. */
|
|
157
|
+
.icon-button {
|
|
158
|
+
flex: 0 0 auto;
|
|
159
|
+
width: 30px;
|
|
160
|
+
height: 30px;
|
|
161
|
+
display: inline-flex;
|
|
162
|
+
align-items: center;
|
|
163
|
+
justify-content: center;
|
|
147
164
|
background: var(--chrome-2);
|
|
148
165
|
color: var(--text-0);
|
|
149
166
|
border: 1px solid var(--line);
|
|
150
|
-
border-radius:
|
|
151
|
-
width: 26px;
|
|
152
|
-
height: 24px;
|
|
167
|
+
border-radius: 5px;
|
|
153
168
|
cursor: pointer;
|
|
154
169
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
color: var(--text-0);
|
|
158
|
-
border: 1px solid var(--line);
|
|
159
|
-
border-radius: 4px;
|
|
160
|
-
height: 24px;
|
|
161
|
-
}
|
|
162
|
-
.url-form { flex: 1 1 auto; min-width: 120px; }
|
|
170
|
+
|
|
171
|
+
.url-form { flex: 1 1 auto; min-width: 160px; }
|
|
163
172
|
.url-form input {
|
|
164
173
|
width: 100%;
|
|
165
|
-
height:
|
|
174
|
+
height: 30px;
|
|
166
175
|
box-sizing: border-box;
|
|
167
|
-
padding: 0
|
|
176
|
+
padding: 0 10px;
|
|
177
|
+
font-size: 12.5px;
|
|
168
178
|
background: var(--field);
|
|
169
179
|
color: var(--text-0);
|
|
170
180
|
border: 1px solid var(--line);
|
|
171
|
-
border-radius:
|
|
181
|
+
border-radius: 5px;
|
|
172
182
|
}
|
|
173
183
|
.url-form input[readonly] { color: var(--text-1); }
|
|
184
|
+
|
|
185
|
+
.status-cluster { flex: 0 0 auto; display: flex; align-items: center; gap: 6px; font-size: 10px; }
|
|
174
186
|
.badge-error {
|
|
175
187
|
color: var(--error);
|
|
176
188
|
border: 1px solid var(--error);
|
|
177
189
|
border-radius: 4px;
|
|
178
|
-
padding:
|
|
190
|
+
padding: 2px 6px;
|
|
191
|
+
font-family: var(--mono);
|
|
179
192
|
font-variant-numeric: tabular-nums;
|
|
180
193
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
194
|
+
/* An update is neither a warning nor an error, so it carries no colour. */
|
|
195
|
+
.update-button {
|
|
196
|
+
flex: 0 0 auto;
|
|
197
|
+
height: 30px;
|
|
198
|
+
padding: 0 10px;
|
|
199
|
+
white-space: nowrap;
|
|
200
|
+
font-family: var(--mono);
|
|
201
|
+
font-size: 11.5px;
|
|
202
|
+
font-variant-numeric: tabular-nums;
|
|
203
|
+
background: var(--chrome-2);
|
|
204
|
+
color: var(--text-0);
|
|
205
|
+
border: 1px solid var(--line);
|
|
206
|
+
border-radius: 5px;
|
|
207
|
+
cursor: pointer;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Our surface, the platform's popup: the real select sits transparent on
|
|
211
|
+
top, keeping the native menu, keyboard handling and Playwright's
|
|
212
|
+
`selectOption` working unchanged. */
|
|
213
|
+
.select-shell {
|
|
214
|
+
position: relative;
|
|
215
|
+
flex: 0 0 auto;
|
|
216
|
+
display: inline-flex;
|
|
217
|
+
align-items: center;
|
|
218
|
+
height: 30px;
|
|
219
|
+
box-sizing: border-box;
|
|
220
|
+
padding: 0 26px 0 9px;
|
|
221
|
+
font-size: 11.5px;
|
|
222
|
+
white-space: nowrap;
|
|
223
|
+
background: var(--chrome-2);
|
|
224
|
+
color: var(--text-0);
|
|
225
|
+
border: 1px solid var(--line);
|
|
226
|
+
border-radius: 5px;
|
|
227
|
+
}
|
|
228
|
+
.select-shell select {
|
|
229
|
+
position: absolute;
|
|
230
|
+
inset: 0;
|
|
231
|
+
width: 100%;
|
|
232
|
+
height: 100%;
|
|
233
|
+
opacity: 0;
|
|
234
|
+
appearance: none;
|
|
235
|
+
cursor: pointer;
|
|
236
|
+
}
|
|
237
|
+
.select-shell .select-chevron {
|
|
238
|
+
position: absolute;
|
|
239
|
+
right: 7px;
|
|
240
|
+
display: flex;
|
|
241
|
+
color: var(--text-1);
|
|
242
|
+
pointer-events: none;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/* Segmented groups. Pressed is a fill step and weight, never hue. */
|
|
246
|
+
.segmented {
|
|
247
|
+
flex: 0 0 auto;
|
|
248
|
+
display: inline-flex;
|
|
249
|
+
height: 30px;
|
|
250
|
+
/* A `div`, unlike a `button`, does not get the UA's border-box: without
|
|
251
|
+
this the 1px border makes the group 32px and it sits 1px proud of every
|
|
252
|
+
other 30px control in the row. */
|
|
253
|
+
box-sizing: border-box;
|
|
254
|
+
border: 1px solid var(--line);
|
|
255
|
+
border-radius: 5px;
|
|
256
|
+
overflow: hidden;
|
|
257
|
+
}
|
|
258
|
+
.segmented button {
|
|
259
|
+
height: 100%;
|
|
260
|
+
padding: 0 11px;
|
|
261
|
+
font-size: 11.5px;
|
|
262
|
+
background: var(--chrome-2);
|
|
263
|
+
color: var(--text-1);
|
|
264
|
+
border: 0;
|
|
265
|
+
border-right: 1px solid var(--line);
|
|
266
|
+
border-radius: 0;
|
|
267
|
+
cursor: pointer;
|
|
268
|
+
}
|
|
269
|
+
.segmented button:last-child { border-right: 0; }
|
|
270
|
+
.segmented button[aria-pressed='true'] { background: var(--chrome-3); color: var(--text-0); }
|
|
184
271
|
|
|
185
|
-
/*
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
.
|
|
190
|
-
.
|
|
272
|
+
/* Right-aligned deliberately: the native pane is an OS-level WebContentsView
|
|
273
|
+
that covers renderer paint, so a menu reaching the left half of the window
|
|
274
|
+
would be invisible. 200px against a 900px minWidth always fits the target
|
|
275
|
+
pane. */
|
|
276
|
+
.overflow { position: relative; flex: 0 0 auto; }
|
|
277
|
+
.overflow-menu {
|
|
278
|
+
position: absolute;
|
|
279
|
+
top: calc(100% + 4px);
|
|
280
|
+
right: 0;
|
|
281
|
+
z-index: 10;
|
|
282
|
+
width: 200px;
|
|
283
|
+
padding: 5px 0;
|
|
284
|
+
font-size: 11.5px;
|
|
285
|
+
background: var(--chrome-2);
|
|
286
|
+
border: 1px solid var(--line);
|
|
287
|
+
border-radius: 6px;
|
|
288
|
+
}
|
|
289
|
+
.menu-row {
|
|
290
|
+
display: flex;
|
|
291
|
+
align-items: center;
|
|
292
|
+
gap: 8px;
|
|
293
|
+
width: 100%;
|
|
294
|
+
box-sizing: border-box;
|
|
295
|
+
height: 30px;
|
|
296
|
+
padding: 0 11px;
|
|
297
|
+
text-align: left;
|
|
298
|
+
background: none;
|
|
299
|
+
color: var(--text-0);
|
|
300
|
+
border: 0;
|
|
301
|
+
border-radius: 0;
|
|
302
|
+
cursor: pointer;
|
|
303
|
+
}
|
|
304
|
+
.menu-row:hover { background: var(--chrome-3); }
|
|
305
|
+
/* Which drawer is open. The fill step is the chrome's one pressed idiom,
|
|
306
|
+
shared with `.segmented`; `:hover` is the same fill, so the pressed row also
|
|
307
|
+
carries a 2px inset rule down its leading edge. That marker is the part that
|
|
308
|
+
survives hover — hovering any row fills it, but only the open drawer's row
|
|
309
|
+
keeps the rule — and it is the same neutral weight the surround control uses
|
|
310
|
+
to mark its selection. No hue, here or anywhere else in the chrome. */
|
|
311
|
+
.menu-row[aria-pressed='true'] {
|
|
312
|
+
background: var(--chrome-3);
|
|
313
|
+
box-shadow: inset 2px 0 0 var(--text-0);
|
|
314
|
+
}
|
|
315
|
+
.menu-sep { height: 1px; margin: 5px 0; background: var(--line); }
|
|
316
|
+
/* Sized to the 16px icons the other rows lead with, and stripped of the UA's
|
|
317
|
+
own margins, so every label in the menu starts at the same x — a checkbox
|
|
318
|
+
row and an icon row otherwise indent differently. */
|
|
319
|
+
.overflow-menu input[type='checkbox'] {
|
|
320
|
+
accent-color: var(--text-0);
|
|
321
|
+
flex: 0 0 auto;
|
|
322
|
+
width: 16px;
|
|
323
|
+
height: 16px;
|
|
324
|
+
margin: 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* Focus is a 1px inset neutral ring, never Chromium's accent outline. */
|
|
328
|
+
.chrome :is(button, select, input):focus { outline: none; }
|
|
329
|
+
.chrome :is(button, select, input):focus-visible {
|
|
191
330
|
outline: 1px solid var(--text-1);
|
|
192
331
|
outline-offset: -1px;
|
|
193
332
|
}
|
|
333
|
+
/* The real select is transparent, so its ring must land on the shell. */
|
|
334
|
+
.select-shell:has(select:focus-visible) { outline: 1px solid var(--text-1); outline-offset: -1px; }
|
|
194
335
|
|
|
195
336
|
.panes { flex: 1 1 auto; display: flex; min-height: 0; }
|
|
196
337
|
.pane {
|
|
@@ -232,11 +373,29 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
232
373
|
}
|
|
233
374
|
.pane-footer .role { color: var(--text-0); letter-spacing: 0.08em; }
|
|
234
375
|
|
|
235
|
-
/* Segmented surround control
|
|
236
|
-
|
|
376
|
+
/* Segmented surround control: the same 30px border-box as `.segmented` and
|
|
377
|
+
`.select-shell`, so the screen row has one hit-target rhythm. `box-sizing`
|
|
378
|
+
for the same reason `.segmented` needs it — a `div` gets no border-box from
|
|
379
|
+
the UA, so the 1px border would otherwise push the group to 32px.
|
|
380
|
+
|
|
381
|
+
The swatch stays 12px: the swatch is the affordance, the button around it is
|
|
382
|
+
just the target. The pressed state stays an inset `--text-0` ring, and is
|
|
383
|
+
the one place the chrome does not use the `--chrome-3` fill step — a fill
|
|
384
|
+
ringing a 12px swatch competes with the swatch for the eye, and `--chrome-3`
|
|
385
|
+
lands two steps from the graphite swatch itself. This is the one control
|
|
386
|
+
whose whole job is judging a tint, so the marker stays off the tint. */
|
|
387
|
+
.surround-control {
|
|
388
|
+
box-sizing: border-box;
|
|
389
|
+
flex: 0 0 auto;
|
|
390
|
+
display: inline-flex;
|
|
391
|
+
height: 30px;
|
|
392
|
+
border: 1px solid var(--line);
|
|
393
|
+
border-radius: 5px;
|
|
394
|
+
overflow: hidden;
|
|
395
|
+
}
|
|
237
396
|
.surround-control button {
|
|
238
|
-
width:
|
|
239
|
-
height:
|
|
397
|
+
width: 28px;
|
|
398
|
+
height: 100%;
|
|
240
399
|
border: 0;
|
|
241
400
|
border-right: 1px solid var(--line);
|
|
242
401
|
border-radius: 0;
|
|
@@ -247,28 +406,25 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
247
406
|
.surround-control button[aria-pressed='true'] { box-shadow: inset 0 0 0 2px var(--text-0); }
|
|
248
407
|
.surround-swatch { display: block; width: 12px; height: 12px; margin: 0 auto; }
|
|
249
408
|
|
|
409
|
+
/* Nothing sits to the target pane's left in solo view, so the divider goes. */
|
|
410
|
+
.panes[data-panes='target'] .target-pane { border-left: 0; }
|
|
411
|
+
|
|
250
412
|
/* Numbers never jitter as a slider moves. */
|
|
251
413
|
.num { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|
252
414
|
|
|
253
|
-
/*
|
|
254
|
-
|
|
255
|
-
border-color: var(--text-0);
|
|
256
|
-
background: var(--chrome-1);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/* Agent control: same neutral chrome as every other toggle; the activity
|
|
260
|
-
badge is text-weight only — an agent driving the window is a status, not
|
|
261
|
-
an alert. */
|
|
262
|
-
.toolbar .agent-toggle { width: auto; padding: 0 8px; white-space: nowrap; }
|
|
415
|
+
/* Agent control opens a loopback server, so the chip persists while it is
|
|
416
|
+
enabled; recent activity is a brighter text weight, not a colour. */
|
|
263
417
|
.agent-activity {
|
|
418
|
+
flex: 0 0 auto;
|
|
264
419
|
color: var(--text-1);
|
|
265
420
|
border: 1px solid var(--line);
|
|
266
421
|
border-radius: 4px;
|
|
267
|
-
padding:
|
|
268
|
-
font-size:
|
|
269
|
-
letter-spacing: 0.
|
|
422
|
+
padding: 2px 7px;
|
|
423
|
+
font-size: 10px;
|
|
424
|
+
letter-spacing: 0.08em;
|
|
270
425
|
white-space: nowrap;
|
|
271
426
|
}
|
|
427
|
+
.agent-activity.active { color: var(--text-0); border-color: var(--text-0); }
|
|
272
428
|
|
|
273
429
|
/* Drawers sit beside the panes, never over them: the native WebContentsView is
|
|
274
430
|
an OS-level overlay and would cover anything painted on top of it. */
|
|
@@ -363,7 +519,6 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
363
519
|
}
|
|
364
520
|
.strip button:focus { outline: none; }
|
|
365
521
|
.strip button:focus-visible { outline: 1px solid var(--text-1); outline-offset: 1px; }
|
|
366
|
-
.toolbar .close-image { width: auto; padding: 0 8px; }
|
|
367
522
|
/* `safe`: a file wider than the pane scrolls from its left edge instead of
|
|
368
523
|
losing it off-screen. */
|
|
369
524
|
.image-pane .pane-body { display: flex; align-items: flex-start; justify-content: safe center; }
|
|
@@ -438,25 +593,10 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
438
593
|
.stall button:focus { outline: none; }
|
|
439
594
|
.stall button:focus-visible { outline: 1px solid var(--text-1); outline-offset: 1px; }
|
|
440
595
|
|
|
441
|
-
/* v1.1 fit & pan. The 1:1/Fit
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
.
|
|
445
|
-
width: auto;
|
|
446
|
-
height: 22px;
|
|
447
|
-
padding: 0 8px;
|
|
448
|
-
border: 0;
|
|
449
|
-
border-right: 1px solid var(--line);
|
|
450
|
-
border-radius: 0;
|
|
451
|
-
background: var(--chrome-2);
|
|
452
|
-
font-variant-numeric: tabular-nums;
|
|
453
|
-
cursor: pointer;
|
|
454
|
-
}
|
|
455
|
-
.view-control button:last-child { border-right: 0; }
|
|
456
|
-
.view-control button[aria-pressed='true'] {
|
|
457
|
-
box-shadow: inset 0 0 0 1px var(--text-0);
|
|
458
|
-
font-weight: 600;
|
|
459
|
-
}
|
|
596
|
+
/* v1.1 fit & pan. The 1:1/Fit control is a `Segmented`, so `.segmented` is
|
|
597
|
+
the whole of its styling — the old `.view-control button` rules lived here,
|
|
598
|
+
and being (0,1,1) and later in the file they beat `.segmented button` and
|
|
599
|
+
split the two groups apart. Nothing about 1:1/Fit is control-specific. */
|
|
460
600
|
|
|
461
601
|
/* The overview invites the jump-to-1:1 click; the pan chords (middle drag,
|
|
462
602
|
Option+drag) show the grab affordances. Cursors only — no overlay may sit
|
|
@@ -478,18 +618,6 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
478
618
|
.browser-notice p { margin: 0.75rem 0 0; color: var(--text-1); }
|
|
479
619
|
.browser-notice code { font-family: var(--mono); }
|
|
480
620
|
|
|
481
|
-
/* An update is neither a warning nor an error, so it carries no colour: the
|
|
482
|
-
only chromatic pixels in this chrome stay reserved for real attention. */
|
|
483
|
-
/* `.toolbar button` is (0,1,1) and pins every button to a 26px square, so this
|
|
484
|
-
needs two classes to outrank it — a bare `.update-button` renders as "v0.". */
|
|
485
|
-
.toolbar .update-button {
|
|
486
|
-
font-family: var(--mono);
|
|
487
|
-
font-variant-numeric: tabular-nums;
|
|
488
|
-
white-space: nowrap;
|
|
489
|
-
width: auto;
|
|
490
|
-
flex: 0 0 auto;
|
|
491
|
-
padding: 0 8px;
|
|
492
|
-
}
|
|
493
621
|
.version-block { margin: 8px 0; }
|
|
494
622
|
.version-row {
|
|
495
623
|
display: flex;
|