getobsrv 0.12.0 → 0.14.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/cli/args.js +52 -5
- package/out/main/cli.js +43 -9
- package/out/main/index.js +68 -10
- package/out/main/{targetSource-DlsHc-Ri.js → targetSource-W6YddYef.js} +14 -0
- package/out/mcp/lib.js +14 -1
- package/out/mcp/server.js +67 -8
- package/out/renderer/assets/{index-DvwDBeho.css → index-BpvyvKfD.css} +148 -18
- package/out/renderer/assets/{index-gYDbIgqJ.js → index-WM0cwf7d.js} +198 -18
- package/out/renderer/index.html +2 -2
- package/out/shared/calibration.js +35 -0
- package/out/shared/control.js +59 -1
- package/out/shared/ipcPayloads.js +15 -0
- package/out/shared/presets.js +11 -1
- package/out/shared/url.js +15 -0
- package/package.json +1 -1
package/out/mcp/server.js
CHANGED
|
@@ -97,12 +97,21 @@ const profileField = zod_1.z
|
|
|
97
97
|
.enum(PROFILE_IDS)
|
|
98
98
|
.optional()
|
|
99
99
|
.describe('Panel simulation (contrast floor, gamut, bit depth, brightness). Default: reference (off).');
|
|
100
|
+
const orientationField = zod_1.z
|
|
101
|
+
.enum(['portrait', 'landscape'])
|
|
102
|
+
.optional()
|
|
103
|
+
.describe('Rotate the screen a quarter turn (default portrait). Presets store their natural orientation — ' +
|
|
104
|
+
'portrait for every mobile preset, landscape for the monitors and laptops — and this swaps the CSS ' +
|
|
105
|
+
"viewport's two axes on top of that. Nothing else changes: the diagonal, raster density and physical " +
|
|
106
|
+
'size are orientation-independent, so it is the same panel turned sideways. Use it to check a ' +
|
|
107
|
+
'landscape phone layout, or a monitor stood on end.');
|
|
100
108
|
const snapInputShape = {
|
|
101
109
|
url: urlField,
|
|
102
110
|
preset: zod_1.z
|
|
103
111
|
.enum(PRESET_IDS)
|
|
104
112
|
.optional()
|
|
105
113
|
.describe('Screen preset id (list them with obsrv_presets). Mutually exclusive with width/height. Default: 1080p-24.'),
|
|
114
|
+
orientation: orientationField,
|
|
106
115
|
width: zod_1.z.number().int().min(1).optional().describe('Custom CSS viewport width in px. Needs height; mutually exclusive with preset.'),
|
|
107
116
|
height: zod_1.z.number().int().min(1).optional().describe('Custom CSS viewport height in px. Needs width.'),
|
|
108
117
|
deviceScaleFactor: zod_1.z
|
|
@@ -138,8 +147,24 @@ const snapOutputShape = {
|
|
|
138
147
|
.describe('How the snap was produced: a headless render, or a capture of the visible Obsrv app window (live drive).'),
|
|
139
148
|
out: zod_1.z.string().optional().describe('Headless only: PNG path the CLI wrote (same file as pngPath).'),
|
|
140
149
|
preset: zod_1.z.string().optional().describe('Headless only: preset id, or "custom" for width/height runs.'),
|
|
141
|
-
cssWidth: zod_1.z
|
|
142
|
-
|
|
150
|
+
cssWidth: zod_1.z
|
|
151
|
+
.number()
|
|
152
|
+
.optional()
|
|
153
|
+
.describe('Applied CSS viewport width, already rotated. Headless: grown under fullPage. Live: what the app is rendering.'),
|
|
154
|
+
cssHeight: zod_1.z
|
|
155
|
+
.number()
|
|
156
|
+
.optional()
|
|
157
|
+
.describe('Applied CSS viewport height, already rotated. Headless: grown under fullPage. Live: what the app is rendering.'),
|
|
158
|
+
orientation: zod_1.z
|
|
159
|
+
.string()
|
|
160
|
+
.optional()
|
|
161
|
+
.describe("Live only: the app's rotation flag — 'portrait' (the preset as its table stores it) or 'landscape' " +
|
|
162
|
+
'(rotated a quarter turn). See `screenShape` for the shape that produced. Headless runs report the ' +
|
|
163
|
+
'applied `cssWidth`/`cssHeight` instead, which say the same thing exactly.'),
|
|
164
|
+
screenShape: zod_1.z.string().optional().describe('Live only. ' + "The shape the screen actually has: 'portrait' or 'landscape'. Derived from the CSS dimensions, not from " +
|
|
165
|
+
"the `orientation` flag beside it — the flag means 'the preset as its table stores it' vs 'rotated a " +
|
|
166
|
+
"quarter turn', so for a landscape-natural monitor preset the two diverge (a fresh 1080p-24 tab is " +
|
|
167
|
+
"orientation 'portrait' on a 1920x1080 landscape screen). Report this word to the user, not the flag."),
|
|
143
168
|
deviceScaleFactor: zod_1.z.number().optional().describe('Headless only.'),
|
|
144
169
|
profile: zod_1.z.string().optional().describe('Headless only: applied panel profile id.'),
|
|
145
170
|
settled: zod_1.z
|
|
@@ -218,6 +243,9 @@ const diffOutputShape = {
|
|
|
218
243
|
findings: zod_1.z.array(zod_1.z.string()).describe('Humanised per-band findings. Informational — thresholds are the caller\'s job.'),
|
|
219
244
|
};
|
|
220
245
|
const presetsOutputShape = {
|
|
246
|
+
orientation: zod_1.z
|
|
247
|
+
.string()
|
|
248
|
+
.describe('How the cssWidth/cssHeight below relate to rotation, and how to ask for the other orientation.'),
|
|
221
249
|
presets: zod_1.z.array(zod_1.z.object({
|
|
222
250
|
id: zod_1.z.string(),
|
|
223
251
|
label: zod_1.z.string(),
|
|
@@ -246,6 +274,7 @@ const driveInputShape = {
|
|
|
246
274
|
.optional()
|
|
247
275
|
.describe('Navigate the app (both panes) to this http://, https:// or file:// URL (bare hosts also work).'),
|
|
248
276
|
preset: zod_1.z.enum(PRESET_IDS).optional().describe('Apply this screen preset, exactly as clicking the toolbar would.'),
|
|
277
|
+
orientation: orientationField,
|
|
249
278
|
profile: zod_1.z.enum(PROFILE_IDS).optional().describe('Apply this panel profile in the app.'),
|
|
250
279
|
viewMode: zod_1.z.enum(['1:1', 'fit']).optional().describe("Switch the app's target pane between 1:1 (actual size) and fit."),
|
|
251
280
|
panes: zod_1.z
|
|
@@ -309,6 +338,19 @@ const driveOutputShape = {
|
|
|
309
338
|
url: zod_1.z.string().describe('The URL the target pane reports showing.'),
|
|
310
339
|
presetId: zod_1.z.string(),
|
|
311
340
|
profileId: zod_1.z.string(),
|
|
341
|
+
orientation: zod_1.z
|
|
342
|
+
.string()
|
|
343
|
+
.describe("The rotation flag: 'portrait' (the preset as its table stores it) or 'landscape' (rotated a quarter " +
|
|
344
|
+
"turn). This is what to pass back to change it — for the shape the screen actually has, read " +
|
|
345
|
+
'`screenShape`. Reported as \'portrait\' by an app older than rotation, which is what such an app shows.'),
|
|
346
|
+
screenShape: zod_1.z.string().describe("The shape the screen actually has: 'portrait' or 'landscape'. Derived from the CSS dimensions, not from " +
|
|
347
|
+
"the `orientation` flag beside it — the flag means 'the preset as its table stores it' vs 'rotated a " +
|
|
348
|
+
"quarter turn', so for a landscape-natural monitor preset the two diverge (a fresh 1080p-24 tab is " +
|
|
349
|
+
"orientation 'portrait' on a 1920x1080 landscape screen). Report this word to the user, not the flag."),
|
|
350
|
+
cssWidth: zod_1.z
|
|
351
|
+
.number()
|
|
352
|
+
.describe('The CSS viewport the target is rendering at, already rotated. 0 from an app that predates the field.'),
|
|
353
|
+
cssHeight: zod_1.z.number().describe('The CSS viewport height, already rotated. 0 from an app that predates the field.'),
|
|
312
354
|
viewMode: zod_1.z.string(),
|
|
313
355
|
panes: zod_1.z.string(),
|
|
314
356
|
mode: zod_1.z.string().describe("The app's pane mode: 'url' (live page) or 'image' (a dropped design export)."),
|
|
@@ -442,6 +484,9 @@ async function liveSnap(app, input, notes) {
|
|
|
442
484
|
}
|
|
443
485
|
if (input.preset !== undefined)
|
|
444
486
|
await (0, control_2.controlCall)(info, 'setPreset', { id: input.preset }, LIVE_APPLY_TIMEOUT_MS);
|
|
487
|
+
if (input.orientation !== undefined) {
|
|
488
|
+
await (0, control_2.controlCall)(info, 'setOrientation', { orientation: input.orientation }, LIVE_APPLY_TIMEOUT_MS);
|
|
489
|
+
}
|
|
445
490
|
if (input.profile !== undefined)
|
|
446
491
|
await (0, control_2.controlCall)(info, 'setProfile', { id: input.profile }, LIVE_APPLY_TIMEOUT_MS);
|
|
447
492
|
}
|
|
@@ -488,6 +533,10 @@ async function liveSnap(app, input, notes) {
|
|
|
488
533
|
url: status.url,
|
|
489
534
|
presetId: status.presetId,
|
|
490
535
|
profileId: status.profileId,
|
|
536
|
+
orientation: status.orientation,
|
|
537
|
+
screenShape: status.screenShape,
|
|
538
|
+
cssWidth: status.cssWidth,
|
|
539
|
+
cssHeight: status.cssHeight,
|
|
491
540
|
viewMode: status.viewMode,
|
|
492
541
|
panes: status.panes,
|
|
493
542
|
tabId: status.tabId,
|
|
@@ -515,7 +564,8 @@ server.registerTool('obsrv_snap', {
|
|
|
515
564
|
`presets, the device's 2x/3x DPR plus mobile UA and viewport semantics for phone presets — optionally ` +
|
|
516
565
|
`through a cheap-panel simulation, and return the PNG. Use it to judge how a page actually looks on the ` +
|
|
517
566
|
`screens users own (1366×768 laptops, 1080p desktops, budget Androids) before declaring frontend work done.\n\n` +
|
|
518
|
-
`Pass either \`preset\` (list ids with obsrv_presets) or custom \`width\` + \`height\`, never both
|
|
567
|
+
`Pass either \`preset\` (list ids with obsrv_presets) or custom \`width\` + \`height\`, never both; either can be ` +
|
|
568
|
+
`rotated with \`orientation: "landscape"\`, which is how you check a phone's landscape layout. ` +
|
|
519
569
|
`Returns structured metadata (applied viewport, profile, \`settled\`, warnings, and \`pngPath\` — the PNG ` +
|
|
520
570
|
`kept in a per-call temp dir) plus the PNG as an inline image when it is within the 1.5 MiB cap; larger ` +
|
|
521
571
|
`captures (typically fullPage) stay on disk with a note.\n\n` +
|
|
@@ -635,15 +685,16 @@ server.registerTool('obsrv_diff', {
|
|
|
635
685
|
});
|
|
636
686
|
server.registerTool('obsrv_drive', {
|
|
637
687
|
title: 'Drive the visible Obsrv app',
|
|
638
|
-
description: `Drive the Obsrv desktop app the user is looking at: navigate it to a URL, apply a screen preset,
|
|
639
|
-
`profile, the target pane's 1:1/fit view or pixel-exact
|
|
688
|
+
description: `Drive the Obsrv desktop app the user is looking at: navigate it to a URL, apply a screen preset, rotate that ` +
|
|
689
|
+
`screen to landscape or portrait, apply a panel profile, the target pane's 1:1/fit view or pixel-exact ` +
|
|
690
|
+
`toggle — each exactly as clicking the toolbar would ` +
|
|
640
691
|
`— and steer the session like a guided demo: focus the window, step history (back/forward/reload), scroll ` +
|
|
641
692
|
`both panes, pan the target pane to a pixel, click the live page, and highlight a rect with a temporary ` +
|
|
642
693
|
`neutral marker, all while the user watches.\n\n` +
|
|
643
694
|
`Only the supplied inputs run (none = just read the current state), in this fixed order: focus → url → ` +
|
|
644
|
-
`preset → profile → viewMode → panes → pixelExact → reload → back → forward → scroll → panTo → click → highlight → ` +
|
|
695
|
+
`preset → orientation → profile → viewMode → panes → pixelExact → reload → back → forward → scroll → panTo → click → highlight → ` +
|
|
645
696
|
`capture. ` +
|
|
646
|
-
`The result is the final status: app version, the URL showing, and the selected preset/profile/view. A ` +
|
|
697
|
+
`The result is the final status: app version, the URL showing, and the selected preset/orientation/profile/view. A ` +
|
|
647
698
|
`click that navigates is reflected in that status — the call waits briefly (up to 2 s) for the commit. A ` +
|
|
648
699
|
`scroll adds \`scrolled\` (the offset actually reached) and \`scroller\` ('root' or 'element'): compare ` +
|
|
649
700
|
`\`scrolled\` with what you asked for rather than trusting the call's success, and use \`scroll.scrollSelector\` ` +
|
|
@@ -686,6 +737,12 @@ server.registerTool('obsrv_drive', {
|
|
|
686
737
|
}
|
|
687
738
|
if (input.preset !== undefined)
|
|
688
739
|
await (0, control_2.controlCall)(live.info, 'setPreset', { id: input.preset }, LIVE_APPLY_TIMEOUT_MS);
|
|
740
|
+
// After the preset, before everything else: rotation is applied on top of
|
|
741
|
+
// whichever screen is in force, so a call carrying both has to land in
|
|
742
|
+
// that order or the rotation would be spent on the outgoing preset.
|
|
743
|
+
if (input.orientation !== undefined) {
|
|
744
|
+
await (0, control_2.controlCall)(live.info, 'setOrientation', { orientation: input.orientation }, LIVE_APPLY_TIMEOUT_MS);
|
|
745
|
+
}
|
|
689
746
|
if (input.profile !== undefined)
|
|
690
747
|
await (0, control_2.controlCall)(live.info, 'setProfile', { id: input.profile }, LIVE_APPLY_TIMEOUT_MS);
|
|
691
748
|
if (input.viewMode !== undefined) {
|
|
@@ -777,7 +834,9 @@ server.registerTool('obsrv_presets', {
|
|
|
777
834
|
title: 'List screen presets and panel profiles',
|
|
778
835
|
description: `List every screen preset (id, label, group, CSS dims, deviceScaleFactor, panel diagonal, derived physical ` +
|
|
779
836
|
`ppi) and panel profile (id, label, simulation params) accepted by obsrv_snap and obsrv_diff. Read straight ` +
|
|
780
|
-
`from the app's preset table — nothing is rendered
|
|
837
|
+
`from the app's preset table — nothing is rendered. The dimensions are each preset's natural orientation ` +
|
|
838
|
+
`(portrait for the mobile ones, landscape for the monitors); every preset also rotates — see the ` +
|
|
839
|
+
`\`orientation\` note in the result.`,
|
|
781
840
|
inputSchema: {},
|
|
782
841
|
outputSchema: presetsOutputShape,
|
|
783
842
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
@@ -190,7 +190,14 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
190
190
|
starts at the window's left edge (the row's only border is at the bottom),
|
|
191
191
|
which is the frame `--native-right` is expressed in. */
|
|
192
192
|
.chrome-browse { position: relative; }
|
|
193
|
-
|
|
193
|
+
/* The screen controls sit centred: they describe the render, and the render is
|
|
194
|
+
what the two panes flank, so the middle is the shortest reach from either
|
|
195
|
+
side. The AGENT chip is taken out of flow rather than balanced against a
|
|
196
|
+
spacer — otherwise the group would shift sideways the moment agent control
|
|
197
|
+
was switched on, and a control that moves when an unrelated setting changes
|
|
198
|
+
is worse than one slightly off-centre. */
|
|
199
|
+
.chrome-screen { height: 38px; background: var(--chrome-0); justify-content: center; position: relative; }
|
|
200
|
+
.chrome-screen .agent-activity { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); }
|
|
194
201
|
.chrome-spacer { flex: 1 1 auto; }
|
|
195
202
|
|
|
196
203
|
/* The tab strip. Ground is the darkest chrome step so an inactive tab (one
|
|
@@ -239,18 +246,19 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
239
246
|
.tab > .tab-label[aria-selected='true'] { color: var(--text-0); font-weight: 600; }
|
|
240
247
|
|
|
241
248
|
/* The driven tab, while agent control is on. The 2px inset rule `.menu-row`
|
|
242
|
-
and `.surround-control` already use to mark a choice
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
.tab.driven
|
|
253
|
-
|
|
249
|
+
and `.surround-control` already use to mark a choice, on the leading edge so
|
|
250
|
+
it never competes with the selected fill underneath it.
|
|
251
|
+
|
|
252
|
+
It wears the warn hue for as long as an agent holds the tab, selected or
|
|
253
|
+
not, so a background tab being worked on is visible without switching to it.
|
|
254
|
+
Hue is tolerable here because the tab strip is the row furthest from the
|
|
255
|
+
panes — and nowhere nearer them. No blur: a glow beside a render would spill
|
|
256
|
+
light onto the pixels being measured. */
|
|
257
|
+
.tab.driven { box-shadow: inset 2px 0 0 var(--warn); background: color-mix(in srgb, var(--warn) 14%, var(--chrome-1)); }
|
|
258
|
+
.tab.driven:has(> .tab-label[aria-selected='true']) { background: color-mix(in srgb, var(--warn) 22%, var(--chrome-3)); }
|
|
259
|
+
.tab.driven > .tab-label { color: var(--text-0); }
|
|
260
|
+
/* An arriving command: the rule brightens to the full hue for ~3s. */
|
|
261
|
+
.tab.driven.busy { box-shadow: inset 3px 0 0 var(--warn); }
|
|
254
262
|
|
|
255
263
|
.tab-close {
|
|
256
264
|
flex: 0 0 auto;
|
|
@@ -600,13 +608,32 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
600
608
|
inset: 0 -2.5px;
|
|
601
609
|
}
|
|
602
610
|
.pane-divider:hover, .pane-divider:focus-visible { background: var(--text-1); }
|
|
611
|
+
/* Under the pointer the seam lights up its full height rather than growing a
|
|
612
|
+
handle. Every split-pane tool worth copying does it this way — VS Code,
|
|
613
|
+
Framer, Retool, Rive all leave the divider a hairline and let the cursor and
|
|
614
|
+
the widened hit area carry the affordance. Nothing floats over the pane
|
|
615
|
+
pixels this app exists to show. Painted as an overlay, so no layout moves
|
|
616
|
+
and the target pane is never re-rendered for a mouse-over. */
|
|
617
|
+
.pane-divider::after {
|
|
618
|
+
content: '';
|
|
619
|
+
position: absolute;
|
|
620
|
+
inset: 0 -1px;
|
|
621
|
+
background: var(--text-1);
|
|
622
|
+
opacity: 0;
|
|
623
|
+
}
|
|
624
|
+
.pane-divider:hover::after, .pane-divider:focus-visible::after { opacity: 1; }
|
|
603
625
|
/* The hairline is the affordance; a ring around a 1px element would be a box
|
|
604
626
|
floating on the seam. Same reasoning as `.target-canvas:focus`. */
|
|
605
627
|
.pane-divider:focus-visible { outline: none; }
|
|
606
628
|
/* Square corners, no shadow, no blur: a rounded or blurred pane edge would
|
|
607
629
|
destroy the very corner pixels the user opened this app to inspect. */
|
|
608
630
|
.pane-body { flex: 1 1 auto; min-height: 0; overflow: auto; }
|
|
609
|
-
.native
|
|
631
|
+
/* One pixel short of the seam. The native view is an OS-composited layer sized
|
|
632
|
+
to exactly this rect, so anything the renderer paints inside it is hidden
|
|
633
|
+
underneath — without the inset, the divider's hover band would appear on the
|
|
634
|
+
target side only, since that side is the renderer's own canvas. The view
|
|
635
|
+
still sits exactly over the slot; the slot is simply 1px narrower. */
|
|
636
|
+
.native-slot { width: calc(100% - 1px); height: 100%; }
|
|
610
637
|
/* The backing store is already at device resolution; this only guards against
|
|
611
638
|
a fractional CSS size introducing a resample. */
|
|
612
639
|
.target-canvas { display: block; image-rendering: pixelated; }
|
|
@@ -639,6 +666,41 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
639
666
|
}
|
|
640
667
|
.pane-footer .role { color: var(--text-0); letter-spacing: 0.08em; }
|
|
641
668
|
|
|
669
|
+
/* The rotate control. Same 30px border-box and the same `--chrome-3` pressed
|
|
670
|
+
fill as `.segmented`, so the screen row keeps one hit-target rhythm and one
|
|
671
|
+
way of marking a choice. Two outlines of the screen you get, rather than the
|
|
672
|
+
words: "Portrait"/"Landscape" would take 150px in a row that already holds
|
|
673
|
+
two selects and three groups, and the shape reads faster than either word.
|
|
674
|
+
The buttons are 28px like `.surround-control`'s, for the same reason — the
|
|
675
|
+
glyph is the affordance and the button is only the target.
|
|
676
|
+
|
|
677
|
+
No hue anywhere, pressed included: this control sits a few pixels from the
|
|
678
|
+
pane whose greys the user came to judge. */
|
|
679
|
+
.orientation-control {
|
|
680
|
+
box-sizing: border-box;
|
|
681
|
+
flex: 0 0 auto;
|
|
682
|
+
display: inline-flex;
|
|
683
|
+
height: 30px;
|
|
684
|
+
border: 1px solid var(--line);
|
|
685
|
+
border-radius: 5px;
|
|
686
|
+
overflow: hidden;
|
|
687
|
+
}
|
|
688
|
+
.orientation-control button {
|
|
689
|
+
width: 28px;
|
|
690
|
+
height: 100%;
|
|
691
|
+
display: inline-flex;
|
|
692
|
+
align-items: center;
|
|
693
|
+
justify-content: center;
|
|
694
|
+
border: 0;
|
|
695
|
+
border-right: 1px solid var(--line);
|
|
696
|
+
border-radius: 0;
|
|
697
|
+
background: var(--chrome-2);
|
|
698
|
+
color: var(--text-1);
|
|
699
|
+
cursor: pointer;
|
|
700
|
+
}
|
|
701
|
+
.orientation-control button:last-child { border-right: 0; }
|
|
702
|
+
.orientation-control button[aria-pressed='true'] { background: var(--chrome-3); color: var(--text-0); }
|
|
703
|
+
|
|
642
704
|
/* Segmented surround control: the same 30px border-box as `.segmented` and
|
|
643
705
|
`.select-shell`, so the screen row has one hit-target rhythm. `box-sizing`
|
|
644
706
|
for the same reason `.segmented` needs it — a `div` gets no border-box from
|
|
@@ -676,18 +738,23 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
676
738
|
.num { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|
677
739
|
|
|
678
740
|
/* Agent control opens a loopback server, so the chip persists while it is
|
|
679
|
-
enabled
|
|
741
|
+
enabled. Something else driving the app is exactly the "attention, never
|
|
742
|
+
decoration" case the palette reserves --warn for: the user must be able to
|
|
743
|
+
tell at a glance that the window is not only theirs. Standing state is the
|
|
744
|
+
warn hue outlined; an arriving command fills it. No blur — a glow beside the
|
|
745
|
+
panes would spill light onto the pixels being measured, which is the one
|
|
746
|
+
thing this chrome may never do. */
|
|
680
747
|
.agent-activity {
|
|
681
748
|
flex: 0 0 auto;
|
|
682
|
-
color: var(--
|
|
683
|
-
border: 1px solid var(--
|
|
749
|
+
color: var(--warn);
|
|
750
|
+
border: 1px solid var(--warn);
|
|
684
751
|
border-radius: 4px;
|
|
685
752
|
padding: 2px 7px;
|
|
686
753
|
font-size: 10px;
|
|
687
754
|
letter-spacing: 0.08em;
|
|
688
755
|
white-space: nowrap;
|
|
689
756
|
}
|
|
690
|
-
.agent-activity.active { color: var(--
|
|
757
|
+
.agent-activity.active { color: var(--chrome-0); background: var(--warn); border-color: var(--warn); }
|
|
691
758
|
|
|
692
759
|
/* Drawers sit beside the panes, never over them: the native WebContentsView is
|
|
693
760
|
an OS-level overlay and would cover anything painted on top of it. */
|
|
@@ -944,3 +1011,66 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
944
1011
|
}
|
|
945
1012
|
/* Nothing to erase: the button says so rather than pretending to act. */
|
|
946
1013
|
.clear-history:disabled { color: var(--text-1); cursor: default; }
|
|
1014
|
+
|
|
1015
|
+
/* --- empty state ---------------------------------------------------------- */
|
|
1016
|
+
/* Shown when a tab has no page. It covers the target canvas rather than
|
|
1017
|
+
replacing it: the canvas keeps its GL context and frame subscription, so the
|
|
1018
|
+
first navigation does not pay for a context restore, and the native slot
|
|
1019
|
+
keeps reporting its bounds. `--surround` is the pane field, so the state sits
|
|
1020
|
+
on the same ground the render will. */
|
|
1021
|
+
.empty-state {
|
|
1022
|
+
position: absolute;
|
|
1023
|
+
inset: 0;
|
|
1024
|
+
display: flex;
|
|
1025
|
+
flex-direction: column;
|
|
1026
|
+
align-items: center;
|
|
1027
|
+
justify-content: center;
|
|
1028
|
+
gap: 20px;
|
|
1029
|
+
padding: 24px;
|
|
1030
|
+
background: var(--surround);
|
|
1031
|
+
/* Above `.pane-divider`, which is itself raised to 1: the seam would
|
|
1032
|
+
otherwise draw its hairline straight through the illustration. Covering it
|
|
1033
|
+
must not disarm it, though — the split is still adjustable on a blank tab —
|
|
1034
|
+
so everything here is transparent to the pointer except the form. */
|
|
1035
|
+
z-index: 2;
|
|
1036
|
+
pointer-events: none;
|
|
1037
|
+
}
|
|
1038
|
+
.empty-form { pointer-events: auto; }
|
|
1039
|
+
.panes { position: relative; }
|
|
1040
|
+
.empty-state svg { width: 244px; max-width: 100%; height: auto; }
|
|
1041
|
+
.empty-lede {
|
|
1042
|
+
margin: 0;
|
|
1043
|
+
color: var(--text-1);
|
|
1044
|
+
text-align: center;
|
|
1045
|
+
max-width: 34ch;
|
|
1046
|
+
}
|
|
1047
|
+
.empty-form { display: flex; gap: 8px; width: 100%; max-width: 420px; }
|
|
1048
|
+
.empty-input {
|
|
1049
|
+
flex: 1 1 auto;
|
|
1050
|
+
min-width: 0;
|
|
1051
|
+
height: 30px;
|
|
1052
|
+
padding: 0 10px;
|
|
1053
|
+
color: var(--text-0);
|
|
1054
|
+
background: var(--field);
|
|
1055
|
+
border: 1px solid var(--line);
|
|
1056
|
+
border-radius: 4px;
|
|
1057
|
+
font: inherit;
|
|
1058
|
+
font-family: var(--mono);
|
|
1059
|
+
}
|
|
1060
|
+
.empty-input:focus-visible { outline: none; border-color: var(--text-1); }
|
|
1061
|
+
.empty-go {
|
|
1062
|
+
height: 30px;
|
|
1063
|
+
padding: 0 14px;
|
|
1064
|
+
color: var(--text-0);
|
|
1065
|
+
background: var(--chrome-2);
|
|
1066
|
+
border: 1px solid var(--line);
|
|
1067
|
+
border-radius: 4px;
|
|
1068
|
+
font: inherit;
|
|
1069
|
+
}
|
|
1070
|
+
.empty-go:hover:not(:disabled) { background: var(--chrome-3); }
|
|
1071
|
+
.empty-go:disabled { color: var(--text-1); opacity: 0.6; }
|
|
1072
|
+
.empty-error { margin: 0; color: var(--error); }
|
|
1073
|
+
/* The readouts stay legible through the empty state: they still name the screen
|
|
1074
|
+
being simulated, which is the one fact that survives having no page. Stacking
|
|
1075
|
+
rather than insetting the overlay, so nothing depends on the footer's height. */
|
|
1076
|
+
.pane-footer { position: relative; z-index: 3; }
|