getobsrv 0.5.0 → 0.7.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 +33 -2
- package/out/cli/args.js +5 -1
- package/out/main/cli.js +64 -9
- package/out/main/index.js +223 -13
- package/out/main/{targetSource-DkXWE0ha.js → targetSource-CbJwfugi.js} +22 -6
- package/out/mcp/server.js +202 -44
- package/out/preload/app.js +10 -2
- package/out/preload/sync.js +102 -5
- package/out/renderer/assets/{index-DyCD4ih_.js → index-BP1S2N6S.js} +81 -4
- package/out/renderer/assets/{index-CHF-G97L.css → index-BeroS4wv.css} +46 -0
- package/out/renderer/index.html +2 -2
- package/out/shared/ipcPayloads.js +61 -4
- package/out/shared/presets.js +7 -1
- package/out/shared/types.js +3 -0
- package/package.json +1 -1
- package/skills/obsrv-screens/SKILL.md +10 -0
|
@@ -12688,7 +12688,13 @@ const createImpl = (createState) => {
|
|
|
12688
12688
|
};
|
|
12689
12689
|
const create = ((createState) => createImpl);
|
|
12690
12690
|
const MAX_VIEWPORT = 4096;
|
|
12691
|
-
const DEFAULT_SETTINGS = {
|
|
12691
|
+
const DEFAULT_SETTINGS = {
|
|
12692
|
+
hostDiagonalInches: 27,
|
|
12693
|
+
hostNits: 500,
|
|
12694
|
+
agentControl: false,
|
|
12695
|
+
updateCheck: true,
|
|
12696
|
+
lastUpdateCheck: 0
|
|
12697
|
+
};
|
|
12692
12698
|
const SCREEN_PRESETS = [
|
|
12693
12699
|
// Laptops — ordered largest to smallest panel, then the denser 1080p outlier.
|
|
12694
12700
|
{ id: "laptop-768", label: '1366×768 15.6"', width: 1366, height: 768, diagonalInches: 15.6, deviceScaleFactor: 1, group: "laptop" },
|
|
@@ -12773,6 +12779,7 @@ const useStore = create()((set) => ({
|
|
|
12773
12779
|
targetLoading: false,
|
|
12774
12780
|
error: null,
|
|
12775
12781
|
toast: null,
|
|
12782
|
+
update: null,
|
|
12776
12783
|
image: null,
|
|
12777
12784
|
surround: "graphite",
|
|
12778
12785
|
viewMode: "1:1",
|
|
@@ -12798,6 +12805,7 @@ const useStore = create()((set) => ({
|
|
|
12798
12805
|
// Both panes report the same `loadError` for one failed navigation; the
|
|
12799
12806
|
// duplicate must not replace the object and re-render everything twice.
|
|
12800
12807
|
setError: (error) => set((s) => sameError(s.error, error) ? {} : { error }),
|
|
12808
|
+
setUpdate: (update) => set({ update }),
|
|
12801
12809
|
setToast: (toast) => set({ toast }),
|
|
12802
12810
|
setImage: (image) => set({ image }),
|
|
12803
12811
|
setSurround: (surround) => set({ surround }),
|
|
@@ -13228,6 +13236,18 @@ function PanelControls() {
|
|
|
13228
13236
|
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "muted", children: "Choosing a profile in the toolbar resets these." })
|
|
13229
13237
|
] });
|
|
13230
13238
|
}
|
|
13239
|
+
const MINUTE = 6e4;
|
|
13240
|
+
const HOUR = 60 * MINUTE;
|
|
13241
|
+
const DAY = 24 * HOUR;
|
|
13242
|
+
const plural = (n, unit) => `${n} ${unit}${n === 1 ? "" : "s"} ago`;
|
|
13243
|
+
function formatAge(checkedAt, now) {
|
|
13244
|
+
if (!Number.isFinite(checkedAt) || checkedAt <= 0) return "never";
|
|
13245
|
+
const age = now - checkedAt;
|
|
13246
|
+
if (age < MINUTE) return "just now";
|
|
13247
|
+
if (age < HOUR) return plural(Math.floor(age / MINUTE), "minute");
|
|
13248
|
+
if (age < DAY) return plural(Math.floor(age / HOUR), "hour");
|
|
13249
|
+
return plural(Math.floor(age / DAY), "day");
|
|
13250
|
+
}
|
|
13231
13251
|
function NumberField({ className, label, unit, value, min, step, onCommit, onInvalid }) {
|
|
13232
13252
|
const ref = reactExports.useRef(null);
|
|
13233
13253
|
const [draft, setDraft] = reactExports.useState(String(value));
|
|
@@ -13278,6 +13298,7 @@ function NumberField({ className, label, unit, value, min, step, onCommit, onInv
|
|
|
13278
13298
|
function SettingsPanel() {
|
|
13279
13299
|
const host = useStore(useShallow((s) => s.host));
|
|
13280
13300
|
const settings = useStore(useShallow((s) => s.settings));
|
|
13301
|
+
const update = useStore((s) => s.update);
|
|
13281
13302
|
const custom = useStore(useShallow((s) => s.custom));
|
|
13282
13303
|
const viewport = useStore(useShallow(selectViewport));
|
|
13283
13304
|
const scale = useStore(selectScale);
|
|
@@ -13396,7 +13417,45 @@ function SettingsPanel() {
|
|
|
13396
13417
|
viewport.height,
|
|
13397
13418
|
"."
|
|
13398
13419
|
] }),
|
|
13399
|
-
/* @__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" })
|
|
13420
|
+
/* @__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" }),
|
|
13421
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("h2", { children: "Updates" }),
|
|
13422
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "version-block", children: [
|
|
13423
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "version-row", children: [
|
|
13424
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Version" }),
|
|
13425
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "version-current num", children: update?.current ?? "—" })
|
|
13426
|
+
] }),
|
|
13427
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "version-row", children: [
|
|
13428
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Latest" }),
|
|
13429
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "version-latest", children: [
|
|
13430
|
+
update === null && "Not checked yet",
|
|
13431
|
+
update?.status === "current" && update.checkedAt === 0 && "Not checked yet",
|
|
13432
|
+
update?.status === "current" && update.checkedAt > 0 && "Up to date",
|
|
13433
|
+
update?.status === "error" && "Couldn’t check",
|
|
13434
|
+
update?.status === "available" && update.latest !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
|
|
13435
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "num", children: update.latest }),
|
|
13436
|
+
" · ",
|
|
13437
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "link", onClick: () => void window.obsrv.openRelease(), children: "Download" })
|
|
13438
|
+
] })
|
|
13439
|
+
] })
|
|
13440
|
+
] }),
|
|
13441
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "version-row", children: [
|
|
13442
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Last checked" }),
|
|
13443
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "version-checked num", children: update === null ? "never" : formatAge(update.checkedAt, Date.now()) })
|
|
13444
|
+
] })
|
|
13445
|
+
] }),
|
|
13446
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "control inline update-check-toggle", children: [
|
|
13447
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
13448
|
+
"input",
|
|
13449
|
+
{
|
|
13450
|
+
type: "checkbox",
|
|
13451
|
+
checked: settings.updateCheck,
|
|
13452
|
+
onChange: (e) => commit({ ...settings, updateCheck: e.target.checked })
|
|
13453
|
+
}
|
|
13454
|
+
),
|
|
13455
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Check for updates automatically" })
|
|
13456
|
+
] }),
|
|
13457
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "check-now", onClick: () => void window.obsrv.checkUpdate(), children: "Check now" }),
|
|
13458
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "muted", children: "One unauthenticated request to GitHub, at most once a day. No identifiers are sent." })
|
|
13400
13459
|
] });
|
|
13401
13460
|
}
|
|
13402
13461
|
const VERT_SRC = `#version 300 es
|
|
@@ -14237,6 +14296,7 @@ function Toolbar({ drawer, onTogglePanel, onToggleSettings }) {
|
|
|
14237
14296
|
const setPixelExact = useStore((s) => s.setPixelExact);
|
|
14238
14297
|
const setError = useStore((s) => s.setError);
|
|
14239
14298
|
const surround = useStore((s) => s.surround);
|
|
14299
|
+
const update = useStore((s) => s.update);
|
|
14240
14300
|
const setSurround = useStore((s) => s.setSurround);
|
|
14241
14301
|
const viewMode = useStore((s) => s.viewMode);
|
|
14242
14302
|
const setViewMode = useStore((s) => s.setViewMode);
|
|
@@ -14367,6 +14427,20 @@ function Toolbar({ drawer, onTogglePanel, onToggleSettings }) {
|
|
|
14367
14427
|
),
|
|
14368
14428
|
"Pixel-exact"
|
|
14369
14429
|
] }),
|
|
14430
|
+
update?.status === "available" && update.latest !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs(
|
|
14431
|
+
"button",
|
|
14432
|
+
{
|
|
14433
|
+
className: "update-button",
|
|
14434
|
+
type: "button",
|
|
14435
|
+
title: `Obsrv ${update.latest} is available — opens the download page`,
|
|
14436
|
+
onClick: () => void window.obsrv.openRelease(),
|
|
14437
|
+
children: [
|
|
14438
|
+
"v",
|
|
14439
|
+
update.latest,
|
|
14440
|
+
" ↓"
|
|
14441
|
+
]
|
|
14442
|
+
}
|
|
14443
|
+
),
|
|
14370
14444
|
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "surround-control", role: "group", "aria-label": "Pane surround", children: SURROUNDS.map((s) => /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
14371
14445
|
"button",
|
|
14372
14446
|
{
|
|
@@ -14431,6 +14505,7 @@ function App() {
|
|
|
14431
14505
|
const setUrl = useStore((s) => s.setUrl);
|
|
14432
14506
|
const setError = useStore((s) => s.setError);
|
|
14433
14507
|
const setTargetLoading = useStore((s) => s.setTargetLoading);
|
|
14508
|
+
const setUpdate = useStore((s) => s.setUpdate);
|
|
14434
14509
|
const setImageMeta = useStore((s) => s.setImage);
|
|
14435
14510
|
const setMode = useStore((s) => s.setMode);
|
|
14436
14511
|
const setToast = useStore((s) => s.setToast);
|
|
@@ -14444,6 +14519,7 @@ function App() {
|
|
|
14444
14519
|
reactExports.useEffect(() => {
|
|
14445
14520
|
window.obsrv.getHostInfo().then(setHost, (e) => console.warn("obsrv: getHostInfo failed", e));
|
|
14446
14521
|
window.obsrv.getSettings().then(setSettings, (e) => console.warn("obsrv: getSettings failed", e));
|
|
14522
|
+
window.obsrv.getUpdate().then(setUpdate, (e) => console.warn("obsrv: getUpdate failed", e));
|
|
14447
14523
|
const offs = [
|
|
14448
14524
|
window.obsrv.onHostChanged(setHost),
|
|
14449
14525
|
// A committed navigation — back, forward, reload, a link — supersedes
|
|
@@ -14454,12 +14530,13 @@ function App() {
|
|
|
14454
14530
|
setUrl(url);
|
|
14455
14531
|
}),
|
|
14456
14532
|
window.obsrv.onLoadError(setError),
|
|
14457
|
-
window.obsrv.onTargetLoading(setTargetLoading)
|
|
14533
|
+
window.obsrv.onTargetLoading(setTargetLoading),
|
|
14534
|
+
window.obsrv.onUpdateStatus(setUpdate)
|
|
14458
14535
|
];
|
|
14459
14536
|
return () => {
|
|
14460
14537
|
for (const off of offs) off();
|
|
14461
14538
|
};
|
|
14462
|
-
}, [setHost, setSettings, setUrl, setError, setTargetLoading]);
|
|
14539
|
+
}, [setHost, setSettings, setUrl, setError, setTargetLoading, setUpdate]);
|
|
14463
14540
|
reactExports.useEffect(() => {
|
|
14464
14541
|
void window.obsrv.setViewport(viewport.width, viewport.height, deviceScaleFactor);
|
|
14465
14542
|
}, [viewport.width, viewport.height, deviceScaleFactor]);
|
|
@@ -477,3 +477,49 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
477
477
|
}
|
|
478
478
|
.browser-notice p { margin: 0.75rem 0 0; color: var(--text-1); }
|
|
479
479
|
.browser-notice code { font-family: var(--mono); }
|
|
480
|
+
|
|
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
|
+
.version-block { margin: 8px 0; }
|
|
494
|
+
.version-row {
|
|
495
|
+
display: flex;
|
|
496
|
+
justify-content: space-between;
|
|
497
|
+
gap: 10px;
|
|
498
|
+
padding: 3px 0;
|
|
499
|
+
color: var(--text-1);
|
|
500
|
+
}
|
|
501
|
+
.version-row .version-current,
|
|
502
|
+
.version-row .version-latest,
|
|
503
|
+
.version-row .version-checked {
|
|
504
|
+
color: var(--text-0);
|
|
505
|
+
text-align: right;
|
|
506
|
+
}
|
|
507
|
+
.link {
|
|
508
|
+
background: none;
|
|
509
|
+
border: 0;
|
|
510
|
+
padding: 0;
|
|
511
|
+
color: var(--text-0);
|
|
512
|
+
text-decoration: underline;
|
|
513
|
+
cursor: pointer;
|
|
514
|
+
font: inherit;
|
|
515
|
+
}
|
|
516
|
+
.check-now {
|
|
517
|
+
width: 100%;
|
|
518
|
+
height: 24px;
|
|
519
|
+
margin-top: 8px;
|
|
520
|
+
background: var(--chrome-2);
|
|
521
|
+
color: var(--text-0);
|
|
522
|
+
border: 1px solid var(--line);
|
|
523
|
+
border-radius: 4px;
|
|
524
|
+
cursor: pointer;
|
|
525
|
+
}
|
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-BP1S2N6S.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BeroS4wv.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
|
@@ -8,6 +8,9 @@ exports.parseSettings = parseSettings;
|
|
|
8
8
|
exports.parseMode = parseMode;
|
|
9
9
|
exports.parseUiState = parseUiState;
|
|
10
10
|
exports.parseScrollPos = parseScrollPos;
|
|
11
|
+
exports.parseScrollRequest = parseScrollRequest;
|
|
12
|
+
exports.parseScrollReport = parseScrollReport;
|
|
13
|
+
const types_1 = require("./types");
|
|
11
14
|
/**
|
|
12
15
|
* Parsers for everything the renderer sends main over IPC. Each returns a
|
|
13
16
|
* fresh, fully-typed value or `null`; nothing from the wire is passed through
|
|
@@ -17,6 +20,8 @@ exports.parseScrollPos = parseScrollPos;
|
|
|
17
20
|
*/
|
|
18
21
|
/** Largest coordinate or size a pane rect may carry; far beyond any real window. */
|
|
19
22
|
exports.MAX_RECT = 16384;
|
|
23
|
+
/** Most warnings a pane's scroll reply may carry; the preload sends at most one. */
|
|
24
|
+
const MAX_SCROLL_WARNINGS = 4;
|
|
20
25
|
const isFiniteNumber = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
21
26
|
const isRecord = (v) => typeof v === 'object' && v !== null;
|
|
22
27
|
function parseRect(raw) {
|
|
@@ -94,9 +99,10 @@ function parseDeviceScaleFactor(raw) {
|
|
|
94
99
|
return raw;
|
|
95
100
|
}
|
|
96
101
|
/**
|
|
97
|
-
* Copies exactly the
|
|
98
|
-
* positive. A missing `agentControl` means false
|
|
99
|
-
*
|
|
102
|
+
* Copies exactly the five known keys; the numbers must be finite and
|
|
103
|
+
* positive. A missing `agentControl` means false and a missing `updateCheck`
|
|
104
|
+
* means true (the pre-feature wire shapes); any non-boolean value is refused,
|
|
105
|
+
* never coerced.
|
|
100
106
|
*/
|
|
101
107
|
function parseSettings(raw) {
|
|
102
108
|
if (!isRecord(raw))
|
|
@@ -109,7 +115,13 @@ function parseSettings(raw) {
|
|
|
109
115
|
const agentControl = raw.agentControl ?? false;
|
|
110
116
|
if (typeof agentControl !== 'boolean')
|
|
111
117
|
return null;
|
|
112
|
-
|
|
118
|
+
const updateCheck = raw.updateCheck ?? true;
|
|
119
|
+
if (typeof updateCheck !== 'boolean')
|
|
120
|
+
return null;
|
|
121
|
+
const lastUpdateCheck = raw.lastUpdateCheck ?? 0;
|
|
122
|
+
if (!isFiniteNumber(lastUpdateCheck) || lastUpdateCheck < 0)
|
|
123
|
+
return null;
|
|
124
|
+
return { hostDiagonalInches, hostNits, agentControl, updateCheck, lastUpdateCheck };
|
|
113
125
|
}
|
|
114
126
|
function parseMode(raw) {
|
|
115
127
|
return raw === 'url' || raw === 'image' ? raw : null;
|
|
@@ -156,3 +168,48 @@ function parseScrollPos(raw) {
|
|
|
156
168
|
return null;
|
|
157
169
|
return { x, y };
|
|
158
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* A `scroll` command payload: an offset plus the optional `scrollSelector`
|
|
173
|
+
* escape hatch. The selector is only ever handed to `document.querySelector`
|
|
174
|
+
* in the preload's isolated world — never evaluated — but it is still bounded
|
|
175
|
+
* and type-checked here so a malformed one is refused with an explanation
|
|
176
|
+
* rather than silently ignored by the page. Returns the parsed request, or the
|
|
177
|
+
* error message.
|
|
178
|
+
*/
|
|
179
|
+
function parseScrollRequest(raw) {
|
|
180
|
+
const pos = parseScrollPos(raw);
|
|
181
|
+
if (!pos)
|
|
182
|
+
return 'scroll payload must be { x, y } with finite, non-negative CSS-pixel offsets';
|
|
183
|
+
const selector = raw.scrollSelector;
|
|
184
|
+
if (selector === undefined || selector === null)
|
|
185
|
+
return pos;
|
|
186
|
+
if (typeof selector !== 'string')
|
|
187
|
+
return 'scrollSelector must be a CSS selector string';
|
|
188
|
+
const trimmed = selector.trim();
|
|
189
|
+
if (trimmed === '')
|
|
190
|
+
return 'scrollSelector must not be empty';
|
|
191
|
+
if (trimmed.length > types_1.MAX_SCROLL_SELECTOR)
|
|
192
|
+
return `scrollSelector must be at most ${types_1.MAX_SCROLL_SELECTOR} characters`;
|
|
193
|
+
return { ...pos, selector: trimmed };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* A pane's `IPC.scrollResult` reply. Sent by the sync preload, which runs
|
|
197
|
+
* beside a third-party page, so it is parsed exactly like any renderer
|
|
198
|
+
* message; anything malformed is dropped and the caller times out rather than
|
|
199
|
+
* reporting an offset it cannot trust.
|
|
200
|
+
*/
|
|
201
|
+
function parseScrollReport(raw) {
|
|
202
|
+
if (!isRecord(raw))
|
|
203
|
+
return null;
|
|
204
|
+
const { id, x, y, scroller } = raw;
|
|
205
|
+
if (!isFiniteNumber(id))
|
|
206
|
+
return null;
|
|
207
|
+
if (!isFiniteNumber(x) || !isFiniteNumber(y))
|
|
208
|
+
return null;
|
|
209
|
+
if (scroller !== 'root' && scroller !== 'element')
|
|
210
|
+
return null;
|
|
211
|
+
const warnings = Array.isArray(raw.warnings)
|
|
212
|
+
? raw.warnings.filter((w) => typeof w === 'string').slice(0, MAX_SCROLL_WARNINGS)
|
|
213
|
+
: [];
|
|
214
|
+
return { id, x, y, scroller, warnings };
|
|
215
|
+
}
|
package/out/shared/presets.js
CHANGED
|
@@ -4,7 +4,13 @@ exports.PANEL_PROFILES = exports.SCREEN_PRESETS = exports.DEFAULT_SETTINGS = exp
|
|
|
4
4
|
exports.findPreset = findPreset;
|
|
5
5
|
exports.findProfile = findProfile;
|
|
6
6
|
exports.MAX_VIEWPORT = 4096;
|
|
7
|
-
exports.DEFAULT_SETTINGS = {
|
|
7
|
+
exports.DEFAULT_SETTINGS = {
|
|
8
|
+
hostDiagonalInches: 27,
|
|
9
|
+
hostNits: 500,
|
|
10
|
+
agentControl: false,
|
|
11
|
+
updateCheck: true,
|
|
12
|
+
lastUpdateCheck: 0,
|
|
13
|
+
};
|
|
8
14
|
exports.SCREEN_PRESETS = [
|
|
9
15
|
// Laptops — ordered largest to smallest panel, then the denser 1080p outlier.
|
|
10
16
|
{ id: 'laptop-768', label: '1366×768 15.6"', width: 1366, height: 768, diagonalInches: 15.6, deviceScaleFactor: 1, group: 'laptop' },
|
package/out/shared/types.js
CHANGED
package/package.json
CHANGED
|
@@ -54,6 +54,13 @@ comes back inline. If the Obsrv desktop app is open with "Agent control" on
|
|
|
54
54
|
click, pan and highlight to walk the user through what it found; no app
|
|
55
55
|
means the usual headless render.
|
|
56
56
|
|
|
57
|
+
To see anything below the fold, scroll and capture in the **same**
|
|
58
|
+
`obsrv_drive` call — `{ scroll: { x: 0, y: 1500 }, capture: 'pane' }`. That
|
|
59
|
+
tool never navigates unless you pass `url`, so the scroll is still in place
|
|
60
|
+
when the PNG is taken. Reaching for `obsrv_snap` after a scroll works only
|
|
61
|
+
when the app is already on that exact URL (it answers `navigated: false`);
|
|
62
|
+
snapping a different URL is a fresh load and lands back at the top.
|
|
63
|
+
|
|
57
64
|
## The loop that catches real regressions
|
|
58
65
|
|
|
59
66
|
1. Snap the dev URL across `--matrix laptop-768,android-65,1080p-24`, plus a
|
|
@@ -80,6 +87,9 @@ happened on the matrix snaps.
|
|
|
80
87
|
not colorimetry of one specific panel.
|
|
81
88
|
- `diff` is 1x-only in v1: dsf>1 presets and CSS viewports over 2048px exit
|
|
82
89
|
with an error. Its findings are informational — apply your own thresholds.
|
|
90
|
+
- `diff` on an animating page compares two different frames. Check `settled`
|
|
91
|
+
in its output: when false the band deltas are frame-to-frame noise and
|
|
92
|
+
`findings` says so rather than interpreting them. Snap that page instead.
|
|
83
93
|
- `diff` cannot say "the hairline vanished": a 0.5px hairline renders one
|
|
84
94
|
device row at 1x *and* 2x. It reports ink deltas and row ratios; vanishing
|
|
85
95
|
is judged by reading the PNG.
|